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