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.

Additional methods for solving CP-SAT models:

  • .Constraint">Constraint: A few utility methods for modifying constraints created by CpModel.
  • .LinearExpr">LinearExpr: Methods for creating constraints and the objective from large arrays of coefficients.

Other methods and functions listed are primarily used for developing OR-Tools, rather than for solving specific optimization problems.

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

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.

Domain()

__init__(self: ortools.util.python.sorted_interval_list.Domain, arg0: int, arg1: int) -> None

By default, Domain will be empty.

def all_values(unknown):

all_values() -> ortools.util.python.sorted_interval_list.Domain

Returns the full domain Int64.

def from_values(unknown):

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

def from_intervals(unknown):

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).

def from_flat_intervals(unknown):

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).

def addition_with(unknown):

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}.

def complement(unknown):
def contains(unknown):

contains(self: ortools.util.python.sorted_interval_list.Domain, value: int) -> bool

Returns true iff value is in Domain.

def flattened_intervals(unknown):

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, as a java or C# long[], as a python list of integers).

def intersection_with(unknown):
def is_empty(unknown):

is_empty(self: ortools.util.python.sorted_interval_list.Domain) -> bool

Returns true if this is the empty set.

def size(unknown):

size(self: ortools.util.python.sorted_interval_list.Domain) -> int

Returns the number of elements in the domain. It is capped at kint64max

def max(unknown):

max(self: ortools.util.python.sorted_interval_list.Domain) -> int

Returns the max value of the domain. The domain must not be empty.

def min(unknown):

min(self: ortools.util.python.sorted_interval_list.Domain) -> int

Returns the min value of the domain. The domain must not be empty.

def negation(unknown):

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} !!

def union_with(unknown):
def AllValues(unknown):

AllValues() -> ortools.util.python.sorted_interval_list.Domain

Returns the full domain Int64.

def FromValues(unknown):

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

def FromIntervals(unknown):

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).

def FromFlatIntervals(unknown):

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).

def FlattenedIntervals(unknown):

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, as a java or C# long[], as a python list of integers).

class BoundedLinearExpression(pybind11_builtins.pybind11_object):

A class to hold a linear expression with bounds.

A flattened and optimized floating point linear expression.

It can be used to cache complex expressions as parsing them is only done once.

vars

(arg0: ortools.sat.python.cp_model_helper.FlatFloatExpr) -> list[operations_research::sat::python::BaseIntVar]

coeffs

A flattened and optimized integer linear expression.

It can be used to cache complex expressions as parsing them is only done once.

vars

(arg0: ortools.sat.python.cp_model_helper.FlatIntExpr) -> list[operations_research::sat::python::BaseIntVar]

class LinearExpr(pybind11_builtins.pybind11_object):

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)
LinearExpr(*args, **kwargs)
def sum(unknown):

sum(*args) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns the sum(expressions).

def weighted_sum(unknown):

weighted_sum(expressions: Sequence, coefficients: Sequence) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns the sum of (expressions[i] * coefficients[i])

def term(unknown):

term(args, *kwargs) Overloaded function.

  1. term(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: int) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns expr * coeff.

  1. term(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: float) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns expr * coeff.

def affine(unknown):

affine(args, *kwargs) Overloaded function.

  1. affine(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: int, offset: int) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns expr * coeff + offset.

  1. affine(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: float, offset: float) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns expr * coeff + offset.

def constant(unknown):

constant(args, *kwargs) Overloaded function.

  1. constant(value: int) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns a new LinearExpr that is the given constant.

  1. constant(value: float) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns a new LinearExpr that is the given constant.

def Sum(unknown):
def WeightedSum(unknown):

WeightedSum(expressions: Sequence, coefficients: Sequence) -> ortools.sat.python.cp_model_helper.LinearExpr

def Term(unknown):

Term(args, *kwargs) Overloaded function.

  1. Term(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: int) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns expr * coeff.

  1. Term(expr: ortools.sat.python.cp_model_helper.LinearExpr, coeff: float) -> ortools.sat.python.cp_model_helper.LinearExpr

Returns expr * coeff.

def is_integer(unknown):
class NotBooleanVariable(ortools.sat.python.cp_model_helper.Literal):

A class to hold a negated variable index.

NotBooleanVariable(*args, **kwargs)
def negated(unknown):

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.

def Not(unknown):

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.

INT_MIN = -9223372036854775808
INT_MAX = 9223372036854775807
INT32_MIN = -2147483648
INT32_MAX = 2147483647
UNKNOWN = 0
MODEL_INVALID = 1
FEASIBLE = 2
INFEASIBLE = 3
OPTIMAL = 4
CHOOSE_FIRST = 0
CHOOSE_LOWEST_MIN = 1
CHOOSE_HIGHEST_MAX = 2
CHOOSE_MIN_DOMAIN_SIZE = 3
CHOOSE_MAX_DOMAIN_SIZE = 4
SELECT_MIN_VALUE = 0
SELECT_MAX_VALUE = 1
SELECT_LOWER_HALF = 2
SELECT_UPPER_HALF = 3
SELECT_MEDIAN_VALUE = 4
SELECT_RANDOM_HALF = 5
IntegralT = typing.Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]
IntegralTypes = (<class 'int'>, <class 'numpy.int8'>, <class 'numpy.uint8'>, <class 'numpy.int32'>, <class 'numpy.uint32'>, <class 'numpy.int64'>, <class 'numpy.uint64'>)
NumberT = typing.Union[int, float, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, numpy.float64]
NumberTypes = (<class 'int'>, <class 'float'>, <class 'numpy.int8'>, <class 'numpy.uint8'>, <class 'numpy.int32'>, <class 'numpy.uint32'>, <class 'numpy.int64'>, <class 'numpy.uint64'>, <class 'numpy.float64'>)
LiteralT = typing.Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]
VariableT = typing.Union[ForwardRef('IntVar'), int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]
LinearExprT = typing.Union[LinearExpr, ForwardRef('IntVar'), int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]
ObjLinearExprT = typing.Union[LinearExpr, int, float, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, numpy.float64]
ArcT = typing.Tuple[typing.Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], typing.Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], typing.Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]
def display_bounds(bounds: Sequence[int]) -> str:
174def display_bounds(bounds: Sequence[int]) -> str:
175    """Displays a flattened list of intervals."""
176    out = ""
177    for i in range(0, len(bounds), 2):
178        if i != 0:
179            out += ", "
180        if bounds[i] == bounds[i + 1]:
181            out += str(bounds[i])
182        else:
183            out += str(bounds[i]) + ".." + str(bounds[i + 1])
184    return out

Displays a flattened list of intervals.

def short_name(model: ortools.sat.cp_model_pb2.CpModelProto, i: int) -> str:
187def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str:
188    """Returns a short name of an integer variable, or its negation."""
189    if i < 0:
190        return f"not({short_name(model, -i - 1)})"
191    v = model.variables[i]
192    if v.name:
193        return v.name
194    elif len(v.domain) == 2 and v.domain[0] == v.domain[1]:
195        return str(v.domain[0])
196    else:
197        return f"[{display_bounds(v.domain)}]"

Returns a short name of an integer variable, or its negation.

def short_expr_name( model: ortools.sat.cp_model_pb2.CpModelProto, e: ortools.sat.cp_model_pb2.LinearExpressionProto) -> str:
200def short_expr_name(
201    model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto
202) -> str:
203    """Pretty-print LinearExpressionProto instances."""
204    if not e.vars:
205        return str(e.offset)
206    if len(e.vars) == 1:
207        var_name = short_name(model, e.vars[0])
208        coeff = e.coeffs[0]
209        result = ""
210        if coeff == 1:
211            result = var_name
212        elif coeff == -1:
213            result = f"-{var_name}"
214        elif coeff != 0:
215            result = f"{coeff} * {var_name}"
216        if e.offset > 0:
217            result = f"{result} + {e.offset}"
218        elif e.offset < 0:
219            result = f"{result} - {-e.offset}"
220        return result
221    # TODO(user): Support more than affine expressions.
222    return str(e)

Pretty-print LinearExpressionProto instances.

225class IntVar(cmh.BaseIntVar):
226    """An integer variable.
227
228    An IntVar is an object that can take on any integer value within defined
229    ranges. Variables appear in constraint like:
230
231        x + y >= 5
232        AllDifferent([x, y, z])
233
234    Solving a model is equivalent to finding, for each variable, a single value
235    from the set of initial values (called the initial domain), such that the
236    model is feasible, or optimal if you provided an objective function.
237    """
238
239    def __init__(
240        self,
241        model: cp_model_pb2.CpModelProto,
242        domain: Union[int, sorted_interval_list.Domain],
243        is_boolean: bool,
244        name: Optional[str],
245    ) -> None:
246        """See CpModel.new_int_var below."""
247        self.__model: cp_model_pb2.CpModelProto = model
248        # Python do not support multiple __init__ methods.
249        # This method is only called from the CpModel class.
250        # We hack the parameter to support the two cases:
251        # case 1:
252        #     model is a CpModelProto, domain is a Domain, and name is a string.
253        # case 2:
254        #     model is a CpModelProto, domain is an index (int), and name is None.
255        if isinstance(domain, IntegralTypes) and name is None:
256            cmh.BaseIntVar.__init__(self, int(domain), is_boolean)
257        else:
258            cmh.BaseIntVar.__init__(self, len(model.variables), is_boolean)
259            proto: cp_model_pb2.IntegerVariableProto = self.__model.variables.add()
260            proto.domain.extend(
261                cast(sorted_interval_list.Domain, domain).flattened_intervals()
262            )
263            if name is not None:
264                proto.name = name
265
266    def __copy__(self) -> "IntVar":
267        """Returns a shallowcopy of the variable."""
268        return IntVar(self.__model, self.index, self.is_boolean, None)
269
270    def __deepcopy__(self, memo: Any) -> "IntVar":
271        """Returns a deepcopy of the variable."""
272        return IntVar(
273            copy.deepcopy(self.__model, memo), self.index, self.is_boolean, None
274        )
275
276    @property
277    def proto(self) -> cp_model_pb2.IntegerVariableProto:
278        """Returns the variable protobuf."""
279        return self.__model.variables[self.index]
280
281    @property
282    def model_proto(self) -> cp_model_pb2.CpModelProto:
283        """Returns the model protobuf."""
284        return self.__model
285
286    def is_equal_to(self, other: Any) -> bool:
287        """Returns true if self == other in the python sense."""
288        if not isinstance(other, IntVar):
289            return False
290        return self.index == other.index
291
292    def __str__(self) -> str:
293        if not self.proto.name:
294            if (
295                len(self.proto.domain) == 2
296                and self.proto.domain[0] == self.proto.domain[1]
297            ):
298                # Special case for constants.
299                return str(self.proto.domain[0])
300            elif self.is_boolean:
301                return f"BooleanVar({self.__index})"
302            else:
303                return f"IntVar({self.__index})"
304        else:
305            return self.proto.name
306
307    def __repr__(self) -> str:
308        return f"{self}({display_bounds(self.proto.domain)})"
309
310    @property
311    def name(self) -> str:
312        if not self.proto or not self.proto.name:
313            return ""
314        return self.proto.name
315
316    # Pre PEP8 compatibility.
317    # pylint: disable=invalid-name
318    def Name(self) -> str:
319        return self.name
320
321    def Proto(self) -> cp_model_pb2.IntegerVariableProto:
322        return self.proto
323
324    # pylint: enable=invalid-name

An integer variable.

An IntVar is an object that can take on any integer value within defined ranges. Variables appear in constraint like:

x + y >= 5
AllDifferent([x, y, z])

Solving a model is equivalent to finding, for each variable, a single value from the set of initial values (called the initial domain), such that the model is feasible, or optimal if you provided an objective function.

IntVar( model: ortools.sat.cp_model_pb2.CpModelProto, domain: Union[int, Domain], is_boolean: bool, name: Optional[str])
239    def __init__(
240        self,
241        model: cp_model_pb2.CpModelProto,
242        domain: Union[int, sorted_interval_list.Domain],
243        is_boolean: bool,
244        name: Optional[str],
245    ) -> None:
246        """See CpModel.new_int_var below."""
247        self.__model: cp_model_pb2.CpModelProto = model
248        # Python do not support multiple __init__ methods.
249        # This method is only called from the CpModel class.
250        # We hack the parameter to support the two cases:
251        # case 1:
252        #     model is a CpModelProto, domain is a Domain, and name is a string.
253        # case 2:
254        #     model is a CpModelProto, domain is an index (int), and name is None.
255        if isinstance(domain, IntegralTypes) and name is None:
256            cmh.BaseIntVar.__init__(self, int(domain), is_boolean)
257        else:
258            cmh.BaseIntVar.__init__(self, len(model.variables), is_boolean)
259            proto: cp_model_pb2.IntegerVariableProto = self.__model.variables.add()
260            proto.domain.extend(
261                cast(sorted_interval_list.Domain, domain).flattened_intervals()
262            )
263            if name is not None:
264                proto.name = name
276    @property
277    def proto(self) -> cp_model_pb2.IntegerVariableProto:
278        """Returns the variable protobuf."""
279        return self.__model.variables[self.index]

Returns the variable protobuf.

model_proto: ortools.sat.cp_model_pb2.CpModelProto
281    @property
282    def model_proto(self) -> cp_model_pb2.CpModelProto:
283        """Returns the model protobuf."""
284        return self.__model

Returns the model protobuf.

def is_equal_to(self, other: Any) -> bool:
286    def is_equal_to(self, other: Any) -> bool:
287        """Returns true if self == other in the python sense."""
288        if not isinstance(other, IntVar):
289            return False
290        return self.index == other.index

Returns true if self == other in the python sense.

name: str
310    @property
311    def name(self) -> str:
312        if not self.proto or not self.proto.name:
313            return ""
314        return self.proto.name
def Name(self) -> str:
318    def Name(self) -> str:
319        return self.name
def Proto(self) -> ortools.sat.cp_model_pb2.IntegerVariableProto:
321    def Proto(self) -> cp_model_pb2.IntegerVariableProto:
322        return self.proto
class Constraint:
327class Constraint:
328    """Base class for constraints.
329
330    Constraints are built by the CpModel through the add<XXX> methods.
331    Once created by the CpModel class, they are automatically added to the model.
332    The purpose of this class is to allow specification of enforcement literals
333    for this constraint.
334
335        b = model.new_bool_var('b')
336        x = model.new_int_var(0, 10, 'x')
337        y = model.new_int_var(0, 10, 'y')
338
339        model.add(x + 2 * y == 5).only_enforce_if(b.negated())
340    """
341
342    def __init__(
343        self,
344        cp_model: "CpModel",
345    ) -> None:
346        self.__index: int = len(cp_model.proto.constraints)
347        self.__cp_model: "CpModel" = cp_model
348        self.__constraint: cp_model_pb2.ConstraintProto = (
349            cp_model.proto.constraints.add()
350        )
351
352    @overload
353    def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": ...
354
355    @overload
356    def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": ...
357
358    def only_enforce_if(self, *boolvar) -> "Constraint":
359        """Adds an enforcement literal to the constraint.
360
361        This method adds one or more literals (that is, a boolean variable or its
362        negation) as enforcement literals. The conjunction of all these literals
363        determines whether the constraint is active or not. It acts as an
364        implication, so if the conjunction is true, it implies that the constraint
365        must be enforced. If it is false, then the constraint is ignored.
366
367        BoolOr, BoolAnd, and linear constraints all support enforcement literals.
368
369        Args:
370          *boolvar: One or more Boolean literals.
371
372        Returns:
373          self.
374        """
375        for lit in expand_generator_or_tuple(boolvar):
376            if (cmn.is_boolean(lit) and lit) or (
377                isinstance(lit, IntegralTypes) and lit == 1
378            ):
379                # Always true. Do nothing.
380                pass
381            elif (cmn.is_boolean(lit) and not lit) or (
382                isinstance(lit, IntegralTypes) and lit == 0
383            ):
384                self.__constraint.enforcement_literal.append(
385                    self.__cp_model.new_constant(0).index
386                )
387            else:
388                self.__constraint.enforcement_literal.append(
389                    cast(cmh.Literal, lit).index
390                )
391        return self
392
393    def with_name(self, name: str) -> "Constraint":
394        """Sets the name of the constraint."""
395        if name:
396            self.__constraint.name = name
397        else:
398            self.__constraint.ClearField("name")
399        return self
400
401    @property
402    def name(self) -> str:
403        """Returns the name of the constraint."""
404        if not self.__constraint or not self.__constraint.name:
405            return ""
406        return self.__constraint.name
407
408    @property
409    def index(self) -> int:
410        """Returns the index of the constraint in the model."""
411        return self.__index
412
413    @property
414    def proto(self) -> cp_model_pb2.ConstraintProto:
415        """Returns the constraint protobuf."""
416        return self.__constraint
417
418    # Pre PEP8 compatibility.
419    # pylint: disable=invalid-name
420    OnlyEnforceIf = only_enforce_if
421    WithName = with_name
422
423    def Name(self) -> str:
424        return self.name
425
426    def Index(self) -> int:
427        return self.index
428
429    def Proto(self) -> cp_model_pb2.ConstraintProto:
430        return self.proto
431
432    # pylint: enable=invalid-name

Base class for constraints.

Constraints are built by the CpModel through the add methods. Once created by the CpModel class, they are automatically added to the model. The purpose of this class is to allow specification of enforcement literals for this constraint.

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())
Constraint(cp_model: CpModel)
342    def __init__(
343        self,
344        cp_model: "CpModel",
345    ) -> None:
346        self.__index: int = len(cp_model.proto.constraints)
347        self.__cp_model: "CpModel" = cp_model
348        self.__constraint: cp_model_pb2.ConstraintProto = (
349            cp_model.proto.constraints.add()
350        )
def only_enforce_if(self, *boolvar) -> Constraint:
358    def only_enforce_if(self, *boolvar) -> "Constraint":
359        """Adds an enforcement literal to the constraint.
360
361        This method adds one or more literals (that is, a boolean variable or its
362        negation) as enforcement literals. The conjunction of all these literals
363        determines whether the constraint is active or not. It acts as an
364        implication, so if the conjunction is true, it implies that the constraint
365        must be enforced. If it is false, then the constraint is ignored.
366
367        BoolOr, BoolAnd, and linear constraints all support enforcement literals.
368
369        Args:
370          *boolvar: One or more Boolean literals.
371
372        Returns:
373          self.
374        """
375        for lit in expand_generator_or_tuple(boolvar):
376            if (cmn.is_boolean(lit) and lit) or (
377                isinstance(lit, IntegralTypes) and lit == 1
378            ):
379                # Always true. Do nothing.
380                pass
381            elif (cmn.is_boolean(lit) and not lit) or (
382                isinstance(lit, IntegralTypes) and lit == 0
383            ):
384                self.__constraint.enforcement_literal.append(
385                    self.__cp_model.new_constant(0).index
386                )
387            else:
388                self.__constraint.enforcement_literal.append(
389                    cast(cmh.Literal, lit).index
390                )
391        return self

Adds an enforcement literal to the constraint.

This method adds one or more literals (that is, a boolean variable or its negation) as enforcement literals. The conjunction of all these literals determines whether the constraint is active or not. It acts as an implication, so if the conjunction is true, it implies that the constraint must be enforced. If it is false, then the constraint is ignored.

BoolOr, BoolAnd, and linear constraints all support enforcement literals.

Arguments:
  • *boolvar: One or more Boolean literals.
Returns:

self.

def with_name(self, name: str) -> Constraint:
393    def with_name(self, name: str) -> "Constraint":
394        """Sets the name of the constraint."""
395        if name:
396            self.__constraint.name = name
397        else:
398            self.__constraint.ClearField("name")
399        return self

Sets the name of the constraint.

name: str
401    @property
402    def name(self) -> str:
403        """Returns the name of the constraint."""
404        if not self.__constraint or not self.__constraint.name:
405            return ""
406        return self.__constraint.name

Returns the name of the constraint.

index: int
408    @property
409    def index(self) -> int:
410        """Returns the index of the constraint in the model."""
411        return self.__index

Returns the index of the constraint in the model.

413    @property
414    def proto(self) -> cp_model_pb2.ConstraintProto:
415        """Returns the constraint protobuf."""
416        return self.__constraint

Returns the constraint protobuf.

def OnlyEnforceIf(self, *boolvar) -> Constraint:
358    def only_enforce_if(self, *boolvar) -> "Constraint":
359        """Adds an enforcement literal to the constraint.
360
361        This method adds one or more literals (that is, a boolean variable or its
362        negation) as enforcement literals. The conjunction of all these literals
363        determines whether the constraint is active or not. It acts as an
364        implication, so if the conjunction is true, it implies that the constraint
365        must be enforced. If it is false, then the constraint is ignored.
366
367        BoolOr, BoolAnd, and linear constraints all support enforcement literals.
368
369        Args:
370          *boolvar: One or more Boolean literals.
371
372        Returns:
373          self.
374        """
375        for lit in expand_generator_or_tuple(boolvar):
376            if (cmn.is_boolean(lit) and lit) or (
377                isinstance(lit, IntegralTypes) and lit == 1
378            ):
379                # Always true. Do nothing.
380                pass
381            elif (cmn.is_boolean(lit) and not lit) or (
382                isinstance(lit, IntegralTypes) and lit == 0
383            ):
384                self.__constraint.enforcement_literal.append(
385                    self.__cp_model.new_constant(0).index
386                )
387            else:
388                self.__constraint.enforcement_literal.append(
389                    cast(cmh.Literal, lit).index
390                )
391        return self

Adds an enforcement literal to the constraint.

This method adds one or more literals (that is, a boolean variable or its negation) as enforcement literals. The conjunction of all these literals determines whether the constraint is active or not. It acts as an implication, so if the conjunction is true, it implies that the constraint must be enforced. If it is false, then the constraint is ignored.

BoolOr, BoolAnd, and linear constraints all support enforcement literals.

Arguments:
  • *boolvar: One or more Boolean literals.
Returns:

self.

def WithName(self, name: str) -> Constraint:
393    def with_name(self, name: str) -> "Constraint":
394        """Sets the name of the constraint."""
395        if name:
396            self.__constraint.name = name
397        else:
398            self.__constraint.ClearField("name")
399        return self

Sets the name of the constraint.

def Name(self) -> str:
423    def Name(self) -> str:
424        return self.name
def Index(self) -> int:
426    def Index(self) -> int:
427        return self.index
def Proto(self) -> ortools.sat.cp_model_pb2.ConstraintProto:
429    def Proto(self) -> cp_model_pb2.ConstraintProto:
430        return self.proto
class VariableList:
435class VariableList:
436    """Stores all integer variables of the model."""
437
438    def __init__(self) -> None:
439        self.__var_list: list[IntVar] = []
440
441    def append(self, var: IntVar) -> None:
442        assert var.index == len(self.__var_list)
443        self.__var_list.append(var)
444
445    def get(self, index: int) -> IntVar:
446        if index < 0 or index >= len(self.__var_list):
447            raise ValueError("Index out of bounds.")
448        return self.__var_list[index]
449
450    def rebuild_expr(
451        self,
452        proto: cp_model_pb2.LinearExpressionProto,
453    ) -> LinearExprT:
454        """Recreate a LinearExpr from a LinearExpressionProto."""
455        num_elements = len(proto.vars)
456        if num_elements == 0:
457            return proto.offset
458        elif num_elements == 1:
459            var = self.get(proto.vars[0])
460            return LinearExpr.affine(
461                var, proto.coeffs[0], proto.offset
462            )  # pytype: disable=bad-return-type
463        else:
464            variables = []
465            for var_index in range(len(proto.vars)):
466                var = self.get(var_index)
467                variables.append(var)
468            if proto.offset != 0:
469                coeffs = []
470                coeffs.extend(proto.coeffs)
471                coeffs.append(1)
472                variables.append(proto.offset)
473                return LinearExpr.weighted_sum(variables, coeffs)
474            else:
475                return LinearExpr.weighted_sum(variables, proto.coeffs)

Stores all integer variables of the model.

def append(self, var: IntVar) -> None:
441    def append(self, var: IntVar) -> None:
442        assert var.index == len(self.__var_list)
443        self.__var_list.append(var)
def get(self, index: int) -> IntVar:
445    def get(self, index: int) -> IntVar:
446        if index < 0 or index >= len(self.__var_list):
447            raise ValueError("Index out of bounds.")
448        return self.__var_list[index]
def rebuild_expr( self, proto: ortools.sat.cp_model_pb2.LinearExpressionProto) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
450    def rebuild_expr(
451        self,
452        proto: cp_model_pb2.LinearExpressionProto,
453    ) -> LinearExprT:
454        """Recreate a LinearExpr from a LinearExpressionProto."""
455        num_elements = len(proto.vars)
456        if num_elements == 0:
457            return proto.offset
458        elif num_elements == 1:
459            var = self.get(proto.vars[0])
460            return LinearExpr.affine(
461                var, proto.coeffs[0], proto.offset
462            )  # pytype: disable=bad-return-type
463        else:
464            variables = []
465            for var_index in range(len(proto.vars)):
466                var = self.get(var_index)
467                variables.append(var)
468            if proto.offset != 0:
469                coeffs = []
470                coeffs.extend(proto.coeffs)
471                coeffs.append(1)
472                variables.append(proto.offset)
473                return LinearExpr.weighted_sum(variables, coeffs)
474            else:
475                return LinearExpr.weighted_sum(variables, proto.coeffs)

Recreate a LinearExpr from a LinearExpressionProto.

class IntervalVar:
478class IntervalVar:
479    """Represents an Interval variable.
480
481    An interval variable is both a constraint and a variable. It is defined by
482    three integer variables: start, size, and end.
483
484    It is a constraint because, internally, it enforces that start + size == end.
485
486    It is also a variable as it can appear in specific scheduling constraints:
487    NoOverlap, NoOverlap2D, Cumulative.
488
489    Optionally, an enforcement literal can be added to this constraint, in which
490    case these scheduling constraints will ignore interval variables with
491    enforcement literals assigned to false. Conversely, these constraints will
492    also set these enforcement literals to false if they cannot fit these
493    intervals into the schedule.
494
495    Raises:
496      ValueError: if start, size, end are not defined, or have the wrong type.
497    """
498
499    def __init__(
500        self,
501        model: cp_model_pb2.CpModelProto,
502        var_list: VariableList,
503        start: Union[cp_model_pb2.LinearExpressionProto, int],
504        size: Optional[cp_model_pb2.LinearExpressionProto],
505        end: Optional[cp_model_pb2.LinearExpressionProto],
506        is_present_index: Optional[int],
507        name: Optional[str],
508    ) -> None:
509        self.__model: cp_model_pb2.CpModelProto = model
510        self.__var_list: VariableList = var_list
511        self.__index: int
512        self.__ct: cp_model_pb2.ConstraintProto
513        # As with the IntVar::__init__ method, we hack the __init__ method to
514        # support two use cases:
515        #   case 1: called when creating a new interval variable.
516        #      {start|size|end} are linear expressions, is_present_index is either
517        #      None or the index of a Boolean literal. name is a string
518        #   case 2: called when querying an existing interval variable.
519        #      start_index is an int, all parameters after are None.
520        if isinstance(start, int):
521            if size is not None:
522                raise ValueError("size should be None")
523            if end is not None:
524                raise ValueError("end should be None")
525            if is_present_index is not None:
526                raise ValueError("is_present_index should be None")
527            self.__index = cast(int, start)
528            self.__ct = model.constraints[self.__index]
529        else:
530            self.__index = len(model.constraints)
531            self.__ct = self.__model.constraints.add()
532            if start is None:
533                raise TypeError("start is not defined")
534            self.__ct.interval.start.CopyFrom(start)
535            if size is None:
536                raise TypeError("size is not defined")
537            self.__ct.interval.size.CopyFrom(size)
538            if end is None:
539                raise TypeError("end is not defined")
540            self.__ct.interval.end.CopyFrom(end)
541            if is_present_index is not None:
542                self.__ct.enforcement_literal.append(is_present_index)
543            if name:
544                self.__ct.name = name
545
546    @property
547    def index(self) -> int:
548        """Returns the index of the interval constraint in the model."""
549        return self.__index
550
551    @property
552    def proto(self) -> cp_model_pb2.ConstraintProto:
553        """Returns the interval protobuf."""
554        return self.__model.constraints[self.__index]
555
556    @property
557    def model_proto(self) -> cp_model_pb2.CpModelProto:
558        """Returns the model protobuf."""
559        return self.__model
560
561    def __str__(self):
562        return self.proto.name
563
564    def __repr__(self):
565        interval = self.proto.interval
566        if self.proto.enforcement_literal:
567            return (
568                f"{self.proto.name}(start ="
569                f" {short_expr_name(self.__model, interval.start)}, size ="
570                f" {short_expr_name(self.__model, interval.size)}, end ="
571                f" {short_expr_name(self.__model, interval.end)}, is_present ="
572                f" {short_name(self.__model, self.proto.enforcement_literal[0])})"
573            )
574        else:
575            return (
576                f"{self.proto.name}(start ="
577                f" {short_expr_name(self.__model, interval.start)}, size ="
578                f" {short_expr_name(self.__model, interval.size)}, end ="
579                f" {short_expr_name(self.__model, interval.end)})"
580            )
581
582    @property
583    def name(self) -> str:
584        if not self.proto or not self.proto.name:
585            return ""
586        return self.proto.name
587
588    def start_expr(self) -> LinearExprT:
589        return self.__var_list.rebuild_expr(self.proto.interval.start)
590
591    def size_expr(self) -> LinearExprT:
592        return self.__var_list.rebuild_expr(self.proto.interval.size)
593
594    def end_expr(self) -> LinearExprT:
595        return self.__var_list.rebuild_expr(self.proto.interval.end)
596
597    # Pre PEP8 compatibility.
598    # pylint: disable=invalid-name
599    def Name(self) -> str:
600        return self.name
601
602    def Index(self) -> int:
603        return self.index
604
605    def Proto(self) -> cp_model_pb2.ConstraintProto:
606        return self.proto
607
608    StartExpr = start_expr
609    SizeExpr = size_expr
610    EndExpr = end_expr
611
612    # pylint: enable=invalid-name

Represents an Interval variable.

An interval variable is both a constraint and a variable. It is defined by three integer variables: start, size, and end.

It is a constraint because, internally, it enforces that start + size == end.

It is also a variable as it can appear in specific scheduling constraints: NoOverlap, NoOverlap2D, Cumulative.

Optionally, an enforcement literal can be added to this constraint, in which case these scheduling constraints will ignore interval variables with enforcement literals assigned to false. Conversely, these constraints will also set these enforcement literals to false if they cannot fit these intervals into the schedule.

Raises:
  • ValueError: if start, size, end are not defined, or have the wrong type.
IntervalVar( model: ortools.sat.cp_model_pb2.CpModelProto, var_list: VariableList, start: Union[ortools.sat.cp_model_pb2.LinearExpressionProto, int], size: Optional[ortools.sat.cp_model_pb2.LinearExpressionProto], end: Optional[ortools.sat.cp_model_pb2.LinearExpressionProto], is_present_index: Optional[int], name: Optional[str])
499    def __init__(
500        self,
501        model: cp_model_pb2.CpModelProto,
502        var_list: VariableList,
503        start: Union[cp_model_pb2.LinearExpressionProto, int],
504        size: Optional[cp_model_pb2.LinearExpressionProto],
505        end: Optional[cp_model_pb2.LinearExpressionProto],
506        is_present_index: Optional[int],
507        name: Optional[str],
508    ) -> None:
509        self.__model: cp_model_pb2.CpModelProto = model
510        self.__var_list: VariableList = var_list
511        self.__index: int
512        self.__ct: cp_model_pb2.ConstraintProto
513        # As with the IntVar::__init__ method, we hack the __init__ method to
514        # support two use cases:
515        #   case 1: called when creating a new interval variable.
516        #      {start|size|end} are linear expressions, is_present_index is either
517        #      None or the index of a Boolean literal. name is a string
518        #   case 2: called when querying an existing interval variable.
519        #      start_index is an int, all parameters after are None.
520        if isinstance(start, int):
521            if size is not None:
522                raise ValueError("size should be None")
523            if end is not None:
524                raise ValueError("end should be None")
525            if is_present_index is not None:
526                raise ValueError("is_present_index should be None")
527            self.__index = cast(int, start)
528            self.__ct = model.constraints[self.__index]
529        else:
530            self.__index = len(model.constraints)
531            self.__ct = self.__model.constraints.add()
532            if start is None:
533                raise TypeError("start is not defined")
534            self.__ct.interval.start.CopyFrom(start)
535            if size is None:
536                raise TypeError("size is not defined")
537            self.__ct.interval.size.CopyFrom(size)
538            if end is None:
539                raise TypeError("end is not defined")
540            self.__ct.interval.end.CopyFrom(end)
541            if is_present_index is not None:
542                self.__ct.enforcement_literal.append(is_present_index)
543            if name:
544                self.__ct.name = name
index: int
546    @property
547    def index(self) -> int:
548        """Returns the index of the interval constraint in the model."""
549        return self.__index

Returns the index of the interval constraint in the model.

551    @property
552    def proto(self) -> cp_model_pb2.ConstraintProto:
553        """Returns the interval protobuf."""
554        return self.__model.constraints[self.__index]

Returns the interval protobuf.

model_proto: ortools.sat.cp_model_pb2.CpModelProto
556    @property
557    def model_proto(self) -> cp_model_pb2.CpModelProto:
558        """Returns the model protobuf."""
559        return self.__model

Returns the model protobuf.

