ortools.sat.python.cp_model
Methods for building and solving CP-SAT models.
The following two sections describe the main methods for building and solving CP-SAT models.
- .CpModel">
CpModel
: Methods for creating models, including variables and constraints. - .CpSolver">
CPSolver
: Methods for solving a model and evaluating solutions.
The following methods implement callbacks that the solver calls each time it finds a new solution.
- .CpSolverSolutionCallback">
CpSolverSolutionCallback
: A general method for implementing callbacks. - .ObjectiveSolutionPrinter">
ObjectiveSolutionPrinter
: Print objective values and elapsed time for intermediate solutions. - .VarArraySolutionPrinter">
VarArraySolutionPrinter
: Print intermediate solutions (variable values, time). - [
VarArrayAndObjectiveSolutionPrinter
] (#cp_model.VarArrayAndObjectiveSolutionPrinter): Print both intermediate solutions and objective values.
Additional methods for solving CP-SAT models:
- .Constraint">
Constraint
: A few utility methods for modifying constraints created byCpModel
. - .LinearExpr">
LinearExpr
: Methods for creating constraints and the objective from large arrays of coefficients.
Other methods and functions listed are primarily used for developing OR-Tools, rather than for solving specific optimization problems.
1# Copyright 2010-2025 Google LLC 2# Licensed under the Apache License, Version 2.0 (the "License"); 3# you may not use this file except in compliance with the License. 4# You may obtain a copy of the License at 5# 6# http://www.apache.org/licenses/LICENSE-2.0 7# 8# Unless required by applicable law or agreed to in writing, software 9# distributed under the License is distributed on an "AS IS" BASIS, 10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11# See the License for the specific language governing permissions and 12# limitations under the License. 13 14"""Methods for building and solving 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 copy 49import threading 50import time 51from typing import ( 52 Any, 53 Callable, 54 Dict, 55 Iterable, 56 Optional, 57 Sequence, 58 Tuple, 59 Union, 60 cast, 61 overload, 62) 63import warnings 64 65import numpy as np 66import pandas as pd 67 68from ortools.sat import cp_model_pb2 69from ortools.sat import sat_parameters_pb2 70from ortools.sat.python import cp_model_helper as cmh 71from ortools.sat.python import cp_model_numbers as cmn 72from ortools.util.python import sorted_interval_list 73 74# Import external types. 75Domain = sorted_interval_list.Domain 76BoundedLinearExpression = cmh.BoundedLinearExpression 77FlatFloatExpr = cmh.FlatFloatExpr 78FlatIntExpr = cmh.FlatIntExpr 79LinearExpr = cmh.LinearExpr 80NotBooleanVariable = cmh.NotBooleanVariable 81 82 83# The classes below allow linear expressions to be expressed naturally with the 84# usual arithmetic operators + - * / and with constant numbers, which makes the 85# python API very intuitive. See../ samples/*.py for examples. 86 87INT_MIN = -(2**63) # hardcoded to be platform independent. 88INT_MAX = 2**63 - 1 89INT32_MIN = -(2**31) 90INT32_MAX = 2**31 - 1 91 92# CpSolver status (exported to avoid importing cp_model_cp2). 93UNKNOWN = cp_model_pb2.UNKNOWN 94MODEL_INVALID = cp_model_pb2.MODEL_INVALID 95FEASIBLE = cp_model_pb2.FEASIBLE 96INFEASIBLE = cp_model_pb2.INFEASIBLE 97OPTIMAL = cp_model_pb2.OPTIMAL 98 99# Variable selection strategy 100CHOOSE_FIRST = cp_model_pb2.DecisionStrategyProto.CHOOSE_FIRST 101CHOOSE_LOWEST_MIN = cp_model_pb2.DecisionStrategyProto.CHOOSE_LOWEST_MIN 102CHOOSE_HIGHEST_MAX = cp_model_pb2.DecisionStrategyProto.CHOOSE_HIGHEST_MAX 103CHOOSE_MIN_DOMAIN_SIZE = cp_model_pb2.DecisionStrategyProto.CHOOSE_MIN_DOMAIN_SIZE 104CHOOSE_MAX_DOMAIN_SIZE = cp_model_pb2.DecisionStrategyProto.CHOOSE_MAX_DOMAIN_SIZE 105 106# Domain reduction strategy 107SELECT_MIN_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MIN_VALUE 108SELECT_MAX_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MAX_VALUE 109SELECT_LOWER_HALF = cp_model_pb2.DecisionStrategyProto.SELECT_LOWER_HALF 110SELECT_UPPER_HALF = cp_model_pb2.DecisionStrategyProto.SELECT_UPPER_HALF 111SELECT_MEDIAN_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MEDIAN_VALUE 112SELECT_RANDOM_HALF = cp_model_pb2.DecisionStrategyProto.SELECT_RANDOM_HALF 113 114# Search branching 115AUTOMATIC_SEARCH = sat_parameters_pb2.SatParameters.AUTOMATIC_SEARCH 116FIXED_SEARCH = sat_parameters_pb2.SatParameters.FIXED_SEARCH 117PORTFOLIO_SEARCH = sat_parameters_pb2.SatParameters.PORTFOLIO_SEARCH 118LP_SEARCH = sat_parameters_pb2.SatParameters.LP_SEARCH 119PSEUDO_COST_SEARCH = sat_parameters_pb2.SatParameters.PSEUDO_COST_SEARCH 120PORTFOLIO_WITH_QUICK_RESTART_SEARCH = ( 121 sat_parameters_pb2.SatParameters.PORTFOLIO_WITH_QUICK_RESTART_SEARCH 122) 123HINT_SEARCH = sat_parameters_pb2.SatParameters.HINT_SEARCH 124PARTIAL_FIXED_SEARCH = sat_parameters_pb2.SatParameters.PARTIAL_FIXED_SEARCH 125RANDOMIZED_SEARCH = sat_parameters_pb2.SatParameters.RANDOMIZED_SEARCH 126 127# Type aliases 128IntegralT = Union[int, np.int8, np.uint8, np.int32, np.uint32, np.int64, np.uint64] 129IntegralTypes = ( 130 int, 131 np.int8, 132 np.uint8, 133 np.int32, 134 np.uint32, 135 np.int64, 136 np.uint64, 137) 138NumberT = Union[ 139 int, 140 float, 141 np.int8, 142 np.uint8, 143 np.int32, 144 np.uint32, 145 np.int64, 146 np.uint64, 147 np.double, 148] 149NumberTypes = ( 150 int, 151 float, 152 np.int8, 153 np.uint8, 154 np.int32, 155 np.uint32, 156 np.int64, 157 np.uint64, 158 np.double, 159) 160 161LiteralT = Union[cmh.Literal, IntegralT, bool] 162BoolVarT = cmh.Literal 163VariableT = Union["IntVar", IntegralT] 164 165# We need to add 'IntVar' for pytype. 166LinearExprT = Union[LinearExpr, "IntVar", IntegralT] 167ObjLinearExprT = Union[LinearExpr, NumberT] 168 169ArcT = Tuple[IntegralT, IntegralT, LiteralT] 170_IndexOrSeries = Union[pd.Index, pd.Series] 171 172 173def display_bounds(bounds: Sequence[int]) -> str: 174 """Displays a flattened list of intervals.""" 175 out = "" 176 for i in range(0, len(bounds), 2): 177 if i != 0: 178 out += ", " 179 if bounds[i] == bounds[i + 1]: 180 out += str(bounds[i]) 181 else: 182 out += str(bounds[i]) + ".." + str(bounds[i + 1]) 183 return out 184 185 186def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str: 187 """Returns a short name of an integer variable, or its negation.""" 188 if i < 0: 189 return f"not({short_name(model, -i - 1)})" 190 v = model.variables[i] 191 if v.name: 192 return v.name 193 elif len(v.domain) == 2 and v.domain[0] == v.domain[1]: 194 return str(v.domain[0]) 195 else: 196 return f"[{display_bounds(v.domain)}]" 197 198 199def short_expr_name( 200 model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto 201) -> str: 202 """Pretty-print LinearExpressionProto instances.""" 203 if not e.vars: 204 return str(e.offset) 205 if len(e.vars) == 1: 206 var_name = short_name(model, e.vars[0]) 207 coeff = e.coeffs[0] 208 result = "" 209 if coeff == 1: 210 result = var_name 211 elif coeff == -1: 212 result = f"-{var_name}" 213 elif coeff != 0: 214 result = f"{coeff} * {var_name}" 215 if e.offset > 0: 216 result = f"{result} + {e.offset}" 217 elif e.offset < 0: 218 result = f"{result} - {-e.offset}" 219 return result 220 # TODO(user): Support more than affine expressions. 221 return str(e) 222 223 224class IntVar(cmh.BaseIntVar): 225 """An integer variable. 226 227 An IntVar is an object that can take on any integer value within defined 228 ranges. Variables appear in constraint like: 229 230 x + y >= 5 231 AllDifferent([x, y, z]) 232 233 Solving a model is equivalent to finding, for each variable, a single value 234 from the set of initial values (called the initial domain), such that the 235 model is feasible, or optimal if you provided an objective function. 236 """ 237 238 def __init__( 239 self, 240 model: cp_model_pb2.CpModelProto, 241 domain: Union[int, sorted_interval_list.Domain], 242 is_boolean: bool, 243 name: Optional[str], 244 ) -> None: 245 """See CpModel.new_int_var below.""" 246 self.__model: cp_model_pb2.CpModelProto = model 247 # Python do not support multiple __init__ methods. 248 # This method is only called from the CpModel class. 249 # We hack the parameter to support the two cases: 250 # case 1: 251 # model is a CpModelProto, domain is a Domain, and name is a string. 252 # case 2: 253 # model is a CpModelProto, domain is an index (int), and name is None. 254 if isinstance(domain, IntegralTypes) and name is None: 255 cmh.BaseIntVar.__init__(self, int(domain), is_boolean) 256 else: 257 cmh.BaseIntVar.__init__(self, len(model.variables), is_boolean) 258 proto: cp_model_pb2.IntegerVariableProto = self.__model.variables.add() 259 proto.domain.extend( 260 cast(sorted_interval_list.Domain, domain).flattened_intervals() 261 ) 262 if name is not None: 263 proto.name = name 264 265 def __copy__(self) -> "IntVar": 266 """Returns a shallowcopy of the variable.""" 267 return IntVar(self.__model, self.index, self.is_boolean, None) 268 269 def __deepcopy__(self, memo: Any) -> "IntVar": 270 """Returns a deepcopy of the variable.""" 271 return IntVar( 272 copy.deepcopy(self.__model, memo), self.index, self.is_boolean, None 273 ) 274 275 @property 276 def proto(self) -> cp_model_pb2.IntegerVariableProto: 277 """Returns the variable protobuf.""" 278 return self.__model.variables[self.index] 279 280 @property 281 def model_proto(self) -> cp_model_pb2.CpModelProto: 282 """Returns the model protobuf.""" 283 return self.__model 284 285 def is_equal_to(self, other: Any) -> bool: 286 """Returns true if self == other in the python sense.""" 287 if not isinstance(other, IntVar): 288 return False 289 return self.index == other.index 290 291 def __str__(self) -> str: 292 if not self.proto.name: 293 if ( 294 len(self.proto.domain) == 2 295 and self.proto.domain[0] == self.proto.domain[1] 296 ): 297 # Special case for constants. 298 return str(self.proto.domain[0]) 299 elif self.is_boolean: 300 return f"BooleanVar({self.__index})" 301 else: 302 return f"IntVar({self.__index})" 303 else: 304 return self.proto.name 305 306 def __repr__(self) -> str: 307 return f"{self}({display_bounds(self.proto.domain)})" 308 309 @property 310 def name(self) -> str: 311 if not self.proto or not self.proto.name: 312 return "" 313 return self.proto.name 314 315 # Pre PEP8 compatibility. 316 # pylint: disable=invalid-name 317 def Name(self) -> str: 318 return self.name 319 320 def Proto(self) -> cp_model_pb2.IntegerVariableProto: 321 return self.proto 322 323 # pylint: enable=invalid-name 324 325 326class Constraint: 327 """Base class for constraints. 328 329 Constraints are built by the CpModel through the add<XXX> methods. 330 Once created by the CpModel class, they are automatically added to the model. 331 The purpose of this class is to allow specification of enforcement literals 332 for this constraint. 333 334 b = model.new_bool_var('b') 335 x = model.new_int_var(0, 10, 'x') 336 y = model.new_int_var(0, 10, 'y') 337 338 model.add(x + 2 * y == 5).only_enforce_if(b.negated()) 339 """ 340 341 def __init__( 342 self, 343 cp_model: "CpModel", 344 ) -> None: 345 self.__index: int = len(cp_model.proto.constraints) 346 self.__cp_model: "CpModel" = cp_model 347 self.__constraint: cp_model_pb2.ConstraintProto = ( 348 cp_model.proto.constraints.add() 349 ) 350 351 @overload 352 def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": ... 353 354 @overload 355 def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": ... 356 357 def only_enforce_if(self, *boolvar) -> "Constraint": 358 """Adds an enforcement literal to the constraint. 359 360 This method adds one or more literals (that is, a boolean variable or its 361 negation) as enforcement literals. The conjunction of all these literals 362 determines whether the constraint is active or not. It acts as an 363 implication, so if the conjunction is true, it implies that the constraint 364 must be enforced. If it is false, then the constraint is ignored. 365 366 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 367 368 Args: 369 *boolvar: One or more Boolean literals. 370 371 Returns: 372 self. 373 """ 374 for lit in expand_generator_or_tuple(boolvar): 375 if (cmn.is_boolean(lit) and lit) or ( 376 isinstance(lit, IntegralTypes) and lit == 1 377 ): 378 # Always true. Do nothing. 379 pass 380 elif (cmn.is_boolean(lit) and not lit) or ( 381 isinstance(lit, IntegralTypes) and lit == 0 382 ): 383 self.__constraint.enforcement_literal.append( 384 self.__cp_model.new_constant(0).index 385 ) 386 else: 387 self.__constraint.enforcement_literal.append( 388 cast(cmh.Literal, lit).index 389 ) 390 return self 391 392 def with_name(self, name: str) -> "Constraint": 393 """Sets the name of the constraint.""" 394 if name: 395 self.__constraint.name = name 396 else: 397 self.__constraint.ClearField("name") 398 return self 399 400 @property 401 def name(self) -> str: 402 """Returns the name of the constraint.""" 403 if not self.__constraint or not self.__constraint.name: 404 return "" 405 return self.__constraint.name 406 407 @property 408 def index(self) -> int: 409 """Returns the index of the constraint in the model.""" 410 return self.__index 411 412 @property 413 def proto(self) -> cp_model_pb2.ConstraintProto: 414 """Returns the constraint protobuf.""" 415 return self.__constraint 416 417 # Pre PEP8 compatibility. 418 # pylint: disable=invalid-name 419 OnlyEnforceIf = only_enforce_if 420 WithName = with_name 421 422 def Name(self) -> str: 423 return self.name 424 425 def Index(self) -> int: 426 return self.index 427 428 def Proto(self) -> cp_model_pb2.ConstraintProto: 429 return self.proto 430 431 # pylint: enable=invalid-name 432 433 434class VariableList: 435 """Stores all integer variables of the model.""" 436 437 def __init__(self) -> None: 438 self.__var_list: list[IntVar] = [] 439 440 def append(self, var: IntVar) -> None: 441 assert var.index == len(self.__var_list) 442 self.__var_list.append(var) 443 444 def get(self, index: int) -> IntVar: 445 if index < 0 or index >= len(self.__var_list): 446 raise ValueError("Index out of bounds.") 447 return self.__var_list[index] 448 449 def rebuild_expr( 450 self, 451 proto: cp_model_pb2.LinearExpressionProto, 452 ) -> LinearExprT: 453 """Recreate a LinearExpr from a LinearExpressionProto.""" 454 num_elements = len(proto.vars) 455 if num_elements == 0: 456 return proto.offset 457 elif num_elements == 1: 458 var = self.get(proto.vars[0]) 459 return LinearExpr.affine( 460 var, proto.coeffs[0], proto.offset 461 ) # pytype: disable=bad-return-type 462 else: 463 variables = [] 464 for var_index in range(len(proto.vars)): 465 var = self.get(var_index) 466 variables.append(var) 467 if proto.offset != 0: 468 coeffs = [] 469 coeffs.extend(proto.coeffs) 470 coeffs.append(1) 471 variables.append(proto.offset) 472 return LinearExpr.weighted_sum(variables, coeffs) 473 else: 474 return LinearExpr.weighted_sum(variables, proto.coeffs) 475 476 477class IntervalVar: 478 """Represents an Interval variable. 479 480 An interval variable is both a constraint and a variable. It is defined by 481 three integer variables: start, size, and end. 482 483 It is a constraint because, internally, it enforces that start + size == end. 484 485 It is also a variable as it can appear in specific scheduling constraints: 486 NoOverlap, NoOverlap2D, Cumulative. 487 488 Optionally, an enforcement literal can be added to this constraint, in which 489 case these scheduling constraints will ignore interval variables with 490 enforcement literals assigned to false. Conversely, these constraints will 491 also set these enforcement literals to false if they cannot fit these 492 intervals into the schedule. 493 494 Raises: 495 ValueError: if start, size, end are not defined, or have the wrong type. 496 """ 497 498 def __init__( 499 self, 500 model: cp_model_pb2.CpModelProto, 501 var_list: VariableList, 502 start: Union[cp_model_pb2.LinearExpressionProto, int], 503 size: Optional[cp_model_pb2.LinearExpressionProto], 504 end: Optional[cp_model_pb2.LinearExpressionProto], 505 is_present_index: Optional[int], 506 name: Optional[str], 507 ) -> None: 508 self.__model: cp_model_pb2.CpModelProto = model 509 self.__var_list: VariableList = var_list 510 self.__index: int 511 self.__ct: cp_model_pb2.ConstraintProto 512 # As with the IntVar::__init__ method, we hack the __init__ method to 513 # support two use cases: 514 # case 1: called when creating a new interval variable. 515 # {start|size|end} are linear expressions, is_present_index is either 516 # None or the index of a Boolean literal. name is a string 517 # case 2: called when querying an existing interval variable. 518 # start_index is an int, all parameters after are None. 519 if isinstance(start, int): 520 if size is not None: 521 raise ValueError("size should be None") 522 if end is not None: 523 raise ValueError("end should be None") 524 if is_present_index is not None: 525 raise ValueError("is_present_index should be None") 526 self.__index = cast(int, start) 527 self.__ct = model.constraints[self.__index] 528 else: 529 self.__index = len(model.constraints) 530 self.__ct = self.__model.constraints.add() 531 if start is None: 532 raise TypeError("start is not defined") 533 self.__ct.interval.start.CopyFrom(start) 534 if size is None: 535 raise TypeError("size is not defined") 536 self.__ct.interval.size.CopyFrom(size) 537 if end is None: 538 raise TypeError("end is not defined") 539 self.__ct.interval.end.CopyFrom(end) 540 if is_present_index is not None: 541 self.__ct.enforcement_literal.append(is_present_index) 542 if name: 543 self.__ct.name = name 544 545 @property 546 def index(self) -> int: 547 """Returns the index of the interval constraint in the model.""" 548 return self.__index 549 550 @property 551 def proto(self) -> cp_model_pb2.ConstraintProto: 552 """Returns the interval protobuf.""" 553 return self.__model.constraints[self.__index] 554 555 @property 556 def model_proto(self) -> cp_model_pb2.CpModelProto: 557 """Returns the model protobuf.""" 558 return self.__model 559 560 def __str__(self): 561 return self.proto.name 562 563 def __repr__(self): 564 interval = self.proto.interval 565 if self.proto.enforcement_literal: 566 return ( 567 f"{self.proto.name}(start =" 568 f" {short_expr_name(self.__model, interval.start)}, size =" 569 f" {short_expr_name(self.__model, interval.size)}, end =" 570 f" {short_expr_name(self.__model, interval.end)}, is_present =" 571 f" {short_name(self.__model, self.proto.enforcement_literal[0])})" 572 ) 573 else: 574 return ( 575 f"{self.proto.name}(start =" 576 f" {short_expr_name(self.__model, interval.start)}, size =" 577 f" {short_expr_name(self.__model, interval.size)}, end =" 578 f" {short_expr_name(self.__model, interval.end)})" 579 ) 580 581 @property 582 def name(self) -> str: 583 if not self.proto or not self.proto.name: 584 return "" 585 return self.proto.name 586 587 def start_expr(self) -> LinearExprT: 588 return self.__var_list.rebuild_expr(self.proto.interval.start) 589 590 def size_expr(self) -> LinearExprT: 591 return self.__var_list.rebuild_expr(self.proto.interval.size) 592 593 def end_expr(self) -> LinearExprT: 594 return self.__var_list.rebuild_expr(self.proto.interval.end) 595 596 # Pre PEP8 compatibility. 597 # pylint: disable=invalid-name 598 def Name(self) -> str: 599 return self.name 600 601 def Index(self) -> int: 602 return self.index 603 604 def Proto(self) -> cp_model_pb2.ConstraintProto: 605 return self.proto 606 607 StartExpr = start_expr 608 SizeExpr = size_expr 609 EndExpr = end_expr 610 611 # pylint: enable=invalid-name 612 613 614def object_is_a_true_literal(literal: LiteralT) -> bool: 615 """Checks if literal is either True, or a Boolean literals fixed to True.""" 616 if isinstance(literal, IntVar): 617 proto = literal.proto 618 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 619 if isinstance(literal, cmh.NotBooleanVariable): 620 proto = literal.negated().proto 621 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 622 if isinstance(literal, IntegralTypes): 623 return int(literal) == 1 624 return False 625 626 627def object_is_a_false_literal(literal: LiteralT) -> bool: 628 """Checks if literal is either False, or a Boolean literals fixed to False.""" 629 if isinstance(literal, IntVar): 630 proto = literal.proto 631 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 632 if isinstance(literal, cmh.NotBooleanVariable): 633 proto = literal.negated().proto 634 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 635 if isinstance(literal, IntegralTypes): 636 return int(literal) == 0 637 return False 638 639 640class CpModel: 641 """Methods for building a CP model. 642 643 Methods beginning with: 644 645 * ```New``` create integer, boolean, or interval variables. 646 * ```add``` create new constraints and add them to the model. 647 """ 648 649 def __init__(self) -> None: 650 self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto() 651 self.__constant_map: Dict[IntegralT, int] = {} 652 self.__var_list: VariableList = VariableList() 653 654 # Naming. 655 @property 656 def name(self) -> str: 657 """Returns the name of the model.""" 658 if not self.__model or not self.__model.name: 659 return "" 660 return self.__model.name 661 662 @name.setter 663 def name(self, name: str): 664 """Sets the name of the model.""" 665 self.__model.name = name 666 667 # Integer variable. 668 669 def _append_int_var(self, var: IntVar) -> IntVar: 670 """Appends an integer variable to the list of variables.""" 671 self.__var_list.append(var) 672 return var 673 674 def _get_int_var(self, index: int) -> IntVar: 675 return self.__var_list.get(index) 676 677 def rebuild_from_linear_expression_proto( 678 self, 679 proto: cp_model_pb2.LinearExpressionProto, 680 ) -> LinearExpr: 681 return self.__var_list.rebuild_expr(proto) 682 683 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 684 """Create an integer variable with domain [lb, ub]. 685 686 The CP-SAT solver is limited to integer variables. If you have fractional 687 values, scale them up so that they become integers; if you have strings, 688 encode them as integers. 689 690 Args: 691 lb: Lower bound for the variable. 692 ub: Upper bound for the variable. 693 name: The name of the variable. 694 695 Returns: 696 a variable whose domain is [lb, ub]. 697 """ 698 domain_is_boolean = lb >= 0 and ub <= 1 699 return self._append_int_var( 700 IntVar( 701 self.__model, 702 sorted_interval_list.Domain(lb, ub), 703 domain_is_boolean, 704 name, 705 ) 706 ) 707 708 def new_int_var_from_domain( 709 self, domain: sorted_interval_list.Domain, name: str 710 ) -> IntVar: 711 """Create an integer variable from a domain. 712 713 A domain is a set of integers specified by a collection of intervals. 714 For example, `model.new_int_var_from_domain(cp_model. 715 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 716 717 Args: 718 domain: An instance of the Domain class. 719 name: The name of the variable. 720 721 Returns: 722 a variable whose domain is the given domain. 723 """ 724 domain_is_boolean = domain.min() >= 0 and domain.max() <= 1 725 return self._append_int_var( 726 IntVar(self.__model, domain, domain_is_boolean, name) 727 ) 728 729 def new_bool_var(self, name: str) -> IntVar: 730 """Creates a 0-1 variable with the given name.""" 731 return self._append_int_var( 732 IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name) 733 ) 734 735 def new_constant(self, value: IntegralT) -> IntVar: 736 """Declares a constant integer.""" 737 index: int = self.get_or_make_index_from_constant(value) 738 return self._get_int_var(index) 739 740 def new_int_var_series( 741 self, 742 name: str, 743 index: pd.Index, 744 lower_bounds: Union[IntegralT, pd.Series], 745 upper_bounds: Union[IntegralT, pd.Series], 746 ) -> pd.Series: 747 """Creates a series of (scalar-valued) variables with the given name. 748 749 Args: 750 name (str): Required. The name of the variable set. 751 index (pd.Index): Required. The index to use for the variable set. 752 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 753 set. If a `pd.Series` is passed in, it will be based on the 754 corresponding values of the pd.Series. 755 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 756 set. If a `pd.Series` is passed in, it will be based on the 757 corresponding values of the pd.Series. 758 759 Returns: 760 pd.Series: The variable set indexed by its corresponding dimensions. 761 762 Raises: 763 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 764 ValueError: if the `name` is not a valid identifier or already exists. 765 ValueError: if the `lowerbound` is greater than the `upperbound`. 766 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 767 the input index. 768 """ 769 if not isinstance(index, pd.Index): 770 raise TypeError("Non-index object is used as index") 771 if not name.isidentifier(): 772 raise ValueError(f"name={name!r} is not a valid identifier") 773 if ( 774 isinstance(lower_bounds, IntegralTypes) 775 and isinstance(upper_bounds, IntegralTypes) 776 and lower_bounds > upper_bounds 777 ): 778 raise ValueError( 779 f"lower_bound={lower_bounds} is greater than" 780 f" upper_bound={upper_bounds} for variable set={name}" 781 ) 782 783 lower_bounds = _convert_to_integral_series_and_validate_index( 784 lower_bounds, index 785 ) 786 upper_bounds = _convert_to_integral_series_and_validate_index( 787 upper_bounds, index 788 ) 789 return pd.Series( 790 index=index, 791 data=[ 792 # pylint: disable=g-complex-comprehension 793 self._append_int_var( 794 IntVar( 795 model=self.__model, 796 name=f"{name}[{i}]", 797 domain=sorted_interval_list.Domain( 798 lower_bounds[i], upper_bounds[i] 799 ), 800 is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1, 801 ) 802 ) 803 for i in index 804 ], 805 ) 806 807 def new_bool_var_series( 808 self, 809 name: str, 810 index: pd.Index, 811 ) -> pd.Series: 812 """Creates a series of (scalar-valued) variables with the given name. 813 814 Args: 815 name (str): Required. The name of the variable set. 816 index (pd.Index): Required. The index to use for the variable set. 817 818 Returns: 819 pd.Series: The variable set indexed by its corresponding dimensions. 820 821 Raises: 822 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 823 ValueError: if the `name` is not a valid identifier or already exists. 824 """ 825 if not isinstance(index, pd.Index): 826 raise TypeError("Non-index object is used as index") 827 if not name.isidentifier(): 828 raise ValueError(f"name={name!r} is not a valid identifier") 829 return pd.Series( 830 index=index, 831 data=[ 832 # pylint: disable=g-complex-comprehension 833 self._append_int_var( 834 IntVar( 835 model=self.__model, 836 name=f"{name}[{i}]", 837 domain=sorted_interval_list.Domain(0, 1), 838 is_boolean=True, 839 ) 840 ) 841 for i in index 842 ], 843 ) 844 845 # Linear constraints. 846 847 def add_linear_constraint( 848 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 849 ) -> Constraint: 850 """Adds the constraint: `lb <= linear_expr <= ub`.""" 851 return self.add_linear_expression_in_domain( 852 linear_expr, sorted_interval_list.Domain(lb, ub) 853 ) 854 855 def add_linear_expression_in_domain( 856 self, 857 linear_expr: LinearExprT, 858 domain: sorted_interval_list.Domain, 859 ) -> Constraint: 860 """Adds the constraint: `linear_expr` in `domain`.""" 861 if isinstance(linear_expr, LinearExpr): 862 ble = BoundedLinearExpression(linear_expr, domain) 863 if not ble.ok: 864 raise TypeError( 865 "Cannot add a linear expression containing floating point" 866 f" coefficients or constants: {type(linear_expr).__name__!r}" 867 ) 868 return self.add(ble) 869 if isinstance(linear_expr, IntegralTypes): 870 if not domain.contains(int(linear_expr)): 871 return self.add_bool_or([]) # Evaluate to false. 872 else: 873 return self.add_bool_and([]) # Evaluate to true. 874 raise TypeError( 875 "not supported:" 876 f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})" 877 ) 878 879 def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint: 880 """Adds a `BoundedLinearExpression` to the model. 881 882 Args: 883 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 884 885 Returns: 886 An instance of the `Constraint` class. 887 888 Raises: 889 TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean. 890 """ 891 if isinstance(ct, BoundedLinearExpression): 892 result = Constraint(self) 893 model_ct = self.__model.constraints[result.index] 894 for var in ct.vars: 895 model_ct.linear.vars.append(var.index) 896 model_ct.linear.coeffs.extend(ct.coeffs) 897 model_ct.linear.domain.extend( 898 [ 899 cmn.capped_subtraction(x, ct.offset) 900 for x in ct.bounds.flattened_intervals() 901 ] 902 ) 903 return result 904 if ct and cmn.is_boolean(ct): 905 return self.add_bool_or([True]) 906 if not ct and cmn.is_boolean(ct): 907 return self.add_bool_or([]) # Evaluate to false. 908 raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})") 909 910 # General Integer Constraints. 911 912 @overload 913 def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ... 914 915 @overload 916 def add_all_different(self, *expressions: LinearExprT) -> Constraint: ... 917 918 def add_all_different(self, *expressions): 919 """Adds AllDifferent(expressions). 920 921 This constraint forces all expressions to have different values. 922 923 Args: 924 *expressions: simple expressions of the form a * var + constant. 925 926 Returns: 927 An instance of the `Constraint` class. 928 """ 929 ct = Constraint(self) 930 model_ct = self.__model.constraints[ct.index] 931 expanded = expand_generator_or_tuple(expressions) 932 model_ct.all_diff.exprs.extend( 933 self.parse_linear_expression(x) for x in expanded 934 ) 935 return ct 936 937 def add_element( 938 self, 939 index: LinearExprT, 940 expressions: Sequence[LinearExprT], 941 target: LinearExprT, 942 ) -> Constraint: 943 """Adds the element constraint: `expressions[index] == target`. 944 945 Args: 946 index: The index of the selected expression in the array. It must be an 947 affine expression (a * var + b). 948 expressions: A list of affine expressions. 949 target: The expression constrained to be equal to the selected expression. 950 It must be an affine expression (a * var + b). 951 952 Returns: 953 An instance of the `Constraint` class. 954 """ 955 956 if not expressions: 957 raise ValueError("add_element expects a non-empty expressions array") 958 959 if isinstance(index, IntegralTypes): 960 expression: LinearExprT = list(expressions)[int(index)] 961 return self.add(expression == target) 962 963 ct = Constraint(self) 964 model_ct = self.__model.constraints[ct.index] 965 model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index)) 966 model_ct.element.exprs.extend( 967 [self.parse_linear_expression(e) for e in expressions] 968 ) 969 model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target)) 970 return ct 971 972 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 973 """Adds Circuit(arcs). 974 975 Adds a circuit constraint from a sparse list of arcs that encode the graph. 976 977 A circuit is a unique Hamiltonian cycle in a subgraph of the total 978 graph. In case a node 'i' is not in the cycle, then there must be a 979 loop arc 'i -> i' associated with a true literal. Otherwise 980 this constraint will fail. 981 982 Args: 983 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 984 literal). The arc is selected in the circuit if the literal is true. 985 Both source_node and destination_node must be integers between 0 and the 986 number of nodes - 1. 987 988 Returns: 989 An instance of the `Constraint` class. 990 991 Raises: 992 ValueError: If the list of arcs is empty. 993 """ 994 if not arcs: 995 raise ValueError("add_circuit expects a non-empty array of arcs") 996 ct = Constraint(self) 997 model_ct = self.__model.constraints[ct.index] 998 for arc in arcs: 999 model_ct.circuit.tails.append(arc[0]) 1000 model_ct.circuit.heads.append(arc[1]) 1001 model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2])) 1002 return ct 1003 1004 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1005 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1006 1007 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1008 literals[i] is true must satisfy this set of properties: 1009 - #incoming arcs == 1 except for node 0. 1010 - #outgoing arcs == 1 except for node 0. 1011 - for node zero, #incoming arcs == #outgoing arcs. 1012 - There are no duplicate arcs. 1013 - Self-arcs are allowed except for node 0. 1014 - There is no cycle in this graph, except through node 0. 1015 1016 Args: 1017 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1018 literal). The arc is selected in the circuit if the literal is true. 1019 Both source_node and destination_node must be integers between 0 and the 1020 number of nodes - 1. 1021 1022 Returns: 1023 An instance of the `Constraint` class. 1024 1025 Raises: 1026 ValueError: If the list of arcs is empty. 1027 """ 1028 if not arcs: 1029 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1030 ct = Constraint(self) 1031 model_ct = self.__model.constraints[ct.index] 1032 for arc in arcs: 1033 model_ct.routes.tails.append(arc[0]) 1034 model_ct.routes.heads.append(arc[1]) 1035 model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2])) 1036 return ct 1037 1038 def add_allowed_assignments( 1039 self, 1040 expressions: Sequence[LinearExprT], 1041 tuples_list: Iterable[Sequence[IntegralT]], 1042 ) -> Constraint: 1043 """Adds AllowedAssignments(expressions, tuples_list). 1044 1045 An AllowedAssignments constraint is a constraint on an array of affine 1046 expressions, which requires that when all expressions are assigned values, 1047 the 1048 resulting array equals one of the tuples in `tuple_list`. 1049 1050 Args: 1051 expressions: A list of affine expressions (a * var + b). 1052 tuples_list: A list of admissible tuples. Each tuple must have the same 1053 length as the expressions, and the ith value of a tuple corresponds to 1054 the ith expression. 1055 1056 Returns: 1057 An instance of the `Constraint` class. 1058 1059 Raises: 1060 TypeError: If a tuple does not have the same size as the list of 1061 expressions. 1062 ValueError: If the array of expressions is empty. 1063 """ 1064 1065 if not expressions: 1066 raise ValueError( 1067 "add_allowed_assignments expects a non-empty expressions array" 1068 ) 1069 1070 ct: Constraint = Constraint(self) 1071 model_ct = self.__model.constraints[ct.index] 1072 model_ct.table.exprs.extend( 1073 [self.parse_linear_expression(e) for e in expressions] 1074 ) 1075 arity: int = len(expressions) 1076 for one_tuple in tuples_list: 1077 if len(one_tuple) != arity: 1078 raise TypeError(f"Tuple {one_tuple!r} has the wrong arity") 1079 1080 # duck-typing (no explicit type checks here) 1081 try: 1082 for one_tuple in tuples_list: 1083 model_ct.table.values.extend(one_tuple) 1084 except ValueError as ex: 1085 raise TypeError( 1086 "add_xxx_assignment: Not an integer or does not fit in an int64_t:" 1087 f" {type(ex.args).__name__!r}" 1088 ) from ex 1089 1090 return ct 1091 1092 def add_forbidden_assignments( 1093 self, 1094 expressions: Sequence[LinearExprT], 1095 tuples_list: Iterable[Sequence[IntegralT]], 1096 ) -> Constraint: 1097 """Adds add_forbidden_assignments(expressions, [tuples_list]). 1098 1099 A ForbiddenAssignments constraint is a constraint on an array of affine 1100 expressions where the list of impossible combinations is provided in the 1101 tuples list. 1102 1103 Args: 1104 expressions: A list of affine expressions (a * var + b). 1105 tuples_list: A list of forbidden tuples. Each tuple must have the same 1106 length as the expressions, and the *i*th value of a tuple corresponds to 1107 the *i*th expression. 1108 1109 Returns: 1110 An instance of the `Constraint` class. 1111 1112 Raises: 1113 TypeError: If a tuple does not have the same size as the list of 1114 expressions. 1115 ValueError: If the array of expressions is empty. 1116 """ 1117 1118 if not expressions: 1119 raise ValueError( 1120 "add_forbidden_assignments expects a non-empty expressions array" 1121 ) 1122 1123 index: int = len(self.__model.constraints) 1124 ct: Constraint = self.add_allowed_assignments(expressions, tuples_list) 1125 self.__model.constraints[index].table.negated = True 1126 return ct 1127 1128 def add_automaton( 1129 self, 1130 transition_expressions: Sequence[LinearExprT], 1131 starting_state: IntegralT, 1132 final_states: Sequence[IntegralT], 1133 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1134 ) -> Constraint: 1135 """Adds an automaton constraint. 1136 1137 An automaton constraint takes a list of affine expressions (a * var + b) (of 1138 size *n*), an initial state, a set of final states, and a set of 1139 transitions. A transition is a triplet (*tail*, *transition*, *head*), where 1140 *tail* and *head* are states, and *transition* is the label of an arc from 1141 *head* to *tail*, corresponding to the value of one expression in the list 1142 of 1143 expressions. 1144 1145 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1146 contains the possible states of the automaton. The first state contains the 1147 initial state. The last phase contains the final states. 1148 1149 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1150 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1151 an arc from the state *tail* of phase *i* and the state *head* of phase 1152 *i* + 1. This arc is labeled by the value *transition* of the expression 1153 `expressions[i]`. That is, this arc can only be selected if `expressions[i]` 1154 is assigned the value *transition*. 1155 1156 A feasible solution of this constraint is an assignment of expressions such 1157 that, starting from the initial state in phase 0, there is a path labeled by 1158 the values of the expressions that ends in one of the final states in the 1159 final phase. 1160 1161 Args: 1162 transition_expressions: A non-empty list of affine expressions (a * var + 1163 b) whose values correspond to the labels of the arcs traversed by the 1164 automaton. 1165 starting_state: The initial state of the automaton. 1166 final_states: A non-empty list of admissible final states. 1167 transition_triples: A list of transitions for the automaton, in the 1168 following format (current_state, variable_value, next_state). 1169 1170 Returns: 1171 An instance of the `Constraint` class. 1172 1173 Raises: 1174 ValueError: if `transition_expressions`, `final_states`, or 1175 `transition_triples` are empty. 1176 """ 1177 1178 if not transition_expressions: 1179 raise ValueError( 1180 "add_automaton expects a non-empty transition_expressions array" 1181 ) 1182 if not final_states: 1183 raise ValueError("add_automaton expects some final states") 1184 1185 if not transition_triples: 1186 raise ValueError("add_automaton expects some transition triples") 1187 1188 ct = Constraint(self) 1189 model_ct = self.__model.constraints[ct.index] 1190 model_ct.automaton.exprs.extend( 1191 [self.parse_linear_expression(e) for e in transition_expressions] 1192 ) 1193 model_ct.automaton.starting_state = starting_state 1194 for v in final_states: 1195 model_ct.automaton.final_states.append(v) 1196 for t in transition_triples: 1197 if len(t) != 3: 1198 raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)") 1199 model_ct.automaton.transition_tail.append(t[0]) 1200 model_ct.automaton.transition_label.append(t[1]) 1201 model_ct.automaton.transition_head.append(t[2]) 1202 return ct 1203 1204 def add_inverse( 1205 self, 1206 variables: Sequence[VariableT], 1207 inverse_variables: Sequence[VariableT], 1208 ) -> Constraint: 1209 """Adds Inverse(variables, inverse_variables). 1210 1211 An inverse constraint enforces that if `variables[i]` is assigned a value 1212 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1213 1214 Args: 1215 variables: An array of integer variables. 1216 inverse_variables: An array of integer variables. 1217 1218 Returns: 1219 An instance of the `Constraint` class. 1220 1221 Raises: 1222 TypeError: if variables and inverse_variables have different lengths, or 1223 if they are empty. 1224 """ 1225 1226 if not variables or not inverse_variables: 1227 raise TypeError("The Inverse constraint does not accept empty arrays") 1228 if len(variables) != len(inverse_variables): 1229 raise TypeError( 1230 "In the inverse constraint, the two array variables and" 1231 " inverse_variables must have the same length." 1232 ) 1233 ct = Constraint(self) 1234 model_ct = self.__model.constraints[ct.index] 1235 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1236 model_ct.inverse.f_inverse.extend( 1237 [self.get_or_make_index(x) for x in inverse_variables] 1238 ) 1239 return ct 1240 1241 def add_reservoir_constraint( 1242 self, 1243 times: Iterable[LinearExprT], 1244 level_changes: Iterable[LinearExprT], 1245 min_level: int, 1246 max_level: int, 1247 ) -> Constraint: 1248 """Adds Reservoir(times, level_changes, min_level, max_level). 1249 1250 Maintains a reservoir level within bounds. The water level starts at 0, and 1251 at any time, it must be between min_level and max_level. 1252 1253 If the affine expression `times[i]` is assigned a value t, then the current 1254 level changes by `level_changes[i]`, which is constant, at time t. 1255 1256 Note that min level must be <= 0, and the max level must be >= 0. Please 1257 use fixed level_changes to simulate initial state. 1258 1259 Therefore, at any time: 1260 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1261 1262 Args: 1263 times: A list of 1-var affine expressions (a * x + b) which specify the 1264 time of the filling or emptying the reservoir. 1265 level_changes: A list of integer values that specifies the amount of the 1266 emptying or filling. Currently, variable demands are not supported. 1267 min_level: At any time, the level of the reservoir must be greater or 1268 equal than the min level. 1269 max_level: At any time, the level of the reservoir must be less or equal 1270 than the max level. 1271 1272 Returns: 1273 An instance of the `Constraint` class. 1274 1275 Raises: 1276 ValueError: if max_level < min_level. 1277 1278 ValueError: if max_level < 0. 1279 1280 ValueError: if min_level > 0 1281 """ 1282 1283 if max_level < min_level: 1284 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1285 1286 if max_level < 0: 1287 raise ValueError("Reservoir constraint must have a max_level >= 0") 1288 1289 if min_level > 0: 1290 raise ValueError("Reservoir constraint must have a min_level <= 0") 1291 1292 ct = Constraint(self) 1293 model_ct = self.__model.constraints[ct.index] 1294 model_ct.reservoir.time_exprs.extend( 1295 [self.parse_linear_expression(x) for x in times] 1296 ) 1297 model_ct.reservoir.level_changes.extend( 1298 [self.parse_linear_expression(x) for x in level_changes] 1299 ) 1300 model_ct.reservoir.min_level = min_level 1301 model_ct.reservoir.max_level = max_level 1302 return ct 1303 1304 def add_reservoir_constraint_with_active( 1305 self, 1306 times: Iterable[LinearExprT], 1307 level_changes: Iterable[LinearExprT], 1308 actives: Iterable[LiteralT], 1309 min_level: int, 1310 max_level: int, 1311 ) -> Constraint: 1312 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1313 1314 Maintains a reservoir level within bounds. The water level starts at 0, and 1315 at any time, it must be between min_level and max_level. 1316 1317 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1318 `True`, then the current level changes by `level_changes[i]`, which is 1319 constant, 1320 at time t. 1321 1322 Note that min level must be <= 0, and the max level must be >= 0. Please 1323 use fixed level_changes to simulate initial state. 1324 1325 Therefore, at any time: 1326 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1327 max_level] 1328 1329 1330 The array of boolean variables 'actives', if defined, indicates which 1331 actions are actually performed. 1332 1333 Args: 1334 times: A list of 1-var affine expressions (a * x + b) which specify the 1335 time of the filling or emptying the reservoir. 1336 level_changes: A list of integer values that specifies the amount of the 1337 emptying or filling. Currently, variable demands are not supported. 1338 actives: a list of boolean variables. They indicates if the 1339 emptying/refilling events actually take place. 1340 min_level: At any time, the level of the reservoir must be greater or 1341 equal than the min level. 1342 max_level: At any time, the level of the reservoir must be less or equal 1343 than the max level. 1344 1345 Returns: 1346 An instance of the `Constraint` class. 1347 1348 Raises: 1349 ValueError: if max_level < min_level. 1350 1351 ValueError: if max_level < 0. 1352 1353 ValueError: if min_level > 0 1354 """ 1355 1356 if max_level < min_level: 1357 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1358 1359 if max_level < 0: 1360 raise ValueError("Reservoir constraint must have a max_level >= 0") 1361 1362 if min_level > 0: 1363 raise ValueError("Reservoir constraint must have a min_level <= 0") 1364 1365 ct = Constraint(self) 1366 model_ct = self.__model.constraints[ct.index] 1367 model_ct.reservoir.time_exprs.extend( 1368 [self.parse_linear_expression(x) for x in times] 1369 ) 1370 model_ct.reservoir.level_changes.extend( 1371 [self.parse_linear_expression(x) for x in level_changes] 1372 ) 1373 model_ct.reservoir.active_literals.extend( 1374 [self.get_or_make_boolean_index(x) for x in actives] 1375 ) 1376 model_ct.reservoir.min_level = min_level 1377 model_ct.reservoir.max_level = max_level 1378 return ct 1379 1380 def add_map_domain( 1381 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 1382 ): 1383 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 1384 1385 for i, bool_var in enumerate(bool_var_array): 1386 b_index = bool_var.index 1387 var_index = var.index 1388 model_ct = self.__model.constraints.add() 1389 model_ct.linear.vars.append(var_index) 1390 model_ct.linear.coeffs.append(1) 1391 offset_as_int = int(offset) 1392 model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i]) 1393 model_ct.enforcement_literal.append(b_index) 1394 1395 model_ct = self.__model.constraints.add() 1396 model_ct.linear.vars.append(var_index) 1397 model_ct.linear.coeffs.append(1) 1398 model_ct.enforcement_literal.append(-b_index - 1) 1399 if offset + i - 1 >= INT_MIN: 1400 model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1]) 1401 if offset + i + 1 <= INT_MAX: 1402 model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX]) 1403 1404 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 1405 """Adds `a => b` (`a` implies `b`).""" 1406 ct = Constraint(self) 1407 model_ct = self.__model.constraints[ct.index] 1408 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 1409 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 1410 return ct 1411 1412 @overload 1413 def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ... 1414 1415 @overload 1416 def add_bool_or(self, *literals: LiteralT) -> Constraint: ... 1417 1418 def add_bool_or(self, *literals): 1419 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 1420 ct = Constraint(self) 1421 model_ct = self.__model.constraints[ct.index] 1422 model_ct.bool_or.literals.extend( 1423 [ 1424 self.get_or_make_boolean_index(x) 1425 for x in expand_generator_or_tuple(literals) 1426 ] 1427 ) 1428 return ct 1429 1430 @overload 1431 def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 1432 1433 @overload 1434 def add_at_least_one(self, *literals: LiteralT) -> Constraint: ... 1435 1436 def add_at_least_one(self, *literals): 1437 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 1438 return self.add_bool_or(*literals) 1439 1440 @overload 1441 def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 1442 1443 @overload 1444 def add_at_most_one(self, *literals: LiteralT) -> Constraint: ... 1445 1446 def add_at_most_one(self, *literals): 1447 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 1448 ct = Constraint(self) 1449 model_ct = self.__model.constraints[ct.index] 1450 model_ct.at_most_one.literals.extend( 1451 [ 1452 self.get_or_make_boolean_index(x) 1453 for x in expand_generator_or_tuple(literals) 1454 ] 1455 ) 1456 return ct 1457 1458 @overload 1459 def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 1460 1461 @overload 1462 def add_exactly_one(self, *literals: LiteralT) -> Constraint: ... 1463 1464 def add_exactly_one(self, *literals): 1465 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 1466 ct = Constraint(self) 1467 model_ct = self.__model.constraints[ct.index] 1468 model_ct.exactly_one.literals.extend( 1469 [ 1470 self.get_or_make_boolean_index(x) 1471 for x in expand_generator_or_tuple(literals) 1472 ] 1473 ) 1474 return ct 1475 1476 @overload 1477 def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ... 1478 1479 @overload 1480 def add_bool_and(self, *literals: LiteralT) -> Constraint: ... 1481 1482 def add_bool_and(self, *literals): 1483 """Adds `And(literals) == true`.""" 1484 ct = Constraint(self) 1485 model_ct = self.__model.constraints[ct.index] 1486 model_ct.bool_and.literals.extend( 1487 [ 1488 self.get_or_make_boolean_index(x) 1489 for x in expand_generator_or_tuple(literals) 1490 ] 1491 ) 1492 return ct 1493 1494 @overload 1495 def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ... 1496 1497 @overload 1498 def add_bool_xor(self, *literals: LiteralT) -> Constraint: ... 1499 1500 def add_bool_xor(self, *literals): 1501 """Adds `XOr(literals) == true`. 1502 1503 In contrast to add_bool_or and add_bool_and, it does not support 1504 .only_enforce_if(). 1505 1506 Args: 1507 *literals: the list of literals in the constraint. 1508 1509 Returns: 1510 An `Constraint` object. 1511 """ 1512 ct = Constraint(self) 1513 model_ct = self.__model.constraints[ct.index] 1514 model_ct.bool_xor.literals.extend( 1515 [ 1516 self.get_or_make_boolean_index(x) 1517 for x in expand_generator_or_tuple(literals) 1518 ] 1519 ) 1520 return ct 1521 1522 def add_min_equality( 1523 self, target: LinearExprT, exprs: Iterable[LinearExprT] 1524 ) -> Constraint: 1525 """Adds `target == Min(exprs)`.""" 1526 ct = Constraint(self) 1527 model_ct = self.__model.constraints[ct.index] 1528 model_ct.lin_max.exprs.extend( 1529 [self.parse_linear_expression(x, True) for x in exprs] 1530 ) 1531 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 1532 return ct 1533 1534 def add_max_equality( 1535 self, target: LinearExprT, exprs: Iterable[LinearExprT] 1536 ) -> Constraint: 1537 """Adds `target == Max(exprs)`.""" 1538 ct = Constraint(self) 1539 model_ct = self.__model.constraints[ct.index] 1540 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 1541 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 1542 return ct 1543 1544 def add_division_equality( 1545 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 1546 ) -> Constraint: 1547 """Adds `target == num // denom` (integer division rounded towards 0).""" 1548 ct = Constraint(self) 1549 model_ct = self.__model.constraints[ct.index] 1550 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 1551 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 1552 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 1553 return ct 1554 1555 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 1556 """Adds `target == Abs(expr)`.""" 1557 ct = Constraint(self) 1558 model_ct = self.__model.constraints[ct.index] 1559 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 1560 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 1561 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 1562 return ct 1563 1564 def add_modulo_equality( 1565 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 1566 ) -> Constraint: 1567 """Adds `target = expr % mod`. 1568 1569 It uses the C convention, that is the result is the remainder of the 1570 integral division rounded towards 0. 1571 1572 For example: 1573 * 10 % 3 = 1 1574 * -10 % 3 = -1 1575 * 10 % -3 = 1 1576 * -10 % -3 = -1 1577 1578 Args: 1579 target: the target expression. 1580 expr: the expression to compute the modulo of. 1581 mod: the modulus expression. 1582 1583 Returns: 1584 A `Constraint` object. 1585 """ 1586 ct = Constraint(self) 1587 model_ct = self.__model.constraints[ct.index] 1588 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 1589 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 1590 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 1591 return ct 1592 1593 def add_multiplication_equality( 1594 self, 1595 target: LinearExprT, 1596 *expressions: Union[Iterable[LinearExprT], LinearExprT], 1597 ) -> Constraint: 1598 """Adds `target == expressions[0] * .. * expressions[n]`.""" 1599 ct = Constraint(self) 1600 model_ct = self.__model.constraints[ct.index] 1601 model_ct.int_prod.exprs.extend( 1602 [ 1603 self.parse_linear_expression(expr) 1604 for expr in expand_generator_or_tuple(expressions) 1605 ] 1606 ) 1607 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 1608 return ct 1609 1610 # Scheduling support 1611 1612 def new_interval_var( 1613 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 1614 ) -> IntervalVar: 1615 """Creates an interval variable from start, size, and end. 1616 1617 An interval variable is a constraint, that is itself used in other 1618 constraints like NoOverlap. 1619 1620 Internally, it ensures that `start + size == end`. 1621 1622 Args: 1623 start: The start of the interval. It must be of the form a * var + b. 1624 size: The size of the interval. It must be of the form a * var + b. 1625 end: The end of the interval. It must be of the form a * var + b. 1626 name: The name of the interval variable. 1627 1628 Returns: 1629 An `IntervalVar` object. 1630 """ 1631 1632 start_expr = self.parse_linear_expression(start) 1633 size_expr = self.parse_linear_expression(size) 1634 end_expr = self.parse_linear_expression(end) 1635 if len(start_expr.vars) > 1: 1636 raise TypeError( 1637 "cp_model.new_interval_var: start must be 1-var affine or constant." 1638 ) 1639 if len(size_expr.vars) > 1: 1640 raise TypeError( 1641 "cp_model.new_interval_var: size must be 1-var affine or constant." 1642 ) 1643 if len(end_expr.vars) > 1: 1644 raise TypeError( 1645 "cp_model.new_interval_var: end must be 1-var affine or constant." 1646 ) 1647 return IntervalVar( 1648 self.__model, 1649 self.__var_list, 1650 start_expr, 1651 size_expr, 1652 end_expr, 1653 None, 1654 name, 1655 ) 1656 1657 def new_interval_var_series( 1658 self, 1659 name: str, 1660 index: pd.Index, 1661 starts: Union[LinearExprT, pd.Series], 1662 sizes: Union[LinearExprT, pd.Series], 1663 ends: Union[LinearExprT, pd.Series], 1664 ) -> pd.Series: 1665 """Creates a series of interval variables with the given name. 1666 1667 Args: 1668 name (str): Required. The name of the variable set. 1669 index (pd.Index): Required. The index to use for the variable set. 1670 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1671 set. If a `pd.Series` is passed in, it will be based on the 1672 corresponding values of the pd.Series. 1673 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 1674 set. If a `pd.Series` is passed in, it will be based on the 1675 corresponding values of the pd.Series. 1676 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 1677 set. If a `pd.Series` is passed in, it will be based on the 1678 corresponding values of the pd.Series. 1679 1680 Returns: 1681 pd.Series: The interval variable set indexed by its corresponding 1682 dimensions. 1683 1684 Raises: 1685 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1686 ValueError: if the `name` is not a valid identifier or already exists. 1687 ValueError: if the all the indexes do not match. 1688 """ 1689 if not isinstance(index, pd.Index): 1690 raise TypeError("Non-index object is used as index") 1691 if not name.isidentifier(): 1692 raise ValueError(f"name={name!r} is not a valid identifier") 1693 1694 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1695 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 1696 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 1697 interval_array = [] 1698 for i in index: 1699 interval_array.append( 1700 self.new_interval_var( 1701 start=starts[i], 1702 size=sizes[i], 1703 end=ends[i], 1704 name=f"{name}[{i}]", 1705 ) 1706 ) 1707 return pd.Series(index=index, data=interval_array) 1708 1709 def new_fixed_size_interval_var( 1710 self, start: LinearExprT, size: IntegralT, name: str 1711 ) -> IntervalVar: 1712 """Creates an interval variable from start, and a fixed size. 1713 1714 An interval variable is a constraint, that is itself used in other 1715 constraints like NoOverlap. 1716 1717 Args: 1718 start: The start of the interval. It must be of the form a * var + b. 1719 size: The size of the interval. It must be an integer value. 1720 name: The name of the interval variable. 1721 1722 Returns: 1723 An `IntervalVar` object. 1724 """ 1725 start_expr = self.parse_linear_expression(start) 1726 size_expr = self.parse_linear_expression(size) 1727 end_expr = self.parse_linear_expression(start + size) 1728 if len(start_expr.vars) > 1: 1729 raise TypeError( 1730 "cp_model.new_interval_var: start must be affine or constant." 1731 ) 1732 return IntervalVar( 1733 self.__model, 1734 self.__var_list, 1735 start_expr, 1736 size_expr, 1737 end_expr, 1738 None, 1739 name, 1740 ) 1741 1742 def new_fixed_size_interval_var_series( 1743 self, 1744 name: str, 1745 index: pd.Index, 1746 starts: Union[LinearExprT, pd.Series], 1747 sizes: Union[IntegralT, pd.Series], 1748 ) -> pd.Series: 1749 """Creates a series of interval variables with the given name. 1750 1751 Args: 1752 name (str): Required. The name of the variable set. 1753 index (pd.Index): Required. The index to use for the variable set. 1754 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1755 set. If a `pd.Series` is passed in, it will be based on the 1756 corresponding values of the pd.Series. 1757 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 1758 the set. If a `pd.Series` is passed in, it will be based on the 1759 corresponding values of the pd.Series. 1760 1761 Returns: 1762 pd.Series: The interval variable set indexed by its corresponding 1763 dimensions. 1764 1765 Raises: 1766 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1767 ValueError: if the `name` is not a valid identifier or already exists. 1768 ValueError: if the all the indexes do not match. 1769 """ 1770 if not isinstance(index, pd.Index): 1771 raise TypeError("Non-index object is used as index") 1772 if not name.isidentifier(): 1773 raise ValueError(f"name={name!r} is not a valid identifier") 1774 1775 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1776 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 1777 interval_array = [] 1778 for i in index: 1779 interval_array.append( 1780 self.new_fixed_size_interval_var( 1781 start=starts[i], 1782 size=sizes[i], 1783 name=f"{name}[{i}]", 1784 ) 1785 ) 1786 return pd.Series(index=index, data=interval_array) 1787 1788 def new_optional_interval_var( 1789 self, 1790 start: LinearExprT, 1791 size: LinearExprT, 1792 end: LinearExprT, 1793 is_present: LiteralT, 1794 name: str, 1795 ) -> IntervalVar: 1796 """Creates an optional interval var from start, size, end, and is_present. 1797 1798 An optional interval variable is a constraint, that is itself used in other 1799 constraints like NoOverlap. This constraint is protected by a presence 1800 literal that indicates if it is active or not. 1801 1802 Internally, it ensures that `is_present` implies `start + size == 1803 end`. 1804 1805 Args: 1806 start: The start of the interval. It must be of the form a * var + b. 1807 size: The size of the interval. It must be of the form a * var + b. 1808 end: The end of the interval. It must be of the form a * var + b. 1809 is_present: A literal that indicates if the interval is active or not. A 1810 inactive interval is simply ignored by all constraints. 1811 name: The name of the interval variable. 1812 1813 Returns: 1814 An `IntervalVar` object. 1815 """ 1816 1817 # Creates the IntervalConstraintProto object. 1818 is_present_index = self.get_or_make_boolean_index(is_present) 1819 start_expr = self.parse_linear_expression(start) 1820 size_expr = self.parse_linear_expression(size) 1821 end_expr = self.parse_linear_expression(end) 1822 if len(start_expr.vars) > 1: 1823 raise TypeError( 1824 "cp_model.new_interval_var: start must be affine or constant." 1825 ) 1826 if len(size_expr.vars) > 1: 1827 raise TypeError( 1828 "cp_model.new_interval_var: size must be affine or constant." 1829 ) 1830 if len(end_expr.vars) > 1: 1831 raise TypeError( 1832 "cp_model.new_interval_var: end must be affine or constant." 1833 ) 1834 return IntervalVar( 1835 self.__model, 1836 self.__var_list, 1837 start_expr, 1838 size_expr, 1839 end_expr, 1840 is_present_index, 1841 name, 1842 ) 1843 1844 def new_optional_interval_var_series( 1845 self, 1846 name: str, 1847 index: pd.Index, 1848 starts: Union[LinearExprT, pd.Series], 1849 sizes: Union[LinearExprT, pd.Series], 1850 ends: Union[LinearExprT, pd.Series], 1851 are_present: Union[LiteralT, pd.Series], 1852 ) -> pd.Series: 1853 """Creates a series of interval variables with the given name. 1854 1855 Args: 1856 name (str): Required. The name of the variable set. 1857 index (pd.Index): Required. The index to use for the variable set. 1858 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1859 set. If a `pd.Series` is passed in, it will be based on the 1860 corresponding values of the pd.Series. 1861 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 1862 set. If a `pd.Series` is passed in, it will be based on the 1863 corresponding values of the pd.Series. 1864 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 1865 set. If a `pd.Series` is passed in, it will be based on the 1866 corresponding values of the pd.Series. 1867 are_present (Union[LiteralT, pd.Series]): The performed literal of each 1868 interval in the set. If a `pd.Series` is passed in, it will be based on 1869 the corresponding values of the pd.Series. 1870 1871 Returns: 1872 pd.Series: The interval variable set indexed by its corresponding 1873 dimensions. 1874 1875 Raises: 1876 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1877 ValueError: if the `name` is not a valid identifier or already exists. 1878 ValueError: if the all the indexes do not match. 1879 """ 1880 if not isinstance(index, pd.Index): 1881 raise TypeError("Non-index object is used as index") 1882 if not name.isidentifier(): 1883 raise ValueError(f"name={name!r} is not a valid identifier") 1884 1885 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1886 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 1887 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 1888 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 1889 1890 interval_array = [] 1891 for i in index: 1892 interval_array.append( 1893 self.new_optional_interval_var( 1894 start=starts[i], 1895 size=sizes[i], 1896 end=ends[i], 1897 is_present=are_present[i], 1898 name=f"{name}[{i}]", 1899 ) 1900 ) 1901 return pd.Series(index=index, data=interval_array) 1902 1903 def new_optional_fixed_size_interval_var( 1904 self, 1905 start: LinearExprT, 1906 size: IntegralT, 1907 is_present: LiteralT, 1908 name: str, 1909 ) -> IntervalVar: 1910 """Creates an interval variable from start, and a fixed size. 1911 1912 An interval variable is a constraint, that is itself used in other 1913 constraints like NoOverlap. 1914 1915 Args: 1916 start: The start of the interval. It must be of the form a * var + b. 1917 size: The size of the interval. It must be an integer value. 1918 is_present: A literal that indicates if the interval is active or not. A 1919 inactive interval is simply ignored by all constraints. 1920 name: The name of the interval variable. 1921 1922 Returns: 1923 An `IntervalVar` object. 1924 """ 1925 start_expr = self.parse_linear_expression(start) 1926 size_expr = self.parse_linear_expression(size) 1927 end_expr = self.parse_linear_expression(start + size) 1928 if len(start_expr.vars) > 1: 1929 raise TypeError( 1930 "cp_model.new_interval_var: start must be affine or constant." 1931 ) 1932 is_present_index = self.get_or_make_boolean_index(is_present) 1933 return IntervalVar( 1934 self.__model, 1935 self.__var_list, 1936 start_expr, 1937 size_expr, 1938 end_expr, 1939 is_present_index, 1940 name, 1941 ) 1942 1943 def new_optional_fixed_size_interval_var_series( 1944 self, 1945 name: str, 1946 index: pd.Index, 1947 starts: Union[LinearExprT, pd.Series], 1948 sizes: Union[IntegralT, pd.Series], 1949 are_present: Union[LiteralT, pd.Series], 1950 ) -> pd.Series: 1951 """Creates a series of interval variables with the given name. 1952 1953 Args: 1954 name (str): Required. The name of the variable set. 1955 index (pd.Index): Required. The index to use for the variable set. 1956 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1957 set. If a `pd.Series` is passed in, it will be based on the 1958 corresponding values of the pd.Series. 1959 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 1960 the set. If a `pd.Series` is passed in, it will be based on the 1961 corresponding values of the pd.Series. 1962 are_present (Union[LiteralT, pd.Series]): The performed literal of each 1963 interval in the set. If a `pd.Series` is passed in, it will be based on 1964 the corresponding values of the pd.Series. 1965 1966 Returns: 1967 pd.Series: The interval variable set indexed by its corresponding 1968 dimensions. 1969 1970 Raises: 1971 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1972 ValueError: if the `name` is not a valid identifier or already exists. 1973 ValueError: if the all the indexes do not match. 1974 """ 1975 if not isinstance(index, pd.Index): 1976 raise TypeError("Non-index object is used as index") 1977 if not name.isidentifier(): 1978 raise ValueError(f"name={name!r} is not a valid identifier") 1979 1980 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1981 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 1982 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 1983 interval_array = [] 1984 for i in index: 1985 interval_array.append( 1986 self.new_optional_fixed_size_interval_var( 1987 start=starts[i], 1988 size=sizes[i], 1989 is_present=are_present[i], 1990 name=f"{name}[{i}]", 1991 ) 1992 ) 1993 return pd.Series(index=index, data=interval_array) 1994 1995 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 1996 """Adds NoOverlap(interval_vars). 1997 1998 A NoOverlap constraint ensures that all present intervals do not overlap 1999 in time. 2000 2001 Args: 2002 interval_vars: The list of interval variables to constrain. 2003 2004 Returns: 2005 An instance of the `Constraint` class. 2006 """ 2007 ct = Constraint(self) 2008 model_ct = self.__model.constraints[ct.index] 2009 model_ct.no_overlap.intervals.extend( 2010 [self.get_interval_index(x) for x in interval_vars] 2011 ) 2012 return ct 2013 2014 def add_no_overlap_2d( 2015 self, 2016 x_intervals: Iterable[IntervalVar], 2017 y_intervals: Iterable[IntervalVar], 2018 ) -> Constraint: 2019 """Adds NoOverlap2D(x_intervals, y_intervals). 2020 2021 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2022 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2023 by two intervals which represent its projection onto the X and Y axis. 2024 2025 Furthermore, one box is optional if at least one of the x or y interval is 2026 optional. 2027 2028 Args: 2029 x_intervals: The X coordinates of the rectangles. 2030 y_intervals: The Y coordinates of the rectangles. 2031 2032 Returns: 2033 An instance of the `Constraint` class. 2034 """ 2035 ct = Constraint(self) 2036 model_ct = self.__model.constraints[ct.index] 2037 model_ct.no_overlap_2d.x_intervals.extend( 2038 [self.get_interval_index(x) for x in x_intervals] 2039 ) 2040 model_ct.no_overlap_2d.y_intervals.extend( 2041 [self.get_interval_index(x) for x in y_intervals] 2042 ) 2043 return ct 2044 2045 def add_cumulative( 2046 self, 2047 intervals: Iterable[IntervalVar], 2048 demands: Iterable[LinearExprT], 2049 capacity: LinearExprT, 2050 ) -> Constraint: 2051 """Adds Cumulative(intervals, demands, capacity). 2052 2053 This constraint enforces that: 2054 2055 for all t: 2056 sum(demands[i] 2057 if (start(intervals[i]) <= t < end(intervals[i])) and 2058 (intervals[i] is present)) <= capacity 2059 2060 Args: 2061 intervals: The list of intervals. 2062 demands: The list of demands for each interval. Each demand must be >= 0. 2063 Each demand can be a 1-var affine expression (a * x + b). 2064 capacity: The maximum capacity of the cumulative constraint. It can be a 2065 1-var affine expression (a * x + b). 2066 2067 Returns: 2068 An instance of the `Constraint` class. 2069 """ 2070 cumulative = Constraint(self) 2071 model_ct = self.__model.constraints[cumulative.index] 2072 model_ct.cumulative.intervals.extend( 2073 [self.get_interval_index(x) for x in intervals] 2074 ) 2075 for d in demands: 2076 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2077 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2078 return cumulative 2079 2080 # Support for model cloning. 2081 def clone(self) -> "CpModel": 2082 """Reset the model, and creates a new one from a CpModelProto instance.""" 2083 clone = CpModel() 2084 clone.proto.CopyFrom(self.proto) 2085 clone.rebuild_var_and_constant_map() 2086 return clone 2087 2088 def rebuild_var_and_constant_map(self): 2089 """Internal method used during model cloning.""" 2090 for i, var in enumerate(self.__model.variables): 2091 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2092 self.__constant_map[var.domain[0]] = i 2093 is_boolean = ( 2094 len(var.domain) == 2 and var.domain[0] >= 0 and var.domain[1] <= 1 2095 ) 2096 self.__var_list.append(IntVar(self.__model, i, is_boolean, None)) 2097 2098 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2099 """Returns an already created Boolean variable from its index.""" 2100 result = self._get_int_var(index) 2101 if not result.is_boolean: 2102 raise ValueError( 2103 f"get_bool_var_from_proto_index: index {index} does not reference a" 2104 " boolean variable" 2105 ) 2106 return result 2107 2108 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2109 """Returns an already created integer variable from its index.""" 2110 return self._get_int_var(index) 2111 2112 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2113 """Returns an already created interval variable from its index.""" 2114 if index < 0 or index >= len(self.__model.constraints): 2115 raise ValueError( 2116 f"get_interval_var_from_proto_index: out of bound index {index}" 2117 ) 2118 ct = self.__model.constraints[index] 2119 if not ct.HasField("interval"): 2120 raise ValueError( 2121 f"get_interval_var_from_proto_index: index {index} does not" 2122 " reference an" + " interval variable" 2123 ) 2124 2125 return IntervalVar(self.__model, self.__var_list, index, None, None, None, None) 2126 2127 # Helpers. 2128 2129 def __str__(self) -> str: 2130 return str(self.__model) 2131 2132 @property 2133 def proto(self) -> cp_model_pb2.CpModelProto: 2134 """Returns the underlying CpModelProto.""" 2135 return self.__model 2136 2137 def negated(self, index: int) -> int: 2138 return -index - 1 2139 2140 def get_or_make_index(self, arg: VariableT) -> int: 2141 """Returns the index of a variable, its negation, or a number.""" 2142 if isinstance(arg, IntVar): 2143 return arg.index 2144 if isinstance(arg, IntegralTypes): 2145 return self.get_or_make_index_from_constant(arg) 2146 raise TypeError( 2147 f"NotSupported: model.get_or_make_index({type(arg).__name__!r})" 2148 ) 2149 2150 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2151 """Returns an index from a boolean expression.""" 2152 if isinstance(arg, IntVar): 2153 self.assert_is_boolean_variable(arg) 2154 return arg.index 2155 if isinstance(arg, cmh.NotBooleanVariable): 2156 self.assert_is_boolean_variable(arg.negated()) 2157 return arg.index 2158 if isinstance(arg, IntegralTypes): 2159 if arg == ~int(False): 2160 return self.get_or_make_index_from_constant(1) 2161 if arg == ~int(True): 2162 return self.get_or_make_index_from_constant(0) 2163 arg = cmn.assert_is_zero_or_one(arg) 2164 return self.get_or_make_index_from_constant(arg) 2165 if cmn.is_boolean(arg): 2166 return self.get_or_make_index_from_constant(int(arg)) 2167 raise TypeError( 2168 "not supported:" f" model.get_or_make_boolean_index({type(arg).__name__!r})" 2169 ) 2170 2171 def get_interval_index(self, arg: IntervalVar) -> int: 2172 if not isinstance(arg, IntervalVar): 2173 raise TypeError( 2174 f"NotSupported: model.get_interval_index({type(arg).__name__!r})" 2175 ) 2176 return arg.index 2177 2178 def get_or_make_index_from_constant(self, value: IntegralT) -> int: 2179 if value in self.__constant_map: 2180 return self.__constant_map[value] 2181 constant_var = self.new_int_var(value, value, "") 2182 self.__constant_map[value] = constant_var.index 2183 return constant_var.index 2184 2185 def parse_linear_expression( 2186 self, linear_expr: LinearExprT, negate: bool = False 2187 ) -> cp_model_pb2.LinearExpressionProto: 2188 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2189 result: cp_model_pb2.LinearExpressionProto = ( 2190 cp_model_pb2.LinearExpressionProto() 2191 ) 2192 mult = -1 if negate else 1 2193 if isinstance(linear_expr, IntegralTypes): 2194 result.offset = int(linear_expr) * mult 2195 return result 2196 2197 # Raises TypeError if linear_expr is not an integer. 2198 flat_expr = cmh.FlatIntExpr(linear_expr) 2199 result.offset = flat_expr.offset * mult 2200 for var in flat_expr.vars: 2201 result.vars.append(var.index) 2202 for coeff in flat_expr.coeffs: 2203 result.coeffs.append(coeff * mult) 2204 return result 2205 2206 def _set_objective(self, obj: ObjLinearExprT, minimize: bool): 2207 """Sets the objective of the model.""" 2208 self.clear_objective() 2209 if isinstance(obj, IntegralTypes): 2210 self.__model.objective.offset = int(obj) 2211 self.__model.objective.scaling_factor = 1.0 2212 elif isinstance(obj, LinearExpr): 2213 if obj.is_integer(): 2214 int_obj = cmh.FlatIntExpr(obj) 2215 for var in int_obj.vars: 2216 self.__model.objective.vars.append(var.index) 2217 if minimize: 2218 self.__model.objective.scaling_factor = 1.0 2219 self.__model.objective.offset = int_obj.offset 2220 self.__model.objective.coeffs.extend(int_obj.coeffs) 2221 else: 2222 self.__model.objective.scaling_factor = -1.0 2223 self.__model.objective.offset = -int_obj.offset 2224 for c in int_obj.coeffs: 2225 self.__model.objective.coeffs.append(-c) 2226 else: 2227 float_obj = cmh.FlatFloatExpr(obj) 2228 for var in float_obj.vars: 2229 self.__model.floating_point_objective.vars.append(var.index) 2230 self.__model.floating_point_objective.coeffs.extend(float_obj.coeffs) 2231 self.__model.floating_point_objective.maximize = not minimize 2232 self.__model.floating_point_objective.offset = float_obj.offset 2233 else: 2234 raise TypeError( 2235 f"TypeError: {type(obj).__name__!r} is not a valid objective" 2236 ) 2237 2238 def minimize(self, obj: ObjLinearExprT): 2239 """Sets the objective of the model to minimize(obj).""" 2240 self._set_objective(obj, minimize=True) 2241 2242 def maximize(self, obj: ObjLinearExprT): 2243 """Sets the objective of the model to maximize(obj).""" 2244 self._set_objective(obj, minimize=False) 2245 2246 def has_objective(self) -> bool: 2247 return self.__model.HasField("objective") or self.__model.HasField( 2248 "floating_point_objective" 2249 ) 2250 2251 def clear_objective(self): 2252 self.__model.ClearField("objective") 2253 self.__model.ClearField("floating_point_objective") 2254 2255 def add_decision_strategy( 2256 self, 2257 variables: Sequence[IntVar], 2258 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2259 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2260 ) -> None: 2261 """Adds a search strategy to the model. 2262 2263 Args: 2264 variables: a list of variables this strategy will assign. 2265 var_strategy: heuristic to choose the next variable to assign. 2266 domain_strategy: heuristic to reduce the domain of the selected variable. 2267 Currently, this is advanced code: the union of all strategies added to 2268 the model must be complete, i.e. instantiates all variables. Otherwise, 2269 solve() will fail. 2270 """ 2271 2272 strategy: cp_model_pb2.DecisionStrategyProto = ( 2273 self.__model.search_strategy.add() 2274 ) 2275 for v in variables: 2276 expr = strategy.exprs.add() 2277 if v.index >= 0: 2278 expr.vars.append(v.index) 2279 expr.coeffs.append(1) 2280 else: 2281 expr.vars.append(self.negated(v.index)) 2282 expr.coeffs.append(-1) 2283 expr.offset = 1 2284 2285 strategy.variable_selection_strategy = var_strategy 2286 strategy.domain_reduction_strategy = domain_strategy 2287 2288 def model_stats(self) -> str: 2289 """Returns a string containing some model statistics.""" 2290 return cmh.CpSatHelper.model_stats(self.__model) 2291 2292 def validate(self) -> str: 2293 """Returns a string indicating that the model is invalid.""" 2294 return cmh.CpSatHelper.validate_model(self.__model) 2295 2296 def export_to_file(self, file: str) -> bool: 2297 """Write the model as a protocol buffer to 'file'. 2298 2299 Args: 2300 file: file to write the model to. If the filename ends with 'txt', the 2301 model will be written as a text file, otherwise, the binary format will 2302 be used. 2303 2304 Returns: 2305 True if the model was correctly written. 2306 """ 2307 return cmh.CpSatHelper.write_model_to_file(self.__model, file) 2308 2309 def remove_all_names(self) -> None: 2310 """Removes all names from the model.""" 2311 self.__model.ClearField("name") 2312 for v in self.__model.variables: 2313 v.ClearField("name") 2314 for c in self.__model.constraints: 2315 c.ClearField("name") 2316 2317 @overload 2318 def add_hint(self, var: IntVar, value: int) -> None: ... 2319 2320 @overload 2321 def add_hint(self, literal: BoolVarT, value: bool) -> None: ... 2322 2323 def add_hint(self, var, value) -> None: 2324 """Adds 'var == value' as a hint to the solver.""" 2325 if var.index >= 0: 2326 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2327 self.__model.solution_hint.values.append(int(value)) 2328 else: 2329 self.__model.solution_hint.vars.append(self.negated(var.index)) 2330 self.__model.solution_hint.values.append(int(not value)) 2331 2332 def clear_hints(self): 2333 """Removes any solution hint from the model.""" 2334 self.__model.ClearField("solution_hint") 2335 2336 def add_assumption(self, lit: LiteralT) -> None: 2337 """Adds the literal to the model as assumptions.""" 2338 self.__model.assumptions.append(self.get_or_make_boolean_index(lit)) 2339 2340 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2341 """Adds the literals to the model as assumptions.""" 2342 for lit in literals: 2343 self.add_assumption(lit) 2344 2345 def clear_assumptions(self) -> None: 2346 """Removes all assumptions from the model.""" 2347 self.__model.ClearField("assumptions") 2348 2349 # Helpers. 2350 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2351 if isinstance(x, IntVar): 2352 var = self.__model.variables[x.index] 2353 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2354 raise TypeError( 2355 f"TypeError: {type(x).__name__!r} is not a boolean variable" 2356 ) 2357 elif not isinstance(x, cmh.NotBooleanVariable): 2358 raise TypeError( 2359 f"TypeError: {type(x).__name__!r} is not a boolean variable" 2360 ) 2361 2362 # Compatibility with pre PEP8 2363 # pylint: disable=invalid-name 2364 2365 def Name(self) -> str: 2366 return self.name 2367 2368 def SetName(self, name: str) -> None: 2369 self.name = name 2370 2371 def Proto(self) -> cp_model_pb2.CpModelProto: 2372 return self.proto 2373 2374 NewIntVar = new_int_var 2375 NewIntVarFromDomain = new_int_var_from_domain 2376 NewBoolVar = new_bool_var 2377 NewConstant = new_constant 2378 NewIntVarSeries = new_int_var_series 2379 NewBoolVarSeries = new_bool_var_series 2380 AddLinearConstraint = add_linear_constraint 2381 AddLinearExpressionInDomain = add_linear_expression_in_domain 2382 Add = add 2383 AddAllDifferent = add_all_different 2384 AddElement = add_element 2385 AddCircuit = add_circuit 2386 AddMultipleCircuit = add_multiple_circuit 2387 AddAllowedAssignments = add_allowed_assignments 2388 AddForbiddenAssignments = add_forbidden_assignments 2389 AddAutomaton = add_automaton 2390 AddInverse = add_inverse 2391 AddReservoirConstraint = add_reservoir_constraint 2392 AddReservoirConstraintWithActive = add_reservoir_constraint_with_active 2393 AddImplication = add_implication 2394 AddBoolOr = add_bool_or 2395 AddAtLeastOne = add_at_least_one 2396 AddAtMostOne = add_at_most_one 2397 AddExactlyOne = add_exactly_one 2398 AddBoolAnd = add_bool_and 2399 AddBoolXOr = add_bool_xor 2400 AddMinEquality = add_min_equality 2401 AddMaxEquality = add_max_equality 2402 AddDivisionEquality = add_division_equality 2403 AddAbsEquality = add_abs_equality 2404 AddModuloEquality = add_modulo_equality 2405 AddMultiplicationEquality = add_multiplication_equality 2406 NewIntervalVar = new_interval_var 2407 NewIntervalVarSeries = new_interval_var_series 2408 NewFixedSizeIntervalVar = new_fixed_size_interval_var 2409 NewOptionalIntervalVar = new_optional_interval_var 2410 NewOptionalIntervalVarSeries = new_optional_interval_var_series 2411 NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var 2412 NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series 2413 AddNoOverlap = add_no_overlap 2414 AddNoOverlap2D = add_no_overlap_2d 2415 AddCumulative = add_cumulative 2416 Clone = clone 2417 GetBoolVarFromProtoIndex = get_bool_var_from_proto_index 2418 GetIntVarFromProtoIndex = get_int_var_from_proto_index 2419 GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index 2420 Minimize = minimize 2421 Maximize = maximize 2422 HasObjective = has_objective 2423 ClearObjective = clear_objective 2424 AddDecisionStrategy = add_decision_strategy 2425 ModelStats = model_stats 2426 Validate = validate 2427 ExportToFile = export_to_file 2428 AddHint = add_hint 2429 ClearHints = clear_hints 2430 AddAssumption = add_assumption 2431 AddAssumptions = add_assumptions 2432 ClearAssumptions = clear_assumptions 2433 2434 # pylint: enable=invalid-name 2435 2436 2437@overload 2438def expand_generator_or_tuple( 2439 args: Union[Tuple[LiteralT, ...], Iterable[LiteralT]], 2440) -> Union[Iterable[LiteralT], LiteralT]: ... 2441 2442 2443@overload 2444def expand_generator_or_tuple( 2445 args: Union[Tuple[LinearExprT, ...], Iterable[LinearExprT]], 2446) -> Union[Iterable[LinearExprT], LinearExprT]: ... 2447 2448 2449def expand_generator_or_tuple(args): 2450 if hasattr(args, "__len__"): # Tuple 2451 if len(args) != 1: 2452 return args 2453 if isinstance(args[0], (NumberTypes, LinearExpr)): 2454 return args 2455 # Generator 2456 return args[0] 2457 2458 2459class CpSolver: 2460 """Main solver class. 2461 2462 The purpose of this class is to search for a solution to the model provided 2463 to the solve() method. 2464 2465 Once solve() is called, this class allows inspecting the solution found 2466 with the value() and boolean_value() methods, as well as general statistics 2467 about the solve procedure. 2468 """ 2469 2470 def __init__(self) -> None: 2471 self.__response_wrapper: Optional[cmh.ResponseWrapper] = None 2472 self.parameters: sat_parameters_pb2.SatParameters = ( 2473 sat_parameters_pb2.SatParameters() 2474 ) 2475 self.log_callback: Optional[Callable[[str], None]] = None 2476 self.best_bound_callback: Optional[Callable[[float], None]] = None 2477 self.__solve_wrapper: Optional[cmh.SolveWrapper] = None 2478 self.__lock: threading.Lock = threading.Lock() 2479 2480 def solve( 2481 self, 2482 model: CpModel, 2483 solution_callback: Optional["CpSolverSolutionCallback"] = None, 2484 ) -> cp_model_pb2.CpSolverStatus: 2485 """Solves a problem and passes each solution to the callback if not null.""" 2486 with self.__lock: 2487 self.__solve_wrapper = cmh.SolveWrapper() 2488 2489 self.__solve_wrapper.set_parameters(self.parameters) 2490 if solution_callback is not None: 2491 self.__solve_wrapper.add_solution_callback(solution_callback) 2492 2493 if self.log_callback is not None: 2494 self.__solve_wrapper.add_log_callback(self.log_callback) 2495 2496 if self.best_bound_callback is not None: 2497 self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback) 2498 2499 self.__response_wrapper = ( 2500 self.__solve_wrapper.solve_and_return_response_wrapper(model.proto) 2501 ) 2502 2503 if solution_callback is not None: 2504 self.__solve_wrapper.clear_solution_callback(solution_callback) 2505 2506 with self.__lock: 2507 self.__solve_wrapper = None 2508 2509 return self.__response_wrapper.status() 2510 2511 def stop_search(self) -> None: 2512 """Stops the current search asynchronously.""" 2513 with self.__lock: 2514 if self.__solve_wrapper: 2515 self.__solve_wrapper.stop_search() 2516 2517 def value(self, expression: LinearExprT) -> int: 2518 """Returns the value of a linear expression after solve.""" 2519 return self._checked_response.value(expression) 2520 2521 def values(self, variables: _IndexOrSeries) -> pd.Series: 2522 """Returns the values of the input variables. 2523 2524 If `variables` is a `pd.Index`, then the output will be indexed by the 2525 variables. If `variables` is a `pd.Series` indexed by the underlying 2526 dimensions, then the output will be indexed by the same underlying 2527 dimensions. 2528 2529 Args: 2530 variables (Union[pd.Index, pd.Series]): The set of variables from which to 2531 get the values. 2532 2533 Returns: 2534 pd.Series: The values of all variables in the set. 2535 2536 Raises: 2537 RuntimeError: if solve() has not been called. 2538 """ 2539 if self.__response_wrapper is None: 2540 raise RuntimeError("solve() has not been called.") 2541 return pd.Series( 2542 data=[self.__response_wrapper.value(var) for var in variables], 2543 index=_get_index(variables), 2544 ) 2545 2546 def float_value(self, expression: LinearExprT) -> float: 2547 """Returns the value of a linear expression after solve.""" 2548 return self._checked_response.float_value(expression) 2549 2550 def float_values(self, expressions: _IndexOrSeries) -> pd.Series: 2551 """Returns the float values of the input linear expressions. 2552 2553 If `expressions` is a `pd.Index`, then the output will be indexed by the 2554 variables. If `variables` is a `pd.Series` indexed by the underlying 2555 dimensions, then the output will be indexed by the same underlying 2556 dimensions. 2557 2558 Args: 2559 expressions (Union[pd.Index, pd.Series]): The set of expressions from 2560 which to get the values. 2561 2562 Returns: 2563 pd.Series: The values of all variables in the set. 2564 2565 Raises: 2566 RuntimeError: if solve() has not been called. 2567 """ 2568 if self.__response_wrapper is None: 2569 raise RuntimeError("solve() has not been called.") 2570 return pd.Series( 2571 data=[self.__response_wrapper.float_value(expr) for expr in expressions], 2572 index=_get_index(expressions), 2573 ) 2574 2575 def boolean_value(self, literal: LiteralT) -> bool: 2576 """Returns the boolean value of a literal after solve.""" 2577 return self._checked_response.boolean_value(literal) 2578 2579 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 2580 """Returns the values of the input variables. 2581 2582 If `variables` is a `pd.Index`, then the output will be indexed by the 2583 variables. If `variables` is a `pd.Series` indexed by the underlying 2584 dimensions, then the output will be indexed by the same underlying 2585 dimensions. 2586 2587 Args: 2588 variables (Union[pd.Index, pd.Series]): The set of variables from which to 2589 get the values. 2590 2591 Returns: 2592 pd.Series: The values of all variables in the set. 2593 2594 Raises: 2595 RuntimeError: if solve() has not been called. 2596 """ 2597 if self.__response_wrapper is None: 2598 raise RuntimeError("solve() has not been called.") 2599 return pd.Series( 2600 data=[ 2601 self.__response_wrapper.boolean_value(literal) for literal in variables 2602 ], 2603 index=_get_index(variables), 2604 ) 2605 2606 @property 2607 def objective_value(self) -> float: 2608 """Returns the value of the objective after solve.""" 2609 return self._checked_response.objective_value() 2610 2611 @property 2612 def best_objective_bound(self) -> float: 2613 """Returns the best lower (upper) bound found when min(max)imizing.""" 2614 return self._checked_response.best_objective_bound() 2615 2616 @property 2617 def num_booleans(self) -> int: 2618 """Returns the number of boolean variables managed by the SAT solver.""" 2619 return self._checked_response.num_booleans() 2620 2621 @property 2622 def num_conflicts(self) -> int: 2623 """Returns the number of conflicts since the creation of the solver.""" 2624 return self._checked_response.num_conflicts() 2625 2626 @property 2627 def num_branches(self) -> int: 2628 """Returns the number of search branches explored by the solver.""" 2629 return self._checked_response.num_branches() 2630 2631 @property 2632 def wall_time(self) -> float: 2633 """Returns the wall time in seconds since the creation of the solver.""" 2634 return self._checked_response.wall_time() 2635 2636 @property 2637 def user_time(self) -> float: 2638 """Returns the user time in seconds since the creation of the solver.""" 2639 return self._checked_response.user_time() 2640 2641 @property 2642 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 2643 """Returns the response object.""" 2644 return self._checked_response.response() 2645 2646 def response_stats(self) -> str: 2647 """Returns some statistics on the solution found as a string.""" 2648 return self._checked_response.response_stats() 2649 2650 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 2651 """Returns the indices of the infeasible assumptions.""" 2652 return self._checked_response.sufficient_assumptions_for_infeasibility() 2653 2654 def status_name(self, status: Optional[Any] = None) -> str: 2655 """Returns the name of the status returned by solve().""" 2656 if status is None: 2657 status = self._checked_response.status() 2658 return cp_model_pb2.CpSolverStatus.Name(status) 2659 2660 def solution_info(self) -> str: 2661 """Returns some information on the solve process. 2662 2663 Returns some information on how the solution was found, or the reason 2664 why the model or the parameters are invalid. 2665 2666 Raises: 2667 RuntimeError: if solve() has not been called. 2668 """ 2669 return self._checked_response.solution_info() 2670 2671 @property 2672 def _checked_response(self) -> cmh.ResponseWrapper: 2673 """Checks solve() has been called, and returns a response wrapper.""" 2674 if self.__response_wrapper is None: 2675 raise RuntimeError("solve() has not been called.") 2676 return self.__response_wrapper 2677 2678 # Compatibility with pre PEP8 2679 # pylint: disable=invalid-name 2680 2681 def BestObjectiveBound(self) -> float: 2682 return self.best_objective_bound 2683 2684 def BooleanValue(self, literal: LiteralT) -> bool: 2685 return self.boolean_value(literal) 2686 2687 def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series: 2688 return self.boolean_values(variables) 2689 2690 def NumBooleans(self) -> int: 2691 return self.num_booleans 2692 2693 def NumConflicts(self) -> int: 2694 return self.num_conflicts 2695 2696 def NumBranches(self) -> int: 2697 return self.num_branches 2698 2699 def ObjectiveValue(self) -> float: 2700 return self.objective_value 2701 2702 def ResponseProto(self) -> cp_model_pb2.CpSolverResponse: 2703 return self.response_proto 2704 2705 def ResponseStats(self) -> str: 2706 return self.response_stats() 2707 2708 def Solve( 2709 self, 2710 model: CpModel, 2711 solution_callback: Optional["CpSolverSolutionCallback"] = None, 2712 ) -> cp_model_pb2.CpSolverStatus: 2713 return self.solve(model, solution_callback) 2714 2715 def SolutionInfo(self) -> str: 2716 return self.solution_info() 2717 2718 def StatusName(self, status: Optional[Any] = None) -> str: 2719 return self.status_name(status) 2720 2721 def StopSearch(self) -> None: 2722 self.stop_search() 2723 2724 def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]: 2725 return self.sufficient_assumptions_for_infeasibility() 2726 2727 def UserTime(self) -> float: 2728 return self.user_time 2729 2730 def Value(self, expression: LinearExprT) -> int: 2731 return self.value(expression) 2732 2733 def Values(self, variables: _IndexOrSeries) -> pd.Series: 2734 return self.values(variables) 2735 2736 def WallTime(self) -> float: 2737 return self.wall_time 2738 2739 def SolveWithSolutionCallback( 2740 self, model: CpModel, callback: "CpSolverSolutionCallback" 2741 ) -> cp_model_pb2.CpSolverStatus: 2742 """DEPRECATED Use solve() with the callback argument.""" 2743 warnings.warn( 2744 "solve_with_solution_callback is deprecated; use solve() with" 2745 + "the callback argument.", 2746 DeprecationWarning, 2747 ) 2748 return self.solve(model, callback) 2749 2750 def SearchForAllSolutions( 2751 self, model: CpModel, callback: "CpSolverSolutionCallback" 2752 ) -> cp_model_pb2.CpSolverStatus: 2753 """DEPRECATED Use solve() with the right parameter. 2754 2755 Search for all solutions of a satisfiability problem. 2756 2757 This method searches for all feasible solutions of a given model. 2758 Then it feeds the solution to the callback. 2759 2760 Note that the model cannot contain an objective. 2761 2762 Args: 2763 model: The model to solve. 2764 callback: The callback that will be called at each solution. 2765 2766 Returns: 2767 The status of the solve: 2768 2769 * *FEASIBLE* if some solutions have been found 2770 * *INFEASIBLE* if the solver has proved there are no solution 2771 * *OPTIMAL* if all solutions have been found 2772 """ 2773 warnings.warn( 2774 "search_for_all_solutions is deprecated; use solve() with" 2775 + "enumerate_all_solutions = True.", 2776 DeprecationWarning, 2777 ) 2778 if model.has_objective(): 2779 raise TypeError( 2780 "Search for all solutions is only defined on satisfiability problems" 2781 ) 2782 # Store old parameter. 2783 enumerate_all = self.parameters.enumerate_all_solutions 2784 self.parameters.enumerate_all_solutions = True 2785 2786 status: cp_model_pb2.CpSolverStatus = self.solve(model, callback) 2787 2788 # Restore parameter. 2789 self.parameters.enumerate_all_solutions = enumerate_all 2790 return status 2791 2792 2793# pylint: enable=invalid-name 2794 2795 2796class CpSolverSolutionCallback(cmh.SolutionCallback): 2797 """Solution callback. 2798 2799 This class implements a callback that will be called at each new solution 2800 found during search. 2801 2802 The method on_solution_callback() will be called by the solver, and must be 2803 implemented. The current solution can be queried using the boolean_value() 2804 and value() methods. 2805 2806 These methods returns the same information as their counterpart in the 2807 `CpSolver` class. 2808 """ 2809 2810 def __init__(self) -> None: 2811 cmh.SolutionCallback.__init__(self) 2812 2813 def OnSolutionCallback(self) -> None: 2814 """Proxy for the same method in snake case.""" 2815 self.on_solution_callback() 2816 2817 def boolean_value(self, lit: LiteralT) -> bool: 2818 """Returns the boolean value of a boolean literal. 2819 2820 Args: 2821 lit: A boolean variable or its negation. 2822 2823 Returns: 2824 The Boolean value of the literal in the solution. 2825 2826 Raises: 2827 RuntimeError: if `lit` is not a boolean variable or its negation. 2828 """ 2829 if not self.has_response(): 2830 raise RuntimeError("solve() has not been called.") 2831 return self.BooleanValue(lit) 2832 2833 def value(self, expression: LinearExprT) -> int: 2834 """Evaluates an linear expression in the current solution. 2835 2836 Args: 2837 expression: a linear expression of the model. 2838 2839 Returns: 2840 An integer value equal to the evaluation of the linear expression 2841 against the current solution. 2842 2843 Raises: 2844 RuntimeError: if 'expression' is not a LinearExpr. 2845 """ 2846 if not self.has_response(): 2847 raise RuntimeError("solve() has not been called.") 2848 return self.Value(expression) 2849 2850 def float_value(self, expression: LinearExprT) -> float: 2851 """Evaluates an linear expression in the current solution. 2852 2853 Args: 2854 expression: a linear expression of the model. 2855 2856 Returns: 2857 An integer value equal to the evaluation of the linear expression 2858 against the current solution. 2859 2860 Raises: 2861 RuntimeError: if 'expression' is not a LinearExpr. 2862 """ 2863 if not self.has_response(): 2864 raise RuntimeError("solve() has not been called.") 2865 return self.FloatValue(expression) 2866 2867 def has_response(self) -> bool: 2868 return self.HasResponse() 2869 2870 def stop_search(self) -> None: 2871 """Stops the current search asynchronously.""" 2872 if not self.has_response(): 2873 raise RuntimeError("solve() has not been called.") 2874 self.StopSearch() 2875 2876 @property 2877 def objective_value(self) -> float: 2878 """Returns the value of the objective after solve.""" 2879 if not self.has_response(): 2880 raise RuntimeError("solve() has not been called.") 2881 return self.ObjectiveValue() 2882 2883 @property 2884 def best_objective_bound(self) -> float: 2885 """Returns the best lower (upper) bound found when min(max)imizing.""" 2886 if not self.has_response(): 2887 raise RuntimeError("solve() has not been called.") 2888 return self.BestObjectiveBound() 2889 2890 @property 2891 def num_booleans(self) -> int: 2892 """Returns the number of boolean variables managed by the SAT solver.""" 2893 if not self.has_response(): 2894 raise RuntimeError("solve() has not been called.") 2895 return self.NumBooleans() 2896 2897 @property 2898 def num_conflicts(self) -> int: 2899 """Returns the number of conflicts since the creation of the solver.""" 2900 if not self.has_response(): 2901 raise RuntimeError("solve() has not been called.") 2902 return self.NumConflicts() 2903 2904 @property 2905 def num_branches(self) -> int: 2906 """Returns the number of search branches explored by the solver.""" 2907 if not self.has_response(): 2908 raise RuntimeError("solve() has not been called.") 2909 return self.NumBranches() 2910 2911 @property 2912 def num_integer_propagations(self) -> int: 2913 """Returns the number of integer propagations done by the solver.""" 2914 if not self.has_response(): 2915 raise RuntimeError("solve() has not been called.") 2916 return self.NumIntegerPropagations() 2917 2918 @property 2919 def num_boolean_propagations(self) -> int: 2920 """Returns the number of Boolean propagations done by the solver.""" 2921 if not self.has_response(): 2922 raise RuntimeError("solve() has not been called.") 2923 return self.NumBooleanPropagations() 2924 2925 @property 2926 def deterministic_time(self) -> float: 2927 """Returns the determistic time in seconds since the creation of the solver.""" 2928 if not self.has_response(): 2929 raise RuntimeError("solve() has not been called.") 2930 return self.DeterministicTime() 2931 2932 @property 2933 def wall_time(self) -> float: 2934 """Returns the wall time in seconds since the creation of the solver.""" 2935 if not self.has_response(): 2936 raise RuntimeError("solve() has not been called.") 2937 return self.WallTime() 2938 2939 @property 2940 def user_time(self) -> float: 2941 """Returns the user time in seconds since the creation of the solver.""" 2942 if not self.has_response(): 2943 raise RuntimeError("solve() has not been called.") 2944 return self.UserTime() 2945 2946 @property 2947 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 2948 """Returns the response object.""" 2949 if not self.has_response(): 2950 raise RuntimeError("solve() has not been called.") 2951 return self.Response() 2952 2953 2954class ObjectiveSolutionPrinter(CpSolverSolutionCallback): 2955 """Display the objective value and time of intermediate solutions.""" 2956 2957 def __init__(self) -> None: 2958 CpSolverSolutionCallback.__init__(self) 2959 self.__solution_count = 0 2960 self.__start_time = time.time() 2961 2962 def on_solution_callback(self) -> None: 2963 """Called on each new solution.""" 2964 current_time = time.time() 2965 obj = self.objective_value 2966 print( 2967 f"Solution {self.__solution_count}, time =" 2968 f" {current_time - self.__start_time:0.2f} s, objective = {obj}", 2969 flush=True, 2970 ) 2971 self.__solution_count += 1 2972 2973 def solution_count(self) -> int: 2974 """Returns the number of solutions found.""" 2975 return self.__solution_count 2976 2977 2978class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback): 2979 """Print intermediate solutions (objective, variable values, time).""" 2980 2981 def __init__(self, variables: Sequence[IntVar]) -> None: 2982 CpSolverSolutionCallback.__init__(self) 2983 self.__variables: Sequence[IntVar] = variables 2984 self.__solution_count: int = 0 2985 self.__start_time: float = time.time() 2986 2987 def on_solution_callback(self) -> None: 2988 """Called on each new solution.""" 2989 current_time = time.time() 2990 obj = self.objective_value 2991 print( 2992 f"Solution {self.__solution_count}, time =" 2993 f" {current_time - self.__start_time:0.2f} s, objective = {obj}" 2994 ) 2995 for v in self.__variables: 2996 print(f" {v} = {self.value(v)}", end=" ") 2997 print(flush=True) 2998 self.__solution_count += 1 2999 3000 @property 3001 def solution_count(self) -> int: 3002 """Returns the number of solutions found.""" 3003 return self.__solution_count 3004 3005 3006class VarArraySolutionPrinter(CpSolverSolutionCallback): 3007 """Print intermediate solutions (variable values, time).""" 3008 3009 def __init__(self, variables: Sequence[IntVar]) -> None: 3010 CpSolverSolutionCallback.__init__(self) 3011 self.__variables: Sequence[IntVar] = variables 3012 self.__solution_count: int = 0 3013 self.__start_time: float = time.time() 3014 3015 def on_solution_callback(self) -> None: 3016 """Called on each new solution.""" 3017 current_time = time.time() 3018 print( 3019 f"Solution {self.__solution_count}, time =" 3020 f" {current_time - self.__start_time:0.2f} s" 3021 ) 3022 for v in self.__variables: 3023 print(f" {v} = {self.value(v)}", end=" ") 3024 print(flush=True) 3025 self.__solution_count += 1 3026 3027 @property 3028 def solution_count(self) -> int: 3029 """Returns the number of solutions found.""" 3030 return self.__solution_count 3031 3032 3033def _get_index(obj: _IndexOrSeries) -> pd.Index: 3034 """Returns the indices of `obj` as a `pd.Index`.""" 3035 if isinstance(obj, pd.Series): 3036 return obj.index 3037 return obj 3038 3039 3040def _convert_to_integral_series_and_validate_index( 3041 value_or_series: Union[IntegralT, pd.Series], index: pd.Index 3042) -> pd.Series: 3043 """Returns a pd.Series of the given index with the corresponding values. 3044 3045 Args: 3046 value_or_series: the values to be converted (if applicable). 3047 index: the index of the resulting pd.Series. 3048 3049 Returns: 3050 pd.Series: The set of values with the given index. 3051 3052 Raises: 3053 TypeError: If the type of `value_or_series` is not recognized. 3054 ValueError: If the index does not match. 3055 """ 3056 if isinstance(value_or_series, IntegralTypes): 3057 return pd.Series(data=value_or_series, index=index) 3058 elif isinstance(value_or_series, pd.Series): 3059 if value_or_series.index.equals(index): 3060 return value_or_series 3061 else: 3062 raise ValueError("index does not match") 3063 else: 3064 raise TypeError(f"invalid type={type(value_or_series).__name__!r}") 3065 3066 3067def _convert_to_linear_expr_series_and_validate_index( 3068 value_or_series: Union[LinearExprT, pd.Series], index: pd.Index 3069) -> pd.Series: 3070 """Returns a pd.Series of the given index with the corresponding values. 3071 3072 Args: 3073 value_or_series: the values to be converted (if applicable). 3074 index: the index of the resulting pd.Series. 3075 3076 Returns: 3077 pd.Series: The set of values with the given index. 3078 3079 Raises: 3080 TypeError: If the type of `value_or_series` is not recognized. 3081 ValueError: If the index does not match. 3082 """ 3083 if isinstance(value_or_series, IntegralTypes): 3084 return pd.Series(data=value_or_series, index=index) 3085 elif isinstance(value_or_series, pd.Series): 3086 if value_or_series.index.equals(index): 3087 return value_or_series 3088 else: 3089 raise ValueError("index does not match") 3090 else: 3091 raise TypeError(f"invalid type={type(value_or_series).__name__!r}") 3092 3093 3094def _convert_to_literal_series_and_validate_index( 3095 value_or_series: Union[LiteralT, pd.Series], index: pd.Index 3096) -> pd.Series: 3097 """Returns a pd.Series of the given index with the corresponding values. 3098 3099 Args: 3100 value_or_series: the values to be converted (if applicable). 3101 index: the index of the resulting pd.Series. 3102 3103 Returns: 3104 pd.Series: The set of values with the given index. 3105 3106 Raises: 3107 TypeError: If the type of `value_or_series` is not recognized. 3108 ValueError: If the index does not match. 3109 """ 3110 if isinstance(value_or_series, IntegralTypes): 3111 return pd.Series(data=value_or_series, index=index) 3112 elif isinstance(value_or_series, pd.Series): 3113 if value_or_series.index.equals(index): 3114 return value_or_series 3115 else: 3116 raise ValueError("index does not match") 3117 else: 3118 raise TypeError(f"invalid type={type(value_or_series).__name__!r}")
We call domain any subset of Int64 = [kint64min, kint64max].
This class can be used to represent such set efficiently as a sorted and non-adjacent list of intervals. This is efficient as long as the size of such list stays reasonable.
In the comments below, the domain of *this will always be written 'D'. Note that all the functions are safe with respect to integer overflow.
__init__(self: ortools.util.python.sorted_interval_list.Domain, arg0: int, arg1: int) -> None
By default, Domain will be empty.
all_values() -> ortools.util.python.sorted_interval_list.Domain
Returns the full domain Int64.
from_values(values: list[int]) -> ortools.util.python.sorted_interval_list.Domain
Creates a domain from the union of an unsorted list of integer values. Input values may be repeated, with no consequence on the output
from_intervals(intervals: list[list[int]]) -> ortools.util.python.sorted_interval_list.Domain
This method is available in Python, Java and .NET. It allows building a Domain object from a list of intervals (long[][] in Java and .NET, [[0, 2], [5, 5], [8, 10]] in python).
from_flat_intervals(flat_intervals: list[int]) -> ortools.util.python.sorted_interval_list.Domain
This method is available in Python, Java and .NET. It allows building a Domain object from a flattened list of intervals (long[] in Java and .NET, [0, 2, 5, 5, 8, 10] in python).
addition_with(self: ortools.util.python.sorted_interval_list.Domain, domain: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns {x ∈ Int64, ∃ a ∈ D, ∃ b ∈ domain, x = a + b}.
complement(self: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns the set Int64 ∖ D.
contains(self: ortools.util.python.sorted_interval_list.Domain, value: int) -> bool
Returns true iff value is in Domain.
flattened_intervals(self: ortools.util.python.sorted_interval_list.Domain) -> list[int]
This method returns the flattened list of interval bounds of the domain.
Thus the domain {0, 1, 2, 5, 8, 9, 10} will return [0, 2, 5, 5, 8, 10]
(as a C++ std::vector
intersection_with(self: ortools.util.python.sorted_interval_list.Domain, domain: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns the intersection of D and domain.
is_empty(self: ortools.util.python.sorted_interval_list.Domain) -> bool
Returns true if this is the empty set.
size(self: ortools.util.python.sorted_interval_list.Domain) -> int
Returns the number of elements in the domain. It is capped at kint64max
max(self: ortools.util.python.sorted_interval_list.Domain) -> int
Returns the max value of the domain. The domain must not be empty.
min(self: ortools.util.python.sorted_interval_list.Domain) -> int
Returns the min value of the domain. The domain must not be empty.
negation(self: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns {x ∈ Int64, ∃ e ∈ D, x = -e}.
Note in particular that if the negation of Int64 is not Int64 but Int64 \ {kint64min} !!
union_with(self: ortools.util.python.sorted_interval_list.Domain, domain: ortools.util.python.sorted_interval_list.Domain) -> ortools.util.python.sorted_interval_list.Domain
Returns the union of D and domain.
AllValues() -> ortools.util.python.sorted_interval_list.Domain
Returns the full domain Int64.
FromValues(values: list[int]) -> ortools.util.python.sorted_interval_list.Domain
Creates a domain from the union of an unsorted list of integer values. Input values may be repeated, with no consequence on the output
FromIntervals(intervals: list[list[int]]) -> ortools.util.python.sorted_interval_list.Domain
This method is available in Python, Java and .NET. It allows building a Domain object from a list of intervals (long[][] in Java and .NET, [[0, 2], [5, 5], [8, 10]] in python).
FromFlatIntervals(flat_intervals: list[int]) -> ortools.util.python.sorted_interval_list.Domain
This method is available in Python, Java and .NET. It allows building a Domain object from a flattened list of intervals (long[] in Java and .NET, [0, 2, 5, 5, 8, 10] in python).
FlattenedIntervals(self: ortools.util.python.sorted_interval_list.Domain) -> list[int]
This method returns the flattened list of interval bounds of the domain.
Thus the domain {0, 1, 2, 5, 8, 9, 10} will return [0, 2, 5, 5, 8, 10]
(as a C++ std::vector
A class to hold a linear expression with bounds.
__init__(args, *kwargs) Overloaded function.
__init__(self: ortools.sat.python.cp_model_helper.BoundedLinearExpression, arg0: ortools.sat.python.cp_model_helper.LinearExpr, arg1: ortools.util.python.sorted_interval_list.Domain) -> None
__init__(self: ortools.sat.python.cp_model_helper.BoundedLinearExpression, arg0: ortools.sat.python.cp_model_helper.LinearExpr, arg1: ortools.sat.python.cp_model_helper.LinearExpr, arg2: ortools.util.python.sorted_interval_list.Domain) -> None
A flattened and optimized floating point linear expression.
It can be used to cache complex expressions as parsing them is only done once.
__init__(self: ortools.sat.python.cp_model_helper.FlatFloatExpr, arg0: ortools.sat.python.cp_model_helper.LinearExpr) -> None
(arg0: ortools.sat.python.cp_model_helper.FlatFloatExpr) -> list[operations_research::sat::python::BaseIntVar]
Inherited Members
A flattened and optimized integer linear expression.
It can be used to cache complex expressions as parsing them is only done once.
__init__(self: ortools.sat.python.cp_model_helper.FlatIntExpr, arg0: ortools.sat.python.cp_model_helper.LinearExpr) -> None
(arg0: ortools.sat.python.cp_model_helper.FlatIntExpr) -> list[operations_research::sat::python::BaseIntVar]
Inherited Members
A class to hold an integer or floating point linear expression.
A linear expression is built from (integer or floating point)
constants and variables. For example, x + 2 * (y - z + 1)
.
Linear expressions are used in CP-SAT models in constraints and in the objective.
Note that constraints only accept linear expressions with integral coefficients and constants. On the other hand, The objective can be a linear expression with floating point coefficients and constants.
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)
sum(*args) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns the sum(expressions).
weighted_sum(expressions: Sequence, coefficients: Sequence) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns the sum of (expressions[i] * coefficients[i])
term(args, *kwargs) Overloaded function.
- term(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: int) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns expr * coeff.
- term(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: float) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns expr * coeff.
affine(args, *kwargs) Overloaded function.
- affine(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: int, offset: int) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns expr * coeff + offset.
- affine(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: float, offset: float) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns expr * coeff + offset.
constant(args, *kwargs) Overloaded function.
- constant(value: int) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns a new LinearExpr that is the given constant.
- constant(value: float) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns a new LinearExpr that is the given constant.
WeightedSum(expressions: Sequence, coefficients: Sequence) -> ortools.sat.python.cp_model_helper.LinearExpr
Term(args, *kwargs) Overloaded function.
- Term(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: int) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns expr * coeff.
- Term(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: float) -> ortools.sat.python.cp_model_helper.LinearExpr
Returns expr * coeff.
A class to hold a negated variable index.
negated(self: ortools.sat.python.cp_model_helper.NotBooleanVariable) -> ortools.sat.python.cp_model_helper.Literal
Returns the negation of the current literal, that is the original Boolean variable.
Not(self: ortools.sat.python.cp_model_helper.NotBooleanVariable) -> ortools.sat.python.cp_model_helper.Literal
Returns the negation of the current literal, that is the original Boolean variable.
174def display_bounds(bounds: Sequence[int]) -> str: 175 """Displays a flattened list of intervals.""" 176 out = "" 177 for i in range(0, len(bounds), 2): 178 if i != 0: 179 out += ", " 180 if bounds[i] == bounds[i + 1]: 181 out += str(bounds[i]) 182 else: 183 out += str(bounds[i]) + ".." + str(bounds[i + 1]) 184 return out
Displays a flattened list of intervals.
187def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str: 188 """Returns a short name of an integer variable, or its negation.""" 189 if i < 0: 190 return f"not({short_name(model, -i - 1)})" 191 v = model.variables[i] 192 if v.name: 193 return v.name 194 elif len(v.domain) == 2 and v.domain[0] == v.domain[1]: 195 return str(v.domain[0]) 196 else: 197 return f"[{display_bounds(v.domain)}]"
Returns a short name of an integer variable, or its negation.
200def short_expr_name( 201 model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto 202) -> str: 203 """Pretty-print LinearExpressionProto instances.""" 204 if not e.vars: 205 return str(e.offset) 206 if len(e.vars) == 1: 207 var_name = short_name(model, e.vars[0]) 208 coeff = e.coeffs[0] 209 result = "" 210 if coeff == 1: 211 result = var_name 212 elif coeff == -1: 213 result = f"-{var_name}" 214 elif coeff != 0: 215 result = f"{coeff} * {var_name}" 216 if e.offset > 0: 217 result = f"{result} + {e.offset}" 218 elif e.offset < 0: 219 result = f"{result} - {-e.offset}" 220 return result 221 # TODO(user): Support more than affine expressions. 222 return str(e)
Pretty-print LinearExpressionProto instances.
225class IntVar(cmh.BaseIntVar): 226 """An integer variable. 227 228 An IntVar is an object that can take on any integer value within defined 229 ranges. Variables appear in constraint like: 230 231 x + y >= 5 232 AllDifferent([x, y, z]) 233 234 Solving a model is equivalent to finding, for each variable, a single value 235 from the set of initial values (called the initial domain), such that the 236 model is feasible, or optimal if you provided an objective function. 237 """ 238 239 def __init__( 240 self, 241 model: cp_model_pb2.CpModelProto, 242 domain: Union[int, sorted_interval_list.Domain], 243 is_boolean: bool, 244 name: Optional[str], 245 ) -> None: 246 """See CpModel.new_int_var below.""" 247 self.__model: cp_model_pb2.CpModelProto = model 248 # Python do not support multiple __init__ methods. 249 # This method is only called from the CpModel class. 250 # We hack the parameter to support the two cases: 251 # case 1: 252 # model is a CpModelProto, domain is a Domain, and name is a string. 253 # case 2: 254 # model is a CpModelProto, domain is an index (int), and name is None. 255 if isinstance(domain, IntegralTypes) and name is None: 256 cmh.BaseIntVar.__init__(self, int(domain), is_boolean) 257 else: 258 cmh.BaseIntVar.__init__(self, len(model.variables), is_boolean) 259 proto: cp_model_pb2.IntegerVariableProto = self.__model.variables.add() 260 proto.domain.extend( 261 cast(sorted_interval_list.Domain, domain).flattened_intervals() 262 ) 263 if name is not None: 264 proto.name = name 265 266 def __copy__(self) -> "IntVar": 267 """Returns a shallowcopy of the variable.""" 268 return IntVar(self.__model, self.index, self.is_boolean, None) 269 270 def __deepcopy__(self, memo: Any) -> "IntVar": 271 """Returns a deepcopy of the variable.""" 272 return IntVar( 273 copy.deepcopy(self.__model, memo), self.index, self.is_boolean, None 274 ) 275 276 @property 277 def proto(self) -> cp_model_pb2.IntegerVariableProto: 278 """Returns the variable protobuf.""" 279 return self.__model.variables[self.index] 280 281 @property 282 def model_proto(self) -> cp_model_pb2.CpModelProto: 283 """Returns the model protobuf.""" 284 return self.__model 285 286 def is_equal_to(self, other: Any) -> bool: 287 """Returns true if self == other in the python sense.""" 288 if not isinstance(other, IntVar): 289 return False 290 return self.index == other.index 291 292 def __str__(self) -> str: 293 if not self.proto.name: 294 if ( 295 len(self.proto.domain) == 2 296 and self.proto.domain[0] == self.proto.domain[1] 297 ): 298 # Special case for constants. 299 return str(self.proto.domain[0]) 300 elif self.is_boolean: 301 return f"BooleanVar({self.__index})" 302 else: 303 return f"IntVar({self.__index})" 304 else: 305 return self.proto.name 306 307 def __repr__(self) -> str: 308 return f"{self}({display_bounds(self.proto.domain)})" 309 310 @property 311 def name(self) -> str: 312 if not self.proto or not self.proto.name: 313 return "" 314 return self.proto.name 315 316 # Pre PEP8 compatibility. 317 # pylint: disable=invalid-name 318 def Name(self) -> str: 319 return self.name 320 321 def Proto(self) -> cp_model_pb2.IntegerVariableProto: 322 return self.proto 323 324 # 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.
239 def __init__( 240 self, 241 model: cp_model_pb2.CpModelProto, 242 domain: Union[int, sorted_interval_list.Domain], 243 is_boolean: bool, 244 name: Optional[str], 245 ) -> None: 246 """See CpModel.new_int_var below.""" 247 self.__model: cp_model_pb2.CpModelProto = model 248 # Python do not support multiple __init__ methods. 249 # This method is only called from the CpModel class. 250 # We hack the parameter to support the two cases: 251 # case 1: 252 # model is a CpModelProto, domain is a Domain, and name is a string. 253 # case 2: 254 # model is a CpModelProto, domain is an index (int), and name is None. 255 if isinstance(domain, IntegralTypes) and name is None: 256 cmh.BaseIntVar.__init__(self, int(domain), is_boolean) 257 else: 258 cmh.BaseIntVar.__init__(self, len(model.variables), is_boolean) 259 proto: cp_model_pb2.IntegerVariableProto = self.__model.variables.add() 260 proto.domain.extend( 261 cast(sorted_interval_list.Domain, domain).flattened_intervals() 262 ) 263 if name is not None: 264 proto.name = name
See CpModel.new_int_var below.
276 @property 277 def proto(self) -> cp_model_pb2.IntegerVariableProto: 278 """Returns the variable protobuf.""" 279 return self.__model.variables[self.index]
Returns the variable protobuf.
281 @property 282 def model_proto(self) -> cp_model_pb2.CpModelProto: 283 """Returns the model protobuf.""" 284 return self.__model
Returns the model protobuf.
286 def is_equal_to(self, other: Any) -> bool: 287 """Returns true if self == other in the python sense.""" 288 if not isinstance(other, IntVar): 289 return False 290 return self.index == other.index
Returns true if self == other in the python sense.
327class Constraint: 328 """Base class for constraints. 329 330 Constraints are built by the CpModel through the add<XXX> methods. 331 Once created by the CpModel class, they are automatically added to the model. 332 The purpose of this class is to allow specification of enforcement literals 333 for this constraint. 334 335 b = model.new_bool_var('b') 336 x = model.new_int_var(0, 10, 'x') 337 y = model.new_int_var(0, 10, 'y') 338 339 model.add(x + 2 * y == 5).only_enforce_if(b.negated()) 340 """ 341 342 def __init__( 343 self, 344 cp_model: "CpModel", 345 ) -> None: 346 self.__index: int = len(cp_model.proto.constraints) 347 self.__cp_model: "CpModel" = cp_model 348 self.__constraint: cp_model_pb2.ConstraintProto = ( 349 cp_model.proto.constraints.add() 350 ) 351 352 @overload 353 def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": ... 354 355 @overload 356 def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": ... 357 358 def only_enforce_if(self, *boolvar) -> "Constraint": 359 """Adds an enforcement literal to the constraint. 360 361 This method adds one or more literals (that is, a boolean variable or its 362 negation) as enforcement literals. The conjunction of all these literals 363 determines whether the constraint is active or not. It acts as an 364 implication, so if the conjunction is true, it implies that the constraint 365 must be enforced. If it is false, then the constraint is ignored. 366 367 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 368 369 Args: 370 *boolvar: One or more Boolean literals. 371 372 Returns: 373 self. 374 """ 375 for lit in expand_generator_or_tuple(boolvar): 376 if (cmn.is_boolean(lit) and lit) or ( 377 isinstance(lit, IntegralTypes) and lit == 1 378 ): 379 # Always true. Do nothing. 380 pass 381 elif (cmn.is_boolean(lit) and not lit) or ( 382 isinstance(lit, IntegralTypes) and lit == 0 383 ): 384 self.__constraint.enforcement_literal.append( 385 self.__cp_model.new_constant(0).index 386 ) 387 else: 388 self.__constraint.enforcement_literal.append( 389 cast(cmh.Literal, lit).index 390 ) 391 return self 392 393 def with_name(self, name: str) -> "Constraint": 394 """Sets the name of the constraint.""" 395 if name: 396 self.__constraint.name = name 397 else: 398 self.__constraint.ClearField("name") 399 return self 400 401 @property 402 def name(self) -> str: 403 """Returns the name of the constraint.""" 404 if not self.__constraint or not self.__constraint.name: 405 return "" 406 return self.__constraint.name 407 408 @property 409 def index(self) -> int: 410 """Returns the index of the constraint in the model.""" 411 return self.__index 412 413 @property 414 def proto(self) -> cp_model_pb2.ConstraintProto: 415 """Returns the constraint protobuf.""" 416 return self.__constraint 417 418 # Pre PEP8 compatibility. 419 # pylint: disable=invalid-name 420 OnlyEnforceIf = only_enforce_if 421 WithName = with_name 422 423 def Name(self) -> str: 424 return self.name 425 426 def Index(self) -> int: 427 return self.index 428 429 def Proto(self) -> cp_model_pb2.ConstraintProto: 430 return self.proto 431 432 # 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())
358 def only_enforce_if(self, *boolvar) -> "Constraint": 359 """Adds an enforcement literal to the constraint. 360 361 This method adds one or more literals (that is, a boolean variable or its 362 negation) as enforcement literals. The conjunction of all these literals 363 determines whether the constraint is active or not. It acts as an 364 implication, so if the conjunction is true, it implies that the constraint 365 must be enforced. If it is false, then the constraint is ignored. 366 367 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 368 369 Args: 370 *boolvar: One or more Boolean literals. 371 372 Returns: 373 self. 374 """ 375 for lit in expand_generator_or_tuple(boolvar): 376 if (cmn.is_boolean(lit) and lit) or ( 377 isinstance(lit, IntegralTypes) and lit == 1 378 ): 379 # Always true. Do nothing. 380 pass 381 elif (cmn.is_boolean(lit) and not lit) or ( 382 isinstance(lit, IntegralTypes) and lit == 0 383 ): 384 self.__constraint.enforcement_literal.append( 385 self.__cp_model.new_constant(0).index 386 ) 387 else: 388 self.__constraint.enforcement_literal.append( 389 cast(cmh.Literal, lit).index 390 ) 391 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.
393 def with_name(self, name: str) -> "Constraint": 394 """Sets the name of the constraint.""" 395 if name: 396 self.__constraint.name = name 397 else: 398 self.__constraint.ClearField("name") 399 return self
Sets the name of the constraint.
401 @property 402 def name(self) -> str: 403 """Returns the name of the constraint.""" 404 if not self.__constraint or not self.__constraint.name: 405 return "" 406 return self.__constraint.name
Returns the name of the constraint.
408 @property 409 def index(self) -> int: 410 """Returns the index of the constraint in the model.""" 411 return self.__index
Returns the index of the constraint in the model.
413 @property 414 def proto(self) -> cp_model_pb2.ConstraintProto: 415 """Returns the constraint protobuf.""" 416 return self.__constraint
Returns the constraint protobuf.
358 def only_enforce_if(self, *boolvar) -> "Constraint": 359 """Adds an enforcement literal to the constraint. 360 361 This method adds one or more literals (that is, a boolean variable or its 362 negation) as enforcement literals. The conjunction of all these literals 363 determines whether the constraint is active or not. It acts as an 364 implication, so if the conjunction is true, it implies that the constraint 365 must be enforced. If it is false, then the constraint is ignored. 366 367 BoolOr, BoolAnd, and linear constraints all support enforcement literals. 368 369 Args: 370 *boolvar: One or more Boolean literals. 371 372 Returns: 373 self. 374 """ 375 for lit in expand_generator_or_tuple(boolvar): 376 if (cmn.is_boolean(lit) and lit) or ( 377 isinstance(lit, IntegralTypes) and lit == 1 378 ): 379 # Always true. Do nothing. 380 pass 381 elif (cmn.is_boolean(lit) and not lit) or ( 382 isinstance(lit, IntegralTypes) and lit == 0 383 ): 384 self.__constraint.enforcement_literal.append( 385 self.__cp_model.new_constant(0).index 386 ) 387 else: 388 self.__constraint.enforcement_literal.append( 389 cast(cmh.Literal, lit).index 390 ) 391 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.
393 def with_name(self, name: str) -> "Constraint": 394 """Sets the name of the constraint.""" 395 if name: 396 self.__constraint.name = name 397 else: 398 self.__constraint.ClearField("name") 399 return self
Sets the name of the constraint.
435class VariableList: 436 """Stores all integer variables of the model.""" 437 438 def __init__(self) -> None: 439 self.__var_list: list[IntVar] = [] 440 441 def append(self, var: IntVar) -> None: 442 assert var.index == len(self.__var_list) 443 self.__var_list.append(var) 444 445 def get(self, index: int) -> IntVar: 446 if index < 0 or index >= len(self.__var_list): 447 raise ValueError("Index out of bounds.") 448 return self.__var_list[index] 449 450 def rebuild_expr( 451 self, 452 proto: cp_model_pb2.LinearExpressionProto, 453 ) -> LinearExprT: 454 """Recreate a LinearExpr from a LinearExpressionProto.""" 455 num_elements = len(proto.vars) 456 if num_elements == 0: 457 return proto.offset 458 elif num_elements == 1: 459 var = self.get(proto.vars[0]) 460 return LinearExpr.affine( 461 var, proto.coeffs[0], proto.offset 462 ) # pytype: disable=bad-return-type 463 else: 464 variables = [] 465 for var_index in range(len(proto.vars)): 466 var = self.get(var_index) 467 variables.append(var) 468 if proto.offset != 0: 469 coeffs = [] 470 coeffs.extend(proto.coeffs) 471 coeffs.append(1) 472 variables.append(proto.offset) 473 return LinearExpr.weighted_sum(variables, coeffs) 474 else: 475 return LinearExpr.weighted_sum(variables, proto.coeffs)
Stores all integer variables of the model.
450 def rebuild_expr( 451 self, 452 proto: cp_model_pb2.LinearExpressionProto, 453 ) -> LinearExprT: 454 """Recreate a LinearExpr from a LinearExpressionProto.""" 455 num_elements = len(proto.vars) 456 if num_elements == 0: 457 return proto.offset 458 elif num_elements == 1: 459 var = self.get(proto.vars[0]) 460 return LinearExpr.affine( 461 var, proto.coeffs[0], proto.offset 462 ) # pytype: disable=bad-return-type 463 else: 464 variables = [] 465 for var_index in range(len(proto.vars)): 466 var = self.get(var_index) 467 variables.append(var) 468 if proto.offset != 0: 469 coeffs = [] 470 coeffs.extend(proto.coeffs) 471 coeffs.append(1) 472 variables.append(proto.offset) 473 return LinearExpr.weighted_sum(variables, coeffs) 474 else: 475 return LinearExpr.weighted_sum(variables, proto.coeffs)
Recreate a LinearExpr from a LinearExpressionProto.
478class IntervalVar: 479 """Represents an Interval variable. 480 481 An interval variable is both a constraint and a variable. It is defined by 482 three integer variables: start, size, and end. 483 484 It is a constraint because, internally, it enforces that start + size == end. 485 486 It is also a variable as it can appear in specific scheduling constraints: 487 NoOverlap, NoOverlap2D, Cumulative. 488 489 Optionally, an enforcement literal can be added to this constraint, in which 490 case these scheduling constraints will ignore interval variables with 491 enforcement literals assigned to false. Conversely, these constraints will 492 also set these enforcement literals to false if they cannot fit these 493 intervals into the schedule. 494 495 Raises: 496 ValueError: if start, size, end are not defined, or have the wrong type. 497 """ 498 499 def __init__( 500 self, 501 model: cp_model_pb2.CpModelProto, 502 var_list: VariableList, 503 start: Union[cp_model_pb2.LinearExpressionProto, int], 504 size: Optional[cp_model_pb2.LinearExpressionProto], 505 end: Optional[cp_model_pb2.LinearExpressionProto], 506 is_present_index: Optional[int], 507 name: Optional[str], 508 ) -> None: 509 self.__model: cp_model_pb2.CpModelProto = model 510 self.__var_list: VariableList = var_list 511 self.__index: int 512 self.__ct: cp_model_pb2.ConstraintProto 513 # As with the IntVar::__init__ method, we hack the __init__ method to 514 # support two use cases: 515 # case 1: called when creating a new interval variable. 516 # {start|size|end} are linear expressions, is_present_index is either 517 # None or the index of a Boolean literal. name is a string 518 # case 2: called when querying an existing interval variable. 519 # start_index is an int, all parameters after are None. 520 if isinstance(start, int): 521 if size is not None: 522 raise ValueError("size should be None") 523 if end is not None: 524 raise ValueError("end should be None") 525 if is_present_index is not None: 526 raise ValueError("is_present_index should be None") 527 self.__index = cast(int, start) 528 self.__ct = model.constraints[self.__index] 529 else: 530 self.__index = len(model.constraints) 531 self.__ct = self.__model.constraints.add() 532 if start is None: 533 raise TypeError("start is not defined") 534 self.__ct.interval.start.CopyFrom(start) 535 if size is None: 536 raise TypeError("size is not defined") 537 self.__ct.interval.size.CopyFrom(size) 538 if end is None: 539 raise TypeError("end is not defined") 540 self.__ct.interval.end.CopyFrom(end) 541 if is_present_index is not None: 542 self.__ct.enforcement_literal.append(is_present_index) 543 if name: 544 self.__ct.name = name 545 546 @property 547 def index(self) -> int: 548 """Returns the index of the interval constraint in the model.""" 549 return self.__index 550 551 @property 552 def proto(self) -> cp_model_pb2.ConstraintProto: 553 """Returns the interval protobuf.""" 554 return self.__model.constraints[self.__index] 555 556 @property 557 def model_proto(self) -> cp_model_pb2.CpModelProto: 558 """Returns the model protobuf.""" 559 return self.__model 560 561 def __str__(self): 562 return self.proto.name 563 564 def __repr__(self): 565 interval = self.proto.interval 566 if self.proto.enforcement_literal: 567 return ( 568 f"{self.proto.name}(start =" 569 f" {short_expr_name(self.__model, interval.start)}, size =" 570 f" {short_expr_name(self.__model, interval.size)}, end =" 571 f" {short_expr_name(self.__model, interval.end)}, is_present =" 572 f" {short_name(self.__model, self.proto.enforcement_literal[0])})" 573 ) 574 else: 575 return ( 576 f"{self.proto.name}(start =" 577 f" {short_expr_name(self.__model, interval.start)}, size =" 578 f" {short_expr_name(self.__model, interval.size)}, end =" 579 f" {short_expr_name(self.__model, interval.end)})" 580 ) 581 582 @property 583 def name(self) -> str: 584 if not self.proto or not self.proto.name: 585 return "" 586 return self.proto.name 587 588 def start_expr(self) -> LinearExprT: 589 return self.__var_list.rebuild_expr(self.proto.interval.start) 590 591 def size_expr(self) -> LinearExprT: 592 return self.__var_list.rebuild_expr(self.proto.interval.size) 593 594 def end_expr(self) -> LinearExprT: 595 return self.__var_list.rebuild_expr(self.proto.interval.end) 596 597 # Pre PEP8 compatibility. 598 # pylint: disable=invalid-name 599 def Name(self) -> str: 600 return self.name 601 602 def Index(self) -> int: 603 return self.index 604 605 def Proto(self) -> cp_model_pb2.ConstraintProto: 606 return self.proto 607 608 StartExpr = start_expr 609 SizeExpr = size_expr 610 EndExpr = end_expr 611 612 # pylint: enable=invalid-name
Represents an Interval variable.
An interval variable is both a constraint and a variable. It is defined by three integer variables: start, size, and end.
It is a constraint because, internally, it enforces that start + size == end.
It is also a variable as it can appear in specific scheduling constraints: NoOverlap, NoOverlap2D, Cumulative.
Optionally, an enforcement literal can be added to this constraint, in which case these scheduling constraints will ignore interval variables with enforcement literals assigned to false. Conversely, these constraints will also set these enforcement literals to false if they cannot fit these intervals into the schedule.
Raises:
- ValueError: if start, size, end are not defined, or have the wrong type.
499 def __init__( 500 self, 501 model: cp_model_pb2.CpModelProto, 502 var_list: VariableList, 503 start: Union[cp_model_pb2.LinearExpressionProto, int], 504 size: Optional[cp_model_pb2.LinearExpressionProto], 505 end: Optional[cp_model_pb2.LinearExpressionProto], 506 is_present_index: Optional[int], 507 name: Optional[str], 508 ) -> None: 509 self.__model: cp_model_pb2.CpModelProto = model 510 self.__var_list: VariableList = var_list 511 self.__index: int 512 self.__ct: cp_model_pb2.ConstraintProto 513 # As with the IntVar::__init__ method, we hack the __init__ method to 514 # support two use cases: 515 # case 1: called when creating a new interval variable. 516 # {start|size|end} are linear expressions, is_present_index is either 517 # None or the index of a Boolean literal. name is a string 518 # case 2: called when querying an existing interval variable. 519 # start_index is an int, all parameters after are None. 520 if isinstance(start, int): 521 if size is not None: 522 raise ValueError("size should be None") 523 if end is not None: 524 raise ValueError("end should be None") 525 if is_present_index is not None: 526 raise ValueError("is_present_index should be None") 527 self.__index = cast(int, start) 528 self.__ct = model.constraints[self.__index] 529 else: 530 self.__index = len(model.constraints) 531 self.__ct = self.__model.constraints.add() 532 if start is None: 533 raise TypeError("start is not defined") 534 self.__ct.interval.start.CopyFrom(start) 535 if size is None: 536 raise TypeError("size is not defined") 537 self.__ct.interval.size.CopyFrom(size) 538 if end is None: 539 raise TypeError("end is not defined") 540 self.__ct.interval.end.CopyFrom(end) 541 if is_present_index is not None: 542 self.__ct.enforcement_literal.append(is_present_index) 543 if name: 544 self.__ct.name = name
546 @property 547 def index(self) -> int: 548 """Returns the index of the interval constraint in the model.""" 549 return self.__index
Returns the index of the interval constraint in the model.
551 @property 552 def proto(self) -> cp_model_pb2.ConstraintProto: 553 """Returns the interval protobuf.""" 554 return self.__model.constraints[self.__index]
Returns the interval protobuf.
556 @property 557 def model_proto(self) -> cp_model_pb2.CpModelProto: 558 """Returns the model protobuf.""" 559 return self.__model
Returns the model protobuf.
615def object_is_a_true_literal(literal: LiteralT) -> bool: 616 """Checks if literal is either True, or a Boolean literals fixed to True.""" 617 if isinstance(literal, IntVar): 618 proto = literal.proto 619 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 620 if isinstance(literal, cmh.NotBooleanVariable): 621 proto = literal.negated().proto 622 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 623 if isinstance(literal, IntegralTypes): 624 return int(literal) == 1 625 return False
Checks if literal is either True, or a Boolean literals fixed to True.
628def object_is_a_false_literal(literal: LiteralT) -> bool: 629 """Checks if literal is either False, or a Boolean literals fixed to False.""" 630 if isinstance(literal, IntVar): 631 proto = literal.proto 632 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 633 if isinstance(literal, cmh.NotBooleanVariable): 634 proto = literal.negated().proto 635 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 636 if isinstance(literal, IntegralTypes): 637 return int(literal) == 0 638 return False
Checks if literal is either False, or a Boolean literals fixed to False.
641class CpModel: 642 """Methods for building a CP model. 643 644 Methods beginning with: 645 646 * ```New``` create integer, boolean, or interval variables. 647 * ```add``` create new constraints and add them to the model. 648 """ 649 650 def __init__(self) -> None: 651 self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto() 652 self.__constant_map: Dict[IntegralT, int] = {} 653 self.__var_list: VariableList = VariableList() 654 655 # Naming. 656 @property 657 def name(self) -> str: 658 """Returns the name of the model.""" 659 if not self.__model or not self.__model.name: 660 return "" 661 return self.__model.name 662 663 @name.setter 664 def name(self, name: str): 665 """Sets the name of the model.""" 666 self.__model.name = name 667 668 # Integer variable. 669 670 def _append_int_var(self, var: IntVar) -> IntVar: 671 """Appends an integer variable to the list of variables.""" 672 self.__var_list.append(var) 673 return var 674 675 def _get_int_var(self, index: int) -> IntVar: 676 return self.__var_list.get(index) 677 678 def rebuild_from_linear_expression_proto( 679 self, 680 proto: cp_model_pb2.LinearExpressionProto, 681 ) -> LinearExpr: 682 return self.__var_list.rebuild_expr(proto) 683 684 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 685 """Create an integer variable with domain [lb, ub]. 686 687 The CP-SAT solver is limited to integer variables. If you have fractional 688 values, scale them up so that they become integers; if you have strings, 689 encode them as integers. 690 691 Args: 692 lb: Lower bound for the variable. 693 ub: Upper bound for the variable. 694 name: The name of the variable. 695 696 Returns: 697 a variable whose domain is [lb, ub]. 698 """ 699 domain_is_boolean = lb >= 0 and ub <= 1 700 return self._append_int_var( 701 IntVar( 702 self.__model, 703 sorted_interval_list.Domain(lb, ub), 704 domain_is_boolean, 705 name, 706 ) 707 ) 708 709 def new_int_var_from_domain( 710 self, domain: sorted_interval_list.Domain, name: str 711 ) -> IntVar: 712 """Create an integer variable from a domain. 713 714 A domain is a set of integers specified by a collection of intervals. 715 For example, `model.new_int_var_from_domain(cp_model. 716 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 717 718 Args: 719 domain: An instance of the Domain class. 720 name: The name of the variable. 721 722 Returns: 723 a variable whose domain is the given domain. 724 """ 725 domain_is_boolean = domain.min() >= 0 and domain.max() <= 1 726 return self._append_int_var( 727 IntVar(self.__model, domain, domain_is_boolean, name) 728 ) 729 730 def new_bool_var(self, name: str) -> IntVar: 731 """Creates a 0-1 variable with the given name.""" 732 return self._append_int_var( 733 IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name) 734 ) 735 736 def new_constant(self, value: IntegralT) -> IntVar: 737 """Declares a constant integer.""" 738 index: int = self.get_or_make_index_from_constant(value) 739 return self._get_int_var(index) 740 741 def new_int_var_series( 742 self, 743 name: str, 744 index: pd.Index, 745 lower_bounds: Union[IntegralT, pd.Series], 746 upper_bounds: Union[IntegralT, pd.Series], 747 ) -> pd.Series: 748 """Creates a series of (scalar-valued) variables with the given name. 749 750 Args: 751 name (str): Required. The name of the variable set. 752 index (pd.Index): Required. The index to use for the variable set. 753 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 754 set. If a `pd.Series` is passed in, it will be based on the 755 corresponding values of the pd.Series. 756 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 757 set. If a `pd.Series` is passed in, it will be based on the 758 corresponding values of the pd.Series. 759 760 Returns: 761 pd.Series: The variable set indexed by its corresponding dimensions. 762 763 Raises: 764 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 765 ValueError: if the `name` is not a valid identifier or already exists. 766 ValueError: if the `lowerbound` is greater than the `upperbound`. 767 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 768 the input index. 769 """ 770 if not isinstance(index, pd.Index): 771 raise TypeError("Non-index object is used as index") 772 if not name.isidentifier(): 773 raise ValueError(f"name={name!r} is not a valid identifier") 774 if ( 775 isinstance(lower_bounds, IntegralTypes) 776 and isinstance(upper_bounds, IntegralTypes) 777 and lower_bounds > upper_bounds 778 ): 779 raise ValueError( 780 f"lower_bound={lower_bounds} is greater than" 781 f" upper_bound={upper_bounds} for variable set={name}" 782 ) 783 784 lower_bounds = _convert_to_integral_series_and_validate_index( 785 lower_bounds, index 786 ) 787 upper_bounds = _convert_to_integral_series_and_validate_index( 788 upper_bounds, index 789 ) 790 return pd.Series( 791 index=index, 792 data=[ 793 # pylint: disable=g-complex-comprehension 794 self._append_int_var( 795 IntVar( 796 model=self.__model, 797 name=f"{name}[{i}]", 798 domain=sorted_interval_list.Domain( 799 lower_bounds[i], upper_bounds[i] 800 ), 801 is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1, 802 ) 803 ) 804 for i in index 805 ], 806 ) 807 808 def new_bool_var_series( 809 self, 810 name: str, 811 index: pd.Index, 812 ) -> pd.Series: 813 """Creates a series of (scalar-valued) variables with the given name. 814 815 Args: 816 name (str): Required. The name of the variable set. 817 index (pd.Index): Required. The index to use for the variable set. 818 819 Returns: 820 pd.Series: The variable set indexed by its corresponding dimensions. 821 822 Raises: 823 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 824 ValueError: if the `name` is not a valid identifier or already exists. 825 """ 826 if not isinstance(index, pd.Index): 827 raise TypeError("Non-index object is used as index") 828 if not name.isidentifier(): 829 raise ValueError(f"name={name!r} is not a valid identifier") 830 return pd.Series( 831 index=index, 832 data=[ 833 # pylint: disable=g-complex-comprehension 834 self._append_int_var( 835 IntVar( 836 model=self.__model, 837 name=f"{name}[{i}]", 838 domain=sorted_interval_list.Domain(0, 1), 839 is_boolean=True, 840 ) 841 ) 842 for i in index 843 ], 844 ) 845 846 # Linear constraints. 847 848 def add_linear_constraint( 849 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 850 ) -> Constraint: 851 """Adds the constraint: `lb <= linear_expr <= ub`.""" 852 return self.add_linear_expression_in_domain( 853 linear_expr, sorted_interval_list.Domain(lb, ub) 854 ) 855 856 def add_linear_expression_in_domain( 857 self, 858 linear_expr: LinearExprT, 859 domain: sorted_interval_list.Domain, 860 ) -> Constraint: 861 """Adds the constraint: `linear_expr` in `domain`.""" 862 if isinstance(linear_expr, LinearExpr): 863 ble = BoundedLinearExpression(linear_expr, domain) 864 if not ble.ok: 865 raise TypeError( 866 "Cannot add a linear expression containing floating point" 867 f" coefficients or constants: {type(linear_expr).__name__!r}" 868 ) 869 return self.add(ble) 870 if isinstance(linear_expr, IntegralTypes): 871 if not domain.contains(int(linear_expr)): 872 return self.add_bool_or([]) # Evaluate to false. 873 else: 874 return self.add_bool_and([]) # Evaluate to true. 875 raise TypeError( 876 "not supported:" 877 f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})" 878 ) 879 880 def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint: 881 """Adds a `BoundedLinearExpression` to the model. 882 883 Args: 884 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 885 886 Returns: 887 An instance of the `Constraint` class. 888 889 Raises: 890 TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean. 891 """ 892 if isinstance(ct, BoundedLinearExpression): 893 result = Constraint(self) 894 model_ct = self.__model.constraints[result.index] 895 for var in ct.vars: 896 model_ct.linear.vars.append(var.index) 897 model_ct.linear.coeffs.extend(ct.coeffs) 898 model_ct.linear.domain.extend( 899 [ 900 cmn.capped_subtraction(x, ct.offset) 901 for x in ct.bounds.flattened_intervals() 902 ] 903 ) 904 return result 905 if ct and cmn.is_boolean(ct): 906 return self.add_bool_or([True]) 907 if not ct and cmn.is_boolean(ct): 908 return self.add_bool_or([]) # Evaluate to false. 909 raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})") 910 911 # General Integer Constraints. 912 913 @overload 914 def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ... 915 916 @overload 917 def add_all_different(self, *expressions: LinearExprT) -> Constraint: ... 918 919 def add_all_different(self, *expressions): 920 """Adds AllDifferent(expressions). 921 922 This constraint forces all expressions to have different values. 923 924 Args: 925 *expressions: simple expressions of the form a * var + constant. 926 927 Returns: 928 An instance of the `Constraint` class. 929 """ 930 ct = Constraint(self) 931 model_ct = self.__model.constraints[ct.index] 932 expanded = expand_generator_or_tuple(expressions) 933 model_ct.all_diff.exprs.extend( 934 self.parse_linear_expression(x) for x in expanded 935 ) 936 return ct 937 938 def add_element( 939 self, 940 index: LinearExprT, 941 expressions: Sequence[LinearExprT], 942 target: LinearExprT, 943 ) -> Constraint: 944 """Adds the element constraint: `expressions[index] == target`. 945 946 Args: 947 index: The index of the selected expression in the array. It must be an 948 affine expression (a * var + b). 949 expressions: A list of affine expressions. 950 target: The expression constrained to be equal to the selected expression. 951 It must be an affine expression (a * var + b). 952 953 Returns: 954 An instance of the `Constraint` class. 955 """ 956 957 if not expressions: 958 raise ValueError("add_element expects a non-empty expressions array") 959 960 if isinstance(index, IntegralTypes): 961 expression: LinearExprT = list(expressions)[int(index)] 962 return self.add(expression == target) 963 964 ct = Constraint(self) 965 model_ct = self.__model.constraints[ct.index] 966 model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index)) 967 model_ct.element.exprs.extend( 968 [self.parse_linear_expression(e) for e in expressions] 969 ) 970 model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target)) 971 return ct 972 973 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 974 """Adds Circuit(arcs). 975 976 Adds a circuit constraint from a sparse list of arcs that encode the graph. 977 978 A circuit is a unique Hamiltonian cycle in a subgraph of the total 979 graph. In case a node 'i' is not in the cycle, then there must be a 980 loop arc 'i -> i' associated with a true literal. Otherwise 981 this constraint will fail. 982 983 Args: 984 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 985 literal). The arc is selected in the circuit if the literal is true. 986 Both source_node and destination_node must be integers between 0 and the 987 number of nodes - 1. 988 989 Returns: 990 An instance of the `Constraint` class. 991 992 Raises: 993 ValueError: If the list of arcs is empty. 994 """ 995 if not arcs: 996 raise ValueError("add_circuit expects a non-empty array of arcs") 997 ct = Constraint(self) 998 model_ct = self.__model.constraints[ct.index] 999 for arc in arcs: 1000 model_ct.circuit.tails.append(arc[0]) 1001 model_ct.circuit.heads.append(arc[1]) 1002 model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2])) 1003 return ct 1004 1005 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1006 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1007 1008 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1009 literals[i] is true must satisfy this set of properties: 1010 - #incoming arcs == 1 except for node 0. 1011 - #outgoing arcs == 1 except for node 0. 1012 - for node zero, #incoming arcs == #outgoing arcs. 1013 - There are no duplicate arcs. 1014 - Self-arcs are allowed except for node 0. 1015 - There is no cycle in this graph, except through node 0. 1016 1017 Args: 1018 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1019 literal). The arc is selected in the circuit if the literal is true. 1020 Both source_node and destination_node must be integers between 0 and the 1021 number of nodes - 1. 1022 1023 Returns: 1024 An instance of the `Constraint` class. 1025 1026 Raises: 1027 ValueError: If the list of arcs is empty. 1028 """ 1029 if not arcs: 1030 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1031 ct = Constraint(self) 1032 model_ct = self.__model.constraints[ct.index] 1033 for arc in arcs: 1034 model_ct.routes.tails.append(arc[0]) 1035 model_ct.routes.heads.append(arc[1]) 1036 model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2])) 1037 return ct 1038 1039 def add_allowed_assignments( 1040 self, 1041 expressions: Sequence[LinearExprT], 1042 tuples_list: Iterable[Sequence[IntegralT]], 1043 ) -> Constraint: 1044 """Adds AllowedAssignments(expressions, tuples_list). 1045 1046 An AllowedAssignments constraint is a constraint on an array of affine 1047 expressions, which requires that when all expressions are assigned values, 1048 the 1049 resulting array equals one of the tuples in `tuple_list`. 1050 1051 Args: 1052 expressions: A list of affine expressions (a * var + b). 1053 tuples_list: A list of admissible tuples. Each tuple must have the same 1054 length as the expressions, and the ith value of a tuple corresponds to 1055 the ith expression. 1056 1057 Returns: 1058 An instance of the `Constraint` class. 1059 1060 Raises: 1061 TypeError: If a tuple does not have the same size as the list of 1062 expressions. 1063 ValueError: If the array of expressions is empty. 1064 """ 1065 1066 if not expressions: 1067 raise ValueError( 1068 "add_allowed_assignments expects a non-empty expressions array" 1069 ) 1070 1071 ct: Constraint = Constraint(self) 1072 model_ct = self.__model.constraints[ct.index] 1073 model_ct.table.exprs.extend( 1074 [self.parse_linear_expression(e) for e in expressions] 1075 ) 1076 arity: int = len(expressions) 1077 for one_tuple in tuples_list: 1078 if len(one_tuple) != arity: 1079 raise TypeError(f"Tuple {one_tuple!r} has the wrong arity") 1080 1081 # duck-typing (no explicit type checks here) 1082 try: 1083 for one_tuple in tuples_list: 1084 model_ct.table.values.extend(one_tuple) 1085 except ValueError as ex: 1086 raise TypeError( 1087 "add_xxx_assignment: Not an integer or does not fit in an int64_t:" 1088 f" {type(ex.args).__name__!r}" 1089 ) from ex 1090 1091 return ct 1092 1093 def add_forbidden_assignments( 1094 self, 1095 expressions: Sequence[LinearExprT], 1096 tuples_list: Iterable[Sequence[IntegralT]], 1097 ) -> Constraint: 1098 """Adds add_forbidden_assignments(expressions, [tuples_list]). 1099 1100 A ForbiddenAssignments constraint is a constraint on an array of affine 1101 expressions where the list of impossible combinations is provided in the 1102 tuples list. 1103 1104 Args: 1105 expressions: A list of affine expressions (a * var + b). 1106 tuples_list: A list of forbidden tuples. Each tuple must have the same 1107 length as the expressions, and the *i*th value of a tuple corresponds to 1108 the *i*th expression. 1109 1110 Returns: 1111 An instance of the `Constraint` class. 1112 1113 Raises: 1114 TypeError: If a tuple does not have the same size as the list of 1115 expressions. 1116 ValueError: If the array of expressions is empty. 1117 """ 1118 1119 if not expressions: 1120 raise ValueError( 1121 "add_forbidden_assignments expects a non-empty expressions array" 1122 ) 1123 1124 index: int = len(self.__model.constraints) 1125 ct: Constraint = self.add_allowed_assignments(expressions, tuples_list) 1126 self.__model.constraints[index].table.negated = True 1127 return ct 1128 1129 def add_automaton( 1130 self, 1131 transition_expressions: Sequence[LinearExprT], 1132 starting_state: IntegralT, 1133 final_states: Sequence[IntegralT], 1134 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1135 ) -> Constraint: 1136 """Adds an automaton constraint. 1137 1138 An automaton constraint takes a list of affine expressions (a * var + b) (of 1139 size *n*), an initial state, a set of final states, and a set of 1140 transitions. A transition is a triplet (*tail*, *transition*, *head*), where 1141 *tail* and *head* are states, and *transition* is the label of an arc from 1142 *head* to *tail*, corresponding to the value of one expression in the list 1143 of 1144 expressions. 1145 1146 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1147 contains the possible states of the automaton. The first state contains the 1148 initial state. The last phase contains the final states. 1149 1150 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1151 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1152 an arc from the state *tail* of phase *i* and the state *head* of phase 1153 *i* + 1. This arc is labeled by the value *transition* of the expression 1154 `expressions[i]`. That is, this arc can only be selected if `expressions[i]` 1155 is assigned the value *transition*. 1156 1157 A feasible solution of this constraint is an assignment of expressions such 1158 that, starting from the initial state in phase 0, there is a path labeled by 1159 the values of the expressions that ends in one of the final states in the 1160 final phase. 1161 1162 Args: 1163 transition_expressions: A non-empty list of affine expressions (a * var + 1164 b) whose values correspond to the labels of the arcs traversed by the 1165 automaton. 1166 starting_state: The initial state of the automaton. 1167 final_states: A non-empty list of admissible final states. 1168 transition_triples: A list of transitions for the automaton, in the 1169 following format (current_state, variable_value, next_state). 1170 1171 Returns: 1172 An instance of the `Constraint` class. 1173 1174 Raises: 1175 ValueError: if `transition_expressions`, `final_states`, or 1176 `transition_triples` are empty. 1177 """ 1178 1179 if not transition_expressions: 1180 raise ValueError( 1181 "add_automaton expects a non-empty transition_expressions array" 1182 ) 1183 if not final_states: 1184 raise ValueError("add_automaton expects some final states") 1185 1186 if not transition_triples: 1187 raise ValueError("add_automaton expects some transition triples") 1188 1189 ct = Constraint(self) 1190 model_ct = self.__model.constraints[ct.index] 1191 model_ct.automaton.exprs.extend( 1192 [self.parse_linear_expression(e) for e in transition_expressions] 1193 ) 1194 model_ct.automaton.starting_state = starting_state 1195 for v in final_states: 1196 model_ct.automaton.final_states.append(v) 1197 for t in transition_triples: 1198 if len(t) != 3: 1199 raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)") 1200 model_ct.automaton.transition_tail.append(t[0]) 1201 model_ct.automaton.transition_label.append(t[1]) 1202 model_ct.automaton.transition_head.append(t[2]) 1203 return ct 1204 1205 def add_inverse( 1206 self, 1207 variables: Sequence[VariableT], 1208 inverse_variables: Sequence[VariableT], 1209 ) -> Constraint: 1210 """Adds Inverse(variables, inverse_variables). 1211 1212 An inverse constraint enforces that if `variables[i]` is assigned a value 1213 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1214 1215 Args: 1216 variables: An array of integer variables. 1217 inverse_variables: An array of integer variables. 1218 1219 Returns: 1220 An instance of the `Constraint` class. 1221 1222 Raises: 1223 TypeError: if variables and inverse_variables have different lengths, or 1224 if they are empty. 1225 """ 1226 1227 if not variables or not inverse_variables: 1228 raise TypeError("The Inverse constraint does not accept empty arrays") 1229 if len(variables) != len(inverse_variables): 1230 raise TypeError( 1231 "In the inverse constraint, the two array variables and" 1232 " inverse_variables must have the same length." 1233 ) 1234 ct = Constraint(self) 1235 model_ct = self.__model.constraints[ct.index] 1236 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1237 model_ct.inverse.f_inverse.extend( 1238 [self.get_or_make_index(x) for x in inverse_variables] 1239 ) 1240 return ct 1241 1242 def add_reservoir_constraint( 1243 self, 1244 times: Iterable[LinearExprT], 1245 level_changes: Iterable[LinearExprT], 1246 min_level: int, 1247 max_level: int, 1248 ) -> Constraint: 1249 """Adds Reservoir(times, level_changes, min_level, max_level). 1250 1251 Maintains a reservoir level within bounds. The water level starts at 0, and 1252 at any time, it must be between min_level and max_level. 1253 1254 If the affine expression `times[i]` is assigned a value t, then the current 1255 level changes by `level_changes[i]`, which is constant, at time t. 1256 1257 Note that min level must be <= 0, and the max level must be >= 0. Please 1258 use fixed level_changes to simulate initial state. 1259 1260 Therefore, at any time: 1261 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1262 1263 Args: 1264 times: A list of 1-var affine expressions (a * x + b) which specify the 1265 time of the filling or emptying the reservoir. 1266 level_changes: A list of integer values that specifies the amount of the 1267 emptying or filling. Currently, variable demands are not supported. 1268 min_level: At any time, the level of the reservoir must be greater or 1269 equal than the min level. 1270 max_level: At any time, the level of the reservoir must be less or equal 1271 than the max level. 1272 1273 Returns: 1274 An instance of the `Constraint` class. 1275 1276 Raises: 1277 ValueError: if max_level < min_level. 1278 1279 ValueError: if max_level < 0. 1280 1281 ValueError: if min_level > 0 1282 """ 1283 1284 if max_level < min_level: 1285 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1286 1287 if max_level < 0: 1288 raise ValueError("Reservoir constraint must have a max_level >= 0") 1289 1290 if min_level > 0: 1291 raise ValueError("Reservoir constraint must have a min_level <= 0") 1292 1293 ct = Constraint(self) 1294 model_ct = self.__model.constraints[ct.index] 1295 model_ct.reservoir.time_exprs.extend( 1296 [self.parse_linear_expression(x) for x in times] 1297 ) 1298 model_ct.reservoir.level_changes.extend( 1299 [self.parse_linear_expression(x) for x in level_changes] 1300 ) 1301 model_ct.reservoir.min_level = min_level 1302 model_ct.reservoir.max_level = max_level 1303 return ct 1304 1305 def add_reservoir_constraint_with_active( 1306 self, 1307 times: Iterable[LinearExprT], 1308 level_changes: Iterable[LinearExprT], 1309 actives: Iterable[LiteralT], 1310 min_level: int, 1311 max_level: int, 1312 ) -> Constraint: 1313 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1314 1315 Maintains a reservoir level within bounds. The water level starts at 0, and 1316 at any time, it must be between min_level and max_level. 1317 1318 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1319 `True`, then the current level changes by `level_changes[i]`, which is 1320 constant, 1321 at time t. 1322 1323 Note that min level must be <= 0, and the max level must be >= 0. Please 1324 use fixed level_changes to simulate initial state. 1325 1326 Therefore, at any time: 1327 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1328 max_level] 1329 1330 1331 The array of boolean variables 'actives', if defined, indicates which 1332 actions are actually performed. 1333 1334 Args: 1335 times: A list of 1-var affine expressions (a * x + b) which specify the 1336 time of the filling or emptying the reservoir. 1337 level_changes: A list of integer values that specifies the amount of the 1338 emptying or filling. Currently, variable demands are not supported. 1339 actives: a list of boolean variables. They indicates if the 1340 emptying/refilling events actually take place. 1341 min_level: At any time, the level of the reservoir must be greater or 1342 equal than the min level. 1343 max_level: At any time, the level of the reservoir must be less or equal 1344 than the max level. 1345 1346 Returns: 1347 An instance of the `Constraint` class. 1348 1349 Raises: 1350 ValueError: if max_level < min_level. 1351 1352 ValueError: if max_level < 0. 1353 1354 ValueError: if min_level > 0 1355 """ 1356 1357 if max_level < min_level: 1358 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1359 1360 if max_level < 0: 1361 raise ValueError("Reservoir constraint must have a max_level >= 0") 1362 1363 if min_level > 0: 1364 raise ValueError("Reservoir constraint must have a min_level <= 0") 1365 1366 ct = Constraint(self) 1367 model_ct = self.__model.constraints[ct.index] 1368 model_ct.reservoir.time_exprs.extend( 1369 [self.parse_linear_expression(x) for x in times] 1370 ) 1371 model_ct.reservoir.level_changes.extend( 1372 [self.parse_linear_expression(x) for x in level_changes] 1373 ) 1374 model_ct.reservoir.active_literals.extend( 1375 [self.get_or_make_boolean_index(x) for x in actives] 1376 ) 1377 model_ct.reservoir.min_level = min_level 1378 model_ct.reservoir.max_level = max_level 1379 return ct 1380 1381 def add_map_domain( 1382 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 1383 ): 1384 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 1385 1386 for i, bool_var in enumerate(bool_var_array): 1387 b_index = bool_var.index 1388 var_index = var.index 1389 model_ct = self.__model.constraints.add() 1390 model_ct.linear.vars.append(var_index) 1391 model_ct.linear.coeffs.append(1) 1392 offset_as_int = int(offset) 1393 model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i]) 1394 model_ct.enforcement_literal.append(b_index) 1395 1396 model_ct = self.__model.constraints.add() 1397 model_ct.linear.vars.append(var_index) 1398 model_ct.linear.coeffs.append(1) 1399 model_ct.enforcement_literal.append(-b_index - 1) 1400 if offset + i - 1 >= INT_MIN: 1401 model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1]) 1402 if offset + i + 1 <= INT_MAX: 1403 model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX]) 1404 1405 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 1406 """Adds `a => b` (`a` implies `b`).""" 1407 ct = Constraint(self) 1408 model_ct = self.__model.constraints[ct.index] 1409 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 1410 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 1411 return ct 1412 1413 @overload 1414 def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ... 1415 1416 @overload 1417 def add_bool_or(self, *literals: LiteralT) -> Constraint: ... 1418 1419 def add_bool_or(self, *literals): 1420 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 1421 ct = Constraint(self) 1422 model_ct = self.__model.constraints[ct.index] 1423 model_ct.bool_or.literals.extend( 1424 [ 1425 self.get_or_make_boolean_index(x) 1426 for x in expand_generator_or_tuple(literals) 1427 ] 1428 ) 1429 return ct 1430 1431 @overload 1432 def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 1433 1434 @overload 1435 def add_at_least_one(self, *literals: LiteralT) -> Constraint: ... 1436 1437 def add_at_least_one(self, *literals): 1438 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 1439 return self.add_bool_or(*literals) 1440 1441 @overload 1442 def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 1443 1444 @overload 1445 def add_at_most_one(self, *literals: LiteralT) -> Constraint: ... 1446 1447 def add_at_most_one(self, *literals): 1448 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 1449 ct = Constraint(self) 1450 model_ct = self.__model.constraints[ct.index] 1451 model_ct.at_most_one.literals.extend( 1452 [ 1453 self.get_or_make_boolean_index(x) 1454 for x in expand_generator_or_tuple(literals) 1455 ] 1456 ) 1457 return ct 1458 1459 @overload 1460 def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ... 1461 1462 @overload 1463 def add_exactly_one(self, *literals: LiteralT) -> Constraint: ... 1464 1465 def add_exactly_one(self, *literals): 1466 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 1467 ct = Constraint(self) 1468 model_ct = self.__model.constraints[ct.index] 1469 model_ct.exactly_one.literals.extend( 1470 [ 1471 self.get_or_make_boolean_index(x) 1472 for x in expand_generator_or_tuple(literals) 1473 ] 1474 ) 1475 return ct 1476 1477 @overload 1478 def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ... 1479 1480 @overload 1481 def add_bool_and(self, *literals: LiteralT) -> Constraint: ... 1482 1483 def add_bool_and(self, *literals): 1484 """Adds `And(literals) == true`.""" 1485 ct = Constraint(self) 1486 model_ct = self.__model.constraints[ct.index] 1487 model_ct.bool_and.literals.extend( 1488 [ 1489 self.get_or_make_boolean_index(x) 1490 for x in expand_generator_or_tuple(literals) 1491 ] 1492 ) 1493 return ct 1494 1495 @overload 1496 def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ... 1497 1498 @overload 1499 def add_bool_xor(self, *literals: LiteralT) -> Constraint: ... 1500 1501 def add_bool_xor(self, *literals): 1502 """Adds `XOr(literals) == true`. 1503 1504 In contrast to add_bool_or and add_bool_and, it does not support 1505 .only_enforce_if(). 1506 1507 Args: 1508 *literals: the list of literals in the constraint. 1509 1510 Returns: 1511 An `Constraint` object. 1512 """ 1513 ct = Constraint(self) 1514 model_ct = self.__model.constraints[ct.index] 1515 model_ct.bool_xor.literals.extend( 1516 [ 1517 self.get_or_make_boolean_index(x) 1518 for x in expand_generator_or_tuple(literals) 1519 ] 1520 ) 1521 return ct 1522 1523 def add_min_equality( 1524 self, target: LinearExprT, exprs: Iterable[LinearExprT] 1525 ) -> Constraint: 1526 """Adds `target == Min(exprs)`.""" 1527 ct = Constraint(self) 1528 model_ct = self.__model.constraints[ct.index] 1529 model_ct.lin_max.exprs.extend( 1530 [self.parse_linear_expression(x, True) for x in exprs] 1531 ) 1532 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 1533 return ct 1534 1535 def add_max_equality( 1536 self, target: LinearExprT, exprs: Iterable[LinearExprT] 1537 ) -> Constraint: 1538 """Adds `target == Max(exprs)`.""" 1539 ct = Constraint(self) 1540 model_ct = self.__model.constraints[ct.index] 1541 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 1542 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 1543 return ct 1544 1545 def add_division_equality( 1546 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 1547 ) -> Constraint: 1548 """Adds `target == num // denom` (integer division rounded towards 0).""" 1549 ct = Constraint(self) 1550 model_ct = self.__model.constraints[ct.index] 1551 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 1552 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 1553 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 1554 return ct 1555 1556 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 1557 """Adds `target == Abs(expr)`.""" 1558 ct = Constraint(self) 1559 model_ct = self.__model.constraints[ct.index] 1560 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 1561 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 1562 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 1563 return ct 1564 1565 def add_modulo_equality( 1566 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 1567 ) -> Constraint: 1568 """Adds `target = expr % mod`. 1569 1570 It uses the C convention, that is the result is the remainder of the 1571 integral division rounded towards 0. 1572 1573 For example: 1574 * 10 % 3 = 1 1575 * -10 % 3 = -1 1576 * 10 % -3 = 1 1577 * -10 % -3 = -1 1578 1579 Args: 1580 target: the target expression. 1581 expr: the expression to compute the modulo of. 1582 mod: the modulus expression. 1583 1584 Returns: 1585 A `Constraint` object. 1586 """ 1587 ct = Constraint(self) 1588 model_ct = self.__model.constraints[ct.index] 1589 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 1590 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 1591 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 1592 return ct 1593 1594 def add_multiplication_equality( 1595 self, 1596 target: LinearExprT, 1597 *expressions: Union[Iterable[LinearExprT], LinearExprT], 1598 ) -> Constraint: 1599 """Adds `target == expressions[0] * .. * expressions[n]`.""" 1600 ct = Constraint(self) 1601 model_ct = self.__model.constraints[ct.index] 1602 model_ct.int_prod.exprs.extend( 1603 [ 1604 self.parse_linear_expression(expr) 1605 for expr in expand_generator_or_tuple(expressions) 1606 ] 1607 ) 1608 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 1609 return ct 1610 1611 # Scheduling support 1612 1613 def new_interval_var( 1614 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 1615 ) -> IntervalVar: 1616 """Creates an interval variable from start, size, and end. 1617 1618 An interval variable is a constraint, that is itself used in other 1619 constraints like NoOverlap. 1620 1621 Internally, it ensures that `start + size == end`. 1622 1623 Args: 1624 start: The start of the interval. It must be of the form a * var + b. 1625 size: The size of the interval. It must be of the form a * var + b. 1626 end: The end of the interval. It must be of the form a * var + b. 1627 name: The name of the interval variable. 1628 1629 Returns: 1630 An `IntervalVar` object. 1631 """ 1632 1633 start_expr = self.parse_linear_expression(start) 1634 size_expr = self.parse_linear_expression(size) 1635 end_expr = self.parse_linear_expression(end) 1636 if len(start_expr.vars) > 1: 1637 raise TypeError( 1638 "cp_model.new_interval_var: start must be 1-var affine or constant." 1639 ) 1640 if len(size_expr.vars) > 1: 1641 raise TypeError( 1642 "cp_model.new_interval_var: size must be 1-var affine or constant." 1643 ) 1644 if len(end_expr.vars) > 1: 1645 raise TypeError( 1646 "cp_model.new_interval_var: end must be 1-var affine or constant." 1647 ) 1648 return IntervalVar( 1649 self.__model, 1650 self.__var_list, 1651 start_expr, 1652 size_expr, 1653 end_expr, 1654 None, 1655 name, 1656 ) 1657 1658 def new_interval_var_series( 1659 self, 1660 name: str, 1661 index: pd.Index, 1662 starts: Union[LinearExprT, pd.Series], 1663 sizes: Union[LinearExprT, pd.Series], 1664 ends: Union[LinearExprT, pd.Series], 1665 ) -> pd.Series: 1666 """Creates a series of interval variables with the given name. 1667 1668 Args: 1669 name (str): Required. The name of the variable set. 1670 index (pd.Index): Required. The index to use for the variable set. 1671 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1672 set. If a `pd.Series` is passed in, it will be based on the 1673 corresponding values of the pd.Series. 1674 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 1675 set. If a `pd.Series` is passed in, it will be based on the 1676 corresponding values of the pd.Series. 1677 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 1678 set. If a `pd.Series` is passed in, it will be based on the 1679 corresponding values of the pd.Series. 1680 1681 Returns: 1682 pd.Series: The interval variable set indexed by its corresponding 1683 dimensions. 1684 1685 Raises: 1686 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1687 ValueError: if the `name` is not a valid identifier or already exists. 1688 ValueError: if the all the indexes do not match. 1689 """ 1690 if not isinstance(index, pd.Index): 1691 raise TypeError("Non-index object is used as index") 1692 if not name.isidentifier(): 1693 raise ValueError(f"name={name!r} is not a valid identifier") 1694 1695 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1696 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 1697 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 1698 interval_array = [] 1699 for i in index: 1700 interval_array.append( 1701 self.new_interval_var( 1702 start=starts[i], 1703 size=sizes[i], 1704 end=ends[i], 1705 name=f"{name}[{i}]", 1706 ) 1707 ) 1708 return pd.Series(index=index, data=interval_array) 1709 1710 def new_fixed_size_interval_var( 1711 self, start: LinearExprT, size: IntegralT, name: str 1712 ) -> IntervalVar: 1713 """Creates an interval variable from start, and a fixed size. 1714 1715 An interval variable is a constraint, that is itself used in other 1716 constraints like NoOverlap. 1717 1718 Args: 1719 start: The start of the interval. It must be of the form a * var + b. 1720 size: The size of the interval. It must be an integer value. 1721 name: The name of the interval variable. 1722 1723 Returns: 1724 An `IntervalVar` object. 1725 """ 1726 start_expr = self.parse_linear_expression(start) 1727 size_expr = self.parse_linear_expression(size) 1728 end_expr = self.parse_linear_expression(start + size) 1729 if len(start_expr.vars) > 1: 1730 raise TypeError( 1731 "cp_model.new_interval_var: start must be affine or constant." 1732 ) 1733 return IntervalVar( 1734 self.__model, 1735 self.__var_list, 1736 start_expr, 1737 size_expr, 1738 end_expr, 1739 None, 1740 name, 1741 ) 1742 1743 def new_fixed_size_interval_var_series( 1744 self, 1745 name: str, 1746 index: pd.Index, 1747 starts: Union[LinearExprT, pd.Series], 1748 sizes: Union[IntegralT, pd.Series], 1749 ) -> pd.Series: 1750 """Creates a series of interval variables with the given name. 1751 1752 Args: 1753 name (str): Required. The name of the variable set. 1754 index (pd.Index): Required. The index to use for the variable set. 1755 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1756 set. If a `pd.Series` is passed in, it will be based on the 1757 corresponding values of the pd.Series. 1758 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 1759 the set. If a `pd.Series` is passed in, it will be based on the 1760 corresponding values of the pd.Series. 1761 1762 Returns: 1763 pd.Series: The interval variable set indexed by its corresponding 1764 dimensions. 1765 1766 Raises: 1767 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1768 ValueError: if the `name` is not a valid identifier or already exists. 1769 ValueError: if the all the indexes do not match. 1770 """ 1771 if not isinstance(index, pd.Index): 1772 raise TypeError("Non-index object is used as index") 1773 if not name.isidentifier(): 1774 raise ValueError(f"name={name!r} is not a valid identifier") 1775 1776 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1777 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 1778 interval_array = [] 1779 for i in index: 1780 interval_array.append( 1781 self.new_fixed_size_interval_var( 1782 start=starts[i], 1783 size=sizes[i], 1784 name=f"{name}[{i}]", 1785 ) 1786 ) 1787 return pd.Series(index=index, data=interval_array) 1788 1789 def new_optional_interval_var( 1790 self, 1791 start: LinearExprT, 1792 size: LinearExprT, 1793 end: LinearExprT, 1794 is_present: LiteralT, 1795 name: str, 1796 ) -> IntervalVar: 1797 """Creates an optional interval var from start, size, end, and is_present. 1798 1799 An optional interval variable is a constraint, that is itself used in other 1800 constraints like NoOverlap. This constraint is protected by a presence 1801 literal that indicates if it is active or not. 1802 1803 Internally, it ensures that `is_present` implies `start + size == 1804 end`. 1805 1806 Args: 1807 start: The start of the interval. It must be of the form a * var + b. 1808 size: The size of the interval. It must be of the form a * var + b. 1809 end: The end of the interval. It must be of the form a * var + b. 1810 is_present: A literal that indicates if the interval is active or not. A 1811 inactive interval is simply ignored by all constraints. 1812 name: The name of the interval variable. 1813 1814 Returns: 1815 An `IntervalVar` object. 1816 """ 1817 1818 # Creates the IntervalConstraintProto object. 1819 is_present_index = self.get_or_make_boolean_index(is_present) 1820 start_expr = self.parse_linear_expression(start) 1821 size_expr = self.parse_linear_expression(size) 1822 end_expr = self.parse_linear_expression(end) 1823 if len(start_expr.vars) > 1: 1824 raise TypeError( 1825 "cp_model.new_interval_var: start must be affine or constant." 1826 ) 1827 if len(size_expr.vars) > 1: 1828 raise TypeError( 1829 "cp_model.new_interval_var: size must be affine or constant." 1830 ) 1831 if len(end_expr.vars) > 1: 1832 raise TypeError( 1833 "cp_model.new_interval_var: end must be affine or constant." 1834 ) 1835 return IntervalVar( 1836 self.__model, 1837 self.__var_list, 1838 start_expr, 1839 size_expr, 1840 end_expr, 1841 is_present_index, 1842 name, 1843 ) 1844 1845 def new_optional_interval_var_series( 1846 self, 1847 name: str, 1848 index: pd.Index, 1849 starts: Union[LinearExprT, pd.Series], 1850 sizes: Union[LinearExprT, pd.Series], 1851 ends: Union[LinearExprT, pd.Series], 1852 are_present: Union[LiteralT, pd.Series], 1853 ) -> pd.Series: 1854 """Creates a series of interval variables with the given name. 1855 1856 Args: 1857 name (str): Required. The name of the variable set. 1858 index (pd.Index): Required. The index to use for the variable set. 1859 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1860 set. If a `pd.Series` is passed in, it will be based on the 1861 corresponding values of the pd.Series. 1862 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 1863 set. If a `pd.Series` is passed in, it will be based on the 1864 corresponding values of the pd.Series. 1865 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 1866 set. If a `pd.Series` is passed in, it will be based on the 1867 corresponding values of the pd.Series. 1868 are_present (Union[LiteralT, pd.Series]): The performed literal of each 1869 interval in the set. If a `pd.Series` is passed in, it will be based on 1870 the corresponding values of the pd.Series. 1871 1872 Returns: 1873 pd.Series: The interval variable set indexed by its corresponding 1874 dimensions. 1875 1876 Raises: 1877 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1878 ValueError: if the `name` is not a valid identifier or already exists. 1879 ValueError: if the all the indexes do not match. 1880 """ 1881 if not isinstance(index, pd.Index): 1882 raise TypeError("Non-index object is used as index") 1883 if not name.isidentifier(): 1884 raise ValueError(f"name={name!r} is not a valid identifier") 1885 1886 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1887 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 1888 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 1889 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 1890 1891 interval_array = [] 1892 for i in index: 1893 interval_array.append( 1894 self.new_optional_interval_var( 1895 start=starts[i], 1896 size=sizes[i], 1897 end=ends[i], 1898 is_present=are_present[i], 1899 name=f"{name}[{i}]", 1900 ) 1901 ) 1902 return pd.Series(index=index, data=interval_array) 1903 1904 def new_optional_fixed_size_interval_var( 1905 self, 1906 start: LinearExprT, 1907 size: IntegralT, 1908 is_present: LiteralT, 1909 name: str, 1910 ) -> IntervalVar: 1911 """Creates an interval variable from start, and a fixed size. 1912 1913 An interval variable is a constraint, that is itself used in other 1914 constraints like NoOverlap. 1915 1916 Args: 1917 start: The start of the interval. It must be of the form a * var + b. 1918 size: The size of the interval. It must be an integer value. 1919 is_present: A literal that indicates if the interval is active or not. A 1920 inactive interval is simply ignored by all constraints. 1921 name: The name of the interval variable. 1922 1923 Returns: 1924 An `IntervalVar` object. 1925 """ 1926 start_expr = self.parse_linear_expression(start) 1927 size_expr = self.parse_linear_expression(size) 1928 end_expr = self.parse_linear_expression(start + size) 1929 if len(start_expr.vars) > 1: 1930 raise TypeError( 1931 "cp_model.new_interval_var: start must be affine or constant." 1932 ) 1933 is_present_index = self.get_or_make_boolean_index(is_present) 1934 return IntervalVar( 1935 self.__model, 1936 self.__var_list, 1937 start_expr, 1938 size_expr, 1939 end_expr, 1940 is_present_index, 1941 name, 1942 ) 1943 1944 def new_optional_fixed_size_interval_var_series( 1945 self, 1946 name: str, 1947 index: pd.Index, 1948 starts: Union[LinearExprT, pd.Series], 1949 sizes: Union[IntegralT, pd.Series], 1950 are_present: Union[LiteralT, pd.Series], 1951 ) -> pd.Series: 1952 """Creates a series of interval variables with the given name. 1953 1954 Args: 1955 name (str): Required. The name of the variable set. 1956 index (pd.Index): Required. The index to use for the variable set. 1957 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1958 set. If a `pd.Series` is passed in, it will be based on the 1959 corresponding values of the pd.Series. 1960 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 1961 the set. If a `pd.Series` is passed in, it will be based on the 1962 corresponding values of the pd.Series. 1963 are_present (Union[LiteralT, pd.Series]): The performed literal of each 1964 interval in the set. If a `pd.Series` is passed in, it will be based on 1965 the corresponding values of the pd.Series. 1966 1967 Returns: 1968 pd.Series: The interval variable set indexed by its corresponding 1969 dimensions. 1970 1971 Raises: 1972 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1973 ValueError: if the `name` is not a valid identifier or already exists. 1974 ValueError: if the all the indexes do not match. 1975 """ 1976 if not isinstance(index, pd.Index): 1977 raise TypeError("Non-index object is used as index") 1978 if not name.isidentifier(): 1979 raise ValueError(f"name={name!r} is not a valid identifier") 1980 1981 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1982 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 1983 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 1984 interval_array = [] 1985 for i in index: 1986 interval_array.append( 1987 self.new_optional_fixed_size_interval_var( 1988 start=starts[i], 1989 size=sizes[i], 1990 is_present=are_present[i], 1991 name=f"{name}[{i}]", 1992 ) 1993 ) 1994 return pd.Series(index=index, data=interval_array) 1995 1996 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 1997 """Adds NoOverlap(interval_vars). 1998 1999 A NoOverlap constraint ensures that all present intervals do not overlap 2000 in time. 2001 2002 Args: 2003 interval_vars: The list of interval variables to constrain. 2004 2005 Returns: 2006 An instance of the `Constraint` class. 2007 """ 2008 ct = Constraint(self) 2009 model_ct = self.__model.constraints[ct.index] 2010 model_ct.no_overlap.intervals.extend( 2011 [self.get_interval_index(x) for x in interval_vars] 2012 ) 2013 return ct 2014 2015 def add_no_overlap_2d( 2016 self, 2017 x_intervals: Iterable[IntervalVar], 2018 y_intervals: Iterable[IntervalVar], 2019 ) -> Constraint: 2020 """Adds NoOverlap2D(x_intervals, y_intervals). 2021 2022 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2023 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2024 by two intervals which represent its projection onto the X and Y axis. 2025 2026 Furthermore, one box is optional if at least one of the x or y interval is 2027 optional. 2028 2029 Args: 2030 x_intervals: The X coordinates of the rectangles. 2031 y_intervals: The Y coordinates of the rectangles. 2032 2033 Returns: 2034 An instance of the `Constraint` class. 2035 """ 2036 ct = Constraint(self) 2037 model_ct = self.__model.constraints[ct.index] 2038 model_ct.no_overlap_2d.x_intervals.extend( 2039 [self.get_interval_index(x) for x in x_intervals] 2040 ) 2041 model_ct.no_overlap_2d.y_intervals.extend( 2042 [self.get_interval_index(x) for x in y_intervals] 2043 ) 2044 return ct 2045 2046 def add_cumulative( 2047 self, 2048 intervals: Iterable[IntervalVar], 2049 demands: Iterable[LinearExprT], 2050 capacity: LinearExprT, 2051 ) -> Constraint: 2052 """Adds Cumulative(intervals, demands, capacity). 2053 2054 This constraint enforces that: 2055 2056 for all t: 2057 sum(demands[i] 2058 if (start(intervals[i]) <= t < end(intervals[i])) and 2059 (intervals[i] is present)) <= capacity 2060 2061 Args: 2062 intervals: The list of intervals. 2063 demands: The list of demands for each interval. Each demand must be >= 0. 2064 Each demand can be a 1-var affine expression (a * x + b). 2065 capacity: The maximum capacity of the cumulative constraint. It can be a 2066 1-var affine expression (a * x + b). 2067 2068 Returns: 2069 An instance of the `Constraint` class. 2070 """ 2071 cumulative = Constraint(self) 2072 model_ct = self.__model.constraints[cumulative.index] 2073 model_ct.cumulative.intervals.extend( 2074 [self.get_interval_index(x) for x in intervals] 2075 ) 2076 for d in demands: 2077 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2078 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2079 return cumulative 2080 2081 # Support for model cloning. 2082 def clone(self) -> "CpModel": 2083 """Reset the model, and creates a new one from a CpModelProto instance.""" 2084 clone = CpModel() 2085 clone.proto.CopyFrom(self.proto) 2086 clone.rebuild_var_and_constant_map() 2087 return clone 2088 2089 def rebuild_var_and_constant_map(self): 2090 """Internal method used during model cloning.""" 2091 for i, var in enumerate(self.__model.variables): 2092 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2093 self.__constant_map[var.domain[0]] = i 2094 is_boolean = ( 2095 len(var.domain) == 2 and var.domain[0] >= 0 and var.domain[1] <= 1 2096 ) 2097 self.__var_list.append(IntVar(self.__model, i, is_boolean, None)) 2098 2099 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2100 """Returns an already created Boolean variable from its index.""" 2101 result = self._get_int_var(index) 2102 if not result.is_boolean: 2103 raise ValueError( 2104 f"get_bool_var_from_proto_index: index {index} does not reference a" 2105 " boolean variable" 2106 ) 2107 return result 2108 2109 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2110 """Returns an already created integer variable from its index.""" 2111 return self._get_int_var(index) 2112 2113 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2114 """Returns an already created interval variable from its index.""" 2115 if index < 0 or index >= len(self.__model.constraints): 2116 raise ValueError( 2117 f"get_interval_var_from_proto_index: out of bound index {index}" 2118 ) 2119 ct = self.__model.constraints[index] 2120 if not ct.HasField("interval"): 2121 raise ValueError( 2122 f"get_interval_var_from_proto_index: index {index} does not" 2123 " reference an" + " interval variable" 2124 ) 2125 2126 return IntervalVar(self.__model, self.__var_list, index, None, None, None, None) 2127 2128 # Helpers. 2129 2130 def __str__(self) -> str: 2131 return str(self.__model) 2132 2133 @property 2134 def proto(self) -> cp_model_pb2.CpModelProto: 2135 """Returns the underlying CpModelProto.""" 2136 return self.__model 2137 2138 def negated(self, index: int) -> int: 2139 return -index - 1 2140 2141 def get_or_make_index(self, arg: VariableT) -> int: 2142 """Returns the index of a variable, its negation, or a number.""" 2143 if isinstance(arg, IntVar): 2144 return arg.index 2145 if isinstance(arg, IntegralTypes): 2146 return self.get_or_make_index_from_constant(arg) 2147 raise TypeError( 2148 f"NotSupported: model.get_or_make_index({type(arg).__name__!r})" 2149 ) 2150 2151 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2152 """Returns an index from a boolean expression.""" 2153 if isinstance(arg, IntVar): 2154 self.assert_is_boolean_variable(arg) 2155 return arg.index 2156 if isinstance(arg, cmh.NotBooleanVariable): 2157 self.assert_is_boolean_variable(arg.negated()) 2158 return arg.index 2159 if isinstance(arg, IntegralTypes): 2160 if arg == ~int(False): 2161 return self.get_or_make_index_from_constant(1) 2162 if arg == ~int(True): 2163 return self.get_or_make_index_from_constant(0) 2164 arg = cmn.assert_is_zero_or_one(arg) 2165 return self.get_or_make_index_from_constant(arg) 2166 if cmn.is_boolean(arg): 2167 return self.get_or_make_index_from_constant(int(arg)) 2168 raise TypeError( 2169 "not supported:" f" model.get_or_make_boolean_index({type(arg).__name__!r})" 2170 ) 2171 2172 def get_interval_index(self, arg: IntervalVar) -> int: 2173 if not isinstance(arg, IntervalVar): 2174 raise TypeError( 2175 f"NotSupported: model.get_interval_index({type(arg).__name__!r})" 2176 ) 2177 return arg.index 2178 2179 def get_or_make_index_from_constant(self, value: IntegralT) -> int: 2180 if value in self.__constant_map: 2181 return self.__constant_map[value] 2182 constant_var = self.new_int_var(value, value, "") 2183 self.__constant_map[value] = constant_var.index 2184 return constant_var.index 2185 2186 def parse_linear_expression( 2187 self, linear_expr: LinearExprT, negate: bool = False 2188 ) -> cp_model_pb2.LinearExpressionProto: 2189 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2190 result: cp_model_pb2.LinearExpressionProto = ( 2191 cp_model_pb2.LinearExpressionProto() 2192 ) 2193 mult = -1 if negate else 1 2194 if isinstance(linear_expr, IntegralTypes): 2195 result.offset = int(linear_expr) * mult 2196 return result 2197 2198 # Raises TypeError if linear_expr is not an integer. 2199 flat_expr = cmh.FlatIntExpr(linear_expr) 2200 result.offset = flat_expr.offset * mult 2201 for var in flat_expr.vars: 2202 result.vars.append(var.index) 2203 for coeff in flat_expr.coeffs: 2204 result.coeffs.append(coeff * mult) 2205 return result 2206 2207 def _set_objective(self, obj: ObjLinearExprT, minimize: bool): 2208 """Sets the objective of the model.""" 2209 self.clear_objective() 2210 if isinstance(obj, IntegralTypes): 2211 self.__model.objective.offset = int(obj) 2212 self.__model.objective.scaling_factor = 1.0 2213 elif isinstance(obj, LinearExpr): 2214 if obj.is_integer(): 2215 int_obj = cmh.FlatIntExpr(obj) 2216 for var in int_obj.vars: 2217 self.__model.objective.vars.append(var.index) 2218 if minimize: 2219 self.__model.objective.scaling_factor = 1.0 2220 self.__model.objective.offset = int_obj.offset 2221 self.__model.objective.coeffs.extend(int_obj.coeffs) 2222 else: 2223 self.__model.objective.scaling_factor = -1.0 2224 self.__model.objective.offset = -int_obj.offset 2225 for c in int_obj.coeffs: 2226 self.__model.objective.coeffs.append(-c) 2227 else: 2228 float_obj = cmh.FlatFloatExpr(obj) 2229 for var in float_obj.vars: 2230 self.__model.floating_point_objective.vars.append(var.index) 2231 self.__model.floating_point_objective.coeffs.extend(float_obj.coeffs) 2232 self.__model.floating_point_objective.maximize = not minimize 2233 self.__model.floating_point_objective.offset = float_obj.offset 2234 else: 2235 raise TypeError( 2236 f"TypeError: {type(obj).__name__!r} is not a valid objective" 2237 ) 2238 2239 def minimize(self, obj: ObjLinearExprT): 2240 """Sets the objective of the model to minimize(obj).""" 2241 self._set_objective(obj, minimize=True) 2242 2243 def maximize(self, obj: ObjLinearExprT): 2244 """Sets the objective of the model to maximize(obj).""" 2245 self._set_objective(obj, minimize=False) 2246 2247 def has_objective(self) -> bool: 2248 return self.__model.HasField("objective") or self.__model.HasField( 2249 "floating_point_objective" 2250 ) 2251 2252 def clear_objective(self): 2253 self.__model.ClearField("objective") 2254 self.__model.ClearField("floating_point_objective") 2255 2256 def add_decision_strategy( 2257 self, 2258 variables: Sequence[IntVar], 2259 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2260 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2261 ) -> None: 2262 """Adds a search strategy to the model. 2263 2264 Args: 2265 variables: a list of variables this strategy will assign. 2266 var_strategy: heuristic to choose the next variable to assign. 2267 domain_strategy: heuristic to reduce the domain of the selected variable. 2268 Currently, this is advanced code: the union of all strategies added to 2269 the model must be complete, i.e. instantiates all variables. Otherwise, 2270 solve() will fail. 2271 """ 2272 2273 strategy: cp_model_pb2.DecisionStrategyProto = ( 2274 self.__model.search_strategy.add() 2275 ) 2276 for v in variables: 2277 expr = strategy.exprs.add() 2278 if v.index >= 0: 2279 expr.vars.append(v.index) 2280 expr.coeffs.append(1) 2281 else: 2282 expr.vars.append(self.negated(v.index)) 2283 expr.coeffs.append(-1) 2284 expr.offset = 1 2285 2286 strategy.variable_selection_strategy = var_strategy 2287 strategy.domain_reduction_strategy = domain_strategy 2288 2289 def model_stats(self) -> str: 2290 """Returns a string containing some model statistics.""" 2291 return cmh.CpSatHelper.model_stats(self.__model) 2292 2293 def validate(self) -> str: 2294 """Returns a string indicating that the model is invalid.""" 2295 return cmh.CpSatHelper.validate_model(self.__model) 2296 2297 def export_to_file(self, file: str) -> bool: 2298 """Write the model as a protocol buffer to 'file'. 2299 2300 Args: 2301 file: file to write the model to. If the filename ends with 'txt', the 2302 model will be written as a text file, otherwise, the binary format will 2303 be used. 2304 2305 Returns: 2306 True if the model was correctly written. 2307 """ 2308 return cmh.CpSatHelper.write_model_to_file(self.__model, file) 2309 2310 def remove_all_names(self) -> None: 2311 """Removes all names from the model.""" 2312 self.__model.ClearField("name") 2313 for v in self.__model.variables: 2314 v.ClearField("name") 2315 for c in self.__model.constraints: 2316 c.ClearField("name") 2317 2318 @overload 2319 def add_hint(self, var: IntVar, value: int) -> None: ... 2320 2321 @overload 2322 def add_hint(self, literal: BoolVarT, value: bool) -> None: ... 2323 2324 def add_hint(self, var, value) -> None: 2325 """Adds 'var == value' as a hint to the solver.""" 2326 if var.index >= 0: 2327 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2328 self.__model.solution_hint.values.append(int(value)) 2329 else: 2330 self.__model.solution_hint.vars.append(self.negated(var.index)) 2331 self.__model.solution_hint.values.append(int(not value)) 2332 2333 def clear_hints(self): 2334 """Removes any solution hint from the model.""" 2335 self.__model.ClearField("solution_hint") 2336 2337 def add_assumption(self, lit: LiteralT) -> None: 2338 """Adds the literal to the model as assumptions.""" 2339 self.__model.assumptions.append(self.get_or_make_boolean_index(lit)) 2340 2341 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2342 """Adds the literals to the model as assumptions.""" 2343 for lit in literals: 2344 self.add_assumption(lit) 2345 2346 def clear_assumptions(self) -> None: 2347 """Removes all assumptions from the model.""" 2348 self.__model.ClearField("assumptions") 2349 2350 # Helpers. 2351 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2352 if isinstance(x, IntVar): 2353 var = self.__model.variables[x.index] 2354 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2355 raise TypeError( 2356 f"TypeError: {type(x).__name__!r} is not a boolean variable" 2357 ) 2358 elif not isinstance(x, cmh.NotBooleanVariable): 2359 raise TypeError( 2360 f"TypeError: {type(x).__name__!r} is not a boolean variable" 2361 ) 2362 2363 # Compatibility with pre PEP8 2364 # pylint: disable=invalid-name 2365 2366 def Name(self) -> str: 2367 return self.name 2368 2369 def SetName(self, name: str) -> None: 2370 self.name = name 2371 2372 def Proto(self) -> cp_model_pb2.CpModelProto: 2373 return self.proto 2374 2375 NewIntVar = new_int_var 2376 NewIntVarFromDomain = new_int_var_from_domain 2377 NewBoolVar = new_bool_var 2378 NewConstant = new_constant 2379 NewIntVarSeries = new_int_var_series 2380 NewBoolVarSeries = new_bool_var_series 2381 AddLinearConstraint = add_linear_constraint 2382 AddLinearExpressionInDomain = add_linear_expression_in_domain 2383 Add = add 2384 AddAllDifferent = add_all_different 2385 AddElement = add_element 2386 AddCircuit = add_circuit 2387 AddMultipleCircuit = add_multiple_circuit 2388 AddAllowedAssignments = add_allowed_assignments 2389 AddForbiddenAssignments = add_forbidden_assignments 2390 AddAutomaton = add_automaton 2391 AddInverse = add_inverse 2392 AddReservoirConstraint = add_reservoir_constraint 2393 AddReservoirConstraintWithActive = add_reservoir_constraint_with_active 2394 AddImplication = add_implication 2395 AddBoolOr = add_bool_or 2396 AddAtLeastOne = add_at_least_one 2397 AddAtMostOne = add_at_most_one 2398 AddExactlyOne = add_exactly_one 2399 AddBoolAnd = add_bool_and 2400 AddBoolXOr = add_bool_xor 2401 AddMinEquality = add_min_equality 2402 AddMaxEquality = add_max_equality 2403 AddDivisionEquality = add_division_equality 2404 AddAbsEquality = add_abs_equality 2405 AddModuloEquality = add_modulo_equality 2406 AddMultiplicationEquality = add_multiplication_equality 2407 NewIntervalVar = new_interval_var 2408 NewIntervalVarSeries = new_interval_var_series 2409 NewFixedSizeIntervalVar = new_fixed_size_interval_var 2410 NewOptionalIntervalVar = new_optional_interval_var 2411 NewOptionalIntervalVarSeries = new_optional_interval_var_series 2412 NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var 2413 NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series 2414 AddNoOverlap = add_no_overlap 2415 AddNoOverlap2D = add_no_overlap_2d 2416 AddCumulative = add_cumulative 2417 Clone = clone 2418 GetBoolVarFromProtoIndex = get_bool_var_from_proto_index 2419 GetIntVarFromProtoIndex = get_int_var_from_proto_index 2420 GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index 2421 Minimize = minimize 2422 Maximize = maximize 2423 HasObjective = has_objective 2424 ClearObjective = clear_objective 2425 AddDecisionStrategy = add_decision_strategy 2426 ModelStats = model_stats 2427 Validate = validate 2428 ExportToFile = export_to_file 2429 AddHint = add_hint 2430 ClearHints = clear_hints 2431 AddAssumption = add_assumption 2432 AddAssumptions = add_assumptions 2433 ClearAssumptions = clear_assumptions 2434 2435 # 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.
656 @property 657 def name(self) -> str: 658 """Returns the name of the model.""" 659 if not self.__model or not self.__model.name: 660 return "" 661 return self.__model.name
Returns the name of the model.
684 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 685 """Create an integer variable with domain [lb, ub]. 686 687 The CP-SAT solver is limited to integer variables. If you have fractional 688 values, scale them up so that they become integers; if you have strings, 689 encode them as integers. 690 691 Args: 692 lb: Lower bound for the variable. 693 ub: Upper bound for the variable. 694 name: The name of the variable. 695 696 Returns: 697 a variable whose domain is [lb, ub]. 698 """ 699 domain_is_boolean = lb >= 0 and ub <= 1 700 return self._append_int_var( 701 IntVar( 702 self.__model, 703 sorted_interval_list.Domain(lb, ub), 704 domain_is_boolean, 705 name, 706 ) 707 )
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].
709 def new_int_var_from_domain( 710 self, domain: sorted_interval_list.Domain, name: str 711 ) -> IntVar: 712 """Create an integer variable from a domain. 713 714 A domain is a set of integers specified by a collection of intervals. 715 For example, `model.new_int_var_from_domain(cp_model. 716 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 717 718 Args: 719 domain: An instance of the Domain class. 720 name: The name of the variable. 721 722 Returns: 723 a variable whose domain is the given domain. 724 """ 725 domain_is_boolean = domain.min() >= 0 and domain.max() <= 1 726 return self._append_int_var( 727 IntVar(self.__model, domain, domain_is_boolean, name) 728 )
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.
730 def new_bool_var(self, name: str) -> IntVar: 731 """Creates a 0-1 variable with the given name.""" 732 return self._append_int_var( 733 IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name) 734 )
Creates a 0-1 variable with the given name.
736 def new_constant(self, value: IntegralT) -> IntVar: 737 """Declares a constant integer.""" 738 index: int = self.get_or_make_index_from_constant(value) 739 return self._get_int_var(index)
Declares a constant integer.
741 def new_int_var_series( 742 self, 743 name: str, 744 index: pd.Index, 745 lower_bounds: Union[IntegralT, pd.Series], 746 upper_bounds: Union[IntegralT, pd.Series], 747 ) -> pd.Series: 748 """Creates a series of (scalar-valued) variables with the given name. 749 750 Args: 751 name (str): Required. The name of the variable set. 752 index (pd.Index): Required. The index to use for the variable set. 753 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 754 set. If a `pd.Series` is passed in, it will be based on the 755 corresponding values of the pd.Series. 756 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 757 set. If a `pd.Series` is passed in, it will be based on the 758 corresponding values of the pd.Series. 759 760 Returns: 761 pd.Series: The variable set indexed by its corresponding dimensions. 762 763 Raises: 764 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 765 ValueError: if the `name` is not a valid identifier or already exists. 766 ValueError: if the `lowerbound` is greater than the `upperbound`. 767 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 768 the input index. 769 """ 770 if not isinstance(index, pd.Index): 771 raise TypeError("Non-index object is used as index") 772 if not name.isidentifier(): 773 raise ValueError(f"name={name!r} is not a valid identifier") 774 if ( 775 isinstance(lower_bounds, IntegralTypes) 776 and isinstance(upper_bounds, IntegralTypes) 777 and lower_bounds > upper_bounds 778 ): 779 raise ValueError( 780 f"lower_bound={lower_bounds} is greater than" 781 f" upper_bound={upper_bounds} for variable set={name}" 782 ) 783 784 lower_bounds = _convert_to_integral_series_and_validate_index( 785 lower_bounds, index 786 ) 787 upper_bounds = _convert_to_integral_series_and_validate_index( 788 upper_bounds, index 789 ) 790 return pd.Series( 791 index=index, 792 data=[ 793 # pylint: disable=g-complex-comprehension 794 self._append_int_var( 795 IntVar( 796 model=self.__model, 797 name=f"{name}[{i}]", 798 domain=sorted_interval_list.Domain( 799 lower_bounds[i], upper_bounds[i] 800 ), 801 is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1, 802 ) 803 ) 804 for i in index 805 ], 806 )
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.
808 def new_bool_var_series( 809 self, 810 name: str, 811 index: pd.Index, 812 ) -> pd.Series: 813 """Creates a series of (scalar-valued) variables with the given name. 814 815 Args: 816 name (str): Required. The name of the variable set. 817 index (pd.Index): Required. The index to use for the variable set. 818 819 Returns: 820 pd.Series: The variable set indexed by its corresponding dimensions. 821 822 Raises: 823 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 824 ValueError: if the `name` is not a valid identifier or already exists. 825 """ 826 if not isinstance(index, pd.Index): 827 raise TypeError("Non-index object is used as index") 828 if not name.isidentifier(): 829 raise ValueError(f"name={name!r} is not a valid identifier") 830 return pd.Series( 831 index=index, 832 data=[ 833 # pylint: disable=g-complex-comprehension 834 self._append_int_var( 835 IntVar( 836 model=self.__model, 837 name=f"{name}[{i}]", 838 domain=sorted_interval_list.Domain(0, 1), 839 is_boolean=True, 840 ) 841 ) 842 for i in index 843 ], 844 )
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.
848 def add_linear_constraint( 849 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 850 ) -> Constraint: 851 """Adds the constraint: `lb <= linear_expr <= ub`.""" 852 return self.add_linear_expression_in_domain( 853 linear_expr, sorted_interval_list.Domain(lb, ub) 854 )
Adds the constraint: lb <= linear_expr <= ub
.
856 def add_linear_expression_in_domain( 857 self, 858 linear_expr: LinearExprT, 859 domain: sorted_interval_list.Domain, 860 ) -> Constraint: 861 """Adds the constraint: `linear_expr` in `domain`.""" 862 if isinstance(linear_expr, LinearExpr): 863 ble = BoundedLinearExpression(linear_expr, domain) 864 if not ble.ok: 865 raise TypeError( 866 "Cannot add a linear expression containing floating point" 867 f" coefficients or constants: {type(linear_expr).__name__!r}" 868 ) 869 return self.add(ble) 870 if isinstance(linear_expr, IntegralTypes): 871 if not domain.contains(int(linear_expr)): 872 return self.add_bool_or([]) # Evaluate to false. 873 else: 874 return self.add_bool_and([]) # Evaluate to true. 875 raise TypeError( 876 "not supported:" 877 f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})" 878 )
Adds the constraint: linear_expr
in domain
.
880 def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint: 881 """Adds a `BoundedLinearExpression` to the model. 882 883 Args: 884 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 885 886 Returns: 887 An instance of the `Constraint` class. 888 889 Raises: 890 TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean. 891 """ 892 if isinstance(ct, BoundedLinearExpression): 893 result = Constraint(self) 894 model_ct = self.__model.constraints[result.index] 895 for var in ct.vars: 896 model_ct.linear.vars.append(var.index) 897 model_ct.linear.coeffs.extend(ct.coeffs) 898 model_ct.linear.domain.extend( 899 [ 900 cmn.capped_subtraction(x, ct.offset) 901 for x in ct.bounds.flattened_intervals() 902 ] 903 ) 904 return result 905 if ct and cmn.is_boolean(ct): 906 return self.add_bool_or([True]) 907 if not ct and cmn.is_boolean(ct): 908 return self.add_bool_or([]) # Evaluate to false. 909 raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})")
Adds a BoundedLinearExpression
to the model.
Arguments:
- ct: A
BoundedLinearExpression
.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If the
ct
is not aBoundedLinearExpression
or a Boolean.
919 def add_all_different(self, *expressions): 920 """Adds AllDifferent(expressions). 921 922 This constraint forces all expressions to have different values. 923 924 Args: 925 *expressions: simple expressions of the form a * var + constant. 926 927 Returns: 928 An instance of the `Constraint` class. 929 """ 930 ct = Constraint(self) 931 model_ct = self.__model.constraints[ct.index] 932 expanded = expand_generator_or_tuple(expressions) 933 model_ct.all_diff.exprs.extend( 934 self.parse_linear_expression(x) for x in expanded 935 ) 936 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.
938 def add_element( 939 self, 940 index: LinearExprT, 941 expressions: Sequence[LinearExprT], 942 target: LinearExprT, 943 ) -> Constraint: 944 """Adds the element constraint: `expressions[index] == target`. 945 946 Args: 947 index: The index of the selected expression in the array. It must be an 948 affine expression (a * var + b). 949 expressions: A list of affine expressions. 950 target: The expression constrained to be equal to the selected expression. 951 It must be an affine expression (a * var + b). 952 953 Returns: 954 An instance of the `Constraint` class. 955 """ 956 957 if not expressions: 958 raise ValueError("add_element expects a non-empty expressions array") 959 960 if isinstance(index, IntegralTypes): 961 expression: LinearExprT = list(expressions)[int(index)] 962 return self.add(expression == target) 963 964 ct = Constraint(self) 965 model_ct = self.__model.constraints[ct.index] 966 model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index)) 967 model_ct.element.exprs.extend( 968 [self.parse_linear_expression(e) for e in expressions] 969 ) 970 model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target)) 971 return ct
Adds the element constraint: expressions[index] == target
.
Arguments:
- index: The index of the selected expression in the array. It must be an affine expression (a * var + b).
- expressions: A list of affine expressions.
- target: The expression constrained to be equal to the selected expression. It must be an affine expression (a * var + b).
Returns:
An instance of the
Constraint
class.
973 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 974 """Adds Circuit(arcs). 975 976 Adds a circuit constraint from a sparse list of arcs that encode the graph. 977 978 A circuit is a unique Hamiltonian cycle in a subgraph of the total 979 graph. In case a node 'i' is not in the cycle, then there must be a 980 loop arc 'i -> i' associated with a true literal. Otherwise 981 this constraint will fail. 982 983 Args: 984 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 985 literal). The arc is selected in the circuit if the literal is true. 986 Both source_node and destination_node must be integers between 0 and the 987 number of nodes - 1. 988 989 Returns: 990 An instance of the `Constraint` class. 991 992 Raises: 993 ValueError: If the list of arcs is empty. 994 """ 995 if not arcs: 996 raise ValueError("add_circuit expects a non-empty array of arcs") 997 ct = Constraint(self) 998 model_ct = self.__model.constraints[ct.index] 999 for arc in arcs: 1000 model_ct.circuit.tails.append(arc[0]) 1001 model_ct.circuit.heads.append(arc[1]) 1002 model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2])) 1003 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 cycle in a subgraph of the total graph. In case a node 'i' is not in the cycle, 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.
1005 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1006 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1007 1008 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1009 literals[i] is true must satisfy this set of properties: 1010 - #incoming arcs == 1 except for node 0. 1011 - #outgoing arcs == 1 except for node 0. 1012 - for node zero, #incoming arcs == #outgoing arcs. 1013 - There are no duplicate arcs. 1014 - Self-arcs are allowed except for node 0. 1015 - There is no cycle in this graph, except through node 0. 1016 1017 Args: 1018 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1019 literal). The arc is selected in the circuit if the literal is true. 1020 Both source_node and destination_node must be integers between 0 and the 1021 number of nodes - 1. 1022 1023 Returns: 1024 An instance of the `Constraint` class. 1025 1026 Raises: 1027 ValueError: If the list of arcs is empty. 1028 """ 1029 if not arcs: 1030 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1031 ct = Constraint(self) 1032 model_ct = self.__model.constraints[ct.index] 1033 for arc in arcs: 1034 model_ct.routes.tails.append(arc[0]) 1035 model_ct.routes.heads.append(arc[1]) 1036 model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2])) 1037 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.
1039 def add_allowed_assignments( 1040 self, 1041 expressions: Sequence[LinearExprT], 1042 tuples_list: Iterable[Sequence[IntegralT]], 1043 ) -> Constraint: 1044 """Adds AllowedAssignments(expressions, tuples_list). 1045 1046 An AllowedAssignments constraint is a constraint on an array of affine 1047 expressions, which requires that when all expressions are assigned values, 1048 the 1049 resulting array equals one of the tuples in `tuple_list`. 1050 1051 Args: 1052 expressions: A list of affine expressions (a * var + b). 1053 tuples_list: A list of admissible tuples. Each tuple must have the same 1054 length as the expressions, and the ith value of a tuple corresponds to 1055 the ith expression. 1056 1057 Returns: 1058 An instance of the `Constraint` class. 1059 1060 Raises: 1061 TypeError: If a tuple does not have the same size as the list of 1062 expressions. 1063 ValueError: If the array of expressions is empty. 1064 """ 1065 1066 if not expressions: 1067 raise ValueError( 1068 "add_allowed_assignments expects a non-empty expressions array" 1069 ) 1070 1071 ct: Constraint = Constraint(self) 1072 model_ct = self.__model.constraints[ct.index] 1073 model_ct.table.exprs.extend( 1074 [self.parse_linear_expression(e) for e in expressions] 1075 ) 1076 arity: int = len(expressions) 1077 for one_tuple in tuples_list: 1078 if len(one_tuple) != arity: 1079 raise TypeError(f"Tuple {one_tuple!r} has the wrong arity") 1080 1081 # duck-typing (no explicit type checks here) 1082 try: 1083 for one_tuple in tuples_list: 1084 model_ct.table.values.extend(one_tuple) 1085 except ValueError as ex: 1086 raise TypeError( 1087 "add_xxx_assignment: Not an integer or does not fit in an int64_t:" 1088 f" {type(ex.args).__name__!r}" 1089 ) from ex 1090 1091 return ct
Adds AllowedAssignments(expressions, tuples_list).
An AllowedAssignments constraint is a constraint on an array of affine
expressions, which requires that when all expressions are assigned values,
the
resulting array equals one of the tuples in tuple_list
.
Arguments:
- expressions: A list of affine expressions (a * var + b).
- tuples_list: A list of admissible tuples. Each tuple must have the same length as the expressions, and the ith value of a tuple corresponds to the ith expression.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If a tuple does not have the same size as the list of expressions.
- ValueError: If the array of expressions is empty.
1093 def add_forbidden_assignments( 1094 self, 1095 expressions: Sequence[LinearExprT], 1096 tuples_list: Iterable[Sequence[IntegralT]], 1097 ) -> Constraint: 1098 """Adds add_forbidden_assignments(expressions, [tuples_list]). 1099 1100 A ForbiddenAssignments constraint is a constraint on an array of affine 1101 expressions where the list of impossible combinations is provided in the 1102 tuples list. 1103 1104 Args: 1105 expressions: A list of affine expressions (a * var + b). 1106 tuples_list: A list of forbidden tuples. Each tuple must have the same 1107 length as the expressions, and the *i*th value of a tuple corresponds to 1108 the *i*th expression. 1109 1110 Returns: 1111 An instance of the `Constraint` class. 1112 1113 Raises: 1114 TypeError: If a tuple does not have the same size as the list of 1115 expressions. 1116 ValueError: If the array of expressions is empty. 1117 """ 1118 1119 if not expressions: 1120 raise ValueError( 1121 "add_forbidden_assignments expects a non-empty expressions array" 1122 ) 1123 1124 index: int = len(self.__model.constraints) 1125 ct: Constraint = self.add_allowed_assignments(expressions, tuples_list) 1126 self.__model.constraints[index].table.negated = True 1127 return ct
Adds add_forbidden_assignments(expressions, [tuples_list]).
A ForbiddenAssignments constraint is a constraint on an array of affine expressions where the list of impossible combinations is provided in the tuples list.
Arguments:
- expressions: A list of affine expressions (a * var + b).
- tuples_list: A list of forbidden tuples. Each tuple must have the same length as the expressions, and the ith value of a tuple corresponds to the ith expression.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If a tuple does not have the same size as the list of expressions.
- ValueError: If the array of expressions is empty.
1129 def add_automaton( 1130 self, 1131 transition_expressions: Sequence[LinearExprT], 1132 starting_state: IntegralT, 1133 final_states: Sequence[IntegralT], 1134 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1135 ) -> Constraint: 1136 """Adds an automaton constraint. 1137 1138 An automaton constraint takes a list of affine expressions (a * var + b) (of 1139 size *n*), an initial state, a set of final states, and a set of 1140 transitions. A transition is a triplet (*tail*, *transition*, *head*), where 1141 *tail* and *head* are states, and *transition* is the label of an arc from 1142 *head* to *tail*, corresponding to the value of one expression in the list 1143 of 1144 expressions. 1145 1146 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1147 contains the possible states of the automaton. The first state contains the 1148 initial state. The last phase contains the final states. 1149 1150 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1151 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1152 an arc from the state *tail* of phase *i* and the state *head* of phase 1153 *i* + 1. This arc is labeled by the value *transition* of the expression 1154 `expressions[i]`. That is, this arc can only be selected if `expressions[i]` 1155 is assigned the value *transition*. 1156 1157 A feasible solution of this constraint is an assignment of expressions such 1158 that, starting from the initial state in phase 0, there is a path labeled by 1159 the values of the expressions that ends in one of the final states in the 1160 final phase. 1161 1162 Args: 1163 transition_expressions: A non-empty list of affine expressions (a * var + 1164 b) whose values correspond to the labels of the arcs traversed by the 1165 automaton. 1166 starting_state: The initial state of the automaton. 1167 final_states: A non-empty list of admissible final states. 1168 transition_triples: A list of transitions for the automaton, in the 1169 following format (current_state, variable_value, next_state). 1170 1171 Returns: 1172 An instance of the `Constraint` class. 1173 1174 Raises: 1175 ValueError: if `transition_expressions`, `final_states`, or 1176 `transition_triples` are empty. 1177 """ 1178 1179 if not transition_expressions: 1180 raise ValueError( 1181 "add_automaton expects a non-empty transition_expressions array" 1182 ) 1183 if not final_states: 1184 raise ValueError("add_automaton expects some final states") 1185 1186 if not transition_triples: 1187 raise ValueError("add_automaton expects some transition triples") 1188 1189 ct = Constraint(self) 1190 model_ct = self.__model.constraints[ct.index] 1191 model_ct.automaton.exprs.extend( 1192 [self.parse_linear_expression(e) for e in transition_expressions] 1193 ) 1194 model_ct.automaton.starting_state = starting_state 1195 for v in final_states: 1196 model_ct.automaton.final_states.append(v) 1197 for t in transition_triples: 1198 if len(t) != 3: 1199 raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)") 1200 model_ct.automaton.transition_tail.append(t[0]) 1201 model_ct.automaton.transition_label.append(t[1]) 1202 model_ct.automaton.transition_head.append(t[2]) 1203 return ct
Adds an automaton constraint.
An automaton constraint takes a list of affine expressions (a * var + b) (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 expression in the list of expressions.
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 expression
expressions[i]
. That is, this arc can only be selected if expressions[i]
is assigned the value transition.
A feasible solution of this constraint is an assignment of expressions such that, starting from the initial state in phase 0, there is a path labeled by the values of the expressions that ends in one of the final states in the final phase.
Arguments:
- transition_expressions: A non-empty list of affine expressions (a * var + b) 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_expressions
,final_states
, ortransition_triples
are empty.
1205 def add_inverse( 1206 self, 1207 variables: Sequence[VariableT], 1208 inverse_variables: Sequence[VariableT], 1209 ) -> Constraint: 1210 """Adds Inverse(variables, inverse_variables). 1211 1212 An inverse constraint enforces that if `variables[i]` is assigned a value 1213 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1214 1215 Args: 1216 variables: An array of integer variables. 1217 inverse_variables: An array of integer variables. 1218 1219 Returns: 1220 An instance of the `Constraint` class. 1221 1222 Raises: 1223 TypeError: if variables and inverse_variables have different lengths, or 1224 if they are empty. 1225 """ 1226 1227 if not variables or not inverse_variables: 1228 raise TypeError("The Inverse constraint does not accept empty arrays") 1229 if len(variables) != len(inverse_variables): 1230 raise TypeError( 1231 "In the inverse constraint, the two array variables and" 1232 " inverse_variables must have the same length." 1233 ) 1234 ct = Constraint(self) 1235 model_ct = self.__model.constraints[ct.index] 1236 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1237 model_ct.inverse.f_inverse.extend( 1238 [self.get_or_make_index(x) for x in inverse_variables] 1239 ) 1240 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.
1242 def add_reservoir_constraint( 1243 self, 1244 times: Iterable[LinearExprT], 1245 level_changes: Iterable[LinearExprT], 1246 min_level: int, 1247 max_level: int, 1248 ) -> Constraint: 1249 """Adds Reservoir(times, level_changes, min_level, max_level). 1250 1251 Maintains a reservoir level within bounds. The water level starts at 0, and 1252 at any time, it must be between min_level and max_level. 1253 1254 If the affine expression `times[i]` is assigned a value t, then the current 1255 level changes by `level_changes[i]`, which is constant, at time t. 1256 1257 Note that min level must be <= 0, and the max level must be >= 0. Please 1258 use fixed level_changes to simulate initial state. 1259 1260 Therefore, at any time: 1261 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1262 1263 Args: 1264 times: A list of 1-var affine expressions (a * x + b) which specify the 1265 time of the filling or emptying the reservoir. 1266 level_changes: A list of integer values that specifies the amount of the 1267 emptying or filling. Currently, variable demands are not supported. 1268 min_level: At any time, the level of the reservoir must be greater or 1269 equal than the min level. 1270 max_level: At any time, the level of the reservoir must be less or equal 1271 than the max level. 1272 1273 Returns: 1274 An instance of the `Constraint` class. 1275 1276 Raises: 1277 ValueError: if max_level < min_level. 1278 1279 ValueError: if max_level < 0. 1280 1281 ValueError: if min_level > 0 1282 """ 1283 1284 if max_level < min_level: 1285 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1286 1287 if max_level < 0: 1288 raise ValueError("Reservoir constraint must have a max_level >= 0") 1289 1290 if min_level > 0: 1291 raise ValueError("Reservoir constraint must have a min_level <= 0") 1292 1293 ct = Constraint(self) 1294 model_ct = self.__model.constraints[ct.index] 1295 model_ct.reservoir.time_exprs.extend( 1296 [self.parse_linear_expression(x) for x in times] 1297 ) 1298 model_ct.reservoir.level_changes.extend( 1299 [self.parse_linear_expression(x) for x in level_changes] 1300 ) 1301 model_ct.reservoir.min_level = min_level 1302 model_ct.reservoir.max_level = max_level 1303 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
1305 def add_reservoir_constraint_with_active( 1306 self, 1307 times: Iterable[LinearExprT], 1308 level_changes: Iterable[LinearExprT], 1309 actives: Iterable[LiteralT], 1310 min_level: int, 1311 max_level: int, 1312 ) -> Constraint: 1313 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1314 1315 Maintains a reservoir level within bounds. The water level starts at 0, and 1316 at any time, it must be between min_level and max_level. 1317 1318 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1319 `True`, then the current level changes by `level_changes[i]`, which is 1320 constant, 1321 at time t. 1322 1323 Note that min level must be <= 0, and the max level must be >= 0. Please 1324 use fixed level_changes to simulate initial state. 1325 1326 Therefore, at any time: 1327 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1328 max_level] 1329 1330 1331 The array of boolean variables 'actives', if defined, indicates which 1332 actions are actually performed. 1333 1334 Args: 1335 times: A list of 1-var affine expressions (a * x + b) which specify the 1336 time of the filling or emptying the reservoir. 1337 level_changes: A list of integer values that specifies the amount of the 1338 emptying or filling. Currently, variable demands are not supported. 1339 actives: a list of boolean variables. They indicates if the 1340 emptying/refilling events actually take place. 1341 min_level: At any time, the level of the reservoir must be greater or 1342 equal than the min level. 1343 max_level: At any time, the level of the reservoir must be less or equal 1344 than the max level. 1345 1346 Returns: 1347 An instance of the `Constraint` class. 1348 1349 Raises: 1350 ValueError: if max_level < min_level. 1351 1352 ValueError: if max_level < 0. 1353 1354 ValueError: if min_level > 0 1355 """ 1356 1357 if max_level < min_level: 1358 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1359 1360 if max_level < 0: 1361 raise ValueError("Reservoir constraint must have a max_level >= 0") 1362 1363 if min_level > 0: 1364 raise ValueError("Reservoir constraint must have a min_level <= 0") 1365 1366 ct = Constraint(self) 1367 model_ct = self.__model.constraints[ct.index] 1368 model_ct.reservoir.time_exprs.extend( 1369 [self.parse_linear_expression(x) for x in times] 1370 ) 1371 model_ct.reservoir.level_changes.extend( 1372 [self.parse_linear_expression(x) for x in level_changes] 1373 ) 1374 model_ct.reservoir.active_literals.extend( 1375 [self.get_or_make_boolean_index(x) for x in actives] 1376 ) 1377 model_ct.reservoir.min_level = min_level 1378 model_ct.reservoir.max_level = max_level 1379 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
1381 def add_map_domain( 1382 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 1383 ): 1384 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" 1385 1386 for i, bool_var in enumerate(bool_var_array): 1387 b_index = bool_var.index 1388 var_index = var.index 1389 model_ct = self.__model.constraints.add() 1390 model_ct.linear.vars.append(var_index) 1391 model_ct.linear.coeffs.append(1) 1392 offset_as_int = int(offset) 1393 model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i]) 1394 model_ct.enforcement_literal.append(b_index) 1395 1396 model_ct = self.__model.constraints.add() 1397 model_ct.linear.vars.append(var_index) 1398 model_ct.linear.coeffs.append(1) 1399 model_ct.enforcement_literal.append(-b_index - 1) 1400 if offset + i - 1 >= INT_MIN: 1401 model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1]) 1402 if offset + i + 1 <= INT_MAX: 1403 model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX])
Adds var == i + offset <=> bool_var_array[i] == true for all i
.
1405 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 1406 """Adds `a => b` (`a` implies `b`).""" 1407 ct = Constraint(self) 1408 model_ct = self.__model.constraints[ct.index] 1409 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 1410 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 1411 return ct
Adds a => b
(a
implies b
).
1419 def add_bool_or(self, *literals): 1420 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 1421 ct = Constraint(self) 1422 model_ct = self.__model.constraints[ct.index] 1423 model_ct.bool_or.literals.extend( 1424 [ 1425 self.get_or_make_boolean_index(x) 1426 for x in expand_generator_or_tuple(literals) 1427 ] 1428 ) 1429 return ct
Adds Or(literals) == true
: sum(literals) >= 1.
1437 def add_at_least_one(self, *literals): 1438 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 1439 return self.add_bool_or(*literals)
Same as add_bool_or
: sum(literals) >= 1
.
1447 def add_at_most_one(self, *literals): 1448 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 1449 ct = Constraint(self) 1450 model_ct = self.__model.constraints[ct.index] 1451 model_ct.at_most_one.literals.extend( 1452 [ 1453 self.get_or_make_boolean_index(x) 1454 for x in expand_generator_or_tuple(literals) 1455 ] 1456 ) 1457 return ct
Adds AtMostOne(literals)
: sum(literals) <= 1
.
1465 def add_exactly_one(self, *literals): 1466 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 1467 ct = Constraint(self) 1468 model_ct = self.__model.constraints[ct.index] 1469 model_ct.exactly_one.literals.extend( 1470 [ 1471 self.get_or_make_boolean_index(x) 1472 for x in expand_generator_or_tuple(literals) 1473 ] 1474 ) 1475 return ct
Adds ExactlyOne(literals)
: sum(literals) == 1
.
1483 def add_bool_and(self, *literals): 1484 """Adds `And(literals) == true`.""" 1485 ct = Constraint(self) 1486 model_ct = self.__model.constraints[ct.index] 1487 model_ct.bool_and.literals.extend( 1488 [ 1489 self.get_or_make_boolean_index(x) 1490 for x in expand_generator_or_tuple(literals) 1491 ] 1492 ) 1493 return ct
Adds And(literals) == true
.
1501 def add_bool_xor(self, *literals): 1502 """Adds `XOr(literals) == true`. 1503 1504 In contrast to add_bool_or and add_bool_and, it does not support 1505 .only_enforce_if(). 1506 1507 Args: 1508 *literals: the list of literals in the constraint. 1509 1510 Returns: 1511 An `Constraint` object. 1512 """ 1513 ct = Constraint(self) 1514 model_ct = self.__model.constraints[ct.index] 1515 model_ct.bool_xor.literals.extend( 1516 [ 1517 self.get_or_make_boolean_index(x) 1518 for x in expand_generator_or_tuple(literals) 1519 ] 1520 ) 1521 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.
1523 def add_min_equality( 1524 self, target: LinearExprT, exprs: Iterable[LinearExprT] 1525 ) -> Constraint: 1526 """Adds `target == Min(exprs)`.""" 1527 ct = Constraint(self) 1528 model_ct = self.__model.constraints[ct.index] 1529 model_ct.lin_max.exprs.extend( 1530 [self.parse_linear_expression(x, True) for x in exprs] 1531 ) 1532 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 1533 return ct
Adds target == Min(exprs)
.
1535 def add_max_equality( 1536 self, target: LinearExprT, exprs: Iterable[LinearExprT] 1537 ) -> Constraint: 1538 """Adds `target == Max(exprs)`.""" 1539 ct = Constraint(self) 1540 model_ct = self.__model.constraints[ct.index] 1541 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 1542 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 1543 return ct
Adds target == Max(exprs)
.
1545 def add_division_equality( 1546 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 1547 ) -> Constraint: 1548 """Adds `target == num // denom` (integer division rounded towards 0).""" 1549 ct = Constraint(self) 1550 model_ct = self.__model.constraints[ct.index] 1551 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 1552 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 1553 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 1554 return ct
Adds target == num // denom
(integer division rounded towards 0).
1556 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 1557 """Adds `target == Abs(expr)`.""" 1558 ct = Constraint(self) 1559 model_ct = self.__model.constraints[ct.index] 1560 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 1561 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 1562 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 1563 return ct
Adds target == Abs(expr)
.
1565 def add_modulo_equality( 1566 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 1567 ) -> Constraint: 1568 """Adds `target = expr % mod`. 1569 1570 It uses the C convention, that is the result is the remainder of the 1571 integral division rounded towards 0. 1572 1573 For example: 1574 * 10 % 3 = 1 1575 * -10 % 3 = -1 1576 * 10 % -3 = 1 1577 * -10 % -3 = -1 1578 1579 Args: 1580 target: the target expression. 1581 expr: the expression to compute the modulo of. 1582 mod: the modulus expression. 1583 1584 Returns: 1585 A `Constraint` object. 1586 """ 1587 ct = Constraint(self) 1588 model_ct = self.__model.constraints[ct.index] 1589 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 1590 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 1591 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 1592 return ct
Adds target = expr % mod
.
It uses the C convention, that is the result is the remainder of the integral division rounded towards 0.
For example:
* 10 % 3 = 1
* -10 % 3 = -1
* 10 % -3 = 1
* -10 % -3 = -1
Arguments:
- target: the target expression.
- expr: the expression to compute the modulo of.
- mod: the modulus expression.
Returns:
A
Constraint
object.
1594 def add_multiplication_equality( 1595 self, 1596 target: LinearExprT, 1597 *expressions: Union[Iterable[LinearExprT], LinearExprT], 1598 ) -> Constraint: 1599 """Adds `target == expressions[0] * .. * expressions[n]`.""" 1600 ct = Constraint(self) 1601 model_ct = self.__model.constraints[ct.index] 1602 model_ct.int_prod.exprs.extend( 1603 [ 1604 self.parse_linear_expression(expr) 1605 for expr in expand_generator_or_tuple(expressions) 1606 ] 1607 ) 1608 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 1609 return ct
Adds target == expressions[0] * .. * expressions[n]
.
1613 def new_interval_var( 1614 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 1615 ) -> IntervalVar: 1616 """Creates an interval variable from start, size, and end. 1617 1618 An interval variable is a constraint, that is itself used in other 1619 constraints like NoOverlap. 1620 1621 Internally, it ensures that `start + size == end`. 1622 1623 Args: 1624 start: The start of the interval. It must be of the form a * var + b. 1625 size: The size of the interval. It must be of the form a * var + b. 1626 end: The end of the interval. It must be of the form a * var + b. 1627 name: The name of the interval variable. 1628 1629 Returns: 1630 An `IntervalVar` object. 1631 """ 1632 1633 start_expr = self.parse_linear_expression(start) 1634 size_expr = self.parse_linear_expression(size) 1635 end_expr = self.parse_linear_expression(end) 1636 if len(start_expr.vars) > 1: 1637 raise TypeError( 1638 "cp_model.new_interval_var: start must be 1-var affine or constant." 1639 ) 1640 if len(size_expr.vars) > 1: 1641 raise TypeError( 1642 "cp_model.new_interval_var: size must be 1-var affine or constant." 1643 ) 1644 if len(end_expr.vars) > 1: 1645 raise TypeError( 1646 "cp_model.new_interval_var: end must be 1-var affine or constant." 1647 ) 1648 return IntervalVar( 1649 self.__model, 1650 self.__var_list, 1651 start_expr, 1652 size_expr, 1653 end_expr, 1654 None, 1655 name, 1656 )
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.
1658 def new_interval_var_series( 1659 self, 1660 name: str, 1661 index: pd.Index, 1662 starts: Union[LinearExprT, pd.Series], 1663 sizes: Union[LinearExprT, pd.Series], 1664 ends: Union[LinearExprT, pd.Series], 1665 ) -> pd.Series: 1666 """Creates a series of interval variables with the given name. 1667 1668 Args: 1669 name (str): Required. The name of the variable set. 1670 index (pd.Index): Required. The index to use for the variable set. 1671 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1672 set. If a `pd.Series` is passed in, it will be based on the 1673 corresponding values of the pd.Series. 1674 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 1675 set. If a `pd.Series` is passed in, it will be based on the 1676 corresponding values of the pd.Series. 1677 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 1678 set. If a `pd.Series` is passed in, it will be based on the 1679 corresponding values of the pd.Series. 1680 1681 Returns: 1682 pd.Series: The interval variable set indexed by its corresponding 1683 dimensions. 1684 1685 Raises: 1686 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1687 ValueError: if the `name` is not a valid identifier or already exists. 1688 ValueError: if the all the indexes do not match. 1689 """ 1690 if not isinstance(index, pd.Index): 1691 raise TypeError("Non-index object is used as index") 1692 if not name.isidentifier(): 1693 raise ValueError(f"name={name!r} is not a valid identifier") 1694 1695 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1696 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 1697 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 1698 interval_array = [] 1699 for i in index: 1700 interval_array.append( 1701 self.new_interval_var( 1702 start=starts[i], 1703 size=sizes[i], 1704 end=ends[i], 1705 name=f"{name}[{i}]", 1706 ) 1707 ) 1708 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.
1710 def new_fixed_size_interval_var( 1711 self, start: LinearExprT, size: IntegralT, name: str 1712 ) -> IntervalVar: 1713 """Creates an interval variable from start, and a fixed size. 1714 1715 An interval variable is a constraint, that is itself used in other 1716 constraints like NoOverlap. 1717 1718 Args: 1719 start: The start of the interval. It must be of the form a * var + b. 1720 size: The size of the interval. It must be an integer value. 1721 name: The name of the interval variable. 1722 1723 Returns: 1724 An `IntervalVar` object. 1725 """ 1726 start_expr = self.parse_linear_expression(start) 1727 size_expr = self.parse_linear_expression(size) 1728 end_expr = self.parse_linear_expression(start + size) 1729 if len(start_expr.vars) > 1: 1730 raise TypeError( 1731 "cp_model.new_interval_var: start must be affine or constant." 1732 ) 1733 return IntervalVar( 1734 self.__model, 1735 self.__var_list, 1736 start_expr, 1737 size_expr, 1738 end_expr, 1739 None, 1740 name, 1741 )
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.
1743 def new_fixed_size_interval_var_series( 1744 self, 1745 name: str, 1746 index: pd.Index, 1747 starts: Union[LinearExprT, pd.Series], 1748 sizes: Union[IntegralT, pd.Series], 1749 ) -> pd.Series: 1750 """Creates a series of interval variables with the given name. 1751 1752 Args: 1753 name (str): Required. The name of the variable set. 1754 index (pd.Index): Required. The index to use for the variable set. 1755 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1756 set. If a `pd.Series` is passed in, it will be based on the 1757 corresponding values of the pd.Series. 1758 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 1759 the set. If a `pd.Series` is passed in, it will be based on the 1760 corresponding values of the pd.Series. 1761 1762 Returns: 1763 pd.Series: The interval variable set indexed by its corresponding 1764 dimensions. 1765 1766 Raises: 1767 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1768 ValueError: if the `name` is not a valid identifier or already exists. 1769 ValueError: if the all the indexes do not match. 1770 """ 1771 if not isinstance(index, pd.Index): 1772 raise TypeError("Non-index object is used as index") 1773 if not name.isidentifier(): 1774 raise ValueError(f"name={name!r} is not a valid identifier") 1775 1776 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1777 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 1778 interval_array = [] 1779 for i in index: 1780 interval_array.append( 1781 self.new_fixed_size_interval_var( 1782 start=starts[i], 1783 size=sizes[i], 1784 name=f"{name}[{i}]", 1785 ) 1786 ) 1787 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.
1789 def new_optional_interval_var( 1790 self, 1791 start: LinearExprT, 1792 size: LinearExprT, 1793 end: LinearExprT, 1794 is_present: LiteralT, 1795 name: str, 1796 ) -> IntervalVar: 1797 """Creates an optional interval var from start, size, end, and is_present. 1798 1799 An optional interval variable is a constraint, that is itself used in other 1800 constraints like NoOverlap. This constraint is protected by a presence 1801 literal that indicates if it is active or not. 1802 1803 Internally, it ensures that `is_present` implies `start + size == 1804 end`. 1805 1806 Args: 1807 start: The start of the interval. It must be of the form a * var + b. 1808 size: The size of the interval. It must be of the form a * var + b. 1809 end: The end of the interval. It must be of the form a * var + b. 1810 is_present: A literal that indicates if the interval is active or not. A 1811 inactive interval is simply ignored by all constraints. 1812 name: The name of the interval variable. 1813 1814 Returns: 1815 An `IntervalVar` object. 1816 """ 1817 1818 # Creates the IntervalConstraintProto object. 1819 is_present_index = self.get_or_make_boolean_index(is_present) 1820 start_expr = self.parse_linear_expression(start) 1821 size_expr = self.parse_linear_expression(size) 1822 end_expr = self.parse_linear_expression(end) 1823 if len(start_expr.vars) > 1: 1824 raise TypeError( 1825 "cp_model.new_interval_var: start must be affine or constant." 1826 ) 1827 if len(size_expr.vars) > 1: 1828 raise TypeError( 1829 "cp_model.new_interval_var: size must be affine or constant." 1830 ) 1831 if len(end_expr.vars) > 1: 1832 raise TypeError( 1833 "cp_model.new_interval_var: end must be affine or constant." 1834 ) 1835 return IntervalVar( 1836 self.__model, 1837 self.__var_list, 1838 start_expr, 1839 size_expr, 1840 end_expr, 1841 is_present_index, 1842 name, 1843 )
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.
1845 def new_optional_interval_var_series( 1846 self, 1847 name: str, 1848 index: pd.Index, 1849 starts: Union[LinearExprT, pd.Series], 1850 sizes: Union[LinearExprT, pd.Series], 1851 ends: Union[LinearExprT, pd.Series], 1852 are_present: Union[LiteralT, pd.Series], 1853 ) -> pd.Series: 1854 """Creates a series of interval variables with the given name. 1855 1856 Args: 1857 name (str): Required. The name of the variable set. 1858 index (pd.Index): Required. The index to use for the variable set. 1859 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1860 set. If a `pd.Series` is passed in, it will be based on the 1861 corresponding values of the pd.Series. 1862 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 1863 set. If a `pd.Series` is passed in, it will be based on the 1864 corresponding values of the pd.Series. 1865 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 1866 set. If a `pd.Series` is passed in, it will be based on the 1867 corresponding values of the pd.Series. 1868 are_present (Union[LiteralT, pd.Series]): The performed literal of each 1869 interval in the set. If a `pd.Series` is passed in, it will be based on 1870 the corresponding values of the pd.Series. 1871 1872 Returns: 1873 pd.Series: The interval variable set indexed by its corresponding 1874 dimensions. 1875 1876 Raises: 1877 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1878 ValueError: if the `name` is not a valid identifier or already exists. 1879 ValueError: if the all the indexes do not match. 1880 """ 1881 if not isinstance(index, pd.Index): 1882 raise TypeError("Non-index object is used as index") 1883 if not name.isidentifier(): 1884 raise ValueError(f"name={name!r} is not a valid identifier") 1885 1886 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1887 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 1888 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 1889 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 1890 1891 interval_array = [] 1892 for i in index: 1893 interval_array.append( 1894 self.new_optional_interval_var( 1895 start=starts[i], 1896 size=sizes[i], 1897 end=ends[i], 1898 is_present=are_present[i], 1899 name=f"{name}[{i}]", 1900 ) 1901 ) 1902 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.
1904 def new_optional_fixed_size_interval_var( 1905 self, 1906 start: LinearExprT, 1907 size: IntegralT, 1908 is_present: LiteralT, 1909 name: str, 1910 ) -> IntervalVar: 1911 """Creates an interval variable from start, and a fixed size. 1912 1913 An interval variable is a constraint, that is itself used in other 1914 constraints like NoOverlap. 1915 1916 Args: 1917 start: The start of the interval. It must be of the form a * var + b. 1918 size: The size of the interval. It must be an integer value. 1919 is_present: A literal that indicates if the interval is active or not. A 1920 inactive interval is simply ignored by all constraints. 1921 name: The name of the interval variable. 1922 1923 Returns: 1924 An `IntervalVar` object. 1925 """ 1926 start_expr = self.parse_linear_expression(start) 1927 size_expr = self.parse_linear_expression(size) 1928 end_expr = self.parse_linear_expression(start + size) 1929 if len(start_expr.vars) > 1: 1930 raise TypeError( 1931 "cp_model.new_interval_var: start must be affine or constant." 1932 ) 1933 is_present_index = self.get_or_make_boolean_index(is_present) 1934 return IntervalVar( 1935 self.__model, 1936 self.__var_list, 1937 start_expr, 1938 size_expr, 1939 end_expr, 1940 is_present_index, 1941 name, 1942 )
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.
1944 def new_optional_fixed_size_interval_var_series( 1945 self, 1946 name: str, 1947 index: pd.Index, 1948 starts: Union[LinearExprT, pd.Series], 1949 sizes: Union[IntegralT, pd.Series], 1950 are_present: Union[LiteralT, pd.Series], 1951 ) -> pd.Series: 1952 """Creates a series of interval variables with the given name. 1953 1954 Args: 1955 name (str): Required. The name of the variable set. 1956 index (pd.Index): Required. The index to use for the variable set. 1957 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1958 set. If a `pd.Series` is passed in, it will be based on the 1959 corresponding values of the pd.Series. 1960 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 1961 the set. If a `pd.Series` is passed in, it will be based on the 1962 corresponding values of the pd.Series. 1963 are_present (Union[LiteralT, pd.Series]): The performed literal of each 1964 interval in the set. If a `pd.Series` is passed in, it will be based on 1965 the corresponding values of the pd.Series. 1966 1967 Returns: 1968 pd.Series: The interval variable set indexed by its corresponding 1969 dimensions. 1970 1971 Raises: 1972 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1973 ValueError: if the `name` is not a valid identifier or already exists. 1974 ValueError: if the all the indexes do not match. 1975 """ 1976 if not isinstance(index, pd.Index): 1977 raise TypeError("Non-index object is used as index") 1978 if not name.isidentifier(): 1979 raise ValueError(f"name={name!r} is not a valid identifier") 1980 1981 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1982 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 1983 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 1984 interval_array = [] 1985 for i in index: 1986 interval_array.append( 1987 self.new_optional_fixed_size_interval_var( 1988 start=starts[i], 1989 size=sizes[i], 1990 is_present=are_present[i], 1991 name=f"{name}[{i}]", 1992 ) 1993 ) 1994 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.
1996 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 1997 """Adds NoOverlap(interval_vars). 1998 1999 A NoOverlap constraint ensures that all present intervals do not overlap 2000 in time. 2001 2002 Args: 2003 interval_vars: The list of interval variables to constrain. 2004 2005 Returns: 2006 An instance of the `Constraint` class. 2007 """ 2008 ct = Constraint(self) 2009 model_ct = self.__model.constraints[ct.index] 2010 model_ct.no_overlap.intervals.extend( 2011 [self.get_interval_index(x) for x in interval_vars] 2012 ) 2013 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.
2015 def add_no_overlap_2d( 2016 self, 2017 x_intervals: Iterable[IntervalVar], 2018 y_intervals: Iterable[IntervalVar], 2019 ) -> Constraint: 2020 """Adds NoOverlap2D(x_intervals, y_intervals). 2021 2022 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2023 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2024 by two intervals which represent its projection onto the X and Y axis. 2025 2026 Furthermore, one box is optional if at least one of the x or y interval is 2027 optional. 2028 2029 Args: 2030 x_intervals: The X coordinates of the rectangles. 2031 y_intervals: The Y coordinates of the rectangles. 2032 2033 Returns: 2034 An instance of the `Constraint` class. 2035 """ 2036 ct = Constraint(self) 2037 model_ct = self.__model.constraints[ct.index] 2038 model_ct.no_overlap_2d.x_intervals.extend( 2039 [self.get_interval_index(x) for x in x_intervals] 2040 ) 2041 model_ct.no_overlap_2d.y_intervals.extend( 2042 [self.get_interval_index(x) for x in y_intervals] 2043 ) 2044 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.
2046 def add_cumulative( 2047 self, 2048 intervals: Iterable[IntervalVar], 2049 demands: Iterable[LinearExprT], 2050 capacity: LinearExprT, 2051 ) -> Constraint: 2052 """Adds Cumulative(intervals, demands, capacity). 2053 2054 This constraint enforces that: 2055 2056 for all t: 2057 sum(demands[i] 2058 if (start(intervals[i]) <= t < end(intervals[i])) and 2059 (intervals[i] is present)) <= capacity 2060 2061 Args: 2062 intervals: The list of intervals. 2063 demands: The list of demands for each interval. Each demand must be >= 0. 2064 Each demand can be a 1-var affine expression (a * x + b). 2065 capacity: The maximum capacity of the cumulative constraint. It can be a 2066 1-var affine expression (a * x + b). 2067 2068 Returns: 2069 An instance of the `Constraint` class. 2070 """ 2071 cumulative = Constraint(self) 2072 model_ct = self.__model.constraints[cumulative.index] 2073 model_ct.cumulative.intervals.extend( 2074 [self.get_interval_index(x) for x in intervals] 2075 ) 2076 for d in demands: 2077 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2078 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2079 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.
2082 def clone(self) -> "CpModel": 2083 """Reset the model, and creates a new one from a CpModelProto instance.""" 2084 clone = CpModel() 2085 clone.proto.CopyFrom(self.proto) 2086 clone.rebuild_var_and_constant_map() 2087 return clone
Reset the model, and creates a new one from a CpModelProto instance.
2089 def rebuild_var_and_constant_map(self): 2090 """Internal method used during model cloning.""" 2091 for i, var in enumerate(self.__model.variables): 2092 if len(var.domain) == 2 and var.domain[0] == var.domain[1]: 2093 self.__constant_map[var.domain[0]] = i 2094 is_boolean = ( 2095 len(var.domain) == 2 and var.domain[0] >= 0 and var.domain[1] <= 1 2096 ) 2097 self.__var_list.append(IntVar(self.__model, i, is_boolean, None))
Internal method used during model cloning.
2099 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2100 """Returns an already created Boolean variable from its index.""" 2101 result = self._get_int_var(index) 2102 if not result.is_boolean: 2103 raise ValueError( 2104 f"get_bool_var_from_proto_index: index {index} does not reference a" 2105 " boolean variable" 2106 ) 2107 return result
Returns an already created Boolean variable from its index.
2109 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2110 """Returns an already created integer variable from its index.""" 2111 return self._get_int_var(index)
Returns an already created integer variable from its index.
2113 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2114 """Returns an already created interval variable from its index.""" 2115 if index < 0 or index >= len(self.__model.constraints): 2116 raise ValueError( 2117 f"get_interval_var_from_proto_index: out of bound index {index}" 2118 ) 2119 ct = self.__model.constraints[index] 2120 if not ct.HasField("interval"): 2121 raise ValueError( 2122 f"get_interval_var_from_proto_index: index {index} does not" 2123 " reference an" + " interval variable" 2124 ) 2125 2126 return IntervalVar(self.__model, self.__var_list, index, None, None, None, None)
Returns an already created interval variable from its index.
2133 @property 2134 def proto(self) -> cp_model_pb2.CpModelProto: 2135 """Returns the underlying CpModelProto.""" 2136 return self.__model
Returns the underlying CpModelProto.
2141 def get_or_make_index(self, arg: VariableT) -> int: 2142 """Returns the index of a variable, its negation, or a number.""" 2143 if isinstance(arg, IntVar): 2144 return arg.index 2145 if isinstance(arg, IntegralTypes): 2146 return self.get_or_make_index_from_constant(arg) 2147 raise TypeError( 2148 f"NotSupported: model.get_or_make_index({type(arg).__name__!r})" 2149 )
Returns the index of a variable, its negation, or a number.
2151 def get_or_make_boolean_index(self, arg: LiteralT) -> int: 2152 """Returns an index from a boolean expression.""" 2153 if isinstance(arg, IntVar): 2154 self.assert_is_boolean_variable(arg) 2155 return arg.index 2156 if isinstance(arg, cmh.NotBooleanVariable): 2157 self.assert_is_boolean_variable(arg.negated()) 2158 return arg.index 2159 if isinstance(arg, IntegralTypes): 2160 if arg == ~int(False): 2161 return self.get_or_make_index_from_constant(1) 2162 if arg == ~int(True): 2163 return self.get_or_make_index_from_constant(0) 2164 arg = cmn.assert_is_zero_or_one(arg) 2165 return self.get_or_make_index_from_constant(arg) 2166 if cmn.is_boolean(arg): 2167 return self.get_or_make_index_from_constant(int(arg)) 2168 raise TypeError( 2169 "not supported:" f" model.get_or_make_boolean_index({type(arg).__name__!r})" 2170 )
Returns an index from a boolean expression.
2186 def parse_linear_expression( 2187 self, linear_expr: LinearExprT, negate: bool = False 2188 ) -> cp_model_pb2.LinearExpressionProto: 2189 """Returns a LinearExpressionProto built from a LinearExpr instance.""" 2190 result: cp_model_pb2.LinearExpressionProto = ( 2191 cp_model_pb2.LinearExpressionProto() 2192 ) 2193 mult = -1 if negate else 1 2194 if isinstance(linear_expr, IntegralTypes): 2195 result.offset = int(linear_expr) * mult 2196 return result 2197 2198 # Raises TypeError if linear_expr is not an integer. 2199 flat_expr = cmh.FlatIntExpr(linear_expr) 2200 result.offset = flat_expr.offset * mult 2201 for var in flat_expr.vars: 2202 result.vars.append(var.index) 2203 for coeff in flat_expr.coeffs: 2204 result.coeffs.append(coeff * mult) 2205 return result
Returns a LinearExpressionProto built from a LinearExpr instance.
2239 def minimize(self, obj: ObjLinearExprT): 2240 """Sets the objective of the model to minimize(obj).""" 2241 self._set_objective(obj, minimize=True)
Sets the objective of the model to minimize(obj).
2243 def maximize(self, obj: ObjLinearExprT): 2244 """Sets the objective of the model to maximize(obj).""" 2245 self._set_objective(obj, minimize=False)
Sets the objective of the model to maximize(obj).
2256 def add_decision_strategy( 2257 self, 2258 variables: Sequence[IntVar], 2259 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2260 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2261 ) -> None: 2262 """Adds a search strategy to the model. 2263 2264 Args: 2265 variables: a list of variables this strategy will assign. 2266 var_strategy: heuristic to choose the next variable to assign. 2267 domain_strategy: heuristic to reduce the domain of the selected variable. 2268 Currently, this is advanced code: the union of all strategies added to 2269 the model must be complete, i.e. instantiates all variables. Otherwise, 2270 solve() will fail. 2271 """ 2272 2273 strategy: cp_model_pb2.DecisionStrategyProto = ( 2274 self.__model.search_strategy.add() 2275 ) 2276 for v in variables: 2277 expr = strategy.exprs.add() 2278 if v.index >= 0: 2279 expr.vars.append(v.index) 2280 expr.coeffs.append(1) 2281 else: 2282 expr.vars.append(self.negated(v.index)) 2283 expr.coeffs.append(-1) 2284 expr.offset = 1 2285 2286 strategy.variable_selection_strategy = var_strategy 2287 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.
2289 def model_stats(self) -> str: 2290 """Returns a string containing some model statistics.""" 2291 return cmh.CpSatHelper.model_stats(self.__model)
Returns a string containing some model statistics.
2293 def validate(self) -> str: 2294 """Returns a string indicating that the model is invalid.""" 2295 return cmh.CpSatHelper.validate_model(self.__model)
Returns a string indicating that the model is invalid.
2297 def export_to_file(self, file: str) -> bool: 2298 """Write the model as a protocol buffer to 'file'. 2299 2300 Args: 2301 file: file to write the model to. If the filename ends with 'txt', the 2302 model will be written as a text file, otherwise, the binary format will 2303 be used. 2304 2305 Returns: 2306 True if the model was correctly written. 2307 """ 2308 return cmh.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.
2310 def remove_all_names(self) -> None: 2311 """Removes all names from the model.""" 2312 self.__model.ClearField("name") 2313 for v in self.__model.variables: 2314 v.ClearField("name") 2315 for c in self.__model.constraints: 2316 c.ClearField("name")
Removes all names from the model.
2324 def add_hint(self, var, value) -> None: 2325 """Adds 'var == value' as a hint to the solver.""" 2326 if var.index >= 0: 2327 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2328 self.__model.solution_hint.values.append(int(value)) 2329 else: 2330 self.__model.solution_hint.vars.append(self.negated(var.index)) 2331 self.__model.solution_hint.values.append(int(not value))
Adds 'var == value' as a hint to the solver.
2333 def clear_hints(self): 2334 """Removes any solution hint from the model.""" 2335 self.__model.ClearField("solution_hint")
Removes any solution hint from the model.
2337 def add_assumption(self, lit: LiteralT) -> None: 2338 """Adds the literal to the model as assumptions.""" 2339 self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
Adds the literal to the model as assumptions.
2341 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2342 """Adds the literals to the model as assumptions.""" 2343 for lit in literals: 2344 self.add_assumption(lit)
Adds the literals to the model as assumptions.
2346 def clear_assumptions(self) -> None: 2347 """Removes all assumptions from the model.""" 2348 self.__model.ClearField("assumptions")
Removes all assumptions from the model.
2351 def assert_is_boolean_variable(self, x: LiteralT) -> None: 2352 if isinstance(x, IntVar): 2353 var = self.__model.variables[x.index] 2354 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: 2355 raise TypeError( 2356 f"TypeError: {type(x).__name__!r} is not a boolean variable" 2357 ) 2358 elif not isinstance(x, cmh.NotBooleanVariable): 2359 raise TypeError( 2360 f"TypeError: {type(x).__name__!r} is not a boolean variable" 2361 )
684 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: 685 """Create an integer variable with domain [lb, ub]. 686 687 The CP-SAT solver is limited to integer variables. If you have fractional 688 values, scale them up so that they become integers; if you have strings, 689 encode them as integers. 690 691 Args: 692 lb: Lower bound for the variable. 693 ub: Upper bound for the variable. 694 name: The name of the variable. 695 696 Returns: 697 a variable whose domain is [lb, ub]. 698 """ 699 domain_is_boolean = lb >= 0 and ub <= 1 700 return self._append_int_var( 701 IntVar( 702 self.__model, 703 sorted_interval_list.Domain(lb, ub), 704 domain_is_boolean, 705 name, 706 ) 707 )
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].
709 def new_int_var_from_domain( 710 self, domain: sorted_interval_list.Domain, name: str 711 ) -> IntVar: 712 """Create an integer variable from a domain. 713 714 A domain is a set of integers specified by a collection of intervals. 715 For example, `model.new_int_var_from_domain(cp_model. 716 Domain.from_intervals([[1, 2], [4, 6]]), 'x')` 717 718 Args: 719 domain: An instance of the Domain class. 720 name: The name of the variable. 721 722 Returns: 723 a variable whose domain is the given domain. 724 """ 725 domain_is_boolean = domain.min() >= 0 and domain.max() <= 1 726 return self._append_int_var( 727 IntVar(self.__model, domain, domain_is_boolean, name) 728 )
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.
730 def new_bool_var(self, name: str) -> IntVar: 731 """Creates a 0-1 variable with the given name.""" 732 return self._append_int_var( 733 IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name) 734 )
Creates a 0-1 variable with the given name.
736 def new_constant(self, value: IntegralT) -> IntVar: 737 """Declares a constant integer.""" 738 index: int = self.get_or_make_index_from_constant(value) 739 return self._get_int_var(index)
Declares a constant integer.
741 def new_int_var_series( 742 self, 743 name: str, 744 index: pd.Index, 745 lower_bounds: Union[IntegralT, pd.Series], 746 upper_bounds: Union[IntegralT, pd.Series], 747 ) -> pd.Series: 748 """Creates a series of (scalar-valued) variables with the given name. 749 750 Args: 751 name (str): Required. The name of the variable set. 752 index (pd.Index): Required. The index to use for the variable set. 753 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the 754 set. If a `pd.Series` is passed in, it will be based on the 755 corresponding values of the pd.Series. 756 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the 757 set. If a `pd.Series` is passed in, it will be based on the 758 corresponding values of the pd.Series. 759 760 Returns: 761 pd.Series: The variable set indexed by its corresponding dimensions. 762 763 Raises: 764 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 765 ValueError: if the `name` is not a valid identifier or already exists. 766 ValueError: if the `lowerbound` is greater than the `upperbound`. 767 ValueError: if the index of `lower_bound`, or `upper_bound` does not match 768 the input index. 769 """ 770 if not isinstance(index, pd.Index): 771 raise TypeError("Non-index object is used as index") 772 if not name.isidentifier(): 773 raise ValueError(f"name={name!r} is not a valid identifier") 774 if ( 775 isinstance(lower_bounds, IntegralTypes) 776 and isinstance(upper_bounds, IntegralTypes) 777 and lower_bounds > upper_bounds 778 ): 779 raise ValueError( 780 f"lower_bound={lower_bounds} is greater than" 781 f" upper_bound={upper_bounds} for variable set={name}" 782 ) 783 784 lower_bounds = _convert_to_integral_series_and_validate_index( 785 lower_bounds, index 786 ) 787 upper_bounds = _convert_to_integral_series_and_validate_index( 788 upper_bounds, index 789 ) 790 return pd.Series( 791 index=index, 792 data=[ 793 # pylint: disable=g-complex-comprehension 794 self._append_int_var( 795 IntVar( 796 model=self.__model, 797 name=f"{name}[{i}]", 798 domain=sorted_interval_list.Domain( 799 lower_bounds[i], upper_bounds[i] 800 ), 801 is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1, 802 ) 803 ) 804 for i in index 805 ], 806 )
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.
808 def new_bool_var_series( 809 self, 810 name: str, 811 index: pd.Index, 812 ) -> pd.Series: 813 """Creates a series of (scalar-valued) variables with the given name. 814 815 Args: 816 name (str): Required. The name of the variable set. 817 index (pd.Index): Required. The index to use for the variable set. 818 819 Returns: 820 pd.Series: The variable set indexed by its corresponding dimensions. 821 822 Raises: 823 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 824 ValueError: if the `name` is not a valid identifier or already exists. 825 """ 826 if not isinstance(index, pd.Index): 827 raise TypeError("Non-index object is used as index") 828 if not name.isidentifier(): 829 raise ValueError(f"name={name!r} is not a valid identifier") 830 return pd.Series( 831 index=index, 832 data=[ 833 # pylint: disable=g-complex-comprehension 834 self._append_int_var( 835 IntVar( 836 model=self.__model, 837 name=f"{name}[{i}]", 838 domain=sorted_interval_list.Domain(0, 1), 839 is_boolean=True, 840 ) 841 ) 842 for i in index 843 ], 844 )
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.
848 def add_linear_constraint( 849 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT 850 ) -> Constraint: 851 """Adds the constraint: `lb <= linear_expr <= ub`.""" 852 return self.add_linear_expression_in_domain( 853 linear_expr, sorted_interval_list.Domain(lb, ub) 854 )
Adds the constraint: lb <= linear_expr <= ub
.
856 def add_linear_expression_in_domain( 857 self, 858 linear_expr: LinearExprT, 859 domain: sorted_interval_list.Domain, 860 ) -> Constraint: 861 """Adds the constraint: `linear_expr` in `domain`.""" 862 if isinstance(linear_expr, LinearExpr): 863 ble = BoundedLinearExpression(linear_expr, domain) 864 if not ble.ok: 865 raise TypeError( 866 "Cannot add a linear expression containing floating point" 867 f" coefficients or constants: {type(linear_expr).__name__!r}" 868 ) 869 return self.add(ble) 870 if isinstance(linear_expr, IntegralTypes): 871 if not domain.contains(int(linear_expr)): 872 return self.add_bool_or([]) # Evaluate to false. 873 else: 874 return self.add_bool_and([]) # Evaluate to true. 875 raise TypeError( 876 "not supported:" 877 f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})" 878 )
Adds the constraint: linear_expr
in domain
.
880 def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint: 881 """Adds a `BoundedLinearExpression` to the model. 882 883 Args: 884 ct: A [`BoundedLinearExpression`](#boundedlinearexpression). 885 886 Returns: 887 An instance of the `Constraint` class. 888 889 Raises: 890 TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean. 891 """ 892 if isinstance(ct, BoundedLinearExpression): 893 result = Constraint(self) 894 model_ct = self.__model.constraints[result.index] 895 for var in ct.vars: 896 model_ct.linear.vars.append(var.index) 897 model_ct.linear.coeffs.extend(ct.coeffs) 898 model_ct.linear.domain.extend( 899 [ 900 cmn.capped_subtraction(x, ct.offset) 901 for x in ct.bounds.flattened_intervals() 902 ] 903 ) 904 return result 905 if ct and cmn.is_boolean(ct): 906 return self.add_bool_or([True]) 907 if not ct and cmn.is_boolean(ct): 908 return self.add_bool_or([]) # Evaluate to false. 909 raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})")
Adds a BoundedLinearExpression
to the model.
Arguments:
- ct: A
BoundedLinearExpression
.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If the
ct
is not aBoundedLinearExpression
or a Boolean.
919 def add_all_different(self, *expressions): 920 """Adds AllDifferent(expressions). 921 922 This constraint forces all expressions to have different values. 923 924 Args: 925 *expressions: simple expressions of the form a * var + constant. 926 927 Returns: 928 An instance of the `Constraint` class. 929 """ 930 ct = Constraint(self) 931 model_ct = self.__model.constraints[ct.index] 932 expanded = expand_generator_or_tuple(expressions) 933 model_ct.all_diff.exprs.extend( 934 self.parse_linear_expression(x) for x in expanded 935 ) 936 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.
938 def add_element( 939 self, 940 index: LinearExprT, 941 expressions: Sequence[LinearExprT], 942 target: LinearExprT, 943 ) -> Constraint: 944 """Adds the element constraint: `expressions[index] == target`. 945 946 Args: 947 index: The index of the selected expression in the array. It must be an 948 affine expression (a * var + b). 949 expressions: A list of affine expressions. 950 target: The expression constrained to be equal to the selected expression. 951 It must be an affine expression (a * var + b). 952 953 Returns: 954 An instance of the `Constraint` class. 955 """ 956 957 if not expressions: 958 raise ValueError("add_element expects a non-empty expressions array") 959 960 if isinstance(index, IntegralTypes): 961 expression: LinearExprT = list(expressions)[int(index)] 962 return self.add(expression == target) 963 964 ct = Constraint(self) 965 model_ct = self.__model.constraints[ct.index] 966 model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index)) 967 model_ct.element.exprs.extend( 968 [self.parse_linear_expression(e) for e in expressions] 969 ) 970 model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target)) 971 return ct
Adds the element constraint: expressions[index] == target
.
Arguments:
- index: The index of the selected expression in the array. It must be an affine expression (a * var + b).
- expressions: A list of affine expressions.
- target: The expression constrained to be equal to the selected expression. It must be an affine expression (a * var + b).
Returns:
An instance of the
Constraint
class.
973 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 974 """Adds Circuit(arcs). 975 976 Adds a circuit constraint from a sparse list of arcs that encode the graph. 977 978 A circuit is a unique Hamiltonian cycle in a subgraph of the total 979 graph. In case a node 'i' is not in the cycle, then there must be a 980 loop arc 'i -> i' associated with a true literal. Otherwise 981 this constraint will fail. 982 983 Args: 984 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 985 literal). The arc is selected in the circuit if the literal is true. 986 Both source_node and destination_node must be integers between 0 and the 987 number of nodes - 1. 988 989 Returns: 990 An instance of the `Constraint` class. 991 992 Raises: 993 ValueError: If the list of arcs is empty. 994 """ 995 if not arcs: 996 raise ValueError("add_circuit expects a non-empty array of arcs") 997 ct = Constraint(self) 998 model_ct = self.__model.constraints[ct.index] 999 for arc in arcs: 1000 model_ct.circuit.tails.append(arc[0]) 1001 model_ct.circuit.heads.append(arc[1]) 1002 model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2])) 1003 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 cycle in a subgraph of the total graph. In case a node 'i' is not in the cycle, 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.
1005 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: 1006 """Adds a multiple circuit constraint, aka the 'VRP' constraint. 1007 1008 The direct graph where arc #i (from tails[i] to head[i]) is present iff 1009 literals[i] is true must satisfy this set of properties: 1010 - #incoming arcs == 1 except for node 0. 1011 - #outgoing arcs == 1 except for node 0. 1012 - for node zero, #incoming arcs == #outgoing arcs. 1013 - There are no duplicate arcs. 1014 - Self-arcs are allowed except for node 0. 1015 - There is no cycle in this graph, except through node 0. 1016 1017 Args: 1018 arcs: a list of arcs. An arc is a tuple (source_node, destination_node, 1019 literal). The arc is selected in the circuit if the literal is true. 1020 Both source_node and destination_node must be integers between 0 and the 1021 number of nodes - 1. 1022 1023 Returns: 1024 An instance of the `Constraint` class. 1025 1026 Raises: 1027 ValueError: If the list of arcs is empty. 1028 """ 1029 if not arcs: 1030 raise ValueError("add_multiple_circuit expects a non-empty array of arcs") 1031 ct = Constraint(self) 1032 model_ct = self.__model.constraints[ct.index] 1033 for arc in arcs: 1034 model_ct.routes.tails.append(arc[0]) 1035 model_ct.routes.heads.append(arc[1]) 1036 model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2])) 1037 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.
1039 def add_allowed_assignments( 1040 self, 1041 expressions: Sequence[LinearExprT], 1042 tuples_list: Iterable[Sequence[IntegralT]], 1043 ) -> Constraint: 1044 """Adds AllowedAssignments(expressions, tuples_list). 1045 1046 An AllowedAssignments constraint is a constraint on an array of affine 1047 expressions, which requires that when all expressions are assigned values, 1048 the 1049 resulting array equals one of the tuples in `tuple_list`. 1050 1051 Args: 1052 expressions: A list of affine expressions (a * var + b). 1053 tuples_list: A list of admissible tuples. Each tuple must have the same 1054 length as the expressions, and the ith value of a tuple corresponds to 1055 the ith expression. 1056 1057 Returns: 1058 An instance of the `Constraint` class. 1059 1060 Raises: 1061 TypeError: If a tuple does not have the same size as the list of 1062 expressions. 1063 ValueError: If the array of expressions is empty. 1064 """ 1065 1066 if not expressions: 1067 raise ValueError( 1068 "add_allowed_assignments expects a non-empty expressions array" 1069 ) 1070 1071 ct: Constraint = Constraint(self) 1072 model_ct = self.__model.constraints[ct.index] 1073 model_ct.table.exprs.extend( 1074 [self.parse_linear_expression(e) for e in expressions] 1075 ) 1076 arity: int = len(expressions) 1077 for one_tuple in tuples_list: 1078 if len(one_tuple) != arity: 1079 raise TypeError(f"Tuple {one_tuple!r} has the wrong arity") 1080 1081 # duck-typing (no explicit type checks here) 1082 try: 1083 for one_tuple in tuples_list: 1084 model_ct.table.values.extend(one_tuple) 1085 except ValueError as ex: 1086 raise TypeError( 1087 "add_xxx_assignment: Not an integer or does not fit in an int64_t:" 1088 f" {type(ex.args).__name__!r}" 1089 ) from ex 1090 1091 return ct
Adds AllowedAssignments(expressions, tuples_list).
An AllowedAssignments constraint is a constraint on an array of affine
expressions, which requires that when all expressions are assigned values,
the
resulting array equals one of the tuples in tuple_list
.
Arguments:
- expressions: A list of affine expressions (a * var + b).
- tuples_list: A list of admissible tuples. Each tuple must have the same length as the expressions, and the ith value of a tuple corresponds to the ith expression.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If a tuple does not have the same size as the list of expressions.
- ValueError: If the array of expressions is empty.
1093 def add_forbidden_assignments( 1094 self, 1095 expressions: Sequence[LinearExprT], 1096 tuples_list: Iterable[Sequence[IntegralT]], 1097 ) -> Constraint: 1098 """Adds add_forbidden_assignments(expressions, [tuples_list]). 1099 1100 A ForbiddenAssignments constraint is a constraint on an array of affine 1101 expressions where the list of impossible combinations is provided in the 1102 tuples list. 1103 1104 Args: 1105 expressions: A list of affine expressions (a * var + b). 1106 tuples_list: A list of forbidden tuples. Each tuple must have the same 1107 length as the expressions, and the *i*th value of a tuple corresponds to 1108 the *i*th expression. 1109 1110 Returns: 1111 An instance of the `Constraint` class. 1112 1113 Raises: 1114 TypeError: If a tuple does not have the same size as the list of 1115 expressions. 1116 ValueError: If the array of expressions is empty. 1117 """ 1118 1119 if not expressions: 1120 raise ValueError( 1121 "add_forbidden_assignments expects a non-empty expressions array" 1122 ) 1123 1124 index: int = len(self.__model.constraints) 1125 ct: Constraint = self.add_allowed_assignments(expressions, tuples_list) 1126 self.__model.constraints[index].table.negated = True 1127 return ct
Adds add_forbidden_assignments(expressions, [tuples_list]).
A ForbiddenAssignments constraint is a constraint on an array of affine expressions where the list of impossible combinations is provided in the tuples list.
Arguments:
- expressions: A list of affine expressions (a * var + b).
- tuples_list: A list of forbidden tuples. Each tuple must have the same length as the expressions, and the ith value of a tuple corresponds to the ith expression.
Returns:
An instance of the
Constraint
class.
Raises:
- TypeError: If a tuple does not have the same size as the list of expressions.
- ValueError: If the array of expressions is empty.
1129 def add_automaton( 1130 self, 1131 transition_expressions: Sequence[LinearExprT], 1132 starting_state: IntegralT, 1133 final_states: Sequence[IntegralT], 1134 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], 1135 ) -> Constraint: 1136 """Adds an automaton constraint. 1137 1138 An automaton constraint takes a list of affine expressions (a * var + b) (of 1139 size *n*), an initial state, a set of final states, and a set of 1140 transitions. A transition is a triplet (*tail*, *transition*, *head*), where 1141 *tail* and *head* are states, and *transition* is the label of an arc from 1142 *head* to *tail*, corresponding to the value of one expression in the list 1143 of 1144 expressions. 1145 1146 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase 1147 contains the possible states of the automaton. The first state contains the 1148 initial state. The last phase contains the final states. 1149 1150 Between two consecutive phases *i* and *i* + 1, the automaton creates a set 1151 of arcs. For each transition (*tail*, *transition*, *head*), it will add 1152 an arc from the state *tail* of phase *i* and the state *head* of phase 1153 *i* + 1. This arc is labeled by the value *transition* of the expression 1154 `expressions[i]`. That is, this arc can only be selected if `expressions[i]` 1155 is assigned the value *transition*. 1156 1157 A feasible solution of this constraint is an assignment of expressions such 1158 that, starting from the initial state in phase 0, there is a path labeled by 1159 the values of the expressions that ends in one of the final states in the 1160 final phase. 1161 1162 Args: 1163 transition_expressions: A non-empty list of affine expressions (a * var + 1164 b) whose values correspond to the labels of the arcs traversed by the 1165 automaton. 1166 starting_state: The initial state of the automaton. 1167 final_states: A non-empty list of admissible final states. 1168 transition_triples: A list of transitions for the automaton, in the 1169 following format (current_state, variable_value, next_state). 1170 1171 Returns: 1172 An instance of the `Constraint` class. 1173 1174 Raises: 1175 ValueError: if `transition_expressions`, `final_states`, or 1176 `transition_triples` are empty. 1177 """ 1178 1179 if not transition_expressions: 1180 raise ValueError( 1181 "add_automaton expects a non-empty transition_expressions array" 1182 ) 1183 if not final_states: 1184 raise ValueError("add_automaton expects some final states") 1185 1186 if not transition_triples: 1187 raise ValueError("add_automaton expects some transition triples") 1188 1189 ct = Constraint(self) 1190 model_ct = self.__model.constraints[ct.index] 1191 model_ct.automaton.exprs.extend( 1192 [self.parse_linear_expression(e) for e in transition_expressions] 1193 ) 1194 model_ct.automaton.starting_state = starting_state 1195 for v in final_states: 1196 model_ct.automaton.final_states.append(v) 1197 for t in transition_triples: 1198 if len(t) != 3: 1199 raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)") 1200 model_ct.automaton.transition_tail.append(t[0]) 1201 model_ct.automaton.transition_label.append(t[1]) 1202 model_ct.automaton.transition_head.append(t[2]) 1203 return ct
Adds an automaton constraint.
An automaton constraint takes a list of affine expressions (a * var + b) (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 expression in the list of expressions.
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 expression
expressions[i]
. That is, this arc can only be selected if expressions[i]
is assigned the value transition.
A feasible solution of this constraint is an assignment of expressions such that, starting from the initial state in phase 0, there is a path labeled by the values of the expressions that ends in one of the final states in the final phase.
Arguments:
- transition_expressions: A non-empty list of affine expressions (a * var + b) 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_expressions
,final_states
, ortransition_triples
are empty.
1205 def add_inverse( 1206 self, 1207 variables: Sequence[VariableT], 1208 inverse_variables: Sequence[VariableT], 1209 ) -> Constraint: 1210 """Adds Inverse(variables, inverse_variables). 1211 1212 An inverse constraint enforces that if `variables[i]` is assigned a value 1213 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. 1214 1215 Args: 1216 variables: An array of integer variables. 1217 inverse_variables: An array of integer variables. 1218 1219 Returns: 1220 An instance of the `Constraint` class. 1221 1222 Raises: 1223 TypeError: if variables and inverse_variables have different lengths, or 1224 if they are empty. 1225 """ 1226 1227 if not variables or not inverse_variables: 1228 raise TypeError("The Inverse constraint does not accept empty arrays") 1229 if len(variables) != len(inverse_variables): 1230 raise TypeError( 1231 "In the inverse constraint, the two array variables and" 1232 " inverse_variables must have the same length." 1233 ) 1234 ct = Constraint(self) 1235 model_ct = self.__model.constraints[ct.index] 1236 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) 1237 model_ct.inverse.f_inverse.extend( 1238 [self.get_or_make_index(x) for x in inverse_variables] 1239 ) 1240 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.
1242 def add_reservoir_constraint( 1243 self, 1244 times: Iterable[LinearExprT], 1245 level_changes: Iterable[LinearExprT], 1246 min_level: int, 1247 max_level: int, 1248 ) -> Constraint: 1249 """Adds Reservoir(times, level_changes, min_level, max_level). 1250 1251 Maintains a reservoir level within bounds. The water level starts at 0, and 1252 at any time, it must be between min_level and max_level. 1253 1254 If the affine expression `times[i]` is assigned a value t, then the current 1255 level changes by `level_changes[i]`, which is constant, at time t. 1256 1257 Note that min level must be <= 0, and the max level must be >= 0. Please 1258 use fixed level_changes to simulate initial state. 1259 1260 Therefore, at any time: 1261 sum(level_changes[i] if times[i] <= t) in [min_level, max_level] 1262 1263 Args: 1264 times: A list of 1-var affine expressions (a * x + b) which specify the 1265 time of the filling or emptying the reservoir. 1266 level_changes: A list of integer values that specifies the amount of the 1267 emptying or filling. Currently, variable demands are not supported. 1268 min_level: At any time, the level of the reservoir must be greater or 1269 equal than the min level. 1270 max_level: At any time, the level of the reservoir must be less or equal 1271 than the max level. 1272 1273 Returns: 1274 An instance of the `Constraint` class. 1275 1276 Raises: 1277 ValueError: if max_level < min_level. 1278 1279 ValueError: if max_level < 0. 1280 1281 ValueError: if min_level > 0 1282 """ 1283 1284 if max_level < min_level: 1285 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1286 1287 if max_level < 0: 1288 raise ValueError("Reservoir constraint must have a max_level >= 0") 1289 1290 if min_level > 0: 1291 raise ValueError("Reservoir constraint must have a min_level <= 0") 1292 1293 ct = Constraint(self) 1294 model_ct = self.__model.constraints[ct.index] 1295 model_ct.reservoir.time_exprs.extend( 1296 [self.parse_linear_expression(x) for x in times] 1297 ) 1298 model_ct.reservoir.level_changes.extend( 1299 [self.parse_linear_expression(x) for x in level_changes] 1300 ) 1301 model_ct.reservoir.min_level = min_level 1302 model_ct.reservoir.max_level = max_level 1303 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
1305 def add_reservoir_constraint_with_active( 1306 self, 1307 times: Iterable[LinearExprT], 1308 level_changes: Iterable[LinearExprT], 1309 actives: Iterable[LiteralT], 1310 min_level: int, 1311 max_level: int, 1312 ) -> Constraint: 1313 """Adds Reservoir(times, level_changes, actives, min_level, max_level). 1314 1315 Maintains a reservoir level within bounds. The water level starts at 0, and 1316 at any time, it must be between min_level and max_level. 1317 1318 If the variable `times[i]` is assigned a value t, and `actives[i]` is 1319 `True`, then the current level changes by `level_changes[i]`, which is 1320 constant, 1321 at time t. 1322 1323 Note that min level must be <= 0, and the max level must be >= 0. Please 1324 use fixed level_changes to simulate initial state. 1325 1326 Therefore, at any time: 1327 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, 1328 max_level] 1329 1330 1331 The array of boolean variables 'actives', if defined, indicates which 1332 actions are actually performed. 1333 1334 Args: 1335 times: A list of 1-var affine expressions (a * x + b) which specify the 1336 time of the filling or emptying the reservoir. 1337 level_changes: A list of integer values that specifies the amount of the 1338 emptying or filling. Currently, variable demands are not supported. 1339 actives: a list of boolean variables. They indicates if the 1340 emptying/refilling events actually take place. 1341 min_level: At any time, the level of the reservoir must be greater or 1342 equal than the min level. 1343 max_level: At any time, the level of the reservoir must be less or equal 1344 than the max level. 1345 1346 Returns: 1347 An instance of the `Constraint` class. 1348 1349 Raises: 1350 ValueError: if max_level < min_level. 1351 1352 ValueError: if max_level < 0. 1353 1354 ValueError: if min_level > 0 1355 """ 1356 1357 if max_level < min_level: 1358 raise ValueError("Reservoir constraint must have a max_level >= min_level") 1359 1360 if max_level < 0: 1361 raise ValueError("Reservoir constraint must have a max_level >= 0") 1362 1363 if min_level > 0: 1364 raise ValueError("Reservoir constraint must have a min_level <= 0") 1365 1366 ct = Constraint(self) 1367 model_ct = self.__model.constraints[ct.index] 1368 model_ct.reservoir.time_exprs.extend( 1369 [self.parse_linear_expression(x) for x in times] 1370 ) 1371 model_ct.reservoir.level_changes.extend( 1372 [self.parse_linear_expression(x) for x in level_changes] 1373 ) 1374 model_ct.reservoir.active_literals.extend( 1375 [self.get_or_make_boolean_index(x) for x in actives] 1376 ) 1377 model_ct.reservoir.min_level = min_level 1378 model_ct.reservoir.max_level = max_level 1379 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
1405 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: 1406 """Adds `a => b` (`a` implies `b`).""" 1407 ct = Constraint(self) 1408 model_ct = self.__model.constraints[ct.index] 1409 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) 1410 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) 1411 return ct
Adds a => b
(a
implies b
).
1419 def add_bool_or(self, *literals): 1420 """Adds `Or(literals) == true`: sum(literals) >= 1.""" 1421 ct = Constraint(self) 1422 model_ct = self.__model.constraints[ct.index] 1423 model_ct.bool_or.literals.extend( 1424 [ 1425 self.get_or_make_boolean_index(x) 1426 for x in expand_generator_or_tuple(literals) 1427 ] 1428 ) 1429 return ct
Adds Or(literals) == true
: sum(literals) >= 1.
1437 def add_at_least_one(self, *literals): 1438 """Same as `add_bool_or`: `sum(literals) >= 1`.""" 1439 return self.add_bool_or(*literals)
Same as add_bool_or
: sum(literals) >= 1
.
1447 def add_at_most_one(self, *literals): 1448 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" 1449 ct = Constraint(self) 1450 model_ct = self.__model.constraints[ct.index] 1451 model_ct.at_most_one.literals.extend( 1452 [ 1453 self.get_or_make_boolean_index(x) 1454 for x in expand_generator_or_tuple(literals) 1455 ] 1456 ) 1457 return ct
Adds AtMostOne(literals)
: sum(literals) <= 1
.
1465 def add_exactly_one(self, *literals): 1466 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" 1467 ct = Constraint(self) 1468 model_ct = self.__model.constraints[ct.index] 1469 model_ct.exactly_one.literals.extend( 1470 [ 1471 self.get_or_make_boolean_index(x) 1472 for x in expand_generator_or_tuple(literals) 1473 ] 1474 ) 1475 return ct
Adds ExactlyOne(literals)
: sum(literals) == 1
.
1483 def add_bool_and(self, *literals): 1484 """Adds `And(literals) == true`.""" 1485 ct = Constraint(self) 1486 model_ct = self.__model.constraints[ct.index] 1487 model_ct.bool_and.literals.extend( 1488 [ 1489 self.get_or_make_boolean_index(x) 1490 for x in expand_generator_or_tuple(literals) 1491 ] 1492 ) 1493 return ct
Adds And(literals) == true
.
1501 def add_bool_xor(self, *literals): 1502 """Adds `XOr(literals) == true`. 1503 1504 In contrast to add_bool_or and add_bool_and, it does not support 1505 .only_enforce_if(). 1506 1507 Args: 1508 *literals: the list of literals in the constraint. 1509 1510 Returns: 1511 An `Constraint` object. 1512 """ 1513 ct = Constraint(self) 1514 model_ct = self.__model.constraints[ct.index] 1515 model_ct.bool_xor.literals.extend( 1516 [ 1517 self.get_or_make_boolean_index(x) 1518 for x in expand_generator_or_tuple(literals) 1519 ] 1520 ) 1521 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.
1523 def add_min_equality( 1524 self, target: LinearExprT, exprs: Iterable[LinearExprT] 1525 ) -> Constraint: 1526 """Adds `target == Min(exprs)`.""" 1527 ct = Constraint(self) 1528 model_ct = self.__model.constraints[ct.index] 1529 model_ct.lin_max.exprs.extend( 1530 [self.parse_linear_expression(x, True) for x in exprs] 1531 ) 1532 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) 1533 return ct
Adds target == Min(exprs)
.
1535 def add_max_equality( 1536 self, target: LinearExprT, exprs: Iterable[LinearExprT] 1537 ) -> Constraint: 1538 """Adds `target == Max(exprs)`.""" 1539 ct = Constraint(self) 1540 model_ct = self.__model.constraints[ct.index] 1541 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) 1542 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 1543 return ct
Adds target == Max(exprs)
.
1545 def add_division_equality( 1546 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT 1547 ) -> Constraint: 1548 """Adds `target == num // denom` (integer division rounded towards 0).""" 1549 ct = Constraint(self) 1550 model_ct = self.__model.constraints[ct.index] 1551 model_ct.int_div.exprs.append(self.parse_linear_expression(num)) 1552 model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) 1553 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) 1554 return ct
Adds target == num // denom
(integer division rounded towards 0).
1556 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: 1557 """Adds `target == Abs(expr)`.""" 1558 ct = Constraint(self) 1559 model_ct = self.__model.constraints[ct.index] 1560 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) 1561 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) 1562 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) 1563 return ct
Adds target == Abs(expr)
.
1565 def add_modulo_equality( 1566 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT 1567 ) -> Constraint: 1568 """Adds `target = expr % mod`. 1569 1570 It uses the C convention, that is the result is the remainder of the 1571 integral division rounded towards 0. 1572 1573 For example: 1574 * 10 % 3 = 1 1575 * -10 % 3 = -1 1576 * 10 % -3 = 1 1577 * -10 % -3 = -1 1578 1579 Args: 1580 target: the target expression. 1581 expr: the expression to compute the modulo of. 1582 mod: the modulus expression. 1583 1584 Returns: 1585 A `Constraint` object. 1586 """ 1587 ct = Constraint(self) 1588 model_ct = self.__model.constraints[ct.index] 1589 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) 1590 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) 1591 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) 1592 return ct
Adds target = expr % mod
.
It uses the C convention, that is the result is the remainder of the integral division rounded towards 0.
For example:
* 10 % 3 = 1
* -10 % 3 = -1
* 10 % -3 = 1
* -10 % -3 = -1
Arguments:
- target: the target expression.
- expr: the expression to compute the modulo of.
- mod: the modulus expression.
Returns:
A
Constraint
object.
1594 def add_multiplication_equality( 1595 self, 1596 target: LinearExprT, 1597 *expressions: Union[Iterable[LinearExprT], LinearExprT], 1598 ) -> Constraint: 1599 """Adds `target == expressions[0] * .. * expressions[n]`.""" 1600 ct = Constraint(self) 1601 model_ct = self.__model.constraints[ct.index] 1602 model_ct.int_prod.exprs.extend( 1603 [ 1604 self.parse_linear_expression(expr) 1605 for expr in expand_generator_or_tuple(expressions) 1606 ] 1607 ) 1608 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) 1609 return ct
Adds target == expressions[0] * .. * expressions[n]
.
1613 def new_interval_var( 1614 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str 1615 ) -> IntervalVar: 1616 """Creates an interval variable from start, size, and end. 1617 1618 An interval variable is a constraint, that is itself used in other 1619 constraints like NoOverlap. 1620 1621 Internally, it ensures that `start + size == end`. 1622 1623 Args: 1624 start: The start of the interval. It must be of the form a * var + b. 1625 size: The size of the interval. It must be of the form a * var + b. 1626 end: The end of the interval. It must be of the form a * var + b. 1627 name: The name of the interval variable. 1628 1629 Returns: 1630 An `IntervalVar` object. 1631 """ 1632 1633 start_expr = self.parse_linear_expression(start) 1634 size_expr = self.parse_linear_expression(size) 1635 end_expr = self.parse_linear_expression(end) 1636 if len(start_expr.vars) > 1: 1637 raise TypeError( 1638 "cp_model.new_interval_var: start must be 1-var affine or constant." 1639 ) 1640 if len(size_expr.vars) > 1: 1641 raise TypeError( 1642 "cp_model.new_interval_var: size must be 1-var affine or constant." 1643 ) 1644 if len(end_expr.vars) > 1: 1645 raise TypeError( 1646 "cp_model.new_interval_var: end must be 1-var affine or constant." 1647 ) 1648 return IntervalVar( 1649 self.__model, 1650 self.__var_list, 1651 start_expr, 1652 size_expr, 1653 end_expr, 1654 None, 1655 name, 1656 )
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.
1658 def new_interval_var_series( 1659 self, 1660 name: str, 1661 index: pd.Index, 1662 starts: Union[LinearExprT, pd.Series], 1663 sizes: Union[LinearExprT, pd.Series], 1664 ends: Union[LinearExprT, pd.Series], 1665 ) -> pd.Series: 1666 """Creates a series of interval variables with the given name. 1667 1668 Args: 1669 name (str): Required. The name of the variable set. 1670 index (pd.Index): Required. The index to use for the variable set. 1671 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1672 set. If a `pd.Series` is passed in, it will be based on the 1673 corresponding values of the pd.Series. 1674 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 1675 set. If a `pd.Series` is passed in, it will be based on the 1676 corresponding values of the pd.Series. 1677 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 1678 set. If a `pd.Series` is passed in, it will be based on the 1679 corresponding values of the pd.Series. 1680 1681 Returns: 1682 pd.Series: The interval variable set indexed by its corresponding 1683 dimensions. 1684 1685 Raises: 1686 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1687 ValueError: if the `name` is not a valid identifier or already exists. 1688 ValueError: if the all the indexes do not match. 1689 """ 1690 if not isinstance(index, pd.Index): 1691 raise TypeError("Non-index object is used as index") 1692 if not name.isidentifier(): 1693 raise ValueError(f"name={name!r} is not a valid identifier") 1694 1695 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1696 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 1697 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 1698 interval_array = [] 1699 for i in index: 1700 interval_array.append( 1701 self.new_interval_var( 1702 start=starts[i], 1703 size=sizes[i], 1704 end=ends[i], 1705 name=f"{name}[{i}]", 1706 ) 1707 ) 1708 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.
1710 def new_fixed_size_interval_var( 1711 self, start: LinearExprT, size: IntegralT, name: str 1712 ) -> IntervalVar: 1713 """Creates an interval variable from start, and a fixed size. 1714 1715 An interval variable is a constraint, that is itself used in other 1716 constraints like NoOverlap. 1717 1718 Args: 1719 start: The start of the interval. It must be of the form a * var + b. 1720 size: The size of the interval. It must be an integer value. 1721 name: The name of the interval variable. 1722 1723 Returns: 1724 An `IntervalVar` object. 1725 """ 1726 start_expr = self.parse_linear_expression(start) 1727 size_expr = self.parse_linear_expression(size) 1728 end_expr = self.parse_linear_expression(start + size) 1729 if len(start_expr.vars) > 1: 1730 raise TypeError( 1731 "cp_model.new_interval_var: start must be affine or constant." 1732 ) 1733 return IntervalVar( 1734 self.__model, 1735 self.__var_list, 1736 start_expr, 1737 size_expr, 1738 end_expr, 1739 None, 1740 name, 1741 )
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.
1789 def new_optional_interval_var( 1790 self, 1791 start: LinearExprT, 1792 size: LinearExprT, 1793 end: LinearExprT, 1794 is_present: LiteralT, 1795 name: str, 1796 ) -> IntervalVar: 1797 """Creates an optional interval var from start, size, end, and is_present. 1798 1799 An optional interval variable is a constraint, that is itself used in other 1800 constraints like NoOverlap. This constraint is protected by a presence 1801 literal that indicates if it is active or not. 1802 1803 Internally, it ensures that `is_present` implies `start + size == 1804 end`. 1805 1806 Args: 1807 start: The start of the interval. It must be of the form a * var + b. 1808 size: The size of the interval. It must be of the form a * var + b. 1809 end: The end of the interval. It must be of the form a * var + b. 1810 is_present: A literal that indicates if the interval is active or not. A 1811 inactive interval is simply ignored by all constraints. 1812 name: The name of the interval variable. 1813 1814 Returns: 1815 An `IntervalVar` object. 1816 """ 1817 1818 # Creates the IntervalConstraintProto object. 1819 is_present_index = self.get_or_make_boolean_index(is_present) 1820 start_expr = self.parse_linear_expression(start) 1821 size_expr = self.parse_linear_expression(size) 1822 end_expr = self.parse_linear_expression(end) 1823 if len(start_expr.vars) > 1: 1824 raise TypeError( 1825 "cp_model.new_interval_var: start must be affine or constant." 1826 ) 1827 if len(size_expr.vars) > 1: 1828 raise TypeError( 1829 "cp_model.new_interval_var: size must be affine or constant." 1830 ) 1831 if len(end_expr.vars) > 1: 1832 raise TypeError( 1833 "cp_model.new_interval_var: end must be affine or constant." 1834 ) 1835 return IntervalVar( 1836 self.__model, 1837 self.__var_list, 1838 start_expr, 1839 size_expr, 1840 end_expr, 1841 is_present_index, 1842 name, 1843 )
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.
1845 def new_optional_interval_var_series( 1846 self, 1847 name: str, 1848 index: pd.Index, 1849 starts: Union[LinearExprT, pd.Series], 1850 sizes: Union[LinearExprT, pd.Series], 1851 ends: Union[LinearExprT, pd.Series], 1852 are_present: Union[LiteralT, pd.Series], 1853 ) -> pd.Series: 1854 """Creates a series of interval variables with the given name. 1855 1856 Args: 1857 name (str): Required. The name of the variable set. 1858 index (pd.Index): Required. The index to use for the variable set. 1859 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1860 set. If a `pd.Series` is passed in, it will be based on the 1861 corresponding values of the pd.Series. 1862 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the 1863 set. If a `pd.Series` is passed in, it will be based on the 1864 corresponding values of the pd.Series. 1865 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the 1866 set. If a `pd.Series` is passed in, it will be based on the 1867 corresponding values of the pd.Series. 1868 are_present (Union[LiteralT, pd.Series]): The performed literal of each 1869 interval in the set. If a `pd.Series` is passed in, it will be based on 1870 the corresponding values of the pd.Series. 1871 1872 Returns: 1873 pd.Series: The interval variable set indexed by its corresponding 1874 dimensions. 1875 1876 Raises: 1877 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1878 ValueError: if the `name` is not a valid identifier or already exists. 1879 ValueError: if the all the indexes do not match. 1880 """ 1881 if not isinstance(index, pd.Index): 1882 raise TypeError("Non-index object is used as index") 1883 if not name.isidentifier(): 1884 raise ValueError(f"name={name!r} is not a valid identifier") 1885 1886 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1887 sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) 1888 ends = _convert_to_linear_expr_series_and_validate_index(ends, index) 1889 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 1890 1891 interval_array = [] 1892 for i in index: 1893 interval_array.append( 1894 self.new_optional_interval_var( 1895 start=starts[i], 1896 size=sizes[i], 1897 end=ends[i], 1898 is_present=are_present[i], 1899 name=f"{name}[{i}]", 1900 ) 1901 ) 1902 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.
1904 def new_optional_fixed_size_interval_var( 1905 self, 1906 start: LinearExprT, 1907 size: IntegralT, 1908 is_present: LiteralT, 1909 name: str, 1910 ) -> IntervalVar: 1911 """Creates an interval variable from start, and a fixed size. 1912 1913 An interval variable is a constraint, that is itself used in other 1914 constraints like NoOverlap. 1915 1916 Args: 1917 start: The start of the interval. It must be of the form a * var + b. 1918 size: The size of the interval. It must be an integer value. 1919 is_present: A literal that indicates if the interval is active or not. A 1920 inactive interval is simply ignored by all constraints. 1921 name: The name of the interval variable. 1922 1923 Returns: 1924 An `IntervalVar` object. 1925 """ 1926 start_expr = self.parse_linear_expression(start) 1927 size_expr = self.parse_linear_expression(size) 1928 end_expr = self.parse_linear_expression(start + size) 1929 if len(start_expr.vars) > 1: 1930 raise TypeError( 1931 "cp_model.new_interval_var: start must be affine or constant." 1932 ) 1933 is_present_index = self.get_or_make_boolean_index(is_present) 1934 return IntervalVar( 1935 self.__model, 1936 self.__var_list, 1937 start_expr, 1938 size_expr, 1939 end_expr, 1940 is_present_index, 1941 name, 1942 )
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.
1944 def new_optional_fixed_size_interval_var_series( 1945 self, 1946 name: str, 1947 index: pd.Index, 1948 starts: Union[LinearExprT, pd.Series], 1949 sizes: Union[IntegralT, pd.Series], 1950 are_present: Union[LiteralT, pd.Series], 1951 ) -> pd.Series: 1952 """Creates a series of interval variables with the given name. 1953 1954 Args: 1955 name (str): Required. The name of the variable set. 1956 index (pd.Index): Required. The index to use for the variable set. 1957 starts (Union[LinearExprT, pd.Series]): The start of each interval in the 1958 set. If a `pd.Series` is passed in, it will be based on the 1959 corresponding values of the pd.Series. 1960 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in 1961 the set. If a `pd.Series` is passed in, it will be based on the 1962 corresponding values of the pd.Series. 1963 are_present (Union[LiteralT, pd.Series]): The performed literal of each 1964 interval in the set. If a `pd.Series` is passed in, it will be based on 1965 the corresponding values of the pd.Series. 1966 1967 Returns: 1968 pd.Series: The interval variable set indexed by its corresponding 1969 dimensions. 1970 1971 Raises: 1972 TypeError: if the `index` is invalid (e.g. a `DataFrame`). 1973 ValueError: if the `name` is not a valid identifier or already exists. 1974 ValueError: if the all the indexes do not match. 1975 """ 1976 if not isinstance(index, pd.Index): 1977 raise TypeError("Non-index object is used as index") 1978 if not name.isidentifier(): 1979 raise ValueError(f"name={name!r} is not a valid identifier") 1980 1981 starts = _convert_to_linear_expr_series_and_validate_index(starts, index) 1982 sizes = _convert_to_integral_series_and_validate_index(sizes, index) 1983 are_present = _convert_to_literal_series_and_validate_index(are_present, index) 1984 interval_array = [] 1985 for i in index: 1986 interval_array.append( 1987 self.new_optional_fixed_size_interval_var( 1988 start=starts[i], 1989 size=sizes[i], 1990 is_present=are_present[i], 1991 name=f"{name}[{i}]", 1992 ) 1993 ) 1994 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.
1996 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: 1997 """Adds NoOverlap(interval_vars). 1998 1999 A NoOverlap constraint ensures that all present intervals do not overlap 2000 in time. 2001 2002 Args: 2003 interval_vars: The list of interval variables to constrain. 2004 2005 Returns: 2006 An instance of the `Constraint` class. 2007 """ 2008 ct = Constraint(self) 2009 model_ct = self.__model.constraints[ct.index] 2010 model_ct.no_overlap.intervals.extend( 2011 [self.get_interval_index(x) for x in interval_vars] 2012 ) 2013 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.
2015 def add_no_overlap_2d( 2016 self, 2017 x_intervals: Iterable[IntervalVar], 2018 y_intervals: Iterable[IntervalVar], 2019 ) -> Constraint: 2020 """Adds NoOverlap2D(x_intervals, y_intervals). 2021 2022 A NoOverlap2D constraint ensures that all present rectangles do not overlap 2023 on a plane. Each rectangle is aligned with the X and Y axis, and is defined 2024 by two intervals which represent its projection onto the X and Y axis. 2025 2026 Furthermore, one box is optional if at least one of the x or y interval is 2027 optional. 2028 2029 Args: 2030 x_intervals: The X coordinates of the rectangles. 2031 y_intervals: The Y coordinates of the rectangles. 2032 2033 Returns: 2034 An instance of the `Constraint` class. 2035 """ 2036 ct = Constraint(self) 2037 model_ct = self.__model.constraints[ct.index] 2038 model_ct.no_overlap_2d.x_intervals.extend( 2039 [self.get_interval_index(x) for x in x_intervals] 2040 ) 2041 model_ct.no_overlap_2d.y_intervals.extend( 2042 [self.get_interval_index(x) for x in y_intervals] 2043 ) 2044 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.
2046 def add_cumulative( 2047 self, 2048 intervals: Iterable[IntervalVar], 2049 demands: Iterable[LinearExprT], 2050 capacity: LinearExprT, 2051 ) -> Constraint: 2052 """Adds Cumulative(intervals, demands, capacity). 2053 2054 This constraint enforces that: 2055 2056 for all t: 2057 sum(demands[i] 2058 if (start(intervals[i]) <= t < end(intervals[i])) and 2059 (intervals[i] is present)) <= capacity 2060 2061 Args: 2062 intervals: The list of intervals. 2063 demands: The list of demands for each interval. Each demand must be >= 0. 2064 Each demand can be a 1-var affine expression (a * x + b). 2065 capacity: The maximum capacity of the cumulative constraint. It can be a 2066 1-var affine expression (a * x + b). 2067 2068 Returns: 2069 An instance of the `Constraint` class. 2070 """ 2071 cumulative = Constraint(self) 2072 model_ct = self.__model.constraints[cumulative.index] 2073 model_ct.cumulative.intervals.extend( 2074 [self.get_interval_index(x) for x in intervals] 2075 ) 2076 for d in demands: 2077 model_ct.cumulative.demands.append(self.parse_linear_expression(d)) 2078 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) 2079 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.
2082 def clone(self) -> "CpModel": 2083 """Reset the model, and creates a new one from a CpModelProto instance.""" 2084 clone = CpModel() 2085 clone.proto.CopyFrom(self.proto) 2086 clone.rebuild_var_and_constant_map() 2087 return clone
Reset the model, and creates a new one from a CpModelProto instance.
2099 def get_bool_var_from_proto_index(self, index: int) -> IntVar: 2100 """Returns an already created Boolean variable from its index.""" 2101 result = self._get_int_var(index) 2102 if not result.is_boolean: 2103 raise ValueError( 2104 f"get_bool_var_from_proto_index: index {index} does not reference a" 2105 " boolean variable" 2106 ) 2107 return result
Returns an already created Boolean variable from its index.
2109 def get_int_var_from_proto_index(self, index: int) -> IntVar: 2110 """Returns an already created integer variable from its index.""" 2111 return self._get_int_var(index)
Returns an already created integer variable from its index.
2113 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: 2114 """Returns an already created interval variable from its index.""" 2115 if index < 0 or index >= len(self.__model.constraints): 2116 raise ValueError( 2117 f"get_interval_var_from_proto_index: out of bound index {index}" 2118 ) 2119 ct = self.__model.constraints[index] 2120 if not ct.HasField("interval"): 2121 raise ValueError( 2122 f"get_interval_var_from_proto_index: index {index} does not" 2123 " reference an" + " interval variable" 2124 ) 2125 2126 return IntervalVar(self.__model, self.__var_list, index, None, None, None, None)
Returns an already created interval variable from its index.
2239 def minimize(self, obj: ObjLinearExprT): 2240 """Sets the objective of the model to minimize(obj).""" 2241 self._set_objective(obj, minimize=True)
Sets the objective of the model to minimize(obj).
2243 def maximize(self, obj: ObjLinearExprT): 2244 """Sets the objective of the model to maximize(obj).""" 2245 self._set_objective(obj, minimize=False)
Sets the objective of the model to maximize(obj).
2256 def add_decision_strategy( 2257 self, 2258 variables: Sequence[IntVar], 2259 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, 2260 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, 2261 ) -> None: 2262 """Adds a search strategy to the model. 2263 2264 Args: 2265 variables: a list of variables this strategy will assign. 2266 var_strategy: heuristic to choose the next variable to assign. 2267 domain_strategy: heuristic to reduce the domain of the selected variable. 2268 Currently, this is advanced code: the union of all strategies added to 2269 the model must be complete, i.e. instantiates all variables. Otherwise, 2270 solve() will fail. 2271 """ 2272 2273 strategy: cp_model_pb2.DecisionStrategyProto = ( 2274 self.__model.search_strategy.add() 2275 ) 2276 for v in variables: 2277 expr = strategy.exprs.add() 2278 if v.index >= 0: 2279 expr.vars.append(v.index) 2280 expr.coeffs.append(1) 2281 else: 2282 expr.vars.append(self.negated(v.index)) 2283 expr.coeffs.append(-1) 2284 expr.offset = 1 2285 2286 strategy.variable_selection_strategy = var_strategy 2287 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.
2289 def model_stats(self) -> str: 2290 """Returns a string containing some model statistics.""" 2291 return cmh.CpSatHelper.model_stats(self.__model)
Returns a string containing some model statistics.
2293 def validate(self) -> str: 2294 """Returns a string indicating that the model is invalid.""" 2295 return cmh.CpSatHelper.validate_model(self.__model)
Returns a string indicating that the model is invalid.
2297 def export_to_file(self, file: str) -> bool: 2298 """Write the model as a protocol buffer to 'file'. 2299 2300 Args: 2301 file: file to write the model to. If the filename ends with 'txt', the 2302 model will be written as a text file, otherwise, the binary format will 2303 be used. 2304 2305 Returns: 2306 True if the model was correctly written. 2307 """ 2308 return cmh.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.
2324 def add_hint(self, var, value) -> None: 2325 """Adds 'var == value' as a hint to the solver.""" 2326 if var.index >= 0: 2327 self.__model.solution_hint.vars.append(self.get_or_make_index(var)) 2328 self.__model.solution_hint.values.append(int(value)) 2329 else: 2330 self.__model.solution_hint.vars.append(self.negated(var.index)) 2331 self.__model.solution_hint.values.append(int(not value))
Adds 'var == value' as a hint to the solver.
2333 def clear_hints(self): 2334 """Removes any solution hint from the model.""" 2335 self.__model.ClearField("solution_hint")
Removes any solution hint from the model.
2337 def add_assumption(self, lit: LiteralT) -> None: 2338 """Adds the literal to the model as assumptions.""" 2339 self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
Adds the literal to the model as assumptions.
2341 def add_assumptions(self, literals: Iterable[LiteralT]) -> None: 2342 """Adds the literals to the model as assumptions.""" 2343 for lit in literals: 2344 self.add_assumption(lit)
Adds the literals to the model as assumptions.
2460class CpSolver: 2461 """Main solver class. 2462 2463 The purpose of this class is to search for a solution to the model provided 2464 to the solve() method. 2465 2466 Once solve() is called, this class allows inspecting the solution found 2467 with the value() and boolean_value() methods, as well as general statistics 2468 about the solve procedure. 2469 """ 2470 2471 def __init__(self) -> None: 2472 self.__response_wrapper: Optional[cmh.ResponseWrapper] = None 2473 self.parameters: sat_parameters_pb2.SatParameters = ( 2474 sat_parameters_pb2.SatParameters() 2475 ) 2476 self.log_callback: Optional[Callable[[str], None]] = None 2477 self.best_bound_callback: Optional[Callable[[float], None]] = None 2478 self.__solve_wrapper: Optional[cmh.SolveWrapper] = None 2479 self.__lock: threading.Lock = threading.Lock() 2480 2481 def solve( 2482 self, 2483 model: CpModel, 2484 solution_callback: Optional["CpSolverSolutionCallback"] = None, 2485 ) -> cp_model_pb2.CpSolverStatus: 2486 """Solves a problem and passes each solution to the callback if not null.""" 2487 with self.__lock: 2488 self.__solve_wrapper = cmh.SolveWrapper() 2489 2490 self.__solve_wrapper.set_parameters(self.parameters) 2491 if solution_callback is not None: 2492 self.__solve_wrapper.add_solution_callback(solution_callback) 2493 2494 if self.log_callback is not None: 2495 self.__solve_wrapper.add_log_callback(self.log_callback) 2496 2497 if self.best_bound_callback is not None: 2498 self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback) 2499 2500 self.__response_wrapper = ( 2501 self.__solve_wrapper.solve_and_return_response_wrapper(model.proto) 2502 ) 2503 2504 if solution_callback is not None: 2505 self.__solve_wrapper.clear_solution_callback(solution_callback) 2506 2507 with self.__lock: 2508 self.__solve_wrapper = None 2509 2510 return self.__response_wrapper.status() 2511 2512 def stop_search(self) -> None: 2513 """Stops the current search asynchronously.""" 2514 with self.__lock: 2515 if self.__solve_wrapper: 2516 self.__solve_wrapper.stop_search() 2517 2518 def value(self, expression: LinearExprT) -> int: 2519 """Returns the value of a linear expression after solve.""" 2520 return self._checked_response.value(expression) 2521 2522 def values(self, variables: _IndexOrSeries) -> pd.Series: 2523 """Returns the values of the input variables. 2524 2525 If `variables` is a `pd.Index`, then the output will be indexed by the 2526 variables. If `variables` is a `pd.Series` indexed by the underlying 2527 dimensions, then the output will be indexed by the same underlying 2528 dimensions. 2529 2530 Args: 2531 variables (Union[pd.Index, pd.Series]): The set of variables from which to 2532 get the values. 2533 2534 Returns: 2535 pd.Series: The values of all variables in the set. 2536 2537 Raises: 2538 RuntimeError: if solve() has not been called. 2539 """ 2540 if self.__response_wrapper is None: 2541 raise RuntimeError("solve() has not been called.") 2542 return pd.Series( 2543 data=[self.__response_wrapper.value(var) for var in variables], 2544 index=_get_index(variables), 2545 ) 2546 2547 def float_value(self, expression: LinearExprT) -> float: 2548 """Returns the value of a linear expression after solve.""" 2549 return self._checked_response.float_value(expression) 2550 2551 def float_values(self, expressions: _IndexOrSeries) -> pd.Series: 2552 """Returns the float values of the input linear expressions. 2553 2554 If `expressions` is a `pd.Index`, then the output will be indexed by the 2555 variables. If `variables` is a `pd.Series` indexed by the underlying 2556 dimensions, then the output will be indexed by the same underlying 2557 dimensions. 2558 2559 Args: 2560 expressions (Union[pd.Index, pd.Series]): The set of expressions from 2561 which to get the values. 2562 2563 Returns: 2564 pd.Series: The values of all variables in the set. 2565 2566 Raises: 2567 RuntimeError: if solve() has not been called. 2568 """ 2569 if self.__response_wrapper is None: 2570 raise RuntimeError("solve() has not been called.") 2571 return pd.Series( 2572 data=[self.__response_wrapper.float_value(expr) for expr in expressions], 2573 index=_get_index(expressions), 2574 ) 2575 2576 def boolean_value(self, literal: LiteralT) -> bool: 2577 """Returns the boolean value of a literal after solve.""" 2578 return self._checked_response.boolean_value(literal) 2579 2580 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 2581 """Returns the values of the input variables. 2582 2583 If `variables` is a `pd.Index`, then the output will be indexed by the 2584 variables. If `variables` is a `pd.Series` indexed by the underlying 2585 dimensions, then the output will be indexed by the same underlying 2586 dimensions. 2587 2588 Args: 2589 variables (Union[pd.Index, pd.Series]): The set of variables from which to 2590 get the values. 2591 2592 Returns: 2593 pd.Series: The values of all variables in the set. 2594 2595 Raises: 2596 RuntimeError: if solve() has not been called. 2597 """ 2598 if self.__response_wrapper is None: 2599 raise RuntimeError("solve() has not been called.") 2600 return pd.Series( 2601 data=[ 2602 self.__response_wrapper.boolean_value(literal) for literal in variables 2603 ], 2604 index=_get_index(variables), 2605 ) 2606 2607 @property 2608 def objective_value(self) -> float: 2609 """Returns the value of the objective after solve.""" 2610 return self._checked_response.objective_value() 2611 2612 @property 2613 def best_objective_bound(self) -> float: 2614 """Returns the best lower (upper) bound found when min(max)imizing.""" 2615 return self._checked_response.best_objective_bound() 2616 2617 @property 2618 def num_booleans(self) -> int: 2619 """Returns the number of boolean variables managed by the SAT solver.""" 2620 return self._checked_response.num_booleans() 2621 2622 @property 2623 def num_conflicts(self) -> int: 2624 """Returns the number of conflicts since the creation of the solver.""" 2625 return self._checked_response.num_conflicts() 2626 2627 @property 2628 def num_branches(self) -> int: 2629 """Returns the number of search branches explored by the solver.""" 2630 return self._checked_response.num_branches() 2631 2632 @property 2633 def wall_time(self) -> float: 2634 """Returns the wall time in seconds since the creation of the solver.""" 2635 return self._checked_response.wall_time() 2636 2637 @property 2638 def user_time(self) -> float: 2639 """Returns the user time in seconds since the creation of the solver.""" 2640 return self._checked_response.user_time() 2641 2642 @property 2643 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 2644 """Returns the response object.""" 2645 return self._checked_response.response() 2646 2647 def response_stats(self) -> str: 2648 """Returns some statistics on the solution found as a string.""" 2649 return self._checked_response.response_stats() 2650 2651 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 2652 """Returns the indices of the infeasible assumptions.""" 2653 return self._checked_response.sufficient_assumptions_for_infeasibility() 2654 2655 def status_name(self, status: Optional[Any] = None) -> str: 2656 """Returns the name of the status returned by solve().""" 2657 if status is None: 2658 status = self._checked_response.status() 2659 return cp_model_pb2.CpSolverStatus.Name(status) 2660 2661 def solution_info(self) -> str: 2662 """Returns some information on the solve process. 2663 2664 Returns some information on how the solution was found, or the reason 2665 why the model or the parameters are invalid. 2666 2667 Raises: 2668 RuntimeError: if solve() has not been called. 2669 """ 2670 return self._checked_response.solution_info() 2671 2672 @property 2673 def _checked_response(self) -> cmh.ResponseWrapper: 2674 """Checks solve() has been called, and returns a response wrapper.""" 2675 if self.__response_wrapper is None: 2676 raise RuntimeError("solve() has not been called.") 2677 return self.__response_wrapper 2678 2679 # Compatibility with pre PEP8 2680 # pylint: disable=invalid-name 2681 2682 def BestObjectiveBound(self) -> float: 2683 return self.best_objective_bound 2684 2685 def BooleanValue(self, literal: LiteralT) -> bool: 2686 return self.boolean_value(literal) 2687 2688 def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series: 2689 return self.boolean_values(variables) 2690 2691 def NumBooleans(self) -> int: 2692 return self.num_booleans 2693 2694 def NumConflicts(self) -> int: 2695 return self.num_conflicts 2696 2697 def NumBranches(self) -> int: 2698 return self.num_branches 2699 2700 def ObjectiveValue(self) -> float: 2701 return self.objective_value 2702 2703 def ResponseProto(self) -> cp_model_pb2.CpSolverResponse: 2704 return self.response_proto 2705 2706 def ResponseStats(self) -> str: 2707 return self.response_stats() 2708 2709 def Solve( 2710 self, 2711 model: CpModel, 2712 solution_callback: Optional["CpSolverSolutionCallback"] = None, 2713 ) -> cp_model_pb2.CpSolverStatus: 2714 return self.solve(model, solution_callback) 2715 2716 def SolutionInfo(self) -> str: 2717 return self.solution_info() 2718 2719 def StatusName(self, status: Optional[Any] = None) -> str: 2720 return self.status_name(status) 2721 2722 def StopSearch(self) -> None: 2723 self.stop_search() 2724 2725 def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]: 2726 return self.sufficient_assumptions_for_infeasibility() 2727 2728 def UserTime(self) -> float: 2729 return self.user_time 2730 2731 def Value(self, expression: LinearExprT) -> int: 2732 return self.value(expression) 2733 2734 def Values(self, variables: _IndexOrSeries) -> pd.Series: 2735 return self.values(variables) 2736 2737 def WallTime(self) -> float: 2738 return self.wall_time 2739 2740 def SolveWithSolutionCallback( 2741 self, model: CpModel, callback: "CpSolverSolutionCallback" 2742 ) -> cp_model_pb2.CpSolverStatus: 2743 """DEPRECATED Use solve() with the callback argument.""" 2744 warnings.warn( 2745 "solve_with_solution_callback is deprecated; use solve() with" 2746 + "the callback argument.", 2747 DeprecationWarning, 2748 ) 2749 return self.solve(model, callback) 2750 2751 def SearchForAllSolutions( 2752 self, model: CpModel, callback: "CpSolverSolutionCallback" 2753 ) -> cp_model_pb2.CpSolverStatus: 2754 """DEPRECATED Use solve() with the right parameter. 2755 2756 Search for all solutions of a satisfiability problem. 2757 2758 This method searches for all feasible solutions of a given model. 2759 Then it feeds the solution to the callback. 2760 2761 Note that the model cannot contain an objective. 2762 2763 Args: 2764 model: The model to solve. 2765 callback: The callback that will be called at each solution. 2766 2767 Returns: 2768 The status of the solve: 2769 2770 * *FEASIBLE* if some solutions have been found 2771 * *INFEASIBLE* if the solver has proved there are no solution 2772 * *OPTIMAL* if all solutions have been found 2773 """ 2774 warnings.warn( 2775 "search_for_all_solutions is deprecated; use solve() with" 2776 + "enumerate_all_solutions = True.", 2777 DeprecationWarning, 2778 ) 2779 if model.has_objective(): 2780 raise TypeError( 2781 "Search for all solutions is only defined on satisfiability problems" 2782 ) 2783 # Store old parameter. 2784 enumerate_all = self.parameters.enumerate_all_solutions 2785 self.parameters.enumerate_all_solutions = True 2786 2787 status: cp_model_pb2.CpSolverStatus = self.solve(model, callback) 2788 2789 # Restore parameter. 2790 self.parameters.enumerate_all_solutions = enumerate_all 2791 return status
Main solver class.
The purpose of this class is to search for a solution to the model provided to the solve() method.
Once solve() is called, this class allows inspecting the solution found with the value() and boolean_value() methods, as well as general statistics about the solve procedure.
2481 def solve( 2482 self, 2483 model: CpModel, 2484 solution_callback: Optional["CpSolverSolutionCallback"] = None, 2485 ) -> cp_model_pb2.CpSolverStatus: 2486 """Solves a problem and passes each solution to the callback if not null.""" 2487 with self.__lock: 2488 self.__solve_wrapper = cmh.SolveWrapper() 2489 2490 self.__solve_wrapper.set_parameters(self.parameters) 2491 if solution_callback is not None: 2492 self.__solve_wrapper.add_solution_callback(solution_callback) 2493 2494 if self.log_callback is not None: 2495 self.__solve_wrapper.add_log_callback(self.log_callback) 2496 2497 if self.best_bound_callback is not None: 2498 self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback) 2499 2500 self.__response_wrapper = ( 2501 self.__solve_wrapper.solve_and_return_response_wrapper(model.proto) 2502 ) 2503 2504 if solution_callback is not None: 2505 self.__solve_wrapper.clear_solution_callback(solution_callback) 2506 2507 with self.__lock: 2508 self.__solve_wrapper = None 2509 2510 return self.__response_wrapper.status()
Solves a problem and passes each solution to the callback if not null.
2512 def stop_search(self) -> None: 2513 """Stops the current search asynchronously.""" 2514 with self.__lock: 2515 if self.__solve_wrapper: 2516 self.__solve_wrapper.stop_search()
Stops the current search asynchronously.
2518 def value(self, expression: LinearExprT) -> int: 2519 """Returns the value of a linear expression after solve.""" 2520 return self._checked_response.value(expression)
Returns the value of a linear expression after solve.
2522 def values(self, variables: _IndexOrSeries) -> pd.Series: 2523 """Returns the values of the input variables. 2524 2525 If `variables` is a `pd.Index`, then the output will be indexed by the 2526 variables. If `variables` is a `pd.Series` indexed by the underlying 2527 dimensions, then the output will be indexed by the same underlying 2528 dimensions. 2529 2530 Args: 2531 variables (Union[pd.Index, pd.Series]): The set of variables from which to 2532 get the values. 2533 2534 Returns: 2535 pd.Series: The values of all variables in the set. 2536 2537 Raises: 2538 RuntimeError: if solve() has not been called. 2539 """ 2540 if self.__response_wrapper is None: 2541 raise RuntimeError("solve() has not been called.") 2542 return pd.Series( 2543 data=[self.__response_wrapper.value(var) for var in variables], 2544 index=_get_index(variables), 2545 )
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.
Raises:
- RuntimeError: if solve() has not been called.
2547 def float_value(self, expression: LinearExprT) -> float: 2548 """Returns the value of a linear expression after solve.""" 2549 return self._checked_response.float_value(expression)
Returns the value of a linear expression after solve.
2551 def float_values(self, expressions: _IndexOrSeries) -> pd.Series: 2552 """Returns the float values of the input linear expressions. 2553 2554 If `expressions` is a `pd.Index`, then the output will be indexed by the 2555 variables. If `variables` is a `pd.Series` indexed by the underlying 2556 dimensions, then the output will be indexed by the same underlying 2557 dimensions. 2558 2559 Args: 2560 expressions (Union[pd.Index, pd.Series]): The set of expressions from 2561 which to get the values. 2562 2563 Returns: 2564 pd.Series: The values of all variables in the set. 2565 2566 Raises: 2567 RuntimeError: if solve() has not been called. 2568 """ 2569 if self.__response_wrapper is None: 2570 raise RuntimeError("solve() has not been called.") 2571 return pd.Series( 2572 data=[self.__response_wrapper.float_value(expr) for expr in expressions], 2573 index=_get_index(expressions), 2574 )
Returns the float values of the input linear expressions.
If expressions
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:
- expressions (Union[pd.Index, pd.Series]): The set of expressions from which to get the values.
Returns:
pd.Series: The values of all variables in the set.
Raises:
- RuntimeError: if solve() has not been called.
2576 def boolean_value(self, literal: LiteralT) -> bool: 2577 """Returns the boolean value of a literal after solve.""" 2578 return self._checked_response.boolean_value(literal)
Returns the boolean value of a literal after solve.
2580 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: 2581 """Returns the values of the input variables. 2582 2583 If `variables` is a `pd.Index`, then the output will be indexed by the 2584 variables. If `variables` is a `pd.Series` indexed by the underlying 2585 dimensions, then the output will be indexed by the same underlying 2586 dimensions. 2587 2588 Args: 2589 variables (Union[pd.Index, pd.Series]): The set of variables from which to 2590 get the values. 2591 2592 Returns: 2593 pd.Series: The values of all variables in the set. 2594 2595 Raises: 2596 RuntimeError: if solve() has not been called. 2597 """ 2598 if self.__response_wrapper is None: 2599 raise RuntimeError("solve() has not been called.") 2600 return pd.Series( 2601 data=[ 2602 self.__response_wrapper.boolean_value(literal) for literal in variables 2603 ], 2604 index=_get_index(variables), 2605 )
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.
Raises:
- RuntimeError: if solve() has not been called.
2607 @property 2608 def objective_value(self) -> float: 2609 """Returns the value of the objective after solve.""" 2610 return self._checked_response.objective_value()
Returns the value of the objective after solve.
2612 @property 2613 def best_objective_bound(self) -> float: 2614 """Returns the best lower (upper) bound found when min(max)imizing.""" 2615 return self._checked_response.best_objective_bound()
Returns the best lower (upper) bound found when min(max)imizing.
2617 @property 2618 def num_booleans(self) -> int: 2619 """Returns the number of boolean variables managed by the SAT solver.""" 2620 return self._checked_response.num_booleans()
Returns the number of boolean variables managed by the SAT solver.
2622 @property 2623 def num_conflicts(self) -> int: 2624 """Returns the number of conflicts since the creation of the solver.""" 2625 return self._checked_response.num_conflicts()
Returns the number of conflicts since the creation of the solver.
2627 @property 2628 def num_branches(self) -> int: 2629 """Returns the number of search branches explored by the solver.""" 2630 return self._checked_response.num_branches()
Returns the number of search branches explored by the solver.
2632 @property 2633 def wall_time(self) -> float: 2634 """Returns the wall time in seconds since the creation of the solver.""" 2635 return self._checked_response.wall_time()
Returns the wall time in seconds since the creation of the solver.
2637 @property 2638 def user_time(self) -> float: 2639 """Returns the user time in seconds since the creation of the solver.""" 2640 return self._checked_response.user_time()
Returns the user time in seconds since the creation of the solver.
2642 @property 2643 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 2644 """Returns the response object.""" 2645 return self._checked_response.response()
Returns the response object.
2647 def response_stats(self) -> str: 2648 """Returns some statistics on the solution found as a string.""" 2649 return self._checked_response.response_stats()
Returns some statistics on the solution found as a string.
2651 def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: 2652 """Returns the indices of the infeasible assumptions.""" 2653 return self._checked_response.sufficient_assumptions_for_infeasibility()
Returns the indices of the infeasible assumptions.
2655 def status_name(self, status: Optional[Any] = None) -> str: 2656 """Returns the name of the status returned by solve().""" 2657 if status is None: 2658 status = self._checked_response.status() 2659 return cp_model_pb2.CpSolverStatus.Name(status)
Returns the name of the status returned by solve().
2661 def solution_info(self) -> str: 2662 """Returns some information on the solve process. 2663 2664 Returns some information on how the solution was found, or the reason 2665 why the model or the parameters are invalid. 2666 2667 Raises: 2668 RuntimeError: if solve() has not been called. 2669 """ 2670 return self._checked_response.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.
2740 def SolveWithSolutionCallback( 2741 self, model: CpModel, callback: "CpSolverSolutionCallback" 2742 ) -> cp_model_pb2.CpSolverStatus: 2743 """DEPRECATED Use solve() with the callback argument.""" 2744 warnings.warn( 2745 "solve_with_solution_callback is deprecated; use solve() with" 2746 + "the callback argument.", 2747 DeprecationWarning, 2748 ) 2749 return self.solve(model, callback)
DEPRECATED Use solve() with the callback argument.
2751 def SearchForAllSolutions( 2752 self, model: CpModel, callback: "CpSolverSolutionCallback" 2753 ) -> cp_model_pb2.CpSolverStatus: 2754 """DEPRECATED Use solve() with the right parameter. 2755 2756 Search for all solutions of a satisfiability problem. 2757 2758 This method searches for all feasible solutions of a given model. 2759 Then it feeds the solution to the callback. 2760 2761 Note that the model cannot contain an objective. 2762 2763 Args: 2764 model: The model to solve. 2765 callback: The callback that will be called at each solution. 2766 2767 Returns: 2768 The status of the solve: 2769 2770 * *FEASIBLE* if some solutions have been found 2771 * *INFEASIBLE* if the solver has proved there are no solution 2772 * *OPTIMAL* if all solutions have been found 2773 """ 2774 warnings.warn( 2775 "search_for_all_solutions is deprecated; use solve() with" 2776 + "enumerate_all_solutions = True.", 2777 DeprecationWarning, 2778 ) 2779 if model.has_objective(): 2780 raise TypeError( 2781 "Search for all solutions is only defined on satisfiability problems" 2782 ) 2783 # Store old parameter. 2784 enumerate_all = self.parameters.enumerate_all_solutions 2785 self.parameters.enumerate_all_solutions = True 2786 2787 status: cp_model_pb2.CpSolverStatus = self.solve(model, callback) 2788 2789 # Restore parameter. 2790 self.parameters.enumerate_all_solutions = enumerate_all 2791 return status
DEPRECATED Use solve() with the right parameter.
Search for all solutions of a satisfiability problem.
This method searches for all feasible solutions of a given model. Then it feeds the solution to the callback.
Note that the model cannot contain an objective.
Arguments:
- model: The model to solve.
- callback: The callback that will be called at each solution.
Returns:
The status of the solve:
- FEASIBLE if some solutions have been found
- INFEASIBLE if the solver has proved there are no solution
- OPTIMAL if all solutions have been found
2797class CpSolverSolutionCallback(cmh.SolutionCallback): 2798 """Solution callback. 2799 2800 This class implements a callback that will be called at each new solution 2801 found during search. 2802 2803 The method on_solution_callback() will be called by the solver, and must be 2804 implemented. The current solution can be queried using the boolean_value() 2805 and value() methods. 2806 2807 These methods returns the same information as their counterpart in the 2808 `CpSolver` class. 2809 """ 2810 2811 def __init__(self) -> None: 2812 cmh.SolutionCallback.__init__(self) 2813 2814 def OnSolutionCallback(self) -> None: 2815 """Proxy for the same method in snake case.""" 2816 self.on_solution_callback() 2817 2818 def boolean_value(self, lit: LiteralT) -> bool: 2819 """Returns the boolean value of a boolean literal. 2820 2821 Args: 2822 lit: A boolean variable or its negation. 2823 2824 Returns: 2825 The Boolean value of the literal in the solution. 2826 2827 Raises: 2828 RuntimeError: if `lit` is not a boolean variable or its negation. 2829 """ 2830 if not self.has_response(): 2831 raise RuntimeError("solve() has not been called.") 2832 return self.BooleanValue(lit) 2833 2834 def value(self, expression: LinearExprT) -> int: 2835 """Evaluates an linear expression in the current solution. 2836 2837 Args: 2838 expression: a linear expression of the model. 2839 2840 Returns: 2841 An integer value equal to the evaluation of the linear expression 2842 against the current solution. 2843 2844 Raises: 2845 RuntimeError: if 'expression' is not a LinearExpr. 2846 """ 2847 if not self.has_response(): 2848 raise RuntimeError("solve() has not been called.") 2849 return self.Value(expression) 2850 2851 def float_value(self, expression: LinearExprT) -> float: 2852 """Evaluates an linear expression in the current solution. 2853 2854 Args: 2855 expression: a linear expression of the model. 2856 2857 Returns: 2858 An integer value equal to the evaluation of the linear expression 2859 against the current solution. 2860 2861 Raises: 2862 RuntimeError: if 'expression' is not a LinearExpr. 2863 """ 2864 if not self.has_response(): 2865 raise RuntimeError("solve() has not been called.") 2866 return self.FloatValue(expression) 2867 2868 def has_response(self) -> bool: 2869 return self.HasResponse() 2870 2871 def stop_search(self) -> None: 2872 """Stops the current search asynchronously.""" 2873 if not self.has_response(): 2874 raise RuntimeError("solve() has not been called.") 2875 self.StopSearch() 2876 2877 @property 2878 def objective_value(self) -> float: 2879 """Returns the value of the objective after solve.""" 2880 if not self.has_response(): 2881 raise RuntimeError("solve() has not been called.") 2882 return self.ObjectiveValue() 2883 2884 @property 2885 def best_objective_bound(self) -> float: 2886 """Returns the best lower (upper) bound found when min(max)imizing.""" 2887 if not self.has_response(): 2888 raise RuntimeError("solve() has not been called.") 2889 return self.BestObjectiveBound() 2890 2891 @property 2892 def num_booleans(self) -> int: 2893 """Returns the number of boolean variables managed by the SAT solver.""" 2894 if not self.has_response(): 2895 raise RuntimeError("solve() has not been called.") 2896 return self.NumBooleans() 2897 2898 @property 2899 def num_conflicts(self) -> int: 2900 """Returns the number of conflicts since the creation of the solver.""" 2901 if not self.has_response(): 2902 raise RuntimeError("solve() has not been called.") 2903 return self.NumConflicts() 2904 2905 @property 2906 def num_branches(self) -> int: 2907 """Returns the number of search branches explored by the solver.""" 2908 if not self.has_response(): 2909 raise RuntimeError("solve() has not been called.") 2910 return self.NumBranches() 2911 2912 @property 2913 def num_integer_propagations(self) -> int: 2914 """Returns the number of integer propagations done by the solver.""" 2915 if not self.has_response(): 2916 raise RuntimeError("solve() has not been called.") 2917 return self.NumIntegerPropagations() 2918 2919 @property 2920 def num_boolean_propagations(self) -> int: 2921 """Returns the number of Boolean propagations done by the solver.""" 2922 if not self.has_response(): 2923 raise RuntimeError("solve() has not been called.") 2924 return self.NumBooleanPropagations() 2925 2926 @property 2927 def deterministic_time(self) -> float: 2928 """Returns the determistic time in seconds since the creation of the solver.""" 2929 if not self.has_response(): 2930 raise RuntimeError("solve() has not been called.") 2931 return self.DeterministicTime() 2932 2933 @property 2934 def wall_time(self) -> float: 2935 """Returns the wall time in seconds since the creation of the solver.""" 2936 if not self.has_response(): 2937 raise RuntimeError("solve() has not been called.") 2938 return self.WallTime() 2939 2940 @property 2941 def user_time(self) -> float: 2942 """Returns the user time in seconds since the creation of the solver.""" 2943 if not self.has_response(): 2944 raise RuntimeError("solve() has not been called.") 2945 return self.UserTime() 2946 2947 @property 2948 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 2949 """Returns the response object.""" 2950 if not self.has_response(): 2951 raise RuntimeError("solve() has not been called.") 2952 return self.Response()
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.
__init__(self: ortools.sat.python.cp_model_helper.SolutionCallback) -> None
2814 def OnSolutionCallback(self) -> None: 2815 """Proxy for the same method in snake case.""" 2816 self.on_solution_callback()
Proxy for the same method in snake case.
2818 def boolean_value(self, lit: LiteralT) -> bool: 2819 """Returns the boolean value of a boolean literal. 2820 2821 Args: 2822 lit: A boolean variable or its negation. 2823 2824 Returns: 2825 The Boolean value of the literal in the solution. 2826 2827 Raises: 2828 RuntimeError: if `lit` is not a boolean variable or its negation. 2829 """ 2830 if not self.has_response(): 2831 raise RuntimeError("solve() has not been called.") 2832 return self.BooleanValue(lit)
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.
2834 def value(self, expression: LinearExprT) -> int: 2835 """Evaluates an linear expression in the current solution. 2836 2837 Args: 2838 expression: a linear expression of the model. 2839 2840 Returns: 2841 An integer value equal to the evaluation of the linear expression 2842 against the current solution. 2843 2844 Raises: 2845 RuntimeError: if 'expression' is not a LinearExpr. 2846 """ 2847 if not self.has_response(): 2848 raise RuntimeError("solve() has not been called.") 2849 return self.Value(expression)
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.
2851 def float_value(self, expression: LinearExprT) -> float: 2852 """Evaluates an linear expression in the current solution. 2853 2854 Args: 2855 expression: a linear expression of the model. 2856 2857 Returns: 2858 An integer value equal to the evaluation of the linear expression 2859 against the current solution. 2860 2861 Raises: 2862 RuntimeError: if 'expression' is not a LinearExpr. 2863 """ 2864 if not self.has_response(): 2865 raise RuntimeError("solve() has not been called.") 2866 return self.FloatValue(expression)
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.
2871 def stop_search(self) -> None: 2872 """Stops the current search asynchronously.""" 2873 if not self.has_response(): 2874 raise RuntimeError("solve() has not been called.") 2875 self.StopSearch()
Stops the current search asynchronously.
2877 @property 2878 def objective_value(self) -> float: 2879 """Returns the value of the objective after solve.""" 2880 if not self.has_response(): 2881 raise RuntimeError("solve() has not been called.") 2882 return self.ObjectiveValue()
Returns the value of the objective after solve.
2884 @property 2885 def best_objective_bound(self) -> float: 2886 """Returns the best lower (upper) bound found when min(max)imizing.""" 2887 if not self.has_response(): 2888 raise RuntimeError("solve() has not been called.") 2889 return self.BestObjectiveBound()
Returns the best lower (upper) bound found when min(max)imizing.
2891 @property 2892 def num_booleans(self) -> int: 2893 """Returns the number of boolean variables managed by the SAT solver.""" 2894 if not self.has_response(): 2895 raise RuntimeError("solve() has not been called.") 2896 return self.NumBooleans()
Returns the number of boolean variables managed by the SAT solver.
2898 @property 2899 def num_conflicts(self) -> int: 2900 """Returns the number of conflicts since the creation of the solver.""" 2901 if not self.has_response(): 2902 raise RuntimeError("solve() has not been called.") 2903 return self.NumConflicts()
Returns the number of conflicts since the creation of the solver.
2905 @property 2906 def num_branches(self) -> int: 2907 """Returns the number of search branches explored by the solver.""" 2908 if not self.has_response(): 2909 raise RuntimeError("solve() has not been called.") 2910 return self.NumBranches()
Returns the number of search branches explored by the solver.
2912 @property 2913 def num_integer_propagations(self) -> int: 2914 """Returns the number of integer propagations done by the solver.""" 2915 if not self.has_response(): 2916 raise RuntimeError("solve() has not been called.") 2917 return self.NumIntegerPropagations()
Returns the number of integer propagations done by the solver.
2919 @property 2920 def num_boolean_propagations(self) -> int: 2921 """Returns the number of Boolean propagations done by the solver.""" 2922 if not self.has_response(): 2923 raise RuntimeError("solve() has not been called.") 2924 return self.NumBooleanPropagations()
Returns the number of Boolean propagations done by the solver.
2926 @property 2927 def deterministic_time(self) -> float: 2928 """Returns the determistic time in seconds since the creation of the solver.""" 2929 if not self.has_response(): 2930 raise RuntimeError("solve() has not been called.") 2931 return self.DeterministicTime()
Returns the determistic time in seconds since the creation of the solver.
2933 @property 2934 def wall_time(self) -> float: 2935 """Returns the wall time in seconds since the creation of the solver.""" 2936 if not self.has_response(): 2937 raise RuntimeError("solve() has not been called.") 2938 return self.WallTime()
Returns the wall time in seconds since the creation of the solver.
2940 @property 2941 def user_time(self) -> float: 2942 """Returns the user time in seconds since the creation of the solver.""" 2943 if not self.has_response(): 2944 raise RuntimeError("solve() has not been called.") 2945 return self.UserTime()
Returns the user time in seconds since the creation of the solver.
2947 @property 2948 def response_proto(self) -> cp_model_pb2.CpSolverResponse: 2949 """Returns the response object.""" 2950 if not self.has_response(): 2951 raise RuntimeError("solve() has not been called.") 2952 return self.Response()
Returns the response object.
Inherited Members
- ortools.sat.python.cp_model_helper.SolutionCallback
- BestObjectiveBound
- DeterministicTime
- HasResponse
- NumBinaryPropagations
- NumBooleans
- NumBranches
- NumConflicts
- NumIntegerPropagations
- ObjectiveValue
- Response
- SolutionBooleanValue
- SolutionIntegerValue
- StopSearch
- UserTime
- WallTime
- Value
- FloatValue
- BooleanValue
2955class ObjectiveSolutionPrinter(CpSolverSolutionCallback): 2956 """Display the objective value and time of intermediate solutions.""" 2957 2958 def __init__(self) -> None: 2959 CpSolverSolutionCallback.__init__(self) 2960 self.__solution_count = 0 2961 self.__start_time = time.time() 2962 2963 def on_solution_callback(self) -> None: 2964 """Called on each new solution.""" 2965 current_time = time.time() 2966 obj = self.objective_value 2967 print( 2968 f"Solution {self.__solution_count}, time =" 2969 f" {current_time - self.__start_time:0.2f} s, objective = {obj}", 2970 flush=True, 2971 ) 2972 self.__solution_count += 1 2973 2974 def solution_count(self) -> int: 2975 """Returns the number of solutions found.""" 2976 return self.__solution_count
Display the objective value and time of intermediate solutions.
2958 def __init__(self) -> None: 2959 CpSolverSolutionCallback.__init__(self) 2960 self.__solution_count = 0 2961 self.__start_time = time.time()
__init__(self: ortools.sat.python.cp_model_helper.SolutionCallback) -> None
2963 def on_solution_callback(self) -> None: 2964 """Called on each new solution.""" 2965 current_time = time.time() 2966 obj = self.objective_value 2967 print( 2968 f"Solution {self.__solution_count}, time =" 2969 f" {current_time - self.__start_time:0.2f} s, objective = {obj}", 2970 flush=True, 2971 ) 2972 self.__solution_count += 1
Called on each new solution.
2974 def solution_count(self) -> int: 2975 """Returns the number of solutions found.""" 2976 return self.__solution_count
Returns the number of solutions found.
Inherited Members
- CpSolverSolutionCallback
- OnSolutionCallback
- boolean_value
- value
- float_value
- has_response
- stop_search
- objective_value
- best_objective_bound
- num_booleans
- num_conflicts
- num_branches
- num_integer_propagations
- num_boolean_propagations
- deterministic_time
- wall_time
- user_time
- response_proto
- ortools.sat.python.cp_model_helper.SolutionCallback
- BestObjectiveBound
- DeterministicTime
- HasResponse
- NumBinaryPropagations
- NumBooleans
- NumBranches
- NumConflicts
- NumIntegerPropagations
- ObjectiveValue
- Response
- SolutionBooleanValue
- SolutionIntegerValue
- StopSearch
- UserTime
- WallTime
- Value
- FloatValue
- BooleanValue
2979class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback): 2980 """Print intermediate solutions (objective, variable values, time).""" 2981 2982 def __init__(self, variables: Sequence[IntVar]) -> None: 2983 CpSolverSolutionCallback.__init__(self) 2984 self.__variables: Sequence[IntVar] = variables 2985 self.__solution_count: int = 0 2986 self.__start_time: float = time.time() 2987 2988 def on_solution_callback(self) -> None: 2989 """Called on each new solution.""" 2990 current_time = time.time() 2991 obj = self.objective_value 2992 print( 2993 f"Solution {self.__solution_count}, time =" 2994 f" {current_time - self.__start_time:0.2f} s, objective = {obj}" 2995 ) 2996 for v in self.__variables: 2997 print(f" {v} = {self.value(v)}", end=" ") 2998 print(flush=True) 2999 self.__solution_count += 1 3000 3001 @property 3002 def solution_count(self) -> int: 3003 """Returns the number of solutions found.""" 3004 return self.__solution_count
Print intermediate solutions (objective, variable values, time).
2982 def __init__(self, variables: Sequence[IntVar]) -> None: 2983 CpSolverSolutionCallback.__init__(self) 2984 self.__variables: Sequence[IntVar] = variables 2985 self.__solution_count: int = 0 2986 self.__start_time: float = time.time()
__init__(self: ortools.sat.python.cp_model_helper.SolutionCallback) -> None
2988 def on_solution_callback(self) -> None: 2989 """Called on each new solution.""" 2990 current_time = time.time() 2991 obj = self.objective_value 2992 print( 2993 f"Solution {self.__solution_count}, time =" 2994 f" {current_time - self.__start_time:0.2f} s, objective = {obj}" 2995 ) 2996 for v in self.__variables: 2997 print(f" {v} = {self.value(v)}", end=" ") 2998 print(flush=True) 2999 self.__solution_count += 1
Called on each new solution.
3001 @property 3002 def solution_count(self) -> int: 3003 """Returns the number of solutions found.""" 3004 return self.__solution_count
Returns the number of solutions found.
Inherited Members
- CpSolverSolutionCallback
- OnSolutionCallback
- boolean_value
- value
- float_value
- has_response
- stop_search
- objective_value
- best_objective_bound
- num_booleans
- num_conflicts
- num_branches
- num_integer_propagations
- num_boolean_propagations
- deterministic_time
- wall_time
- user_time
- response_proto
- ortools.sat.python.cp_model_helper.SolutionCallback
- BestObjectiveBound
- DeterministicTime
- HasResponse
- NumBinaryPropagations
- NumBooleans
- NumBranches
- NumConflicts
- NumIntegerPropagations
- ObjectiveValue
- Response
- SolutionBooleanValue
- SolutionIntegerValue
- StopSearch
- UserTime
- WallTime
- Value
- FloatValue
- BooleanValue
3007class VarArraySolutionPrinter(CpSolverSolutionCallback): 3008 """Print intermediate solutions (variable values, time).""" 3009 3010 def __init__(self, variables: Sequence[IntVar]) -> None: 3011 CpSolverSolutionCallback.__init__(self) 3012 self.__variables: Sequence[IntVar] = variables 3013 self.__solution_count: int = 0 3014 self.__start_time: float = time.time() 3015 3016 def on_solution_callback(self) -> None: 3017 """Called on each new solution.""" 3018 current_time = time.time() 3019 print( 3020 f"Solution {self.__solution_count}, time =" 3021 f" {current_time - self.__start_time:0.2f} s" 3022 ) 3023 for v in self.__variables: 3024 print(f" {v} = {self.value(v)}", end=" ") 3025 print(flush=True) 3026 self.__solution_count += 1 3027 3028 @property 3029 def solution_count(self) -> int: 3030 """Returns the number of solutions found.""" 3031 return self.__solution_count
Print intermediate solutions (variable values, time).
3010 def __init__(self, variables: Sequence[IntVar]) -> None: 3011 CpSolverSolutionCallback.__init__(self) 3012 self.__variables: Sequence[IntVar] = variables 3013 self.__solution_count: int = 0 3014 self.__start_time: float = time.time()
__init__(self: ortools.sat.python.cp_model_helper.SolutionCallback) -> None
3016 def on_solution_callback(self) -> None: 3017 """Called on each new solution.""" 3018 current_time = time.time() 3019 print( 3020 f"Solution {self.__solution_count}, time =" 3021 f" {current_time - self.__start_time:0.2f} s" 3022 ) 3023 for v in self.__variables: 3024 print(f" {v} = {self.value(v)}", end=" ") 3025 print(flush=True) 3026 self.__solution_count += 1
Called on each new solution.
3028 @property 3029 def solution_count(self) -> int: 3030 """Returns the number of solutions found.""" 3031 return self.__solution_count
Returns the number of solutions found.
Inherited Members
- CpSolverSolutionCallback
- OnSolutionCallback
- boolean_value
- value
- float_value
- has_response
- stop_search
- objective_value
- best_objective_bound
- num_booleans
- num_conflicts
- num_branches
- num_integer_propagations
- num_boolean_propagations
- deterministic_time
- wall_time
- user_time
- response_proto
- ortools.sat.python.cp_model_helper.SolutionCallback
- BestObjectiveBound
- DeterministicTime
- HasResponse
- NumBinaryPropagations
- NumBooleans
- NumBranches
- NumConflicts
- NumIntegerPropagations
- ObjectiveValue
- Response
- SolutionBooleanValue
- SolutionIntegerValue
- StopSearch
- UserTime
- WallTime
- Value
- FloatValue
- BooleanValue