name: str
582    @property
583    def name(self) -> str:
584        if not self.proto or not self.proto.name:
585            return ""
586        return self.proto.name
def start_expr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
588    def start_expr(self) -> LinearExprT:
589        return self.__var_list.rebuild_expr(self.proto.interval.start)
def size_expr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
591    def size_expr(self) -> LinearExprT:
592        return self.__var_list.rebuild_expr(self.proto.interval.size)
def end_expr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
594    def end_expr(self) -> LinearExprT:
595        return self.__var_list.rebuild_expr(self.proto.interval.end)
def Name(self) -> str:
599    def Name(self) -> str:
600        return self.name
def Index(self) -> int:
602    def Index(self) -> int:
603        return self.index
def Proto(self) -> ortools.sat.cp_model_pb2.ConstraintProto:
605    def Proto(self) -> cp_model_pb2.ConstraintProto:
606        return self.proto
def StartExpr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
588    def start_expr(self) -> LinearExprT:
589        return self.__var_list.rebuild_expr(self.proto.interval.start)
def SizeExpr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
591    def size_expr(self) -> LinearExprT:
592        return self.__var_list.rebuild_expr(self.proto.interval.size)
def EndExpr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
594    def end_expr(self) -> LinearExprT:
595        return self.__var_list.rebuild_expr(self.proto.interval.end)
def object_is_a_true_literal( literal: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
615def object_is_a_true_literal(literal: LiteralT) -> bool:
616    """Checks if literal is either True, or a Boolean literals fixed to True."""
617    if isinstance(literal, IntVar):
618        proto = literal.proto
619        return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1
620    if isinstance(literal, cmh.NotBooleanVariable):
621        proto = literal.negated().proto
622        return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0
623    if isinstance(literal, IntegralTypes):
624        return int(literal) == 1
625    return False

Checks if literal is either True, or a Boolean literals fixed to True.

def object_is_a_false_literal( literal: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
628def object_is_a_false_literal(literal: LiteralT) -> bool:
629    """Checks if literal is either False, or a Boolean literals fixed to False."""
630    if isinstance(literal, IntVar):
631        proto = literal.proto
632        return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0
633    if isinstance(literal, cmh.NotBooleanVariable):
634        proto = literal.negated().proto
635        return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1
636    if isinstance(literal, IntegralTypes):
637        return int(literal) == 0
638    return False

Checks if literal is either False, or a Boolean literals fixed to False.

class CpModel:
 641class CpModel:
 642    """Methods for building a CP model.
 643
 644    Methods beginning with:
 645
 646    * ```New``` create integer, boolean, or interval variables.
 647    * ```add``` create new constraints and add them to the model.
 648    """
 649
 650    def __init__(self) -> None:
 651        self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto()
 652        self.__constant_map: Dict[IntegralT, int] = {}
 653        self.__var_list: VariableList = VariableList()
 654
 655    # Naming.
 656    @property
 657    def name(self) -> str:
 658        """Returns the name of the model."""
 659        if not self.__model or not self.__model.name:
 660            return ""
 661        return self.__model.name
 662
 663    @name.setter
 664    def name(self, name: str):
 665        """Sets the name of the model."""
 666        self.__model.name = name
 667
 668    # Integer variable.
 669
 670    def _append_int_var(self, var: IntVar) -> IntVar:
 671        """Appends an integer variable to the list of variables."""
 672        self.__var_list.append(var)
 673        return var
 674
 675    def _get_int_var(self, index: int) -> IntVar:
 676        return self.__var_list.get(index)
 677
 678    def rebuild_from_linear_expression_proto(
 679        self,
 680        proto: cp_model_pb2.LinearExpressionProto,
 681    ) -> LinearExpr:
 682        return self.__var_list.rebuild_expr(proto)
 683
 684    def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar:
 685        """Create an integer variable with domain [lb, ub].
 686
 687        The CP-SAT solver is limited to integer variables. If you have fractional
 688        values, scale them up so that they become integers; if you have strings,
 689        encode them as integers.
 690
 691        Args:
 692          lb: Lower bound for the variable.
 693          ub: Upper bound for the variable.
 694          name: The name of the variable.
 695
 696        Returns:
 697          a variable whose domain is [lb, ub].
 698        """
 699        domain_is_boolean = lb >= 0 and ub <= 1
 700        return self._append_int_var(
 701            IntVar(
 702                self.__model,
 703                sorted_interval_list.Domain(lb, ub),
 704                domain_is_boolean,
 705                name,
 706            )
 707        )
 708
 709    def new_int_var_from_domain(
 710        self, domain: sorted_interval_list.Domain, name: str
 711    ) -> IntVar:
 712        """Create an integer variable from a domain.
 713
 714        A domain is a set of integers specified by a collection of intervals.
 715        For example, `model.new_int_var_from_domain(cp_model.
 716             Domain.from_intervals([[1, 2], [4, 6]]), 'x')`
 717
 718        Args:
 719          domain: An instance of the Domain class.
 720          name: The name of the variable.
 721
 722        Returns:
 723            a variable whose domain is the given domain.
 724        """
 725        domain_is_boolean = domain.min() >= 0 and domain.max() <= 1
 726        return self._append_int_var(
 727            IntVar(self.__model, domain, domain_is_boolean, name)
 728        )
 729
 730    def new_bool_var(self, name: str) -> IntVar:
 731        """Creates a 0-1 variable with the given name."""
 732        return self._append_int_var(
 733            IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name)
 734        )
 735
 736    def new_constant(self, value: IntegralT) -> IntVar:
 737        """Declares a constant integer."""
 738        index: int = self.get_or_make_index_from_constant(value)
 739        return self._get_int_var(index)
 740
 741    def new_int_var_series(
 742        self,
 743        name: str,
 744        index: pd.Index,
 745        lower_bounds: Union[IntegralT, pd.Series],
 746        upper_bounds: Union[IntegralT, pd.Series],
 747    ) -> pd.Series:
 748        """Creates a series of (scalar-valued) variables with the given name.
 749
 750        Args:
 751          name (str): Required. The name of the variable set.
 752          index (pd.Index): Required. The index to use for the variable set.
 753          lower_bounds (Union[int, pd.Series]): A lower bound for variables in the
 754            set. If a `pd.Series` is passed in, it will be based on the
 755            corresponding values of the pd.Series.
 756          upper_bounds (Union[int, pd.Series]): An upper bound for variables in the
 757            set. If a `pd.Series` is passed in, it will be based on the
 758            corresponding values of the pd.Series.
 759
 760        Returns:
 761          pd.Series: The variable set indexed by its corresponding dimensions.
 762
 763        Raises:
 764          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 765          ValueError: if the `name` is not a valid identifier or already exists.
 766          ValueError: if the `lowerbound` is greater than the `upperbound`.
 767          ValueError: if the index of `lower_bound`, or `upper_bound` does not match
 768          the input index.
 769        """
 770        if not isinstance(index, pd.Index):
 771            raise TypeError("Non-index object is used as index")
 772        if not name.isidentifier():
 773            raise ValueError(f"name={name!r} is not a valid identifier")
 774        if (
 775            isinstance(lower_bounds, IntegralTypes)
 776            and isinstance(upper_bounds, IntegralTypes)
 777            and lower_bounds > upper_bounds
 778        ):
 779            raise ValueError(
 780                f"lower_bound={lower_bounds} is greater than"
 781                f" upper_bound={upper_bounds} for variable set={name}"
 782            )
 783
 784        lower_bounds = _convert_to_integral_series_and_validate_index(
 785            lower_bounds, index
 786        )
 787        upper_bounds = _convert_to_integral_series_and_validate_index(
 788            upper_bounds, index
 789        )
 790        return pd.Series(
 791            index=index,
 792            data=[
 793                # pylint: disable=g-complex-comprehension
 794                self._append_int_var(
 795                    IntVar(
 796                        model=self.__model,
 797                        name=f"{name}[{i}]",
 798                        domain=sorted_interval_list.Domain(
 799                            lower_bounds[i], upper_bounds[i]
 800                        ),
 801                        is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1,
 802                    )
 803                )
 804                for i in index
 805            ],
 806        )
 807
 808    def new_bool_var_series(
 809        self,
 810        name: str,
 811        index: pd.Index,
 812    ) -> pd.Series:
 813        """Creates a series of (scalar-valued) variables with the given name.
 814
 815        Args:
 816          name (str): Required. The name of the variable set.
 817          index (pd.Index): Required. The index to use for the variable set.
 818
 819        Returns:
 820          pd.Series: The variable set indexed by its corresponding dimensions.
 821
 822        Raises:
 823          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 824          ValueError: if the `name` is not a valid identifier or already exists.
 825        """
 826        if not isinstance(index, pd.Index):
 827            raise TypeError("Non-index object is used as index")
 828        if not name.isidentifier():
 829            raise ValueError(f"name={name!r} is not a valid identifier")
 830        return pd.Series(
 831            index=index,
 832            data=[
 833                # pylint: disable=g-complex-comprehension
 834                self._append_int_var(
 835                    IntVar(
 836                        model=self.__model,
 837                        name=f"{name}[{i}]",
 838                        domain=sorted_interval_list.Domain(0, 1),
 839                        is_boolean=True,
 840                    )
 841                )
 842                for i in index
 843            ],
 844        )
 845
 846    # Linear constraints.
 847
 848    def add_linear_constraint(
 849        self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT
 850    ) -> Constraint:
 851        """Adds the constraint: `lb <= linear_expr <= ub`."""
 852        return self.add_linear_expression_in_domain(
 853            linear_expr, sorted_interval_list.Domain(lb, ub)
 854        )
 855
 856    def add_linear_expression_in_domain(
 857        self,
 858        linear_expr: LinearExprT,
 859        domain: sorted_interval_list.Domain,
 860    ) -> Constraint:
 861        """Adds the constraint: `linear_expr` in `domain`."""
 862        if isinstance(linear_expr, LinearExpr):
 863            ble = BoundedLinearExpression(linear_expr, domain)
 864            if not ble.ok:
 865                raise TypeError(
 866                    "Cannot add a linear expression containing floating point"
 867                    f" coefficients or constants: {type(linear_expr).__name__!r}"
 868                )
 869            return self.add(ble)
 870        if isinstance(linear_expr, IntegralTypes):
 871            if not domain.contains(int(linear_expr)):
 872                return self.add_bool_or([])  # Evaluate to false.
 873            else:
 874                return self.add_bool_and([])  # Evaluate to true.
 875        raise TypeError(
 876            "not supported:"
 877            f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})"
 878        )
 879
 880    def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint:
 881        """Adds a `BoundedLinearExpression` to the model.
 882
 883        Args:
 884          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
 885
 886        Returns:
 887          An instance of the `Constraint` class.
 888
 889        Raises:
 890          TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean.
 891        """
 892        if isinstance(ct, BoundedLinearExpression):
 893            result = Constraint(self)
 894            model_ct = self.__model.constraints[result.index]
 895            for var in ct.vars:
 896                model_ct.linear.vars.append(var.index)
 897            model_ct.linear.coeffs.extend(ct.coeffs)
 898            model_ct.linear.domain.extend(
 899                [
 900                    cmn.capped_subtraction(x, ct.offset)
 901                    for x in ct.bounds.flattened_intervals()
 902                ]
 903            )
 904            return result
 905        if ct and cmn.is_boolean(ct):
 906            return self.add_bool_or([True])
 907        if not ct and cmn.is_boolean(ct):
 908            return self.add_bool_or([])  # Evaluate to false.
 909        raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})")
 910
 911    # General Integer Constraints.
 912
 913    @overload
 914    def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ...
 915
 916    @overload
 917    def add_all_different(self, *expressions: LinearExprT) -> Constraint: ...
 918
 919    def add_all_different(self, *expressions):
 920        """Adds AllDifferent(expressions).
 921
 922        This constraint forces all expressions to have different values.
 923
 924        Args:
 925          *expressions: simple expressions of the form a * var + constant.
 926
 927        Returns:
 928          An instance of the `Constraint` class.
 929        """
 930        ct = Constraint(self)
 931        model_ct = self.__model.constraints[ct.index]
 932        expanded = expand_generator_or_tuple(expressions)
 933        model_ct.all_diff.exprs.extend(
 934            self.parse_linear_expression(x) for x in expanded
 935        )
 936        return ct
 937
 938    def add_element(
 939        self,
 940        index: LinearExprT,
 941        expressions: Sequence[LinearExprT],
 942        target: LinearExprT,
 943    ) -> Constraint:
 944        """Adds the element constraint: `expressions[index] == target`.
 945
 946        Args:
 947          index: The index of the selected expression in the array. It must be an
 948            affine expression (a * var + b).
 949          expressions: A list of affine expressions.
 950          target: The expression constrained to be equal to the selected expression.
 951            It must be an affine expression (a * var + b).
 952
 953        Returns:
 954          An instance of the `Constraint` class.
 955        """
 956
 957        if not expressions:
 958            raise ValueError("add_element expects a non-empty expressions array")
 959
 960        if isinstance(index, IntegralTypes):
 961            expression: LinearExprT = list(expressions)[int(index)]
 962            return self.add(expression == target)
 963
 964        ct = Constraint(self)
 965        model_ct = self.__model.constraints[ct.index]
 966        model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index))
 967        model_ct.element.exprs.extend(
 968            [self.parse_linear_expression(e) for e in expressions]
 969        )
 970        model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target))
 971        return ct
 972
 973    def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
 974        """Adds Circuit(arcs).
 975
 976        Adds a circuit constraint from a sparse list of arcs that encode the graph.
 977
 978        A circuit is a unique Hamiltonian cycle in a subgraph of the total
 979        graph. In case a node 'i' is not in the cycle, then there must be a
 980        loop arc 'i -> i' associated with a true literal. Otherwise
 981        this constraint will fail.
 982
 983        Args:
 984          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
 985            literal). The arc is selected in the circuit if the literal is true.
 986            Both source_node and destination_node must be integers between 0 and the
 987            number of nodes - 1.
 988
 989        Returns:
 990          An instance of the `Constraint` class.
 991
 992        Raises:
 993          ValueError: If the list of arcs is empty.
 994        """
 995        if not arcs:
 996            raise ValueError("add_circuit expects a non-empty array of arcs")
 997        ct = Constraint(self)
 998        model_ct = self.__model.constraints[ct.index]
 999        for arc in arcs:
1000            model_ct.circuit.tails.append(arc[0])
1001            model_ct.circuit.heads.append(arc[1])
1002            model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2]))
1003        return ct
1004
1005    def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1006        """Adds a multiple circuit constraint, aka the 'VRP' constraint.
1007
1008        The direct graph where arc #i (from tails[i] to head[i]) is present iff
1009        literals[i] is true must satisfy this set of properties:
1010        - #incoming arcs == 1 except for node 0.
1011        - #outgoing arcs == 1 except for node 0.
1012        - for node zero, #incoming arcs == #outgoing arcs.
1013        - There are no duplicate arcs.
1014        - Self-arcs are allowed except for node 0.
1015        - There is no cycle in this graph, except through node 0.
1016
1017        Args:
1018          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1019            literal). The arc is selected in the circuit if the literal is true.
1020            Both source_node and destination_node must be integers between 0 and the
1021            number of nodes - 1.
1022
1023        Returns:
1024          An instance of the `Constraint` class.
1025
1026        Raises:
1027          ValueError: If the list of arcs is empty.
1028        """
1029        if not arcs:
1030            raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
1031        ct = Constraint(self)
1032        model_ct = self.__model.constraints[ct.index]
1033        for arc in arcs:
1034            model_ct.routes.tails.append(arc[0])
1035            model_ct.routes.heads.append(arc[1])
1036            model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2]))
1037        return ct
1038
1039    def add_allowed_assignments(
1040        self,
1041        expressions: Sequence[LinearExprT],
1042        tuples_list: Iterable[Sequence[IntegralT]],
1043    ) -> Constraint:
1044        """Adds AllowedAssignments(expressions, tuples_list).
1045
1046        An AllowedAssignments constraint is a constraint on an array of affine
1047        expressions, which requires that when all expressions are assigned values,
1048        the
1049        resulting array equals one of the  tuples in `tuple_list`.
1050
1051        Args:
1052          expressions: A list of affine expressions (a * var + b).
1053          tuples_list: A list of admissible tuples. Each tuple must have the same
1054            length as the expressions, and the ith value of a tuple corresponds to
1055            the ith expression.
1056
1057        Returns:
1058          An instance of the `Constraint` class.
1059
1060        Raises:
1061          TypeError: If a tuple does not have the same size as the list of
1062              expressions.
1063          ValueError: If the array of expressions is empty.
1064        """
1065
1066        if not expressions:
1067            raise ValueError(
1068                "add_allowed_assignments expects a non-empty expressions array"
1069            )
1070
1071        ct: Constraint = Constraint(self)
1072        model_ct = self.__model.constraints[ct.index]
1073        model_ct.table.exprs.extend(
1074            [self.parse_linear_expression(e) for e in expressions]
1075        )
1076        arity: int = len(expressions)
1077        for one_tuple in tuples_list:
1078            if len(one_tuple) != arity:
1079                raise TypeError(f"Tuple {one_tuple!r} has the wrong arity")
1080
1081        # duck-typing (no explicit type checks here)
1082        try:
1083            for one_tuple in tuples_list:
1084                model_ct.table.values.extend(one_tuple)
1085        except ValueError as ex:
1086            raise TypeError(
1087                "add_xxx_assignment: Not an integer or does not fit in an int64_t:"
1088                f" {type(ex.args).__name__!r}"
1089            ) from ex
1090
1091        return ct
1092
1093    def add_forbidden_assignments(
1094        self,
1095        expressions: Sequence[LinearExprT],
1096        tuples_list: Iterable[Sequence[IntegralT]],
1097    ) -> Constraint:
1098        """Adds add_forbidden_assignments(expressions, [tuples_list]).
1099
1100        A ForbiddenAssignments constraint is a constraint on an array of affine
1101        expressions where the list of impossible combinations is provided in the
1102        tuples list.
1103
1104        Args:
1105          expressions: A list of affine expressions (a * var + b).
1106          tuples_list: A list of forbidden tuples. Each tuple must have the same
1107            length as the expressions, and the *i*th value of a tuple corresponds to
1108            the *i*th expression.
1109
1110        Returns:
1111          An instance of the `Constraint` class.
1112
1113        Raises:
1114          TypeError: If a tuple does not have the same size as the list of
1115                     expressions.
1116          ValueError: If the array of expressions is empty.
1117        """
1118
1119        if not expressions:
1120            raise ValueError(
1121                "add_forbidden_assignments expects a non-empty expressions array"
1122            )
1123
1124        index: int = len(self.__model.constraints)
1125        ct: Constraint = self.add_allowed_assignments(expressions, tuples_list)
1126        self.__model.constraints[index].table.negated = True
1127        return ct
1128
1129    def add_automaton(
1130        self,
1131        transition_expressions: Sequence[LinearExprT],
1132        starting_state: IntegralT,
1133        final_states: Sequence[IntegralT],
1134        transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]],
1135    ) -> Constraint:
1136        """Adds an automaton constraint.
1137
1138        An automaton constraint takes a list of affine expressions (a * var + b) (of
1139        size *n*), an initial state, a set of final states, and a set of
1140        transitions. A transition is a triplet (*tail*, *transition*, *head*), where
1141        *tail* and *head* are states, and *transition* is the label of an arc from
1142        *head* to *tail*, corresponding to the value of one expression in the list
1143        of
1144        expressions.
1145
1146        This automaton will be unrolled into a flow with *n* + 1 phases. Each phase
1147        contains the possible states of the automaton. The first state contains the
1148        initial state. The last phase contains the final states.
1149
1150        Between two consecutive phases *i* and *i* + 1, the automaton creates a set
1151        of arcs. For each transition (*tail*, *transition*, *head*), it will add
1152        an arc from the state *tail* of phase *i* and the state *head* of phase
1153        *i* + 1. This arc is labeled by the value *transition* of the expression
1154        `expressions[i]`. That is, this arc can only be selected if `expressions[i]`
1155        is assigned the value *transition*.
1156
1157        A feasible solution of this constraint is an assignment of expressions such
1158        that, starting from the initial state in phase 0, there is a path labeled by
1159        the values of the expressions that ends in one of the final states in the
1160        final phase.
1161
1162        Args:
1163          transition_expressions: A non-empty list of affine expressions (a * var +
1164            b) whose values correspond to the labels of the arcs traversed by the
1165            automaton.
1166          starting_state: The initial state of the automaton.
1167          final_states: A non-empty list of admissible final states.
1168          transition_triples: A list of transitions for the automaton, in the
1169            following format (current_state, variable_value, next_state).
1170
1171        Returns:
1172          An instance of the `Constraint` class.
1173
1174        Raises:
1175          ValueError: if `transition_expressions`, `final_states`, or
1176            `transition_triples` are empty.
1177        """
1178
1179        if not transition_expressions:
1180            raise ValueError(
1181                "add_automaton expects a non-empty transition_expressions array"
1182            )
1183        if not final_states:
1184            raise ValueError("add_automaton expects some final states")
1185
1186        if not transition_triples:
1187            raise ValueError("add_automaton expects some transition triples")
1188
1189        ct = Constraint(self)
1190        model_ct = self.__model.constraints[ct.index]
1191        model_ct.automaton.exprs.extend(
1192            [self.parse_linear_expression(e) for e in transition_expressions]
1193        )
1194        model_ct.automaton.starting_state = starting_state
1195        for v in final_states:
1196            model_ct.automaton.final_states.append(v)
1197        for t in transition_triples:
1198            if len(t) != 3:
1199                raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)")
1200            model_ct.automaton.transition_tail.append(t[0])
1201            model_ct.automaton.transition_label.append(t[1])
1202            model_ct.automaton.transition_head.append(t[2])
1203        return ct
1204
1205    def add_inverse(
1206        self,
1207        variables: Sequence[VariableT],
1208        inverse_variables: Sequence[VariableT],
1209    ) -> Constraint:
1210        """Adds Inverse(variables, inverse_variables).
1211
1212        An inverse constraint enforces that if `variables[i]` is assigned a value
1213        `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa.
1214
1215        Args:
1216          variables: An array of integer variables.
1217          inverse_variables: An array of integer variables.
1218
1219        Returns:
1220          An instance of the `Constraint` class.
1221
1222        Raises:
1223          TypeError: if variables and inverse_variables have different lengths, or
1224              if they are empty.
1225        """
1226
1227        if not variables or not inverse_variables:
1228            raise TypeError("The Inverse constraint does not accept empty arrays")
1229        if len(variables) != len(inverse_variables):
1230            raise TypeError(
1231                "In the inverse constraint, the two array variables and"
1232                " inverse_variables must have the same length."
1233            )
1234        ct = Constraint(self)
1235        model_ct = self.__model.constraints[ct.index]
1236        model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables])
1237        model_ct.inverse.f_inverse.extend(
1238            [self.get_or_make_index(x) for x in inverse_variables]
1239        )
1240        return ct
1241
1242    def add_reservoir_constraint(
1243        self,
1244        times: Iterable[LinearExprT],
1245        level_changes: Iterable[LinearExprT],
1246        min_level: int,
1247        max_level: int,
1248    ) -> Constraint:
1249        """Adds Reservoir(times, level_changes, min_level, max_level).
1250
1251        Maintains a reservoir level within bounds. The water level starts at 0, and
1252        at any time, it must be between min_level and max_level.
1253
1254        If the affine expression `times[i]` is assigned a value t, then the current
1255        level changes by `level_changes[i]`, which is constant, at time t.
1256
1257         Note that min level must be <= 0, and the max level must be >= 0. Please
1258         use fixed level_changes to simulate initial state.
1259
1260         Therefore, at any time:
1261             sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
1262
1263        Args:
1264          times: A list of 1-var affine expressions (a * x + b) which specify the
1265            time of the filling or emptying the reservoir.
1266          level_changes: A list of integer values that specifies the amount of the
1267            emptying or filling. Currently, variable demands are not supported.
1268          min_level: At any time, the level of the reservoir must be greater or
1269            equal than the min level.
1270          max_level: At any time, the level of the reservoir must be less or equal
1271            than the max level.
1272
1273        Returns:
1274          An instance of the `Constraint` class.
1275
1276        Raises:
1277          ValueError: if max_level < min_level.
1278
1279          ValueError: if max_level < 0.
1280
1281          ValueError: if min_level > 0
1282        """
1283
1284        if max_level < min_level:
1285            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1286
1287        if max_level < 0:
1288            raise ValueError("Reservoir constraint must have a max_level >= 0")
1289
1290        if min_level > 0:
1291            raise ValueError("Reservoir constraint must have a min_level <= 0")
1292
1293        ct = Constraint(self)
1294        model_ct = self.__model.constraints[ct.index]
1295        model_ct.reservoir.time_exprs.extend(
1296            [self.parse_linear_expression(x) for x in times]
1297        )
1298        model_ct.reservoir.level_changes.extend(
1299            [self.parse_linear_expression(x) for x in level_changes]
1300        )
1301        model_ct.reservoir.min_level = min_level
1302        model_ct.reservoir.max_level = max_level
1303        return ct
1304
1305    def add_reservoir_constraint_with_active(
1306        self,
1307        times: Iterable[LinearExprT],
1308        level_changes: Iterable[LinearExprT],
1309        actives: Iterable[LiteralT],
1310        min_level: int,
1311        max_level: int,
1312    ) -> Constraint:
1313        """Adds Reservoir(times, level_changes, actives, min_level, max_level).
1314
1315        Maintains a reservoir level within bounds. The water level starts at 0, and
1316        at any time, it must be between min_level and max_level.
1317
1318        If the variable `times[i]` is assigned a value t, and `actives[i]` is
1319        `True`, then the current level changes by `level_changes[i]`, which is
1320        constant,
1321        at time t.
1322
1323         Note that min level must be <= 0, and the max level must be >= 0. Please
1324         use fixed level_changes to simulate initial state.
1325
1326         Therefore, at any time:
1327             sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level,
1328             max_level]
1329
1330
1331        The array of boolean variables 'actives', if defined, indicates which
1332        actions are actually performed.
1333
1334        Args:
1335          times: A list of 1-var affine expressions (a * x + b) which specify the
1336            time of the filling or emptying the reservoir.
1337          level_changes: A list of integer values that specifies the amount of the
1338            emptying or filling. Currently, variable demands are not supported.
1339          actives: a list of boolean variables. They indicates if the
1340            emptying/refilling events actually take place.
1341          min_level: At any time, the level of the reservoir must be greater or
1342            equal than the min level.
1343          max_level: At any time, the level of the reservoir must be less or equal
1344            than the max level.
1345
1346        Returns:
1347          An instance of the `Constraint` class.
1348
1349        Raises:
1350          ValueError: if max_level < min_level.
1351
1352          ValueError: if max_level < 0.
1353
1354          ValueError: if min_level > 0
1355        """
1356
1357        if max_level < min_level:
1358            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1359
1360        if max_level < 0:
1361            raise ValueError("Reservoir constraint must have a max_level >= 0")
1362
1363        if min_level > 0:
1364            raise ValueError("Reservoir constraint must have a min_level <= 0")
1365
1366        ct = Constraint(self)
1367        model_ct = self.__model.constraints[ct.index]
1368        model_ct.reservoir.time_exprs.extend(
1369            [self.parse_linear_expression(x) for x in times]
1370        )
1371        model_ct.reservoir.level_changes.extend(
1372            [self.parse_linear_expression(x) for x in level_changes]
1373        )
1374        model_ct.reservoir.active_literals.extend(
1375            [self.get_or_make_boolean_index(x) for x in actives]
1376        )
1377        model_ct.reservoir.min_level = min_level
1378        model_ct.reservoir.max_level = max_level
1379        return ct
1380
1381    def add_map_domain(
1382        self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0
1383    ):
1384        """Adds `var == i + offset <=> bool_var_array[i] == true for all i`."""
1385
1386        for i, bool_var in enumerate(bool_var_array):
1387            b_index = bool_var.index
1388            var_index = var.index
1389            model_ct = self.__model.constraints.add()
1390            model_ct.linear.vars.append(var_index)
1391            model_ct.linear.coeffs.append(1)
1392            offset_as_int = int(offset)
1393            model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i])
1394            model_ct.enforcement_literal.append(b_index)
1395
1396            model_ct = self.__model.constraints.add()
1397            model_ct.linear.vars.append(var_index)
1398            model_ct.linear.coeffs.append(1)
1399            model_ct.enforcement_literal.append(-b_index - 1)
1400            if offset + i - 1 >= INT_MIN:
1401                model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1])
1402            if offset + i + 1 <= INT_MAX:
1403                model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX])
1404
1405    def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint:
1406        """Adds `a => b` (`a` implies `b`)."""
1407        ct = Constraint(self)
1408        model_ct = self.__model.constraints[ct.index]
1409        model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b))
1410        model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a))
1411        return ct
1412
1413    @overload
1414    def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ...
1415
1416    @overload
1417    def add_bool_or(self, *literals: LiteralT) -> Constraint: ...
1418
1419    def add_bool_or(self, *literals):
1420        """Adds `Or(literals) == true`: sum(literals) >= 1."""
1421        ct = Constraint(self)
1422        model_ct = self.__model.constraints[ct.index]
1423        model_ct.bool_or.literals.extend(
1424            [
1425                self.get_or_make_boolean_index(x)
1426                for x in expand_generator_or_tuple(literals)
1427            ]
1428        )
1429        return ct
1430
1431    @overload
1432    def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
1433
1434    @overload
1435    def add_at_least_one(self, *literals: LiteralT) -> Constraint: ...
1436
1437    def add_at_least_one(self, *literals):
1438        """Same as `add_bool_or`: `sum(literals) >= 1`."""
1439        return self.add_bool_or(*literals)
1440
1441    @overload
1442    def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
1443
1444    @overload
1445    def add_at_most_one(self, *literals: LiteralT) -> Constraint: ...
1446
1447    def add_at_most_one(self, *literals):
1448        """Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
1449        ct = Constraint(self)
1450        model_ct = self.__model.constraints[ct.index]
1451        model_ct.at_most_one.literals.extend(
1452            [
1453                self.get_or_make_boolean_index(x)
1454                for x in expand_generator_or_tuple(literals)
1455            ]
1456        )
1457        return ct
1458
1459    @overload
1460    def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
1461
1462    @overload
1463    def add_exactly_one(self, *literals: LiteralT) -> Constraint: ...
1464
1465    def add_exactly_one(self, *literals):
1466        """Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
1467        ct = Constraint(self)
1468        model_ct = self.__model.constraints[ct.index]
1469        model_ct.exactly_one.literals.extend(
1470            [
1471                self.get_or_make_boolean_index(x)
1472                for x in expand_generator_or_tuple(literals)
1473            ]
1474        )
1475        return ct
1476
1477    @overload
1478    def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ...
1479
1480    @overload
1481    def add_bool_and(self, *literals: LiteralT) -> Constraint: ...
1482
1483    def add_bool_and(self, *literals):
1484        """Adds `And(literals) == true`."""
1485        ct = Constraint(self)
1486        model_ct = self.__model.constraints[ct.index]
1487        model_ct.bool_and.literals.extend(
1488            [
1489                self.get_or_make_boolean_index(x)
1490                for x in expand_generator_or_tuple(literals)
1491            ]
1492        )
1493        return ct
1494
1495    @overload
1496    def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ...
1497
1498    @overload
1499    def add_bool_xor(self, *literals: LiteralT) -> Constraint: ...
1500
1501    def add_bool_xor(self, *literals):
1502        """Adds `XOr(literals) == true`.
1503
1504        In contrast to add_bool_or and add_bool_and, it does not support
1505            .only_enforce_if().
1506
1507        Args:
1508          *literals: the list of literals in the constraint.
1509
1510        Returns:
1511          An `Constraint` object.
1512        """
1513        ct = Constraint(self)
1514        model_ct = self.__model.constraints[ct.index]
1515        model_ct.bool_xor.literals.extend(
1516            [
1517                self.get_or_make_boolean_index(x)
1518                for x in expand_generator_or_tuple(literals)
1519            ]
1520        )
1521        return ct
1522
1523    def add_min_equality(
1524        self, target: LinearExprT, exprs: Iterable[LinearExprT]
1525    ) -> Constraint:
1526        """Adds `target == Min(exprs)`."""
1527        ct = Constraint(self)
1528        model_ct = self.__model.constraints[ct.index]
1529        model_ct.lin_max.exprs.extend(
1530            [self.parse_linear_expression(x, True) for x in exprs]
1531        )
1532        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True))
1533        return ct
1534
1535    def add_max_equality(
1536        self, target: LinearExprT, exprs: Iterable[LinearExprT]
1537    ) -> Constraint:
1538        """Adds `target == Max(exprs)`."""
1539        ct = Constraint(self)
1540        model_ct = self.__model.constraints[ct.index]
1541        model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs])
1542        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
1543        return ct
1544
1545    def add_division_equality(
1546        self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
1547    ) -> Constraint:
1548        """Adds `target == num // denom` (integer division rounded towards 0)."""
1549        ct = Constraint(self)
1550        model_ct = self.__model.constraints[ct.index]
1551        model_ct.int_div.exprs.append(self.parse_linear_expression(num))
1552        model_ct.int_div.exprs.append(self.parse_linear_expression(denom))
1553        model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target))
1554        return ct
1555
1556    def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
1557        """Adds `target == Abs(expr)`."""
1558        ct = Constraint(self)
1559        model_ct = self.__model.constraints[ct.index]
1560        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr))
1561        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True))
1562        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
1563        return ct
1564
1565    def add_modulo_equality(
1566        self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
1567    ) -> Constraint:
1568        """Adds `target = expr % mod`.
1569
1570        It uses the C convention, that is the result is the remainder of the
1571        integral division rounded towards 0.
1572
1573            For example:
1574            * 10 % 3 = 1
1575            * -10 % 3 = -1
1576            * 10 % -3 = 1
1577            * -10 % -3 = -1
1578
1579        Args:
1580          target: the target expression.
1581          expr: the expression to compute the modulo of.
1582          mod: the modulus expression.
1583
1584        Returns:
1585          A `Constraint` object.
1586        """
1587        ct = Constraint(self)
1588        model_ct = self.__model.constraints[ct.index]
1589        model_ct.int_mod.exprs.append(self.parse_linear_expression(expr))
1590        model_ct.int_mod.exprs.append(self.parse_linear_expression(mod))
1591        model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target))
1592        return ct
1593
1594    def add_multiplication_equality(
1595        self,
1596        target: LinearExprT,
1597        *expressions: Union[Iterable[LinearExprT], LinearExprT],
1598    ) -> Constraint:
1599        """Adds `target == expressions[0] * .. * expressions[n]`."""
1600        ct = Constraint(self)
1601        model_ct = self.__model.constraints[ct.index]
1602        model_ct.int_prod.exprs.extend(
1603            [
1604                self.parse_linear_expression(expr)
1605                for expr in expand_generator_or_tuple(expressions)
1606            ]
1607        )
1608        model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target))
1609        return ct
1610
1611    # Scheduling support
1612
1613    def new_interval_var(
1614        self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str
1615    ) -> IntervalVar:
1616        """Creates an interval variable from start, size, and end.
1617
1618        An interval variable is a constraint, that is itself used in other
1619        constraints like NoOverlap.
1620
1621        Internally, it ensures that `start + size == end`.
1622
1623        Args:
1624          start: The start of the interval. It must be of the form a * var + b.
1625          size: The size of the interval. It must be of the form a * var + b.
1626          end: The end of the interval. It must be of the form a * var + b.
1627          name: The name of the interval variable.
1628
1629        Returns:
1630          An `IntervalVar` object.
1631        """
1632
1633        start_expr = self.parse_linear_expression(start)
1634        size_expr = self.parse_linear_expression(size)
1635        end_expr = self.parse_linear_expression(end)
1636        if len(start_expr.vars) > 1:
1637            raise TypeError(
1638                "cp_model.new_interval_var: start must be 1-var affine or constant."
1639            )
1640        if len(size_expr.vars) > 1:
1641            raise TypeError(
1642                "cp_model.new_interval_var: size must be 1-var affine or constant."
1643            )
1644        if len(end_expr.vars) > 1:
1645            raise TypeError(
1646                "cp_model.new_interval_var: end must be 1-var affine or constant."
1647            )
1648        return IntervalVar(
1649            self.__model,
1650            self.__var_list,
1651            start_expr,
1652            size_expr,
1653            end_expr,
1654            None,
1655            name,
1656        )
1657
1658    def new_interval_var_series(
1659        self,
1660        name: str,
1661        index: pd.Index,
1662        starts: Union[LinearExprT, pd.Series],
1663        sizes: Union[LinearExprT, pd.Series],
1664        ends: Union[LinearExprT, pd.Series],
1665    ) -> pd.Series:
1666        """Creates a series of interval variables with the given name.
1667
1668        Args:
1669          name (str): Required. The name of the variable set.
1670          index (pd.Index): Required. The index to use for the variable set.
1671          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1672            set. If a `pd.Series` is passed in, it will be based on the
1673            corresponding values of the pd.Series.
1674          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1675            set. If a `pd.Series` is passed in, it will be based on the
1676            corresponding values of the pd.Series.
1677          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1678            set. If a `pd.Series` is passed in, it will be based on the
1679            corresponding values of the pd.Series.
1680
1681        Returns:
1682          pd.Series: The interval variable set indexed by its corresponding
1683          dimensions.
1684
1685        Raises:
1686          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1687          ValueError: if the `name` is not a valid identifier or already exists.
1688          ValueError: if the all the indexes do not match.
1689        """
1690        if not isinstance(index, pd.Index):
1691            raise TypeError("Non-index object is used as index")
1692        if not name.isidentifier():
1693            raise ValueError(f"name={name!r} is not a valid identifier")
1694
1695        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1696        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
1697        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
1698        interval_array = []
1699        for i in index:
1700            interval_array.append(
1701                self.new_interval_var(
1702                    start=starts[i],
1703                    size=sizes[i],
1704                    end=ends[i],
1705                    name=f"{name}[{i}]",
1706                )
1707            )
1708        return pd.Series(index=index, data=interval_array)
1709
1710    def new_fixed_size_interval_var(
1711        self, start: LinearExprT, size: IntegralT, name: str
1712    ) -> IntervalVar:
1713        """Creates an interval variable from start, and a fixed size.
1714
1715        An interval variable is a constraint, that is itself used in other
1716        constraints like NoOverlap.
1717
1718        Args:
1719          start: The start of the interval. It must be of the form a * var + b.
1720          size: The size of the interval. It must be an integer value.
1721          name: The name of the interval variable.
1722
1723        Returns:
1724          An `IntervalVar` object.
1725        """
1726        start_expr = self.parse_linear_expression(start)
1727        size_expr = self.parse_linear_expression(size)
1728        end_expr = self.parse_linear_expression(start + size)
1729        if len(start_expr.vars) > 1:
1730            raise TypeError(
1731                "cp_model.new_interval_var: start must be affine or constant."
1732            )
1733        return IntervalVar(
1734            self.__model,
1735            self.__var_list,
1736            start_expr,
1737            size_expr,
1738            end_expr,
1739            None,
1740            name,
1741        )
1742
1743    def new_fixed_size_interval_var_series(
1744        self,
1745        name: str,
1746        index: pd.Index,
1747        starts: Union[LinearExprT, pd.Series],
1748        sizes: Union[IntegralT, pd.Series],
1749    ) -> pd.Series:
1750        """Creates a series of interval variables with the given name.
1751
1752        Args:
1753          name (str): Required. The name of the variable set.
1754          index (pd.Index): Required. The index to use for the variable set.
1755          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1756            set. If a `pd.Series` is passed in, it will be based on the
1757            corresponding values of the pd.Series.
1758          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1759            the set. If a `pd.Series` is passed in, it will be based on the
1760            corresponding values of the pd.Series.
1761
1762        Returns:
1763          pd.Series: The interval variable set indexed by its corresponding
1764          dimensions.
1765
1766        Raises:
1767          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1768          ValueError: if the `name` is not a valid identifier or already exists.
1769          ValueError: if the all the indexes do not match.
1770        """
1771        if not isinstance(index, pd.Index):
1772            raise TypeError("Non-index object is used as index")
1773        if not name.isidentifier():
1774            raise ValueError(f"name={name!r} is not a valid identifier")
1775
1776        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1777        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
1778        interval_array = []
1779        for i in index:
1780            interval_array.append(
1781                self.new_fixed_size_interval_var(
1782                    start=starts[i],
1783                    size=sizes[i],
1784                    name=f"{name}[{i}]",
1785                )
1786            )
1787        return pd.Series(index=index, data=interval_array)
1788
1789    def new_optional_interval_var(
1790        self,
1791        start: LinearExprT,
1792        size: LinearExprT,
1793        end: LinearExprT,
1794        is_present: LiteralT,
1795        name: str,
1796    ) -> IntervalVar:
1797        """Creates an optional interval var from start, size, end, and is_present.
1798
1799        An optional interval variable is a constraint, that is itself used in other
1800        constraints like NoOverlap. This constraint is protected by a presence
1801        literal that indicates if it is active or not.
1802
1803        Internally, it ensures that `is_present` implies `start + size ==
1804        end`.
1805
1806        Args:
1807          start: The start of the interval. It must be of the form a * var + b.
1808          size: The size of the interval. It must be of the form a * var + b.
1809          end: The end of the interval. It must be of the form a * var + b.
1810          is_present: A literal that indicates if the interval is active or not. A
1811            inactive interval is simply ignored by all constraints.
1812          name: The name of the interval variable.
1813
1814        Returns:
1815          An `IntervalVar` object.
1816        """
1817
1818        # Creates the IntervalConstraintProto object.
1819        is_present_index = self.get_or_make_boolean_index(is_present)
1820        start_expr = self.parse_linear_expression(start)
1821        size_expr = self.parse_linear_expression(size)
1822        end_expr = self.parse_linear_expression(end)
1823        if len(start_expr.vars) > 1:
1824            raise TypeError(
1825                "cp_model.new_interval_var: start must be affine or constant."
1826            )
1827        if len(size_expr.vars) > 1:
1828            raise TypeError(
1829                "cp_model.new_interval_var: size must be affine or constant."
1830            )
1831        if len(end_expr.vars) > 1:
1832            raise TypeError(
1833                "cp_model.new_interval_var: end must be affine or constant."
1834            )
1835        return IntervalVar(
1836            self.__model,
1837            self.__var_list,
1838            start_expr,
1839            size_expr,
1840            end_expr,
1841            is_present_index,
1842            name,
1843        )
1844
1845    def new_optional_interval_var_series(
1846        self,
1847        name: str,
1848        index: pd.Index,
1849        starts: Union[LinearExprT, pd.Series],
1850        sizes: Union[LinearExprT, pd.Series],
1851        ends: Union[LinearExprT, pd.Series],
1852        are_present: Union[LiteralT, pd.Series],
1853    ) -> pd.Series:
1854        """Creates a series of interval variables with the given name.
1855
1856        Args:
1857          name (str): Required. The name of the variable set.
1858          index (pd.Index): Required. The index to use for the variable set.
1859          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1860            set. If a `pd.Series` is passed in, it will be based on the
1861            corresponding values of the pd.Series.
1862          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1863            set. If a `pd.Series` is passed in, it will be based on the
1864            corresponding values of the pd.Series.
1865          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1866            set. If a `pd.Series` is passed in, it will be based on the
1867            corresponding values of the pd.Series.
1868          are_present (Union[LiteralT, pd.Series]): The performed literal of each
1869            interval in the set. If a `pd.Series` is passed in, it will be based on
1870            the corresponding values of the pd.Series.
1871
1872        Returns:
1873          pd.Series: The interval variable set indexed by its corresponding
1874          dimensions.
1875
1876        Raises:
1877          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1878          ValueError: if the `name` is not a valid identifier or already exists.
1879          ValueError: if the all the indexes do not match.
1880        """
1881        if not isinstance(index, pd.Index):
1882            raise TypeError("Non-index object is used as index")
1883        if not name.isidentifier():
1884            raise ValueError(f"name={name!r} is not a valid identifier")
1885
1886        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1887        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
1888        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
1889        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
1890
1891        interval_array = []
1892        for i in index:
1893            interval_array.append(
1894                self.new_optional_interval_var(
1895                    start=starts[i],
1896                    size=sizes[i],
1897                    end=ends[i],
1898                    is_present=are_present[i],
1899                    name=f"{name}[{i}]",
1900                )
1901            )
1902        return pd.Series(index=index, data=interval_array)
1903
1904    def new_optional_fixed_size_interval_var(
1905        self,
1906        start: LinearExprT,
1907        size: IntegralT,
1908        is_present: LiteralT,
1909        name: str,
1910    ) -> IntervalVar:
1911        """Creates an interval variable from start, and a fixed size.
1912
1913        An interval variable is a constraint, that is itself used in other
1914        constraints like NoOverlap.
1915
1916        Args:
1917          start: The start of the interval. It must be of the form a * var + b.
1918          size: The size of the interval. It must be an integer value.
1919          is_present: A literal that indicates if the interval is active or not. A
1920            inactive interval is simply ignored by all constraints.
1921          name: The name of the interval variable.
1922
1923        Returns:
1924          An `IntervalVar` object.
1925        """
1926        start_expr = self.parse_linear_expression(start)
1927        size_expr = self.parse_linear_expression(size)
1928        end_expr = self.parse_linear_expression(start + size)
1929        if len(start_expr.vars) > 1:
1930            raise TypeError(
1931                "cp_model.new_interval_var: start must be affine or constant."
1932            )
1933        is_present_index = self.get_or_make_boolean_index(is_present)
1934        return IntervalVar(
1935            self.__model,
1936            self.__var_list,
1937            start_expr,
1938            size_expr,
1939            end_expr,
1940            is_present_index,
1941            name,
1942        )
1943
1944    def new_optional_fixed_size_interval_var_series(
1945        self,
1946        name: str,
1947        index: pd.Index,
1948        starts: Union[LinearExprT, pd.Series],
1949        sizes: Union[IntegralT, pd.Series],
1950        are_present: Union[LiteralT, pd.Series],
1951    ) -> pd.Series:
1952        """Creates a series of interval variables with the given name.
1953
1954        Args:
1955          name (str): Required. The name of the variable set.
1956          index (pd.Index): Required. The index to use for the variable set.
1957          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1958            set. If a `pd.Series` is passed in, it will be based on the
1959            corresponding values of the pd.Series.
1960          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1961            the set. If a `pd.Series` is passed in, it will be based on the
1962            corresponding values of the pd.Series.
1963          are_present (Union[LiteralT, pd.Series]): The performed literal of each
1964            interval in the set. If a `pd.Series` is passed in, it will be based on
1965            the corresponding values of the pd.Series.
1966
1967        Returns:
1968          pd.Series: The interval variable set indexed by its corresponding
1969          dimensions.
1970
1971        Raises:
1972          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1973          ValueError: if the `name` is not a valid identifier or already exists.
1974          ValueError: if the all the indexes do not match.
1975        """
1976        if not isinstance(index, pd.Index):
1977            raise TypeError("Non-index object is used as index")
1978        if not name.isidentifier():
1979            raise ValueError(f"name={name!r} is not a valid identifier")
1980
1981        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1982        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
1983        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
1984        interval_array = []
1985        for i in index:
1986            interval_array.append(
1987                self.new_optional_fixed_size_interval_var(
1988                    start=starts[i],
1989                    size=sizes[i],
1990                    is_present=are_present[i],
1991                    name=f"{name}[{i}]",
1992                )
1993            )
1994        return pd.Series(index=index, data=interval_array)
1995
1996    def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint:
1997        """Adds NoOverlap(interval_vars).
1998
1999        A NoOverlap constraint ensures that all present intervals do not overlap
2000        in time.
2001
2002        Args:
2003          interval_vars: The list of interval variables to constrain.
2004
2005        Returns:
2006          An instance of the `Constraint` class.
2007        """
2008        ct = Constraint(self)
2009        model_ct = self.__model.constraints[ct.index]
2010        model_ct.no_overlap.intervals.extend(
2011            [self.get_interval_index(x) for x in interval_vars]
2012        )
2013        return ct
2014
2015    def add_no_overlap_2d(
2016        self,
2017        x_intervals: Iterable[IntervalVar],
2018        y_intervals: Iterable[IntervalVar],
2019    ) -> Constraint:
2020        """Adds NoOverlap2D(x_intervals, y_intervals).
2021
2022        A NoOverlap2D constraint ensures that all present rectangles do not overlap
2023        on a plane. Each rectangle is aligned with the X and Y axis, and is defined
2024        by two intervals which represent its projection onto the X and Y axis.
2025
2026        Furthermore, one box is optional if at least one of the x or y interval is
2027        optional.
2028
2029        Args:
2030          x_intervals: The X coordinates of the rectangles.
2031          y_intervals: The Y coordinates of the rectangles.
2032
2033        Returns:
2034          An instance of the `Constraint` class.
2035        """
2036        ct = Constraint(self)
2037        model_ct = self.__model.constraints[ct.index]
2038        model_ct.no_overlap_2d.x_intervals.extend(
2039            [self.get_interval_index(x) for x in x_intervals]
2040        )
2041        model_ct.no_overlap_2d.y_intervals.extend(
2042            [self.get_interval_index(x) for x in y_intervals]
2043        )
2044        return ct
2045
2046    def add_cumulative(
2047        self,
2048        intervals: Iterable[IntervalVar],
2049        demands: Iterable[LinearExprT],
2050        capacity: LinearExprT,
2051    ) -> Constraint:
2052        """Adds Cumulative(intervals, demands, capacity).
2053
2054        This constraint enforces that:
2055
2056            for all t:
2057              sum(demands[i]
2058                if (start(intervals[i]) <= t < end(intervals[i])) and
2059                (intervals[i] is present)) <= capacity
2060
2061        Args:
2062          intervals: The list of intervals.
2063          demands: The list of demands for each interval. Each demand must be >= 0.
2064            Each demand can be a 1-var affine expression (a * x + b).
2065          capacity: The maximum capacity of the cumulative constraint. It can be a
2066            1-var affine expression (a * x + b).
2067
2068        Returns:
2069          An instance of the `Constraint` class.
2070        """
2071        cumulative = Constraint(self)
2072        model_ct = self.__model.constraints[cumulative.index]
2073        model_ct.cumulative.intervals.extend(
2074            [self.get_interval_index(x) for x in intervals]
2075        )
2076        for d in demands:
2077            model_ct.cumulative.demands.append(self.parse_linear_expression(d))
2078        model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity))
2079        return cumulative
2080
2081    # Support for model cloning.
2082    def clone(self) -> "CpModel":
2083        """Reset the model, and creates a new one from a CpModelProto instance."""
2084        clone = CpModel()
2085        clone.proto.CopyFrom(self.proto)
2086        clone.rebuild_var_and_constant_map()
2087        return clone
2088
2089    def rebuild_var_and_constant_map(self):
2090        """Internal method used during model cloning."""
2091        for i, var in enumerate(self.__model.variables):
2092            if len(var.domain) == 2 and var.domain[0] == var.domain[1]:
2093                self.__constant_map[var.domain[0]] = i
2094            is_boolean = (
2095                len(var.domain) == 2 and var.domain[0] >= 0 and var.domain[1] <= 1
2096            )
2097            self.__var_list.append(IntVar(self.__model, i, is_boolean, None))
2098
2099    def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2100        """Returns an already created Boolean variable from its index."""
2101        result = self._get_int_var(index)
2102        if not result.is_boolean:
2103            raise ValueError(
2104                f"get_bool_var_from_proto_index: index {index} does not reference a"
2105                " boolean variable"
2106            )
2107        return result
2108
2109    def get_int_var_from_proto_index(self, index: int) -> IntVar:
2110        """Returns an already created integer variable from its index."""
2111        return self._get_int_var(index)
2112
2113    def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2114        """Returns an already created interval variable from its index."""
2115        if index < 0 or index >= len(self.__model.constraints):
2116            raise ValueError(
2117                f"get_interval_var_from_proto_index: out of bound index {index}"
2118            )
2119        ct = self.__model.constraints[index]
2120        if not ct.HasField("interval"):
2121            raise ValueError(
2122                f"get_interval_var_from_proto_index: index {index} does not"
2123                " reference an" + " interval variable"
2124            )
2125
2126        return IntervalVar(self.__model, self.__var_list, index, None, None, None, None)
2127
2128    # Helpers.
2129
2130    def __str__(self) -> str:
2131        return str(self.__model)
2132
2133    @property
2134    def proto(self) -> cp_model_pb2.CpModelProto:
2135        """Returns the underlying CpModelProto."""
2136        return self.__model
2137
2138    def negated(self, index: int) -> int:
2139        return -index - 1
2140
2141    def get_or_make_index(self, arg: VariableT) -> int:
2142        """Returns the index of a variable, its negation, or a number."""
2143        if isinstance(arg, IntVar):
2144            return arg.index
2145        if isinstance(arg, IntegralTypes):
2146            return self.get_or_make_index_from_constant(arg)
2147        raise TypeError(
2148            f"NotSupported: model.get_or_make_index({type(arg).__name__!r})"
2149        )
2150
2151    def get_or_make_boolean_index(self, arg: LiteralT) -> int:
2152        """Returns an index from a boolean expression."""
2153        if isinstance(arg, IntVar):
2154            self.assert_is_boolean_variable(arg)
2155            return arg.index
2156        if isinstance(arg, cmh.NotBooleanVariable):
2157            self.assert_is_boolean_variable(arg.negated())
2158            return arg.index
2159        if isinstance(arg, IntegralTypes):
2160            if arg == ~int(False):
2161                return self.get_or_make_index_from_constant(1)
2162            if arg == ~int(True):
2163                return self.get_or_make_index_from_constant(0)
2164            arg = cmn.assert_is_zero_or_one(arg)
2165            return self.get_or_make_index_from_constant(arg)
2166        if cmn.is_boolean(arg):
2167            return self.get_or_make_index_from_constant(int(arg))
2168        raise TypeError(
2169            "not supported:" f" model.get_or_make_boolean_index({type(arg).__name__!r})"
2170        )
2171
2172    def get_interval_index(self, arg: IntervalVar) -> int:
2173        if not isinstance(arg, IntervalVar):
2174            raise TypeError(
2175                f"NotSupported: model.get_interval_index({type(arg).__name__!r})"
2176            )
2177        return arg.index
2178
2179    def get_or_make_index_from_constant(self, value: IntegralT) -> int:
2180        if value in self.__constant_map:
2181            return self.__constant_map[value]
2182        constant_var = self.new_int_var(value, value, "")
2183        self.__constant_map[value] = constant_var.index
2184        return constant_var.index
2185
2186    def parse_linear_expression(
2187        self, linear_expr: LinearExprT, negate: bool = False
2188    ) -> cp_model_pb2.LinearExpressionProto:
2189        """Returns a LinearExpressionProto built from a LinearExpr instance."""
2190        result: cp_model_pb2.LinearExpressionProto = (
2191            cp_model_pb2.LinearExpressionProto()
2192        )
2193        mult = -1 if negate else 1
2194        if isinstance(linear_expr, IntegralTypes):
2195            result.offset = int(linear_expr) * mult
2196            return result
2197
2198        # Raises TypeError if linear_expr is not an integer.
2199        flat_expr = cmh.FlatIntExpr(linear_expr)
2200        result.offset = flat_expr.offset * mult
2201        for var in flat_expr.vars:
2202            result.vars.append(var.index)
2203        for coeff in flat_expr.coeffs:
2204            result.coeffs.append(coeff * mult)
2205        return result
2206
2207    def _set_objective(self, obj: ObjLinearExprT, minimize: bool):
2208        """Sets the objective of the model."""
2209        self.clear_objective()
2210        if isinstance(obj, IntegralTypes):
2211            self.__model.objective.offset = int(obj)
2212            self.__model.objective.scaling_factor = 1.0
2213        elif isinstance(obj, LinearExpr):
2214            if obj.is_integer():
2215                int_obj = cmh.FlatIntExpr(obj)
2216                for var in int_obj.vars:
2217                    self.__model.objective.vars.append(var.index)
2218                if minimize:
2219                    self.__model.objective.scaling_factor = 1.0
2220                    self.__model.objective.offset = int_obj.offset
2221                    self.__model.objective.coeffs.extend(int_obj.coeffs)
2222                else:
2223                    self.__model.objective.scaling_factor = -1.0
2224                    self.__model.objective.offset = -int_obj.offset
2225                    for c in int_obj.coeffs:
2226                        self.__model.objective.coeffs.append(-c)
2227            else:
2228                float_obj = cmh.FlatFloatExpr(obj)
2229                for var in float_obj.vars:
2230                    self.__model.floating_point_objective.vars.append(var.index)
2231                self.__model.floating_point_objective.coeffs.extend(float_obj.coeffs)
2232                self.__model.floating_point_objective.maximize = not minimize
2233                self.__model.floating_point_objective.offset = float_obj.offset
2234        else:
2235            raise TypeError(
2236                f"TypeError: {type(obj).__name__!r} is not a valid objective"
2237            )
2238
2239    def minimize(self, obj: ObjLinearExprT):
2240        """Sets the objective of the model to minimize(obj)."""
2241        self._set_objective(obj, minimize=True)
2242
2243    def maximize(self, obj: ObjLinearExprT):
2244        """Sets the objective of the model to maximize(obj)."""
2245        self._set_objective(obj, minimize=False)
2246
2247    def has_objective(self) -> bool:
2248        return self.__model.HasField("objective") or self.__model.HasField(
2249            "floating_point_objective"
2250        )
2251
2252    def clear_objective(self):
2253        self.__model.ClearField("objective")
2254        self.__model.ClearField("floating_point_objective")
2255
2256    def add_decision_strategy(
2257        self,
2258        variables: Sequence[IntVar],
2259        var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy,
2260        domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy,
2261    ) -> None:
2262        """Adds a search strategy to the model.
2263
2264        Args:
2265          variables: a list of variables this strategy will assign.
2266          var_strategy: heuristic to choose the next variable to assign.
2267          domain_strategy: heuristic to reduce the domain of the selected variable.
2268            Currently, this is advanced code: the union of all strategies added to
2269            the model must be complete, i.e. instantiates all variables. Otherwise,
2270            solve() will fail.
2271        """
2272
2273        strategy: cp_model_pb2.DecisionStrategyProto = (
2274            self.__model.search_strategy.add()
2275        )
2276        for v in variables:
2277            expr = strategy.exprs.add()
2278            if v.index >= 0:
2279                expr.vars.append(v.index)
2280                expr.coeffs.append(1)
2281            else:
2282                expr.vars.append(self.negated(v.index))
2283                expr.coeffs.append(-1)
2284                expr.offset = 1
2285
2286        strategy.variable_selection_strategy = var_strategy
2287        strategy.domain_reduction_strategy = domain_strategy
2288
2289    def model_stats(self) -> str:
2290        """Returns a string containing some model statistics."""
2291        return cmh.CpSatHelper.model_stats(self.__model)
2292
2293    def validate(self) -> str:
2294        """Returns a string indicating that the model is invalid."""
2295        return cmh.CpSatHelper.validate_model(self.__model)
2296
2297    def export_to_file(self, file: str) -> bool:
2298        """Write the model as a protocol buffer to 'file'.
2299
2300        Args:
2301          file: file to write the model to. If the filename ends with 'txt', the
2302            model will be written as a text file, otherwise, the binary format will
2303            be used.
2304
2305        Returns:
2306          True if the model was correctly written.
2307        """
2308        return cmh.CpSatHelper.write_model_to_file(self.__model, file)
2309
2310    def remove_all_names(self) -> None:
2311        """Removes all names from the model."""
2312        self.__model.ClearField("name")
2313        for v in self.__model.variables:
2314            v.ClearField("name")
2315        for c in self.__model.constraints:
2316            c.ClearField("name")
2317
2318    @overload
2319    def add_hint(self, var: IntVar, value: int) -> None: ...
2320
2321    @overload
2322    def add_hint(self, literal: BoolVarT, value: bool) -> None: ...
2323
2324    def add_hint(self, var, value) -> None:
2325        """Adds 'var == value' as a hint to the solver."""
2326        if var.index >= 0:
2327            self.__model.solution_hint.vars.append(self.get_or_make_index(var))
2328            self.__model.solution_hint.values.append(int(value))
2329        else:
2330            self.__model.solution_hint.vars.append(self.negated(var.index))
2331            self.__model.solution_hint.values.append(int(not value))
2332
2333    def clear_hints(self):
2334        """Removes any solution hint from the model."""
2335        self.__model.ClearField("solution_hint")
2336
2337    def add_assumption(self, lit: LiteralT) -> None:
2338        """Adds the literal to the model as assumptions."""
2339        self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
2340
2341    def add_assumptions(self, literals: Iterable[LiteralT]) -> None:
2342        """Adds the literals to the model as assumptions."""
2343        for lit in literals:
2344            self.add_assumption(lit)
2345
2346    def clear_assumptions(self) -> None:
2347        """Removes all assumptions from the model."""
2348        self.__model.ClearField("assumptions")
2349
2350    # Helpers.
2351    def assert_is_boolean_variable(self, x: LiteralT) -> None:
2352        if isinstance(x, IntVar):
2353            var = self.__model.variables[x.index]
2354            if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1:
2355                raise TypeError(
2356                    f"TypeError: {type(x).__name__!r} is not a boolean variable"
2357                )
2358        elif not isinstance(x, cmh.NotBooleanVariable):
2359            raise TypeError(
2360                f"TypeError: {type(x).__name__!r}  is not a boolean variable"
2361            )
2362
2363    # Compatibility with pre PEP8
2364    # pylint: disable=invalid-name
2365
2366    def Name(self) -> str:
2367        return self.name
2368
2369    def SetName(self, name: str) -> None:
2370        self.name = name
2371
2372    def Proto(self) -> cp_model_pb2.CpModelProto:
2373        return self.proto
2374
2375    NewIntVar = new_int_var
2376    NewIntVarFromDomain = new_int_var_from_domain
2377    NewBoolVar = new_bool_var
2378    NewConstant = new_constant
2379    NewIntVarSeries = new_int_var_series
2380    NewBoolVarSeries = new_bool_var_series
2381    AddLinearConstraint = add_linear_constraint
2382    AddLinearExpressionInDomain = add_linear_expression_in_domain
2383    Add = add
2384    AddAllDifferent = add_all_different
2385    AddElement = add_element
2386    AddCircuit = add_circuit
2387    AddMultipleCircuit = add_multiple_circuit
2388    AddAllowedAssignments = add_allowed_assignments
2389    AddForbiddenAssignments = add_forbidden_assignments
2390    AddAutomaton = add_automaton
2391    AddInverse = add_inverse
2392    AddReservoirConstraint = add_reservoir_constraint
2393    AddReservoirConstraintWithActive = add_reservoir_constraint_with_active
2394    AddImplication = add_implication
2395    AddBoolOr = add_bool_or
2396    AddAtLeastOne = add_at_least_one
2397    AddAtMostOne = add_at_most_one
2398    AddExactlyOne = add_exactly_one
2399    AddBoolAnd = add_bool_and
2400    AddBoolXOr = add_bool_xor
2401    AddMinEquality = add_min_equality
2402    AddMaxEquality = add_max_equality
2403    AddDivisionEquality = add_division_equality
2404    AddAbsEquality = add_abs_equality
2405    AddModuloEquality = add_modulo_equality
2406    AddMultiplicationEquality = add_multiplication_equality
2407    NewIntervalVar = new_interval_var
2408    NewIntervalVarSeries = new_interval_var_series
2409    NewFixedSizeIntervalVar = new_fixed_size_interval_var
2410    NewOptionalIntervalVar = new_optional_interval_var
2411    NewOptionalIntervalVarSeries = new_optional_interval_var_series
2412    NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var
2413    NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series
2414    AddNoOverlap = add_no_overlap
2415    AddNoOverlap2D = add_no_overlap_2d
2416    AddCumulative = add_cumulative
2417    Clone = clone
2418    GetBoolVarFromProtoIndex = get_bool_var_from_proto_index
2419    GetIntVarFromProtoIndex = get_int_var_from_proto_index
2420    GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index
2421    Minimize = minimize
2422    Maximize = maximize
2423    HasObjective = has_objective
2424    ClearObjective = clear_objective
2425    AddDecisionStrategy = add_decision_strategy
2426    ModelStats = model_stats
2427    Validate = validate
2428    ExportToFile = export_to_file
2429    AddHint = add_hint
2430    ClearHints = clear_hints
2431    AddAssumption = add_assumption
2432    AddAssumptions = add_assumptions
2433    ClearAssumptions = clear_assumptions
2434
2435    # pylint: enable=invalid-name

Methods for building a CP model.

Methods beginning with:

  • New create integer, boolean, or interval variables.
  • add create new constraints and add them to the model.
name: str
656    @property
657    def name(self) -> str:
658        """Returns the name of the model."""
659        if not self.__model or not self.__model.name:
660            return ""
661        return self.__model.name

Returns the name of the model.

def rebuild_from_linear_expression_proto( self, proto: ortools.sat.cp_model_pb2.LinearExpressionProto) -> LinearExpr:
678    def rebuild_from_linear_expression_proto(
679        self,
680        proto: cp_model_pb2.LinearExpressionProto,
681    ) -> LinearExpr:
682        return self.__var_list.rebuild_expr(proto)
def new_int_var( self, lb: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], ub: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], name: str) -> IntVar:
684    def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar:
685        """Create an integer variable with domain [lb, ub].
686
687        The CP-SAT solver is limited to integer variables. If you have fractional
688        values, scale them up so that they become integers; if you have strings,
689        encode them as integers.
690
691        Args:
692          lb: Lower bound for the variable.
693          ub: Upper bound for the variable.
694          name: The name of the variable.
695
696        Returns:
697          a variable whose domain is [lb, ub].
698        """
699        domain_is_boolean = lb >= 0 and ub <= 1
700        return self._append_int_var(
701            IntVar(
702                self.__model,
703                sorted_interval_list.Domain(lb, ub),
704                domain_is_boolean,
705                name,
706            )
707        )

Create an integer variable with domain [lb, ub].

The CP-SAT solver is limited to integer variables. If you have fractional values, scale them up so that they become integers; if you have strings, encode them as integers.

Arguments:
  • lb: Lower bound for the variable.
  • ub: Upper bound for the variable.
  • name: The name of the variable.
Returns:

a variable whose domain is [lb, ub].

def new_int_var_from_domain( self, domain: Domain, name: str) -> IntVar:
709    def new_int_var_from_domain(
710        self, domain: sorted_interval_list.Domain, name: str
711    ) -> IntVar:
712        """Create an integer variable from a domain.
713
714        A domain is a set of integers specified by a collection of intervals.
715        For example, `model.new_int_var_from_domain(cp_model.
716             Domain.from_intervals([[1, 2], [4, 6]]), 'x')`
717
718        Args:
719          domain: An instance of the Domain class.
720          name: The name of the variable.
721
722        Returns:
723            a variable whose domain is the given domain.
724        """
725        domain_is_boolean = domain.min() >= 0 and domain.max() <= 1
726        return self._append_int_var(
727            IntVar(self.__model, domain, domain_is_boolean, name)
728        )

Create an integer variable from a domain.

A domain is a set of integers specified by a collection of intervals. For example, model.new_int_var_from_domain(cp_model. Domain.from_intervals([[1, 2], [4, 6]]), 'x')

Arguments:
  • domain: An instance of the Domain class.
  • name: The name of the variable.
Returns:

a variable whose domain is the given domain.

def new_bool_var(self, name: str) -> IntVar:
730    def new_bool_var(self, name: str) -> IntVar:
731        """Creates a 0-1 variable with the given name."""
732        return self._append_int_var(
733            IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name)
734        )

Creates a 0-1 variable with the given name.

def new_constant( self, value: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> IntVar:
736    def new_constant(self, value: IntegralT) -> IntVar:
737        """Declares a constant integer."""
738        index: int = self.get_or_make_index_from_constant(value)
739        return self._get_int_var(index)

Declares a constant integer.

def new_int_var_series( self, name: str, index: pandas.core.indexes.base.Index, lower_bounds: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], upper_bounds: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series]) -> pandas.core.series.Series:
741    def new_int_var_series(
742        self,
743        name: str,
744        index: pd.Index,
745        lower_bounds: Union[IntegralT, pd.Series],
746        upper_bounds: Union[IntegralT, pd.Series],
747    ) -> pd.Series:
748        """Creates a series of (scalar-valued) variables with the given name.
749
750        Args:
751          name (str): Required. The name of the variable set.
752          index (pd.Index): Required. The index to use for the variable set.
753          lower_bounds (Union[int, pd.Series]): A lower bound for variables in the
754            set. If a `pd.Series` is passed in, it will be based on the
755            corresponding values of the pd.Series.
756          upper_bounds (Union[int, pd.Series]): An upper bound for variables in the
757            set. If a `pd.Series` is passed in, it will be based on the
758            corresponding values of the pd.Series.
759
760        Returns:
761          pd.Series: The variable set indexed by its corresponding dimensions.
762
763        Raises:
764          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
765          ValueError: if the `name` is not a valid identifier or already exists.
766          ValueError: if the `lowerbound` is greater than the `upperbound`.
767          ValueError: if the index of `lower_bound`, or `upper_bound` does not match
768          the input index.
769        """
770        if not isinstance(index, pd.Index):
771            raise TypeError("Non-index object is used as index")
772        if not name.isidentifier():
773            raise ValueError(f"name={name!r} is not a valid identifier")
774        if (
775            isinstance(lower_bounds, IntegralTypes)
776            and isinstance(upper_bounds, IntegralTypes)
777            and lower_bounds > upper_bounds
778        ):
779            raise ValueError(
780                f"lower_bound={lower_bounds} is greater than"
781                f" upper_bound={upper_bounds} for variable set={name}"
782            )
783
784        lower_bounds = _convert_to_integral_series_and_validate_index(
785            lower_bounds, index
786        )
787        upper_bounds = _convert_to_integral_series_and_validate_index(
788            upper_bounds, index
789        )
790        return pd.Series(
791            index=index,
792            data=[
793                # pylint: disable=g-complex-comprehension
794                self._append_int_var(
795                    IntVar(
796                        model=self.__model,
797                        name=f"{name}[{i}]",
798                        domain=sorted_interval_list.Domain(
799                            lower_bounds[i], upper_bounds[i]
800                        ),
801                        is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1,
802                    )
803                )
804                for i in index
805            ],
806        )

Creates a series of (scalar-valued) variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • lower_bounds (Union[int, pd.Series]): A lower bound for variables in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • upper_bounds (Union[int, pd.Series]): An upper bound for variables in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the lowerbound is greater than the upperbound.
  • ValueError: if the index of lower_bound, or upper_bound does not match
  • the input index.
def new_bool_var_series( self, name: str, index: pandas.core.indexes.base.Index) -> pandas.core.series.Series:
808    def new_bool_var_series(
809        self,
810        name: str,
811        index: pd.Index,
812    ) -> pd.Series:
813        """Creates a series of (scalar-valued) variables with the given name.
814
815        Args:
816          name (str): Required. The name of the variable set.
817          index (pd.Index): Required. The index to use for the variable set.
818
819        Returns:
820          pd.Series: The variable set indexed by its corresponding dimensions.
821
822        Raises:
823          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
824          ValueError: if the `name` is not a valid identifier or already exists.
825        """
826        if not isinstance(index, pd.Index):
827            raise TypeError("Non-index object is used as index")
828        if not name.isidentifier():
829            raise ValueError(f"name={name!r} is not a valid identifier")
830        return pd.Series(
831            index=index,
832            data=[
833                # pylint: disable=g-complex-comprehension
834                self._append_int_var(
835                    IntVar(
836                        model=self.__model,
837                        name=f"{name}[{i}]",
838                        domain=sorted_interval_list.Domain(0, 1),
839                        is_boolean=True,
840                    )
841                )
842                for i in index
843            ],
844        )

Creates a series of (scalar-valued) variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
Returns:

pd.Series: The variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
def add_linear_constraint( self, linear_expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], lb: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], ub: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
848    def add_linear_constraint(
849        self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT
850    ) -> Constraint:
851        """Adds the constraint: `lb <= linear_expr <= ub`."""
852        return self.add_linear_expression_in_domain(
853            linear_expr, sorted_interval_list.Domain(lb, ub)
854        )

Adds the constraint: lb <= linear_expr <= ub.

def add_linear_expression_in_domain( self, linear_expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], domain: Domain) -> Constraint:
856    def add_linear_expression_in_domain(
857        self,
858        linear_expr: LinearExprT,
859        domain: sorted_interval_list.Domain,
860    ) -> Constraint:
861        """Adds the constraint: `linear_expr` in `domain`."""
862        if isinstance(linear_expr, LinearExpr):
863            ble = BoundedLinearExpression(linear_expr, domain)
864            if not ble.ok:
865                raise TypeError(
866                    "Cannot add a linear expression containing floating point"
867                    f" coefficients or constants: {type(linear_expr).__name__!r}"
868                )
869            return self.add(ble)
870        if isinstance(linear_expr, IntegralTypes):
871            if not domain.contains(int(linear_expr)):
872                return self.add_bool_or([])  # Evaluate to false.
873            else:
874                return self.add_bool_and([])  # Evaluate to true.
875        raise TypeError(
876            "not supported:"
877            f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})"
878        )

Adds the constraint: linear_expr in domain.

def add( self, ct: Union[BoundedLinearExpression, bool, numpy.bool]) -> Constraint:
880    def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint:
881        """Adds a `BoundedLinearExpression` to the model.
882
883        Args:
884          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
885
886        Returns:
887          An instance of the `Constraint` class.
888
889        Raises:
890          TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean.
891        """
892        if isinstance(ct, BoundedLinearExpression):
893            result = Constraint(self)
894            model_ct = self.__model.constraints[result.index]
895            for var in ct.vars:
896                model_ct.linear.vars.append(var.index)
897            model_ct.linear.coeffs.extend(ct.coeffs)
898            model_ct.linear.domain.extend(
899                [
900                    cmn.capped_subtraction(x, ct.offset)
901                    for x in ct.bounds.flattened_intervals()
902                ]
903            )
904            return result
905        if ct and cmn.is_boolean(ct):
906            return self.add_bool_or([True])
907        if not ct and cmn.is_boolean(ct):
908            return self.add_bool_or([])  # Evaluate to false.
909        raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})")

Adds a BoundedLinearExpression to the model.

Arguments:
Returns:

An instance of the Constraint class.

Raises:
def add_all_different(self, *expressions):
919    def add_all_different(self, *expressions):
920        """Adds AllDifferent(expressions).
921
922        This constraint forces all expressions to have different values.
923
924        Args:
925          *expressions: simple expressions of the form a * var + constant.
926
927        Returns:
928          An instance of the `Constraint` class.
929        """
930        ct = Constraint(self)
931        model_ct = self.__model.constraints[ct.index]
932        expanded = expand_generator_or_tuple(expressions)
933        model_ct.all_diff.exprs.extend(
934            self.parse_linear_expression(x) for x in expanded
935        )
936        return ct

Adds AllDifferent(expressions).

This constraint forces all expressions to have different values.

Arguments:
  • *expressions: simple expressions of the form a * var + constant.
Returns:

An instance of the Constraint class.

def add_element( self, index: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
938    def add_element(
939        self,
940        index: LinearExprT,
941        expressions: Sequence[LinearExprT],
942        target: LinearExprT,
943    ) -> Constraint:
944        """Adds the element constraint: `expressions[index] == target`.
945
946        Args:
947          index: The index of the selected expression in the array. It must be an
948            affine expression (a * var + b).
949          expressions: A list of affine expressions.
950          target: The expression constrained to be equal to the selected expression.
951            It must be an affine expression (a * var + b).
952
953        Returns:
954          An instance of the `Constraint` class.
955        """
956
957        if not expressions:
958            raise ValueError("add_element expects a non-empty expressions array")
959
960        if isinstance(index, IntegralTypes):
961            expression: LinearExprT = list(expressions)[int(index)]
962            return self.add(expression == target)
963
964        ct = Constraint(self)
965        model_ct = self.__model.constraints[ct.index]
966        model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index))
967        model_ct.element.exprs.extend(
968            [self.parse_linear_expression(e) for e in expressions]
969        )
970        model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target))
971        return ct

Adds the element constraint: expressions[index] == target.

Arguments:
  • index: The index of the selected expression in the array. It must be an affine expression (a * var + b).
  • expressions: A list of affine expressions.
  • target: The expression constrained to be equal to the selected expression. It must be an affine expression (a * var + b).
Returns:

An instance of the Constraint class.

def add_circuit( self, arcs: Sequence[Tuple[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]]) -> Constraint:
 973    def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
 974        """Adds Circuit(arcs).
 975
 976        Adds a circuit constraint from a sparse list of arcs that encode the graph.
 977
 978        A circuit is a unique Hamiltonian cycle in a subgraph of the total
 979        graph. In case a node 'i' is not in the cycle, then there must be a
 980        loop arc 'i -> i' associated with a true literal. Otherwise
 981        this constraint will fail.
 982
 983        Args:
 984          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
 985            literal). The arc is selected in the circuit if the literal is true.
 986            Both source_node and destination_node must be integers between 0 and the
 987            number of nodes - 1.
 988
 989        Returns:
 990          An instance of the `Constraint` class.
 991
 992        Raises:
 993          ValueError: If the list of arcs is empty.
 994        """
 995        if not arcs:
 996            raise ValueError("add_circuit expects a non-empty array of arcs")
 997        ct = Constraint(self)
 998        model_ct = self.__model.constraints[ct.index]
 999        for arc in arcs:
1000            model_ct.circuit.tails.append(arc[0])
1001            model_ct.circuit.heads.append(arc[1])
1002            model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2]))
1003        return ct

Adds Circuit(arcs).

Adds a circuit constraint from a sparse list of arcs that encode the graph.

A circuit is a unique Hamiltonian cycle in a subgraph of the total graph. In case a node 'i' is not in the cycle, then there must be a loop arc 'i -> i' associated with a true literal. Otherwise this constraint will fail.

Arguments:
  • arcs: a list of arcs. An arc is a tuple (source_node, destination_node, literal). The arc is selected in the circuit if the literal is true. Both source_node and destination_node must be integers between 0 and the number of nodes - 1.
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: If the list of arcs is empty.
def add_multiple_circuit( self, arcs: Sequence[Tuple[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]]) -> Constraint:
1005    def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1006        """Adds a multiple circuit constraint, aka the 'VRP' constraint.
1007
1008        The direct graph where arc #i (from tails[i] to head[i]) is present iff
1009        literals[i] is true must satisfy this set of properties:
1010        - #incoming arcs == 1 except for node 0.
1011        - #outgoing arcs == 1 except for node 0.
1012        - for node zero, #incoming arcs == #outgoing arcs.
1013        - There are no duplicate arcs.
1014        - Self-arcs are allowed except for node 0.
1015        - There is no cycle in this graph, except through node 0.
1016
1017        Args:
1018          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1019            literal). The arc is selected in the circuit if the literal is true.
1020            Both source_node and destination_node must be integers between 0 and the
1021            number of nodes - 1.
1022
1023        Returns:
1024          An instance of the `Constraint` class.
1025
1026        Raises:
1027          ValueError: If the list of arcs is empty.
1028        """
1029        if not arcs:
1030            raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
1031        ct = Constraint(self)
1032        model_ct = self.__model.constraints[ct.index]
1033        for arc in arcs:
1034            model_ct.routes.tails.append(arc[0])
1035            model_ct.routes.heads.append(arc[1])
1036            model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2]))
1037        return ct

Adds a multiple circuit constraint, aka the 'VRP' constraint.

The direct graph where arc #i (from tails[i] to head[i]) is present iff literals[i] is true must satisfy this set of properties:

  • #incoming arcs == 1 except for node 0.
  • #outgoing arcs == 1 except for node 0.
  • for node zero, #incoming arcs == #outgoing arcs.
  • There are no duplicate arcs.
  • Self-arcs are allowed except for node 0.
  • There is no cycle in this graph, except through node 0.
Arguments:
  • arcs: a list of arcs. An arc is a tuple (source_node, destination_node, literal). The arc is selected in the circuit if the literal is true. Both source_node and destination_node must be integers between 0 and the number of nodes - 1.
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: If the list of arcs is empty.
def add_allowed_assignments( self, expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], tuples_list: Iterable[Sequence[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]]) -> Constraint:
1039    def add_allowed_assignments(
1040        self,
1041        expressions: Sequence[LinearExprT],
1042        tuples_list: Iterable[Sequence[IntegralT]],
1043    ) -> Constraint:
1044        """Adds AllowedAssignments(expressions, tuples_list).
1045
1046        An AllowedAssignments constraint is a constraint on an array of affine
1047        expressions, which requires that when all expressions are assigned values,
1048        the
1049        resulting array equals one of the  tuples in `tuple_list`.
1050
1051        Args:
1052          expressions: A list of affine expressions (a * var + b).
1053          tuples_list: A list of admissible tuples. Each tuple must have the same
1054            length as the expressions, and the ith value of a tuple corresponds to
1055            the ith expression.
1056
1057        Returns:
1058          An instance of the `Constraint` class.
1059
1060        Raises:
1061          TypeError: If a tuple does not have the same size as the list of
1062              expressions.
1063          ValueError: If the array of expressions is empty.
1064        """
1065
1066        if not expressions:
1067            raise ValueError(
1068                "add_allowed_assignments expects a non-empty expressions array"
1069            )
1070
1071        ct: Constraint = Constraint(self)
1072        model_ct = self.__model.constraints[ct.index]
1073        model_ct.table.exprs.extend(
1074            [self.parse_linear_expression(e) for e in expressions]
1075        )
1076        arity: int = len(expressions)
1077        for one_tuple in tuples_list:
1078            if len(one_tuple) != arity:
1079                raise TypeError(f"Tuple {one_tuple!r} has the wrong arity")
1080
1081        # duck-typing (no explicit type checks here)
1082        try:
1083            for one_tuple in tuples_list:
1084                model_ct.table.values.extend(one_tuple)
1085        except ValueError as ex:
1086            raise TypeError(
1087                "add_xxx_assignment: Not an integer or does not fit in an int64_t:"
1088                f" {type(ex.args).__name__!r}"
1089            ) from ex
1090
1091        return ct

Adds AllowedAssignments(expressions, tuples_list).

An AllowedAssignments constraint is a constraint on an array of affine expressions, which requires that when all expressions are assigned values, the resulting array equals one of the tuples in tuple_list.

Arguments:
  • expressions: A list of affine expressions (a * var + b).
  • tuples_list: A list of admissible tuples. Each tuple must have the same length as the expressions, and the ith value of a tuple corresponds to the ith expression.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: If a tuple does not have the same size as the list of expressions.
  • ValueError: If the array of expressions is empty.
def add_forbidden_assignments( self, expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], tuples_list: Iterable[Sequence[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]]) -> Constraint:
1093    def add_forbidden_assignments(
1094        self,
1095        expressions: Sequence[LinearExprT],
1096        tuples_list: Iterable[Sequence[IntegralT]],
1097    ) -> Constraint:
1098        """Adds add_forbidden_assignments(expressions, [tuples_list]).
1099
1100        A ForbiddenAssignments constraint is a constraint on an array of affine
1101        expressions where the list of impossible combinations is provided in the
1102        tuples list.
1103
1104        Args:
1105          expressions: A list of affine expressions (a * var + b).
1106          tuples_list: A list of forbidden tuples. Each tuple must have the same
1107            length as the expressions, and the *i*th value of a tuple corresponds to
1108            the *i*th expression.
1109
1110        Returns:
1111          An instance of the `Constraint` class.
1112
1113        Raises:
1114          TypeError: If a tuple does not have the same size as the list of
1115                     expressions.
1116          ValueError: If the array of expressions is empty.
1117        """
1118
1119        if not expressions:
1120            raise ValueError(
1121                "add_forbidden_assignments expects a non-empty expressions array"
1122            )
1123
1124        index: int = len(self.__model.constraints)
1125        ct: Constraint = self.add_allowed_assignments(expressions, tuples_list)
1126        self.__model.constraints[index].table.negated = True
1127        return ct

Adds add_forbidden_assignments(expressions, [tuples_list]).

A ForbiddenAssignments constraint is a constraint on an array of affine expressions where the list of impossible combinations is provided in the tuples list.

Arguments:
  • expressions: A list of affine expressions (a * var + b).
  • tuples_list: A list of forbidden tuples. Each tuple must have the same length as the expressions, and the ith value of a tuple corresponds to the ith expression.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: If a tuple does not have the same size as the list of expressions.
  • ValueError: If the array of expressions is empty.
def add_automaton( self, transition_expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], starting_state: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], final_states: Sequence[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], transition_triples: Sequence[Tuple[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]]) -> Constraint:
1129    def add_automaton(
1130        self,
1131        transition_expressions: Sequence[LinearExprT],
1132        starting_state: IntegralT,
1133        final_states: Sequence[IntegralT],
1134        transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]],
1135    ) -> Constraint:
1136        """Adds an automaton constraint.
1137
1138        An automaton constraint takes a list of affine expressions (a * var + b) (of
1139        size *n*), an initial state, a set of final states, and a set of
1140        transitions. A transition is a triplet (*tail*, *transition*, *head*), where
1141        *tail* and *head* are states, and *transition* is the label of an arc from
1142        *head* to *tail*, corresponding to the value of one expression in the list
1143        of
1144        expressions.
1145
1146        This automaton will be unrolled into a flow with *n* + 1 phases. Each phase
1147        contains the possible states of the automaton. The first state contains the
1148        initial state. The last phase contains the final states.
1149
1150        Between two consecutive phases *i* and *i* + 1, the automaton creates a set
1151        of arcs. For each transition (*tail*, *transition*, *head*), it will add
1152        an arc from the state *tail* of phase *i* and the state *head* of phase
1153        *i* + 1. This arc is labeled by the value *transition* of the expression
1154        `expressions[i]`. That is, this arc can only be selected if `expressions[i]`
1155        is assigned the value *transition*.
1156
1157        A feasible solution of this constraint is an assignment of expressions such
1158        that, starting from the initial state in phase 0, there is a path labeled by
1159        the values of the expressions that ends in one of the final states in the
1160        final phase.
1161
1162        Args:
1163          transition_expressions: A non-empty list of affine expressions (a * var +
1164            b) whose values correspond to the labels of the arcs traversed by the
1165            automaton.
1166          starting_state: The initial state of the automaton.
1167          final_states: A non-empty list of admissible final states.
1168          transition_triples: A list of transitions for the automaton, in the
1169            following format (current_state, variable_value, next_state).
1170
1171        Returns:
1172          An instance of the `Constraint` class.
1173
1174        Raises:
1175          ValueError: if `transition_expressions`, `final_states`, or
1176            `transition_triples` are empty.
1177        """
1178
1179        if not transition_expressions:
1180            raise ValueError(
1181                "add_automaton expects a non-empty transition_expressions array"
1182            )
1183        if not final_states:
1184            raise ValueError("add_automaton expects some final states")
1185
1186        if not transition_triples:
1187            raise ValueError("add_automaton expects some transition triples")
1188
1189        ct = Constraint(self)
1190        model_ct = self.__model.constraints[ct.index]
1191        model_ct.automaton.exprs.extend(
1192            [self.parse_linear_expression(e) for e in transition_expressions]
1193        )
1194        model_ct.automaton.starting_state = starting_state
1195        for v in final_states:
1196            model_ct.automaton.final_states.append(v)
1197        for t in transition_triples:
1198            if len(t) != 3:
1199                raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)")
1200            model_ct.automaton.transition_tail.append(t[0])
1201            model_ct.automaton.transition_label.append(t[1])
1202            model_ct.automaton.transition_head.append(t[2])
1203        return ct

Adds an automaton constraint.

An automaton constraint takes a list of affine expressions (a * var + b) (of size n), an initial state, a set of final states, and a set of transitions. A transition is a triplet (tail, transition, head), where tail and head are states, and transition is the label of an arc from head to tail, corresponding to the value of one expression in the list of expressions.

This automaton will be unrolled into a flow with n + 1 phases. Each phase contains the possible states of the automaton. The first state contains the initial state. The last phase contains the final states.

Between two consecutive phases i and i + 1, the automaton creates a set of arcs. For each transition (tail, transition, head), it will add an arc from the state tail of phase i and the state head of phase i + 1. This arc is labeled by the value transition of the expression expressions[i]. That is, this arc can only be selected if expressions[i] is assigned the value transition.

A feasible solution of this constraint is an assignment of expressions such that, starting from the initial state in phase 0, there is a path labeled by the values of the expressions that ends in one of the final states in the final phase.

Arguments:
  • transition_expressions: A non-empty list of affine expressions (a * var + b) whose values correspond to the labels of the arcs traversed by the automaton.
  • starting_state: The initial state of the automaton.
  • final_states: A non-empty list of admissible final states.
  • transition_triples: A list of transitions for the automaton, in the following format (current_state, variable_value, next_state).
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: if transition_expressions, final_states, or transition_triples are empty.
def add_inverse( self, variables: Sequence[Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], inverse_variables: Sequence[Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]) -> Constraint:
1205    def add_inverse(
1206        self,
1207        variables: Sequence[VariableT],
1208        inverse_variables: Sequence[VariableT],
1209    ) -> Constraint:
1210        """Adds Inverse(variables, inverse_variables).
1211
1212        An inverse constraint enforces that if `variables[i]` is assigned a value
1213        `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa.
1214
1215        Args:
1216          variables: An array of integer variables.
1217          inverse_variables: An array of integer variables.
1218
1219        Returns:
1220          An instance of the `Constraint` class.
1221
1222        Raises:
1223          TypeError: if variables and inverse_variables have different lengths, or
1224              if they are empty.
1225        """
1226
1227        if not variables or not inverse_variables:
1228            raise TypeError("The Inverse constraint does not accept empty arrays")
1229        if len(variables) != len(inverse_variables):
1230            raise TypeError(
1231                "In the inverse constraint, the two array variables and"
1232                " inverse_variables must have the same length."
1233            )
1234        ct = Constraint(self)
1235        model_ct = self.__model.constraints[ct.index]
1236        model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables])
1237        model_ct.inverse.f_inverse.extend(
1238            [self.get_or_make_index(x) for x in inverse_variables]
1239        )
1240        return ct

Adds Inverse(variables, inverse_variables).

An inverse constraint enforces that if variables[i] is assigned a value j, then inverse_variables[j] is assigned a value i. And vice versa.

Arguments:
  • variables: An array of integer variables.
  • inverse_variables: An array of integer variables.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: if variables and inverse_variables have different lengths, or if they are empty.
def add_reservoir_constraint( self, times: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], level_changes: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], min_level: int, max_level: int) -> Constraint:
1242    def add_reservoir_constraint(
1243        self,
1244        times: Iterable[LinearExprT],
1245        level_changes: Iterable[LinearExprT],
1246        min_level: int,
1247        max_level: int,
1248    ) -> Constraint:
1249        """Adds Reservoir(times, level_changes, min_level, max_level).
1250
1251        Maintains a reservoir level within bounds. The water level starts at 0, and
1252        at any time, it must be between min_level and max_level.
1253
1254        If the affine expression `times[i]` is assigned a value t, then the current
1255        level changes by `level_changes[i]`, which is constant, at time t.
1256
1257         Note that min level must be <= 0, and the max level must be >= 0. Please
1258         use fixed level_changes to simulate initial state.
1259
1260         Therefore, at any time:
1261             sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
1262
1263        Args:
1264          times: A list of 1-var affine expressions (a * x + b) which specify the
1265            time of the filling or emptying the reservoir.
1266          level_changes: A list of integer values that specifies the amount of the
1267            emptying or filling. Currently, variable demands are not supported.
1268          min_level: At any time, the level of the reservoir must be greater or
1269            equal than the min level.
1270          max_level: At any time, the level of the reservoir must be less or equal
1271            than the max level.
1272
1273        Returns:
1274          An instance of the `Constraint` class.
1275
1276        Raises:
1277          ValueError: if max_level < min_level.
1278
1279          ValueError: if max_level < 0.
1280
1281          ValueError: if min_level > 0
1282        """
1283
1284        if max_level < min_level:
1285            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1286
1287        if max_level < 0:
1288            raise ValueError("Reservoir constraint must have a max_level >= 0")
1289
1290        if min_level > 0:
1291            raise ValueError("Reservoir constraint must have a min_level <= 0")
1292
1293        ct = Constraint(self)
1294        model_ct = self.__model.constraints[ct.index]
1295        model_ct.reservoir.time_exprs.extend(
1296            [self.parse_linear_expression(x) for x in times]
1297        )
1298        model_ct.reservoir.level_changes.extend(
1299            [self.parse_linear_expression(x) for x in level_changes]
1300        )
1301        model_ct.reservoir.min_level = min_level
1302        model_ct.reservoir.max_level = max_level
1303        return ct

Adds Reservoir(times, level_changes, min_level, max_level).

Maintains a reservoir level within bounds. The water level starts at 0, and at any time, it must be between min_level and max_level.

If the affine expression times[i] is assigned a value t, then the current level changes by level_changes[i], which is constant, at time t.

Note that min level must be <= 0, and the max level must be >= 0. Please use fixed level_changes to simulate initial state.

Therefore, at any time: sum(level_changes[i] if times[i] <= t) in [min_level, max_level]

Arguments:
  • times: A list of 1-var affine expressions (a * x + b) which specify the time of the filling or emptying the reservoir.
  • level_changes: A list of integer values that specifies the amount of the emptying or filling. Currently, variable demands are not supported.
  • min_level: At any time, the level of the reservoir must be greater or equal than the min level.
  • max_level: At any time, the level of the reservoir must be less or equal than the max level.
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: if max_level < min_level.
  • ValueError: if max_level < 0.
  • ValueError: if min_level > 0
def add_reservoir_constraint_with_active( self, times: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], level_changes: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], actives: Iterable[Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]], min_level: int, max_level: int) -> Constraint:
1305    def add_reservoir_constraint_with_active(
1306        self,
1307        times: Iterable[LinearExprT],
1308        level_changes: Iterable[LinearExprT],
1309        actives: Iterable[LiteralT],
1310        min_level: int,
1311        max_level: int,
1312    ) -> Constraint:
1313        """Adds Reservoir(times, level_changes, actives, min_level, max_level).
1314
1315        Maintains a reservoir level within bounds. The water level starts at 0, and
1316        at any time, it must be between min_level and max_level.
1317
1318        If the variable `times[i]` is assigned a value t, and `actives[i]` is
1319        `True`, then the current level changes by `level_changes[i]`, which is
1320        constant,
1321        at time t.
1322
1323         Note that min level must be <= 0, and the max level must be >= 0. Please
1324         use fixed level_changes to simulate initial state.
1325
1326         Therefore, at any time:
1327             sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level,
1328             max_level]
1329
1330
1331        The array of boolean variables 'actives', if defined, indicates which
1332        actions are actually performed.
1333
1334        Args:
1335          times: A list of 1-var affine expressions (a * x + b) which specify the
1336            time of the filling or emptying the reservoir.
1337          level_changes: A list of integer values that specifies the amount of the
1338            emptying or filling. Currently, variable demands are not supported.
1339          actives: a list of boolean variables. They indicates if the
1340            emptying/refilling events actually take place.
1341          min_level: At any time, the level of the reservoir must be greater or
1342            equal than the min level.
1343          max_level: At any time, the level of the reservoir must be less or equal
1344            than the max level.
1345
1346        Returns:
1347          An instance of the `Constraint` class.
1348
1349        Raises:
1350          ValueError: if max_level < min_level.
1351
1352          ValueError: if max_level < 0.
1353
1354          ValueError: if min_level > 0
1355        """
1356
1357        if max_level < min_level:
1358            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1359
1360        if max_level < 0:
1361            raise ValueError("Reservoir constraint must have a max_level >= 0")
1362
1363        if min_level > 0:
1364            raise ValueError("Reservoir constraint must have a min_level <= 0")
1365
1366        ct = Constraint(self)
1367        model_ct = self.__model.constraints[ct.index]
1368        model_ct.reservoir.time_exprs.extend(
1369            [self.parse_linear_expression(x) for x in times]
1370        )
1371        model_ct.reservoir.level_changes.extend(
1372            [self.parse_linear_expression(x) for x in level_changes]
1373        )
1374        model_ct.reservoir.active_literals.extend(
1375            [self.get_or_make_boolean_index(x) for x in actives]
1376        )
1377        model_ct.reservoir.min_level = min_level
1378        model_ct.reservoir.max_level = max_level
1379        return ct

Adds Reservoir(times, level_changes, actives, min_level, max_level).

Maintains a reservoir level within bounds. The water level starts at 0, and at any time, it must be between min_level and max_level.

If the variable times[i] is assigned a value t, and actives[i] is True, then the current level changes by level_changes[i], which is constant, at time t.

Note that min level must be <= 0, and the max level must be >= 0. Please use fixed level_changes to simulate initial state.

Therefore, at any time: sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, max_level]

The array of boolean variables 'actives', if defined, indicates which actions are actually performed.

Arguments:
  • times: A list of 1-var affine expressions (a * x + b) which specify the time of the filling or emptying the reservoir.
  • level_changes: A list of integer values that specifies the amount of the emptying or filling. Currently, variable demands are not supported.
  • actives: a list of boolean variables. They indicates if the emptying/refilling events actually take place.
  • min_level: At any time, the level of the reservoir must be greater or equal than the min level.
  • max_level: At any time, the level of the reservoir must be less or equal than the max level.
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: if max_level < min_level.
  • ValueError: if max_level < 0.
  • ValueError: if min_level > 0
def add_map_domain( self, var: IntVar, bool_var_array: Iterable[IntVar], offset: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64] = 0):
1381    def add_map_domain(
1382        self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0
1383    ):
1384        """Adds `var == i + offset <=> bool_var_array[i] == true for all i`."""
1385
1386        for i, bool_var in enumerate(bool_var_array):
1387            b_index = bool_var.index
1388            var_index = var.index
1389            model_ct = self.__model.constraints.add()
1390            model_ct.linear.vars.append(var_index)
1391            model_ct.linear.coeffs.append(1)
1392            offset_as_int = int(offset)
1393            model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i])
1394            model_ct.enforcement_literal.append(b_index)
1395
1396            model_ct = self.__model.constraints.add()
1397            model_ct.linear.vars.append(var_index)
1398            model_ct.linear.coeffs.append(1)
1399            model_ct.enforcement_literal.append(-b_index - 1)
1400            if offset + i - 1 >= INT_MIN:
1401                model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1])
1402            if offset + i + 1 <= INT_MAX:
1403                model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX])

Adds var == i + offset <=> bool_var_array[i] == true for all i.

def add_implication( self, a: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], b: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> Constraint:
1405    def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint:
1406        """Adds `a => b` (`a` implies `b`)."""
1407        ct = Constraint(self)
1408        model_ct = self.__model.constraints[ct.index]
1409        model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b))
1410        model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a))
1411        return ct

Adds a => b (a implies b).

def add_bool_or(self, *literals):
1419    def add_bool_or(self, *literals):
1420        """Adds `Or(literals) == true`: sum(literals) >= 1."""
1421        ct = Constraint(self)
1422        model_ct = self.__model.constraints[ct.index]
1423        model_ct.bool_or.literals.extend(
1424            [
1425                self.get_or_make_boolean_index(x)
1426                for x in expand_generator_or_tuple(literals)
1427            ]
1428        )
1429        return ct

Adds Or(literals) == true: sum(literals) >= 1.

def add_at_least_one(self, *literals):
1437    def add_at_least_one(self, *literals):
1438        """Same as `add_bool_or`: `sum(literals) >= 1`."""
1439        return self.add_bool_or(*literals)

Same as add_bool_or: sum(literals) >= 1.

def add_at_most_one(self, *literals):
1447    def add_at_most_one(self, *literals):
1448        """Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
1449        ct = Constraint(self)
1450        model_ct = self.__model.constraints[ct.index]
1451        model_ct.at_most_one.literals.extend(
1452            [
1453                self.get_or_make_boolean_index(x)
1454                for x in expand_generator_or_tuple(literals)
1455            ]
1456        )
1457        return ct

Adds AtMostOne(literals): sum(literals) <= 1.

def add_exactly_one(self, *literals):
1465    def add_exactly_one(self, *literals):
1466        """Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
1467        ct = Constraint(self)
1468        model_ct = self.__model.constraints[ct.index]
1469        model_ct.exactly_one.literals.extend(
1470            [
1471                self.get_or_make_boolean_index(x)
1472                for x in expand_generator_or_tuple(literals)
1473            ]
1474        )
1475        return ct

Adds ExactlyOne(literals): sum(literals) == 1.

def add_bool_and(self, *literals):
1483    def add_bool_and(self, *literals):
1484        """Adds `And(literals) == true`."""
1485        ct = Constraint(self)
1486        model_ct = self.__model.constraints[ct.index]
1487        model_ct.bool_and.literals.extend(
1488            [
1489                self.get_or_make_boolean_index(x)
1490                for x in expand_generator_or_tuple(literals)
1491            ]
1492        )
1493        return ct

Adds And(literals) == true.

def add_bool_xor(self, *literals):
1501    def add_bool_xor(self, *literals):
1502        """Adds `XOr(literals) == true`.
1503
1504        In contrast to add_bool_or and add_bool_and, it does not support
1505            .only_enforce_if().
1506
1507        Args:
1508          *literals: the list of literals in the constraint.
1509
1510        Returns:
1511          An `Constraint` object.
1512        """
1513        ct = Constraint(self)
1514        model_ct = self.__model.constraints[ct.index]
1515        model_ct.bool_xor.literals.extend(
1516            [
1517                self.get_or_make_boolean_index(x)
1518                for x in expand_generator_or_tuple(literals)
1519            ]
1520        )
1521        return ct

Adds XOr(literals) == true.

In contrast to add_bool_or and add_bool_and, it does not support .only_enforce_if().

Arguments:
  • *literals: the list of literals in the constraint.
Returns:

An Constraint object.

def add_min_equality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], exprs: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]) -> Constraint:
1523    def add_min_equality(
1524        self, target: LinearExprT, exprs: Iterable[LinearExprT]
1525    ) -> Constraint:
1526        """Adds `target == Min(exprs)`."""
1527        ct = Constraint(self)
1528        model_ct = self.__model.constraints[ct.index]
1529        model_ct.lin_max.exprs.extend(
1530            [self.parse_linear_expression(x, True) for x in exprs]
1531        )
1532        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True))
1533        return ct

Adds target == Min(exprs).

def add_max_equality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], exprs: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]) -> Constraint:
1535    def add_max_equality(
1536        self, target: LinearExprT, exprs: Iterable[LinearExprT]
1537    ) -> Constraint:
1538        """Adds `target == Max(exprs)`."""
1539        ct = Constraint(self)
1540        model_ct = self.__model.constraints[ct.index]
1541        model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs])
1542        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
1543        return ct

Adds target == Max(exprs).

def add_division_equality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], num: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], denom: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1545    def add_division_equality(
1546        self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
1547    ) -> Constraint:
1548        """Adds `target == num // denom` (integer division rounded towards 0)."""
1549        ct = Constraint(self)
1550        model_ct = self.__model.constraints[ct.index]
1551        model_ct.int_div.exprs.append(self.parse_linear_expression(num))
1552        model_ct.int_div.exprs.append(self.parse_linear_expression(denom))
1553        model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target))
1554        return ct

Adds target == num // denom (integer division rounded towards 0).

def add_abs_equality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1556    def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
1557        """Adds `target == Abs(expr)`."""
1558        ct = Constraint(self)
1559        model_ct = self.__model.constraints[ct.index]
1560        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr))
1561        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True))
1562        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
1563        return ct

Adds target == Abs(expr).

def add_modulo_equality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], mod: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1565    def add_modulo_equality(
1566        self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
1567    ) -> Constraint:
1568        """Adds `target = expr % mod`.
1569
1570        It uses the C convention, that is the result is the remainder of the
1571        integral division rounded towards 0.
1572
1573            For example:
1574            * 10 % 3 = 1
1575            * -10 % 3 = -1
1576            * 10 % -3 = 1
1577            * -10 % -3 = -1
1578
1579        Args:
1580          target: the target expression.
1581          expr: the expression to compute the modulo of.
1582          mod: the modulus expression.
1583
1584        Returns:
1585          A `Constraint` object.
1586        """
1587        ct = Constraint(self)
1588        model_ct = self.__model.constraints[ct.index]
1589        model_ct.int_mod.exprs.append(self.parse_linear_expression(expr))
1590        model_ct.int_mod.exprs.append(self.parse_linear_expression(mod))
1591        model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target))
1592        return ct

Adds target = expr % mod.

It uses the C convention, that is the result is the remainder of the integral division rounded towards 0.

For example:
* 10 % 3 = 1
* -10 % 3 = -1
* 10 % -3 = 1
* -10 % -3 = -1
Arguments:
  • target: the target expression.
  • expr: the expression to compute the modulo of.
  • mod: the modulus expression.
Returns:

A Constraint object.

def add_multiplication_equality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], *expressions: Union[Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1594    def add_multiplication_equality(
1595        self,
1596        target: LinearExprT,
1597        *expressions: Union[Iterable[LinearExprT], LinearExprT],
1598    ) -> Constraint:
1599        """Adds `target == expressions[0] * .. * expressions[n]`."""
1600        ct = Constraint(self)
1601        model_ct = self.__model.constraints[ct.index]
1602        model_ct.int_prod.exprs.extend(
1603            [
1604                self.parse_linear_expression(expr)
1605                for expr in expand_generator_or_tuple(expressions)
1606            ]
1607        )
1608        model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target))
1609        return ct

Adds target == expressions[0] * .. * expressions[n].

def new_interval_var( self, start: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], size: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], end: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], name: str) -> IntervalVar:
1613    def new_interval_var(
1614        self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str
1615    ) -> IntervalVar:
1616        """Creates an interval variable from start, size, and end.
1617
1618        An interval variable is a constraint, that is itself used in other
1619        constraints like NoOverlap.
1620
1621        Internally, it ensures that `start + size == end`.
1622
1623        Args:
1624          start: The start of the interval. It must be of the form a * var + b.
1625          size: The size of the interval. It must be of the form a * var + b.
1626          end: The end of the interval. It must be of the form a * var + b.
1627          name: The name of the interval variable.
1628
1629        Returns:
1630          An `IntervalVar` object.
1631        """
1632
1633        start_expr = self.parse_linear_expression(start)
1634        size_expr = self.parse_linear_expression(size)
1635        end_expr = self.parse_linear_expression(end)
1636        if len(start_expr.vars) > 1:
1637            raise TypeError(
1638                "cp_model.new_interval_var: start must be 1-var affine or constant."
1639            )
1640        if len(size_expr.vars) > 1:
1641            raise TypeError(
1642                "cp_model.new_interval_var: size must be 1-var affine or constant."
1643            )
1644        if len(end_expr.vars) > 1:
1645            raise TypeError(
1646                "cp_model.new_interval_var: end must be 1-var affine or constant."
1647            )
1648        return IntervalVar(
1649            self.__model,
1650            self.__var_list,
1651            start_expr,
1652            size_expr,
1653            end_expr,
1654            None,
1655            name,
1656        )

Creates an interval variable from start, size, and end.

An interval variable is a constraint, that is itself used in other constraints like NoOverlap.

Internally, it ensures that start + size == end.

Arguments:
  • start: The start of the interval. It must be of the form a * var + b.
  • size: The size of the interval. It must be of the form a * var + b.
  • end: The end of the interval. It must be of the form a * var + b.
  • name: The name of the interval variable.
Returns:

An IntervalVar object.

def new_interval_var_series( self, name: str, index: pandas.core.indexes.base.Index, starts: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], sizes: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], ends: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series]) -> pandas.core.series.Series:
1658    def new_interval_var_series(
1659        self,
1660        name: str,
1661        index: pd.Index,
1662        starts: Union[LinearExprT, pd.Series],
1663        sizes: Union[LinearExprT, pd.Series],
1664        ends: Union[LinearExprT, pd.Series],
1665    ) -> pd.Series:
1666        """Creates a series of interval variables with the given name.
1667
1668        Args:
1669          name (str): Required. The name of the variable set.
1670          index (pd.Index): Required. The index to use for the variable set.
1671          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1672            set. If a `pd.Series` is passed in, it will be based on the
1673            corresponding values of the pd.Series.
1674          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1675            set. If a `pd.Series` is passed in, it will be based on the
1676            corresponding values of the pd.Series.
1677          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1678            set. If a `pd.Series` is passed in, it will be based on the
1679            corresponding values of the pd.Series.
1680
1681        Returns:
1682          pd.Series: The interval variable set indexed by its corresponding
1683          dimensions.
1684
1685        Raises:
1686          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1687          ValueError: if the `name` is not a valid identifier or already exists.
1688          ValueError: if the all the indexes do not match.
1689        """
1690        if not isinstance(index, pd.Index):
1691            raise TypeError("Non-index object is used as index")
1692        if not name.isidentifier():
1693            raise ValueError(f"name={name!r} is not a valid identifier")
1694
1695        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1696        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
1697        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
1698        interval_array = []
1699        for i in index:
1700            interval_array.append(
1701                self.new_interval_var(
1702                    start=starts[i],
1703                    size=sizes[i],
1704                    end=ends[i],
1705                    name=f"{name}[{i}]",
1706                )
1707            )
1708        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • starts (Union[LinearExprT, pd.Series]): The start of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • sizes (Union[LinearExprT, pd.Series]): The size of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • ends (Union[LinearExprT, pd.Series]): The ends of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The interval variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the all the indexes do not match.
def new_fixed_size_interval_var( self, start: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], size: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], name: str) -> IntervalVar:
1710    def new_fixed_size_interval_var(
1711        self, start: LinearExprT, size: IntegralT, name: str
1712    ) -> IntervalVar:
1713        """Creates an interval variable from start, and a fixed size.
1714
1715        An interval variable is a constraint, that is itself used in other
1716        constraints like NoOverlap.
1717
1718        Args:
1719          start: The start of the interval. It must be of the form a * var + b.
1720          size: The size of the interval. It must be an integer value.
1721          name: The name of the interval variable.
1722
1723        Returns:
1724          An `IntervalVar` object.
1725        """
1726        start_expr = self.parse_linear_expression(start)
1727        size_expr = self.parse_linear_expression(size)
1728        end_expr = self.parse_linear_expression(start + size)
1729        if len(start_expr.vars) > 1:
1730            raise TypeError(
1731                "cp_model.new_interval_var: start must be affine or constant."
1732            )
1733        return IntervalVar(
1734            self.__model,
1735            self.__var_list,
1736            start_expr,
1737            size_expr,
1738            end_expr,
1739            None,
1740            name,
1741        )

Creates an interval variable from start, and a fixed size.

An interval variable is a constraint, that is itself used in other constraints like NoOverlap.

Arguments:
  • start: The start of the interval. It must be of the form a * var + b.
  • size: The size of the interval. It must be an integer value.
  • name: The name of the interval variable.
Returns:

An IntervalVar object.

def new_fixed_size_interval_var_series( self, name: str, index: pandas.core.indexes.base.Index, starts: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], sizes: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series]) -> pandas.core.series.Series:
1743    def new_fixed_size_interval_var_series(
1744        self,
1745        name: str,
1746        index: pd.Index,
1747        starts: Union[LinearExprT, pd.Series],
1748        sizes: Union[IntegralT, pd.Series],
1749    ) -> pd.Series:
1750        """Creates a series of interval variables with the given name.
1751
1752        Args:
1753          name (str): Required. The name of the variable set.
1754          index (pd.Index): Required. The index to use for the variable set.
1755          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1756            set. If a `pd.Series` is passed in, it will be based on the
1757            corresponding values of the pd.Series.
1758          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1759            the set. If a `pd.Series` is passed in, it will be based on the
1760            corresponding values of the pd.Series.
1761
1762        Returns:
1763          pd.Series: The interval variable set indexed by its corresponding
1764          dimensions.
1765
1766        Raises:
1767          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1768          ValueError: if the `name` is not a valid identifier or already exists.
1769          ValueError: if the all the indexes do not match.
1770        """
1771        if not isinstance(index, pd.Index):
1772            raise TypeError("Non-index object is used as index")
1773        if not name.isidentifier():
1774            raise ValueError(f"name={name!r} is not a valid identifier")
1775
1776        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1777        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
1778        interval_array = []
1779        for i in index:
1780            interval_array.append(
1781                self.new_fixed_size_interval_var(
1782                    start=starts[i],
1783                    size=sizes[i],
1784                    name=f"{name}[{i}]",
1785                )
1786            )
1787        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • starts (Union[LinearExprT, pd.Series]): The start of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The interval variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the all the indexes do not match.
def new_optional_interval_var( self, start: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], size: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], end: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], is_present: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], name: str) -> IntervalVar:
1789    def new_optional_interval_var(
1790        self,
1791        start: LinearExprT,
1792        size: LinearExprT,
1793        end: LinearExprT,
1794        is_present: LiteralT,
1795        name: str,
1796    ) -> IntervalVar:
1797        """Creates an optional interval var from start, size, end, and is_present.
1798
1799        An optional interval variable is a constraint, that is itself used in other
1800        constraints like NoOverlap. This constraint is protected by a presence
1801        literal that indicates if it is active or not.
1802
1803        Internally, it ensures that `is_present` implies `start + size ==
1804        end`.
1805
1806        Args:
1807          start: The start of the interval. It must be of the form a * var + b.
1808          size: The size of the interval. It must be of the form a * var + b.
1809          end: The end of the interval. It must be of the form a * var + b.
1810          is_present: A literal that indicates if the interval is active or not. A
1811            inactive interval is simply ignored by all constraints.
1812          name: The name of the interval variable.
1813
1814        Returns:
1815          An `IntervalVar` object.
1816        """
1817
1818        # Creates the IntervalConstraintProto object.
1819        is_present_index = self.get_or_make_boolean_index(is_present)
1820        start_expr = self.parse_linear_expression(start)
1821        size_expr = self.parse_linear_expression(size)
1822        end_expr = self.parse_linear_expression(end)
1823        if len(start_expr.vars) > 1:
1824            raise TypeError(
1825                "cp_model.new_interval_var: start must be affine or constant."
1826            )
1827        if len(size_expr.vars) > 1:
1828            raise TypeError(
1829                "cp_model.new_interval_var: size must be affine or constant."
1830            )
1831        if len(end_expr.vars) > 1:
1832            raise TypeError(
1833                "cp_model.new_interval_var: end must be affine or constant."
1834            )
1835        return IntervalVar(
1836            self.__model,
1837            self.__var_list,
1838            start_expr,
1839            size_expr,
1840            end_expr,
1841            is_present_index,
1842            name,
1843        )

Creates an optional interval var from start, size, end, and is_present.

An optional interval variable is a constraint, that is itself used in other constraints like NoOverlap. This constraint is protected by a presence literal that indicates if it is active or not.

Internally, it ensures that is_present implies start + size == end.

Arguments:
  • start: The start of the interval. It must be of the form a * var + b.
  • size: The size of the interval. It must be of the form a * var + b.
  • end: The end of the interval. It must be of the form a * var + b.
  • is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints.
  • name: The name of the interval variable.
Returns:

An IntervalVar object.

def new_optional_interval_var_series( self, name: str, index: pandas.core.indexes.base.Index, starts: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], sizes: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], ends: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], are_present: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool, pandas.core.series.Series]) -> pandas.core.series.Series:
1845    def new_optional_interval_var_series(
1846        self,
1847        name: str,
1848        index: pd.Index,
1849        starts: Union[LinearExprT, pd.Series],
1850        sizes: Union[LinearExprT, pd.Series],
1851        ends: Union[LinearExprT, pd.Series],
1852        are_present: Union[LiteralT, pd.Series],
1853    ) -> pd.Series:
1854        """Creates a series of interval variables with the given name.
1855
1856        Args:
1857          name (str): Required. The name of the variable set.
1858          index (pd.Index): Required. The index to use for the variable set.
1859          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1860            set. If a `pd.Series` is passed in, it will be based on the
1861            corresponding values of the pd.Series.
1862          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1863            set. If a `pd.Series` is passed in, it will be based on the
1864            corresponding values of the pd.Series.
1865          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1866            set. If a `pd.Series` is passed in, it will be based on the
1867            corresponding values of the pd.Series.
1868          are_present (Union[LiteralT, pd.Series]): The performed literal of each
1869            interval in the set. If a `pd.Series` is passed in, it will be based on
1870            the corresponding values of the pd.Series.
1871
1872        Returns:
1873          pd.Series: The interval variable set indexed by its corresponding
1874          dimensions.
1875
1876        Raises:
1877          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1878          ValueError: if the `name` is not a valid identifier or already exists.
1879          ValueError: if the all the indexes do not match.
1880        """
1881        if not isinstance(index, pd.Index):
1882            raise TypeError("Non-index object is used as index")
1883        if not name.isidentifier():
1884            raise ValueError(f"name={name!r} is not a valid identifier")
1885
1886        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1887        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
1888        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
1889        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
1890
1891        interval_array = []
1892        for i in index:
1893            interval_array.append(
1894                self.new_optional_interval_var(
1895                    start=starts[i],
1896                    size=sizes[i],
1897                    end=ends[i],
1898                    is_present=are_present[i],
1899                    name=f"{name}[{i}]",
1900                )
1901            )
1902        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • starts (Union[LinearExprT, pd.Series]): The start of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • sizes (Union[LinearExprT, pd.Series]): The size of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • ends (Union[LinearExprT, pd.Series]): The ends of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • are_present (Union[LiteralT, pd.Series]): The performed literal of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The interval variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the all the indexes do not match.
def new_optional_fixed_size_interval_var( self, start: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], size: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], is_present: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], name: str) -> IntervalVar:
1904    def new_optional_fixed_size_interval_var(
1905        self,
1906        start: LinearExprT,
1907        size: IntegralT,
1908        is_present: LiteralT,
1909        name: str,
1910    ) -> IntervalVar:
1911        """Creates an interval variable from start, and a fixed size.
1912
1913        An interval variable is a constraint, that is itself used in other
1914        constraints like NoOverlap.
1915
1916        Args:
1917          start: The start of the interval. It must be of the form a * var + b.
1918          size: The size of the interval. It must be an integer value.
1919          is_present: A literal that indicates if the interval is active or not. A
1920            inactive interval is simply ignored by all constraints.
1921          name: The name of the interval variable.
1922
1923        Returns:
1924          An `IntervalVar` object.
1925        """
1926        start_expr = self.parse_linear_expression(start)
1927        size_expr = self.parse_linear_expression(size)
1928        end_expr = self.parse_linear_expression(start + size)
1929        if len(start_expr.vars) > 1:
1930            raise TypeError(
1931                "cp_model.new_interval_var: start must be affine or constant."
1932            )
1933        is_present_index = self.get_or_make_boolean_index(is_present)
1934        return IntervalVar(
1935            self.__model,
1936            self.__var_list,
1937            start_expr,
1938            size_expr,
1939            end_expr,
1940            is_present_index,
1941            name,
1942        )

Creates an interval variable from start, and a fixed size.

An interval variable is a constraint, that is itself used in other constraints like NoOverlap.

Arguments:
  • start: The start of the interval. It must be of the form a * var + b.
  • size: The size of the interval. It must be an integer value.
  • is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints.
  • name: The name of the interval variable.
Returns:

An IntervalVar object.

def new_optional_fixed_size_interval_var_series( self, name: str, index: pandas.core.indexes.base.Index, starts: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], sizes: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], are_present: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool, pandas.core.series.Series]) -> pandas.core.series.Series:
1944    def new_optional_fixed_size_interval_var_series(
1945        self,
1946        name: str,
1947        index: pd.Index,
1948        starts: Union[LinearExprT, pd.Series],
1949        sizes: Union[IntegralT, pd.Series],
1950        are_present: Union[LiteralT, pd.Series],
1951    ) -> pd.Series:
1952        """Creates a series of interval variables with the given name.
1953
1954        Args:
1955          name (str): Required. The name of the variable set.
1956          index (pd.Index): Required. The index to use for the variable set.
1957          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1958            set. If a `pd.Series` is passed in, it will be based on the
1959            corresponding values of the pd.Series.
1960          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1961            the set. If a `pd.Series` is passed in, it will be based on the
1962            corresponding values of the pd.Series.
1963          are_present (Union[LiteralT, pd.Series]): The performed literal of each
1964            interval in the set. If a `pd.Series` is passed in, it will be based on
1965            the corresponding values of the pd.Series.
1966
1967        Returns:
1968          pd.Series: The interval variable set indexed by its corresponding
1969          dimensions.
1970
1971        Raises:
1972          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1973          ValueError: if the `name` is not a valid identifier or already exists.
1974          ValueError: if the all the indexes do not match.
1975        """
1976        if not isinstance(index, pd.Index):
1977            raise TypeError("Non-index object is used as index")
1978        if not name.isidentifier():
1979            raise ValueError(f"name={name!r} is not a valid identifier")
1980
1981        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1982        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
1983        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
1984        interval_array = []
1985        for i in index:
1986            interval_array.append(
1987                self.new_optional_fixed_size_interval_var(
1988                    start=starts[i],
1989                    size=sizes[i],
1990                    is_present=are_present[i],
1991                    name=f"{name}[{i}]",
1992                )
1993            )
1994        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • starts (Union[LinearExprT, pd.Series]): The start of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • are_present (Union[LiteralT, pd.Series]): The performed literal of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The interval variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the all the indexes do not match.
def add_no_overlap( self, interval_vars: Iterable[IntervalVar]) -> Constraint:
1996    def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint:
1997        """Adds NoOverlap(interval_vars).
1998
1999        A NoOverlap constraint ensures that all present intervals do not overlap
2000        in time.
2001
2002        Args:
2003          interval_vars: The list of interval variables to constrain.
2004
2005        Returns:
2006          An instance of the `Constraint` class.
2007        """
2008        ct = Constraint(self)
2009        model_ct = self.__model.constraints[ct.index]
2010        model_ct.no_overlap.intervals.extend(
2011            [self.get_interval_index(x) for x in interval_vars]
2012        )
2013        return ct

Adds NoOverlap(interval_vars).

A NoOverlap constraint ensures that all present intervals do not overlap in time.

Arguments:
  • interval_vars: The list of interval variables to constrain.
Returns:

An instance of the Constraint class.

def add_no_overlap_2d( self, x_intervals: Iterable[IntervalVar], y_intervals: Iterable[IntervalVar]) -> Constraint:
2015    def add_no_overlap_2d(
2016        self,
2017        x_intervals: Iterable[IntervalVar],
2018        y_intervals: Iterable[IntervalVar],
2019    ) -> Constraint:
2020        """Adds NoOverlap2D(x_intervals, y_intervals).
2021
2022        A NoOverlap2D constraint ensures that all present rectangles do not overlap
2023        on a plane. Each rectangle is aligned with the X and Y axis, and is defined
2024        by two intervals which represent its projection onto the X and Y axis.
2025
2026        Furthermore, one box is optional if at least one of the x or y interval is
2027        optional.
2028
2029        Args:
2030          x_intervals: The X coordinates of the rectangles.
2031          y_intervals: The Y coordinates of the rectangles.
2032
2033        Returns:
2034          An instance of the `Constraint` class.
2035        """
2036        ct = Constraint(self)
2037        model_ct = self.__model.constraints[ct.index]
2038        model_ct.no_overlap_2d.x_intervals.extend(
2039            [self.get_interval_index(x) for x in x_intervals]
2040        )
2041        model_ct.no_overlap_2d.y_intervals.extend(
2042            [self.get_interval_index(x) for x in y_intervals]
2043        )
2044        return ct

Adds NoOverlap2D(x_intervals, y_intervals).

A NoOverlap2D constraint ensures that all present rectangles do not overlap on a plane. Each rectangle is aligned with the X and Y axis, and is defined by two intervals which represent its projection onto the X and Y axis.

Furthermore, one box is optional if at least one of the x or y interval is optional.

Arguments:
  • x_intervals: The X coordinates of the rectangles.
  • y_intervals: The Y coordinates of the rectangles.
Returns:

An instance of the Constraint class.

def add_cumulative( self, intervals: Iterable[IntervalVar], demands: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], capacity: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
2046    def add_cumulative(
2047        self,
2048        intervals: Iterable[IntervalVar],
2049        demands: Iterable[LinearExprT],
2050        capacity: LinearExprT,
2051    ) -> Constraint:
2052        """Adds Cumulative(intervals, demands, capacity).
2053
2054        This constraint enforces that:
2055
2056            for all t:
2057              sum(demands[i]
2058                if (start(intervals[i]) <= t < end(intervals[i])) and
2059                (intervals[i] is present)) <= capacity
2060
2061        Args:
2062          intervals: The list of intervals.
2063          demands: The list of demands for each interval. Each demand must be >= 0.
2064            Each demand can be a 1-var affine expression (a * x + b).
2065          capacity: The maximum capacity of the cumulative constraint. It can be a
2066            1-var affine expression (a * x + b).
2067
2068        Returns:
2069          An instance of the `Constraint` class.
2070        """
2071        cumulative = Constraint(self)
2072        model_ct = self.__model.constraints[cumulative.index]
2073        model_ct.cumulative.intervals.extend(
2074            [self.get_interval_index(x) for x in intervals]
2075        )
2076        for d in demands:
2077            model_ct.cumulative.demands.append(self.parse_linear_expression(d))
2078        model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity))
2079        return cumulative

Adds Cumulative(intervals, demands, capacity).

This constraint enforces that:

for all t: sum(demands[i] if (start(intervals[i]) <= t < end(intervals[i])) and (intervals[i] is present)) <= capacity

Arguments:
  • intervals: The list of intervals.
  • demands: The list of demands for each interval. Each demand must be >= 0. Each demand can be a 1-var affine expression (a * x + b).
  • capacity: The maximum capacity of the cumulative constraint. It can be a 1-var affine expression (a * x + b).
Returns:

An instance of the Constraint class.

def clone(self) -> CpModel:
2082    def clone(self) -> "CpModel":
2083        """Reset the model, and creates a new one from a CpModelProto instance."""
2084        clone = CpModel()
2085        clone.proto.CopyFrom(self.proto)
2086        clone.rebuild_var_and_constant_map()
2087        return clone

Reset the model, and creates a new one from a CpModelProto instance.

def rebuild_var_and_constant_map(self):
2089    def rebuild_var_and_constant_map(self):
2090        """Internal method used during model cloning."""
2091        for i, var in enumerate(self.__model.variables):
2092            if len(var.domain) == 2 and var.domain[0] == var.domain[1]:
2093                self.__constant_map[var.domain[0]] = i
2094            is_boolean = (
2095                len(var.domain) == 2 and var.domain[0] >= 0 and var.domain[1] <= 1
2096            )
2097            self.__var_list.append(IntVar(self.__model, i, is_boolean, None))

Internal method used during model cloning.

def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2099    def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2100        """Returns an already created Boolean variable from its index."""
2101        result = self._get_int_var(index)
2102        if not result.is_boolean:
2103            raise ValueError(
2104                f"get_bool_var_from_proto_index: index {index} does not reference a"
2105                " boolean variable"
2106            )
2107        return result

Returns an already created Boolean variable from its index.

def get_int_var_from_proto_index(self, index: int) -> IntVar:
2109    def get_int_var_from_proto_index(self, index: int) -> IntVar:
2110        """Returns an already created integer variable from its index."""
2111        return self._get_int_var(index)

Returns an already created integer variable from its index.

def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2113    def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2114        """Returns an already created interval variable from its index."""
2115        if index < 0 or index >= len(self.__model.constraints):
2116            raise ValueError(
2117                f"get_interval_var_from_proto_index: out of bound index {index}"
2118            )
2119        ct = self.__model.constraints[index]
2120        if not ct.HasField("interval"):
2121            raise ValueError(
2122                f"get_interval_var_from_proto_index: index {index} does not"
2123                " reference an" + " interval variable"
2124            )
2125
2126        return IntervalVar(self.__model, self.__var_list, index, None, None, None, None)

Returns an already created interval variable from its index.

2133    @property
2134    def proto(self) -> cp_model_pb2.CpModelProto:
2135        """Returns the underlying CpModelProto."""
2136        return self.__model

Returns the underlying CpModelProto.

def negated(self, index: int) -> int:
2138    def negated(self, index: int) -> int:
2139        return -index - 1
def get_or_make_index( self, arg: Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
2141    def get_or_make_index(self, arg: VariableT) -> int:
2142        """Returns the index of a variable, its negation, or a number."""
2143        if isinstance(arg, IntVar):
2144            return arg.index
2145        if isinstance(arg, IntegralTypes):
2146            return self.get_or_make_index_from_constant(arg)
2147        raise TypeError(
2148            f"NotSupported: model.get_or_make_index({type(arg).__name__!r})"
2149        )

Returns the index of a variable, its negation, or a number.

def get_or_make_boolean_index( self, arg: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> int:
2151    def get_or_make_boolean_index(self, arg: LiteralT) -> int:
2152        """Returns an index from a boolean expression."""
2153        if isinstance(arg, IntVar):
2154            self.assert_is_boolean_variable(arg)
2155            return arg.index
2156        if isinstance(arg, cmh.NotBooleanVariable):
2157            self.assert_is_boolean_variable(arg.negated())
2158            return arg.index
2159        if isinstance(arg, IntegralTypes):
2160            if arg == ~int(False):
2161                return self.get_or_make_index_from_constant(1)
2162            if arg == ~int(True):
2163                return self.get_or_make_index_from_constant(0)
2164            arg = cmn.assert_is_zero_or_one(arg)
2165            return self.get_or_make_index_from_constant(arg)
2166        if cmn.is_boolean(arg):
2167            return self.get_or_make_index_from_constant(int(arg))
2168        raise TypeError(
2169            "not supported:" f" model.get_or_make_boolean_index({type(arg).__name__!r})"
2170        )

Returns an index from a boolean expression.

def get_interval_index(self, arg: IntervalVar) -> int:
2172    def get_interval_index(self, arg: IntervalVar) -> int:
2173        if not isinstance(arg, IntervalVar):
2174            raise TypeError(
2175                f"NotSupported: model.get_interval_index({type(arg).__name__!r})"
2176            )
2177        return arg.index
def get_or_make_index_from_constant( self, value: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
2179    def get_or_make_index_from_constant(self, value: IntegralT) -> int:
2180        if value in self.__constant_map:
2181            return self.__constant_map[value]
2182        constant_var = self.new_int_var(value, value, "")
2183        self.__constant_map[value] = constant_var.index
2184        return constant_var.index
def parse_linear_expression( self, linear_expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], negate: bool = False) -> ortools.sat.cp_model_pb2.LinearExpressionProto:
2186    def parse_linear_expression(
2187        self, linear_expr: LinearExprT, negate: bool = False
2188    ) -> cp_model_pb2.LinearExpressionProto:
2189        """Returns a LinearExpressionProto built from a LinearExpr instance."""
2190        result: cp_model_pb2.LinearExpressionProto = (
2191            cp_model_pb2.LinearExpressionProto()
2192        )
2193        mult = -1 if negate else 1
2194        if isinstance(linear_expr, IntegralTypes):
2195            result.offset = int(linear_expr) * mult
2196            return result
2197
2198        # Raises TypeError if linear_expr is not an integer.
2199        flat_expr = cmh.FlatIntExpr(linear_expr)
2200        result.offset = flat_expr.offset * mult
2201        for var in flat_expr.vars:
2202            result.vars.append(var.index)
2203        for coeff in flat_expr.coeffs:
2204            result.coeffs.append(coeff * mult)
2205        return result

Returns a LinearExpressionProto built from a LinearExpr instance.

def minimize( self, obj: Union[LinearExpr, int, float, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, numpy.float64]):
2239    def minimize(self, obj: ObjLinearExprT):
2240        """Sets the objective of the model to minimize(obj)."""
2241        self._set_objective(obj, minimize=True)

Sets the objective of the model to minimize(obj).

def maximize( self, obj: Union[LinearExpr, int, float, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, numpy.float64]):
2243    def maximize(self, obj: ObjLinearExprT):
2244        """Sets the objective of the model to maximize(obj)."""
2245        self._set_objective(obj, minimize=False)

Sets the objective of the model to maximize(obj).

def has_objective(self) -> bool:
2247    def has_objective(self) -> bool:
2248        return self.__model.HasField("objective") or self.__model.HasField(
2249            "floating_point_objective"
2250        )
def clear_objective(self):
2252    def clear_objective(self):
2253        self.__model.ClearField("objective")
2254        self.__model.ClearField("floating_point_objective")
def add_decision_strategy( self, variables: Sequence[IntVar], var_strategy: <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object>, domain_strategy: <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object>) -> None:
2256    def add_decision_strategy(
2257        self,
2258        variables: Sequence[IntVar],
2259        var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy,
2260        domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy,
2261    ) -> None:
2262        """Adds a search strategy to the model.
2263
2264        Args:
2265          variables: a list of variables this strategy will assign.
2266          var_strategy: heuristic to choose the next variable to assign.
2267          domain_strategy: heuristic to reduce the domain of the selected variable.
2268            Currently, this is advanced code: the union of all strategies added to
2269            the model must be complete, i.e. instantiates all variables. Otherwise,
2270            solve() will fail.
2271        """
2272
2273        strategy: cp_model_pb2.DecisionStrategyProto = (
2274            self.__model.search_strategy.add()
2275        )
2276        for v in variables:
2277            expr = strategy.exprs.add()
2278            if v.index >= 0:
2279                expr.vars.append(v.index)
2280                expr.coeffs.append(1)
2281            else:
2282                expr.vars.append(self.negated(v.index))
2283                expr.coeffs.append(-1)
2284                expr.offset = 1
2285
2286        strategy.variable_selection_strategy = var_strategy
2287        strategy.domain_reduction_strategy = domain_strategy

Adds a search strategy to the model.

Arguments:
  • variables: a list of variables this strategy will assign.
  • var_strategy: heuristic to choose the next variable to assign.
  • domain_strategy: heuristic to reduce the domain of the selected variable. Currently, this is advanced code: the union of all strategies added to the model must be complete, i.e. instantiates all variables. Otherwise, solve() will fail.
def model_stats(self) -> str:
2289    def model_stats(self) -> str:
2290        """Returns a string containing some model statistics."""
2291        return cmh.CpSatHelper.model_stats(self.__model)

Returns a string containing some model statistics.

def validate(self) -> str:
2293    def validate(self) -> str:
2294        """Returns a string indicating that the model is invalid."""
2295        return cmh.CpSatHelper.validate_model(self.__model)

Returns a string indicating that the model is invalid.

def export_to_file(self, file: str) -> bool:
2297    def export_to_file(self, file: str) -> bool:
2298        """Write the model as a protocol buffer to 'file'.
2299
2300        Args:
2301          file: file to write the model to. If the filename ends with 'txt', the
2302            model will be written as a text file, otherwise, the binary format will
2303            be used.
2304
2305        Returns:
2306          True if the model was correctly written.
2307        """
2308        return cmh.CpSatHelper.write_model_to_file(self.__model, file)

Write the model as a protocol buffer to 'file'.

Arguments:
  • file: file to write the model to. If the filename ends with 'txt', the model will be written as a text file, otherwise, the binary format will be used.
Returns:

True if the model was correctly written.

def remove_all_names(self) -> None:
2310    def remove_all_names(self) -> None:
2311        """Removes all names from the model."""
2312        self.__model.ClearField("name")
2313        for v in self.__model.variables:
2314            v.ClearField("name")
2315        for c in self.__model.constraints:
2316            c.ClearField("name")

Removes all names from the model.

def add_hint(self, var, value) -> None:
2324    def add_hint(self, var, value) -> None:
2325        """Adds 'var == value' as a hint to the solver."""
2326        if var.index >= 0:
2327            self.__model.solution_hint.vars.append(self.get_or_make_index(var))
2328            self.__model.solution_hint.values.append(int(value))
2329        else:
2330            self.__model.solution_hint.vars.append(self.negated(var.index))
2331            self.__model.solution_hint.values.append(int(not value))

Adds 'var == value' as a hint to the solver.

def clear_hints(self):
2333    def clear_hints(self):
2334        """Removes any solution hint from the model."""
2335        self.__model.ClearField("solution_hint")

Removes any solution hint from the model.

def add_assumption( self, lit: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> None:
2337    def add_assumption(self, lit: LiteralT) -> None:
2338        """Adds the literal to the model as assumptions."""
2339        self.__model.assumptions.append(self.get_or_make_boolean_index(lit))

Adds the literal to the model as assumptions.

def add_assumptions( self, literals: Iterable[Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]) -> None:
2341    def add_assumptions(self, literals: Iterable[LiteralT]) -> None:
2342        """Adds the literals to the model as assumptions."""
2343        for lit in literals:
2344            self.add_assumption(lit)

Adds the literals to the model as assumptions.

def clear_assumptions(self) -> None:
2346    def clear_assumptions(self) -> None:
2347        """Removes all assumptions from the model."""
2348        self.__model.ClearField("assumptions")

Removes all assumptions from the model.

def assert_is_boolean_variable( self, x: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> None:
2351    def assert_is_boolean_variable(self, x: LiteralT) -> None:
2352        if isinstance(x, IntVar):
2353            var = self.__model.variables[x.index]
2354            if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1:
2355                raise TypeError(
2356                    f"TypeError: {type(x).__name__!r} is not a boolean variable"
2357                )
2358        elif not isinstance(x, cmh.NotBooleanVariable):
2359            raise TypeError(
2360                f"TypeError: {type(x).__name__!r}  is not a boolean variable"
2361            )
def Name(self) -> str:
2366    def Name(self) -> str:
2367        return self.name
def SetName(self, name: str) -> None:
2369    def SetName(self, name: str) -> None:
2370        self.name = name
def Proto(self) -> ortools.sat.cp_model_pb2.CpModelProto:
2372    def Proto(self) -> cp_model_pb2.CpModelProto:
2373        return self.proto
def NewIntVar( self, lb: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], ub: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], name: str) -> IntVar:
684    def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar:
685        """Create an integer variable with domain [lb, ub].
686
687        The CP-SAT solver is limited to integer variables. If you have fractional
688        values, scale them up so that they become integers; if you have strings,
689        encode them as integers.
690
691        Args:
692          lb: Lower bound for the variable.
693          ub: Upper bound for the variable.
694          name: The name of the variable.
695
696        Returns:
697          a variable whose domain is [lb, ub].
698        """
699        domain_is_boolean = lb >= 0 and ub <= 1
700        return self._append_int_var(
701            IntVar(
702                self.__model,
703                sorted_interval_list.Domain(lb, ub),
704                domain_is_boolean,
705                name,
706            )
707        )

Create an integer variable with domain [lb, ub].

The CP-SAT solver is limited to integer variables. If you have fractional values, scale them up so that they become integers; if you have strings, encode them as integers.

Arguments:
  • lb: Lower bound for the variable.
  • ub: Upper bound for the variable.
  • name: The name of the variable.
Returns:

a variable whose domain is [lb, ub].

def NewIntVarFromDomain( self, domain: Domain, name: str) -> IntVar:
709    def new_int_var_from_domain(
710        self, domain: sorted_interval_list.Domain, name: str
711    ) -> IntVar:
712        """Create an integer variable from a domain.
713
714        A domain is a set of integers specified by a collection of intervals.
715        For example, `model.new_int_var_from_domain(cp_model.
716             Domain.from_intervals([[1, 2], [4, 6]]), 'x')`
717
718        Args:
719          domain: An instance of the Domain class.
720          name: The name of the variable.
721
722        Returns:
723            a variable whose domain is the given domain.
724        """
725        domain_is_boolean = domain.min() >= 0 and domain.max() <= 1
726        return self._append_int_var(
727            IntVar(self.__model, domain, domain_is_boolean, name)
728        )

Create an integer variable from a domain.

A domain is a set of integers specified by a collection of intervals. For example, model.new_int_var_from_domain(cp_model. Domain.from_intervals([[1, 2], [4, 6]]), 'x')

Arguments:
  • domain: An instance of the Domain class.
  • name: The name of the variable.
Returns:

a variable whose domain is the given domain.

def NewBoolVar(self, name: str) -> IntVar:
730    def new_bool_var(self, name: str) -> IntVar:
731        """Creates a 0-1 variable with the given name."""
732        return self._append_int_var(
733            IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name)
734        )

Creates a 0-1 variable with the given name.

def NewConstant( self, value: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> IntVar:
736    def new_constant(self, value: IntegralT) -> IntVar:
737        """Declares a constant integer."""
738        index: int = self.get_or_make_index_from_constant(value)
739        return self._get_int_var(index)

Declares a constant integer.

def NewIntVarSeries( self, name: str, index: pandas.core.indexes.base.Index, lower_bounds: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], upper_bounds: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series]) -> pandas.core.series.Series:
741    def new_int_var_series(
742        self,
743        name: str,
744        index: pd.Index,
745        lower_bounds: Union[IntegralT, pd.Series],
746        upper_bounds: Union[IntegralT, pd.Series],
747    ) -> pd.Series:
748        """Creates a series of (scalar-valued) variables with the given name.
749
750        Args:
751          name (str): Required. The name of the variable set.
752          index (pd.Index): Required. The index to use for the variable set.
753          lower_bounds (Union[int, pd.Series]): A lower bound for variables in the
754            set. If a `pd.Series` is passed in, it will be based on the
755            corresponding values of the pd.Series.
756          upper_bounds (Union[int, pd.Series]): An upper bound for variables in the
757            set. If a `pd.Series` is passed in, it will be based on the
758            corresponding values of the pd.Series.
759
760        Returns:
761          pd.Series: The variable set indexed by its corresponding dimensions.
762
763        Raises:
764          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
765          ValueError: if the `name` is not a valid identifier or already exists.
766          ValueError: if the `lowerbound` is greater than the `upperbound`.
767          ValueError: if the index of `lower_bound`, or `upper_bound` does not match
768          the input index.
769        """
770        if not isinstance(index, pd.Index):
771            raise TypeError("Non-index object is used as index")
772        if not name.isidentifier():
773            raise ValueError(f"name={name!r} is not a valid identifier")
774        if (
775            isinstance(lower_bounds, IntegralTypes)
776            and isinstance(upper_bounds, IntegralTypes)
777            and lower_bounds > upper_bounds
778        ):
779            raise ValueError(
780                f"lower_bound={lower_bounds} is greater than"
781                f" upper_bound={upper_bounds} for variable set={name}"
782            )
783
784        lower_bounds = _convert_to_integral_series_and_validate_index(
785            lower_bounds, index
786        )
787        upper_bounds = _convert_to_integral_series_and_validate_index(
788            upper_bounds, index
789        )
790        return pd.Series(
791            index=index,
792            data=[
793                # pylint: disable=g-complex-comprehension
794                self._append_int_var(
795                    IntVar(
796                        model=self.__model,
797                        name=f"{name}[{i}]",
798                        domain=sorted_interval_list.Domain(
799                            lower_bounds[i], upper_bounds[i]
800                        ),
801                        is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1,
802                    )
803                )
804                for i in index
805            ],
806        )

Creates a series of (scalar-valued) variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • lower_bounds (Union[int, pd.Series]): A lower bound for variables in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • upper_bounds (Union[int, pd.Series]): An upper bound for variables in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the lowerbound is greater than the upperbound.
  • ValueError: if the index of lower_bound, or upper_bound does not match
  • the input index.
def NewBoolVarSeries( self, name: str, index: pandas.core.indexes.base.Index) -> pandas.core.series.Series:
808    def new_bool_var_series(
809        self,
810        name: str,
811        index: pd.Index,
812    ) -> pd.Series:
813        """Creates a series of (scalar-valued) variables with the given name.
814
815        Args:
816          name (str): Required. The name of the variable set.
817          index (pd.Index): Required. The index to use for the variable set.
818
819        Returns:
820          pd.Series: The variable set indexed by its corresponding dimensions.
821
822        Raises:
823          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
824          ValueError: if the `name` is not a valid identifier or already exists.
825        """
826        if not isinstance(index, pd.Index):
827            raise TypeError("Non-index object is used as index")
828        if not name.isidentifier():
829            raise ValueError(f"name={name!r} is not a valid identifier")
830        return pd.Series(
831            index=index,
832            data=[
833                # pylint: disable=g-complex-comprehension
834                self._append_int_var(
835                    IntVar(
836                        model=self.__model,
837                        name=f"{name}[{i}]",
838                        domain=sorted_interval_list.Domain(0, 1),
839                        is_boolean=True,
840                    )
841                )
842                for i in index
843            ],
844        )

Creates a series of (scalar-valued) variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
Returns:

pd.Series: The variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
def AddLinearConstraint( self, linear_expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], lb: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], ub: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
848    def add_linear_constraint(
849        self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT
850    ) -> Constraint:
851        """Adds the constraint: `lb <= linear_expr <= ub`."""
852        return self.add_linear_expression_in_domain(
853            linear_expr, sorted_interval_list.Domain(lb, ub)
854        )

Adds the constraint: lb <= linear_expr <= ub.

def AddLinearExpressionInDomain( self, linear_expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], domain: Domain) -> Constraint:
856    def add_linear_expression_in_domain(
857        self,
858        linear_expr: LinearExprT,
859        domain: sorted_interval_list.Domain,
860    ) -> Constraint:
861        """Adds the constraint: `linear_expr` in `domain`."""
862        if isinstance(linear_expr, LinearExpr):
863            ble = BoundedLinearExpression(linear_expr, domain)
864            if not ble.ok:
865                raise TypeError(
866                    "Cannot add a linear expression containing floating point"
867                    f" coefficients or constants: {type(linear_expr).__name__!r}"
868                )
869            return self.add(ble)
870        if isinstance(linear_expr, IntegralTypes):
871            if not domain.contains(int(linear_expr)):
872                return self.add_bool_or([])  # Evaluate to false.
873            else:
874                return self.add_bool_and([])  # Evaluate to true.
875        raise TypeError(
876            "not supported:"
877            f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})"
878        )

Adds the constraint: linear_expr in domain.

def Add( self, ct: Union[BoundedLinearExpression, bool, numpy.bool]) -> Constraint:
880    def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint:
881        """Adds a `BoundedLinearExpression` to the model.
882
883        Args:
884          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
885
886        Returns:
887          An instance of the `Constraint` class.
888
889        Raises:
890          TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean.
891        """
892        if isinstance(ct, BoundedLinearExpression):
893            result = Constraint(self)
894            model_ct = self.__model.constraints[result.index]
895            for var in ct.vars:
896                model_ct.linear.vars.append(var.index)
897            model_ct.linear.coeffs.extend(ct.coeffs)
898            model_ct.linear.domain.extend(
899                [
900                    cmn.capped_subtraction(x, ct.offset)
901                    for x in ct.bounds.flattened_intervals()
902                ]
903            )
904            return result
905        if ct and cmn.is_boolean(ct):
906            return self.add_bool_or([True])
907        if not ct and cmn.is_boolean(ct):
908            return self.add_bool_or([])  # Evaluate to false.
909        raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})")

Adds a BoundedLinearExpression to the model.

Arguments:
Returns:

An instance of the Constraint class.

Raises:
def AddAllDifferent(self, *expressions):
919    def add_all_different(self, *expressions):
920        """Adds AllDifferent(expressions).
921
922        This constraint forces all expressions to have different values.
923
924        Args:
925          *expressions: simple expressions of the form a * var + constant.
926
927        Returns:
928          An instance of the `Constraint` class.
929        """
930        ct = Constraint(self)
931        model_ct = self.__model.constraints[ct.index]
932        expanded = expand_generator_or_tuple(expressions)
933        model_ct.all_diff.exprs.extend(
934            self.parse_linear_expression(x) for x in expanded
935        )
936        return ct

Adds AllDifferent(expressions).

This constraint forces all expressions to have different values.

Arguments:
  • *expressions: simple expressions of the form a * var + constant.
Returns:

An instance of the Constraint class.

def AddElement( self, index: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
938    def add_element(
939        self,
940        index: LinearExprT,
941        expressions: Sequence[LinearExprT],
942        target: LinearExprT,
943    ) -> Constraint:
944        """Adds the element constraint: `expressions[index] == target`.
945
946        Args:
947          index: The index of the selected expression in the array. It must be an
948            affine expression (a * var + b).
949          expressions: A list of affine expressions.
950          target: The expression constrained to be equal to the selected expression.
951            It must be an affine expression (a * var + b).
952
953        Returns:
954          An instance of the `Constraint` class.
955        """
956
957        if not expressions:
958            raise ValueError("add_element expects a non-empty expressions array")
959
960        if isinstance(index, IntegralTypes):
961            expression: LinearExprT = list(expressions)[int(index)]
962            return self.add(expression == target)
963
964        ct = Constraint(self)
965        model_ct = self.__model.constraints[ct.index]
966        model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index))
967        model_ct.element.exprs.extend(
968            [self.parse_linear_expression(e) for e in expressions]
969        )
970        model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target))
971        return ct

Adds the element constraint: expressions[index] == target.

Arguments:
  • index: The index of the selected expression in the array. It must be an affine expression (a * var + b).
  • expressions: A list of affine expressions.
  • target: The expression constrained to be equal to the selected expression. It must be an affine expression (a * var + b).
Returns:

An instance of the Constraint class.

def AddCircuit( self, arcs: Sequence[Tuple[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]]) -> Constraint:
 973    def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
 974        """Adds Circuit(arcs).
 975
 976        Adds a circuit constraint from a sparse list of arcs that encode the graph.
 977
 978        A circuit is a unique Hamiltonian cycle in a subgraph of the total
 979        graph. In case a node 'i' is not in the cycle, then there must be a
 980        loop arc 'i -> i' associated with a true literal. Otherwise
 981        this constraint will fail.
 982
 983        Args:
 984          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
 985            literal). The arc is selected in the circuit if the literal is true.
 986            Both source_node and destination_node must be integers between 0 and the
 987            number of nodes - 1.
 988
 989        Returns:
 990          An instance of the `Constraint` class.
 991
 992        Raises:
 993          ValueError: If the list of arcs is empty.
 994        """
 995        if not arcs:
 996            raise ValueError("add_circuit expects a non-empty array of arcs")
 997        ct = Constraint(self)
 998        model_ct = self.__model.constraints[ct.index]
 999        for arc in arcs:
1000            model_ct.circuit.tails.append(arc[0])
1001            model_ct.circuit.heads.append(arc[1])
1002            model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2]))
1003        return ct

Adds Circuit(arcs).

Adds a circuit constraint from a sparse list of arcs that encode the graph.

A circuit is a unique Hamiltonian cycle in a subgraph of the total graph. In case a node 'i' is not in the cycle, then there must be a loop arc 'i -> i' associated with a true literal. Otherwise this constraint will fail.

Arguments:
  • arcs: a list of arcs. An arc is a tuple (source_node, destination_node, literal). The arc is selected in the circuit if the literal is true. Both source_node and destination_node must be integers between 0 and the number of nodes - 1.
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: If the list of arcs is empty.
def AddMultipleCircuit( self, arcs: Sequence[Tuple[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]]) -> Constraint:
1005    def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1006        """Adds a multiple circuit constraint, aka the 'VRP' constraint.
1007
1008        The direct graph where arc #i (from tails[i] to head[i]) is present iff
1009        literals[i] is true must satisfy this set of properties:
1010        - #incoming arcs == 1 except for node 0.
1011        - #outgoing arcs == 1 except for node 0.
1012        - for node zero, #incoming arcs == #outgoing arcs.
1013        - There are no duplicate arcs.
1014        - Self-arcs are allowed except for node 0.
1015        - There is no cycle in this graph, except through node 0.
1016
1017        Args:
1018          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1019            literal). The arc is selected in the circuit if the literal is true.
1020            Both source_node and destination_node must be integers between 0 and the
1021            number of nodes - 1.
1022
1023        Returns:
1024          An instance of the `Constraint` class.
1025
1026        Raises:
1027          ValueError: If the list of arcs is empty.
1028        """
1029        if not arcs:
1030            raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
1031        ct = Constraint(self)
1032        model_ct = self.__model.constraints[ct.index]
1033        for arc in arcs:
1034            model_ct.routes.tails.append(arc[0])
1035            model_ct.routes.heads.append(arc[1])
1036            model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2]))
1037        return ct

Adds a multiple circuit constraint, aka the 'VRP' constraint.

The direct graph where arc #i (from tails[i] to head[i]) is present iff literals[i] is true must satisfy this set of properties:

  • #incoming arcs == 1 except for node 0.
  • #outgoing arcs == 1 except for node 0.
  • for node zero, #incoming arcs == #outgoing arcs.
  • There are no duplicate arcs.
  • Self-arcs are allowed except for node 0.
  • There is no cycle in this graph, except through node 0.
Arguments:
  • arcs: a list of arcs. An arc is a tuple (source_node, destination_node, literal). The arc is selected in the circuit if the literal is true. Both source_node and destination_node must be integers between 0 and the number of nodes - 1.
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: If the list of arcs is empty.
def AddAllowedAssignments( self, expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], tuples_list: Iterable[Sequence[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]]) -> Constraint:
1039    def add_allowed_assignments(
1040        self,
1041        expressions: Sequence[LinearExprT],
1042        tuples_list: Iterable[Sequence[IntegralT]],
1043    ) -> Constraint:
1044        """Adds AllowedAssignments(expressions, tuples_list).
1045
1046        An AllowedAssignments constraint is a constraint on an array of affine
1047        expressions, which requires that when all expressions are assigned values,
1048        the
1049        resulting array equals one of the  tuples in `tuple_list`.
1050
1051        Args:
1052          expressions: A list of affine expressions (a * var + b).
1053          tuples_list: A list of admissible tuples. Each tuple must have the same
1054            length as the expressions, and the ith value of a tuple corresponds to
1055            the ith expression.
1056
1057        Returns:
1058          An instance of the `Constraint` class.
1059
1060        Raises:
1061          TypeError: If a tuple does not have the same size as the list of
1062              expressions.
1063          ValueError: If the array of expressions is empty.
1064        """
1065
1066        if not expressions:
1067            raise ValueError(
1068                "add_allowed_assignments expects a non-empty expressions array"
1069            )
1070
1071        ct: Constraint = Constraint(self)
1072        model_ct = self.__model.constraints[ct.index]
1073        model_ct.table.exprs.extend(
1074            [self.parse_linear_expression(e) for e in expressions]
1075        )
1076        arity: int = len(expressions)
1077        for one_tuple in tuples_list:
1078            if len(one_tuple) != arity:
1079                raise TypeError(f"Tuple {one_tuple!r} has the wrong arity")
1080
1081        # duck-typing (no explicit type checks here)
1082        try:
1083            for one_tuple in tuples_list:
1084                model_ct.table.values.extend(one_tuple)
1085        except ValueError as ex:
1086            raise TypeError(
1087                "add_xxx_assignment: Not an integer or does not fit in an int64_t:"
1088                f" {type(ex.args).__name__!r}"
1089            ) from ex
1090
1091        return ct

Adds AllowedAssignments(expressions, tuples_list).

An AllowedAssignments constraint is a constraint on an array of affine expressions, which requires that when all expressions are assigned values, the resulting array equals one of the tuples in tuple_list.

Arguments:
  • expressions: A list of affine expressions (a * var + b).
  • tuples_list: A list of admissible tuples. Each tuple must have the same length as the expressions, and the ith value of a tuple corresponds to the ith expression.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: If a tuple does not have the same size as the list of expressions.
  • ValueError: If the array of expressions is empty.
def AddForbiddenAssignments( self, expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], tuples_list: Iterable[Sequence[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]]) -> Constraint:
1093    def add_forbidden_assignments(
1094        self,
1095        expressions: Sequence[LinearExprT],
1096        tuples_list: Iterable[Sequence[IntegralT]],
1097    ) -> Constraint:
1098        """Adds add_forbidden_assignments(expressions, [tuples_list]).
1099
1100        A ForbiddenAssignments constraint is a constraint on an array of affine
1101        expressions where the list of impossible combinations is provided in the
1102        tuples list.
1103
1104        Args:
1105          expressions: A list of affine expressions (a * var + b).
1106          tuples_list: A list of forbidden tuples. Each tuple must have the same
1107            length as the expressions, and the *i*th value of a tuple corresponds to
1108            the *i*th expression.
1109
1110        Returns:
1111          An instance of the `Constraint` class.
1112
1113        Raises:
1114          TypeError: If a tuple does not have the same size as the list of
1115                     expressions.
1116          ValueError: If the array of expressions is empty.
1117        """
1118
1119        if not expressions:
1120            raise ValueError(
1121                "add_forbidden_assignments expects a non-empty expressions array"
1122            )
1123
1124        index: int = len(self.__model.constraints)
1125        ct: Constraint = self.add_allowed_assignments(expressions, tuples_list)
1126        self.__model.constraints[index].table.negated = True
1127        return ct

Adds add_forbidden_assignments(expressions, [tuples_list]).

A ForbiddenAssignments constraint is a constraint on an array of affine expressions where the list of impossible combinations is provided in the tuples list.

Arguments:
  • expressions: A list of affine expressions (a * var + b).
  • tuples_list: A list of forbidden tuples. Each tuple must have the same length as the expressions, and the ith value of a tuple corresponds to the ith expression.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: If a tuple does not have the same size as the list of expressions.
  • ValueError: If the array of expressions is empty.
def AddAutomaton( self, transition_expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], starting_state: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], final_states: Sequence[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], transition_triples: Sequence[Tuple[Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]]) -> Constraint:
1129    def add_automaton(
1130        self,
1131        transition_expressions: Sequence[LinearExprT],
1132        starting_state: IntegralT,
1133        final_states: Sequence[IntegralT],
1134        transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]],
1135    ) -> Constraint:
1136        """Adds an automaton constraint.
1137
1138        An automaton constraint takes a list of affine expressions (a * var + b) (of
1139        size *n*), an initial state, a set of final states, and a set of
1140        transitions. A transition is a triplet (*tail*, *transition*, *head*), where
1141        *tail* and *head* are states, and *transition* is the label of an arc from
1142        *head* to *tail*, corresponding to the value of one expression in the list
1143        of
1144        expressions.
1145
1146        This automaton will be unrolled into a flow with *n* + 1 phases. Each phase
1147        contains the possible states of the automaton. The first state contains the
1148        initial state. The last phase contains the final states.
1149
1150        Between two consecutive phases *i* and *i* + 1, the automaton creates a set
1151        of arcs. For each transition (*tail*, *transition*, *head*), it will add
1152        an arc from the state *tail* of phase *i* and the state *head* of phase
1153        *i* + 1. This arc is labeled by the value *transition* of the expression
1154        `expressions[i]`. That is, this arc can only be selected if `expressions[i]`
1155        is assigned the value *transition*.
1156
1157        A feasible solution of this constraint is an assignment of expressions such
1158        that, starting from the initial state in phase 0, there is a path labeled by
1159        the values of the expressions that ends in one of the final states in the
1160        final phase.
1161
1162        Args:
1163          transition_expressions: A non-empty list of affine expressions (a * var +
1164            b) whose values correspond to the labels of the arcs traversed by the
1165            automaton.
1166          starting_state: The initial state of the automaton.
1167          final_states: A non-empty list of admissible final states.
1168          transition_triples: A list of transitions for the automaton, in the
1169            following format (current_state, variable_value, next_state).
1170
1171        Returns:
1172          An instance of the `Constraint` class.
1173
1174        Raises:
1175          ValueError: if `transition_expressions`, `final_states`, or
1176            `transition_triples` are empty.
1177        """
1178
1179        if not transition_expressions:
1180            raise ValueError(
1181                "add_automaton expects a non-empty transition_expressions array"
1182            )
1183        if not final_states:
1184            raise ValueError("add_automaton expects some final states")
1185
1186        if not transition_triples:
1187            raise ValueError("add_automaton expects some transition triples")
1188
1189        ct = Constraint(self)
1190        model_ct = self.__model.constraints[ct.index]
1191        model_ct.automaton.exprs.extend(
1192            [self.parse_linear_expression(e) for e in transition_expressions]
1193        )
1194        model_ct.automaton.starting_state = starting_state
1195        for v in final_states:
1196            model_ct.automaton.final_states.append(v)
1197        for t in transition_triples:
1198            if len(t) != 3:
1199                raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)")
1200            model_ct.automaton.transition_tail.append(t[0])
1201            model_ct.automaton.transition_label.append(t[1])
1202            model_ct.automaton.transition_head.append(t[2])
1203        return ct

Adds an automaton constraint.

An automaton constraint takes a list of affine expressions (a * var + b) (of size n), an initial state, a set of final states, and a set of transitions. A transition is a triplet (tail, transition, head), where tail and head are states, and transition is the label of an arc from head to tail, corresponding to the value of one expression in the list of expressions.

This automaton will be unrolled into a flow with n + 1 phases. Each phase contains the possible states of the automaton. The first state contains the initial state. The last phase contains the final states.

Between two consecutive phases i and i + 1, the automaton creates a set of arcs. For each transition (tail, transition, head), it will add an arc from the state tail of phase i and the state head of phase i + 1. This arc is labeled by the value transition of the expression expressions[i]. That is, this arc can only be selected if expressions[i] is assigned the value transition.

A feasible solution of this constraint is an assignment of expressions such that, starting from the initial state in phase 0, there is a path labeled by the values of the expressions that ends in one of the final states in the final phase.

Arguments:
  • transition_expressions: A non-empty list of affine expressions (a * var + b) whose values correspond to the labels of the arcs traversed by the automaton.
  • starting_state: The initial state of the automaton.
  • final_states: A non-empty list of admissible final states.
  • transition_triples: A list of transitions for the automaton, in the following format (current_state, variable_value, next_state).
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: if transition_expressions, final_states, or transition_triples are empty.
def AddInverse( self, variables: Sequence[Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], inverse_variables: Sequence[Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]) -> Constraint:
1205    def add_inverse(
1206        self,
1207        variables: Sequence[VariableT],
1208        inverse_variables: Sequence[VariableT],
1209    ) -> Constraint:
1210        """Adds Inverse(variables, inverse_variables).
1211
1212        An inverse constraint enforces that if `variables[i]` is assigned a value
1213        `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa.
1214
1215        Args:
1216          variables: An array of integer variables.
1217          inverse_variables: An array of integer variables.
1218
1219        Returns:
1220          An instance of the `Constraint` class.
1221
1222        Raises:
1223          TypeError: if variables and inverse_variables have different lengths, or
1224              if they are empty.
1225        """
1226
1227        if not variables or not inverse_variables:
1228            raise TypeError("The Inverse constraint does not accept empty arrays")
1229        if len(variables) != len(inverse_variables):
1230            raise TypeError(
1231                "In the inverse constraint, the two array variables and"
1232                " inverse_variables must have the same length."
1233            )
1234        ct = Constraint(self)
1235        model_ct = self.__model.constraints[ct.index]
1236        model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables])
1237        model_ct.inverse.f_inverse.extend(
1238            [self.get_or_make_index(x) for x in inverse_variables]
1239        )
1240        return ct

Adds Inverse(variables, inverse_variables).

An inverse constraint enforces that if variables[i] is assigned a value j, then inverse_variables[j] is assigned a value i. And vice versa.

Arguments:
  • variables: An array of integer variables.
  • inverse_variables: An array of integer variables.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: if variables and inverse_variables have different lengths, or if they are empty.
def AddReservoirConstraint( self, times: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], level_changes: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], min_level: int, max_level: int) -> Constraint:
1242    def add_reservoir_constraint(
1243        self,
1244        times: Iterable[LinearExprT],
1245        level_changes: Iterable[LinearExprT],
1246        min_level: int,
1247        max_level: int,
1248    ) -> Constraint:
1249        """Adds Reservoir(times, level_changes, min_level, max_level).
1250
1251        Maintains a reservoir level within bounds. The water level starts at 0, and
1252        at any time, it must be between min_level and max_level.
1253
1254        If the affine expression `times[i]` is assigned a value t, then the current
1255        level changes by `level_changes[i]`, which is constant, at time t.
1256
1257         Note that min level must be <= 0, and the max level must be >= 0. Please
1258         use fixed level_changes to simulate initial state.
1259
1260         Therefore, at any time:
1261             sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
1262
1263        Args:
1264          times: A list of 1-var affine expressions (a * x + b) which specify the
1265            time of the filling or emptying the reservoir.
1266          level_changes: A list of integer values that specifies the amount of the
1267            emptying or filling. Currently, variable demands are not supported.
1268          min_level: At any time, the level of the reservoir must be greater or
1269            equal than the min level.
1270          max_level: At any time, the level of the reservoir must be less or equal
1271            than the max level.
1272
1273        Returns:
1274          An instance of the `Constraint` class.
1275
1276        Raises:
1277          ValueError: if max_level < min_level.
1278
1279          ValueError: if max_level < 0.
1280
1281          ValueError: if min_level > 0
1282        """
1283
1284        if max_level < min_level:
1285            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1286
1287        if max_level < 0:
1288            raise ValueError("Reservoir constraint must have a max_level >= 0")
1289
1290        if min_level > 0:
1291            raise ValueError("Reservoir constraint must have a min_level <= 0")
1292
1293        ct = Constraint(self)
1294        model_ct = self.__model.constraints[ct.index]
1295        model_ct.reservoir.time_exprs.extend(
1296            [self.parse_linear_expression(x) for x in times]
1297        )
1298        model_ct.reservoir.level_changes.extend(
1299            [self.parse_linear_expression(x) for x in level_changes]
1300        )
1301        model_ct.reservoir.min_level = min_level
1302        model_ct.reservoir.max_level = max_level
1303        return ct

Adds Reservoir(times, level_changes, min_level, max_level).

Maintains a reservoir level within bounds. The water level starts at 0, and at any time, it must be between min_level and max_level.

If the affine expression times[i] is assigned a value t, then the current level changes by level_changes[i], which is constant, at time t.

Note that min level must be <= 0, and the max level must be >= 0. Please use fixed level_changes to simulate initial state.

Therefore, at any time: sum(level_changes[i] if times[i] <= t) in [min_level, max_level]

Arguments:
  • times: A list of 1-var affine expressions (a * x + b) which specify the time of the filling or emptying the reservoir.
  • level_changes: A list of integer values that specifies the amount of the emptying or filling. Currently, variable demands are not supported.
  • min_level: At any time, the level of the reservoir must be greater or equal than the min level.
  • max_level: At any time, the level of the reservoir must be less or equal than the max level.
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: if max_level < min_level.
  • ValueError: if max_level < 0.
  • ValueError: if min_level > 0
def AddReservoirConstraintWithActive( self, times: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], level_changes: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], actives: Iterable[Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]], min_level: int, max_level: int) -> Constraint:
1305    def add_reservoir_constraint_with_active(
1306        self,
1307        times: Iterable[LinearExprT],
1308        level_changes: Iterable[LinearExprT],
1309        actives: Iterable[LiteralT],
1310        min_level: int,
1311        max_level: int,
1312    ) -> Constraint:
1313        """Adds Reservoir(times, level_changes, actives, min_level, max_level).
1314
1315        Maintains a reservoir level within bounds. The water level starts at 0, and
1316        at any time, it must be between min_level and max_level.
1317
1318        If the variable `times[i]` is assigned a value t, and `actives[i]` is
1319        `True`, then the current level changes by `level_changes[i]`, which is
1320        constant,
1321        at time t.
1322
1323         Note that min level must be <= 0, and the max level must be >= 0. Please
1324         use fixed level_changes to simulate initial state.
1325
1326         Therefore, at any time:
1327             sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level,
1328             max_level]
1329
1330
1331        The array of boolean variables 'actives', if defined, indicates which
1332        actions are actually performed.
1333
1334        Args:
1335          times: A list of 1-var affine expressions (a * x + b) which specify the
1336            time of the filling or emptying the reservoir.
1337          level_changes: A list of integer values that specifies the amount of the
1338            emptying or filling. Currently, variable demands are not supported.
1339          actives: a list of boolean variables. They indicates if the
1340            emptying/refilling events actually take place.
1341          min_level: At any time, the level of the reservoir must be greater or
1342            equal than the min level.
1343          max_level: At any time, the level of the reservoir must be less or equal
1344            than the max level.
1345
1346        Returns:
1347          An instance of the `Constraint` class.
1348
1349        Raises:
1350          ValueError: if max_level < min_level.
1351
1352          ValueError: if max_level < 0.
1353
1354          ValueError: if min_level > 0
1355        """
1356
1357        if max_level < min_level:
1358            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1359
1360        if max_level < 0:
1361            raise ValueError("Reservoir constraint must have a max_level >= 0")
1362
1363        if min_level > 0:
1364            raise ValueError("Reservoir constraint must have a min_level <= 0")
1365
1366        ct = Constraint(self)
1367        model_ct = self.__model.constraints[ct.index]
1368        model_ct.reservoir.time_exprs.extend(
1369            [self.parse_linear_expression(x) for x in times]
1370        )
1371        model_ct.reservoir.level_changes.extend(
1372            [self.parse_linear_expression(x) for x in level_changes]
1373        )
1374        model_ct.reservoir.active_literals.extend(
1375            [self.get_or_make_boolean_index(x) for x in actives]
1376        )
1377        model_ct.reservoir.min_level = min_level
1378        model_ct.reservoir.max_level = max_level
1379        return ct

Adds Reservoir(times, level_changes, actives, min_level, max_level).

Maintains a reservoir level within bounds. The water level starts at 0, and at any time, it must be between min_level and max_level.

If the variable times[i] is assigned a value t, and actives[i] is True, then the current level changes by level_changes[i], which is constant, at time t.

Note that min level must be <= 0, and the max level must be >= 0. Please use fixed level_changes to simulate initial state.

Therefore, at any time: sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, max_level]

The array of boolean variables 'actives', if defined, indicates which actions are actually performed.

Arguments:
  • times: A list of 1-var affine expressions (a * x + b) which specify the time of the filling or emptying the reservoir.
  • level_changes: A list of integer values that specifies the amount of the emptying or filling. Currently, variable demands are not supported.
  • actives: a list of boolean variables. They indicates if the emptying/refilling events actually take place.
  • min_level: At any time, the level of the reservoir must be greater or equal than the min level.
  • max_level: At any time, the level of the reservoir must be less or equal than the max level.
Returns:

An instance of the Constraint class.

Raises:
  • ValueError: if max_level < min_level.
  • ValueError: if max_level < 0.
  • ValueError: if min_level > 0
def AddImplication( self, a: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], b: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> Constraint:
1405    def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint:
1406        """Adds `a => b` (`a` implies `b`)."""
1407        ct = Constraint(self)
1408        model_ct = self.__model.constraints[ct.index]
1409        model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b))
1410        model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a))
1411        return ct

Adds a => b (a implies b).

def AddBoolOr(self, *literals):
1419    def add_bool_or(self, *literals):
1420        """Adds `Or(literals) == true`: sum(literals) >= 1."""
1421        ct = Constraint(self)
1422        model_ct = self.__model.constraints[ct.index]
1423        model_ct.bool_or.literals.extend(
1424            [
1425                self.get_or_make_boolean_index(x)
1426                for x in expand_generator_or_tuple(literals)
1427            ]
1428        )
1429        return ct

Adds Or(literals) == true: sum(literals) >= 1.

def AddAtLeastOne(self, *literals):
1437    def add_at_least_one(self, *literals):
1438        """Same as `add_bool_or`: `sum(literals) >= 1`."""
1439        return self.add_bool_or(*literals)

Same as add_bool_or: sum(literals) >= 1.

def AddAtMostOne(self, *literals):
1447    def add_at_most_one(self, *literals):
1448        """Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
1449        ct = Constraint(self)
1450        model_ct = self.__model.constraints[ct.index]
1451        model_ct.at_most_one.literals.extend(
1452            [
1453                self.get_or_make_boolean_index(x)
1454                for x in expand_generator_or_tuple(literals)
1455            ]
1456        )
1457        return ct

Adds AtMostOne(literals): sum(literals) <= 1.

def AddExactlyOne(self, *literals):
1465    def add_exactly_one(self, *literals):
1466        """Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
1467        ct = Constraint(self)
1468        model_ct = self.__model.constraints[ct.index]
1469        model_ct.exactly_one.literals.extend(
1470            [
1471                self.get_or_make_boolean_index(x)
1472                for x in expand_generator_or_tuple(literals)
1473            ]
1474        )
1475        return ct

Adds ExactlyOne(literals): sum(literals) == 1.

def AddBoolAnd(self, *literals):
1483    def add_bool_and(self, *literals):
1484        """Adds `And(literals) == true`."""
1485        ct = Constraint(self)
1486        model_ct = self.__model.constraints[ct.index]
1487        model_ct.bool_and.literals.extend(
1488            [
1489                self.get_or_make_boolean_index(x)
1490                for x in expand_generator_or_tuple(literals)
1491            ]
1492        )
1493        return ct

Adds And(literals) == true.

def AddBoolXOr(self, *literals):
1501    def add_bool_xor(self, *literals):
1502        """Adds `XOr(literals) == true`.
1503
1504        In contrast to add_bool_or and add_bool_and, it does not support
1505            .only_enforce_if().
1506
1507        Args:
1508          *literals: the list of literals in the constraint.
1509
1510        Returns:
1511          An `Constraint` object.
1512        """
1513        ct = Constraint(self)
1514        model_ct = self.__model.constraints[ct.index]
1515        model_ct.bool_xor.literals.extend(
1516            [
1517                self.get_or_make_boolean_index(x)
1518                for x in expand_generator_or_tuple(literals)
1519            ]
1520        )
1521        return ct

Adds XOr(literals) == true.

In contrast to add_bool_or and add_bool_and, it does not support .only_enforce_if().

Arguments:
  • *literals: the list of literals in the constraint.
Returns:

An Constraint object.

def AddMinEquality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], exprs: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]) -> Constraint:
1523    def add_min_equality(
1524        self, target: LinearExprT, exprs: Iterable[LinearExprT]
1525    ) -> Constraint:
1526        """Adds `target == Min(exprs)`."""
1527        ct = Constraint(self)
1528        model_ct = self.__model.constraints[ct.index]
1529        model_ct.lin_max.exprs.extend(
1530            [self.parse_linear_expression(x, True) for x in exprs]
1531        )
1532        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True))
1533        return ct

Adds target == Min(exprs).

def AddMaxEquality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], exprs: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]) -> Constraint:
1535    def add_max_equality(
1536        self, target: LinearExprT, exprs: Iterable[LinearExprT]
1537    ) -> Constraint:
1538        """Adds `target == Max(exprs)`."""
1539        ct = Constraint(self)
1540        model_ct = self.__model.constraints[ct.index]
1541        model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs])
1542        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
1543        return ct

Adds target == Max(exprs).

def AddDivisionEquality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], num: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], denom: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1545    def add_division_equality(
1546        self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
1547    ) -> Constraint:
1548        """Adds `target == num // denom` (integer division rounded towards 0)."""
1549        ct = Constraint(self)
1550        model_ct = self.__model.constraints[ct.index]
1551        model_ct.int_div.exprs.append(self.parse_linear_expression(num))
1552        model_ct.int_div.exprs.append(self.parse_linear_expression(denom))
1553        model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target))
1554        return ct

Adds target == num // denom (integer division rounded towards 0).

def AddAbsEquality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1556    def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
1557        """Adds `target == Abs(expr)`."""
1558        ct = Constraint(self)
1559        model_ct = self.__model.constraints[ct.index]
1560        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr))
1561        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True))
1562        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
1563        return ct

Adds target == Abs(expr).

def AddModuloEquality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], mod: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1565    def add_modulo_equality(
1566        self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
1567    ) -> Constraint:
1568        """Adds `target = expr % mod`.
1569
1570        It uses the C convention, that is the result is the remainder of the
1571        integral division rounded towards 0.
1572
1573            For example:
1574            * 10 % 3 = 1
1575            * -10 % 3 = -1
1576            * 10 % -3 = 1
1577            * -10 % -3 = -1
1578
1579        Args:
1580          target: the target expression.
1581          expr: the expression to compute the modulo of.
1582          mod: the modulus expression.
1583
1584        Returns:
1585          A `Constraint` object.
1586        """
1587        ct = Constraint(self)
1588        model_ct = self.__model.constraints[ct.index]
1589        model_ct.int_mod.exprs.append(self.parse_linear_expression(expr))
1590        model_ct.int_mod.exprs.append(self.parse_linear_expression(mod))
1591        model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target))
1592        return ct

Adds target = expr % mod.

It uses the C convention, that is the result is the remainder of the integral division rounded towards 0.

For example:
* 10 % 3 = 1
* -10 % 3 = -1
* 10 % -3 = 1
* -10 % -3 = -1
Arguments:
  • target: the target expression.
  • expr: the expression to compute the modulo of.
  • mod: the modulus expression.
Returns:

A Constraint object.

def AddMultiplicationEquality( self, target: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], *expressions: Union[Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1594    def add_multiplication_equality(
1595        self,
1596        target: LinearExprT,
1597        *expressions: Union[Iterable[LinearExprT], LinearExprT],
1598    ) -> Constraint:
1599        """Adds `target == expressions[0] * .. * expressions[n]`."""
1600        ct = Constraint(self)
1601        model_ct = self.__model.constraints[ct.index]
1602        model_ct.int_prod.exprs.extend(
1603            [
1604                self.parse_linear_expression(expr)
1605                for expr in expand_generator_or_tuple(expressions)
1606            ]
1607        )
1608        model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target))
1609        return ct

Adds target == expressions[0] * .. * expressions[n].

def NewIntervalVar( self, start: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], size: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], end: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], name: str) -> IntervalVar:
1613    def new_interval_var(
1614        self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str
1615    ) -> IntervalVar:
1616        """Creates an interval variable from start, size, and end.
1617
1618        An interval variable is a constraint, that is itself used in other
1619        constraints like NoOverlap.
1620
1621        Internally, it ensures that `start + size == end`.
1622
1623        Args:
1624          start: The start of the interval. It must be of the form a * var + b.
1625          size: The size of the interval. It must be of the form a * var + b.
1626          end: The end of the interval. It must be of the form a * var + b.
1627          name: The name of the interval variable.
1628
1629        Returns:
1630          An `IntervalVar` object.
1631        """
1632
1633        start_expr = self.parse_linear_expression(start)
1634        size_expr = self.parse_linear_expression(size)
1635        end_expr = self.parse_linear_expression(end)
1636        if len(start_expr.vars) > 1:
1637            raise TypeError(
1638                "cp_model.new_interval_var: start must be 1-var affine or constant."
1639            )
1640        if len(size_expr.vars) > 1:
1641            raise TypeError(
1642                "cp_model.new_interval_var: size must be 1-var affine or constant."
1643            )
1644        if len(end_expr.vars) > 1:
1645            raise TypeError(
1646                "cp_model.new_interval_var: end must be 1-var affine or constant."
1647            )
1648        return IntervalVar(
1649            self.__model,
1650            self.__var_list,
1651            start_expr,
1652            size_expr,
1653            end_expr,
1654            None,
1655            name,
1656        )

Creates an interval variable from start, size, and end.

An interval variable is a constraint, that is itself used in other constraints like NoOverlap.

Internally, it ensures that start + size == end.

Arguments:
  • start: The start of the interval. It must be of the form a * var + b.
  • size: The size of the interval. It must be of the form a * var + b.
  • end: The end of the interval. It must be of the form a * var + b.
  • name: The name of the interval variable.
Returns:

An IntervalVar object.

def NewIntervalVarSeries( self, name: str, index: pandas.core.indexes.base.Index, starts: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], sizes: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], ends: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series]) -> pandas.core.series.Series:
1658    def new_interval_var_series(
1659        self,
1660        name: str,
1661        index: pd.Index,
1662        starts: Union[LinearExprT, pd.Series],
1663        sizes: Union[LinearExprT, pd.Series],
1664        ends: Union[LinearExprT, pd.Series],
1665    ) -> pd.Series:
1666        """Creates a series of interval variables with the given name.
1667
1668        Args:
1669          name (str): Required. The name of the variable set.
1670          index (pd.Index): Required. The index to use for the variable set.
1671          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1672            set. If a `pd.Series` is passed in, it will be based on the
1673            corresponding values of the pd.Series.
1674          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1675            set. If a `pd.Series` is passed in, it will be based on the
1676            corresponding values of the pd.Series.
1677          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1678            set. If a `pd.Series` is passed in, it will be based on the
1679            corresponding values of the pd.Series.
1680
1681        Returns:
1682          pd.Series: The interval variable set indexed by its corresponding
1683          dimensions.
1684
1685        Raises:
1686          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1687          ValueError: if the `name` is not a valid identifier or already exists.
1688          ValueError: if the all the indexes do not match.
1689        """
1690        if not isinstance(index, pd.Index):
1691            raise TypeError("Non-index object is used as index")
1692        if not name.isidentifier():
1693            raise ValueError(f"name={name!r} is not a valid identifier")
1694
1695        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1696        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
1697        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
1698        interval_array = []
1699        for i in index:
1700            interval_array.append(
1701                self.new_interval_var(
1702                    start=starts[i],
1703                    size=sizes[i],
1704                    end=ends[i],
1705                    name=f"{name}[{i}]",
1706                )
1707            )
1708        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • starts (Union[LinearExprT, pd.Series]): The start of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • sizes (Union[LinearExprT, pd.Series]): The size of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • ends (Union[LinearExprT, pd.Series]): The ends of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The interval variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the all the indexes do not match.
def NewFixedSizeIntervalVar( self, start: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], size: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], name: str) -> IntervalVar:
1710    def new_fixed_size_interval_var(
1711        self, start: LinearExprT, size: IntegralT, name: str
1712    ) -> IntervalVar:
1713        """Creates an interval variable from start, and a fixed size.
1714
1715        An interval variable is a constraint, that is itself used in other
1716        constraints like NoOverlap.
1717
1718        Args:
1719          start: The start of the interval. It must be of the form a * var + b.
1720          size: The size of the interval. It must be an integer value.
1721          name: The name of the interval variable.
1722
1723        Returns:
1724          An `IntervalVar` object.
1725        """
1726        start_expr = self.parse_linear_expression(start)
1727        size_expr = self.parse_linear_expression(size)
1728        end_expr = self.parse_linear_expression(start + size)
1729        if len(start_expr.vars) > 1:
1730            raise TypeError(
1731                "cp_model.new_interval_var: start must be affine or constant."
1732            )
1733        return IntervalVar(
1734            self.__model,
1735            self.__var_list,
1736            start_expr,
1737            size_expr,
1738            end_expr,
1739            None,
1740            name,
1741        )

Creates an interval variable from start, and a fixed size.

An interval variable is a constraint, that is itself used in other constraints like NoOverlap.

Arguments:
  • start: The start of the interval. It must be of the form a * var + b.
  • size: The size of the interval. It must be an integer value.
  • name: The name of the interval variable.
Returns:

An IntervalVar object.

def NewOptionalIntervalVar( self, start: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], size: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], end: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], is_present: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], name: str) -> IntervalVar:
1789    def new_optional_interval_var(
1790        self,
1791        start: LinearExprT,
1792        size: LinearExprT,
1793        end: LinearExprT,
1794        is_present: LiteralT,
1795        name: str,
1796    ) -> IntervalVar:
1797        """Creates an optional interval var from start, size, end, and is_present.
1798
1799        An optional interval variable is a constraint, that is itself used in other
1800        constraints like NoOverlap. This constraint is protected by a presence
1801        literal that indicates if it is active or not.
1802
1803        Internally, it ensures that `is_present` implies `start + size ==
1804        end`.
1805
1806        Args:
1807          start: The start of the interval. It must be of the form a * var + b.
1808          size: The size of the interval. It must be of the form a * var + b.
1809          end: The end of the interval. It must be of the form a * var + b.
1810          is_present: A literal that indicates if the interval is active or not. A
1811            inactive interval is simply ignored by all constraints.
1812          name: The name of the interval variable.
1813
1814        Returns:
1815          An `IntervalVar` object.
1816        """
1817
1818        # Creates the IntervalConstraintProto object.
1819        is_present_index = self.get_or_make_boolean_index(is_present)
1820        start_expr = self.parse_linear_expression(start)
1821        size_expr = self.parse_linear_expression(size)
1822        end_expr = self.parse_linear_expression(end)
1823        if len(start_expr.vars) > 1:
1824            raise TypeError(
1825                "cp_model.new_interval_var: start must be affine or constant."
1826            )
1827        if len(size_expr.vars) > 1:
1828            raise TypeError(
1829                "cp_model.new_interval_var: size must be affine or constant."
1830            )
1831        if len(end_expr.vars) > 1:
1832            raise TypeError(
1833                "cp_model.new_interval_var: end must be affine or constant."
1834            )
1835        return IntervalVar(
1836            self.__model,
1837            self.__var_list,
1838            start_expr,
1839            size_expr,
1840            end_expr,
1841            is_present_index,
1842            name,
1843        )

Creates an optional interval var from start, size, end, and is_present.

An optional interval variable is a constraint, that is itself used in other constraints like NoOverlap. This constraint is protected by a presence literal that indicates if it is active or not.

Internally, it ensures that is_present implies start + size == end.

Arguments:
  • start: The start of the interval. It must be of the form a * var + b.
  • size: The size of the interval. It must be of the form a * var + b.
  • end: The end of the interval. It must be of the form a * var + b.
  • is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints.
  • name: The name of the interval variable.
Returns:

An IntervalVar object.

def NewOptionalIntervalVarSeries( self, name: str, index: pandas.core.indexes.base.Index, starts: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], sizes: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], ends: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], are_present: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool, pandas.core.series.Series]) -> pandas.core.series.Series:
1845    def new_optional_interval_var_series(
1846        self,
1847        name: str,
1848        index: pd.Index,
1849        starts: Union[LinearExprT, pd.Series],
1850        sizes: Union[LinearExprT, pd.Series],
1851        ends: Union[LinearExprT, pd.Series],
1852        are_present: Union[LiteralT, pd.Series],
1853    ) -> pd.Series:
1854        """Creates a series of interval variables with the given name.
1855
1856        Args:
1857          name (str): Required. The name of the variable set.
1858          index (pd.Index): Required. The index to use for the variable set.
1859          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1860            set. If a `pd.Series` is passed in, it will be based on the
1861            corresponding values of the pd.Series.
1862          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1863            set. If a `pd.Series` is passed in, it will be based on the
1864            corresponding values of the pd.Series.
1865          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1866            set. If a `pd.Series` is passed in, it will be based on the
1867            corresponding values of the pd.Series.
1868          are_present (Union[LiteralT, pd.Series]): The performed literal of each
1869            interval in the set. If a `pd.Series` is passed in, it will be based on
1870            the corresponding values of the pd.Series.
1871
1872        Returns:
1873          pd.Series: The interval variable set indexed by its corresponding
1874          dimensions.
1875
1876        Raises:
1877          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1878          ValueError: if the `name` is not a valid identifier or already exists.
1879          ValueError: if the all the indexes do not match.
1880        """
1881        if not isinstance(index, pd.Index):
1882            raise TypeError("Non-index object is used as index")
1883        if not name.isidentifier():
1884            raise ValueError(f"name={name!r} is not a valid identifier")
1885
1886        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1887        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
1888        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
1889        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
1890
1891        interval_array = []
1892        for i in index:
1893            interval_array.append(
1894                self.new_optional_interval_var(
1895                    start=starts[i],
1896                    size=sizes[i],
1897                    end=ends[i],
1898                    is_present=are_present[i],
1899                    name=f"{name}[{i}]",
1900                )
1901            )
1902        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • starts (Union[LinearExprT, pd.Series]): The start of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • sizes (Union[LinearExprT, pd.Series]): The size of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • ends (Union[LinearExprT, pd.Series]): The ends of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • are_present (Union[LiteralT, pd.Series]): The performed literal of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The interval variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the all the indexes do not match.
def NewOptionalFixedSizeIntervalVar( self, start: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], size: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], is_present: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], name: str) -> IntervalVar:
1904    def new_optional_fixed_size_interval_var(
1905        self,
1906        start: LinearExprT,
1907        size: IntegralT,
1908        is_present: LiteralT,
1909        name: str,
1910    ) -> IntervalVar:
1911        """Creates an interval variable from start, and a fixed size.
1912
1913        An interval variable is a constraint, that is itself used in other
1914        constraints like NoOverlap.
1915
1916        Args:
1917          start: The start of the interval. It must be of the form a * var + b.
1918          size: The size of the interval. It must be an integer value.
1919          is_present: A literal that indicates if the interval is active or not. A
1920            inactive interval is simply ignored by all constraints.
1921          name: The name of the interval variable.
1922
1923        Returns:
1924          An `IntervalVar` object.
1925        """
1926        start_expr = self.parse_linear_expression(start)
1927        size_expr = self.parse_linear_expression(size)
1928        end_expr = self.parse_linear_expression(start + size)
1929        if len(start_expr.vars) > 1:
1930            raise TypeError(
1931                "cp_model.new_interval_var: start must be affine or constant."
1932            )
1933        is_present_index = self.get_or_make_boolean_index(is_present)
1934        return IntervalVar(
1935            self.__model,
1936            self.__var_list,
1937            start_expr,
1938            size_expr,
1939            end_expr,
1940            is_present_index,
1941            name,
1942        )

Creates an interval variable from start, and a fixed size.

An interval variable is a constraint, that is itself used in other constraints like NoOverlap.

Arguments:
  • start: The start of the interval. It must be of the form a * var + b.
  • size: The size of the interval. It must be an integer value.
  • is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints.
  • name: The name of the interval variable.
Returns:

An IntervalVar object.

def NewOptionalFixedSizeIntervalVarSeries( self, name: str, index: pandas.core.indexes.base.Index, starts: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], sizes: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, pandas.core.series.Series], are_present: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool, pandas.core.series.Series]) -> pandas.core.series.Series:
1944    def new_optional_fixed_size_interval_var_series(
1945        self,
1946        name: str,
1947        index: pd.Index,
1948        starts: Union[LinearExprT, pd.Series],
1949        sizes: Union[IntegralT, pd.Series],
1950        are_present: Union[LiteralT, pd.Series],
1951    ) -> pd.Series:
1952        """Creates a series of interval variables with the given name.
1953
1954        Args:
1955          name (str): Required. The name of the variable set.
1956          index (pd.Index): Required. The index to use for the variable set.
1957          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1958            set. If a `pd.Series` is passed in, it will be based on the
1959            corresponding values of the pd.Series.
1960          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1961            the set. If a `pd.Series` is passed in, it will be based on the
1962            corresponding values of the pd.Series.
1963          are_present (Union[LiteralT, pd.Series]): The performed literal of each
1964            interval in the set. If a `pd.Series` is passed in, it will be based on
1965            the corresponding values of the pd.Series.
1966
1967        Returns:
1968          pd.Series: The interval variable set indexed by its corresponding
1969          dimensions.
1970
1971        Raises:
1972          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1973          ValueError: if the `name` is not a valid identifier or already exists.
1974          ValueError: if the all the indexes do not match.
1975        """
1976        if not isinstance(index, pd.Index):
1977            raise TypeError("Non-index object is used as index")
1978        if not name.isidentifier():
1979            raise ValueError(f"name={name!r} is not a valid identifier")
1980
1981        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
1982        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
1983        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
1984        interval_array = []
1985        for i in index:
1986            interval_array.append(
1987                self.new_optional_fixed_size_interval_var(
1988                    start=starts[i],
1989                    size=sizes[i],
1990                    is_present=are_present[i],
1991                    name=f"{name}[{i}]",
1992                )
1993            )
1994        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • starts (Union[LinearExprT, pd.Series]): The start of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
  • are_present (Union[LiteralT, pd.Series]): The performed literal of each interval in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series.
Returns:

pd.Series: The interval variable set indexed by its corresponding dimensions.

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the all the indexes do not match.
def AddNoOverlap( self, interval_vars: Iterable[IntervalVar]) -> Constraint:
1996    def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint:
1997        """Adds NoOverlap(interval_vars).
1998
1999        A NoOverlap constraint ensures that all present intervals do not overlap
2000        in time.
2001
2002        Args:
2003          interval_vars: The list of interval variables to constrain.
2004
2005        Returns:
2006          An instance of the `Constraint` class.
2007        """
2008        ct = Constraint(self)
2009        model_ct = self.__model.constraints[ct.index]
2010        model_ct.no_overlap.intervals.extend(
2011            [self.get_interval_index(x) for x in interval_vars]
2012        )
2013        return ct

Adds NoOverlap(interval_vars).

A NoOverlap constraint ensures that all present intervals do not overlap in time.

Arguments:
  • interval_vars: The list of interval variables to constrain.
Returns:

An instance of the Constraint class.

def AddNoOverlap2D( self, x_intervals: Iterable[IntervalVar], y_intervals: Iterable[IntervalVar]) -> Constraint:
2015    def add_no_overlap_2d(
2016        self,
2017        x_intervals: Iterable[IntervalVar],
2018        y_intervals: Iterable[IntervalVar],
2019    ) -> Constraint:
2020        """Adds NoOverlap2D(x_intervals, y_intervals).
2021
2022        A NoOverlap2D constraint ensures that all present rectangles do not overlap
2023        on a plane. Each rectangle is aligned with the X and Y axis, and is defined
2024        by two intervals which represent its projection onto the X and Y axis.
2025
2026        Furthermore, one box is optional if at least one of the x or y interval is
2027        optional.
2028
2029        Args:
2030          x_intervals: The X coordinates of the rectangles.
2031          y_intervals: The Y coordinates of the rectangles.
2032
2033        Returns:
2034          An instance of the `Constraint` class.
2035        """
2036        ct = Constraint(self)
2037        model_ct = self.__model.constraints[ct.index]
2038        model_ct.no_overlap_2d.x_intervals.extend(
2039            [self.get_interval_index(x) for x in x_intervals]
2040        )
2041        model_ct.no_overlap_2d.y_intervals.extend(
2042            [self.get_interval_index(x) for x in y_intervals]
2043        )
2044        return ct

Adds NoOverlap2D(x_intervals, y_intervals).

A NoOverlap2D constraint ensures that all present rectangles do not overlap on a plane. Each rectangle is aligned with the X and Y axis, and is defined by two intervals which represent its projection onto the X and Y axis.

Furthermore, one box is optional if at least one of the x or y interval is optional.

Arguments:
  • x_intervals: The X coordinates of the rectangles.
  • y_intervals: The Y coordinates of the rectangles.
Returns:

An instance of the Constraint class.

def AddCumulative( self, intervals: Iterable[IntervalVar], demands: Iterable[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], capacity: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
2046    def add_cumulative(
2047        self,
2048        intervals: Iterable[IntervalVar],
2049        demands: Iterable[LinearExprT],
2050        capacity: LinearExprT,
2051    ) -> Constraint:
2052        """Adds Cumulative(intervals, demands, capacity).
2053
2054        This constraint enforces that:
2055
2056            for all t:
2057              sum(demands[i]
2058                if (start(intervals[i]) <= t < end(intervals[i])) and
2059                (intervals[i] is present)) <= capacity
2060
2061        Args:
2062          intervals: The list of intervals.
2063          demands: The list of demands for each interval. Each demand must be >= 0.
2064            Each demand can be a 1-var affine expression (a * x + b).
2065          capacity: The maximum capacity of the cumulative constraint. It can be a
2066            1-var affine expression (a * x + b).
2067
2068        Returns:
2069          An instance of the `Constraint` class.
2070        """
2071        cumulative = Constraint(self)
2072        model_ct = self.__model.constraints[cumulative.index]
2073        model_ct.cumulative.intervals.extend(
2074            [self.get_interval_index(x) for x in intervals]
2075        )
2076        for d in demands:
2077            model_ct.cumulative.demands.append(self.parse_linear_expression(d))
2078        model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity))
2079        return cumulative

Adds Cumulative(intervals, demands, capacity).

This constraint enforces that:

for all t: sum(demands[i] if (start(intervals[i]) <= t < end(intervals[i])) and (intervals[i] is present)) <= capacity

Arguments:
  • intervals: The list of intervals.
  • demands: The list of demands for each interval. Each demand must be >= 0. Each demand can be a 1-var affine expression (a * x + b).
  • capacity: The maximum capacity of the cumulative constraint. It can be a 1-var affine expression (a * x + b).
Returns:

An instance of the Constraint class.

def Clone(self) -> CpModel:
2082    def clone(self) -> "CpModel":
2083        """Reset the model, and creates a new one from a CpModelProto instance."""
2084        clone = CpModel()
2085        clone.proto.CopyFrom(self.proto)
2086        clone.rebuild_var_and_constant_map()
2087        return clone

Reset the model, and creates a new one from a CpModelProto instance.

def GetBoolVarFromProtoIndex(self, index: int) -> IntVar:
2099    def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2100        """Returns an already created Boolean variable from its index."""
2101        result = self._get_int_var(index)
2102        if not result.is_boolean:
2103            raise ValueError(
2104                f"get_bool_var_from_proto_index: index {index} does not reference a"
2105                " boolean variable"
2106            )
2107        return result

Returns an already created Boolean variable from its index.

def GetIntVarFromProtoIndex(self, index: int) -> IntVar:
2109    def get_int_var_from_proto_index(self, index: int) -> IntVar:
2110        """Returns an already created integer variable from its index."""
2111        return self._get_int_var(index)

Returns an already created integer variable from its index.

def GetIntervalVarFromProtoIndex(self, index: int) -> IntervalVar:
2113    def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2114        """Returns an already created interval variable from its index."""
2115        if index < 0 or index >= len(self.__model.constraints):
2116            raise ValueError(
2117                f"get_interval_var_from_proto_index: out of bound index {index}"
2118            )
2119        ct = self.__model.constraints[index]
2120        if not ct.HasField("interval"):
2121            raise ValueError(
2122                f"get_interval_var_from_proto_index: index {index} does not"
2123                " reference an" + " interval variable"
2124            )
2125
2126        return IntervalVar(self.__model, self.__var_list, index, None, None, None, None)

Returns an already created interval variable from its index.

def Minimize( self, obj: Union[LinearExpr, int, float, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, numpy.float64]):
2239    def minimize(self, obj: ObjLinearExprT):
2240        """Sets the objective of the model to minimize(obj)."""
2241        self._set_objective(obj, minimize=True)

Sets the objective of the model to minimize(obj).

def Maximize( self, obj: Union[LinearExpr, int, float, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, numpy.float64]):
2243    def maximize(self, obj: ObjLinearExprT):
2244        """Sets the objective of the model to maximize(obj)."""
2245        self._set_objective(obj, minimize=False)

Sets the objective of the model to maximize(obj).

def HasObjective(self) -> bool:
2247    def has_objective(self) -> bool:
2248        return self.__model.HasField("objective") or self.__model.HasField(
2249            "floating_point_objective"
2250        )
def ClearObjective(self):
2252    def clear_objective(self):
2253        self.__model.ClearField("objective")
2254        self.__model.ClearField("floating_point_objective")
def AddDecisionStrategy( self, variables: Sequence[IntVar], var_strategy: <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object>, domain_strategy: <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object>) -> None:
2256    def add_decision_strategy(
2257        self,
2258        variables: Sequence[IntVar],
2259        var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy,
2260        domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy,
2261    ) -> None:
2262        """Adds a search strategy to the model.
2263
2264        Args:
2265          variables: a list of variables this strategy will assign.
2266          var_strategy: heuristic to choose the next variable to assign.
2267          domain_strategy: heuristic to reduce the domain of the selected variable.
2268            Currently, this is advanced code: the union of all strategies added to
2269            the model must be complete, i.e. instantiates all variables. Otherwise,
2270            solve() will fail.
2271        """
2272
2273        strategy: cp_model_pb2.DecisionStrategyProto = (
2274            self.__model.search_strategy.add()
2275        )
2276        for v in variables:
2277            expr = strategy.exprs.add()
2278            if v.index >= 0:
2279                expr.vars.append(v.index)
2280                expr.coeffs.append(1)
2281            else:
2282                expr.vars.append(self.negated(v.index))
2283                expr.coeffs.append(-1)
2284                expr.offset = 1
2285
2286        strategy.variable_selection_strategy = var_strategy
2287        strategy.domain_reduction_strategy = domain_strategy

Adds a search strategy to the model.

Arguments:
  • variables: a list of variables this strategy will assign.
  • var_strategy: heuristic to choose the next variable to assign.
  • domain_strategy: heuristic to reduce the domain of the selected variable. Currently, this is advanced code: the union of all strategies added to the model must be complete, i.e. instantiates all variables. Otherwise, solve() will fail.
def ModelStats(self) -> str:
2289    def model_stats(self) -> str:
2290        """Returns a string containing some model statistics."""
2291        return cmh.CpSatHelper.model_stats(self.__model)

Returns a string containing some model statistics.

def Validate(self) -> str:
2293    def validate(self) -> str:
2294        """Returns a string indicating that the model is invalid."""
2295        return cmh.CpSatHelper.validate_model(self.__model)

Returns a string indicating that the model is invalid.

def ExportToFile(self, file: str) -> bool:
2297    def export_to_file(self, file: str) -> bool:
2298        """Write the model as a protocol buffer to 'file'.
2299
2300        Args:
2301          file: file to write the model to. If the filename ends with 'txt', the
2302            model will be written as a text file, otherwise, the binary format will
2303            be used.
2304
2305        Returns:
2306          True if the model was correctly written.
2307        """
2308        return cmh.CpSatHelper.write_model_to_file(self.__model, file)

Write the model as a protocol buffer to 'file'.

Arguments:
  • file: file to write the model to. If the filename ends with 'txt', the model will be written as a text file, otherwise, the binary format will be used.
Returns:

True if the model was correctly written.

def AddHint(self, var, value) -> None:
2324    def add_hint(self, var, value) -> None:
2325        """Adds 'var == value' as a hint to the solver."""
2326        if var.index >= 0:
2327            self.__model.solution_hint.vars.append(self.get_or_make_index(var))
2328            self.__model.solution_hint.values.append(int(value))
2329        else:
2330            self.__model.solution_hint.vars.append(self.negated(var.index))
2331            self.__model.solution_hint.values.append(int(not value))

Adds 'var == value' as a hint to the solver.

def ClearHints(self):
2333    def clear_hints(self):
2334        """Removes any solution hint from the model."""
2335        self.__model.ClearField("solution_hint")

Removes any solution hint from the model.

def AddAssumption( self, lit: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> None:
2337    def add_assumption(self, lit: LiteralT) -> None:
2338        """Adds the literal to the model as assumptions."""
2339        self.__model.assumptions.append(self.get_or_make_boolean_index(lit))

Adds the literal to the model as assumptions.

def AddAssumptions( self, literals: Iterable[Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]) -> None:
2341    def add_assumptions(self, literals: Iterable[LiteralT]) -> None:
2342        """Adds the literals to the model as assumptions."""
2343        for lit in literals:
2344            self.add_assumption(lit)

Adds the literals to the model as assumptions.

def ClearAssumptions(self) -> None:
2346    def clear_assumptions(self) -> None:
2347        """Removes all assumptions from the model."""
2348        self.__model.ClearField("assumptions")

Removes all assumptions from the model.

def expand_generator_or_tuple(args):
2450def expand_generator_or_tuple(args):
2451    if hasattr(args, "__len__"):  # Tuple
2452        if len(args) != 1:
2453            return args
2454        if isinstance(args[0], (NumberTypes, LinearExpr)):
2455            return args
2456    # Generator
2457    return args[0]
class CpSolver:
2460class CpSolver:
2461    """Main solver class.
2462
2463    The purpose of this class is to search for a solution to the model provided
2464    to the solve() method.
2465
2466    Once solve() is called, this class allows inspecting the solution found
2467    with the value() and boolean_value() methods, as well as general statistics
2468    about the solve procedure.
2469    """
2470
2471    def __init__(self) -> None:
2472        self.__response_wrapper: Optional[cmh.ResponseWrapper] = None
2473        self.parameters: sat_parameters_pb2.SatParameters = (
2474            sat_parameters_pb2.SatParameters()
2475        )
2476        self.log_callback: Optional[Callable[[str], None]] = None
2477        self.best_bound_callback: Optional[Callable[[float], None]] = None
2478        self.__solve_wrapper: Optional[cmh.SolveWrapper] = None
2479        self.__lock: threading.Lock = threading.Lock()
2480
2481    def solve(
2482        self,
2483        model: CpModel,
2484        solution_callback: Optional["CpSolverSolutionCallback"] = None,
2485    ) -> cp_model_pb2.CpSolverStatus:
2486        """Solves a problem and passes each solution to the callback if not null."""
2487        with self.__lock:
2488            self.__solve_wrapper = cmh.SolveWrapper()
2489
2490        self.__solve_wrapper.set_parameters(self.parameters)
2491        if solution_callback is not None:
2492            self.__solve_wrapper.add_solution_callback(solution_callback)
2493
2494        if self.log_callback is not None:
2495            self.__solve_wrapper.add_log_callback(self.log_callback)
2496
2497        if self.best_bound_callback is not None:
2498            self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback)
2499
2500        self.__response_wrapper = (
2501            self.__solve_wrapper.solve_and_return_response_wrapper(model.proto)
2502        )
2503
2504        if solution_callback is not None:
2505            self.__solve_wrapper.clear_solution_callback(solution_callback)
2506
2507        with self.__lock:
2508            self.__solve_wrapper = None
2509
2510        return self.__response_wrapper.status()
2511
2512    def stop_search(self) -> None:
2513        """Stops the current search asynchronously."""
2514        with self.__lock:
2515            if self.__solve_wrapper:
2516                self.__solve_wrapper.stop_search()
2517
2518    def value(self, expression: LinearExprT) -> int:
2519        """Returns the value of a linear expression after solve."""
2520        return self._checked_response.value(expression)
2521
2522    def values(self, variables: _IndexOrSeries) -> pd.Series:
2523        """Returns the values of the input variables.
2524
2525        If `variables` is a `pd.Index`, then the output will be indexed by the
2526        variables. If `variables` is a `pd.Series` indexed by the underlying
2527        dimensions, then the output will be indexed by the same underlying
2528        dimensions.
2529
2530        Args:
2531          variables (Union[pd.Index, pd.Series]): The set of variables from which to
2532            get the values.
2533
2534        Returns:
2535          pd.Series: The values of all variables in the set.
2536
2537        Raises:
2538          RuntimeError: if solve() has not been called.
2539        """
2540        if self.__response_wrapper is None:
2541            raise RuntimeError("solve() has not been called.")
2542        return pd.Series(
2543            data=[self.__response_wrapper.value(var) for var in variables],
2544            index=_get_index(variables),
2545        )
2546
2547    def float_value(self, expression: LinearExprT) -> float:
2548        """Returns the value of a linear expression after solve."""
2549        return self._checked_response.float_value(expression)
2550
2551    def float_values(self, expressions: _IndexOrSeries) -> pd.Series:
2552        """Returns the float values of the input linear expressions.
2553
2554        If `expressions` is a `pd.Index`, then the output will be indexed by the
2555        variables. If `variables` is a `pd.Series` indexed by the underlying
2556        dimensions, then the output will be indexed by the same underlying
2557        dimensions.
2558
2559        Args:
2560          expressions (Union[pd.Index, pd.Series]): The set of expressions from
2561            which to get the values.
2562
2563        Returns:
2564          pd.Series: The values of all variables in the set.
2565
2566        Raises:
2567          RuntimeError: if solve() has not been called.
2568        """
2569        if self.__response_wrapper is None:
2570            raise RuntimeError("solve() has not been called.")
2571        return pd.Series(
2572            data=[self.__response_wrapper.float_value(expr) for expr in expressions],
2573            index=_get_index(expressions),
2574        )
2575
2576    def boolean_value(self, literal: LiteralT) -> bool:
2577        """Returns the boolean value of a literal after solve."""
2578        return self._checked_response.boolean_value(literal)
2579
2580    def boolean_values(self, variables: _IndexOrSeries) -> pd.Series:
2581        """Returns the values of the input variables.
2582
2583        If `variables` is a `pd.Index`, then the output will be indexed by the
2584        variables. If `variables` is a `pd.Series` indexed by the underlying
2585        dimensions, then the output will be indexed by the same underlying
2586        dimensions.
2587
2588        Args:
2589          variables (Union[pd.Index, pd.Series]): The set of variables from which to
2590            get the values.
2591
2592        Returns:
2593          pd.Series: The values of all variables in the set.
2594
2595        Raises:
2596          RuntimeError: if solve() has not been called.
2597        """
2598        if self.__response_wrapper is None:
2599            raise RuntimeError("solve() has not been called.")
2600        return pd.Series(
2601            data=[
2602                self.__response_wrapper.boolean_value(literal) for literal in variables
2603            ],
2604            index=_get_index(variables),
2605        )
2606
2607    @property
2608    def objective_value(self) -> float:
2609        """Returns the value of the objective after solve."""
2610        return self._checked_response.objective_value()
2611
2612    @property
2613    def best_objective_bound(self) -> float:
2614        """Returns the best lower (upper) bound found when min(max)imizing."""
2615        return self._checked_response.best_objective_bound()
2616
2617    @property
2618    def num_booleans(self) -> int:
2619        """Returns the number of boolean variables managed by the SAT solver."""
2620        return self._checked_response.num_booleans()
2621
2622    @property
2623    def num_conflicts(self) -> int:
2624        """Returns the number of conflicts since the creation of the solver."""
2625        return self._checked_response.num_conflicts()
2626
2627    @property
2628    def num_branches(self) -> int:
2629        """Returns the number of search branches explored by the solver."""
2630        return self._checked_response.num_branches()
2631
2632    @property
2633    def wall_time(self) -> float:
2634        """Returns the wall time in seconds since the creation of the solver."""
2635        return self._checked_response.wall_time()
2636
2637    @property
2638    def user_time(self) -> float:
2639        """Returns the user time in seconds since the creation of the solver."""
2640        return self._checked_response.user_time()
2641
2642    @property
2643    def response_proto(self) -> cp_model_pb2.CpSolverResponse:
2644        """Returns the response object."""
2645        return self._checked_response.response()
2646
2647    def response_stats(self) -> str:
2648        """Returns some statistics on the solution found as a string."""
2649        return self._checked_response.response_stats()
2650
2651    def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]:
2652        """Returns the indices of the infeasible assumptions."""
2653        return self._checked_response.sufficient_assumptions_for_infeasibility()
2654
2655    def status_name(self, status: Optional[Any] = None) -> str:
2656        """Returns the name of the status returned by solve()."""
2657        if status is None:
2658            status = self._checked_response.status()
2659        return cp_model_pb2.CpSolverStatus.Name(status)
2660
2661    def solution_info(self) -> str:
2662        """Returns some information on the solve process.
2663
2664        Returns some information on how the solution was found, or the reason
2665        why the model or the parameters are invalid.
2666
2667        Raises:
2668          RuntimeError: if solve() has not been called.
2669        """
2670        return self._checked_response.solution_info()
2671
2672    @property
2673    def _checked_response(self) -> cmh.ResponseWrapper:
2674        """Checks solve() has been called, and returns a response wrapper."""
2675        if self.__response_wrapper is None:
2676            raise RuntimeError("solve() has not been called.")
2677        return self.__response_wrapper
2678
2679    # Compatibility with pre PEP8
2680    # pylint: disable=invalid-name
2681
2682    def BestObjectiveBound(self) -> float:
2683        return self.best_objective_bound
2684
2685    def BooleanValue(self, literal: LiteralT) -> bool:
2686        return self.boolean_value(literal)
2687
2688    def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series:
2689        return self.boolean_values(variables)
2690
2691    def NumBooleans(self) -> int:
2692        return self.num_booleans
2693
2694    def NumConflicts(self) -> int:
2695        return self.num_conflicts
2696
2697    def NumBranches(self) -> int:
2698        return self.num_branches
2699
2700    def ObjectiveValue(self) -> float:
2701        return self.objective_value
2702
2703    def ResponseProto(self) -> cp_model_pb2.CpSolverResponse:
2704        return self.response_proto
2705
2706    def ResponseStats(self) -> str:
2707        return self.response_stats()
2708
2709    def Solve(
2710        self,
2711        model: CpModel,
2712        solution_callback: Optional["CpSolverSolutionCallback"] = None,
2713    ) -> cp_model_pb2.CpSolverStatus:
2714        return self.solve(model, solution_callback)
2715
2716    def SolutionInfo(self) -> str:
2717        return self.solution_info()
2718
2719    def StatusName(self, status: Optional[Any] = None) -> str:
2720        return self.status_name(status)
2721
2722    def StopSearch(self) -> None:
2723        self.stop_search()
2724
2725    def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]:
2726        return self.sufficient_assumptions_for_infeasibility()
2727
2728    def UserTime(self) -> float:
2729        return self.user_time
2730
2731    def Value(self, expression: LinearExprT) -> int:
2732        return self.value(expression)
2733
2734    def Values(self, variables: _IndexOrSeries) -> pd.Series:
2735        return self.values(variables)
2736
2737    def WallTime(self) -> float:
2738        return self.wall_time
2739
2740    def SolveWithSolutionCallback(
2741        self, model: CpModel, callback: "CpSolverSolutionCallback"
2742    ) -> cp_model_pb2.CpSolverStatus:
2743        """DEPRECATED Use solve() with the callback argument."""
2744        warnings.warn(
2745            "solve_with_solution_callback is deprecated; use solve() with"
2746            + "the callback argument.",
2747            DeprecationWarning,
2748        )
2749        return self.solve(model, callback)
2750
2751    def SearchForAllSolutions(
2752        self, model: CpModel, callback: "CpSolverSolutionCallback"
2753    ) -> cp_model_pb2.CpSolverStatus:
2754        """DEPRECATED Use solve() with the right parameter.
2755
2756        Search for all solutions of a satisfiability problem.
2757
2758        This method searches for all feasible solutions of a given model.
2759        Then it feeds the solution to the callback.
2760
2761        Note that the model cannot contain an objective.
2762
2763        Args:
2764          model: The model to solve.
2765          callback: The callback that will be called at each solution.
2766
2767        Returns:
2768          The status of the solve:
2769
2770          * *FEASIBLE* if some solutions have been found
2771          * *INFEASIBLE* if the solver has proved there are no solution
2772          * *OPTIMAL* if all solutions have been found
2773        """
2774        warnings.warn(
2775            "search_for_all_solutions is deprecated; use solve() with"
2776            + "enumerate_all_solutions = True.",
2777            DeprecationWarning,
2778        )
2779        if model.has_objective():
2780            raise TypeError(
2781                "Search for all solutions is only defined on satisfiability problems"
2782            )
2783        # Store old parameter.
2784        enumerate_all = self.parameters.enumerate_all_solutions
2785        self.parameters.enumerate_all_solutions = True
2786
2787        status: cp_model_pb2.CpSolverStatus = self.solve(model, callback)
2788
2789        # Restore parameter.
2790        self.parameters.enumerate_all_solutions = enumerate_all
2791        return status

Main solver class.

The purpose of this class is to search for a solution to the model provided to the solve() method.

Once solve() is called, this class allows inspecting the solution found with the value() and boolean_value() methods, as well as general statistics about the solve procedure.

log_callback: Optional[Callable[[str], NoneType]]
best_bound_callback: Optional[Callable[[float], NoneType]]
def solve( self, model: CpModel, solution_callback: Optional[CpSolverSolutionCallback] = None) -> <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7f8c8eb9cef0>:
2481    def solve(
2482        self,
2483        model: CpModel,
2484        solution_callback: Optional["CpSolverSolutionCallback"] = None,
2485    ) -> cp_model_pb2.CpSolverStatus:
2486        """Solves a problem and passes each solution to the callback if not null."""
2487        with self.__lock:
2488            self.__solve_wrapper = cmh.SolveWrapper()
2489
2490        self.__solve_wrapper.set_parameters(self.parameters)
2491        if solution_callback is not None:
2492            self.__solve_wrapper.add_solution_callback(solution_callback)
2493
2494        if self.log_callback is not None:
2495            self.__solve_wrapper.add_log_callback(self.log_callback)
2496
2497        if self.best_bound_callback is not None:
2498            self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback)
2499
2500        self.__response_wrapper = (
2501            self.__solve_wrapper.solve_and_return_response_wrapper(model.proto)
2502        )
2503
2504        if solution_callback is not None:
2505            self.__solve_wrapper.clear_solution_callback(solution_callback)
2506
2507        with self.__lock:
2508            self.__solve_wrapper = None
2509
2510        return self.__response_wrapper.status()

Solves a problem and passes each solution to the callback if not null.

def value( self, expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
2518    def value(self, expression: LinearExprT) -> int:
2519        """Returns the value of a linear expression after solve."""
2520        return self._checked_response.value(expression)

Returns the value of a linear expression after solve.

def values( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
2522    def values(self, variables: _IndexOrSeries) -> pd.Series:
2523        """Returns the values of the input variables.
2524
2525        If `variables` is a `pd.Index`, then the output will be indexed by the
2526        variables. If `variables` is a `pd.Series` indexed by the underlying
2527        dimensions, then the output will be indexed by the same underlying
2528        dimensions.
2529
2530        Args:
2531          variables (Union[pd.Index, pd.Series]): The set of variables from which to
2532            get the values.
2533
2534        Returns:
2535          pd.Series: The values of all variables in the set.
2536
2537        Raises:
2538          RuntimeError: if solve() has not been called.
2539        """
2540        if self.__response_wrapper is None:
2541            raise RuntimeError("solve() has not been called.")
2542        return pd.Series(
2543            data=[self.__response_wrapper.value(var) for var in variables],
2544            index=_get_index(variables),
2545        )

Returns the values of the input variables.

If variables is a pd.Index, then the output will be indexed by the variables. If variables is a pd.Series indexed by the underlying dimensions, then the output will be indexed by the same underlying dimensions.

Arguments:
  • variables (Union[pd.Index, pd.Series]): The set of variables from which to get the values.
Returns:

pd.Series: The values of all variables in the set.

Raises:
  • RuntimeError: if solve() has not been called.
def float_value( self, expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> float:
2547    def float_value(self, expression: LinearExprT) -> float:
2548        """Returns the value of a linear expression after solve."""
2549        return self._checked_response.float_value(expression)

Returns the value of a linear expression after solve.

def float_values( self, expressions: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
2551    def float_values(self, expressions: _IndexOrSeries) -> pd.Series:
2552        """Returns the float values of the input linear expressions.
2553
2554        If `expressions` is a `pd.Index`, then the output will be indexed by the
2555        variables. If `variables` is a `pd.Series` indexed by the underlying
2556        dimensions, then the output will be indexed by the same underlying
2557        dimensions.
2558
2559        Args:
2560          expressions (Union[pd.Index, pd.Series]): The set of expressions from
2561            which to get the values.
2562
2563        Returns:
2564          pd.Series: The values of all variables in the set.
2565
2566        Raises:
2567          RuntimeError: if solve() has not been called.
2568        """
2569        if self.__response_wrapper is None:
2570            raise RuntimeError("solve() has not been called.")
2571        return pd.Series(
2572            data=[self.__response_wrapper.float_value(expr) for expr in expressions],
2573            index=_get_index(expressions),
2574        )

Returns the float values of the input linear expressions.

If expressions is a pd.Index, then the output will be indexed by the variables. If variables is a pd.Series indexed by the underlying dimensions, then the output will be indexed by the same underlying dimensions.

Arguments:
  • expressions (Union[pd.Index, pd.Series]): The set of expressions from which to get the values.
Returns:

pd.Series: The values of all variables in the set.

Raises:
  • RuntimeError: if solve() has not been called.
def boolean_value( self, literal: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
2576    def boolean_value(self, literal: LiteralT) -> bool:
2577        """Returns the boolean value of a literal after solve."""
2578        return self._checked_response.boolean_value(literal)

Returns the boolean value of a literal after solve.

def boolean_values( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
2580    def boolean_values(self, variables: _IndexOrSeries) -> pd.Series:
2581        """Returns the values of the input variables.
2582
2583        If `variables` is a `pd.Index`, then the output will be indexed by the
2584        variables. If `variables` is a `pd.Series` indexed by the underlying
2585        dimensions, then the output will be indexed by the same underlying
2586        dimensions.
2587
2588        Args:
2589          variables (Union[pd.Index, pd.Series]): The set of variables from which to
2590            get the values.
2591
2592        Returns:
2593          pd.Series: The values of all variables in the set.
2594
2595        Raises:
2596          RuntimeError: if solve() has not been called.
2597        """
2598        if self.__response_wrapper is None:
2599            raise RuntimeError("solve() has not been called.")
2600        return pd.Series(
2601            data=[
2602                self.__response_wrapper.boolean_value(literal) for literal in variables
2603            ],
2604            index=_get_index(variables),
2605        )

Returns the values of the input variables.

If variables is a pd.Index, then the output will be indexed by the variables. If variables is a pd.Series indexed by the underlying dimensions, then the output will be indexed by the same underlying dimensions.

Arguments:
  • variables (Union[pd.Index, pd.Series]): The set of variables from which to get the values.
Returns:

pd.Series: The values of all variables in the set.

Raises:
  • RuntimeError: if solve() has not been called.
objective_value: float
2607    @property
2608    def objective_value(self) -> float:
2609        """Returns the value of the objective after solve."""
2610        return self._checked_response.objective_value()

Returns the value of the objective after solve.

best_objective_bound: float
2612    @property
2613    def best_objective_bound(self) -> float:
2614        """Returns the best lower (upper) bound found when min(max)imizing."""
2615        return self._checked_response.best_objective_bound()

Returns the best lower (upper) bound found when min(max)imizing.

num_booleans: int
2617    @property
2618    def num_booleans(self) -> int:
2619        """Returns the number of boolean variables managed by the SAT solver."""
2620        return self._checked_response.num_booleans()

Returns the number of boolean variables managed by the SAT solver.

num_conflicts: int
2622    @property
2623    def num_conflicts(self) -> int:
2624        """Returns the number of conflicts since the creation of the solver."""
2625        return self._checked_response.num_conflicts()

Returns the number of conflicts since the creation of the solver.

num_branches: int
2627    @property
2628    def num_branches(self) -> int:
2629        """Returns the number of search branches explored by the solver."""
2630        return self._checked_response.num_branches()

Returns the number of search branches explored by the solver.

wall_time: float
2632    @property
2633    def wall_time(self) -> float:
2634        """Returns the wall time in seconds since the creation of the solver."""
2635        return self._checked_response.wall_time()

Returns the wall time in seconds since the creation of the solver.

user_time: float
2637    @property
2638    def user_time(self) -> float:
2639        """Returns the user time in seconds since the creation of the solver."""
2640        return self._checked_response.user_time()

Returns the user time in seconds since the creation of the solver.

response_proto: ortools.sat.cp_model_pb2.CpSolverResponse
2642    @property
2643    def response_proto(self) -> cp_model_pb2.CpSolverResponse:
2644        """Returns the response object."""
2645        return self._checked_response.response()

Returns the response object.

def response_stats(self) -> str:
2647    def response_stats(self) -> str:
2648        """Returns some statistics on the solution found as a string."""
2649        return self._checked_response.response_stats()

Returns some statistics on the solution found as a string.

def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]:
2651    def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]:
2652        """Returns the indices of the infeasible assumptions."""
2653        return self._checked_response.sufficient_assumptions_for_infeasibility()

Returns the indices of the infeasible assumptions.

def status_name(self, status: Optional[Any] = None) -> str:
2655    def status_name(self, status: Optional[Any] = None) -> str:
2656        """Returns the name of the status returned by solve()."""
2657        if status is None:
2658            status = self._checked_response.status()
2659        return cp_model_pb2.CpSolverStatus.Name(status)

Returns the name of the status returned by solve().

def solution_info(self) -> str:
2661    def solution_info(self) -> str:
2662        """Returns some information on the solve process.
2663
2664        Returns some information on how the solution was found, or the reason
2665        why the model or the parameters are invalid.
2666
2667        Raises:
2668          RuntimeError: if solve() has not been called.
2669        """
2670        return self._checked_response.solution_info()

Returns some information on the solve process.

Returns some information on how the solution was found, or the reason why the model or the parameters are invalid.

Raises:
  • RuntimeError: if solve() has not been called.
def BestObjectiveBound(self) -> float:
2682    def BestObjectiveBound(self) -> float:
2683        return self.best_objective_bound
def BooleanValue( self, literal: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
2685    def BooleanValue(self, literal: LiteralT) -> bool:
2686        return self.boolean_value(literal)
def BooleanValues( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
2688    def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series:
2689        return self.boolean_values(variables)
def NumBooleans(self) -> int:
2691    def NumBooleans(self) -> int:
2692        return self.num_booleans
def NumConflicts(self) -> int:
2694    def NumConflicts(self) -> int:
2695        return self.num_conflicts
def NumBranches(self) -> int:
2697    def NumBranches(self) -> int:
2698        return self.num_branches
def ObjectiveValue(self) -> float:
2700    def ObjectiveValue(self) -> float:
2701        return self.objective_value
def ResponseProto(self) -> ortools.sat.cp_model_pb2.CpSolverResponse:
2703    def ResponseProto(self) -> cp_model_pb2.CpSolverResponse:
2704        return self.response_proto
def ResponseStats(self) -> str:
2706    def ResponseStats(self) -> str:
2707        return self.response_stats()
def Solve( self, model: CpModel, solution_callback: Optional[CpSolverSolutionCallback] = None) -> <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7f8c8eb9cef0>:
2709    def Solve(
2710        self,
2711        model: CpModel,
2712        solution_callback: Optional["CpSolverSolutionCallback"] = None,
2713    ) -> cp_model_pb2.CpSolverStatus:
2714        return self.solve(model, solution_callback)
def SolutionInfo(self) -> str:
2716    def SolutionInfo(self) -> str:
2717        return self.solution_info()
def StatusName(self, status: Optional[Any] = None) -> str:
2719    def StatusName(self, status: Optional[Any] = None) -> str:
2720        return self.status_name(status)
def StopSearch(self) -> None:
2722    def StopSearch(self) -> None:
2723        self.stop_search()
def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]:
2725    def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]:
2726        return self.sufficient_assumptions_for_infeasibility()
def UserTime(self) -> float:
2728    def UserTime(self) -> float:
2729        return self.user_time
def Value( self, expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
2731    def Value(self, expression: LinearExprT) -> int:
2732        return self.value(expression)
def Values( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
2734    def Values(self, variables: _IndexOrSeries) -> pd.Series:
2735        return self.values(variables)
def WallTime(self) -> float:
2737    def WallTime(self) -> float:
2738        return self.wall_time
def SolveWithSolutionCallback( self, model: CpModel, callback: CpSolverSolutionCallback) -> <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7f8c8eb9cef0>:
2740    def SolveWithSolutionCallback(
2741        self, model: CpModel, callback: "CpSolverSolutionCallback"
2742    ) -> cp_model_pb2.CpSolverStatus:
2743        """DEPRECATED Use solve() with the callback argument."""
2744        warnings.warn(
2745            "solve_with_solution_callback is deprecated; use solve() with"
2746            + "the callback argument.",
2747            DeprecationWarning,
2748        )
2749        return self.solve(model, callback)

DEPRECATED Use solve() with the callback argument.

def SearchForAllSolutions( self, model: CpModel, callback: CpSolverSolutionCallback) -> <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7f8c8eb9cef0>:
2751    def SearchForAllSolutions(
2752        self, model: CpModel, callback: "CpSolverSolutionCallback"
2753    ) -> cp_model_pb2.CpSolverStatus:
2754        """DEPRECATED Use solve() with the right parameter.
2755
2756        Search for all solutions of a satisfiability problem.
2757
2758        This method searches for all feasible solutions of a given model.
2759        Then it feeds the solution to the callback.
2760
2761        Note that the model cannot contain an objective.
2762
2763        Args:
2764          model: The model to solve.
2765          callback: The callback that will be called at each solution.
2766
2767        Returns:
2768          The status of the solve:
2769
2770          * *FEASIBLE* if some solutions have been found
2771          * *INFEASIBLE* if the solver has proved there are no solution
2772          * *OPTIMAL* if all solutions have been found
2773        """
2774        warnings.warn(
2775            "search_for_all_solutions is deprecated; use solve() with"
2776            + "enumerate_all_solutions = True.",
2777            DeprecationWarning,
2778        )
2779        if model.has_objective():
2780            raise TypeError(
2781                "Search for all solutions is only defined on satisfiability problems"
2782            )
2783        # Store old parameter.
2784        enumerate_all = self.parameters.enumerate_all_solutions
2785        self.parameters.enumerate_all_solutions = True
2786
2787        status: cp_model_pb2.CpSolverStatus = self.solve(model, callback)
2788
2789        # Restore parameter.
2790        self.parameters.enumerate_all_solutions = enumerate_all
2791        return status

DEPRECATED Use solve() with the right parameter.

Search for all solutions of a satisfiability problem.

This method searches for all feasible solutions of a given model. Then it feeds the solution to the callback.

Note that the model cannot contain an objective.

Arguments:
  • model: The model to solve.
  • callback: The callback that will be called at each solution.
Returns:

The status of the solve:

  • FEASIBLE if some solutions have been found
  • INFEASIBLE if the solver has proved there are no solution
  • OPTIMAL if all solutions have been found
class CpSolverSolutionCallback(ortools.sat.python.cp_model_helper.SolutionCallback):
2797class CpSolverSolutionCallback(cmh.SolutionCallback):
2798    """Solution callback.
2799
2800    This class implements a callback that will be called at each new solution
2801    found during search.
2802
2803    The method on_solution_callback() will be called by the solver, and must be
2804    implemented. The current solution can be queried using the boolean_value()
2805    and value() methods.
2806
2807    These methods returns the same information as their counterpart in the
2808    `CpSolver` class.
2809    """
2810
2811    def __init__(self) -> None:
2812        cmh.SolutionCallback.__init__(self)
2813
2814    def OnSolutionCallback(self) -> None:
2815        """Proxy for the same method in snake case."""
2816        self.on_solution_callback()
2817
2818    def boolean_value(self, lit: LiteralT) -> bool:
2819        """Returns the boolean value of a boolean literal.
2820
2821        Args:
2822            lit: A boolean variable or its negation.
2823
2824        Returns:
2825            The Boolean value of the literal in the solution.
2826
2827        Raises:
2828            RuntimeError: if `lit` is not a boolean variable or its negation.
2829        """
2830        if not self.has_response():
2831            raise RuntimeError("solve() has not been called.")
2832        return self.BooleanValue(lit)
2833
2834    def value(self, expression: LinearExprT) -> int:
2835        """Evaluates an linear expression in the current solution.
2836
2837        Args:
2838            expression: a linear expression of the model.
2839
2840        Returns:
2841            An integer value equal to the evaluation of the linear expression
2842            against the current solution.
2843
2844        Raises:
2845            RuntimeError: if 'expression' is not a LinearExpr.
2846        """
2847        if not self.has_response():
2848            raise RuntimeError("solve() has not been called.")
2849        return self.Value(expression)
2850
2851    def float_value(self, expression: LinearExprT) -> float:
2852        """Evaluates an linear expression in the current solution.
2853
2854        Args:
2855            expression: a linear expression of the model.
2856
2857        Returns:
2858            An integer value equal to the evaluation of the linear expression
2859            against the current solution.
2860
2861        Raises:
2862            RuntimeError: if 'expression' is not a LinearExpr.
2863        """
2864        if not self.has_response():
2865            raise RuntimeError("solve() has not been called.")
2866        return self.FloatValue(expression)
2867
2868    def has_response(self) -> bool:
2869        return self.HasResponse()
2870
2871    def stop_search(self) -> None:
2872        """Stops the current search asynchronously."""
2873        if not self.has_response():
2874            raise RuntimeError("solve() has not been called.")
2875        self.StopSearch()
2876
2877    @property
2878    def objective_value(self) -> float:
2879        """Returns the value of the objective after solve."""
2880        if not self.has_response():
2881            raise RuntimeError("solve() has not been called.")
2882        return self.ObjectiveValue()
2883
2884    @property
2885    def best_objective_bound(self) -> float:
2886        """Returns the best lower (upper) bound found when min(max)imizing."""
2887        if not self.has_response():
2888            raise RuntimeError("solve() has not been called.")
2889        return self.BestObjectiveBound()
2890
2891    @property
2892    def num_booleans(self) -> int:
2893        """Returns the number of boolean variables managed by the SAT solver."""
2894        if not self.has_response():
2895            raise RuntimeError("solve() has not been called.")
2896        return self.NumBooleans()
2897
2898    @property
2899    def num_conflicts(self) -> int:
2900        """Returns the number of conflicts since the creation of the solver."""
2901        if not self.has_response():
2902            raise RuntimeError("solve() has not been called.")
2903        return self.NumConflicts()
2904
2905    @property
2906    def num_branches(self) -> int:
2907        """Returns the number of search branches explored by the solver."""
2908        if not self.has_response():
2909            raise RuntimeError("solve() has not been called.")
2910        return self.NumBranches()
2911
2912    @property
2913    def num_integer_propagations(self) -> int:
2914        """Returns the number of integer propagations done by the solver."""
2915        if not self.has_response():
2916            raise RuntimeError("solve() has not been called.")
2917        return self.NumIntegerPropagations()
2918
2919    @property
2920    def num_boolean_propagations(self) -> int:
2921        """Returns the number of Boolean propagations done by the solver."""
2922        if not self.has_response():
2923            raise RuntimeError("solve() has not been called.")
2924        return self.NumBooleanPropagations()
2925
2926    @property
2927    def deterministic_time(self) -> float:
2928        """Returns the determistic time in seconds since the creation of the solver."""
2929        if not self.has_response():
2930            raise RuntimeError("solve() has not been called.")
2931        return self.DeterministicTime()
2932
2933    @property
2934    def wall_time(self) -> float:
2935        """Returns the wall time in seconds since the creation of the solver."""
2936        if not self.has_response():
2937            raise RuntimeError("solve() has not been called.")
2938        return self.WallTime()
2939
2940    @property
2941    def user_time(self) -> float:
2942        """Returns the user time in seconds since the creation of the solver."""
2943        if not self.has_response():
2944            raise RuntimeError("solve() has not been called.")
2945        return self.UserTime()
2946
2947    @property
2948    def response_proto(self) -> cp_model_pb2.CpSolverResponse:
2949        """Returns the response object."""
2950        if not self.has_response():
2951            raise RuntimeError("solve() has not been called.")
2952        return self.Response()

Solution callback.

This class implements a callback that will be called at each new solution found during search.

The method on_solution_callback() will be called by the solver, and must be implemented. The current solution can be queried using the boolean_value() and value() methods.

These methods returns the same information as their counterpart in the CpSolver class.

CpSolverSolutionCallback()
2811    def __init__(self) -> None:
2812        cmh.SolutionCallback.__init__(self)
def OnSolutionCallback(self) -> None:
2814    def OnSolutionCallback(self) -> None:
2815        """Proxy for the same method in snake case."""
2816        self.on_solution_callback()

Proxy for the same method in snake case.

def boolean_value( self, lit: Union[ortools.sat.python.cp_model_helper.Literal, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
2818    def boolean_value(self, lit: LiteralT) -> bool:
2819        """Returns the boolean value of a boolean literal.
2820
2821        Args:
2822            lit: A boolean variable or its negation.
2823
2824        Returns:
2825            The Boolean value of the literal in the solution.
2826
2827        Raises:
2828            RuntimeError: if `lit` is not a boolean variable or its negation.
2829        """
2830        if not self.has_response():
2831            raise RuntimeError("solve() has not been called.")
2832        return self.BooleanValue(lit)

Returns the boolean value of a boolean literal.

Arguments:
  • lit: A boolean variable or its negation.
Returns:

The Boolean value of the literal in the solution.

Raises:
  • RuntimeError: if lit is not a boolean variable or its negation.
def value( self, expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
2834    def value(self, expression: LinearExprT) -> int:
2835        """Evaluates an linear expression in the current solution.
2836
2837        Args:
2838            expression: a linear expression of the model.
2839
2840        Returns:
2841            An integer value equal to the evaluation of the linear expression
2842            against the current solution.
2843
2844        Raises:
2845            RuntimeError: if 'expression' is not a LinearExpr.
2846        """
2847        if not self.has_response():
2848            raise RuntimeError("solve() has not been called.")
2849        return self.Value(expression)

Evaluates an linear expression in the current solution.

Arguments:
  • expression: a linear expression of the model.
Returns:

An integer value equal to the evaluation of the linear expression against the current solution.

Raises:
  • RuntimeError: if 'expression' is not a LinearExpr.
def float_value( self, expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> float:
2851    def float_value(self, expression: LinearExprT) -> float:
2852        """Evaluates an linear expression in the current solution.
2853
2854        Args:
2855            expression: a linear expression of the model.
2856
2857        Returns:
2858            An integer value equal to the evaluation of the linear expression
2859            against the current solution.
2860
2861        Raises:
2862            RuntimeError: if 'expression' is not a LinearExpr.
2863        """
2864        if not self.has_response():
2865            raise RuntimeError("solve() has not been called.")
2866        return self.FloatValue(expression)

Evaluates an linear expression in the current solution.

Arguments:
  • expression: a linear expression of the model.
Returns:

An integer value equal to the evaluation of the linear expression against the current solution.

Raises:
  • RuntimeError: if 'expression' is not a LinearExpr.
def has_response(self) -> bool:
2868    def has_response(self) -> bool:
2869        return self.HasResponse()
objective_value: float
2877    @property
2878    def objective_value(self) -> float:
2879        """Returns the value of the objective after solve."""
2880        if not self.has_response():
2881            raise RuntimeError("solve() has not been called.")
2882        return self.ObjectiveValue()

Returns the value of the objective after solve.

best_objective_bound: float
2884    @property
2885    def best_objective_bound(self) -> float:
2886        """Returns the best lower (upper) bound found when min(max)imizing."""
2887        if not self.has_response():
2888            raise RuntimeError("solve() has not been called.")
2889        return self.BestObjectiveBound()

Returns the best lower (upper) bound found when min(max)imizing.

num_booleans: int
2891    @property
2892    def num_booleans(self) -> int:
2893        """Returns the number of boolean variables managed by the SAT solver."""
2894        if not self.has_response():
2895            raise RuntimeError("solve() has not been called.")
2896        return self.NumBooleans()

Returns the number of boolean variables managed by the SAT solver.

num_conflicts: int
2898    @property
2899    def num_conflicts(self) -> int:
2900        """Returns the number of conflicts since the creation of the solver."""
2901        if not self.has_response():
2902            raise RuntimeError("solve() has not been called.")
2903        return self.NumConflicts()

Returns the number of conflicts since the creation of the solver.

num_branches: int
2905    @property
2906    def num_branches(self) -> int:
2907        """Returns the number of search branches explored by the solver."""
2908        if not self.has_response():
2909            raise RuntimeError("solve() has not been called.")
2910        return self.NumBranches()

Returns the number of search branches explored by the solver.

num_integer_propagations: int
2912    @property
2913    def num_integer_propagations(self) -> int:
2914        """Returns the number of integer propagations done by the solver."""
2915        if not self.has_response():
2916            raise RuntimeError("solve() has not been called.")
2917        return self.NumIntegerPropagations()

Returns the number of integer propagations done by the solver.

num_boolean_propagations: int
2919    @property
2920    def num_boolean_propagations(self) -> int:
2921        """Returns the number of Boolean propagations done by the solver."""
2922        if not self.has_response():
2923            raise RuntimeError("solve() has not been called.")
2924        return self.NumBooleanPropagations()

Returns the number of Boolean propagations done by the solver.

deterministic_time: float
2926    @property
2927    def deterministic_time(self) -> float:
2928        """Returns the determistic time in seconds since the creation of the solver."""
2929        if not self.has_response():
2930            raise RuntimeError("solve() has not been called.")
2931        return self.DeterministicTime()

Returns the determistic time in seconds since the creation of the solver.

wall_time: float
2933    @property
2934    def wall_time(self) -> float:
2935        """Returns the wall time in seconds since the creation of the solver."""
2936        if not self.has_response():
2937            raise RuntimeError("solve() has not been called.")
2938        return self.WallTime()

Returns the wall time in seconds since the creation of the solver.

user_time: float
2940    @property
2941    def user_time(self) -> float:
2942        """Returns the user time in seconds since the creation of the solver."""
2943        if not self.has_response():
2944            raise RuntimeError("solve() has not been called.")
2945        return self.UserTime()

Returns the user time in seconds since the creation of the solver.

response_proto: ortools.sat.cp_model_pb2.CpSolverResponse
2947    @property
2948    def response_proto(self) -> cp_model_pb2.CpSolverResponse:
2949        """Returns the response object."""
2950        if not self.has_response():
2951            raise RuntimeError("solve() has not been called.")
2952        return self.Response()

Returns the response object.

class ObjectiveSolutionPrinter(CpSolverSolutionCallback):
2955class ObjectiveSolutionPrinter(CpSolverSolutionCallback):
2956    """Display the objective value and time of intermediate solutions."""
2957
2958    def __init__(self) -> None:
2959        CpSolverSolutionCallback.__init__(self)
2960        self.__solution_count = 0
2961        self.__start_time = time.time()
2962
2963    def on_solution_callback(self) -> None:
2964        """Called on each new solution."""
2965        current_time = time.time()
2966        obj = self.objective_value
2967        print(
2968            f"Solution {self.__solution_count}, time ="
2969            f" {current_time - self.__start_time:0.2f} s, objective = {obj}",
2970            flush=True,
2971        )
2972        self.__solution_count += 1
2973
2974    def solution_count(self) -> int:
2975        """Returns the number of solutions found."""
2976        return self.__solution_count

Display the objective value and time of intermediate solutions.

ObjectiveSolutionPrinter()
2958    def __init__(self) -> None:
2959        CpSolverSolutionCallback.__init__(self)
2960        self.__solution_count = 0
2961        self.__start_time = time.time()
def on_solution_callback(self) -> None:
2963    def on_solution_callback(self) -> None:
2964        """Called on each new solution."""
2965        current_time = time.time()
2966        obj = self.objective_value
2967        print(
2968            f"Solution {self.__solution_count}, time ="
2969            f" {current_time - self.__start_time:0.2f} s, objective = {obj}",
2970            flush=True,
2971        )
2972        self.__solution_count += 1

Called on each new solution.

def solution_count(self) -> int:
2974    def solution_count(self) -> int:
2975        """Returns the number of solutions found."""
2976        return self.__solution_count

Returns the number of solutions found.

class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback):
2979class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback):
2980    """Print intermediate solutions (objective, variable values, time)."""
2981
2982    def __init__(self, variables: Sequence[IntVar]) -> None:
2983        CpSolverSolutionCallback.__init__(self)
2984        self.__variables: Sequence[IntVar] = variables
2985        self.__solution_count: int = 0
2986        self.__start_time: float = time.time()
2987
2988    def on_solution_callback(self) -> None:
2989        """Called on each new solution."""
2990        current_time = time.time()
2991        obj = self.objective_value
2992        print(
2993            f"Solution {self.__solution_count}, time ="
2994            f" {current_time - self.__start_time:0.2f} s, objective = {obj}"
2995        )
2996        for v in self.__variables:
2997            print(f"  {v} = {self.value(v)}", end=" ")
2998        print(flush=True)
2999        self.__solution_count += 1
3000
3001    @property
3002    def solution_count(self) -> int:
3003        """Returns the number of solutions found."""
3004        return self.__solution_count

Print intermediate solutions (objective, variable values, time).

VarArrayAndObjectiveSolutionPrinter(variables: Sequence[IntVar])
2982    def __init__(self, variables: Sequence[IntVar]) -> None:
2983        CpSolverSolutionCallback.__init__(self)
2984        self.__variables: Sequence[IntVar] = variables
2985        self.__solution_count: int = 0
2986        self.__start_time: float = time.time()
def on_solution_callback(self) -> None:
2988    def on_solution_callback(self) -> None:
2989        """Called on each new solution."""
2990        current_time = time.time()
2991        obj = self.objective_value
2992        print(
2993            f"Solution {self.__solution_count}, time ="
2994            f" {current_time - self.__start_time:0.2f} s, objective = {obj}"
2995        )
2996        for v in self.__variables:
2997            print(f"  {v} = {self.value(v)}", end=" ")
2998        print(flush=True)
2999        self.__solution_count += 1

Called on each new solution.

solution_count: int
3001    @property
3002    def solution_count(self) -> int:
3003        """Returns the number of solutions found."""
3004        return self.__solution_count

Returns the number of solutions found.

class VarArraySolutionPrinter(CpSolverSolutionCallback):
3007class VarArraySolutionPrinter(CpSolverSolutionCallback):
3008    """Print intermediate solutions (variable values, time)."""
3009
3010    def __init__(self, variables: Sequence[IntVar]) -> None:
3011        CpSolverSolutionCallback.__init__(self)
3012        self.__variables: Sequence[IntVar] = variables
3013        self.__solution_count: int = 0
3014        self.__start_time: float = time.time()
3015
3016    def on_solution_callback(self) -> None:
3017        """Called on each new solution."""
3018        current_time = time.time()
3019        print(
3020            f"Solution {self.__solution_count}, time ="
3021            f" {current_time - self.__start_time:0.2f} s"
3022        )
3023        for v in self.__variables:
3024            print(f"  {v} = {self.value(v)}", end=" ")
3025        print(flush=True)
3026        self.__solution_count += 1
3027
3028    @property
3029    def solution_count(self) -> int:
3030        """Returns the number of solutions found."""
3031        return self.__solution_count

Print intermediate solutions (variable values, time).

VarArraySolutionPrinter(variables: Sequence[IntVar])
3010    def __init__(self, variables: Sequence[IntVar]) -> None:
3011        CpSolverSolutionCallback.__init__(self)
3012        self.__variables: Sequence[IntVar] = variables
3013        self.__solution_count: int = 0
3014        self.__start_time: float = time.time()
def on_solution_callback(self) -> None:
3016    def on_solution_callback(self) -> None:
3017        """Called on each new solution."""
3018        current_time = time.time()
3019        print(
3020            f"Solution {self.__solution_count}, time ="
3021            f" {current_time - self.__start_time:0.2f} s"
3022        )
3023        for v in self.__variables:
3024            print(f"  {v} = {self.value(v)}", end=" ")
3025        print(flush=True)
3026        self.__solution_count += 1

Called on each new solution.

solution_count: int
3028    @property
3029    def solution_count(self) -> int:
3030        """Returns the number of solutions found."""
3031        return self.__solution_count

Returns the number of solutions found.