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

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
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[ForwardRef('IntVar'), ForwardRef('_NotBooleanVariable'), int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]
BoolVarT = typing.Union[ForwardRef('IntVar'), ForwardRef('_NotBooleanVariable')]
VariableT = typing.Union[ForwardRef('IntVar'), int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]
LinearExprT = typing.Union[ForwardRef('LinearExpr'), ForwardRef('IntVar'), int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]
ObjLinearExprT = typing.Union[ForwardRef('LinearExpr'), int, float, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, numpy.float64]
BoundedLinearExprT = typing.Union[ForwardRef('BoundedLinearExpression'), bool]
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[ForwardRef('IntVar'), ForwardRef('_NotBooleanVariable'), int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]
def display_bounds(bounds: Sequence[int]) -> str:
169def display_bounds(bounds: Sequence[int]) -> str:
170    """Displays a flattened list of intervals."""
171    out = ""
172    for i in range(0, len(bounds), 2):
173        if i != 0:
174            out += ", "
175        if bounds[i] == bounds[i + 1]:
176            out += str(bounds[i])
177        else:
178            out += str(bounds[i]) + ".." + str(bounds[i + 1])
179    return out

Displays a flattened list of intervals.

def short_name(model: ortools.sat.cp_model_pb2.CpModelProto, i: int) -> str:
182def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str:
183    """Returns a short name of an integer variable, or its negation."""
184    if i < 0:
185        return "not(%s)" % short_name(model, -i - 1)
186    v = model.variables[i]
187    if v.name:
188        return v.name
189    elif len(v.domain) == 2 and v.domain[0] == v.domain[1]:
190        return str(v.domain[0])
191    else:
192        return "[%s]" % display_bounds(v.domain)

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

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

Pretty-print LinearExpressionProto instances.

class LinearExpr:
220class LinearExpr:
221    """Holds an integer linear expression.
222
223    A linear expression is built from integer constants and variables.
224    For example, `x + 2 * (y - z + 1)`.
225
226    Linear expressions are used in CP-SAT models in constraints and in the
227    objective:
228
229    * You can define linear constraints as in:
230
231    ```
232    model.add(x + 2 * y <= 5)
233    model.add(sum(array_of_vars) == 5)
234    ```
235
236    * In CP-SAT, the objective is a linear expression:
237
238    ```
239    model.minimize(x + 2 * y + z)
240    ```
241
242    * For large arrays, using the LinearExpr class is faster that using the python
243    `sum()` function. You can create constraints and the objective from lists of
244    linear expressions or coefficients as follows:
245
246    ```
247    model.minimize(cp_model.LinearExpr.sum(expressions))
248    model.add(cp_model.LinearExpr.weighted_sum(expressions, coefficients) >= 0)
249    ```
250    """
251
252    @classmethod
253    def sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT:
254        """Creates the expression sum(expressions)."""
255        if len(expressions) == 1:
256            return expressions[0]
257        return _SumArray(expressions)
258
259    @overload
260    @classmethod
261    def weighted_sum(
262        cls,
263        expressions: Sequence[LinearExprT],
264        coefficients: Sequence[IntegralT],
265    ) -> LinearExprT: ...
266
267    @overload
268    @classmethod
269    def weighted_sum(
270        cls,
271        expressions: Sequence[ObjLinearExprT],
272        coefficients: Sequence[NumberT],
273    ) -> ObjLinearExprT: ...
274
275    @classmethod
276    def weighted_sum(cls, expressions, coefficients):
277        """Creates the expression sum(expressions[i] * coefficients[i])."""
278        if LinearExpr.is_empty_or_all_null(coefficients):
279            return 0
280        elif len(expressions) == 1:
281            return expressions[0] * coefficients[0]
282        else:
283            return _WeightedSum(expressions, coefficients)
284
285    @overload
286    @classmethod
287    def term(
288        cls,
289        expressions: LinearExprT,
290        coefficients: IntegralT,
291    ) -> LinearExprT: ...
292
293    @overload
294    @classmethod
295    def term(
296        cls,
297        expressions: ObjLinearExprT,
298        coefficients: NumberT,
299    ) -> ObjLinearExprT: ...
300
301    @classmethod
302    def term(cls, expression, coefficient):
303        """Creates `expression * coefficient`."""
304        if cmh.is_zero(coefficient):
305            return 0
306        else:
307            return expression * coefficient
308
309    @classmethod
310    def is_empty_or_all_null(cls, coefficients: Sequence[NumberT]) -> bool:
311        for c in coefficients:
312            if not cmh.is_zero(c):
313                return False
314        return True
315
316    @classmethod
317    def rebuild_from_linear_expression_proto(
318        cls,
319        model: cp_model_pb2.CpModelProto,
320        proto: cp_model_pb2.LinearExpressionProto,
321    ) -> LinearExprT:
322        """Recreate a LinearExpr from a LinearExpressionProto."""
323        offset = proto.offset
324        num_elements = len(proto.vars)
325        if num_elements == 0:
326            return offset
327        elif num_elements == 1:
328            return (
329                IntVar(model, proto.vars[0], None) * proto.coeffs[0] + offset
330            )  # pytype: disable=bad-return-type
331        else:
332            variables = []
333            coeffs = []
334            all_ones = True
335            for index, coeff in zip(proto.vars, proto.coeffs):
336                variables.append(IntVar(model, index, None))
337                coeffs.append(coeff)
338                if not cmh.is_one(coeff):
339                    all_ones = False
340            if all_ones:
341                return _SumArray(variables, offset)
342            else:
343                return _WeightedSum(variables, coeffs, offset)
344
345    def get_integer_var_value_map(self) -> Tuple[Dict["IntVar", int], int]:
346        """Scans the expression, and returns (var_coef_map, constant)."""
347        coeffs: Dict["IntVar", int] = collections.defaultdict(int)
348        constant = 0
349        to_process: List[Tuple[LinearExprT, int]] = [(self, 1)]
350        while to_process:  # Flatten to avoid recursion.
351            expr: LinearExprT
352            coeff: int
353            expr, coeff = to_process.pop()
354            if isinstance(expr, IntegralTypes):
355                constant += coeff * int(expr)
356            elif isinstance(expr, _ProductCst):
357                to_process.append((expr.expression(), coeff * expr.coefficient()))
358            elif isinstance(expr, _Sum):
359                to_process.append((expr.left(), coeff))
360                to_process.append((expr.right(), coeff))
361            elif isinstance(expr, _SumArray):
362                for e in expr.expressions():
363                    to_process.append((e, coeff))
364                constant += expr.constant() * coeff
365            elif isinstance(expr, _WeightedSum):
366                for e, c in zip(expr.expressions(), expr.coefficients()):
367                    to_process.append((e, coeff * c))
368                constant += expr.constant() * coeff
369            elif isinstance(expr, IntVar):
370                coeffs[expr] += coeff
371            elif isinstance(expr, _NotBooleanVariable):
372                constant += coeff
373                coeffs[expr.negated()] -= coeff
374            elif isinstance(expr, NumberTypes):
375                raise TypeError(
376                    f"Floating point constants are not supported in constraints: {expr}"
377                )
378            else:
379                raise TypeError("Unrecognized linear expression: " + str(expr))
380
381        return coeffs, constant
382
383    def get_float_var_value_map(
384        self,
385    ) -> Tuple[Dict["IntVar", float], float, bool]:
386        """Scans the expression. Returns (var_coef_map, constant, is_integer)."""
387        coeffs: Dict["IntVar", Union[int, float]] = {}
388        constant: Union[int, float] = 0
389        to_process: List[Tuple[LinearExprT, Union[int, float]]] = [(self, 1)]
390        while to_process:  # Flatten to avoid recursion.
391            expr, coeff = to_process.pop()
392            if isinstance(expr, IntegralTypes):  # Keep integrality.
393                constant += coeff * int(expr)
394            elif isinstance(expr, NumberTypes):
395                constant += coeff * float(expr)
396            elif isinstance(expr, _ProductCst):
397                to_process.append((expr.expression(), coeff * expr.coefficient()))
398            elif isinstance(expr, _Sum):
399                to_process.append((expr.left(), coeff))
400                to_process.append((expr.right(), coeff))
401            elif isinstance(expr, _SumArray):
402                for e in expr.expressions():
403                    to_process.append((e, coeff))
404                constant += expr.constant() * coeff
405            elif isinstance(expr, _WeightedSum):
406                for e, c in zip(expr.expressions(), expr.coefficients()):
407                    to_process.append((e, coeff * c))
408                constant += expr.constant() * coeff
409            elif isinstance(expr, IntVar):
410                if expr in coeffs:
411                    coeffs[expr] += coeff
412                else:
413                    coeffs[expr] = coeff
414            elif isinstance(expr, _NotBooleanVariable):
415                constant += coeff
416                if expr.negated() in coeffs:
417                    coeffs[expr.negated()] -= coeff
418                else:
419                    coeffs[expr.negated()] = -coeff
420            else:
421                raise TypeError("Unrecognized linear expression: " + str(expr))
422        is_integer = isinstance(constant, IntegralTypes)
423        if is_integer:
424            for coeff in coeffs.values():
425                if not isinstance(coeff, IntegralTypes):
426                    is_integer = False
427                    break
428        return coeffs, constant, is_integer
429
430    def __hash__(self) -> int:
431        return object.__hash__(self)
432
433    def __abs__(self) -> NoReturn:
434        raise NotImplementedError(
435            "calling abs() on a linear expression is not supported, "
436            "please use CpModel.add_abs_equality"
437        )
438
439    @overload
440    def __add__(self, arg: "LinearExpr") -> "LinearExpr": ...
441
442    @overload
443    def __add__(self, arg: NumberT) -> "LinearExpr": ...
444
445    def __add__(self, arg):
446        if cmh.is_zero(arg):
447            return self
448        return _Sum(self, arg)
449
450    @overload
451    def __radd__(self, arg: "LinearExpr") -> "LinearExpr": ...
452
453    @overload
454    def __radd__(self, arg: NumberT) -> "LinearExpr": ...
455
456    def __radd__(self, arg):
457        return self.__add__(arg)
458
459    @overload
460    def __sub__(self, arg: "LinearExpr") -> "LinearExpr": ...
461
462    @overload
463    def __sub__(self, arg: NumberT) -> "LinearExpr": ...
464
465    def __sub__(self, arg):
466        if cmh.is_zero(arg):
467            return self
468        if isinstance(arg, NumberTypes):
469            arg = cmh.assert_is_a_number(arg)
470            return _Sum(self, -arg)
471        else:
472            return _Sum(self, -arg)
473
474    @overload
475    def __rsub__(self, arg: "LinearExpr") -> "LinearExpr": ...
476
477    @overload
478    def __rsub__(self, arg: NumberT) -> "LinearExpr": ...
479
480    def __rsub__(self, arg):
481        return _Sum(-self, arg)
482
483    @overload
484    def __mul__(self, arg: IntegralT) -> Union["LinearExpr", IntegralT]: ...
485
486    @overload
487    def __mul__(self, arg: NumberT) -> Union["LinearExpr", NumberT]: ...
488
489    def __mul__(self, arg):
490        arg = cmh.assert_is_a_number(arg)
491        if cmh.is_one(arg):
492            return self
493        elif cmh.is_zero(arg):
494            return 0
495        return _ProductCst(self, arg)
496
497    @overload
498    def __rmul__(self, arg: IntegralT) -> Union["LinearExpr", IntegralT]: ...
499
500    @overload
501    def __rmul__(self, arg: NumberT) -> Union["LinearExpr", NumberT]: ...
502
503    def __rmul__(self, arg):
504        return self.__mul__(arg)
505
506    def __div__(self, _) -> NoReturn:
507        raise NotImplementedError(
508            "calling / on a linear expression is not supported, "
509            "please use CpModel.add_division_equality"
510        )
511
512    def __truediv__(self, _) -> NoReturn:
513        raise NotImplementedError(
514            "calling // on a linear expression is not supported, "
515            "please use CpModel.add_division_equality"
516        )
517
518    def __mod__(self, _) -> NoReturn:
519        raise NotImplementedError(
520            "calling %% on a linear expression is not supported, "
521            "please use CpModel.add_modulo_equality"
522        )
523
524    def __pow__(self, _) -> NoReturn:
525        raise NotImplementedError(
526            "calling ** on a linear expression is not supported, "
527            "please use CpModel.add_multiplication_equality"
528        )
529
530    def __lshift__(self, _) -> NoReturn:
531        raise NotImplementedError(
532            "calling left shift on a linear expression is not supported"
533        )
534
535    def __rshift__(self, _) -> NoReturn:
536        raise NotImplementedError(
537            "calling right shift on a linear expression is not supported"
538        )
539
540    def __and__(self, _) -> NoReturn:
541        raise NotImplementedError(
542            "calling and on a linear expression is not supported, "
543            "please use CpModel.add_bool_and"
544        )
545
546    def __or__(self, _) -> NoReturn:
547        raise NotImplementedError(
548            "calling or on a linear expression is not supported, "
549            "please use CpModel.add_bool_or"
550        )
551
552    def __xor__(self, _) -> NoReturn:
553        raise NotImplementedError(
554            "calling xor on a linear expression is not supported, "
555            "please use CpModel.add_bool_xor"
556        )
557
558    def __neg__(self) -> "LinearExpr":
559        return _ProductCst(self, -1)
560
561    def __bool__(self) -> NoReturn:
562        raise NotImplementedError(
563            "Evaluating a LinearExpr instance as a Boolean is not implemented."
564        )
565
566    def __eq__(self, arg: LinearExprT) -> BoundedLinearExprT:  # type: ignore[override]
567        if arg is None:
568            return False
569        if isinstance(arg, IntegralTypes):
570            arg = cmh.assert_is_int64(arg)
571            return BoundedLinearExpression(self, [arg, arg])
572        elif isinstance(arg, LinearExpr):
573            return BoundedLinearExpression(self - arg, [0, 0])
574        else:
575            return False
576
577    def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression":
578        if isinstance(arg, IntegralTypes):
579            arg = cmh.assert_is_int64(arg)
580            return BoundedLinearExpression(self, [arg, INT_MAX])
581        else:
582            return BoundedLinearExpression(self - arg, [0, INT_MAX])
583
584    def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression":
585        if isinstance(arg, IntegralTypes):
586            arg = cmh.assert_is_int64(arg)
587            return BoundedLinearExpression(self, [INT_MIN, arg])
588        else:
589            return BoundedLinearExpression(self - arg, [INT_MIN, 0])
590
591    def __lt__(self, arg: LinearExprT) -> "BoundedLinearExpression":
592        if isinstance(arg, IntegralTypes):
593            arg = cmh.assert_is_int64(arg)
594            if arg == INT_MIN:
595                raise ArithmeticError("< INT_MIN is not supported")
596            return BoundedLinearExpression(self, [INT_MIN, arg - 1])
597        else:
598            return BoundedLinearExpression(self - arg, [INT_MIN, -1])
599
600    def __gt__(self, arg: LinearExprT) -> "BoundedLinearExpression":
601        if isinstance(arg, IntegralTypes):
602            arg = cmh.assert_is_int64(arg)
603            if arg == INT_MAX:
604                raise ArithmeticError("> INT_MAX is not supported")
605            return BoundedLinearExpression(self, [arg + 1, INT_MAX])
606        else:
607            return BoundedLinearExpression(self - arg, [1, INT_MAX])
608
609    def __ne__(self, arg: LinearExprT) -> BoundedLinearExprT:  # type: ignore[override]
610        if arg is None:
611            return True
612        if isinstance(arg, IntegralTypes):
613            arg = cmh.assert_is_int64(arg)
614            if arg == INT_MAX:
615                return BoundedLinearExpression(self, [INT_MIN, INT_MAX - 1])
616            elif arg == INT_MIN:
617                return BoundedLinearExpression(self, [INT_MIN + 1, INT_MAX])
618            else:
619                return BoundedLinearExpression(
620                    self, [INT_MIN, arg - 1, arg + 1, INT_MAX]
621                )
622        elif isinstance(arg, LinearExpr):
623            return BoundedLinearExpression(self - arg, [INT_MIN, -1, 1, INT_MAX])
624        else:
625            return True
626
627    # Compatibility with pre PEP8
628    # pylint: disable=invalid-name
629    @classmethod
630    def Sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT:
631        """Creates the expression sum(expressions)."""
632        return cls.sum(expressions)
633
634    @overload
635    @classmethod
636    def WeightedSum(
637        cls,
638        expressions: Sequence[LinearExprT],
639        coefficients: Sequence[IntegralT],
640    ) -> LinearExprT: ...
641
642    @overload
643    @classmethod
644    def WeightedSum(
645        cls,
646        expressions: Sequence[ObjLinearExprT],
647        coefficients: Sequence[NumberT],
648    ) -> ObjLinearExprT: ...
649
650    @classmethod
651    def WeightedSum(cls, expressions, coefficients):
652        """Creates the expression sum(expressions[i] * coefficients[i])."""
653        return cls.weighted_sum(expressions, coefficients)
654
655    @overload
656    @classmethod
657    def Term(
658        cls,
659        expressions: LinearExprT,
660        coefficients: IntegralT,
661    ) -> LinearExprT: ...
662
663    @overload
664    @classmethod
665    def Term(
666        cls,
667        expressions: ObjLinearExprT,
668        coefficients: NumberT,
669    ) -> ObjLinearExprT: ...
670
671    @classmethod
672    def Term(cls, expression, coefficient):
673        """Creates `expression * coefficient`."""
674        return cls.term(expression, coefficient)
675
676    # pylint: enable=invalid-name

Holds an integer linear expression.

A linear expression is built from integer constants and variables. For example, x + 2 * (y - z + 1).

Linear expressions are used in CP-SAT models in constraints and in the objective:

  • You can define linear constraints as in:
model.add(x + 2 * y <= 5)
model.add(sum(array_of_vars) == 5)
  • In CP-SAT, the objective is a linear expression:
model.minimize(x + 2 * y + z)
  • For large arrays, using the LinearExpr class is faster that using the python sum() function. You can create constraints and the objective from lists of linear expressions or coefficients as follows:
model.minimize(cp_model.LinearExpr.sum(expressions))
model.add(cp_model.LinearExpr.weighted_sum(expressions, coefficients) >= 0)
@classmethod
def sum( cls, expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
252    @classmethod
253    def sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT:
254        """Creates the expression sum(expressions)."""
255        if len(expressions) == 1:
256            return expressions[0]
257        return _SumArray(expressions)

Creates the expression sum(expressions).

@classmethod
def weighted_sum(cls, expressions, coefficients):
275    @classmethod
276    def weighted_sum(cls, expressions, coefficients):
277        """Creates the expression sum(expressions[i] * coefficients[i])."""
278        if LinearExpr.is_empty_or_all_null(coefficients):
279            return 0
280        elif len(expressions) == 1:
281            return expressions[0] * coefficients[0]
282        else:
283            return _WeightedSum(expressions, coefficients)

Creates the expression sum(expressions[i] * coefficients[i]).

@classmethod
def term(cls, expression, coefficient):
301    @classmethod
302    def term(cls, expression, coefficient):
303        """Creates `expression * coefficient`."""
304        if cmh.is_zero(coefficient):
305            return 0
306        else:
307            return expression * coefficient

Creates expression * coefficient.

@classmethod
def is_empty_or_all_null( cls, coefficients: Sequence[Union[int, float, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, numpy.float64]]) -> bool:
309    @classmethod
310    def is_empty_or_all_null(cls, coefficients: Sequence[NumberT]) -> bool:
311        for c in coefficients:
312            if not cmh.is_zero(c):
313                return False
314        return True
@classmethod
def rebuild_from_linear_expression_proto( cls, model: ortools.sat.cp_model_pb2.CpModelProto, proto: ortools.sat.cp_model_pb2.LinearExpressionProto) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
316    @classmethod
317    def rebuild_from_linear_expression_proto(
318        cls,
319        model: cp_model_pb2.CpModelProto,
320        proto: cp_model_pb2.LinearExpressionProto,
321    ) -> LinearExprT:
322        """Recreate a LinearExpr from a LinearExpressionProto."""
323        offset = proto.offset
324        num_elements = len(proto.vars)
325        if num_elements == 0:
326            return offset
327        elif num_elements == 1:
328            return (
329                IntVar(model, proto.vars[0], None) * proto.coeffs[0] + offset
330            )  # pytype: disable=bad-return-type
331        else:
332            variables = []
333            coeffs = []
334            all_ones = True
335            for index, coeff in zip(proto.vars, proto.coeffs):
336                variables.append(IntVar(model, index, None))
337                coeffs.append(coeff)
338                if not cmh.is_one(coeff):
339                    all_ones = False
340            if all_ones:
341                return _SumArray(variables, offset)
342            else:
343                return _WeightedSum(variables, coeffs, offset)

Recreate a LinearExpr from a LinearExpressionProto.

def get_integer_var_value_map(self) -> Tuple[Dict[IntVar, int], int]:
345    def get_integer_var_value_map(self) -> Tuple[Dict["IntVar", int], int]:
346        """Scans the expression, and returns (var_coef_map, constant)."""
347        coeffs: Dict["IntVar", int] = collections.defaultdict(int)
348        constant = 0
349        to_process: List[Tuple[LinearExprT, int]] = [(self, 1)]
350        while to_process:  # Flatten to avoid recursion.
351            expr: LinearExprT
352            coeff: int
353            expr, coeff = to_process.pop()
354            if isinstance(expr, IntegralTypes):
355                constant += coeff * int(expr)
356            elif isinstance(expr, _ProductCst):
357                to_process.append((expr.expression(), coeff * expr.coefficient()))
358            elif isinstance(expr, _Sum):
359                to_process.append((expr.left(), coeff))
360                to_process.append((expr.right(), coeff))
361            elif isinstance(expr, _SumArray):
362                for e in expr.expressions():
363                    to_process.append((e, coeff))
364                constant += expr.constant() * coeff
365            elif isinstance(expr, _WeightedSum):
366                for e, c in zip(expr.expressions(), expr.coefficients()):
367                    to_process.append((e, coeff * c))
368                constant += expr.constant() * coeff
369            elif isinstance(expr, IntVar):
370                coeffs[expr] += coeff
371            elif isinstance(expr, _NotBooleanVariable):
372                constant += coeff
373                coeffs[expr.negated()] -= coeff
374            elif isinstance(expr, NumberTypes):
375                raise TypeError(
376                    f"Floating point constants are not supported in constraints: {expr}"
377                )
378            else:
379                raise TypeError("Unrecognized linear expression: " + str(expr))
380
381        return coeffs, constant

Scans the expression, and returns (var_coef_map, constant).

def get_float_var_value_map( self) -> Tuple[Dict[IntVar, float], float, bool]:
383    def get_float_var_value_map(
384        self,
385    ) -> Tuple[Dict["IntVar", float], float, bool]:
386        """Scans the expression. Returns (var_coef_map, constant, is_integer)."""
387        coeffs: Dict["IntVar", Union[int, float]] = {}
388        constant: Union[int, float] = 0
389        to_process: List[Tuple[LinearExprT, Union[int, float]]] = [(self, 1)]
390        while to_process:  # Flatten to avoid recursion.
391            expr, coeff = to_process.pop()
392            if isinstance(expr, IntegralTypes):  # Keep integrality.
393                constant += coeff * int(expr)
394            elif isinstance(expr, NumberTypes):
395                constant += coeff * float(expr)
396            elif isinstance(expr, _ProductCst):
397                to_process.append((expr.expression(), coeff * expr.coefficient()))
398            elif isinstance(expr, _Sum):
399                to_process.append((expr.left(), coeff))
400                to_process.append((expr.right(), coeff))
401            elif isinstance(expr, _SumArray):
402                for e in expr.expressions():
403                    to_process.append((e, coeff))
404                constant += expr.constant() * coeff
405            elif isinstance(expr, _WeightedSum):
406                for e, c in zip(expr.expressions(), expr.coefficients()):
407                    to_process.append((e, coeff * c))
408                constant += expr.constant() * coeff
409            elif isinstance(expr, IntVar):
410                if expr in coeffs:
411                    coeffs[expr] += coeff
412                else:
413                    coeffs[expr] = coeff
414            elif isinstance(expr, _NotBooleanVariable):
415                constant += coeff
416                if expr.negated() in coeffs:
417                    coeffs[expr.negated()] -= coeff
418                else:
419                    coeffs[expr.negated()] = -coeff
420            else:
421                raise TypeError("Unrecognized linear expression: " + str(expr))
422        is_integer = isinstance(constant, IntegralTypes)
423        if is_integer:
424            for coeff in coeffs.values():
425                if not isinstance(coeff, IntegralTypes):
426                    is_integer = False
427                    break
428        return coeffs, constant, is_integer

Scans the expression. Returns (var_coef_map, constant, is_integer).

@classmethod
def Sum( cls, expressions: Sequence[Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]]) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
629    @classmethod
630    def Sum(cls, expressions: Sequence[LinearExprT]) -> LinearExprT:
631        """Creates the expression sum(expressions)."""
632        return cls.sum(expressions)

Creates the expression sum(expressions).

@classmethod
def WeightedSum(cls, expressions, coefficients):
650    @classmethod
651    def WeightedSum(cls, expressions, coefficients):
652        """Creates the expression sum(expressions[i] * coefficients[i])."""
653        return cls.weighted_sum(expressions, coefficients)

Creates the expression sum(expressions[i] * coefficients[i]).

@classmethod
def Term(cls, expression, coefficient):
671    @classmethod
672    def Term(cls, expression, coefficient):
673        """Creates `expression * coefficient`."""
674        return cls.term(expression, coefficient)

Creates expression * coefficient.

class IntVar(LinearExpr):
833class IntVar(LinearExpr):
834    """An integer variable.
835
836    An IntVar is an object that can take on any integer value within defined
837    ranges. Variables appear in constraint like:
838
839        x + y >= 5
840        AllDifferent([x, y, z])
841
842    Solving a model is equivalent to finding, for each variable, a single value
843    from the set of initial values (called the initial domain), such that the
844    model is feasible, or optimal if you provided an objective function.
845    """
846
847    def __init__(
848        self,
849        model: cp_model_pb2.CpModelProto,
850        domain: Union[int, sorted_interval_list.Domain],
851        name: Optional[str],
852    ) -> None:
853        """See CpModel.new_int_var below."""
854        self.__index: int
855        self.__var: cp_model_pb2.IntegerVariableProto
856        self.__negation: Optional[_NotBooleanVariable] = None
857        # Python do not support multiple __init__ methods.
858        # This method is only called from the CpModel class.
859        # We hack the parameter to support the two cases:
860        # case 1:
861        #     model is a CpModelProto, domain is a Domain, and name is a string.
862        # case 2:
863        #     model is a CpModelProto, domain is an index (int), and name is None.
864        if isinstance(domain, IntegralTypes) and name is None:
865            self.__index = int(domain)
866            self.__var = model.variables[domain]
867        else:
868            self.__index = len(model.variables)
869            self.__var = model.variables.add()
870            self.__var.domain.extend(
871                cast(sorted_interval_list.Domain, domain).flattened_intervals()
872            )
873            if name is not None:
874                self.__var.name = name
875
876    @property
877    def index(self) -> int:
878        """Returns the index of the variable in the model."""
879        return self.__index
880
881    @property
882    def proto(self) -> cp_model_pb2.IntegerVariableProto:
883        """Returns the variable protobuf."""
884        return self.__var
885
886    def is_equal_to(self, other: Any) -> bool:
887        """Returns true if self == other in the python sense."""
888        if not isinstance(other, IntVar):
889            return False
890        return self.index == other.index
891
892    def __str__(self) -> str:
893        if not self.__var.name:
894            if (
895                len(self.__var.domain) == 2
896                and self.__var.domain[0] == self.__var.domain[1]
897            ):
898                # Special case for constants.
899                return str(self.__var.domain[0])
900            else:
901                return "unnamed_var_%i" % self.__index
902        return self.__var.name
903
904    def __repr__(self) -> str:
905        return "%s(%s)" % (self.__var.name, display_bounds(self.__var.domain))
906
907    @property
908    def name(self) -> str:
909        if not self.__var or not self.__var.name:
910            return ""
911        return self.__var.name
912
913    def negated(self) -> "_NotBooleanVariable":
914        """Returns the negation of a Boolean variable.
915
916        This method implements the logical negation of a Boolean variable.
917        It is only valid if the variable has a Boolean domain (0 or 1).
918
919        Note that this method is nilpotent: `x.negated().negated() == x`.
920        """
921
922        for bound in self.__var.domain:
923            if bound < 0 or bound > 1:
924                raise TypeError(
925                    f"cannot call negated on a non boolean variable: {self}"
926                )
927        if self.__negation is None:
928            self.__negation = _NotBooleanVariable(self)
929        return self.__negation
930
931    def __invert__(self) -> "_NotBooleanVariable":
932        """Returns the logical negation of a Boolean variable."""
933        return self.negated()
934
935    # Pre PEP8 compatibility.
936    # pylint: disable=invalid-name
937    Not = negated
938
939    def Name(self) -> str:
940        return self.name
941
942    def Proto(self) -> cp_model_pb2.IntegerVariableProto:
943        return self.proto
944
945    def Index(self) -> int:
946        return self.index
947
948    # pylint: enable=invalid-name

An integer variable.

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

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

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

IntVar( model: ortools.sat.cp_model_pb2.CpModelProto, domain: Union[int, Domain], name: Optional[str])
847    def __init__(
848        self,
849        model: cp_model_pb2.CpModelProto,
850        domain: Union[int, sorted_interval_list.Domain],
851        name: Optional[str],
852    ) -> None:
853        """See CpModel.new_int_var below."""
854        self.__index: int
855        self.__var: cp_model_pb2.IntegerVariableProto
856        self.__negation: Optional[_NotBooleanVariable] = None
857        # Python do not support multiple __init__ methods.
858        # This method is only called from the CpModel class.
859        # We hack the parameter to support the two cases:
860        # case 1:
861        #     model is a CpModelProto, domain is a Domain, and name is a string.
862        # case 2:
863        #     model is a CpModelProto, domain is an index (int), and name is None.
864        if isinstance(domain, IntegralTypes) and name is None:
865            self.__index = int(domain)
866            self.__var = model.variables[domain]
867        else:
868            self.__index = len(model.variables)
869            self.__var = model.variables.add()
870            self.__var.domain.extend(
871                cast(sorted_interval_list.Domain, domain).flattened_intervals()
872            )
873            if name is not None:
874                self.__var.name = name
index: int
876    @property
877    def index(self) -> int:
878        """Returns the index of the variable in the model."""
879        return self.__index

Returns the index of the variable in the model.

881    @property
882    def proto(self) -> cp_model_pb2.IntegerVariableProto:
883        """Returns the variable protobuf."""
884        return self.__var

Returns the variable protobuf.

def is_equal_to(self, other: Any) -> bool:
886    def is_equal_to(self, other: Any) -> bool:
887        """Returns true if self == other in the python sense."""
888        if not isinstance(other, IntVar):
889            return False
890        return self.index == other.index

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

name: str
907    @property
908    def name(self) -> str:
909        if not self.__var or not self.__var.name:
910            return ""
911        return self.__var.name
def negated(self) -> ortools.sat.python.cp_model._NotBooleanVariable:
913    def negated(self) -> "_NotBooleanVariable":
914        """Returns the negation of a Boolean variable.
915
916        This method implements the logical negation of a Boolean variable.
917        It is only valid if the variable has a Boolean domain (0 or 1).
918
919        Note that this method is nilpotent: `x.negated().negated() == x`.
920        """
921
922        for bound in self.__var.domain:
923            if bound < 0 or bound > 1:
924                raise TypeError(
925                    f"cannot call negated on a non boolean variable: {self}"
926                )
927        if self.__negation is None:
928            self.__negation = _NotBooleanVariable(self)
929        return self.__negation

Returns the negation of a Boolean variable.

This method implements the logical negation of a Boolean variable. It is only valid if the variable has a Boolean domain (0 or 1).

Note that this method is nilpotent: x.negated().negated() == x.

def Not(self) -> ortools.sat.python.cp_model._NotBooleanVariable:
913    def negated(self) -> "_NotBooleanVariable":
914        """Returns the negation of a Boolean variable.
915
916        This method implements the logical negation of a Boolean variable.
917        It is only valid if the variable has a Boolean domain (0 or 1).
918
919        Note that this method is nilpotent: `x.negated().negated() == x`.
920        """
921
922        for bound in self.__var.domain:
923            if bound < 0 or bound > 1:
924                raise TypeError(
925                    f"cannot call negated on a non boolean variable: {self}"
926                )
927        if self.__negation is None:
928            self.__negation = _NotBooleanVariable(self)
929        return self.__negation

Returns the negation of a Boolean variable.

This method implements the logical negation of a Boolean variable. It is only valid if the variable has a Boolean domain (0 or 1).

Note that this method is nilpotent: x.negated().negated() == x.

def Name(self) -> str:
939    def Name(self) -> str:
940        return self.name
def Proto(self) -> ortools.sat.cp_model_pb2.IntegerVariableProto:
942    def Proto(self) -> cp_model_pb2.IntegerVariableProto:
943        return self.proto
def Index(self) -> int:
945    def Index(self) -> int:
946        return self.index
class BoundedLinearExpression:
 991class BoundedLinearExpression:
 992    """Represents a linear constraint: `lb <= linear expression <= ub`.
 993
 994    The only use of this class is to be added to the CpModel through
 995    `CpModel.add(expression)`, as in:
 996
 997        model.add(x + 2 * y -1 >= z)
 998    """
 999
1000    def __init__(self, expr: LinearExprT, bounds: Sequence[int]) -> None:
1001        self.__expr: LinearExprT = expr
1002        self.__bounds: Sequence[int] = bounds
1003
1004    def __str__(self):
1005        if len(self.__bounds) == 2:
1006            lb, ub = self.__bounds
1007            if lb > INT_MIN and ub < INT_MAX:
1008                if lb == ub:
1009                    return str(self.__expr) + " == " + str(lb)
1010                else:
1011                    return str(lb) + " <= " + str(self.__expr) + " <= " + str(ub)
1012            elif lb > INT_MIN:
1013                return str(self.__expr) + " >= " + str(lb)
1014            elif ub < INT_MAX:
1015                return str(self.__expr) + " <= " + str(ub)
1016            else:
1017                return "True (unbounded expr " + str(self.__expr) + ")"
1018        elif (
1019            len(self.__bounds) == 4
1020            and self.__bounds[0] == INT_MIN
1021            and self.__bounds[1] + 2 == self.__bounds[2]
1022            and self.__bounds[3] == INT_MAX
1023        ):
1024            return str(self.__expr) + " != " + str(self.__bounds[1] + 1)
1025        else:
1026            return str(self.__expr) + " in [" + display_bounds(self.__bounds) + "]"
1027
1028    def expression(self) -> LinearExprT:
1029        return self.__expr
1030
1031    def bounds(self) -> Sequence[int]:
1032        return self.__bounds
1033
1034    def __bool__(self) -> bool:
1035        expr = self.__expr
1036        if isinstance(expr, LinearExpr):
1037            coeffs_map, constant = expr.get_integer_var_value_map()
1038            all_coeffs = set(coeffs_map.values())
1039            same_var = set([0])
1040            eq_bounds = [0, 0]
1041            different_vars = set([-1, 1])
1042            ne_bounds = [INT_MIN, -1, 1, INT_MAX]
1043            if (
1044                len(coeffs_map) == 1
1045                and all_coeffs == same_var
1046                and constant == 0
1047                and (self.__bounds == eq_bounds or self.__bounds == ne_bounds)
1048            ):
1049                return self.__bounds == eq_bounds
1050            if (
1051                len(coeffs_map) == 2
1052                and all_coeffs == different_vars
1053                and constant == 0
1054                and (self.__bounds == eq_bounds or self.__bounds == ne_bounds)
1055            ):
1056                return self.__bounds == ne_bounds
1057
1058        raise NotImplementedError(
1059            f'Evaluating a BoundedLinearExpression "{self}" as a Boolean value'
1060            + " is not supported."
1061        )

Represents a linear constraint: lb <= linear expression <= ub.

The only use of this class is to be added to the CpModel through CpModel.add(expression), as in:

model.add(x + 2 * y -1 >= z)
BoundedLinearExpression( expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], bounds: Sequence[int])
1000    def __init__(self, expr: LinearExprT, bounds: Sequence[int]) -> None:
1001        self.__expr: LinearExprT = expr
1002        self.__bounds: Sequence[int] = bounds
def expression( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
1028    def expression(self) -> LinearExprT:
1029        return self.__expr
def bounds(self) -> Sequence[int]:
1031    def bounds(self) -> Sequence[int]:
1032        return self.__bounds
class Constraint:
1064class Constraint:
1065    """Base class for constraints.
1066
1067    Constraints are built by the CpModel through the add<XXX> methods.
1068    Once created by the CpModel class, they are automatically added to the model.
1069    The purpose of this class is to allow specification of enforcement literals
1070    for this constraint.
1071
1072        b = model.new_bool_var('b')
1073        x = model.new_int_var(0, 10, 'x')
1074        y = model.new_int_var(0, 10, 'y')
1075
1076        model.add(x + 2 * y == 5).only_enforce_if(b.negated())
1077    """
1078
1079    def __init__(
1080        self,
1081        cp_model: "CpModel",
1082    ) -> None:
1083        self.__index: int = len(cp_model.proto.constraints)
1084        self.__cp_model: "CpModel" = cp_model
1085        self.__constraint: cp_model_pb2.ConstraintProto = (
1086            cp_model.proto.constraints.add()
1087        )
1088
1089    @overload
1090    def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": ...
1091
1092    @overload
1093    def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": ...
1094
1095    def only_enforce_if(self, *boolvar) -> "Constraint":
1096        """Adds an enforcement literal to the constraint.
1097
1098        This method adds one or more literals (that is, a boolean variable or its
1099        negation) as enforcement literals. The conjunction of all these literals
1100        determines whether the constraint is active or not. It acts as an
1101        implication, so if the conjunction is true, it implies that the constraint
1102        must be enforced. If it is false, then the constraint is ignored.
1103
1104        BoolOr, BoolAnd, and linear constraints all support enforcement literals.
1105
1106        Args:
1107          *boolvar: One or more Boolean literals.
1108
1109        Returns:
1110          self.
1111        """
1112        for lit in expand_generator_or_tuple(boolvar):
1113            if (cmh.is_boolean(lit) and lit) or (
1114                isinstance(lit, IntegralTypes) and lit == 1
1115            ):
1116                # Always true. Do nothing.
1117                pass
1118            elif (cmh.is_boolean(lit) and not lit) or (
1119                isinstance(lit, IntegralTypes) and lit == 0
1120            ):
1121                self.__constraint.enforcement_literal.append(
1122                    self.__cp_model.new_constant(0).index
1123                )
1124            else:
1125                self.__constraint.enforcement_literal.append(
1126                    cast(Union[IntVar, _NotBooleanVariable], lit).index
1127                )
1128        return self
1129
1130    def with_name(self, name: str) -> "Constraint":
1131        """Sets the name of the constraint."""
1132        if name:
1133            self.__constraint.name = name
1134        else:
1135            self.__constraint.ClearField("name")
1136        return self
1137
1138    @property
1139    def name(self) -> str:
1140        """Returns the name of the constraint."""
1141        if not self.__constraint or not self.__constraint.name:
1142            return ""
1143        return self.__constraint.name
1144
1145    @property
1146    def index(self) -> int:
1147        """Returns the index of the constraint in the model."""
1148        return self.__index
1149
1150    @property
1151    def proto(self) -> cp_model_pb2.ConstraintProto:
1152        """Returns the constraint protobuf."""
1153        return self.__constraint
1154
1155    # Pre PEP8 compatibility.
1156    # pylint: disable=invalid-name
1157    OnlyEnforceIf = only_enforce_if
1158    WithName = with_name
1159
1160    def Name(self) -> str:
1161        return self.name
1162
1163    def Index(self) -> int:
1164        return self.index
1165
1166    def Proto(self) -> cp_model_pb2.ConstraintProto:
1167        return self.proto
1168
1169    # pylint: enable=invalid-name

Base class for constraints.

Constraints are built by the CpModel through the add 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)
1079    def __init__(
1080        self,
1081        cp_model: "CpModel",
1082    ) -> None:
1083        self.__index: int = len(cp_model.proto.constraints)
1084        self.__cp_model: "CpModel" = cp_model
1085        self.__constraint: cp_model_pb2.ConstraintProto = (
1086            cp_model.proto.constraints.add()
1087        )
def only_enforce_if(self, *boolvar) -> Constraint:
1095    def only_enforce_if(self, *boolvar) -> "Constraint":
1096        """Adds an enforcement literal to the constraint.
1097
1098        This method adds one or more literals (that is, a boolean variable or its
1099        negation) as enforcement literals. The conjunction of all these literals
1100        determines whether the constraint is active or not. It acts as an
1101        implication, so if the conjunction is true, it implies that the constraint
1102        must be enforced. If it is false, then the constraint is ignored.
1103
1104        BoolOr, BoolAnd, and linear constraints all support enforcement literals.
1105
1106        Args:
1107          *boolvar: One or more Boolean literals.
1108
1109        Returns:
1110          self.
1111        """
1112        for lit in expand_generator_or_tuple(boolvar):
1113            if (cmh.is_boolean(lit) and lit) or (
1114                isinstance(lit, IntegralTypes) and lit == 1
1115            ):
1116                # Always true. Do nothing.
1117                pass
1118            elif (cmh.is_boolean(lit) and not lit) or (
1119                isinstance(lit, IntegralTypes) and lit == 0
1120            ):
1121                self.__constraint.enforcement_literal.append(
1122                    self.__cp_model.new_constant(0).index
1123                )
1124            else:
1125                self.__constraint.enforcement_literal.append(
1126                    cast(Union[IntVar, _NotBooleanVariable], lit).index
1127                )
1128        return self

Adds an enforcement literal to the constraint.

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

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

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

self.

def with_name(self, name: str) -> Constraint:
1130    def with_name(self, name: str) -> "Constraint":
1131        """Sets the name of the constraint."""
1132        if name:
1133            self.__constraint.name = name
1134        else:
1135            self.__constraint.ClearField("name")
1136        return self

Sets the name of the constraint.

name: str
1138    @property
1139    def name(self) -> str:
1140        """Returns the name of the constraint."""
1141        if not self.__constraint or not self.__constraint.name:
1142            return ""
1143        return self.__constraint.name

Returns the name of the constraint.

index: int
1145    @property
1146    def index(self) -> int:
1147        """Returns the index of the constraint in the model."""
1148        return self.__index

Returns the index of the constraint in the model.

1150    @property
1151    def proto(self) -> cp_model_pb2.ConstraintProto:
1152        """Returns the constraint protobuf."""
1153        return self.__constraint

Returns the constraint protobuf.

def OnlyEnforceIf(self, *boolvar) -> Constraint:
1095    def only_enforce_if(self, *boolvar) -> "Constraint":
1096        """Adds an enforcement literal to the constraint.
1097
1098        This method adds one or more literals (that is, a boolean variable or its
1099        negation) as enforcement literals. The conjunction of all these literals
1100        determines whether the constraint is active or not. It acts as an
1101        implication, so if the conjunction is true, it implies that the constraint
1102        must be enforced. If it is false, then the constraint is ignored.
1103
1104        BoolOr, BoolAnd, and linear constraints all support enforcement literals.
1105
1106        Args:
1107          *boolvar: One or more Boolean literals.
1108
1109        Returns:
1110          self.
1111        """
1112        for lit in expand_generator_or_tuple(boolvar):
1113            if (cmh.is_boolean(lit) and lit) or (
1114                isinstance(lit, IntegralTypes) and lit == 1
1115            ):
1116                # Always true. Do nothing.
1117                pass
1118            elif (cmh.is_boolean(lit) and not lit) or (
1119                isinstance(lit, IntegralTypes) and lit == 0
1120            ):
1121                self.__constraint.enforcement_literal.append(
1122                    self.__cp_model.new_constant(0).index
1123                )
1124            else:
1125                self.__constraint.enforcement_literal.append(
1126                    cast(Union[IntVar, _NotBooleanVariable], lit).index
1127                )
1128        return self

Adds an enforcement literal to the constraint.

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

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

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

self.

def WithName(self, name: str) -> Constraint:
1130    def with_name(self, name: str) -> "Constraint":
1131        """Sets the name of the constraint."""
1132        if name:
1133            self.__constraint.name = name
1134        else:
1135            self.__constraint.ClearField("name")
1136        return self

Sets the name of the constraint.

def Name(self) -> str:
1160    def Name(self) -> str:
1161        return self.name
def Index(self) -> int:
1163    def Index(self) -> int:
1164        return self.index
def Proto(self) -> ortools.sat.cp_model_pb2.ConstraintProto:
1166    def Proto(self) -> cp_model_pb2.ConstraintProto:
1167        return self.proto
class IntervalVar:
1172class IntervalVar:
1173    """Represents an Interval variable.
1174
1175    An interval variable is both a constraint and a variable. It is defined by
1176    three integer variables: start, size, and end.
1177
1178    It is a constraint because, internally, it enforces that start + size == end.
1179
1180    It is also a variable as it can appear in specific scheduling constraints:
1181    NoOverlap, NoOverlap2D, Cumulative.
1182
1183    Optionally, an enforcement literal can be added to this constraint, in which
1184    case these scheduling constraints will ignore interval variables with
1185    enforcement literals assigned to false. Conversely, these constraints will
1186    also set these enforcement literals to false if they cannot fit these
1187    intervals into the schedule.
1188
1189    Raises:
1190      ValueError: if start, size, end are not defined, or have the wrong type.
1191    """
1192
1193    def __init__(
1194        self,
1195        model: cp_model_pb2.CpModelProto,
1196        start: Union[cp_model_pb2.LinearExpressionProto, int],
1197        size: Optional[cp_model_pb2.LinearExpressionProto],
1198        end: Optional[cp_model_pb2.LinearExpressionProto],
1199        is_present_index: Optional[int],
1200        name: Optional[str],
1201    ) -> None:
1202        self.__model: cp_model_pb2.CpModelProto = model
1203        self.__index: int
1204        self.__ct: cp_model_pb2.ConstraintProto
1205        # As with the IntVar::__init__ method, we hack the __init__ method to
1206        # support two use cases:
1207        #   case 1: called when creating a new interval variable.
1208        #      {start|size|end} are linear expressions, is_present_index is either
1209        #      None or the index of a Boolean literal. name is a string
1210        #   case 2: called when querying an existing interval variable.
1211        #      start_index is an int, all parameters after are None.
1212        if isinstance(start, int):
1213            if size is not None:
1214                raise ValueError("size should be None")
1215            if end is not None:
1216                raise ValueError("end should be None")
1217            if is_present_index is not None:
1218                raise ValueError("is_present_index should be None")
1219            self.__index = cast(int, start)
1220            self.__ct = model.constraints[self.__index]
1221        else:
1222            self.__index = len(model.constraints)
1223            self.__ct = self.__model.constraints.add()
1224            if start is None:
1225                raise TypeError("start is not defined")
1226            self.__ct.interval.start.CopyFrom(start)
1227            if size is None:
1228                raise TypeError("size is not defined")
1229            self.__ct.interval.size.CopyFrom(size)
1230            if end is None:
1231                raise TypeError("end is not defined")
1232            self.__ct.interval.end.CopyFrom(end)
1233            if is_present_index is not None:
1234                self.__ct.enforcement_literal.append(is_present_index)
1235            if name:
1236                self.__ct.name = name
1237
1238    @property
1239    def index(self) -> int:
1240        """Returns the index of the interval constraint in the model."""
1241        return self.__index
1242
1243    @property
1244    def proto(self) -> cp_model_pb2.IntervalConstraintProto:
1245        """Returns the interval protobuf."""
1246        return self.__ct.interval
1247
1248    def __str__(self):
1249        return self.__ct.name
1250
1251    def __repr__(self):
1252        interval = self.__ct.interval
1253        if self.__ct.enforcement_literal:
1254            return "%s(start = %s, size = %s, end = %s, is_present = %s)" % (
1255                self.__ct.name,
1256                short_expr_name(self.__model, interval.start),
1257                short_expr_name(self.__model, interval.size),
1258                short_expr_name(self.__model, interval.end),
1259                short_name(self.__model, self.__ct.enforcement_literal[0]),
1260            )
1261        else:
1262            return "%s(start = %s, size = %s, end = %s)" % (
1263                self.__ct.name,
1264                short_expr_name(self.__model, interval.start),
1265                short_expr_name(self.__model, interval.size),
1266                short_expr_name(self.__model, interval.end),
1267            )
1268
1269    @property
1270    def name(self) -> str:
1271        if not self.__ct or not self.__ct.name:
1272            return ""
1273        return self.__ct.name
1274
1275    def start_expr(self) -> LinearExprT:
1276        return LinearExpr.rebuild_from_linear_expression_proto(
1277            self.__model, self.__ct.interval.start
1278        )
1279
1280    def size_expr(self) -> LinearExprT:
1281        return LinearExpr.rebuild_from_linear_expression_proto(
1282            self.__model, self.__ct.interval.size
1283        )
1284
1285    def end_expr(self) -> LinearExprT:
1286        return LinearExpr.rebuild_from_linear_expression_proto(
1287            self.__model, self.__ct.interval.end
1288        )
1289
1290    # Pre PEP8 compatibility.
1291    # pylint: disable=invalid-name
1292    def Name(self) -> str:
1293        return self.name
1294
1295    def Index(self) -> int:
1296        return self.index
1297
1298    def Proto(self) -> cp_model_pb2.IntervalConstraintProto:
1299        return self.proto
1300
1301    StartExpr = start_expr
1302    SizeExpr = size_expr
1303    EndExpr = end_expr
1304
1305    # pylint: enable=invalid-name

Represents an Interval variable.

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

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

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

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

Raises:
  • ValueError: if start, size, end are not defined, or have the wrong type.
IntervalVar( model: ortools.sat.cp_model_pb2.CpModelProto, 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])
1193    def __init__(
1194        self,
1195        model: cp_model_pb2.CpModelProto,
1196        start: Union[cp_model_pb2.LinearExpressionProto, int],
1197        size: Optional[cp_model_pb2.LinearExpressionProto],
1198        end: Optional[cp_model_pb2.LinearExpressionProto],
1199        is_present_index: Optional[int],
1200        name: Optional[str],
1201    ) -> None:
1202        self.__model: cp_model_pb2.CpModelProto = model
1203        self.__index: int
1204        self.__ct: cp_model_pb2.ConstraintProto
1205        # As with the IntVar::__init__ method, we hack the __init__ method to
1206        # support two use cases:
1207        #   case 1: called when creating a new interval variable.
1208        #      {start|size|end} are linear expressions, is_present_index is either
1209        #      None or the index of a Boolean literal. name is a string
1210        #   case 2: called when querying an existing interval variable.
1211        #      start_index is an int, all parameters after are None.
1212        if isinstance(start, int):
1213            if size is not None:
1214                raise ValueError("size should be None")
1215            if end is not None:
1216                raise ValueError("end should be None")
1217            if is_present_index is not None:
1218                raise ValueError("is_present_index should be None")
1219            self.__index = cast(int, start)
1220            self.__ct = model.constraints[self.__index]
1221        else:
1222            self.__index = len(model.constraints)
1223            self.__ct = self.__model.constraints.add()
1224            if start is None:
1225                raise TypeError("start is not defined")
1226            self.__ct.interval.start.CopyFrom(start)
1227            if size is None:
1228                raise TypeError("size is not defined")
1229            self.__ct.interval.size.CopyFrom(size)
1230            if end is None:
1231                raise TypeError("end is not defined")
1232            self.__ct.interval.end.CopyFrom(end)
1233            if is_present_index is not None:
1234                self.__ct.enforcement_literal.append(is_present_index)
1235            if name:
1236                self.__ct.name = name
index: int
1238    @property
1239    def index(self) -> int:
1240        """Returns the index of the interval constraint in the model."""
1241        return self.__index

Returns the index of the interval constraint in the model.

1243    @property
1244    def proto(self) -> cp_model_pb2.IntervalConstraintProto:
1245        """Returns the interval protobuf."""
1246        return self.__ct.interval

Returns the interval protobuf.

name: str
1269    @property
1270    def name(self) -> str:
1271        if not self.__ct or not self.__ct.name:
1272            return ""
1273        return self.__ct.name
def start_expr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
1275    def start_expr(self) -> LinearExprT:
1276        return LinearExpr.rebuild_from_linear_expression_proto(
1277            self.__model, self.__ct.interval.start
1278        )
def size_expr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
1280    def size_expr(self) -> LinearExprT:
1281        return LinearExpr.rebuild_from_linear_expression_proto(
1282            self.__model, self.__ct.interval.size
1283        )
def end_expr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
1285    def end_expr(self) -> LinearExprT:
1286        return LinearExpr.rebuild_from_linear_expression_proto(
1287            self.__model, self.__ct.interval.end
1288        )
def Name(self) -> str:
1292    def Name(self) -> str:
1293        return self.name
def Index(self) -> int:
1295    def Index(self) -> int:
1296        return self.index
def Proto(self) -> ortools.sat.cp_model_pb2.IntervalConstraintProto:
1298    def Proto(self) -> cp_model_pb2.IntervalConstraintProto:
1299        return self.proto
def StartExpr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
1275    def start_expr(self) -> LinearExprT:
1276        return LinearExpr.rebuild_from_linear_expression_proto(
1277            self.__model, self.__ct.interval.start
1278        )
def SizeExpr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
1280    def size_expr(self) -> LinearExprT:
1281        return LinearExpr.rebuild_from_linear_expression_proto(
1282            self.__model, self.__ct.interval.size
1283        )
def EndExpr( self) -> Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]:
1285    def end_expr(self) -> LinearExprT:
1286        return LinearExpr.rebuild_from_linear_expression_proto(
1287            self.__model, self.__ct.interval.end
1288        )
def object_is_a_true_literal( literal: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
1308def object_is_a_true_literal(literal: LiteralT) -> bool:
1309    """Checks if literal is either True, or a Boolean literals fixed to True."""
1310    if isinstance(literal, IntVar):
1311        proto = literal.proto
1312        return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1
1313    if isinstance(literal, _NotBooleanVariable):
1314        proto = literal.negated().proto
1315        return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0
1316    if isinstance(literal, IntegralTypes):
1317        return int(literal) == 1
1318    return False

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

def object_is_a_false_literal( literal: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
1321def object_is_a_false_literal(literal: LiteralT) -> bool:
1322    """Checks if literal is either False, or a Boolean literals fixed to False."""
1323    if isinstance(literal, IntVar):
1324        proto = literal.proto
1325        return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0
1326    if isinstance(literal, _NotBooleanVariable):
1327        proto = literal.negated().proto
1328        return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1
1329    if isinstance(literal, IntegralTypes):
1330        return int(literal) == 0
1331    return False

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

class CpModel:
1334class CpModel:
1335    """Methods for building a CP model.
1336
1337    Methods beginning with:
1338
1339    * ```New``` create integer, boolean, or interval variables.
1340    * ```add``` create new constraints and add them to the model.
1341    """
1342
1343    def __init__(self) -> None:
1344        self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto()
1345        self.__constant_map: Dict[IntegralT, int] = {}
1346
1347    # Naming.
1348    @property
1349    def name(self) -> str:
1350        """Returns the name of the model."""
1351        if not self.__model or not self.__model.name:
1352            return ""
1353        return self.__model.name
1354
1355    @name.setter
1356    def name(self, name: str):
1357        """Sets the name of the model."""
1358        self.__model.name = name
1359
1360    # Integer variable.
1361
1362    def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar:
1363        """Create an integer variable with domain [lb, ub].
1364
1365        The CP-SAT solver is limited to integer variables. If you have fractional
1366        values, scale them up so that they become integers; if you have strings,
1367        encode them as integers.
1368
1369        Args:
1370          lb: Lower bound for the variable.
1371          ub: Upper bound for the variable.
1372          name: The name of the variable.
1373
1374        Returns:
1375          a variable whose domain is [lb, ub].
1376        """
1377
1378        return IntVar(self.__model, sorted_interval_list.Domain(lb, ub), name)
1379
1380    def new_int_var_from_domain(
1381        self, domain: sorted_interval_list.Domain, name: str
1382    ) -> IntVar:
1383        """Create an integer variable from a domain.
1384
1385        A domain is a set of integers specified by a collection of intervals.
1386        For example, `model.new_int_var_from_domain(cp_model.
1387             Domain.from_intervals([[1, 2], [4, 6]]), 'x')`
1388
1389        Args:
1390          domain: An instance of the Domain class.
1391          name: The name of the variable.
1392
1393        Returns:
1394            a variable whose domain is the given domain.
1395        """
1396        return IntVar(self.__model, domain, name)
1397
1398    def new_bool_var(self, name: str) -> IntVar:
1399        """Creates a 0-1 variable with the given name."""
1400        return IntVar(self.__model, sorted_interval_list.Domain(0, 1), name)
1401
1402    def new_constant(self, value: IntegralT) -> IntVar:
1403        """Declares a constant integer."""
1404        return IntVar(self.__model, self.get_or_make_index_from_constant(value), None)
1405
1406    def new_int_var_series(
1407        self,
1408        name: str,
1409        index: pd.Index,
1410        lower_bounds: Union[IntegralT, pd.Series],
1411        upper_bounds: Union[IntegralT, pd.Series],
1412    ) -> pd.Series:
1413        """Creates a series of (scalar-valued) variables with the given name.
1414
1415        Args:
1416          name (str): Required. The name of the variable set.
1417          index (pd.Index): Required. The index to use for the variable set.
1418          lower_bounds (Union[int, pd.Series]): A lower bound for variables in the
1419            set. If a `pd.Series` is passed in, it will be based on the
1420            corresponding values of the pd.Series.
1421          upper_bounds (Union[int, pd.Series]): An upper bound for variables in the
1422            set. If a `pd.Series` is passed in, it will be based on the
1423            corresponding values of the pd.Series.
1424
1425        Returns:
1426          pd.Series: The variable set indexed by its corresponding dimensions.
1427
1428        Raises:
1429          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1430          ValueError: if the `name` is not a valid identifier or already exists.
1431          ValueError: if the `lowerbound` is greater than the `upperbound`.
1432          ValueError: if the index of `lower_bound`, or `upper_bound` does not match
1433          the input index.
1434        """
1435        if not isinstance(index, pd.Index):
1436            raise TypeError("Non-index object is used as index")
1437        if not name.isidentifier():
1438            raise ValueError("name={} is not a valid identifier".format(name))
1439        if (
1440            isinstance(lower_bounds, IntegralTypes)
1441            and isinstance(upper_bounds, IntegralTypes)
1442            and lower_bounds > upper_bounds
1443        ):
1444            raise ValueError(
1445                f"lower_bound={lower_bounds} is greater than"
1446                f" upper_bound={upper_bounds} for variable set={name}"
1447            )
1448
1449        lower_bounds = _convert_to_integral_series_and_validate_index(
1450            lower_bounds, index
1451        )
1452        upper_bounds = _convert_to_integral_series_and_validate_index(
1453            upper_bounds, index
1454        )
1455        return pd.Series(
1456            index=index,
1457            data=[
1458                # pylint: disable=g-complex-comprehension
1459                IntVar(
1460                    model=self.__model,
1461                    name=f"{name}[{i}]",
1462                    domain=sorted_interval_list.Domain(
1463                        lower_bounds[i], upper_bounds[i]
1464                    ),
1465                )
1466                for i in index
1467            ],
1468        )
1469
1470    def new_bool_var_series(
1471        self,
1472        name: str,
1473        index: pd.Index,
1474    ) -> pd.Series:
1475        """Creates a series of (scalar-valued) variables with the given name.
1476
1477        Args:
1478          name (str): Required. The name of the variable set.
1479          index (pd.Index): Required. The index to use for the variable set.
1480
1481        Returns:
1482          pd.Series: The variable set indexed by its corresponding dimensions.
1483
1484        Raises:
1485          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1486          ValueError: if the `name` is not a valid identifier or already exists.
1487        """
1488        return self.new_int_var_series(
1489            name=name, index=index, lower_bounds=0, upper_bounds=1
1490        )
1491
1492    # Linear constraints.
1493
1494    def add_linear_constraint(
1495        self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT
1496    ) -> Constraint:
1497        """Adds the constraint: `lb <= linear_expr <= ub`."""
1498        return self.add_linear_expression_in_domain(
1499            linear_expr, sorted_interval_list.Domain(lb, ub)
1500        )
1501
1502    def add_linear_expression_in_domain(
1503        self, linear_expr: LinearExprT, domain: sorted_interval_list.Domain
1504    ) -> Constraint:
1505        """Adds the constraint: `linear_expr` in `domain`."""
1506        if isinstance(linear_expr, LinearExpr):
1507            ct = Constraint(self)
1508            model_ct = self.__model.constraints[ct.index]
1509            coeffs_map, constant = linear_expr.get_integer_var_value_map()
1510            for t in coeffs_map.items():
1511                if not isinstance(t[0], IntVar):
1512                    raise TypeError("Wrong argument" + str(t))
1513                c = cmh.assert_is_int64(t[1])
1514                model_ct.linear.vars.append(t[0].index)
1515                model_ct.linear.coeffs.append(c)
1516            model_ct.linear.domain.extend(
1517                [
1518                    cmh.capped_subtraction(x, constant)
1519                    for x in domain.flattened_intervals()
1520                ]
1521            )
1522            return ct
1523        if isinstance(linear_expr, IntegralTypes):
1524            if not domain.contains(int(linear_expr)):
1525                return self.add_bool_or([])  # Evaluate to false.
1526            else:
1527                return self.add_bool_and([])  # Evaluate to true.
1528        raise TypeError(
1529            "not supported: CpModel.add_linear_expression_in_domain("
1530            + str(linear_expr)
1531            + " "
1532            + str(domain)
1533            + ")"
1534        )
1535
1536    @overload
1537    def add(self, ct: BoundedLinearExpression) -> Constraint: ...
1538
1539    @overload
1540    def add(self, ct: Union[bool, np.bool_]) -> Constraint: ...
1541
1542    def add(self, ct):
1543        """Adds a `BoundedLinearExpression` to the model.
1544
1545        Args:
1546          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1547
1548        Returns:
1549          An instance of the `Constraint` class.
1550        """
1551        if isinstance(ct, BoundedLinearExpression):
1552            return self.add_linear_expression_in_domain(
1553                ct.expression(),
1554                sorted_interval_list.Domain.from_flat_intervals(ct.bounds()),
1555            )
1556        if ct and cmh.is_boolean(ct):
1557            return self.add_bool_or([True])
1558        if not ct and cmh.is_boolean(ct):
1559            return self.add_bool_or([])  # Evaluate to false.
1560        raise TypeError("not supported: CpModel.add(" + str(ct) + ")")
1561
1562    # General Integer Constraints.
1563
1564    @overload
1565    def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ...
1566
1567    @overload
1568    def add_all_different(self, *expressions: LinearExprT) -> Constraint: ...
1569
1570    def add_all_different(self, *expressions):
1571        """Adds AllDifferent(expressions).
1572
1573        This constraint forces all expressions to have different values.
1574
1575        Args:
1576          *expressions: simple expressions of the form a * var + constant.
1577
1578        Returns:
1579          An instance of the `Constraint` class.
1580        """
1581        ct = Constraint(self)
1582        model_ct = self.__model.constraints[ct.index]
1583        expanded = expand_generator_or_tuple(expressions)
1584        model_ct.all_diff.exprs.extend(
1585            self.parse_linear_expression(x) for x in expanded
1586        )
1587        return ct
1588
1589    def add_element(
1590        self, index: VariableT, variables: Sequence[VariableT], target: VariableT
1591    ) -> Constraint:
1592        """Adds the element constraint: `variables[index] == target`.
1593
1594        Args:
1595          index: The index of the variable that's being constrained.
1596          variables: A list of variables.
1597          target: The value that the variable must be equal to.
1598
1599        Returns:
1600          An instance of the `Constraint` class.
1601        """
1602
1603        if not variables:
1604            raise ValueError("add_element expects a non-empty variables array")
1605
1606        if isinstance(index, IntegralTypes):
1607            variable: VariableT = list(variables)[int(index)]
1608            return self.add(variable == target)
1609
1610        ct = Constraint(self)
1611        model_ct = self.__model.constraints[ct.index]
1612        model_ct.element.index = self.get_or_make_index(index)
1613        model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables])
1614        model_ct.element.target = self.get_or_make_index(target)
1615        return ct
1616
1617    def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1618        """Adds Circuit(arcs).
1619
1620        Adds a circuit constraint from a sparse list of arcs that encode the graph.
1621
1622        A circuit is a unique Hamiltonian path in a subgraph of the total
1623        graph. In case a node 'i' is not in the path, then there must be a
1624        loop arc 'i -> i' associated with a true literal. Otherwise
1625        this constraint will fail.
1626
1627        Args:
1628          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1629            literal). The arc is selected in the circuit if the literal is true.
1630            Both source_node and destination_node must be integers between 0 and the
1631            number of nodes - 1.
1632
1633        Returns:
1634          An instance of the `Constraint` class.
1635
1636        Raises:
1637          ValueError: If the list of arcs is empty.
1638        """
1639        if not arcs:
1640            raise ValueError("add_circuit expects a non-empty array of arcs")
1641        ct = Constraint(self)
1642        model_ct = self.__model.constraints[ct.index]
1643        for arc in arcs:
1644            tail = cmh.assert_is_int32(arc[0])
1645            head = cmh.assert_is_int32(arc[1])
1646            lit = self.get_or_make_boolean_index(arc[2])
1647            model_ct.circuit.tails.append(tail)
1648            model_ct.circuit.heads.append(head)
1649            model_ct.circuit.literals.append(lit)
1650        return ct
1651
1652    def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1653        """Adds a multiple circuit constraint, aka the 'VRP' constraint.
1654
1655        The direct graph where arc #i (from tails[i] to head[i]) is present iff
1656        literals[i] is true must satisfy this set of properties:
1657        - #incoming arcs == 1 except for node 0.
1658        - #outgoing arcs == 1 except for node 0.
1659        - for node zero, #incoming arcs == #outgoing arcs.
1660        - There are no duplicate arcs.
1661        - Self-arcs are allowed except for node 0.
1662        - There is no cycle in this graph, except through node 0.
1663
1664        Args:
1665          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1666            literal). The arc is selected in the circuit if the literal is true.
1667            Both source_node and destination_node must be integers between 0 and the
1668            number of nodes - 1.
1669
1670        Returns:
1671          An instance of the `Constraint` class.
1672
1673        Raises:
1674          ValueError: If the list of arcs is empty.
1675        """
1676        if not arcs:
1677            raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
1678        ct = Constraint(self)
1679        model_ct = self.__model.constraints[ct.index]
1680        for arc in arcs:
1681            tail = cmh.assert_is_int32(arc[0])
1682            head = cmh.assert_is_int32(arc[1])
1683            lit = self.get_or_make_boolean_index(arc[2])
1684            model_ct.routes.tails.append(tail)
1685            model_ct.routes.heads.append(head)
1686            model_ct.routes.literals.append(lit)
1687        return ct
1688
1689    def add_allowed_assignments(
1690        self,
1691        variables: Sequence[VariableT],
1692        tuples_list: Iterable[Sequence[IntegralT]],
1693    ) -> Constraint:
1694        """Adds AllowedAssignments(variables, tuples_list).
1695
1696        An AllowedAssignments constraint is a constraint on an array of variables,
1697        which requires that when all variables are assigned values, the resulting
1698        array equals one of the  tuples in `tuple_list`.
1699
1700        Args:
1701          variables: A list of variables.
1702          tuples_list: A list of admissible tuples. Each tuple must have the same
1703            length as the variables, and the ith value of a tuple corresponds to the
1704            ith variable.
1705
1706        Returns:
1707          An instance of the `Constraint` class.
1708
1709        Raises:
1710          TypeError: If a tuple does not have the same size as the list of
1711              variables.
1712          ValueError: If the array of variables is empty.
1713        """
1714
1715        if not variables:
1716            raise ValueError(
1717                "add_allowed_assignments expects a non-empty variables array"
1718            )
1719
1720        ct: Constraint = Constraint(self)
1721        model_ct = self.__model.constraints[ct.index]
1722        model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables])
1723        arity: int = len(variables)
1724        for t in tuples_list:
1725            if len(t) != arity:
1726                raise TypeError("Tuple " + str(t) + " has the wrong arity")
1727
1728        # duck-typing (no explicit type checks here)
1729        try:
1730            model_ct.table.values.extend(a for b in tuples_list for a in b)
1731        except ValueError as ex:
1732            raise TypeError(f"add_xxx_assignment: Not an integer or does not fit in an int64_t: {ex.args}") from ex
1733
1734        return ct
1735
1736    def add_forbidden_assignments(
1737        self,
1738        variables: Sequence[VariableT],
1739        tuples_list: Iterable[Sequence[IntegralT]],
1740    ) -> Constraint:
1741        """Adds add_forbidden_assignments(variables, [tuples_list]).
1742
1743        A ForbiddenAssignments constraint is a constraint on an array of variables
1744        where the list of impossible combinations is provided in the tuples list.
1745
1746        Args:
1747          variables: A list of variables.
1748          tuples_list: A list of forbidden tuples. Each tuple must have the same
1749            length as the variables, and the *i*th value of a tuple corresponds to
1750            the *i*th variable.
1751
1752        Returns:
1753          An instance of the `Constraint` class.
1754
1755        Raises:
1756          TypeError: If a tuple does not have the same size as the list of
1757                     variables.
1758          ValueError: If the array of variables is empty.
1759        """
1760
1761        if not variables:
1762            raise ValueError(
1763                "add_forbidden_assignments expects a non-empty variables array"
1764            )
1765
1766        index = len(self.__model.constraints)
1767        ct: Constraint = self.add_allowed_assignments(variables, tuples_list)
1768        self.__model.constraints[index].table.negated = True
1769        return ct
1770
1771    def add_automaton(
1772        self,
1773        transition_variables: Sequence[VariableT],
1774        starting_state: IntegralT,
1775        final_states: Sequence[IntegralT],
1776        transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]],
1777    ) -> Constraint:
1778        """Adds an automaton constraint.
1779
1780        An automaton constraint takes a list of variables (of size *n*), an initial
1781        state, a set of final states, and a set of transitions. A transition is a
1782        triplet (*tail*, *transition*, *head*), where *tail* and *head* are states,
1783        and *transition* is the label of an arc from *head* to *tail*,
1784        corresponding to the value of one variable in the list of variables.
1785
1786        This automaton will be unrolled into a flow with *n* + 1 phases. Each phase
1787        contains the possible states of the automaton. The first state contains the
1788        initial state. The last phase contains the final states.
1789
1790        Between two consecutive phases *i* and *i* + 1, the automaton creates a set
1791        of arcs. For each transition (*tail*, *transition*, *head*), it will add
1792        an arc from the state *tail* of phase *i* and the state *head* of phase
1793        *i* + 1. This arc is labeled by the value *transition* of the variables
1794        `variables[i]`. That is, this arc can only be selected if `variables[i]`
1795        is assigned the value *transition*.
1796
1797        A feasible solution of this constraint is an assignment of variables such
1798        that, starting from the initial state in phase 0, there is a path labeled by
1799        the values of the variables that ends in one of the final states in the
1800        final phase.
1801
1802        Args:
1803          transition_variables: A non-empty list of variables whose values
1804            correspond to the labels of the arcs traversed by the automaton.
1805          starting_state: The initial state of the automaton.
1806          final_states: A non-empty list of admissible final states.
1807          transition_triples: A list of transitions for the automaton, in the
1808            following format (current_state, variable_value, next_state).
1809
1810        Returns:
1811          An instance of the `Constraint` class.
1812
1813        Raises:
1814          ValueError: if `transition_variables`, `final_states`, or
1815            `transition_triples` are empty.
1816        """
1817
1818        if not transition_variables:
1819            raise ValueError(
1820                "add_automaton expects a non-empty transition_variables array"
1821            )
1822        if not final_states:
1823            raise ValueError("add_automaton expects some final states")
1824
1825        if not transition_triples:
1826            raise ValueError("add_automaton expects some transition triples")
1827
1828        ct = Constraint(self)
1829        model_ct = self.__model.constraints[ct.index]
1830        model_ct.automaton.vars.extend(
1831            [self.get_or_make_index(x) for x in transition_variables]
1832        )
1833        starting_state = cmh.assert_is_int64(starting_state)
1834        model_ct.automaton.starting_state = starting_state
1835        for v in final_states:
1836            v = cmh.assert_is_int64(v)
1837            model_ct.automaton.final_states.append(v)
1838        for t in transition_triples:
1839            if len(t) != 3:
1840                raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)")
1841            tail = cmh.assert_is_int64(t[0])
1842            label = cmh.assert_is_int64(t[1])
1843            head = cmh.assert_is_int64(t[2])
1844            model_ct.automaton.transition_tail.append(tail)
1845            model_ct.automaton.transition_label.append(label)
1846            model_ct.automaton.transition_head.append(head)
1847        return ct
1848
1849    def add_inverse(
1850        self,
1851        variables: Sequence[VariableT],
1852        inverse_variables: Sequence[VariableT],
1853    ) -> Constraint:
1854        """Adds Inverse(variables, inverse_variables).
1855
1856        An inverse constraint enforces that if `variables[i]` is assigned a value
1857        `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa.
1858
1859        Args:
1860          variables: An array of integer variables.
1861          inverse_variables: An array of integer variables.
1862
1863        Returns:
1864          An instance of the `Constraint` class.
1865
1866        Raises:
1867          TypeError: if variables and inverse_variables have different lengths, or
1868              if they are empty.
1869        """
1870
1871        if not variables or not inverse_variables:
1872            raise TypeError("The Inverse constraint does not accept empty arrays")
1873        if len(variables) != len(inverse_variables):
1874            raise TypeError(
1875                "In the inverse constraint, the two array variables and"
1876                " inverse_variables must have the same length."
1877            )
1878        ct = Constraint(self)
1879        model_ct = self.__model.constraints[ct.index]
1880        model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables])
1881        model_ct.inverse.f_inverse.extend(
1882            [self.get_or_make_index(x) for x in inverse_variables]
1883        )
1884        return ct
1885
1886    def add_reservoir_constraint(
1887        self,
1888        times: Iterable[LinearExprT],
1889        level_changes: Iterable[LinearExprT],
1890        min_level: int,
1891        max_level: int,
1892    ) -> Constraint:
1893        """Adds Reservoir(times, level_changes, min_level, max_level).
1894
1895        Maintains a reservoir level within bounds. The water level starts at 0, and
1896        at any time, it must be between min_level and max_level.
1897
1898        If the affine expression `times[i]` is assigned a value t, then the current
1899        level changes by `level_changes[i]`, which is constant, at time t.
1900
1901         Note that min level must be <= 0, and the max level must be >= 0. Please
1902         use fixed level_changes to simulate initial state.
1903
1904         Therefore, at any time:
1905             sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
1906
1907        Args:
1908          times: A list of 1-var affine expressions (a * x + b) which specify the
1909            time of the filling or emptying the reservoir.
1910          level_changes: A list of integer values that specifies the amount of the
1911            emptying or filling. Currently, variable demands are not supported.
1912          min_level: At any time, the level of the reservoir must be greater or
1913            equal than the min level.
1914          max_level: At any time, the level of the reservoir must be less or equal
1915            than the max level.
1916
1917        Returns:
1918          An instance of the `Constraint` class.
1919
1920        Raises:
1921          ValueError: if max_level < min_level.
1922
1923          ValueError: if max_level < 0.
1924
1925          ValueError: if min_level > 0
1926        """
1927
1928        if max_level < min_level:
1929            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1930
1931        if max_level < 0:
1932            raise ValueError("Reservoir constraint must have a max_level >= 0")
1933
1934        if min_level > 0:
1935            raise ValueError("Reservoir constraint must have a min_level <= 0")
1936
1937        ct = Constraint(self)
1938        model_ct = self.__model.constraints[ct.index]
1939        model_ct.reservoir.time_exprs.extend(
1940            [self.parse_linear_expression(x) for x in times]
1941        )
1942        model_ct.reservoir.level_changes.extend(
1943            [self.parse_linear_expression(x) for x in level_changes]
1944        )
1945        model_ct.reservoir.min_level = min_level
1946        model_ct.reservoir.max_level = max_level
1947        return ct
1948
1949    def add_reservoir_constraint_with_active(
1950        self,
1951        times: Iterable[LinearExprT],
1952        level_changes: Iterable[LinearExprT],
1953        actives: Iterable[LiteralT],
1954        min_level: int,
1955        max_level: int,
1956    ) -> Constraint:
1957        """Adds Reservoir(times, level_changes, actives, min_level, max_level).
1958
1959        Maintains a reservoir level within bounds. The water level starts at 0, and
1960        at any time, it must be between min_level and max_level.
1961
1962        If the variable `times[i]` is assigned a value t, and `actives[i]` is
1963        `True`, then the current level changes by `level_changes[i]`, which is
1964        constant,
1965        at time t.
1966
1967         Note that min level must be <= 0, and the max level must be >= 0. Please
1968         use fixed level_changes to simulate initial state.
1969
1970         Therefore, at any time:
1971             sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level,
1972             max_level]
1973
1974
1975        The array of boolean variables 'actives', if defined, indicates which
1976        actions are actually performed.
1977
1978        Args:
1979          times: A list of 1-var affine expressions (a * x + b) which specify the
1980            time of the filling or emptying the reservoir.
1981          level_changes: A list of integer values that specifies the amount of the
1982            emptying or filling. Currently, variable demands are not supported.
1983          actives: a list of boolean variables. They indicates if the
1984            emptying/refilling events actually take place.
1985          min_level: At any time, the level of the reservoir must be greater or
1986            equal than the min level.
1987          max_level: At any time, the level of the reservoir must be less or equal
1988            than the max level.
1989
1990        Returns:
1991          An instance of the `Constraint` class.
1992
1993        Raises:
1994          ValueError: if max_level < min_level.
1995
1996          ValueError: if max_level < 0.
1997
1998          ValueError: if min_level > 0
1999        """
2000
2001        if max_level < min_level:
2002            raise ValueError("Reservoir constraint must have a max_level >= min_level")
2003
2004        if max_level < 0:
2005            raise ValueError("Reservoir constraint must have a max_level >= 0")
2006
2007        if min_level > 0:
2008            raise ValueError("Reservoir constraint must have a min_level <= 0")
2009
2010        ct = Constraint(self)
2011        model_ct = self.__model.constraints[ct.index]
2012        model_ct.reservoir.time_exprs.extend(
2013            [self.parse_linear_expression(x) for x in times]
2014        )
2015        model_ct.reservoir.level_changes.extend(
2016            [self.parse_linear_expression(x) for x in level_changes]
2017        )
2018        model_ct.reservoir.active_literals.extend(
2019            [self.get_or_make_boolean_index(x) for x in actives]
2020        )
2021        model_ct.reservoir.min_level = min_level
2022        model_ct.reservoir.max_level = max_level
2023        return ct
2024
2025    def add_map_domain(
2026        self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0
2027    ):
2028        """Adds `var == i + offset <=> bool_var_array[i] == true for all i`."""
2029
2030        for i, bool_var in enumerate(bool_var_array):
2031            b_index = bool_var.index
2032            var_index = var.index
2033            model_ct = self.__model.constraints.add()
2034            model_ct.linear.vars.append(var_index)
2035            model_ct.linear.coeffs.append(1)
2036            offset_as_int = int(offset)
2037            model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i])
2038            model_ct.enforcement_literal.append(b_index)
2039
2040            model_ct = self.__model.constraints.add()
2041            model_ct.linear.vars.append(var_index)
2042            model_ct.linear.coeffs.append(1)
2043            model_ct.enforcement_literal.append(-b_index - 1)
2044            if offset + i - 1 >= INT_MIN:
2045                model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1])
2046            if offset + i + 1 <= INT_MAX:
2047                model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX])
2048
2049    def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint:
2050        """Adds `a => b` (`a` implies `b`)."""
2051        ct = Constraint(self)
2052        model_ct = self.__model.constraints[ct.index]
2053        model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b))
2054        model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a))
2055        return ct
2056
2057    @overload
2058    def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ...
2059
2060    @overload
2061    def add_bool_or(self, *literals: LiteralT) -> Constraint: ...
2062
2063    def add_bool_or(self, *literals):
2064        """Adds `Or(literals) == true`: sum(literals) >= 1."""
2065        ct = Constraint(self)
2066        model_ct = self.__model.constraints[ct.index]
2067        model_ct.bool_or.literals.extend(
2068            [
2069                self.get_or_make_boolean_index(x)
2070                for x in expand_generator_or_tuple(literals)
2071            ]
2072        )
2073        return ct
2074
2075    @overload
2076    def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
2077
2078    @overload
2079    def add_at_least_one(self, *literals: LiteralT) -> Constraint: ...
2080
2081    def add_at_least_one(self, *literals):
2082        """Same as `add_bool_or`: `sum(literals) >= 1`."""
2083        return self.add_bool_or(*literals)
2084
2085    @overload
2086    def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
2087
2088    @overload
2089    def add_at_most_one(self, *literals: LiteralT) -> Constraint: ...
2090
2091    def add_at_most_one(self, *literals):
2092        """Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
2093        ct = Constraint(self)
2094        model_ct = self.__model.constraints[ct.index]
2095        model_ct.at_most_one.literals.extend(
2096            [
2097                self.get_or_make_boolean_index(x)
2098                for x in expand_generator_or_tuple(literals)
2099            ]
2100        )
2101        return ct
2102
2103    @overload
2104    def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
2105
2106    @overload
2107    def add_exactly_one(self, *literals: LiteralT) -> Constraint: ...
2108
2109    def add_exactly_one(self, *literals):
2110        """Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
2111        ct = Constraint(self)
2112        model_ct = self.__model.constraints[ct.index]
2113        model_ct.exactly_one.literals.extend(
2114            [
2115                self.get_or_make_boolean_index(x)
2116                for x in expand_generator_or_tuple(literals)
2117            ]
2118        )
2119        return ct
2120
2121    @overload
2122    def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ...
2123
2124    @overload
2125    def add_bool_and(self, *literals: LiteralT) -> Constraint: ...
2126
2127    def add_bool_and(self, *literals):
2128        """Adds `And(literals) == true`."""
2129        ct = Constraint(self)
2130        model_ct = self.__model.constraints[ct.index]
2131        model_ct.bool_and.literals.extend(
2132            [
2133                self.get_or_make_boolean_index(x)
2134                for x in expand_generator_or_tuple(literals)
2135            ]
2136        )
2137        return ct
2138
2139    @overload
2140    def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ...
2141
2142    @overload
2143    def add_bool_xor(self, *literals: LiteralT) -> Constraint: ...
2144
2145    def add_bool_xor(self, *literals):
2146        """Adds `XOr(literals) == true`.
2147
2148        In contrast to add_bool_or and add_bool_and, it does not support
2149            .only_enforce_if().
2150
2151        Args:
2152          *literals: the list of literals in the constraint.
2153
2154        Returns:
2155          An `Constraint` object.
2156        """
2157        ct = Constraint(self)
2158        model_ct = self.__model.constraints[ct.index]
2159        model_ct.bool_xor.literals.extend(
2160            [
2161                self.get_or_make_boolean_index(x)
2162                for x in expand_generator_or_tuple(literals)
2163            ]
2164        )
2165        return ct
2166
2167    def add_min_equality(
2168        self, target: LinearExprT, exprs: Iterable[LinearExprT]
2169    ) -> Constraint:
2170        """Adds `target == Min(exprs)`."""
2171        ct = Constraint(self)
2172        model_ct = self.__model.constraints[ct.index]
2173        model_ct.lin_max.exprs.extend(
2174            [self.parse_linear_expression(x, True) for x in exprs]
2175        )
2176        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True))
2177        return ct
2178
2179    def add_max_equality(
2180        self, target: LinearExprT, exprs: Iterable[LinearExprT]
2181    ) -> Constraint:
2182        """Adds `target == Max(exprs)`."""
2183        ct = Constraint(self)
2184        model_ct = self.__model.constraints[ct.index]
2185        model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs])
2186        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
2187        return ct
2188
2189    def add_division_equality(
2190        self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
2191    ) -> Constraint:
2192        """Adds `target == num // denom` (integer division rounded towards 0)."""
2193        ct = Constraint(self)
2194        model_ct = self.__model.constraints[ct.index]
2195        model_ct.int_div.exprs.append(self.parse_linear_expression(num))
2196        model_ct.int_div.exprs.append(self.parse_linear_expression(denom))
2197        model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target))
2198        return ct
2199
2200    def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
2201        """Adds `target == Abs(expr)`."""
2202        ct = Constraint(self)
2203        model_ct = self.__model.constraints[ct.index]
2204        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr))
2205        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True))
2206        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
2207        return ct
2208
2209    def add_modulo_equality(
2210        self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
2211    ) -> Constraint:
2212        """Adds `target = expr % mod`.
2213
2214        It uses the C convention, that is the result is the remainder of the
2215        integral division rounded towards 0.
2216
2217            For example:
2218            * 10 % 3 = 1
2219            * -10 % 3 = -1
2220            * 10 % -3 = 1
2221            * -10 % -3 = -1
2222
2223        Args:
2224          target: the target expression.
2225          expr: the expression to compute the modulo of.
2226          mod: the modulus expression.
2227
2228        Returns:
2229          A `Constraint` object.
2230        """
2231        ct = Constraint(self)
2232        model_ct = self.__model.constraints[ct.index]
2233        model_ct.int_mod.exprs.append(self.parse_linear_expression(expr))
2234        model_ct.int_mod.exprs.append(self.parse_linear_expression(mod))
2235        model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target))
2236        return ct
2237
2238    def add_multiplication_equality(
2239        self,
2240        target: LinearExprT,
2241        *expressions: Union[Iterable[LinearExprT], LinearExprT],
2242    ) -> Constraint:
2243        """Adds `target == expressions[0] * .. * expressions[n]`."""
2244        ct = Constraint(self)
2245        model_ct = self.__model.constraints[ct.index]
2246        model_ct.int_prod.exprs.extend(
2247            [
2248                self.parse_linear_expression(expr)
2249                for expr in expand_generator_or_tuple(expressions)
2250            ]
2251        )
2252        model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target))
2253        return ct
2254
2255    # Scheduling support
2256
2257    def new_interval_var(
2258        self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str
2259    ) -> IntervalVar:
2260        """Creates an interval variable from start, size, and end.
2261
2262        An interval variable is a constraint, that is itself used in other
2263        constraints like NoOverlap.
2264
2265        Internally, it ensures that `start + size == end`.
2266
2267        Args:
2268          start: The start of the interval. It must be of the form a * var + b.
2269          size: The size of the interval. It must be of the form a * var + b.
2270          end: The end of the interval. It must be of the form a * var + b.
2271          name: The name of the interval variable.
2272
2273        Returns:
2274          An `IntervalVar` object.
2275        """
2276
2277        start_expr = self.parse_linear_expression(start)
2278        size_expr = self.parse_linear_expression(size)
2279        end_expr = self.parse_linear_expression(end)
2280        if len(start_expr.vars) > 1:
2281            raise TypeError(
2282                "cp_model.new_interval_var: start must be 1-var affine or constant."
2283            )
2284        if len(size_expr.vars) > 1:
2285            raise TypeError(
2286                "cp_model.new_interval_var: size must be 1-var affine or constant."
2287            )
2288        if len(end_expr.vars) > 1:
2289            raise TypeError(
2290                "cp_model.new_interval_var: end must be 1-var affine or constant."
2291            )
2292        return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)
2293
2294    def new_interval_var_series(
2295        self,
2296        name: str,
2297        index: pd.Index,
2298        starts: Union[LinearExprT, pd.Series],
2299        sizes: Union[LinearExprT, pd.Series],
2300        ends: Union[LinearExprT, pd.Series],
2301    ) -> pd.Series:
2302        """Creates a series of interval variables with the given name.
2303
2304        Args:
2305          name (str): Required. The name of the variable set.
2306          index (pd.Index): Required. The index to use for the variable set.
2307          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2308            set. If a `pd.Series` is passed in, it will be based on the
2309            corresponding values of the pd.Series.
2310          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
2311            set. If a `pd.Series` is passed in, it will be based on the
2312            corresponding values of the pd.Series.
2313          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
2314            set. If a `pd.Series` is passed in, it will be based on the
2315            corresponding values of the pd.Series.
2316
2317        Returns:
2318          pd.Series: The interval variable set indexed by its corresponding
2319          dimensions.
2320
2321        Raises:
2322          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2323          ValueError: if the `name` is not a valid identifier or already exists.
2324          ValueError: if the all the indexes do not match.
2325        """
2326        if not isinstance(index, pd.Index):
2327            raise TypeError("Non-index object is used as index")
2328        if not name.isidentifier():
2329            raise ValueError("name={} is not a valid identifier".format(name))
2330
2331        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2332        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
2333        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
2334        interval_array = []
2335        for i in index:
2336            interval_array.append(
2337                self.new_interval_var(
2338                    start=starts[i],
2339                    size=sizes[i],
2340                    end=ends[i],
2341                    name=f"{name}[{i}]",
2342                )
2343            )
2344        return pd.Series(index=index, data=interval_array)
2345
2346    def new_fixed_size_interval_var(
2347        self, start: LinearExprT, size: IntegralT, name: str
2348    ) -> IntervalVar:
2349        """Creates an interval variable from start, and a fixed size.
2350
2351        An interval variable is a constraint, that is itself used in other
2352        constraints like NoOverlap.
2353
2354        Args:
2355          start: The start of the interval. It must be of the form a * var + b.
2356          size: The size of the interval. It must be an integer value.
2357          name: The name of the interval variable.
2358
2359        Returns:
2360          An `IntervalVar` object.
2361        """
2362        size = cmh.assert_is_int64(size)
2363        start_expr = self.parse_linear_expression(start)
2364        size_expr = self.parse_linear_expression(size)
2365        end_expr = self.parse_linear_expression(start + size)
2366        if len(start_expr.vars) > 1:
2367            raise TypeError(
2368                "cp_model.new_interval_var: start must be affine or constant."
2369            )
2370        return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)
2371
2372    def new_fixed_size_interval_var_series(
2373        self,
2374        name: str,
2375        index: pd.Index,
2376        starts: Union[LinearExprT, pd.Series],
2377        sizes: Union[IntegralT, pd.Series],
2378    ) -> pd.Series:
2379        """Creates a series of interval variables with the given name.
2380
2381        Args:
2382          name (str): Required. The name of the variable set.
2383          index (pd.Index): Required. The index to use for the variable set.
2384          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2385            set. If a `pd.Series` is passed in, it will be based on the
2386            corresponding values of the pd.Series.
2387          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
2388            the set. If a `pd.Series` is passed in, it will be based on the
2389            corresponding values of the pd.Series.
2390
2391        Returns:
2392          pd.Series: The interval variable set indexed by its corresponding
2393          dimensions.
2394
2395        Raises:
2396          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2397          ValueError: if the `name` is not a valid identifier or already exists.
2398          ValueError: if the all the indexes do not match.
2399        """
2400        if not isinstance(index, pd.Index):
2401            raise TypeError("Non-index object is used as index")
2402        if not name.isidentifier():
2403            raise ValueError("name={} is not a valid identifier".format(name))
2404
2405        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2406        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
2407        interval_array = []
2408        for i in index:
2409            interval_array.append(
2410                self.new_fixed_size_interval_var(
2411                    start=starts[i],
2412                    size=sizes[i],
2413                    name=f"{name}[{i}]",
2414                )
2415            )
2416        return pd.Series(index=index, data=interval_array)
2417
2418    def new_optional_interval_var(
2419        self,
2420        start: LinearExprT,
2421        size: LinearExprT,
2422        end: LinearExprT,
2423        is_present: LiteralT,
2424        name: str,
2425    ) -> IntervalVar:
2426        """Creates an optional interval var from start, size, end, and is_present.
2427
2428        An optional interval variable is a constraint, that is itself used in other
2429        constraints like NoOverlap. This constraint is protected by a presence
2430        literal that indicates if it is active or not.
2431
2432        Internally, it ensures that `is_present` implies `start + size ==
2433        end`.
2434
2435        Args:
2436          start: The start of the interval. It must be of the form a * var + b.
2437          size: The size of the interval. It must be of the form a * var + b.
2438          end: The end of the interval. It must be of the form a * var + b.
2439          is_present: A literal that indicates if the interval is active or not. A
2440            inactive interval is simply ignored by all constraints.
2441          name: The name of the interval variable.
2442
2443        Returns:
2444          An `IntervalVar` object.
2445        """
2446
2447        # Creates the IntervalConstraintProto object.
2448        is_present_index = self.get_or_make_boolean_index(is_present)
2449        start_expr = self.parse_linear_expression(start)
2450        size_expr = self.parse_linear_expression(size)
2451        end_expr = self.parse_linear_expression(end)
2452        if len(start_expr.vars) > 1:
2453            raise TypeError(
2454                "cp_model.new_interval_var: start must be affine or constant."
2455            )
2456        if len(size_expr.vars) > 1:
2457            raise TypeError(
2458                "cp_model.new_interval_var: size must be affine or constant."
2459            )
2460        if len(end_expr.vars) > 1:
2461            raise TypeError(
2462                "cp_model.new_interval_var: end must be affine or constant."
2463            )
2464        return IntervalVar(
2465            self.__model, start_expr, size_expr, end_expr, is_present_index, name
2466        )
2467
2468    def new_optional_interval_var_series(
2469        self,
2470        name: str,
2471        index: pd.Index,
2472        starts: Union[LinearExprT, pd.Series],
2473        sizes: Union[LinearExprT, pd.Series],
2474        ends: Union[LinearExprT, pd.Series],
2475        are_present: Union[LiteralT, pd.Series],
2476    ) -> pd.Series:
2477        """Creates a series of interval variables with the given name.
2478
2479        Args:
2480          name (str): Required. The name of the variable set.
2481          index (pd.Index): Required. The index to use for the variable set.
2482          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2483            set. If a `pd.Series` is passed in, it will be based on the
2484            corresponding values of the pd.Series.
2485          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
2486            set. If a `pd.Series` is passed in, it will be based on the
2487            corresponding values of the pd.Series.
2488          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
2489            set. If a `pd.Series` is passed in, it will be based on the
2490            corresponding values of the pd.Series.
2491          are_present (Union[LiteralT, pd.Series]): The performed literal of each
2492            interval in the set. If a `pd.Series` is passed in, it will be based on
2493            the corresponding values of the pd.Series.
2494
2495        Returns:
2496          pd.Series: The interval variable set indexed by its corresponding
2497          dimensions.
2498
2499        Raises:
2500          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2501          ValueError: if the `name` is not a valid identifier or already exists.
2502          ValueError: if the all the indexes do not match.
2503        """
2504        if not isinstance(index, pd.Index):
2505            raise TypeError("Non-index object is used as index")
2506        if not name.isidentifier():
2507            raise ValueError("name={} is not a valid identifier".format(name))
2508
2509        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2510        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
2511        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
2512        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
2513
2514        interval_array = []
2515        for i in index:
2516            interval_array.append(
2517                self.new_optional_interval_var(
2518                    start=starts[i],
2519                    size=sizes[i],
2520                    end=ends[i],
2521                    is_present=are_present[i],
2522                    name=f"{name}[{i}]",
2523                )
2524            )
2525        return pd.Series(index=index, data=interval_array)
2526
2527    def new_optional_fixed_size_interval_var(
2528        self,
2529        start: LinearExprT,
2530        size: IntegralT,
2531        is_present: LiteralT,
2532        name: str,
2533    ) -> IntervalVar:
2534        """Creates an interval variable from start, and a fixed size.
2535
2536        An interval variable is a constraint, that is itself used in other
2537        constraints like NoOverlap.
2538
2539        Args:
2540          start: The start of the interval. It must be of the form a * var + b.
2541          size: The size of the interval. It must be an integer value.
2542          is_present: A literal that indicates if the interval is active or not. A
2543            inactive interval is simply ignored by all constraints.
2544          name: The name of the interval variable.
2545
2546        Returns:
2547          An `IntervalVar` object.
2548        """
2549        size = cmh.assert_is_int64(size)
2550        start_expr = self.parse_linear_expression(start)
2551        size_expr = self.parse_linear_expression(size)
2552        end_expr = self.parse_linear_expression(start + size)
2553        if len(start_expr.vars) > 1:
2554            raise TypeError(
2555                "cp_model.new_interval_var: start must be affine or constant."
2556            )
2557        is_present_index = self.get_or_make_boolean_index(is_present)
2558        return IntervalVar(
2559            self.__model,
2560            start_expr,
2561            size_expr,
2562            end_expr,
2563            is_present_index,
2564            name,
2565        )
2566
2567    def new_optional_fixed_size_interval_var_series(
2568        self,
2569        name: str,
2570        index: pd.Index,
2571        starts: Union[LinearExprT, pd.Series],
2572        sizes: Union[IntegralT, pd.Series],
2573        are_present: Union[LiteralT, pd.Series],
2574    ) -> pd.Series:
2575        """Creates a series of interval variables with the given name.
2576
2577        Args:
2578          name (str): Required. The name of the variable set.
2579          index (pd.Index): Required. The index to use for the variable set.
2580          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2581            set. If a `pd.Series` is passed in, it will be based on the
2582            corresponding values of the pd.Series.
2583          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
2584            the set. If a `pd.Series` is passed in, it will be based on the
2585            corresponding values of the pd.Series.
2586          are_present (Union[LiteralT, pd.Series]): The performed literal of each
2587            interval in the set. If a `pd.Series` is passed in, it will be based on
2588            the corresponding values of the pd.Series.
2589
2590        Returns:
2591          pd.Series: The interval variable set indexed by its corresponding
2592          dimensions.
2593
2594        Raises:
2595          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2596          ValueError: if the `name` is not a valid identifier or already exists.
2597          ValueError: if the all the indexes do not match.
2598        """
2599        if not isinstance(index, pd.Index):
2600            raise TypeError("Non-index object is used as index")
2601        if not name.isidentifier():
2602            raise ValueError("name={} is not a valid identifier".format(name))
2603
2604        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2605        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
2606        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
2607        interval_array = []
2608        for i in index:
2609            interval_array.append(
2610                self.new_optional_fixed_size_interval_var(
2611                    start=starts[i],
2612                    size=sizes[i],
2613                    is_present=are_present[i],
2614                    name=f"{name}[{i}]",
2615                )
2616            )
2617        return pd.Series(index=index, data=interval_array)
2618
2619    def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint:
2620        """Adds NoOverlap(interval_vars).
2621
2622        A NoOverlap constraint ensures that all present intervals do not overlap
2623        in time.
2624
2625        Args:
2626          interval_vars: The list of interval variables to constrain.
2627
2628        Returns:
2629          An instance of the `Constraint` class.
2630        """
2631        ct = Constraint(self)
2632        model_ct = self.__model.constraints[ct.index]
2633        model_ct.no_overlap.intervals.extend(
2634            [self.get_interval_index(x) for x in interval_vars]
2635        )
2636        return ct
2637
2638    def add_no_overlap_2d(
2639        self,
2640        x_intervals: Iterable[IntervalVar],
2641        y_intervals: Iterable[IntervalVar],
2642    ) -> Constraint:
2643        """Adds NoOverlap2D(x_intervals, y_intervals).
2644
2645        A NoOverlap2D constraint ensures that all present rectangles do not overlap
2646        on a plane. Each rectangle is aligned with the X and Y axis, and is defined
2647        by two intervals which represent its projection onto the X and Y axis.
2648
2649        Furthermore, one box is optional if at least one of the x or y interval is
2650        optional.
2651
2652        Args:
2653          x_intervals: The X coordinates of the rectangles.
2654          y_intervals: The Y coordinates of the rectangles.
2655
2656        Returns:
2657          An instance of the `Constraint` class.
2658        """
2659        ct = Constraint(self)
2660        model_ct = self.__model.constraints[ct.index]
2661        model_ct.no_overlap_2d.x_intervals.extend(
2662            [self.get_interval_index(x) for x in x_intervals]
2663        )
2664        model_ct.no_overlap_2d.y_intervals.extend(
2665            [self.get_interval_index(x) for x in y_intervals]
2666        )
2667        return ct
2668
2669    def add_cumulative(
2670        self,
2671        intervals: Iterable[IntervalVar],
2672        demands: Iterable[LinearExprT],
2673        capacity: LinearExprT,
2674    ) -> Constraint:
2675        """Adds Cumulative(intervals, demands, capacity).
2676
2677        This constraint enforces that:
2678
2679            for all t:
2680              sum(demands[i]
2681                if (start(intervals[i]) <= t < end(intervals[i])) and
2682                (intervals[i] is present)) <= capacity
2683
2684        Args:
2685          intervals: The list of intervals.
2686          demands: The list of demands for each interval. Each demand must be >= 0.
2687            Each demand can be a 1-var affine expression (a * x + b).
2688          capacity: The maximum capacity of the cumulative constraint. It can be a
2689            1-var affine expression (a * x + b).
2690
2691        Returns:
2692          An instance of the `Constraint` class.
2693        """
2694        cumulative = Constraint(self)
2695        model_ct = self.__model.constraints[cumulative.index]
2696        model_ct.cumulative.intervals.extend(
2697            [self.get_interval_index(x) for x in intervals]
2698        )
2699        for d in demands:
2700            model_ct.cumulative.demands.append(self.parse_linear_expression(d))
2701        model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity))
2702        return cumulative
2703
2704    # Support for model cloning.
2705    def clone(self) -> "CpModel":
2706        """Reset the model, and creates a new one from a CpModelProto instance."""
2707        clone = CpModel()
2708        clone.proto.CopyFrom(self.proto)
2709        clone.rebuild_constant_map()
2710        return clone
2711
2712    def rebuild_constant_map(self):
2713        """Internal method used during model cloning."""
2714        for i, var in enumerate(self.__model.variables):
2715            if len(var.domain) == 2 and var.domain[0] == var.domain[1]:
2716                self.__constant_map[var.domain[0]] = i
2717
2718    def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2719        """Returns an already created Boolean variable from its index."""
2720        if index < 0 or index >= len(self.__model.variables):
2721            raise ValueError(
2722                f"get_bool_var_from_proto_index: out of bound index {index}"
2723            )
2724        var = self.__model.variables[index]
2725        if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1:
2726            raise ValueError(
2727                f"get_bool_var_from_proto_index: index {index} does not reference"
2728                + " a Boolean variable"
2729            )
2730
2731        return IntVar(self.__model, index, None)
2732
2733    def get_int_var_from_proto_index(self, index: int) -> IntVar:
2734        """Returns an already created integer variable from its index."""
2735        if index < 0 or index >= len(self.__model.variables):
2736            raise ValueError(
2737                f"get_int_var_from_proto_index: out of bound index {index}"
2738            )
2739        return IntVar(self.__model, index, None)
2740
2741    def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2742        """Returns an already created interval variable from its index."""
2743        if index < 0 or index >= len(self.__model.constraints):
2744            raise ValueError(
2745                f"get_interval_var_from_proto_index: out of bound index {index}"
2746            )
2747        ct = self.__model.constraints[index]
2748        if not ct.HasField("interval"):
2749            raise ValueError(
2750                f"get_interval_var_from_proto_index: index {index} does not"
2751                " reference an" + " interval variable"
2752            )
2753
2754        return IntervalVar(self.__model, index, None, None, None, None)
2755
2756    # Helpers.
2757
2758    def __str__(self) -> str:
2759        return str(self.__model)
2760
2761    @property
2762    def proto(self) -> cp_model_pb2.CpModelProto:
2763        """Returns the underlying CpModelProto."""
2764        return self.__model
2765
2766    def negated(self, index: int) -> int:
2767        return -index - 1
2768
2769    def get_or_make_index(self, arg: VariableT) -> int:
2770        """Returns the index of a variable, its negation, or a number."""
2771        if isinstance(arg, IntVar):
2772            return arg.index
2773        if (
2774            isinstance(arg, _ProductCst)
2775            and isinstance(arg.expression(), IntVar)
2776            and arg.coefficient() == -1
2777        ):
2778            return -arg.expression().index - 1
2779        if isinstance(arg, IntegralTypes):
2780            arg = cmh.assert_is_int64(arg)
2781            return self.get_or_make_index_from_constant(arg)
2782        raise TypeError("NotSupported: model.get_or_make_index(" + str(arg) + ")")
2783
2784    def get_or_make_boolean_index(self, arg: LiteralT) -> int:
2785        """Returns an index from a boolean expression."""
2786        if isinstance(arg, IntVar):
2787            self.assert_is_boolean_variable(arg)
2788            return arg.index
2789        if isinstance(arg, _NotBooleanVariable):
2790            self.assert_is_boolean_variable(arg.negated())
2791            return arg.index
2792        if isinstance(arg, IntegralTypes):
2793            if arg == ~False:  # -1
2794                return self.get_or_make_index_from_constant(1)
2795            if arg == ~True:  # -2
2796                return self.get_or_make_index_from_constant(0)
2797            arg = cmh.assert_is_zero_or_one(arg)
2798            return self.get_or_make_index_from_constant(arg)
2799        if cmh.is_boolean(arg):
2800            return self.get_or_make_index_from_constant(int(arg))
2801        raise TypeError(f"not supported: model.get_or_make_boolean_index({arg})")
2802
2803    def get_interval_index(self, arg: IntervalVar) -> int:
2804        if not isinstance(arg, IntervalVar):
2805            raise TypeError("NotSupported: model.get_interval_index(%s)" % arg)
2806        return arg.index
2807
2808    def get_or_make_index_from_constant(self, value: IntegralT) -> int:
2809        if value in self.__constant_map:
2810            return self.__constant_map[value]
2811        index = len(self.__model.variables)
2812        self.__model.variables.add(domain=[value, value])
2813        self.__constant_map[value] = index
2814        return index
2815
2816    def var_index_to_var_proto(
2817        self, var_index: int
2818    ) -> cp_model_pb2.IntegerVariableProto:
2819        if var_index >= 0:
2820            return self.__model.variables[var_index]
2821        else:
2822            return self.__model.variables[-var_index - 1]
2823
2824    def parse_linear_expression(
2825        self, linear_expr: LinearExprT, negate: bool = False
2826    ) -> cp_model_pb2.LinearExpressionProto:
2827        """Returns a LinearExpressionProto built from a LinearExpr instance."""
2828        result: cp_model_pb2.LinearExpressionProto = (
2829            cp_model_pb2.LinearExpressionProto()
2830        )
2831        mult = -1 if negate else 1
2832        if isinstance(linear_expr, IntegralTypes):
2833            result.offset = int(linear_expr) * mult
2834            return result
2835
2836        if isinstance(linear_expr, IntVar):
2837            result.vars.append(self.get_or_make_index(linear_expr))
2838            result.coeffs.append(mult)
2839            return result
2840
2841        coeffs_map, constant = cast(LinearExpr, linear_expr).get_integer_var_value_map()
2842        result.offset = constant * mult
2843        for t in coeffs_map.items():
2844            if not isinstance(t[0], IntVar):
2845                raise TypeError("Wrong argument" + str(t))
2846            c = cmh.assert_is_int64(t[1])
2847            result.vars.append(t[0].index)
2848            result.coeffs.append(c * mult)
2849        return result
2850
2851    def _set_objective(self, obj: ObjLinearExprT, minimize: bool):
2852        """Sets the objective of the model."""
2853        self.clear_objective()
2854        if isinstance(obj, IntVar):
2855            self.__model.objective.vars.append(obj.index)
2856            self.__model.objective.offset = 0
2857            if minimize:
2858                self.__model.objective.coeffs.append(1)
2859                self.__model.objective.scaling_factor = 1
2860            else:
2861                self.__model.objective.coeffs.append(-1)
2862                self.__model.objective.scaling_factor = -1
2863        elif isinstance(obj, LinearExpr):
2864            coeffs_map, constant, is_integer = obj.get_float_var_value_map()
2865            if is_integer:
2866                if minimize:
2867                    self.__model.objective.scaling_factor = 1
2868                    self.__model.objective.offset = constant
2869                else:
2870                    self.__model.objective.scaling_factor = -1
2871                    self.__model.objective.offset = -constant
2872                for v, c in coeffs_map.items():
2873                    c_as_int = int(c)
2874                    self.__model.objective.vars.append(v.index)
2875                    if minimize:
2876                        self.__model.objective.coeffs.append(c_as_int)
2877                    else:
2878                        self.__model.objective.coeffs.append(-c_as_int)
2879            else:
2880                self.__model.floating_point_objective.maximize = not minimize
2881                self.__model.floating_point_objective.offset = constant
2882                for v, c in coeffs_map.items():
2883                    self.__model.floating_point_objective.coeffs.append(c)
2884                    self.__model.floating_point_objective.vars.append(v.index)
2885        elif isinstance(obj, IntegralTypes):
2886            self.__model.objective.offset = int(obj)
2887            self.__model.objective.scaling_factor = 1
2888        else:
2889            raise TypeError("TypeError: " + str(obj) + " is not a valid objective")
2890
2891    def minimize(self, obj: ObjLinearExprT):
2892        """Sets the objective of the model to minimize(obj)."""
2893        self._set_objective(obj, minimize=True)
2894
2895    def maximize(self, obj: ObjLinearExprT):
2896        """Sets the objective of the model to maximize(obj)."""
2897        self._set_objective(obj, minimize=False)
2898
2899    def has_objective(self) -> bool:
2900        return self.__model.HasField("objective") or self.__model.HasField(
2901            "floating_point_objective"
2902        )
2903
2904    def clear_objective(self):
2905        self.__model.ClearField("objective")
2906        self.__model.ClearField("floating_point_objective")
2907
2908    def add_decision_strategy(
2909        self,
2910        variables: Sequence[IntVar],
2911        var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy,
2912        domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy,
2913    ) -> None:
2914        """Adds a search strategy to the model.
2915
2916        Args:
2917          variables: a list of variables this strategy will assign.
2918          var_strategy: heuristic to choose the next variable to assign.
2919          domain_strategy: heuristic to reduce the domain of the selected variable.
2920            Currently, this is advanced code: the union of all strategies added to
2921            the model must be complete, i.e. instantiates all variables. Otherwise,
2922            solve() will fail.
2923        """
2924
2925        strategy: cp_model_pb2.DecisionStrategyProto = (
2926            self.__model.search_strategy.add()
2927        )
2928        for v in variables:
2929            expr = strategy.exprs.add()
2930            if v.index >= 0:
2931                expr.vars.append(v.index)
2932                expr.coeffs.append(1)
2933            else:
2934                expr.vars.append(self.negated(v.index))
2935                expr.coeffs.append(-1)
2936                expr.offset = 1
2937
2938        strategy.variable_selection_strategy = var_strategy
2939        strategy.domain_reduction_strategy = domain_strategy
2940
2941    def model_stats(self) -> str:
2942        """Returns a string containing some model statistics."""
2943        return swig_helper.CpSatHelper.model_stats(self.__model)
2944
2945    def validate(self) -> str:
2946        """Returns a string indicating that the model is invalid."""
2947        return swig_helper.CpSatHelper.validate_model(self.__model)
2948
2949    def export_to_file(self, file: str) -> bool:
2950        """Write the model as a protocol buffer to 'file'.
2951
2952        Args:
2953          file: file to write the model to. If the filename ends with 'txt', the
2954            model will be written as a text file, otherwise, the binary format will
2955            be used.
2956
2957        Returns:
2958          True if the model was correctly written.
2959        """
2960        return swig_helper.CpSatHelper.write_model_to_file(self.__model, file)
2961
2962    def add_hint(self, var: IntVar, value: int) -> None:
2963        """Adds 'var == value' as a hint to the solver."""
2964        self.__model.solution_hint.vars.append(self.get_or_make_index(var))
2965        self.__model.solution_hint.values.append(value)
2966
2967    def clear_hints(self):
2968        """Removes any solution hint from the model."""
2969        self.__model.ClearField("solution_hint")
2970
2971    def add_assumption(self, lit: LiteralT) -> None:
2972        """Adds the literal to the model as assumptions."""
2973        self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
2974
2975    def add_assumptions(self, literals: Iterable[LiteralT]) -> None:
2976        """Adds the literals to the model as assumptions."""
2977        for lit in literals:
2978            self.add_assumption(lit)
2979
2980    def clear_assumptions(self) -> None:
2981        """Removes all assumptions from the model."""
2982        self.__model.ClearField("assumptions")
2983
2984    # Helpers.
2985    def assert_is_boolean_variable(self, x: LiteralT) -> None:
2986        if isinstance(x, IntVar):
2987            var = self.__model.variables[x.index]
2988            if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1:
2989                raise TypeError("TypeError: " + str(x) + " is not a boolean variable")
2990        elif not isinstance(x, _NotBooleanVariable):
2991            raise TypeError("TypeError: " + str(x) + " is not a boolean variable")
2992
2993    # Compatibility with pre PEP8
2994    # pylint: disable=invalid-name
2995
2996    def Name(self) -> str:
2997        return self.name
2998
2999    def SetName(self, name: str) -> None:
3000        self.name = name
3001
3002    def Proto(self) -> cp_model_pb2.CpModelProto:
3003        return self.proto
3004
3005    NewIntVar = new_int_var
3006    NewIntVarFromDomain = new_int_var_from_domain
3007    NewBoolVar = new_bool_var
3008    NewConstant = new_constant
3009    NewIntVarSeries = new_int_var_series
3010    NewBoolVarSeries = new_bool_var_series
3011    AddLinearConstraint = add_linear_constraint
3012    AddLinearExpressionInDomain = add_linear_expression_in_domain
3013    Add = add
3014    AddAllDifferent = add_all_different
3015    AddElement = add_element
3016    AddCircuit = add_circuit
3017    AddMultipleCircuit = add_multiple_circuit
3018    AddAllowedAssignments = add_allowed_assignments
3019    AddForbiddenAssignments = add_forbidden_assignments
3020    AddAutomaton = add_automaton
3021    AddInverse = add_inverse
3022    AddReservoirConstraint = add_reservoir_constraint
3023    AddReservoirConstraintWithActive = add_reservoir_constraint_with_active
3024    AddImplication = add_implication
3025    AddBoolOr = add_bool_or
3026    AddAtLeastOne = add_at_least_one
3027    AddAtMostOne = add_at_most_one
3028    AddExactlyOne = add_exactly_one
3029    AddBoolAnd = add_bool_and
3030    AddBoolXOr = add_bool_xor
3031    AddMinEquality = add_min_equality
3032    AddMaxEquality = add_max_equality
3033    AddDivisionEquality = add_division_equality
3034    AddAbsEquality = add_abs_equality
3035    AddModuloEquality = add_modulo_equality
3036    AddMultiplicationEquality = add_multiplication_equality
3037    NewIntervalVar = new_interval_var
3038    NewIntervalVarSeries = new_interval_var_series
3039    NewFixedSizeIntervalVar = new_fixed_size_interval_var
3040    NewOptionalIntervalVar = new_optional_interval_var
3041    NewOptionalIntervalVarSeries = new_optional_interval_var_series
3042    NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var
3043    NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series
3044    AddNoOverlap = add_no_overlap
3045    AddNoOverlap2D = add_no_overlap_2d
3046    AddCumulative = add_cumulative
3047    Clone = clone
3048    GetBoolVarFromProtoIndex = get_bool_var_from_proto_index
3049    GetIntVarFromProtoIndex = get_int_var_from_proto_index
3050    GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index
3051    Minimize = minimize
3052    Maximize = maximize
3053    HasObjective = has_objective
3054    ClearObjective = clear_objective
3055    AddDecisionStrategy = add_decision_strategy
3056    ModelStats = model_stats
3057    Validate = validate
3058    ExportToFile = export_to_file
3059    AddHint = add_hint
3060    ClearHints = clear_hints
3061    AddAssumption = add_assumption
3062    AddAssumptions = add_assumptions
3063    ClearAssumptions = clear_assumptions
3064
3065    # pylint: enable=invalid-name

Methods for building a CP model.

Methods beginning with:

  • New create integer, boolean, or interval variables.
  • add create new constraints and add them to the model.
name: str
1348    @property
1349    def name(self) -> str:
1350        """Returns the name of the model."""
1351        if not self.__model or not self.__model.name:
1352            return ""
1353        return self.__model.name

Returns the name of the model.

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:
1362    def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar:
1363        """Create an integer variable with domain [lb, ub].
1364
1365        The CP-SAT solver is limited to integer variables. If you have fractional
1366        values, scale them up so that they become integers; if you have strings,
1367        encode them as integers.
1368
1369        Args:
1370          lb: Lower bound for the variable.
1371          ub: Upper bound for the variable.
1372          name: The name of the variable.
1373
1374        Returns:
1375          a variable whose domain is [lb, ub].
1376        """
1377
1378        return IntVar(self.__model, sorted_interval_list.Domain(lb, ub), name)

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

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

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

a variable whose domain is [lb, ub].

def new_int_var_from_domain( self, domain: Domain, name: str) -> IntVar:
1380    def new_int_var_from_domain(
1381        self, domain: sorted_interval_list.Domain, name: str
1382    ) -> IntVar:
1383        """Create an integer variable from a domain.
1384
1385        A domain is a set of integers specified by a collection of intervals.
1386        For example, `model.new_int_var_from_domain(cp_model.
1387             Domain.from_intervals([[1, 2], [4, 6]]), 'x')`
1388
1389        Args:
1390          domain: An instance of the Domain class.
1391          name: The name of the variable.
1392
1393        Returns:
1394            a variable whose domain is the given domain.
1395        """
1396        return IntVar(self.__model, domain, name)

Create an integer variable from a domain.

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

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

a variable whose domain is the given domain.

def new_bool_var(self, name: str) -> IntVar:
1398    def new_bool_var(self, name: str) -> IntVar:
1399        """Creates a 0-1 variable with the given name."""
1400        return IntVar(self.__model, sorted_interval_list.Domain(0, 1), name)

Creates a 0-1 variable with the given name.

def new_constant( self, value: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> IntVar:
1402    def new_constant(self, value: IntegralT) -> IntVar:
1403        """Declares a constant integer."""
1404        return IntVar(self.__model, self.get_or_make_index_from_constant(value), None)

Declares a constant integer.

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:
1406    def new_int_var_series(
1407        self,
1408        name: str,
1409        index: pd.Index,
1410        lower_bounds: Union[IntegralT, pd.Series],
1411        upper_bounds: Union[IntegralT, pd.Series],
1412    ) -> pd.Series:
1413        """Creates a series of (scalar-valued) variables with the given name.
1414
1415        Args:
1416          name (str): Required. The name of the variable set.
1417          index (pd.Index): Required. The index to use for the variable set.
1418          lower_bounds (Union[int, pd.Series]): A lower bound for variables in the
1419            set. If a `pd.Series` is passed in, it will be based on the
1420            corresponding values of the pd.Series.
1421          upper_bounds (Union[int, pd.Series]): An upper bound for variables in the
1422            set. If a `pd.Series` is passed in, it will be based on the
1423            corresponding values of the pd.Series.
1424
1425        Returns:
1426          pd.Series: The variable set indexed by its corresponding dimensions.
1427
1428        Raises:
1429          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1430          ValueError: if the `name` is not a valid identifier or already exists.
1431          ValueError: if the `lowerbound` is greater than the `upperbound`.
1432          ValueError: if the index of `lower_bound`, or `upper_bound` does not match
1433          the input index.
1434        """
1435        if not isinstance(index, pd.Index):
1436            raise TypeError("Non-index object is used as index")
1437        if not name.isidentifier():
1438            raise ValueError("name={} is not a valid identifier".format(name))
1439        if (
1440            isinstance(lower_bounds, IntegralTypes)
1441            and isinstance(upper_bounds, IntegralTypes)
1442            and lower_bounds > upper_bounds
1443        ):
1444            raise ValueError(
1445                f"lower_bound={lower_bounds} is greater than"
1446                f" upper_bound={upper_bounds} for variable set={name}"
1447            )
1448
1449        lower_bounds = _convert_to_integral_series_and_validate_index(
1450            lower_bounds, index
1451        )
1452        upper_bounds = _convert_to_integral_series_and_validate_index(
1453            upper_bounds, index
1454        )
1455        return pd.Series(
1456            index=index,
1457            data=[
1458                # pylint: disable=g-complex-comprehension
1459                IntVar(
1460                    model=self.__model,
1461                    name=f"{name}[{i}]",
1462                    domain=sorted_interval_list.Domain(
1463                        lower_bounds[i], upper_bounds[i]
1464                    ),
1465                )
1466                for i in index
1467            ],
1468        )

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

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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:
1470    def new_bool_var_series(
1471        self,
1472        name: str,
1473        index: pd.Index,
1474    ) -> pd.Series:
1475        """Creates a series of (scalar-valued) variables with the given name.
1476
1477        Args:
1478          name (str): Required. The name of the variable set.
1479          index (pd.Index): Required. The index to use for the variable set.
1480
1481        Returns:
1482          pd.Series: The variable set indexed by its corresponding dimensions.
1483
1484        Raises:
1485          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1486          ValueError: if the `name` is not a valid identifier or already exists.
1487        """
1488        return self.new_int_var_series(
1489            name=name, index=index, lower_bounds=0, upper_bounds=1
1490        )

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

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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:
1494    def add_linear_constraint(
1495        self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT
1496    ) -> Constraint:
1497        """Adds the constraint: `lb <= linear_expr <= ub`."""
1498        return self.add_linear_expression_in_domain(
1499            linear_expr, sorted_interval_list.Domain(lb, ub)
1500        )

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

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:
1502    def add_linear_expression_in_domain(
1503        self, linear_expr: LinearExprT, domain: sorted_interval_list.Domain
1504    ) -> Constraint:
1505        """Adds the constraint: `linear_expr` in `domain`."""
1506        if isinstance(linear_expr, LinearExpr):
1507            ct = Constraint(self)
1508            model_ct = self.__model.constraints[ct.index]
1509            coeffs_map, constant = linear_expr.get_integer_var_value_map()
1510            for t in coeffs_map.items():
1511                if not isinstance(t[0], IntVar):
1512                    raise TypeError("Wrong argument" + str(t))
1513                c = cmh.assert_is_int64(t[1])
1514                model_ct.linear.vars.append(t[0].index)
1515                model_ct.linear.coeffs.append(c)
1516            model_ct.linear.domain.extend(
1517                [
1518                    cmh.capped_subtraction(x, constant)
1519                    for x in domain.flattened_intervals()
1520                ]
1521            )
1522            return ct
1523        if isinstance(linear_expr, IntegralTypes):
1524            if not domain.contains(int(linear_expr)):
1525                return self.add_bool_or([])  # Evaluate to false.
1526            else:
1527                return self.add_bool_and([])  # Evaluate to true.
1528        raise TypeError(
1529            "not supported: CpModel.add_linear_expression_in_domain("
1530            + str(linear_expr)
1531            + " "
1532            + str(domain)
1533            + ")"
1534        )

Adds the constraint: linear_expr in domain.

def add(self, ct):
1542    def add(self, ct):
1543        """Adds a `BoundedLinearExpression` to the model.
1544
1545        Args:
1546          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1547
1548        Returns:
1549          An instance of the `Constraint` class.
1550        """
1551        if isinstance(ct, BoundedLinearExpression):
1552            return self.add_linear_expression_in_domain(
1553                ct.expression(),
1554                sorted_interval_list.Domain.from_flat_intervals(ct.bounds()),
1555            )
1556        if ct and cmh.is_boolean(ct):
1557            return self.add_bool_or([True])
1558        if not ct and cmh.is_boolean(ct):
1559            return self.add_bool_or([])  # Evaluate to false.
1560        raise TypeError("not supported: CpModel.add(" + str(ct) + ")")

Adds a BoundedLinearExpression to the model.

Arguments:
Returns:

An instance of the Constraint class.

def add_all_different(self, *expressions):
1570    def add_all_different(self, *expressions):
1571        """Adds AllDifferent(expressions).
1572
1573        This constraint forces all expressions to have different values.
1574
1575        Args:
1576          *expressions: simple expressions of the form a * var + constant.
1577
1578        Returns:
1579          An instance of the `Constraint` class.
1580        """
1581        ct = Constraint(self)
1582        model_ct = self.__model.constraints[ct.index]
1583        expanded = expand_generator_or_tuple(expressions)
1584        model_ct.all_diff.exprs.extend(
1585            self.parse_linear_expression(x) for x in expanded
1586        )
1587        return ct

Adds AllDifferent(expressions).

This constraint forces all expressions to have different values.

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

An instance of the Constraint class.

def add_element( self, index: Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], variables: Sequence[Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], target: Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1589    def add_element(
1590        self, index: VariableT, variables: Sequence[VariableT], target: VariableT
1591    ) -> Constraint:
1592        """Adds the element constraint: `variables[index] == target`.
1593
1594        Args:
1595          index: The index of the variable that's being constrained.
1596          variables: A list of variables.
1597          target: The value that the variable must be equal to.
1598
1599        Returns:
1600          An instance of the `Constraint` class.
1601        """
1602
1603        if not variables:
1604            raise ValueError("add_element expects a non-empty variables array")
1605
1606        if isinstance(index, IntegralTypes):
1607            variable: VariableT = list(variables)[int(index)]
1608            return self.add(variable == target)
1609
1610        ct = Constraint(self)
1611        model_ct = self.__model.constraints[ct.index]
1612        model_ct.element.index = self.get_or_make_index(index)
1613        model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables])
1614        model_ct.element.target = self.get_or_make_index(target)
1615        return ct

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

Arguments:
  • index: The index of the variable that's being constrained.
  • variables: A list of variables.
  • target: The value that the variable must be equal to.
Returns:

An instance of the Constraint class.

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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]]) -> Constraint:
1617    def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1618        """Adds Circuit(arcs).
1619
1620        Adds a circuit constraint from a sparse list of arcs that encode the graph.
1621
1622        A circuit is a unique Hamiltonian path in a subgraph of the total
1623        graph. In case a node 'i' is not in the path, then there must be a
1624        loop arc 'i -> i' associated with a true literal. Otherwise
1625        this constraint will fail.
1626
1627        Args:
1628          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1629            literal). The arc is selected in the circuit if the literal is true.
1630            Both source_node and destination_node must be integers between 0 and the
1631            number of nodes - 1.
1632
1633        Returns:
1634          An instance of the `Constraint` class.
1635
1636        Raises:
1637          ValueError: If the list of arcs is empty.
1638        """
1639        if not arcs:
1640            raise ValueError("add_circuit expects a non-empty array of arcs")
1641        ct = Constraint(self)
1642        model_ct = self.__model.constraints[ct.index]
1643        for arc in arcs:
1644            tail = cmh.assert_is_int32(arc[0])
1645            head = cmh.assert_is_int32(arc[1])
1646            lit = self.get_or_make_boolean_index(arc[2])
1647            model_ct.circuit.tails.append(tail)
1648            model_ct.circuit.heads.append(head)
1649            model_ct.circuit.literals.append(lit)
1650        return ct

Adds Circuit(arcs).

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: If the list of arcs is empty.
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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]]) -> Constraint:
1652    def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1653        """Adds a multiple circuit constraint, aka the 'VRP' constraint.
1654
1655        The direct graph where arc #i (from tails[i] to head[i]) is present iff
1656        literals[i] is true must satisfy this set of properties:
1657        - #incoming arcs == 1 except for node 0.
1658        - #outgoing arcs == 1 except for node 0.
1659        - for node zero, #incoming arcs == #outgoing arcs.
1660        - There are no duplicate arcs.
1661        - Self-arcs are allowed except for node 0.
1662        - There is no cycle in this graph, except through node 0.
1663
1664        Args:
1665          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1666            literal). The arc is selected in the circuit if the literal is true.
1667            Both source_node and destination_node must be integers between 0 and the
1668            number of nodes - 1.
1669
1670        Returns:
1671          An instance of the `Constraint` class.
1672
1673        Raises:
1674          ValueError: If the list of arcs is empty.
1675        """
1676        if not arcs:
1677            raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
1678        ct = Constraint(self)
1679        model_ct = self.__model.constraints[ct.index]
1680        for arc in arcs:
1681            tail = cmh.assert_is_int32(arc[0])
1682            head = cmh.assert_is_int32(arc[1])
1683            lit = self.get_or_make_boolean_index(arc[2])
1684            model_ct.routes.tails.append(tail)
1685            model_ct.routes.heads.append(head)
1686            model_ct.routes.literals.append(lit)
1687        return ct

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: If the list of arcs is empty.
def add_allowed_assignments( self, variables: Sequence[Union[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:
1689    def add_allowed_assignments(
1690        self,
1691        variables: Sequence[VariableT],
1692        tuples_list: Iterable[Sequence[IntegralT]],
1693    ) -> Constraint:
1694        """Adds AllowedAssignments(variables, tuples_list).
1695
1696        An AllowedAssignments constraint is a constraint on an array of variables,
1697        which requires that when all variables are assigned values, the resulting
1698        array equals one of the  tuples in `tuple_list`.
1699
1700        Args:
1701          variables: A list of variables.
1702          tuples_list: A list of admissible tuples. Each tuple must have the same
1703            length as the variables, and the ith value of a tuple corresponds to the
1704            ith variable.
1705
1706        Returns:
1707          An instance of the `Constraint` class.
1708
1709        Raises:
1710          TypeError: If a tuple does not have the same size as the list of
1711              variables.
1712          ValueError: If the array of variables is empty.
1713        """
1714
1715        if not variables:
1716            raise ValueError(
1717                "add_allowed_assignments expects a non-empty variables array"
1718            )
1719
1720        ct: Constraint = Constraint(self)
1721        model_ct = self.__model.constraints[ct.index]
1722        model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables])
1723        arity: int = len(variables)
1724        for t in tuples_list:
1725            if len(t) != arity:
1726                raise TypeError("Tuple " + str(t) + " has the wrong arity")
1727
1728        # duck-typing (no explicit type checks here)
1729        try:
1730            model_ct.table.values.extend(a for b in tuples_list for a in b)
1731        except ValueError as ex:
1732            raise TypeError(f"add_xxx_assignment: Not an integer or does not fit in an int64_t: {ex.args}") from ex
1733
1734        return ct

Adds AllowedAssignments(variables, tuples_list).

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

Arguments:
  • variables: A list of variables.
  • tuples_list: A list of admissible tuples. Each tuple must have the same length as the variables, and the ith value of a tuple corresponds to the ith variable.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: If a tuple does not have the same size as the list of variables.
  • ValueError: If the array of variables is empty.
def add_forbidden_assignments( self, variables: Sequence[Union[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:
1736    def add_forbidden_assignments(
1737        self,
1738        variables: Sequence[VariableT],
1739        tuples_list: Iterable[Sequence[IntegralT]],
1740    ) -> Constraint:
1741        """Adds add_forbidden_assignments(variables, [tuples_list]).
1742
1743        A ForbiddenAssignments constraint is a constraint on an array of variables
1744        where the list of impossible combinations is provided in the tuples list.
1745
1746        Args:
1747          variables: A list of variables.
1748          tuples_list: A list of forbidden tuples. Each tuple must have the same
1749            length as the variables, and the *i*th value of a tuple corresponds to
1750            the *i*th variable.
1751
1752        Returns:
1753          An instance of the `Constraint` class.
1754
1755        Raises:
1756          TypeError: If a tuple does not have the same size as the list of
1757                     variables.
1758          ValueError: If the array of variables is empty.
1759        """
1760
1761        if not variables:
1762            raise ValueError(
1763                "add_forbidden_assignments expects a non-empty variables array"
1764            )
1765
1766        index = len(self.__model.constraints)
1767        ct: Constraint = self.add_allowed_assignments(variables, tuples_list)
1768        self.__model.constraints[index].table.negated = True
1769        return ct

Adds add_forbidden_assignments(variables, [tuples_list]).

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

Arguments:
  • variables: A list of variables.
  • tuples_list: A list of forbidden tuples. Each tuple must have the same length as the variables, and the ith value of a tuple corresponds to the ith variable.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: If a tuple does not have the same size as the list of variables.
  • ValueError: If the array of variables is empty.
def add_automaton( self, transition_variables: Sequence[Union[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:
1771    def add_automaton(
1772        self,
1773        transition_variables: Sequence[VariableT],
1774        starting_state: IntegralT,
1775        final_states: Sequence[IntegralT],
1776        transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]],
1777    ) -> Constraint:
1778        """Adds an automaton constraint.
1779
1780        An automaton constraint takes a list of variables (of size *n*), an initial
1781        state, a set of final states, and a set of transitions. A transition is a
1782        triplet (*tail*, *transition*, *head*), where *tail* and *head* are states,
1783        and *transition* is the label of an arc from *head* to *tail*,
1784        corresponding to the value of one variable in the list of variables.
1785
1786        This automaton will be unrolled into a flow with *n* + 1 phases. Each phase
1787        contains the possible states of the automaton. The first state contains the
1788        initial state. The last phase contains the final states.
1789
1790        Between two consecutive phases *i* and *i* + 1, the automaton creates a set
1791        of arcs. For each transition (*tail*, *transition*, *head*), it will add
1792        an arc from the state *tail* of phase *i* and the state *head* of phase
1793        *i* + 1. This arc is labeled by the value *transition* of the variables
1794        `variables[i]`. That is, this arc can only be selected if `variables[i]`
1795        is assigned the value *transition*.
1796
1797        A feasible solution of this constraint is an assignment of variables such
1798        that, starting from the initial state in phase 0, there is a path labeled by
1799        the values of the variables that ends in one of the final states in the
1800        final phase.
1801
1802        Args:
1803          transition_variables: A non-empty list of variables whose values
1804            correspond to the labels of the arcs traversed by the automaton.
1805          starting_state: The initial state of the automaton.
1806          final_states: A non-empty list of admissible final states.
1807          transition_triples: A list of transitions for the automaton, in the
1808            following format (current_state, variable_value, next_state).
1809
1810        Returns:
1811          An instance of the `Constraint` class.
1812
1813        Raises:
1814          ValueError: if `transition_variables`, `final_states`, or
1815            `transition_triples` are empty.
1816        """
1817
1818        if not transition_variables:
1819            raise ValueError(
1820                "add_automaton expects a non-empty transition_variables array"
1821            )
1822        if not final_states:
1823            raise ValueError("add_automaton expects some final states")
1824
1825        if not transition_triples:
1826            raise ValueError("add_automaton expects some transition triples")
1827
1828        ct = Constraint(self)
1829        model_ct = self.__model.constraints[ct.index]
1830        model_ct.automaton.vars.extend(
1831            [self.get_or_make_index(x) for x in transition_variables]
1832        )
1833        starting_state = cmh.assert_is_int64(starting_state)
1834        model_ct.automaton.starting_state = starting_state
1835        for v in final_states:
1836            v = cmh.assert_is_int64(v)
1837            model_ct.automaton.final_states.append(v)
1838        for t in transition_triples:
1839            if len(t) != 3:
1840                raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)")
1841            tail = cmh.assert_is_int64(t[0])
1842            label = cmh.assert_is_int64(t[1])
1843            head = cmh.assert_is_int64(t[2])
1844            model_ct.automaton.transition_tail.append(tail)
1845            model_ct.automaton.transition_label.append(label)
1846            model_ct.automaton.transition_head.append(head)
1847        return ct

Adds an automaton constraint.

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

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

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: if transition_variables, final_states, 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:
1849    def add_inverse(
1850        self,
1851        variables: Sequence[VariableT],
1852        inverse_variables: Sequence[VariableT],
1853    ) -> Constraint:
1854        """Adds Inverse(variables, inverse_variables).
1855
1856        An inverse constraint enforces that if `variables[i]` is assigned a value
1857        `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa.
1858
1859        Args:
1860          variables: An array of integer variables.
1861          inverse_variables: An array of integer variables.
1862
1863        Returns:
1864          An instance of the `Constraint` class.
1865
1866        Raises:
1867          TypeError: if variables and inverse_variables have different lengths, or
1868              if they are empty.
1869        """
1870
1871        if not variables or not inverse_variables:
1872            raise TypeError("The Inverse constraint does not accept empty arrays")
1873        if len(variables) != len(inverse_variables):
1874            raise TypeError(
1875                "In the inverse constraint, the two array variables and"
1876                " inverse_variables must have the same length."
1877            )
1878        ct = Constraint(self)
1879        model_ct = self.__model.constraints[ct.index]
1880        model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables])
1881        model_ct.inverse.f_inverse.extend(
1882            [self.get_or_make_index(x) for x in inverse_variables]
1883        )
1884        return ct

Adds Inverse(variables, inverse_variables).

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

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

An instance of the Constraint class.

Raises:
  • TypeError: if variables and inverse_variables have different lengths, or if they are empty.
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:
1886    def add_reservoir_constraint(
1887        self,
1888        times: Iterable[LinearExprT],
1889        level_changes: Iterable[LinearExprT],
1890        min_level: int,
1891        max_level: int,
1892    ) -> Constraint:
1893        """Adds Reservoir(times, level_changes, min_level, max_level).
1894
1895        Maintains a reservoir level within bounds. The water level starts at 0, and
1896        at any time, it must be between min_level and max_level.
1897
1898        If the affine expression `times[i]` is assigned a value t, then the current
1899        level changes by `level_changes[i]`, which is constant, at time t.
1900
1901         Note that min level must be <= 0, and the max level must be >= 0. Please
1902         use fixed level_changes to simulate initial state.
1903
1904         Therefore, at any time:
1905             sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
1906
1907        Args:
1908          times: A list of 1-var affine expressions (a * x + b) which specify the
1909            time of the filling or emptying the reservoir.
1910          level_changes: A list of integer values that specifies the amount of the
1911            emptying or filling. Currently, variable demands are not supported.
1912          min_level: At any time, the level of the reservoir must be greater or
1913            equal than the min level.
1914          max_level: At any time, the level of the reservoir must be less or equal
1915            than the max level.
1916
1917        Returns:
1918          An instance of the `Constraint` class.
1919
1920        Raises:
1921          ValueError: if max_level < min_level.
1922
1923          ValueError: if max_level < 0.
1924
1925          ValueError: if min_level > 0
1926        """
1927
1928        if max_level < min_level:
1929            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1930
1931        if max_level < 0:
1932            raise ValueError("Reservoir constraint must have a max_level >= 0")
1933
1934        if min_level > 0:
1935            raise ValueError("Reservoir constraint must have a min_level <= 0")
1936
1937        ct = Constraint(self)
1938        model_ct = self.__model.constraints[ct.index]
1939        model_ct.reservoir.time_exprs.extend(
1940            [self.parse_linear_expression(x) for x in times]
1941        )
1942        model_ct.reservoir.level_changes.extend(
1943            [self.parse_linear_expression(x) for x in level_changes]
1944        )
1945        model_ct.reservoir.min_level = min_level
1946        model_ct.reservoir.max_level = max_level
1947        return ct

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

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

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

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: if max_level < min_level.
  • ValueError: if max_level < 0.
  • ValueError: if min_level > 0
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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]], min_level: int, max_level: int) -> Constraint:
1949    def add_reservoir_constraint_with_active(
1950        self,
1951        times: Iterable[LinearExprT],
1952        level_changes: Iterable[LinearExprT],
1953        actives: Iterable[LiteralT],
1954        min_level: int,
1955        max_level: int,
1956    ) -> Constraint:
1957        """Adds Reservoir(times, level_changes, actives, min_level, max_level).
1958
1959        Maintains a reservoir level within bounds. The water level starts at 0, and
1960        at any time, it must be between min_level and max_level.
1961
1962        If the variable `times[i]` is assigned a value t, and `actives[i]` is
1963        `True`, then the current level changes by `level_changes[i]`, which is
1964        constant,
1965        at time t.
1966
1967         Note that min level must be <= 0, and the max level must be >= 0. Please
1968         use fixed level_changes to simulate initial state.
1969
1970         Therefore, at any time:
1971             sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level,
1972             max_level]
1973
1974
1975        The array of boolean variables 'actives', if defined, indicates which
1976        actions are actually performed.
1977
1978        Args:
1979          times: A list of 1-var affine expressions (a * x + b) which specify the
1980            time of the filling or emptying the reservoir.
1981          level_changes: A list of integer values that specifies the amount of the
1982            emptying or filling. Currently, variable demands are not supported.
1983          actives: a list of boolean variables. They indicates if the
1984            emptying/refilling events actually take place.
1985          min_level: At any time, the level of the reservoir must be greater or
1986            equal than the min level.
1987          max_level: At any time, the level of the reservoir must be less or equal
1988            than the max level.
1989
1990        Returns:
1991          An instance of the `Constraint` class.
1992
1993        Raises:
1994          ValueError: if max_level < min_level.
1995
1996          ValueError: if max_level < 0.
1997
1998          ValueError: if min_level > 0
1999        """
2000
2001        if max_level < min_level:
2002            raise ValueError("Reservoir constraint must have a max_level >= min_level")
2003
2004        if max_level < 0:
2005            raise ValueError("Reservoir constraint must have a max_level >= 0")
2006
2007        if min_level > 0:
2008            raise ValueError("Reservoir constraint must have a min_level <= 0")
2009
2010        ct = Constraint(self)
2011        model_ct = self.__model.constraints[ct.index]
2012        model_ct.reservoir.time_exprs.extend(
2013            [self.parse_linear_expression(x) for x in times]
2014        )
2015        model_ct.reservoir.level_changes.extend(
2016            [self.parse_linear_expression(x) for x in level_changes]
2017        )
2018        model_ct.reservoir.active_literals.extend(
2019            [self.get_or_make_boolean_index(x) for x in actives]
2020        )
2021        model_ct.reservoir.min_level = min_level
2022        model_ct.reservoir.max_level = max_level
2023        return ct

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

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

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

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

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: if max_level < min_level.
  • ValueError: if max_level < 0.
  • ValueError: if min_level > 0
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):
2025    def add_map_domain(
2026        self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0
2027    ):
2028        """Adds `var == i + offset <=> bool_var_array[i] == true for all i`."""
2029
2030        for i, bool_var in enumerate(bool_var_array):
2031            b_index = bool_var.index
2032            var_index = var.index
2033            model_ct = self.__model.constraints.add()
2034            model_ct.linear.vars.append(var_index)
2035            model_ct.linear.coeffs.append(1)
2036            offset_as_int = int(offset)
2037            model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i])
2038            model_ct.enforcement_literal.append(b_index)
2039
2040            model_ct = self.__model.constraints.add()
2041            model_ct.linear.vars.append(var_index)
2042            model_ct.linear.coeffs.append(1)
2043            model_ct.enforcement_literal.append(-b_index - 1)
2044            if offset + i - 1 >= INT_MIN:
2045                model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1])
2046            if offset + i + 1 <= INT_MAX:
2047                model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX])

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

def add_implication( self, a: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], b: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> Constraint:
2049    def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint:
2050        """Adds `a => b` (`a` implies `b`)."""
2051        ct = Constraint(self)
2052        model_ct = self.__model.constraints[ct.index]
2053        model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b))
2054        model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a))
2055        return ct

Adds a => b (a implies b).

def add_bool_or(self, *literals):
2063    def add_bool_or(self, *literals):
2064        """Adds `Or(literals) == true`: sum(literals) >= 1."""
2065        ct = Constraint(self)
2066        model_ct = self.__model.constraints[ct.index]
2067        model_ct.bool_or.literals.extend(
2068            [
2069                self.get_or_make_boolean_index(x)
2070                for x in expand_generator_or_tuple(literals)
2071            ]
2072        )
2073        return ct

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

def add_at_least_one(self, *literals):
2081    def add_at_least_one(self, *literals):
2082        """Same as `add_bool_or`: `sum(literals) >= 1`."""
2083        return self.add_bool_or(*literals)

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

def add_at_most_one(self, *literals):
2091    def add_at_most_one(self, *literals):
2092        """Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
2093        ct = Constraint(self)
2094        model_ct = self.__model.constraints[ct.index]
2095        model_ct.at_most_one.literals.extend(
2096            [
2097                self.get_or_make_boolean_index(x)
2098                for x in expand_generator_or_tuple(literals)
2099            ]
2100        )
2101        return ct

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

def add_exactly_one(self, *literals):
2109    def add_exactly_one(self, *literals):
2110        """Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
2111        ct = Constraint(self)
2112        model_ct = self.__model.constraints[ct.index]
2113        model_ct.exactly_one.literals.extend(
2114            [
2115                self.get_or_make_boolean_index(x)
2116                for x in expand_generator_or_tuple(literals)
2117            ]
2118        )
2119        return ct

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

def add_bool_and(self, *literals):
2127    def add_bool_and(self, *literals):
2128        """Adds `And(literals) == true`."""
2129        ct = Constraint(self)
2130        model_ct = self.__model.constraints[ct.index]
2131        model_ct.bool_and.literals.extend(
2132            [
2133                self.get_or_make_boolean_index(x)
2134                for x in expand_generator_or_tuple(literals)
2135            ]
2136        )
2137        return ct

Adds And(literals) == true.

def add_bool_xor(self, *literals):
2145    def add_bool_xor(self, *literals):
2146        """Adds `XOr(literals) == true`.
2147
2148        In contrast to add_bool_or and add_bool_and, it does not support
2149            .only_enforce_if().
2150
2151        Args:
2152          *literals: the list of literals in the constraint.
2153
2154        Returns:
2155          An `Constraint` object.
2156        """
2157        ct = Constraint(self)
2158        model_ct = self.__model.constraints[ct.index]
2159        model_ct.bool_xor.literals.extend(
2160            [
2161                self.get_or_make_boolean_index(x)
2162                for x in expand_generator_or_tuple(literals)
2163            ]
2164        )
2165        return ct

Adds XOr(literals) == true.

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

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

An Constraint object.

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:
2167    def add_min_equality(
2168        self, target: LinearExprT, exprs: Iterable[LinearExprT]
2169    ) -> Constraint:
2170        """Adds `target == Min(exprs)`."""
2171        ct = Constraint(self)
2172        model_ct = self.__model.constraints[ct.index]
2173        model_ct.lin_max.exprs.extend(
2174            [self.parse_linear_expression(x, True) for x in exprs]
2175        )
2176        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True))
2177        return ct

Adds target == Min(exprs).

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:
2179    def add_max_equality(
2180        self, target: LinearExprT, exprs: Iterable[LinearExprT]
2181    ) -> Constraint:
2182        """Adds `target == Max(exprs)`."""
2183        ct = Constraint(self)
2184        model_ct = self.__model.constraints[ct.index]
2185        model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs])
2186        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
2187        return ct

Adds target == Max(exprs).

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:
2189    def add_division_equality(
2190        self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
2191    ) -> Constraint:
2192        """Adds `target == num // denom` (integer division rounded towards 0)."""
2193        ct = Constraint(self)
2194        model_ct = self.__model.constraints[ct.index]
2195        model_ct.int_div.exprs.append(self.parse_linear_expression(num))
2196        model_ct.int_div.exprs.append(self.parse_linear_expression(denom))
2197        model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target))
2198        return ct

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

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:
2200    def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
2201        """Adds `target == Abs(expr)`."""
2202        ct = Constraint(self)
2203        model_ct = self.__model.constraints[ct.index]
2204        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr))
2205        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True))
2206        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
2207        return ct

Adds target == Abs(expr).

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:
2209    def add_modulo_equality(
2210        self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
2211    ) -> Constraint:
2212        """Adds `target = expr % mod`.
2213
2214        It uses the C convention, that is the result is the remainder of the
2215        integral division rounded towards 0.
2216
2217            For example:
2218            * 10 % 3 = 1
2219            * -10 % 3 = -1
2220            * 10 % -3 = 1
2221            * -10 % -3 = -1
2222
2223        Args:
2224          target: the target expression.
2225          expr: the expression to compute the modulo of.
2226          mod: the modulus expression.
2227
2228        Returns:
2229          A `Constraint` object.
2230        """
2231        ct = Constraint(self)
2232        model_ct = self.__model.constraints[ct.index]
2233        model_ct.int_mod.exprs.append(self.parse_linear_expression(expr))
2234        model_ct.int_mod.exprs.append(self.parse_linear_expression(mod))
2235        model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target))
2236        return ct

Adds target = expr % mod.

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

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

A Constraint object.

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:
2238    def add_multiplication_equality(
2239        self,
2240        target: LinearExprT,
2241        *expressions: Union[Iterable[LinearExprT], LinearExprT],
2242    ) -> Constraint:
2243        """Adds `target == expressions[0] * .. * expressions[n]`."""
2244        ct = Constraint(self)
2245        model_ct = self.__model.constraints[ct.index]
2246        model_ct.int_prod.exprs.extend(
2247            [
2248                self.parse_linear_expression(expr)
2249                for expr in expand_generator_or_tuple(expressions)
2250            ]
2251        )
2252        model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target))
2253        return ct

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

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:
2257    def new_interval_var(
2258        self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str
2259    ) -> IntervalVar:
2260        """Creates an interval variable from start, size, and end.
2261
2262        An interval variable is a constraint, that is itself used in other
2263        constraints like NoOverlap.
2264
2265        Internally, it ensures that `start + size == end`.
2266
2267        Args:
2268          start: The start of the interval. It must be of the form a * var + b.
2269          size: The size of the interval. It must be of the form a * var + b.
2270          end: The end of the interval. It must be of the form a * var + b.
2271          name: The name of the interval variable.
2272
2273        Returns:
2274          An `IntervalVar` object.
2275        """
2276
2277        start_expr = self.parse_linear_expression(start)
2278        size_expr = self.parse_linear_expression(size)
2279        end_expr = self.parse_linear_expression(end)
2280        if len(start_expr.vars) > 1:
2281            raise TypeError(
2282                "cp_model.new_interval_var: start must be 1-var affine or constant."
2283            )
2284        if len(size_expr.vars) > 1:
2285            raise TypeError(
2286                "cp_model.new_interval_var: size must be 1-var affine or constant."
2287            )
2288        if len(end_expr.vars) > 1:
2289            raise TypeError(
2290                "cp_model.new_interval_var: end must be 1-var affine or constant."
2291            )
2292        return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)

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

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

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

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

An IntervalVar object.

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:
2294    def new_interval_var_series(
2295        self,
2296        name: str,
2297        index: pd.Index,
2298        starts: Union[LinearExprT, pd.Series],
2299        sizes: Union[LinearExprT, pd.Series],
2300        ends: Union[LinearExprT, pd.Series],
2301    ) -> pd.Series:
2302        """Creates a series of interval variables with the given name.
2303
2304        Args:
2305          name (str): Required. The name of the variable set.
2306          index (pd.Index): Required. The index to use for the variable set.
2307          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2308            set. If a `pd.Series` is passed in, it will be based on the
2309            corresponding values of the pd.Series.
2310          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
2311            set. If a `pd.Series` is passed in, it will be based on the
2312            corresponding values of the pd.Series.
2313          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
2314            set. If a `pd.Series` is passed in, it will be based on the
2315            corresponding values of the pd.Series.
2316
2317        Returns:
2318          pd.Series: The interval variable set indexed by its corresponding
2319          dimensions.
2320
2321        Raises:
2322          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2323          ValueError: if the `name` is not a valid identifier or already exists.
2324          ValueError: if the all the indexes do not match.
2325        """
2326        if not isinstance(index, pd.Index):
2327            raise TypeError("Non-index object is used as index")
2328        if not name.isidentifier():
2329            raise ValueError("name={} is not a valid identifier".format(name))
2330
2331        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2332        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
2333        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
2334        interval_array = []
2335        for i in index:
2336            interval_array.append(
2337                self.new_interval_var(
2338                    start=starts[i],
2339                    size=sizes[i],
2340                    end=ends[i],
2341                    name=f"{name}[{i}]",
2342                )
2343            )
2344        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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:
2346    def new_fixed_size_interval_var(
2347        self, start: LinearExprT, size: IntegralT, name: str
2348    ) -> IntervalVar:
2349        """Creates an interval variable from start, and a fixed size.
2350
2351        An interval variable is a constraint, that is itself used in other
2352        constraints like NoOverlap.
2353
2354        Args:
2355          start: The start of the interval. It must be of the form a * var + b.
2356          size: The size of the interval. It must be an integer value.
2357          name: The name of the interval variable.
2358
2359        Returns:
2360          An `IntervalVar` object.
2361        """
2362        size = cmh.assert_is_int64(size)
2363        start_expr = self.parse_linear_expression(start)
2364        size_expr = self.parse_linear_expression(size)
2365        end_expr = self.parse_linear_expression(start + size)
2366        if len(start_expr.vars) > 1:
2367            raise TypeError(
2368                "cp_model.new_interval_var: start must be affine or constant."
2369            )
2370        return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)

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

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

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

An IntervalVar object.

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:
2372    def new_fixed_size_interval_var_series(
2373        self,
2374        name: str,
2375        index: pd.Index,
2376        starts: Union[LinearExprT, pd.Series],
2377        sizes: Union[IntegralT, pd.Series],
2378    ) -> pd.Series:
2379        """Creates a series of interval variables with the given name.
2380
2381        Args:
2382          name (str): Required. The name of the variable set.
2383          index (pd.Index): Required. The index to use for the variable set.
2384          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2385            set. If a `pd.Series` is passed in, it will be based on the
2386            corresponding values of the pd.Series.
2387          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
2388            the set. If a `pd.Series` is passed in, it will be based on the
2389            corresponding values of the pd.Series.
2390
2391        Returns:
2392          pd.Series: The interval variable set indexed by its corresponding
2393          dimensions.
2394
2395        Raises:
2396          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2397          ValueError: if the `name` is not a valid identifier or already exists.
2398          ValueError: if the all the indexes do not match.
2399        """
2400        if not isinstance(index, pd.Index):
2401            raise TypeError("Non-index object is used as index")
2402        if not name.isidentifier():
2403            raise ValueError("name={} is not a valid identifier".format(name))
2404
2405        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2406        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
2407        interval_array = []
2408        for i in index:
2409            interval_array.append(
2410                self.new_fixed_size_interval_var(
2411                    start=starts[i],
2412                    size=sizes[i],
2413                    name=f"{name}[{i}]",
2414                )
2415            )
2416        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], name: str) -> IntervalVar:
2418    def new_optional_interval_var(
2419        self,
2420        start: LinearExprT,
2421        size: LinearExprT,
2422        end: LinearExprT,
2423        is_present: LiteralT,
2424        name: str,
2425    ) -> IntervalVar:
2426        """Creates an optional interval var from start, size, end, and is_present.
2427
2428        An optional interval variable is a constraint, that is itself used in other
2429        constraints like NoOverlap. This constraint is protected by a presence
2430        literal that indicates if it is active or not.
2431
2432        Internally, it ensures that `is_present` implies `start + size ==
2433        end`.
2434
2435        Args:
2436          start: The start of the interval. It must be of the form a * var + b.
2437          size: The size of the interval. It must be of the form a * var + b.
2438          end: The end of the interval. It must be of the form a * var + b.
2439          is_present: A literal that indicates if the interval is active or not. A
2440            inactive interval is simply ignored by all constraints.
2441          name: The name of the interval variable.
2442
2443        Returns:
2444          An `IntervalVar` object.
2445        """
2446
2447        # Creates the IntervalConstraintProto object.
2448        is_present_index = self.get_or_make_boolean_index(is_present)
2449        start_expr = self.parse_linear_expression(start)
2450        size_expr = self.parse_linear_expression(size)
2451        end_expr = self.parse_linear_expression(end)
2452        if len(start_expr.vars) > 1:
2453            raise TypeError(
2454                "cp_model.new_interval_var: start must be affine or constant."
2455            )
2456        if len(size_expr.vars) > 1:
2457            raise TypeError(
2458                "cp_model.new_interval_var: size must be affine or constant."
2459            )
2460        if len(end_expr.vars) > 1:
2461            raise TypeError(
2462                "cp_model.new_interval_var: end must be affine or constant."
2463            )
2464        return IntervalVar(
2465            self.__model, start_expr, size_expr, end_expr, is_present_index, name
2466        )

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

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

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

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

An IntervalVar object.

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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool, pandas.core.series.Series]) -> pandas.core.series.Series:
2468    def new_optional_interval_var_series(
2469        self,
2470        name: str,
2471        index: pd.Index,
2472        starts: Union[LinearExprT, pd.Series],
2473        sizes: Union[LinearExprT, pd.Series],
2474        ends: Union[LinearExprT, pd.Series],
2475        are_present: Union[LiteralT, pd.Series],
2476    ) -> pd.Series:
2477        """Creates a series of interval variables with the given name.
2478
2479        Args:
2480          name (str): Required. The name of the variable set.
2481          index (pd.Index): Required. The index to use for the variable set.
2482          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2483            set. If a `pd.Series` is passed in, it will be based on the
2484            corresponding values of the pd.Series.
2485          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
2486            set. If a `pd.Series` is passed in, it will be based on the
2487            corresponding values of the pd.Series.
2488          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
2489            set. If a `pd.Series` is passed in, it will be based on the
2490            corresponding values of the pd.Series.
2491          are_present (Union[LiteralT, pd.Series]): The performed literal of each
2492            interval in the set. If a `pd.Series` is passed in, it will be based on
2493            the corresponding values of the pd.Series.
2494
2495        Returns:
2496          pd.Series: The interval variable set indexed by its corresponding
2497          dimensions.
2498
2499        Raises:
2500          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2501          ValueError: if the `name` is not a valid identifier or already exists.
2502          ValueError: if the all the indexes do not match.
2503        """
2504        if not isinstance(index, pd.Index):
2505            raise TypeError("Non-index object is used as index")
2506        if not name.isidentifier():
2507            raise ValueError("name={} is not a valid identifier".format(name))
2508
2509        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2510        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
2511        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
2512        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
2513
2514        interval_array = []
2515        for i in index:
2516            interval_array.append(
2517                self.new_optional_interval_var(
2518                    start=starts[i],
2519                    size=sizes[i],
2520                    end=ends[i],
2521                    is_present=are_present[i],
2522                    name=f"{name}[{i}]",
2523                )
2524            )
2525        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], name: str) -> IntervalVar:
2527    def new_optional_fixed_size_interval_var(
2528        self,
2529        start: LinearExprT,
2530        size: IntegralT,
2531        is_present: LiteralT,
2532        name: str,
2533    ) -> IntervalVar:
2534        """Creates an interval variable from start, and a fixed size.
2535
2536        An interval variable is a constraint, that is itself used in other
2537        constraints like NoOverlap.
2538
2539        Args:
2540          start: The start of the interval. It must be of the form a * var + b.
2541          size: The size of the interval. It must be an integer value.
2542          is_present: A literal that indicates if the interval is active or not. A
2543            inactive interval is simply ignored by all constraints.
2544          name: The name of the interval variable.
2545
2546        Returns:
2547          An `IntervalVar` object.
2548        """
2549        size = cmh.assert_is_int64(size)
2550        start_expr = self.parse_linear_expression(start)
2551        size_expr = self.parse_linear_expression(size)
2552        end_expr = self.parse_linear_expression(start + size)
2553        if len(start_expr.vars) > 1:
2554            raise TypeError(
2555                "cp_model.new_interval_var: start must be affine or constant."
2556            )
2557        is_present_index = self.get_or_make_boolean_index(is_present)
2558        return IntervalVar(
2559            self.__model,
2560            start_expr,
2561            size_expr,
2562            end_expr,
2563            is_present_index,
2564            name,
2565        )

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

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

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

An IntervalVar object.

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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool, pandas.core.series.Series]) -> pandas.core.series.Series:
2567    def new_optional_fixed_size_interval_var_series(
2568        self,
2569        name: str,
2570        index: pd.Index,
2571        starts: Union[LinearExprT, pd.Series],
2572        sizes: Union[IntegralT, pd.Series],
2573        are_present: Union[LiteralT, pd.Series],
2574    ) -> pd.Series:
2575        """Creates a series of interval variables with the given name.
2576
2577        Args:
2578          name (str): Required. The name of the variable set.
2579          index (pd.Index): Required. The index to use for the variable set.
2580          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2581            set. If a `pd.Series` is passed in, it will be based on the
2582            corresponding values of the pd.Series.
2583          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
2584            the set. If a `pd.Series` is passed in, it will be based on the
2585            corresponding values of the pd.Series.
2586          are_present (Union[LiteralT, pd.Series]): The performed literal of each
2587            interval in the set. If a `pd.Series` is passed in, it will be based on
2588            the corresponding values of the pd.Series.
2589
2590        Returns:
2591          pd.Series: The interval variable set indexed by its corresponding
2592          dimensions.
2593
2594        Raises:
2595          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2596          ValueError: if the `name` is not a valid identifier or already exists.
2597          ValueError: if the all the indexes do not match.
2598        """
2599        if not isinstance(index, pd.Index):
2600            raise TypeError("Non-index object is used as index")
2601        if not name.isidentifier():
2602            raise ValueError("name={} is not a valid identifier".format(name))
2603
2604        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2605        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
2606        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
2607        interval_array = []
2608        for i in index:
2609            interval_array.append(
2610                self.new_optional_fixed_size_interval_var(
2611                    start=starts[i],
2612                    size=sizes[i],
2613                    is_present=are_present[i],
2614                    name=f"{name}[{i}]",
2615                )
2616            )
2617        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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:
2619    def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint:
2620        """Adds NoOverlap(interval_vars).
2621
2622        A NoOverlap constraint ensures that all present intervals do not overlap
2623        in time.
2624
2625        Args:
2626          interval_vars: The list of interval variables to constrain.
2627
2628        Returns:
2629          An instance of the `Constraint` class.
2630        """
2631        ct = Constraint(self)
2632        model_ct = self.__model.constraints[ct.index]
2633        model_ct.no_overlap.intervals.extend(
2634            [self.get_interval_index(x) for x in interval_vars]
2635        )
2636        return ct

Adds NoOverlap(interval_vars).

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

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

An instance of the Constraint class.

def add_no_overlap_2d( self, x_intervals: Iterable[IntervalVar], y_intervals: Iterable[IntervalVar]) -> Constraint:
2638    def add_no_overlap_2d(
2639        self,
2640        x_intervals: Iterable[IntervalVar],
2641        y_intervals: Iterable[IntervalVar],
2642    ) -> Constraint:
2643        """Adds NoOverlap2D(x_intervals, y_intervals).
2644
2645        A NoOverlap2D constraint ensures that all present rectangles do not overlap
2646        on a plane. Each rectangle is aligned with the X and Y axis, and is defined
2647        by two intervals which represent its projection onto the X and Y axis.
2648
2649        Furthermore, one box is optional if at least one of the x or y interval is
2650        optional.
2651
2652        Args:
2653          x_intervals: The X coordinates of the rectangles.
2654          y_intervals: The Y coordinates of the rectangles.
2655
2656        Returns:
2657          An instance of the `Constraint` class.
2658        """
2659        ct = Constraint(self)
2660        model_ct = self.__model.constraints[ct.index]
2661        model_ct.no_overlap_2d.x_intervals.extend(
2662            [self.get_interval_index(x) for x in x_intervals]
2663        )
2664        model_ct.no_overlap_2d.y_intervals.extend(
2665            [self.get_interval_index(x) for x in y_intervals]
2666        )
2667        return ct

Adds NoOverlap2D(x_intervals, y_intervals).

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

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

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

An instance of the Constraint class.

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:
2669    def add_cumulative(
2670        self,
2671        intervals: Iterable[IntervalVar],
2672        demands: Iterable[LinearExprT],
2673        capacity: LinearExprT,
2674    ) -> Constraint:
2675        """Adds Cumulative(intervals, demands, capacity).
2676
2677        This constraint enforces that:
2678
2679            for all t:
2680              sum(demands[i]
2681                if (start(intervals[i]) <= t < end(intervals[i])) and
2682                (intervals[i] is present)) <= capacity
2683
2684        Args:
2685          intervals: The list of intervals.
2686          demands: The list of demands for each interval. Each demand must be >= 0.
2687            Each demand can be a 1-var affine expression (a * x + b).
2688          capacity: The maximum capacity of the cumulative constraint. It can be a
2689            1-var affine expression (a * x + b).
2690
2691        Returns:
2692          An instance of the `Constraint` class.
2693        """
2694        cumulative = Constraint(self)
2695        model_ct = self.__model.constraints[cumulative.index]
2696        model_ct.cumulative.intervals.extend(
2697            [self.get_interval_index(x) for x in intervals]
2698        )
2699        for d in demands:
2700            model_ct.cumulative.demands.append(self.parse_linear_expression(d))
2701        model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity))
2702        return cumulative

Adds Cumulative(intervals, demands, capacity).

This constraint enforces that:

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

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

An instance of the Constraint class.

def clone(self) -> CpModel:
2705    def clone(self) -> "CpModel":
2706        """Reset the model, and creates a new one from a CpModelProto instance."""
2707        clone = CpModel()
2708        clone.proto.CopyFrom(self.proto)
2709        clone.rebuild_constant_map()
2710        return clone

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

def rebuild_constant_map(self):
2712    def rebuild_constant_map(self):
2713        """Internal method used during model cloning."""
2714        for i, var in enumerate(self.__model.variables):
2715            if len(var.domain) == 2 and var.domain[0] == var.domain[1]:
2716                self.__constant_map[var.domain[0]] = i

Internal method used during model cloning.

def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2718    def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2719        """Returns an already created Boolean variable from its index."""
2720        if index < 0 or index >= len(self.__model.variables):
2721            raise ValueError(
2722                f"get_bool_var_from_proto_index: out of bound index {index}"
2723            )
2724        var = self.__model.variables[index]
2725        if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1:
2726            raise ValueError(
2727                f"get_bool_var_from_proto_index: index {index} does not reference"
2728                + " a Boolean variable"
2729            )
2730
2731        return IntVar(self.__model, index, None)

Returns an already created Boolean variable from its index.

def get_int_var_from_proto_index(self, index: int) -> IntVar:
2733    def get_int_var_from_proto_index(self, index: int) -> IntVar:
2734        """Returns an already created integer variable from its index."""
2735        if index < 0 or index >= len(self.__model.variables):
2736            raise ValueError(
2737                f"get_int_var_from_proto_index: out of bound index {index}"
2738            )
2739        return IntVar(self.__model, index, None)

Returns an already created integer variable from its index.

def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2741    def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2742        """Returns an already created interval variable from its index."""
2743        if index < 0 or index >= len(self.__model.constraints):
2744            raise ValueError(
2745                f"get_interval_var_from_proto_index: out of bound index {index}"
2746            )
2747        ct = self.__model.constraints[index]
2748        if not ct.HasField("interval"):
2749            raise ValueError(
2750                f"get_interval_var_from_proto_index: index {index} does not"
2751                " reference an" + " interval variable"
2752            )
2753
2754        return IntervalVar(self.__model, index, None, None, None, None)

Returns an already created interval variable from its index.

2761    @property
2762    def proto(self) -> cp_model_pb2.CpModelProto:
2763        """Returns the underlying CpModelProto."""
2764        return self.__model

Returns the underlying CpModelProto.

def negated(self, index: int) -> int:
2766    def negated(self, index: int) -> int:
2767        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:
2769    def get_or_make_index(self, arg: VariableT) -> int:
2770        """Returns the index of a variable, its negation, or a number."""
2771        if isinstance(arg, IntVar):
2772            return arg.index
2773        if (
2774            isinstance(arg, _ProductCst)
2775            and isinstance(arg.expression(), IntVar)
2776            and arg.coefficient() == -1
2777        ):
2778            return -arg.expression().index - 1
2779        if isinstance(arg, IntegralTypes):
2780            arg = cmh.assert_is_int64(arg)
2781            return self.get_or_make_index_from_constant(arg)
2782        raise TypeError("NotSupported: model.get_or_make_index(" + str(arg) + ")")

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

def get_or_make_boolean_index( self, arg: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> int:
2784    def get_or_make_boolean_index(self, arg: LiteralT) -> int:
2785        """Returns an index from a boolean expression."""
2786        if isinstance(arg, IntVar):
2787            self.assert_is_boolean_variable(arg)
2788            return arg.index
2789        if isinstance(arg, _NotBooleanVariable):
2790            self.assert_is_boolean_variable(arg.negated())
2791            return arg.index
2792        if isinstance(arg, IntegralTypes):
2793            if arg == ~False:  # -1
2794                return self.get_or_make_index_from_constant(1)
2795            if arg == ~True:  # -2
2796                return self.get_or_make_index_from_constant(0)
2797            arg = cmh.assert_is_zero_or_one(arg)
2798            return self.get_or_make_index_from_constant(arg)
2799        if cmh.is_boolean(arg):
2800            return self.get_or_make_index_from_constant(int(arg))
2801        raise TypeError(f"not supported: model.get_or_make_boolean_index({arg})")

Returns an index from a boolean expression.

def get_interval_index(self, arg: IntervalVar) -> int:
2803    def get_interval_index(self, arg: IntervalVar) -> int:
2804        if not isinstance(arg, IntervalVar):
2805            raise TypeError("NotSupported: model.get_interval_index(%s)" % arg)
2806        return arg.index
def get_or_make_index_from_constant( self, value: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
2808    def get_or_make_index_from_constant(self, value: IntegralT) -> int:
2809        if value in self.__constant_map:
2810            return self.__constant_map[value]
2811        index = len(self.__model.variables)
2812        self.__model.variables.add(domain=[value, value])
2813        self.__constant_map[value] = index
2814        return index
def var_index_to_var_proto(self, var_index: int) -> ortools.sat.cp_model_pb2.IntegerVariableProto:
2816    def var_index_to_var_proto(
2817        self, var_index: int
2818    ) -> cp_model_pb2.IntegerVariableProto:
2819        if var_index >= 0:
2820            return self.__model.variables[var_index]
2821        else:
2822            return self.__model.variables[-var_index - 1]
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:
2824    def parse_linear_expression(
2825        self, linear_expr: LinearExprT, negate: bool = False
2826    ) -> cp_model_pb2.LinearExpressionProto:
2827        """Returns a LinearExpressionProto built from a LinearExpr instance."""
2828        result: cp_model_pb2.LinearExpressionProto = (
2829            cp_model_pb2.LinearExpressionProto()
2830        )
2831        mult = -1 if negate else 1
2832        if isinstance(linear_expr, IntegralTypes):
2833            result.offset = int(linear_expr) * mult
2834            return result
2835
2836        if isinstance(linear_expr, IntVar):
2837            result.vars.append(self.get_or_make_index(linear_expr))
2838            result.coeffs.append(mult)
2839            return result
2840
2841        coeffs_map, constant = cast(LinearExpr, linear_expr).get_integer_var_value_map()
2842        result.offset = constant * mult
2843        for t in coeffs_map.items():
2844            if not isinstance(t[0], IntVar):
2845                raise TypeError("Wrong argument" + str(t))
2846            c = cmh.assert_is_int64(t[1])
2847            result.vars.append(t[0].index)
2848            result.coeffs.append(c * mult)
2849        return result

Returns a LinearExpressionProto built from a LinearExpr instance.

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

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

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

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

def has_objective(self) -> bool:
2899    def has_objective(self) -> bool:
2900        return self.__model.HasField("objective") or self.__model.HasField(
2901            "floating_point_objective"
2902        )
def clear_objective(self):
2904    def clear_objective(self):
2905        self.__model.ClearField("objective")
2906        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:
2908    def add_decision_strategy(
2909        self,
2910        variables: Sequence[IntVar],
2911        var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy,
2912        domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy,
2913    ) -> None:
2914        """Adds a search strategy to the model.
2915
2916        Args:
2917          variables: a list of variables this strategy will assign.
2918          var_strategy: heuristic to choose the next variable to assign.
2919          domain_strategy: heuristic to reduce the domain of the selected variable.
2920            Currently, this is advanced code: the union of all strategies added to
2921            the model must be complete, i.e. instantiates all variables. Otherwise,
2922            solve() will fail.
2923        """
2924
2925        strategy: cp_model_pb2.DecisionStrategyProto = (
2926            self.__model.search_strategy.add()
2927        )
2928        for v in variables:
2929            expr = strategy.exprs.add()
2930            if v.index >= 0:
2931                expr.vars.append(v.index)
2932                expr.coeffs.append(1)
2933            else:
2934                expr.vars.append(self.negated(v.index))
2935                expr.coeffs.append(-1)
2936                expr.offset = 1
2937
2938        strategy.variable_selection_strategy = var_strategy
2939        strategy.domain_reduction_strategy = domain_strategy

Adds a search strategy to the model.

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

Returns a string containing some model statistics.

def validate(self) -> str:
2945    def validate(self) -> str:
2946        """Returns a string indicating that the model is invalid."""
2947        return swig_helper.CpSatHelper.validate_model(self.__model)

Returns a string indicating that the model is invalid.

def export_to_file(self, file: str) -> bool:
2949    def export_to_file(self, file: str) -> bool:
2950        """Write the model as a protocol buffer to 'file'.
2951
2952        Args:
2953          file: file to write the model to. If the filename ends with 'txt', the
2954            model will be written as a text file, otherwise, the binary format will
2955            be used.
2956
2957        Returns:
2958          True if the model was correctly written.
2959        """
2960        return swig_helper.CpSatHelper.write_model_to_file(self.__model, file)

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

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

True if the model was correctly written.

def add_hint(self, var: IntVar, value: int) -> None:
2962    def add_hint(self, var: IntVar, value: int) -> None:
2963        """Adds 'var == value' as a hint to the solver."""
2964        self.__model.solution_hint.vars.append(self.get_or_make_index(var))
2965        self.__model.solution_hint.values.append(value)

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

def clear_hints(self):
2967    def clear_hints(self):
2968        """Removes any solution hint from the model."""
2969        self.__model.ClearField("solution_hint")

Removes any solution hint from the model.

def add_assumption( self, lit: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> None:
2971    def add_assumption(self, lit: LiteralT) -> None:
2972        """Adds the literal to the model as assumptions."""
2973        self.__model.assumptions.append(self.get_or_make_boolean_index(lit))

Adds the literal to the model as assumptions.

def add_assumptions( self, literals: Iterable[Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]) -> None:
2975    def add_assumptions(self, literals: Iterable[LiteralT]) -> None:
2976        """Adds the literals to the model as assumptions."""
2977        for lit in literals:
2978            self.add_assumption(lit)

Adds the literals to the model as assumptions.

def clear_assumptions(self) -> None:
2980    def clear_assumptions(self) -> None:
2981        """Removes all assumptions from the model."""
2982        self.__model.ClearField("assumptions")

Removes all assumptions from the model.

def assert_is_boolean_variable( self, x: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> None:
2985    def assert_is_boolean_variable(self, x: LiteralT) -> None:
2986        if isinstance(x, IntVar):
2987            var = self.__model.variables[x.index]
2988            if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1:
2989                raise TypeError("TypeError: " + str(x) + " is not a boolean variable")
2990        elif not isinstance(x, _NotBooleanVariable):
2991            raise TypeError("TypeError: " + str(x) + " is not a boolean variable")
def Name(self) -> str:
2996    def Name(self) -> str:
2997        return self.name
def SetName(self, name: str) -> None:
2999    def SetName(self, name: str) -> None:
3000        self.name = name
def Proto(self) -> ortools.sat.cp_model_pb2.CpModelProto:
3002    def Proto(self) -> cp_model_pb2.CpModelProto:
3003        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:
1362    def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar:
1363        """Create an integer variable with domain [lb, ub].
1364
1365        The CP-SAT solver is limited to integer variables. If you have fractional
1366        values, scale them up so that they become integers; if you have strings,
1367        encode them as integers.
1368
1369        Args:
1370          lb: Lower bound for the variable.
1371          ub: Upper bound for the variable.
1372          name: The name of the variable.
1373
1374        Returns:
1375          a variable whose domain is [lb, ub].
1376        """
1377
1378        return IntVar(self.__model, sorted_interval_list.Domain(lb, ub), name)

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

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

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

a variable whose domain is [lb, ub].

def NewIntVarFromDomain( self, domain: Domain, name: str) -> IntVar:
1380    def new_int_var_from_domain(
1381        self, domain: sorted_interval_list.Domain, name: str
1382    ) -> IntVar:
1383        """Create an integer variable from a domain.
1384
1385        A domain is a set of integers specified by a collection of intervals.
1386        For example, `model.new_int_var_from_domain(cp_model.
1387             Domain.from_intervals([[1, 2], [4, 6]]), 'x')`
1388
1389        Args:
1390          domain: An instance of the Domain class.
1391          name: The name of the variable.
1392
1393        Returns:
1394            a variable whose domain is the given domain.
1395        """
1396        return IntVar(self.__model, domain, name)

Create an integer variable from a domain.

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

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

a variable whose domain is the given domain.

def NewBoolVar(self, name: str) -> IntVar:
1398    def new_bool_var(self, name: str) -> IntVar:
1399        """Creates a 0-1 variable with the given name."""
1400        return IntVar(self.__model, sorted_interval_list.Domain(0, 1), name)

Creates a 0-1 variable with the given name.

def NewConstant( self, value: Union[int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> IntVar:
1402    def new_constant(self, value: IntegralT) -> IntVar:
1403        """Declares a constant integer."""
1404        return IntVar(self.__model, self.get_or_make_index_from_constant(value), None)

Declares a constant integer.

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:
1406    def new_int_var_series(
1407        self,
1408        name: str,
1409        index: pd.Index,
1410        lower_bounds: Union[IntegralT, pd.Series],
1411        upper_bounds: Union[IntegralT, pd.Series],
1412    ) -> pd.Series:
1413        """Creates a series of (scalar-valued) variables with the given name.
1414
1415        Args:
1416          name (str): Required. The name of the variable set.
1417          index (pd.Index): Required. The index to use for the variable set.
1418          lower_bounds (Union[int, pd.Series]): A lower bound for variables in the
1419            set. If a `pd.Series` is passed in, it will be based on the
1420            corresponding values of the pd.Series.
1421          upper_bounds (Union[int, pd.Series]): An upper bound for variables in the
1422            set. If a `pd.Series` is passed in, it will be based on the
1423            corresponding values of the pd.Series.
1424
1425        Returns:
1426          pd.Series: The variable set indexed by its corresponding dimensions.
1427
1428        Raises:
1429          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1430          ValueError: if the `name` is not a valid identifier or already exists.
1431          ValueError: if the `lowerbound` is greater than the `upperbound`.
1432          ValueError: if the index of `lower_bound`, or `upper_bound` does not match
1433          the input index.
1434        """
1435        if not isinstance(index, pd.Index):
1436            raise TypeError("Non-index object is used as index")
1437        if not name.isidentifier():
1438            raise ValueError("name={} is not a valid identifier".format(name))
1439        if (
1440            isinstance(lower_bounds, IntegralTypes)
1441            and isinstance(upper_bounds, IntegralTypes)
1442            and lower_bounds > upper_bounds
1443        ):
1444            raise ValueError(
1445                f"lower_bound={lower_bounds} is greater than"
1446                f" upper_bound={upper_bounds} for variable set={name}"
1447            )
1448
1449        lower_bounds = _convert_to_integral_series_and_validate_index(
1450            lower_bounds, index
1451        )
1452        upper_bounds = _convert_to_integral_series_and_validate_index(
1453            upper_bounds, index
1454        )
1455        return pd.Series(
1456            index=index,
1457            data=[
1458                # pylint: disable=g-complex-comprehension
1459                IntVar(
1460                    model=self.__model,
1461                    name=f"{name}[{i}]",
1462                    domain=sorted_interval_list.Domain(
1463                        lower_bounds[i], upper_bounds[i]
1464                    ),
1465                )
1466                for i in index
1467            ],
1468        )

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

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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:
1470    def new_bool_var_series(
1471        self,
1472        name: str,
1473        index: pd.Index,
1474    ) -> pd.Series:
1475        """Creates a series of (scalar-valued) variables with the given name.
1476
1477        Args:
1478          name (str): Required. The name of the variable set.
1479          index (pd.Index): Required. The index to use for the variable set.
1480
1481        Returns:
1482          pd.Series: The variable set indexed by its corresponding dimensions.
1483
1484        Raises:
1485          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1486          ValueError: if the `name` is not a valid identifier or already exists.
1487        """
1488        return self.new_int_var_series(
1489            name=name, index=index, lower_bounds=0, upper_bounds=1
1490        )

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

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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:
1494    def add_linear_constraint(
1495        self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT
1496    ) -> Constraint:
1497        """Adds the constraint: `lb <= linear_expr <= ub`."""
1498        return self.add_linear_expression_in_domain(
1499            linear_expr, sorted_interval_list.Domain(lb, ub)
1500        )

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

def AddLinearExpressionInDomain( self, linear_expr: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], domain: Domain) -> Constraint:
1502    def add_linear_expression_in_domain(
1503        self, linear_expr: LinearExprT, domain: sorted_interval_list.Domain
1504    ) -> Constraint:
1505        """Adds the constraint: `linear_expr` in `domain`."""
1506        if isinstance(linear_expr, LinearExpr):
1507            ct = Constraint(self)
1508            model_ct = self.__model.constraints[ct.index]
1509            coeffs_map, constant = linear_expr.get_integer_var_value_map()
1510            for t in coeffs_map.items():
1511                if not isinstance(t[0], IntVar):
1512                    raise TypeError("Wrong argument" + str(t))
1513                c = cmh.assert_is_int64(t[1])
1514                model_ct.linear.vars.append(t[0].index)
1515                model_ct.linear.coeffs.append(c)
1516            model_ct.linear.domain.extend(
1517                [
1518                    cmh.capped_subtraction(x, constant)
1519                    for x in domain.flattened_intervals()
1520                ]
1521            )
1522            return ct
1523        if isinstance(linear_expr, IntegralTypes):
1524            if not domain.contains(int(linear_expr)):
1525                return self.add_bool_or([])  # Evaluate to false.
1526            else:
1527                return self.add_bool_and([])  # Evaluate to true.
1528        raise TypeError(
1529            "not supported: CpModel.add_linear_expression_in_domain("
1530            + str(linear_expr)
1531            + " "
1532            + str(domain)
1533            + ")"
1534        )

Adds the constraint: linear_expr in domain.

def Add(self, ct):
1542    def add(self, ct):
1543        """Adds a `BoundedLinearExpression` to the model.
1544
1545        Args:
1546          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1547
1548        Returns:
1549          An instance of the `Constraint` class.
1550        """
1551        if isinstance(ct, BoundedLinearExpression):
1552            return self.add_linear_expression_in_domain(
1553                ct.expression(),
1554                sorted_interval_list.Domain.from_flat_intervals(ct.bounds()),
1555            )
1556        if ct and cmh.is_boolean(ct):
1557            return self.add_bool_or([True])
1558        if not ct and cmh.is_boolean(ct):
1559            return self.add_bool_or([])  # Evaluate to false.
1560        raise TypeError("not supported: CpModel.add(" + str(ct) + ")")

Adds a BoundedLinearExpression to the model.

Arguments:
Returns:

An instance of the Constraint class.

def AddAllDifferent(self, *expressions):
1570    def add_all_different(self, *expressions):
1571        """Adds AllDifferent(expressions).
1572
1573        This constraint forces all expressions to have different values.
1574
1575        Args:
1576          *expressions: simple expressions of the form a * var + constant.
1577
1578        Returns:
1579          An instance of the `Constraint` class.
1580        """
1581        ct = Constraint(self)
1582        model_ct = self.__model.constraints[ct.index]
1583        expanded = expand_generator_or_tuple(expressions)
1584        model_ct.all_diff.exprs.extend(
1585            self.parse_linear_expression(x) for x in expanded
1586        )
1587        return ct

Adds AllDifferent(expressions).

This constraint forces all expressions to have different values.

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

An instance of the Constraint class.

def AddElement( self, index: Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], variables: Sequence[Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]], target: Union[IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> Constraint:
1589    def add_element(
1590        self, index: VariableT, variables: Sequence[VariableT], target: VariableT
1591    ) -> Constraint:
1592        """Adds the element constraint: `variables[index] == target`.
1593
1594        Args:
1595          index: The index of the variable that's being constrained.
1596          variables: A list of variables.
1597          target: The value that the variable must be equal to.
1598
1599        Returns:
1600          An instance of the `Constraint` class.
1601        """
1602
1603        if not variables:
1604            raise ValueError("add_element expects a non-empty variables array")
1605
1606        if isinstance(index, IntegralTypes):
1607            variable: VariableT = list(variables)[int(index)]
1608            return self.add(variable == target)
1609
1610        ct = Constraint(self)
1611        model_ct = self.__model.constraints[ct.index]
1612        model_ct.element.index = self.get_or_make_index(index)
1613        model_ct.element.vars.extend([self.get_or_make_index(x) for x in variables])
1614        model_ct.element.target = self.get_or_make_index(target)
1615        return ct

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

Arguments:
  • index: The index of the variable that's being constrained.
  • variables: A list of variables.
  • target: The value that the variable must be equal to.
Returns:

An instance of the Constraint class.

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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]]) -> Constraint:
1617    def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1618        """Adds Circuit(arcs).
1619
1620        Adds a circuit constraint from a sparse list of arcs that encode the graph.
1621
1622        A circuit is a unique Hamiltonian path in a subgraph of the total
1623        graph. In case a node 'i' is not in the path, then there must be a
1624        loop arc 'i -> i' associated with a true literal. Otherwise
1625        this constraint will fail.
1626
1627        Args:
1628          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1629            literal). The arc is selected in the circuit if the literal is true.
1630            Both source_node and destination_node must be integers between 0 and the
1631            number of nodes - 1.
1632
1633        Returns:
1634          An instance of the `Constraint` class.
1635
1636        Raises:
1637          ValueError: If the list of arcs is empty.
1638        """
1639        if not arcs:
1640            raise ValueError("add_circuit expects a non-empty array of arcs")
1641        ct = Constraint(self)
1642        model_ct = self.__model.constraints[ct.index]
1643        for arc in arcs:
1644            tail = cmh.assert_is_int32(arc[0])
1645            head = cmh.assert_is_int32(arc[1])
1646            lit = self.get_or_make_boolean_index(arc[2])
1647            model_ct.circuit.tails.append(tail)
1648            model_ct.circuit.heads.append(head)
1649            model_ct.circuit.literals.append(lit)
1650        return ct

Adds Circuit(arcs).

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: If the list of arcs is empty.
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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]]) -> Constraint:
1652    def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
1653        """Adds a multiple circuit constraint, aka the 'VRP' constraint.
1654
1655        The direct graph where arc #i (from tails[i] to head[i]) is present iff
1656        literals[i] is true must satisfy this set of properties:
1657        - #incoming arcs == 1 except for node 0.
1658        - #outgoing arcs == 1 except for node 0.
1659        - for node zero, #incoming arcs == #outgoing arcs.
1660        - There are no duplicate arcs.
1661        - Self-arcs are allowed except for node 0.
1662        - There is no cycle in this graph, except through node 0.
1663
1664        Args:
1665          arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
1666            literal). The arc is selected in the circuit if the literal is true.
1667            Both source_node and destination_node must be integers between 0 and the
1668            number of nodes - 1.
1669
1670        Returns:
1671          An instance of the `Constraint` class.
1672
1673        Raises:
1674          ValueError: If the list of arcs is empty.
1675        """
1676        if not arcs:
1677            raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
1678        ct = Constraint(self)
1679        model_ct = self.__model.constraints[ct.index]
1680        for arc in arcs:
1681            tail = cmh.assert_is_int32(arc[0])
1682            head = cmh.assert_is_int32(arc[1])
1683            lit = self.get_or_make_boolean_index(arc[2])
1684            model_ct.routes.tails.append(tail)
1685            model_ct.routes.heads.append(head)
1686            model_ct.routes.literals.append(lit)
1687        return ct

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: If the list of arcs is empty.
def AddAllowedAssignments( self, variables: Sequence[Union[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:
1689    def add_allowed_assignments(
1690        self,
1691        variables: Sequence[VariableT],
1692        tuples_list: Iterable[Sequence[IntegralT]],
1693    ) -> Constraint:
1694        """Adds AllowedAssignments(variables, tuples_list).
1695
1696        An AllowedAssignments constraint is a constraint on an array of variables,
1697        which requires that when all variables are assigned values, the resulting
1698        array equals one of the  tuples in `tuple_list`.
1699
1700        Args:
1701          variables: A list of variables.
1702          tuples_list: A list of admissible tuples. Each tuple must have the same
1703            length as the variables, and the ith value of a tuple corresponds to the
1704            ith variable.
1705
1706        Returns:
1707          An instance of the `Constraint` class.
1708
1709        Raises:
1710          TypeError: If a tuple does not have the same size as the list of
1711              variables.
1712          ValueError: If the array of variables is empty.
1713        """
1714
1715        if not variables:
1716            raise ValueError(
1717                "add_allowed_assignments expects a non-empty variables array"
1718            )
1719
1720        ct: Constraint = Constraint(self)
1721        model_ct = self.__model.constraints[ct.index]
1722        model_ct.table.vars.extend([self.get_or_make_index(x) for x in variables])
1723        arity: int = len(variables)
1724        for t in tuples_list:
1725            if len(t) != arity:
1726                raise TypeError("Tuple " + str(t) + " has the wrong arity")
1727
1728        # duck-typing (no explicit type checks here)
1729        try:
1730            model_ct.table.values.extend(a for b in tuples_list for a in b)
1731        except ValueError as ex:
1732            raise TypeError(f"add_xxx_assignment: Not an integer or does not fit in an int64_t: {ex.args}") from ex
1733
1734        return ct

Adds AllowedAssignments(variables, tuples_list).

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

Arguments:
  • variables: A list of variables.
  • tuples_list: A list of admissible tuples. Each tuple must have the same length as the variables, and the ith value of a tuple corresponds to the ith variable.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: If a tuple does not have the same size as the list of variables.
  • ValueError: If the array of variables is empty.
def AddForbiddenAssignments( self, variables: Sequence[Union[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:
1736    def add_forbidden_assignments(
1737        self,
1738        variables: Sequence[VariableT],
1739        tuples_list: Iterable[Sequence[IntegralT]],
1740    ) -> Constraint:
1741        """Adds add_forbidden_assignments(variables, [tuples_list]).
1742
1743        A ForbiddenAssignments constraint is a constraint on an array of variables
1744        where the list of impossible combinations is provided in the tuples list.
1745
1746        Args:
1747          variables: A list of variables.
1748          tuples_list: A list of forbidden tuples. Each tuple must have the same
1749            length as the variables, and the *i*th value of a tuple corresponds to
1750            the *i*th variable.
1751
1752        Returns:
1753          An instance of the `Constraint` class.
1754
1755        Raises:
1756          TypeError: If a tuple does not have the same size as the list of
1757                     variables.
1758          ValueError: If the array of variables is empty.
1759        """
1760
1761        if not variables:
1762            raise ValueError(
1763                "add_forbidden_assignments expects a non-empty variables array"
1764            )
1765
1766        index = len(self.__model.constraints)
1767        ct: Constraint = self.add_allowed_assignments(variables, tuples_list)
1768        self.__model.constraints[index].table.negated = True
1769        return ct

Adds add_forbidden_assignments(variables, [tuples_list]).

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

Arguments:
  • variables: A list of variables.
  • tuples_list: A list of forbidden tuples. Each tuple must have the same length as the variables, and the ith value of a tuple corresponds to the ith variable.
Returns:

An instance of the Constraint class.

Raises:
  • TypeError: If a tuple does not have the same size as the list of variables.
  • ValueError: If the array of variables is empty.
def AddAutomaton( self, transition_variables: Sequence[Union[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:
1771    def add_automaton(
1772        self,
1773        transition_variables: Sequence[VariableT],
1774        starting_state: IntegralT,
1775        final_states: Sequence[IntegralT],
1776        transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]],
1777    ) -> Constraint:
1778        """Adds an automaton constraint.
1779
1780        An automaton constraint takes a list of variables (of size *n*), an initial
1781        state, a set of final states, and a set of transitions. A transition is a
1782        triplet (*tail*, *transition*, *head*), where *tail* and *head* are states,
1783        and *transition* is the label of an arc from *head* to *tail*,
1784        corresponding to the value of one variable in the list of variables.
1785
1786        This automaton will be unrolled into a flow with *n* + 1 phases. Each phase
1787        contains the possible states of the automaton. The first state contains the
1788        initial state. The last phase contains the final states.
1789
1790        Between two consecutive phases *i* and *i* + 1, the automaton creates a set
1791        of arcs. For each transition (*tail*, *transition*, *head*), it will add
1792        an arc from the state *tail* of phase *i* and the state *head* of phase
1793        *i* + 1. This arc is labeled by the value *transition* of the variables
1794        `variables[i]`. That is, this arc can only be selected if `variables[i]`
1795        is assigned the value *transition*.
1796
1797        A feasible solution of this constraint is an assignment of variables such
1798        that, starting from the initial state in phase 0, there is a path labeled by
1799        the values of the variables that ends in one of the final states in the
1800        final phase.
1801
1802        Args:
1803          transition_variables: A non-empty list of variables whose values
1804            correspond to the labels of the arcs traversed by the automaton.
1805          starting_state: The initial state of the automaton.
1806          final_states: A non-empty list of admissible final states.
1807          transition_triples: A list of transitions for the automaton, in the
1808            following format (current_state, variable_value, next_state).
1809
1810        Returns:
1811          An instance of the `Constraint` class.
1812
1813        Raises:
1814          ValueError: if `transition_variables`, `final_states`, or
1815            `transition_triples` are empty.
1816        """
1817
1818        if not transition_variables:
1819            raise ValueError(
1820                "add_automaton expects a non-empty transition_variables array"
1821            )
1822        if not final_states:
1823            raise ValueError("add_automaton expects some final states")
1824
1825        if not transition_triples:
1826            raise ValueError("add_automaton expects some transition triples")
1827
1828        ct = Constraint(self)
1829        model_ct = self.__model.constraints[ct.index]
1830        model_ct.automaton.vars.extend(
1831            [self.get_or_make_index(x) for x in transition_variables]
1832        )
1833        starting_state = cmh.assert_is_int64(starting_state)
1834        model_ct.automaton.starting_state = starting_state
1835        for v in final_states:
1836            v = cmh.assert_is_int64(v)
1837            model_ct.automaton.final_states.append(v)
1838        for t in transition_triples:
1839            if len(t) != 3:
1840                raise TypeError("Tuple " + str(t) + " has the wrong arity (!= 3)")
1841            tail = cmh.assert_is_int64(t[0])
1842            label = cmh.assert_is_int64(t[1])
1843            head = cmh.assert_is_int64(t[2])
1844            model_ct.automaton.transition_tail.append(tail)
1845            model_ct.automaton.transition_label.append(label)
1846            model_ct.automaton.transition_head.append(head)
1847        return ct

Adds an automaton constraint.

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

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

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: if transition_variables, final_states, 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:
1849    def add_inverse(
1850        self,
1851        variables: Sequence[VariableT],
1852        inverse_variables: Sequence[VariableT],
1853    ) -> Constraint:
1854        """Adds Inverse(variables, inverse_variables).
1855
1856        An inverse constraint enforces that if `variables[i]` is assigned a value
1857        `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa.
1858
1859        Args:
1860          variables: An array of integer variables.
1861          inverse_variables: An array of integer variables.
1862
1863        Returns:
1864          An instance of the `Constraint` class.
1865
1866        Raises:
1867          TypeError: if variables and inverse_variables have different lengths, or
1868              if they are empty.
1869        """
1870
1871        if not variables or not inverse_variables:
1872            raise TypeError("The Inverse constraint does not accept empty arrays")
1873        if len(variables) != len(inverse_variables):
1874            raise TypeError(
1875                "In the inverse constraint, the two array variables and"
1876                " inverse_variables must have the same length."
1877            )
1878        ct = Constraint(self)
1879        model_ct = self.__model.constraints[ct.index]
1880        model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables])
1881        model_ct.inverse.f_inverse.extend(
1882            [self.get_or_make_index(x) for x in inverse_variables]
1883        )
1884        return ct

Adds Inverse(variables, inverse_variables).

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

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

An instance of the Constraint class.

Raises:
  • TypeError: if variables and inverse_variables have different lengths, or if they are empty.
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:
1886    def add_reservoir_constraint(
1887        self,
1888        times: Iterable[LinearExprT],
1889        level_changes: Iterable[LinearExprT],
1890        min_level: int,
1891        max_level: int,
1892    ) -> Constraint:
1893        """Adds Reservoir(times, level_changes, min_level, max_level).
1894
1895        Maintains a reservoir level within bounds. The water level starts at 0, and
1896        at any time, it must be between min_level and max_level.
1897
1898        If the affine expression `times[i]` is assigned a value t, then the current
1899        level changes by `level_changes[i]`, which is constant, at time t.
1900
1901         Note that min level must be <= 0, and the max level must be >= 0. Please
1902         use fixed level_changes to simulate initial state.
1903
1904         Therefore, at any time:
1905             sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
1906
1907        Args:
1908          times: A list of 1-var affine expressions (a * x + b) which specify the
1909            time of the filling or emptying the reservoir.
1910          level_changes: A list of integer values that specifies the amount of the
1911            emptying or filling. Currently, variable demands are not supported.
1912          min_level: At any time, the level of the reservoir must be greater or
1913            equal than the min level.
1914          max_level: At any time, the level of the reservoir must be less or equal
1915            than the max level.
1916
1917        Returns:
1918          An instance of the `Constraint` class.
1919
1920        Raises:
1921          ValueError: if max_level < min_level.
1922
1923          ValueError: if max_level < 0.
1924
1925          ValueError: if min_level > 0
1926        """
1927
1928        if max_level < min_level:
1929            raise ValueError("Reservoir constraint must have a max_level >= min_level")
1930
1931        if max_level < 0:
1932            raise ValueError("Reservoir constraint must have a max_level >= 0")
1933
1934        if min_level > 0:
1935            raise ValueError("Reservoir constraint must have a min_level <= 0")
1936
1937        ct = Constraint(self)
1938        model_ct = self.__model.constraints[ct.index]
1939        model_ct.reservoir.time_exprs.extend(
1940            [self.parse_linear_expression(x) for x in times]
1941        )
1942        model_ct.reservoir.level_changes.extend(
1943            [self.parse_linear_expression(x) for x in level_changes]
1944        )
1945        model_ct.reservoir.min_level = min_level
1946        model_ct.reservoir.max_level = max_level
1947        return ct

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

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

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

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: if max_level < min_level.
  • ValueError: if max_level < 0.
  • ValueError: if min_level > 0
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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]], min_level: int, max_level: int) -> Constraint:
1949    def add_reservoir_constraint_with_active(
1950        self,
1951        times: Iterable[LinearExprT],
1952        level_changes: Iterable[LinearExprT],
1953        actives: Iterable[LiteralT],
1954        min_level: int,
1955        max_level: int,
1956    ) -> Constraint:
1957        """Adds Reservoir(times, level_changes, actives, min_level, max_level).
1958
1959        Maintains a reservoir level within bounds. The water level starts at 0, and
1960        at any time, it must be between min_level and max_level.
1961
1962        If the variable `times[i]` is assigned a value t, and `actives[i]` is
1963        `True`, then the current level changes by `level_changes[i]`, which is
1964        constant,
1965        at time t.
1966
1967         Note that min level must be <= 0, and the max level must be >= 0. Please
1968         use fixed level_changes to simulate initial state.
1969
1970         Therefore, at any time:
1971             sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level,
1972             max_level]
1973
1974
1975        The array of boolean variables 'actives', if defined, indicates which
1976        actions are actually performed.
1977
1978        Args:
1979          times: A list of 1-var affine expressions (a * x + b) which specify the
1980            time of the filling or emptying the reservoir.
1981          level_changes: A list of integer values that specifies the amount of the
1982            emptying or filling. Currently, variable demands are not supported.
1983          actives: a list of boolean variables. They indicates if the
1984            emptying/refilling events actually take place.
1985          min_level: At any time, the level of the reservoir must be greater or
1986            equal than the min level.
1987          max_level: At any time, the level of the reservoir must be less or equal
1988            than the max level.
1989
1990        Returns:
1991          An instance of the `Constraint` class.
1992
1993        Raises:
1994          ValueError: if max_level < min_level.
1995
1996          ValueError: if max_level < 0.
1997
1998          ValueError: if min_level > 0
1999        """
2000
2001        if max_level < min_level:
2002            raise ValueError("Reservoir constraint must have a max_level >= min_level")
2003
2004        if max_level < 0:
2005            raise ValueError("Reservoir constraint must have a max_level >= 0")
2006
2007        if min_level > 0:
2008            raise ValueError("Reservoir constraint must have a min_level <= 0")
2009
2010        ct = Constraint(self)
2011        model_ct = self.__model.constraints[ct.index]
2012        model_ct.reservoir.time_exprs.extend(
2013            [self.parse_linear_expression(x) for x in times]
2014        )
2015        model_ct.reservoir.level_changes.extend(
2016            [self.parse_linear_expression(x) for x in level_changes]
2017        )
2018        model_ct.reservoir.active_literals.extend(
2019            [self.get_or_make_boolean_index(x) for x in actives]
2020        )
2021        model_ct.reservoir.min_level = min_level
2022        model_ct.reservoir.max_level = max_level
2023        return ct

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

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

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

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

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

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

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

An instance of the Constraint class.

Raises:
  • ValueError: if max_level < min_level.
  • ValueError: if max_level < 0.
  • ValueError: if min_level > 0
def AddImplication( self, a: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], b: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> Constraint:
2049    def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint:
2050        """Adds `a => b` (`a` implies `b`)."""
2051        ct = Constraint(self)
2052        model_ct = self.__model.constraints[ct.index]
2053        model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b))
2054        model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a))
2055        return ct

Adds a => b (a implies b).

def AddBoolOr(self, *literals):
2063    def add_bool_or(self, *literals):
2064        """Adds `Or(literals) == true`: sum(literals) >= 1."""
2065        ct = Constraint(self)
2066        model_ct = self.__model.constraints[ct.index]
2067        model_ct.bool_or.literals.extend(
2068            [
2069                self.get_or_make_boolean_index(x)
2070                for x in expand_generator_or_tuple(literals)
2071            ]
2072        )
2073        return ct

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

def AddAtLeastOne(self, *literals):
2081    def add_at_least_one(self, *literals):
2082        """Same as `add_bool_or`: `sum(literals) >= 1`."""
2083        return self.add_bool_or(*literals)

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

def AddAtMostOne(self, *literals):
2091    def add_at_most_one(self, *literals):
2092        """Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
2093        ct = Constraint(self)
2094        model_ct = self.__model.constraints[ct.index]
2095        model_ct.at_most_one.literals.extend(
2096            [
2097                self.get_or_make_boolean_index(x)
2098                for x in expand_generator_or_tuple(literals)
2099            ]
2100        )
2101        return ct

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

def AddExactlyOne(self, *literals):
2109    def add_exactly_one(self, *literals):
2110        """Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
2111        ct = Constraint(self)
2112        model_ct = self.__model.constraints[ct.index]
2113        model_ct.exactly_one.literals.extend(
2114            [
2115                self.get_or_make_boolean_index(x)
2116                for x in expand_generator_or_tuple(literals)
2117            ]
2118        )
2119        return ct

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

def AddBoolAnd(self, *literals):
2127    def add_bool_and(self, *literals):
2128        """Adds `And(literals) == true`."""
2129        ct = Constraint(self)
2130        model_ct = self.__model.constraints[ct.index]
2131        model_ct.bool_and.literals.extend(
2132            [
2133                self.get_or_make_boolean_index(x)
2134                for x in expand_generator_or_tuple(literals)
2135            ]
2136        )
2137        return ct

Adds And(literals) == true.

def AddBoolXOr(self, *literals):
2145    def add_bool_xor(self, *literals):
2146        """Adds `XOr(literals) == true`.
2147
2148        In contrast to add_bool_or and add_bool_and, it does not support
2149            .only_enforce_if().
2150
2151        Args:
2152          *literals: the list of literals in the constraint.
2153
2154        Returns:
2155          An `Constraint` object.
2156        """
2157        ct = Constraint(self)
2158        model_ct = self.__model.constraints[ct.index]
2159        model_ct.bool_xor.literals.extend(
2160            [
2161                self.get_or_make_boolean_index(x)
2162                for x in expand_generator_or_tuple(literals)
2163            ]
2164        )
2165        return ct

Adds XOr(literals) == true.

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

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

An Constraint object.

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:
2167    def add_min_equality(
2168        self, target: LinearExprT, exprs: Iterable[LinearExprT]
2169    ) -> Constraint:
2170        """Adds `target == Min(exprs)`."""
2171        ct = Constraint(self)
2172        model_ct = self.__model.constraints[ct.index]
2173        model_ct.lin_max.exprs.extend(
2174            [self.parse_linear_expression(x, True) for x in exprs]
2175        )
2176        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True))
2177        return ct

Adds target == Min(exprs).

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:
2179    def add_max_equality(
2180        self, target: LinearExprT, exprs: Iterable[LinearExprT]
2181    ) -> Constraint:
2182        """Adds `target == Max(exprs)`."""
2183        ct = Constraint(self)
2184        model_ct = self.__model.constraints[ct.index]
2185        model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs])
2186        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
2187        return ct

Adds target == Max(exprs).

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:
2189    def add_division_equality(
2190        self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
2191    ) -> Constraint:
2192        """Adds `target == num // denom` (integer division rounded towards 0)."""
2193        ct = Constraint(self)
2194        model_ct = self.__model.constraints[ct.index]
2195        model_ct.int_div.exprs.append(self.parse_linear_expression(num))
2196        model_ct.int_div.exprs.append(self.parse_linear_expression(denom))
2197        model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target))
2198        return ct

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

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:
2200    def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
2201        """Adds `target == Abs(expr)`."""
2202        ct = Constraint(self)
2203        model_ct = self.__model.constraints[ct.index]
2204        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr))
2205        model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True))
2206        model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
2207        return ct

Adds target == Abs(expr).

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:
2209    def add_modulo_equality(
2210        self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
2211    ) -> Constraint:
2212        """Adds `target = expr % mod`.
2213
2214        It uses the C convention, that is the result is the remainder of the
2215        integral division rounded towards 0.
2216
2217            For example:
2218            * 10 % 3 = 1
2219            * -10 % 3 = -1
2220            * 10 % -3 = 1
2221            * -10 % -3 = -1
2222
2223        Args:
2224          target: the target expression.
2225          expr: the expression to compute the modulo of.
2226          mod: the modulus expression.
2227
2228        Returns:
2229          A `Constraint` object.
2230        """
2231        ct = Constraint(self)
2232        model_ct = self.__model.constraints[ct.index]
2233        model_ct.int_mod.exprs.append(self.parse_linear_expression(expr))
2234        model_ct.int_mod.exprs.append(self.parse_linear_expression(mod))
2235        model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target))
2236        return ct

Adds target = expr % mod.

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

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

A Constraint object.

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:
2238    def add_multiplication_equality(
2239        self,
2240        target: LinearExprT,
2241        *expressions: Union[Iterable[LinearExprT], LinearExprT],
2242    ) -> Constraint:
2243        """Adds `target == expressions[0] * .. * expressions[n]`."""
2244        ct = Constraint(self)
2245        model_ct = self.__model.constraints[ct.index]
2246        model_ct.int_prod.exprs.extend(
2247            [
2248                self.parse_linear_expression(expr)
2249                for expr in expand_generator_or_tuple(expressions)
2250            ]
2251        )
2252        model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target))
2253        return ct

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

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:
2257    def new_interval_var(
2258        self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str
2259    ) -> IntervalVar:
2260        """Creates an interval variable from start, size, and end.
2261
2262        An interval variable is a constraint, that is itself used in other
2263        constraints like NoOverlap.
2264
2265        Internally, it ensures that `start + size == end`.
2266
2267        Args:
2268          start: The start of the interval. It must be of the form a * var + b.
2269          size: The size of the interval. It must be of the form a * var + b.
2270          end: The end of the interval. It must be of the form a * var + b.
2271          name: The name of the interval variable.
2272
2273        Returns:
2274          An `IntervalVar` object.
2275        """
2276
2277        start_expr = self.parse_linear_expression(start)
2278        size_expr = self.parse_linear_expression(size)
2279        end_expr = self.parse_linear_expression(end)
2280        if len(start_expr.vars) > 1:
2281            raise TypeError(
2282                "cp_model.new_interval_var: start must be 1-var affine or constant."
2283            )
2284        if len(size_expr.vars) > 1:
2285            raise TypeError(
2286                "cp_model.new_interval_var: size must be 1-var affine or constant."
2287            )
2288        if len(end_expr.vars) > 1:
2289            raise TypeError(
2290                "cp_model.new_interval_var: end must be 1-var affine or constant."
2291            )
2292        return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)

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

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

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

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

An IntervalVar object.

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:
2294    def new_interval_var_series(
2295        self,
2296        name: str,
2297        index: pd.Index,
2298        starts: Union[LinearExprT, pd.Series],
2299        sizes: Union[LinearExprT, pd.Series],
2300        ends: Union[LinearExprT, pd.Series],
2301    ) -> pd.Series:
2302        """Creates a series of interval variables with the given name.
2303
2304        Args:
2305          name (str): Required. The name of the variable set.
2306          index (pd.Index): Required. The index to use for the variable set.
2307          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2308            set. If a `pd.Series` is passed in, it will be based on the
2309            corresponding values of the pd.Series.
2310          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
2311            set. If a `pd.Series` is passed in, it will be based on the
2312            corresponding values of the pd.Series.
2313          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
2314            set. If a `pd.Series` is passed in, it will be based on the
2315            corresponding values of the pd.Series.
2316
2317        Returns:
2318          pd.Series: The interval variable set indexed by its corresponding
2319          dimensions.
2320
2321        Raises:
2322          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2323          ValueError: if the `name` is not a valid identifier or already exists.
2324          ValueError: if the all the indexes do not match.
2325        """
2326        if not isinstance(index, pd.Index):
2327            raise TypeError("Non-index object is used as index")
2328        if not name.isidentifier():
2329            raise ValueError("name={} is not a valid identifier".format(name))
2330
2331        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2332        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
2333        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
2334        interval_array = []
2335        for i in index:
2336            interval_array.append(
2337                self.new_interval_var(
2338                    start=starts[i],
2339                    size=sizes[i],
2340                    end=ends[i],
2341                    name=f"{name}[{i}]",
2342                )
2343            )
2344        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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:
2346    def new_fixed_size_interval_var(
2347        self, start: LinearExprT, size: IntegralT, name: str
2348    ) -> IntervalVar:
2349        """Creates an interval variable from start, and a fixed size.
2350
2351        An interval variable is a constraint, that is itself used in other
2352        constraints like NoOverlap.
2353
2354        Args:
2355          start: The start of the interval. It must be of the form a * var + b.
2356          size: The size of the interval. It must be an integer value.
2357          name: The name of the interval variable.
2358
2359        Returns:
2360          An `IntervalVar` object.
2361        """
2362        size = cmh.assert_is_int64(size)
2363        start_expr = self.parse_linear_expression(start)
2364        size_expr = self.parse_linear_expression(size)
2365        end_expr = self.parse_linear_expression(start + size)
2366        if len(start_expr.vars) > 1:
2367            raise TypeError(
2368                "cp_model.new_interval_var: start must be affine or constant."
2369            )
2370        return IntervalVar(self.__model, start_expr, size_expr, end_expr, None, name)

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

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

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

An IntervalVar object.

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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], name: str) -> IntervalVar:
2418    def new_optional_interval_var(
2419        self,
2420        start: LinearExprT,
2421        size: LinearExprT,
2422        end: LinearExprT,
2423        is_present: LiteralT,
2424        name: str,
2425    ) -> IntervalVar:
2426        """Creates an optional interval var from start, size, end, and is_present.
2427
2428        An optional interval variable is a constraint, that is itself used in other
2429        constraints like NoOverlap. This constraint is protected by a presence
2430        literal that indicates if it is active or not.
2431
2432        Internally, it ensures that `is_present` implies `start + size ==
2433        end`.
2434
2435        Args:
2436          start: The start of the interval. It must be of the form a * var + b.
2437          size: The size of the interval. It must be of the form a * var + b.
2438          end: The end of the interval. It must be of the form a * var + b.
2439          is_present: A literal that indicates if the interval is active or not. A
2440            inactive interval is simply ignored by all constraints.
2441          name: The name of the interval variable.
2442
2443        Returns:
2444          An `IntervalVar` object.
2445        """
2446
2447        # Creates the IntervalConstraintProto object.
2448        is_present_index = self.get_or_make_boolean_index(is_present)
2449        start_expr = self.parse_linear_expression(start)
2450        size_expr = self.parse_linear_expression(size)
2451        end_expr = self.parse_linear_expression(end)
2452        if len(start_expr.vars) > 1:
2453            raise TypeError(
2454                "cp_model.new_interval_var: start must be affine or constant."
2455            )
2456        if len(size_expr.vars) > 1:
2457            raise TypeError(
2458                "cp_model.new_interval_var: size must be affine or constant."
2459            )
2460        if len(end_expr.vars) > 1:
2461            raise TypeError(
2462                "cp_model.new_interval_var: end must be affine or constant."
2463            )
2464        return IntervalVar(
2465            self.__model, start_expr, size_expr, end_expr, is_present_index, name
2466        )

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

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

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

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

An IntervalVar object.

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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool, pandas.core.series.Series]) -> pandas.core.series.Series:
2468    def new_optional_interval_var_series(
2469        self,
2470        name: str,
2471        index: pd.Index,
2472        starts: Union[LinearExprT, pd.Series],
2473        sizes: Union[LinearExprT, pd.Series],
2474        ends: Union[LinearExprT, pd.Series],
2475        are_present: Union[LiteralT, pd.Series],
2476    ) -> pd.Series:
2477        """Creates a series of interval variables with the given name.
2478
2479        Args:
2480          name (str): Required. The name of the variable set.
2481          index (pd.Index): Required. The index to use for the variable set.
2482          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2483            set. If a `pd.Series` is passed in, it will be based on the
2484            corresponding values of the pd.Series.
2485          sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
2486            set. If a `pd.Series` is passed in, it will be based on the
2487            corresponding values of the pd.Series.
2488          ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
2489            set. If a `pd.Series` is passed in, it will be based on the
2490            corresponding values of the pd.Series.
2491          are_present (Union[LiteralT, pd.Series]): The performed literal of each
2492            interval in the set. If a `pd.Series` is passed in, it will be based on
2493            the corresponding values of the pd.Series.
2494
2495        Returns:
2496          pd.Series: The interval variable set indexed by its corresponding
2497          dimensions.
2498
2499        Raises:
2500          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2501          ValueError: if the `name` is not a valid identifier or already exists.
2502          ValueError: if the all the indexes do not match.
2503        """
2504        if not isinstance(index, pd.Index):
2505            raise TypeError("Non-index object is used as index")
2506        if not name.isidentifier():
2507            raise ValueError("name={} is not a valid identifier".format(name))
2508
2509        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2510        sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index)
2511        ends = _convert_to_linear_expr_series_and_validate_index(ends, index)
2512        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
2513
2514        interval_array = []
2515        for i in index:
2516            interval_array.append(
2517                self.new_optional_interval_var(
2518                    start=starts[i],
2519                    size=sizes[i],
2520                    end=ends[i],
2521                    is_present=are_present[i],
2522                    name=f"{name}[{i}]",
2523                )
2524            )
2525        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], name: str) -> IntervalVar:
2527    def new_optional_fixed_size_interval_var(
2528        self,
2529        start: LinearExprT,
2530        size: IntegralT,
2531        is_present: LiteralT,
2532        name: str,
2533    ) -> IntervalVar:
2534        """Creates an interval variable from start, and a fixed size.
2535
2536        An interval variable is a constraint, that is itself used in other
2537        constraints like NoOverlap.
2538
2539        Args:
2540          start: The start of the interval. It must be of the form a * var + b.
2541          size: The size of the interval. It must be an integer value.
2542          is_present: A literal that indicates if the interval is active or not. A
2543            inactive interval is simply ignored by all constraints.
2544          name: The name of the interval variable.
2545
2546        Returns:
2547          An `IntervalVar` object.
2548        """
2549        size = cmh.assert_is_int64(size)
2550        start_expr = self.parse_linear_expression(start)
2551        size_expr = self.parse_linear_expression(size)
2552        end_expr = self.parse_linear_expression(start + size)
2553        if len(start_expr.vars) > 1:
2554            raise TypeError(
2555                "cp_model.new_interval_var: start must be affine or constant."
2556            )
2557        is_present_index = self.get_or_make_boolean_index(is_present)
2558        return IntervalVar(
2559            self.__model,
2560            start_expr,
2561            size_expr,
2562            end_expr,
2563            is_present_index,
2564            name,
2565        )

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

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

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

An IntervalVar object.

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[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool, pandas.core.series.Series]) -> pandas.core.series.Series:
2567    def new_optional_fixed_size_interval_var_series(
2568        self,
2569        name: str,
2570        index: pd.Index,
2571        starts: Union[LinearExprT, pd.Series],
2572        sizes: Union[IntegralT, pd.Series],
2573        are_present: Union[LiteralT, pd.Series],
2574    ) -> pd.Series:
2575        """Creates a series of interval variables with the given name.
2576
2577        Args:
2578          name (str): Required. The name of the variable set.
2579          index (pd.Index): Required. The index to use for the variable set.
2580          starts (Union[LinearExprT, pd.Series]): The start of each interval in the
2581            set. If a `pd.Series` is passed in, it will be based on the
2582            corresponding values of the pd.Series.
2583          sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
2584            the set. If a `pd.Series` is passed in, it will be based on the
2585            corresponding values of the pd.Series.
2586          are_present (Union[LiteralT, pd.Series]): The performed literal of each
2587            interval in the set. If a `pd.Series` is passed in, it will be based on
2588            the corresponding values of the pd.Series.
2589
2590        Returns:
2591          pd.Series: The interval variable set indexed by its corresponding
2592          dimensions.
2593
2594        Raises:
2595          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
2596          ValueError: if the `name` is not a valid identifier or already exists.
2597          ValueError: if the all the indexes do not match.
2598        """
2599        if not isinstance(index, pd.Index):
2600            raise TypeError("Non-index object is used as index")
2601        if not name.isidentifier():
2602            raise ValueError("name={} is not a valid identifier".format(name))
2603
2604        starts = _convert_to_linear_expr_series_and_validate_index(starts, index)
2605        sizes = _convert_to_integral_series_and_validate_index(sizes, index)
2606        are_present = _convert_to_literal_series_and_validate_index(are_present, index)
2607        interval_array = []
2608        for i in index:
2609            interval_array.append(
2610                self.new_optional_fixed_size_interval_var(
2611                    start=starts[i],
2612                    size=sizes[i],
2613                    is_present=are_present[i],
2614                    name=f"{name}[{i}]",
2615                )
2616            )
2617        return pd.Series(index=index, data=interval_array)

Creates a series of interval variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. 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:
2619    def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint:
2620        """Adds NoOverlap(interval_vars).
2621
2622        A NoOverlap constraint ensures that all present intervals do not overlap
2623        in time.
2624
2625        Args:
2626          interval_vars: The list of interval variables to constrain.
2627
2628        Returns:
2629          An instance of the `Constraint` class.
2630        """
2631        ct = Constraint(self)
2632        model_ct = self.__model.constraints[ct.index]
2633        model_ct.no_overlap.intervals.extend(
2634            [self.get_interval_index(x) for x in interval_vars]
2635        )
2636        return ct

Adds NoOverlap(interval_vars).

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

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

An instance of the Constraint class.

def AddNoOverlap2D( self, x_intervals: Iterable[IntervalVar], y_intervals: Iterable[IntervalVar]) -> Constraint:
2638    def add_no_overlap_2d(
2639        self,
2640        x_intervals: Iterable[IntervalVar],
2641        y_intervals: Iterable[IntervalVar],
2642    ) -> Constraint:
2643        """Adds NoOverlap2D(x_intervals, y_intervals).
2644
2645        A NoOverlap2D constraint ensures that all present rectangles do not overlap
2646        on a plane. Each rectangle is aligned with the X and Y axis, and is defined
2647        by two intervals which represent its projection onto the X and Y axis.
2648
2649        Furthermore, one box is optional if at least one of the x or y interval is
2650        optional.
2651
2652        Args:
2653          x_intervals: The X coordinates of the rectangles.
2654          y_intervals: The Y coordinates of the rectangles.
2655
2656        Returns:
2657          An instance of the `Constraint` class.
2658        """
2659        ct = Constraint(self)
2660        model_ct = self.__model.constraints[ct.index]
2661        model_ct.no_overlap_2d.x_intervals.extend(
2662            [self.get_interval_index(x) for x in x_intervals]
2663        )
2664        model_ct.no_overlap_2d.y_intervals.extend(
2665            [self.get_interval_index(x) for x in y_intervals]
2666        )
2667        return ct

Adds NoOverlap2D(x_intervals, y_intervals).

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

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

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

An instance of the Constraint class.

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:
2669    def add_cumulative(
2670        self,
2671        intervals: Iterable[IntervalVar],
2672        demands: Iterable[LinearExprT],
2673        capacity: LinearExprT,
2674    ) -> Constraint:
2675        """Adds Cumulative(intervals, demands, capacity).
2676
2677        This constraint enforces that:
2678
2679            for all t:
2680              sum(demands[i]
2681                if (start(intervals[i]) <= t < end(intervals[i])) and
2682                (intervals[i] is present)) <= capacity
2683
2684        Args:
2685          intervals: The list of intervals.
2686          demands: The list of demands for each interval. Each demand must be >= 0.
2687            Each demand can be a 1-var affine expression (a * x + b).
2688          capacity: The maximum capacity of the cumulative constraint. It can be a
2689            1-var affine expression (a * x + b).
2690
2691        Returns:
2692          An instance of the `Constraint` class.
2693        """
2694        cumulative = Constraint(self)
2695        model_ct = self.__model.constraints[cumulative.index]
2696        model_ct.cumulative.intervals.extend(
2697            [self.get_interval_index(x) for x in intervals]
2698        )
2699        for d in demands:
2700            model_ct.cumulative.demands.append(self.parse_linear_expression(d))
2701        model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity))
2702        return cumulative

Adds Cumulative(intervals, demands, capacity).

This constraint enforces that:

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

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

An instance of the Constraint class.

def Clone(self) -> CpModel:
2705    def clone(self) -> "CpModel":
2706        """Reset the model, and creates a new one from a CpModelProto instance."""
2707        clone = CpModel()
2708        clone.proto.CopyFrom(self.proto)
2709        clone.rebuild_constant_map()
2710        return clone

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

def GetBoolVarFromProtoIndex(self, index: int) -> IntVar:
2718    def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2719        """Returns an already created Boolean variable from its index."""
2720        if index < 0 or index >= len(self.__model.variables):
2721            raise ValueError(
2722                f"get_bool_var_from_proto_index: out of bound index {index}"
2723            )
2724        var = self.__model.variables[index]
2725        if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1:
2726            raise ValueError(
2727                f"get_bool_var_from_proto_index: index {index} does not reference"
2728                + " a Boolean variable"
2729            )
2730
2731        return IntVar(self.__model, index, None)

Returns an already created Boolean variable from its index.

def GetIntVarFromProtoIndex(self, index: int) -> IntVar:
2733    def get_int_var_from_proto_index(self, index: int) -> IntVar:
2734        """Returns an already created integer variable from its index."""
2735        if index < 0 or index >= len(self.__model.variables):
2736            raise ValueError(
2737                f"get_int_var_from_proto_index: out of bound index {index}"
2738            )
2739        return IntVar(self.__model, index, None)

Returns an already created integer variable from its index.

def GetIntervalVarFromProtoIndex(self, index: int) -> IntervalVar:
2741    def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2742        """Returns an already created interval variable from its index."""
2743        if index < 0 or index >= len(self.__model.constraints):
2744            raise ValueError(
2745                f"get_interval_var_from_proto_index: out of bound index {index}"
2746            )
2747        ct = self.__model.constraints[index]
2748        if not ct.HasField("interval"):
2749            raise ValueError(
2750                f"get_interval_var_from_proto_index: index {index} does not"
2751                " reference an" + " interval variable"
2752            )
2753
2754        return IntervalVar(self.__model, index, None, None, None, None)

Returns an already created interval variable from its index.

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

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

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

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

def HasObjective(self) -> bool:
2899    def has_objective(self) -> bool:
2900        return self.__model.HasField("objective") or self.__model.HasField(
2901            "floating_point_objective"
2902        )
def ClearObjective(self):
2904    def clear_objective(self):
2905        self.__model.ClearField("objective")
2906        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:
2908    def add_decision_strategy(
2909        self,
2910        variables: Sequence[IntVar],
2911        var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy,
2912        domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy,
2913    ) -> None:
2914        """Adds a search strategy to the model.
2915
2916        Args:
2917          variables: a list of variables this strategy will assign.
2918          var_strategy: heuristic to choose the next variable to assign.
2919          domain_strategy: heuristic to reduce the domain of the selected variable.
2920            Currently, this is advanced code: the union of all strategies added to
2921            the model must be complete, i.e. instantiates all variables. Otherwise,
2922            solve() will fail.
2923        """
2924
2925        strategy: cp_model_pb2.DecisionStrategyProto = (
2926            self.__model.search_strategy.add()
2927        )
2928        for v in variables:
2929            expr = strategy.exprs.add()
2930            if v.index >= 0:
2931                expr.vars.append(v.index)
2932                expr.coeffs.append(1)
2933            else:
2934                expr.vars.append(self.negated(v.index))
2935                expr.coeffs.append(-1)
2936                expr.offset = 1
2937
2938        strategy.variable_selection_strategy = var_strategy
2939        strategy.domain_reduction_strategy = domain_strategy

Adds a search strategy to the model.

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

Returns a string containing some model statistics.

def Validate(self) -> str:
2945    def validate(self) -> str:
2946        """Returns a string indicating that the model is invalid."""
2947        return swig_helper.CpSatHelper.validate_model(self.__model)

Returns a string indicating that the model is invalid.

def ExportToFile(self, file: str) -> bool:
2949    def export_to_file(self, file: str) -> bool:
2950        """Write the model as a protocol buffer to 'file'.
2951
2952        Args:
2953          file: file to write the model to. If the filename ends with 'txt', the
2954            model will be written as a text file, otherwise, the binary format will
2955            be used.
2956
2957        Returns:
2958          True if the model was correctly written.
2959        """
2960        return swig_helper.CpSatHelper.write_model_to_file(self.__model, file)

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

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

True if the model was correctly written.

def AddHint(self, var: IntVar, value: int) -> None:
2962    def add_hint(self, var: IntVar, value: int) -> None:
2963        """Adds 'var == value' as a hint to the solver."""
2964        self.__model.solution_hint.vars.append(self.get_or_make_index(var))
2965        self.__model.solution_hint.values.append(value)

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

def ClearHints(self):
2967    def clear_hints(self):
2968        """Removes any solution hint from the model."""
2969        self.__model.ClearField("solution_hint")

Removes any solution hint from the model.

def AddAssumption( self, lit: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> None:
2971    def add_assumption(self, lit: LiteralT) -> None:
2972        """Adds the literal to the model as assumptions."""
2973        self.__model.assumptions.append(self.get_or_make_boolean_index(lit))

Adds the literal to the model as assumptions.

def AddAssumptions( self, literals: Iterable[Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]]) -> None:
2975    def add_assumptions(self, literals: Iterable[LiteralT]) -> None:
2976        """Adds the literals to the model as assumptions."""
2977        for lit in literals:
2978            self.add_assumption(lit)

Adds the literals to the model as assumptions.

def ClearAssumptions(self) -> None:
2980    def clear_assumptions(self) -> None:
2981        """Removes all assumptions from the model."""
2982        self.__model.ClearField("assumptions")

Removes all assumptions from the model.

def expand_generator_or_tuple(args):
3080def expand_generator_or_tuple(args):
3081    if hasattr(args, "__len__"):  # Tuple
3082        if len(args) != 1:
3083            return args
3084        if isinstance(args[0], (NumberTypes, LinearExpr)):
3085            return args
3086    # Generator
3087    return args[0]
def evaluate_linear_expr( expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64], solution: ortools.sat.cp_model_pb2.CpSolverResponse) -> int:
3090def evaluate_linear_expr(
3091    expression: LinearExprT, solution: cp_model_pb2.CpSolverResponse
3092) -> int:
3093    """Evaluate a linear expression against a solution."""
3094    if isinstance(expression, IntegralTypes):
3095        return int(expression)
3096    if not isinstance(expression, LinearExpr):
3097        raise TypeError("Cannot interpret %s as a linear expression." % expression)
3098
3099    value = 0
3100    to_process = [(expression, 1)]
3101    while to_process:
3102        expr, coeff = to_process.pop()
3103        if isinstance(expr, IntegralTypes):
3104            value += int(expr) * coeff
3105        elif isinstance(expr, _ProductCst):
3106            to_process.append((expr.expression(), coeff * expr.coefficient()))
3107        elif isinstance(expr, _Sum):
3108            to_process.append((expr.left(), coeff))
3109            to_process.append((expr.right(), coeff))
3110        elif isinstance(expr, _SumArray):
3111            for e in expr.expressions():
3112                to_process.append((e, coeff))
3113            value += expr.constant() * coeff
3114        elif isinstance(expr, _WeightedSum):
3115            for e, c in zip(expr.expressions(), expr.coefficients()):
3116                to_process.append((e, coeff * c))
3117            value += expr.constant() * coeff
3118        elif isinstance(expr, IntVar):
3119            value += coeff * solution.solution[expr.index]
3120        elif isinstance(expr, _NotBooleanVariable):
3121            value += coeff * (1 - solution.solution[expr.negated().index])
3122        else:
3123            raise TypeError(f"Cannot interpret {expr} as a linear expression.")
3124
3125    return value

Evaluate a linear expression against a solution.

def evaluate_boolean_expression( literal: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool], solution: ortools.sat.cp_model_pb2.CpSolverResponse) -> bool:
3128def evaluate_boolean_expression(
3129    literal: LiteralT, solution: cp_model_pb2.CpSolverResponse
3130) -> bool:
3131    """Evaluate a boolean expression against a solution."""
3132    if isinstance(literal, IntegralTypes):
3133        return bool(literal)
3134    elif isinstance(literal, IntVar) or isinstance(literal, _NotBooleanVariable):
3135        index: int = cast(Union[IntVar, _NotBooleanVariable], literal).index
3136        if index >= 0:
3137            return bool(solution.solution[index])
3138        else:
3139            return not solution.solution[-index - 1]
3140    else:
3141        raise TypeError(f"Cannot interpret {literal} as a boolean expression.")

Evaluate a boolean expression against a solution.

class CpSolver:
3144class CpSolver:
3145    """Main solver class.
3146
3147    The purpose of this class is to search for a solution to the model provided
3148    to the solve() method.
3149
3150    Once solve() is called, this class allows inspecting the solution found
3151    with the value() and boolean_value() methods, as well as general statistics
3152    about the solve procedure.
3153    """
3154
3155    def __init__(self) -> None:
3156        self.__solution: Optional[cp_model_pb2.CpSolverResponse] = None
3157        self.parameters: sat_parameters_pb2.SatParameters = (
3158            sat_parameters_pb2.SatParameters()
3159        )
3160        self.log_callback: Optional[Callable[[str], None]] = None
3161        self.best_bound_callback: Optional[Callable[[float], None]] = None
3162        self.__solve_wrapper: Optional[swig_helper.SolveWrapper] = None
3163        self.__lock: threading.Lock = threading.Lock()
3164
3165    def solve(
3166        self,
3167        model: CpModel,
3168        solution_callback: Optional["CpSolverSolutionCallback"] = None,
3169    ) -> cp_model_pb2.CpSolverStatus:
3170        """Solves a problem and passes each solution to the callback if not null."""
3171        with self.__lock:
3172            self.__solve_wrapper = swig_helper.SolveWrapper()
3173
3174        self.__solve_wrapper.set_parameters(self.parameters)
3175        if solution_callback is not None:
3176            self.__solve_wrapper.add_solution_callback(solution_callback)
3177
3178        if self.log_callback is not None:
3179            self.__solve_wrapper.add_log_callback(self.log_callback)
3180
3181        if self.best_bound_callback is not None:
3182            self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback)
3183
3184        solution: cp_model_pb2.CpSolverResponse = self.__solve_wrapper.solve(
3185            model.proto
3186        )
3187        self.__solution = solution
3188
3189        if solution_callback is not None:
3190            self.__solve_wrapper.clear_solution_callback(solution_callback)
3191
3192        with self.__lock:
3193            self.__solve_wrapper = None
3194
3195        return solution.status
3196
3197    def stop_search(self) -> None:
3198        """Stops the current search asynchronously."""
3199        with self.__lock:
3200            if self.__solve_wrapper:
3201                self.__solve_wrapper.stop_search()
3202
3203    def value(self, expression: LinearExprT) -> int:
3204        """Returns the value of a linear expression after solve."""
3205        return evaluate_linear_expr(expression, self._solution)
3206
3207    def values(self, variables: _IndexOrSeries) -> pd.Series:
3208        """Returns the values of the input variables.
3209
3210        If `variables` is a `pd.Index`, then the output will be indexed by the
3211        variables. If `variables` is a `pd.Series` indexed by the underlying
3212        dimensions, then the output will be indexed by the same underlying
3213        dimensions.
3214
3215        Args:
3216          variables (Union[pd.Index, pd.Series]): The set of variables from which to
3217            get the values.
3218
3219        Returns:
3220          pd.Series: The values of all variables in the set.
3221        """
3222        solution = self._solution
3223        return _attribute_series(
3224            func=lambda v: solution.solution[v.index],
3225            values=variables,
3226        )
3227
3228    def boolean_value(self, literal: LiteralT) -> bool:
3229        """Returns the boolean value of a literal after solve."""
3230        return evaluate_boolean_expression(literal, self._solution)
3231
3232    def boolean_values(self, variables: _IndexOrSeries) -> pd.Series:
3233        """Returns the values of the input variables.
3234
3235        If `variables` is a `pd.Index`, then the output will be indexed by the
3236        variables. If `variables` is a `pd.Series` indexed by the underlying
3237        dimensions, then the output will be indexed by the same underlying
3238        dimensions.
3239
3240        Args:
3241          variables (Union[pd.Index, pd.Series]): The set of variables from which to
3242            get the values.
3243
3244        Returns:
3245          pd.Series: The values of all variables in the set.
3246        """
3247        solution = self._solution
3248        return _attribute_series(
3249            func=lambda literal: evaluate_boolean_expression(literal, solution),
3250            values=variables,
3251        )
3252
3253    @property
3254    def objective_value(self) -> float:
3255        """Returns the value of the objective after solve."""
3256        return self._solution.objective_value
3257
3258    @property
3259    def best_objective_bound(self) -> float:
3260        """Returns the best lower (upper) bound found when min(max)imizing."""
3261        return self._solution.best_objective_bound
3262
3263    @property
3264    def num_booleans(self) -> int:
3265        """Returns the number of boolean variables managed by the SAT solver."""
3266        return self._solution.num_booleans
3267
3268    @property
3269    def num_conflicts(self) -> int:
3270        """Returns the number of conflicts since the creation of the solver."""
3271        return self._solution.num_conflicts
3272
3273    @property
3274    def num_branches(self) -> int:
3275        """Returns the number of search branches explored by the solver."""
3276        return self._solution.num_branches
3277
3278    @property
3279    def wall_time(self) -> float:
3280        """Returns the wall time in seconds since the creation of the solver."""
3281        return self._solution.wall_time
3282
3283    @property
3284    def user_time(self) -> float:
3285        """Returns the user time in seconds since the creation of the solver."""
3286        return self._solution.user_time
3287
3288    @property
3289    def response_proto(self) -> cp_model_pb2.CpSolverResponse:
3290        """Returns the response object."""
3291        return self._solution
3292
3293    def response_stats(self) -> str:
3294        """Returns some statistics on the solution found as a string."""
3295        return swig_helper.CpSatHelper.solver_response_stats(self._solution)
3296
3297    def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]:
3298        """Returns the indices of the infeasible assumptions."""
3299        return self._solution.sufficient_assumptions_for_infeasibility
3300
3301    def status_name(self, status: Optional[Any] = None) -> str:
3302        """Returns the name of the status returned by solve()."""
3303        if status is None:
3304            status = self._solution.status
3305        return cp_model_pb2.CpSolverStatus.Name(status)
3306
3307    def solution_info(self) -> str:
3308        """Returns some information on the solve process.
3309
3310        Returns some information on how the solution was found, or the reason
3311        why the model or the parameters are invalid.
3312
3313        Raises:
3314          RuntimeError: if solve() has not been called.
3315        """
3316        return self._solution.solution_info
3317
3318    @property
3319    def _solution(self) -> cp_model_pb2.CpSolverResponse:
3320        """Checks solve() has been called, and returns the solution."""
3321        if self.__solution is None:
3322            raise RuntimeError("solve() has not been called.")
3323        return self.__solution
3324
3325    # Compatibility with pre PEP8
3326    # pylint: disable=invalid-name
3327
3328    def BestObjectiveBound(self) -> float:
3329        return self.best_objective_bound
3330
3331    def BooleanValue(self, literal: LiteralT) -> bool:
3332        return self.boolean_value(literal)
3333
3334    def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series:
3335        return self.boolean_values(variables)
3336
3337    def NumBooleans(self) -> int:
3338        return self.num_booleans
3339
3340    def NumConflicts(self) -> int:
3341        return self.num_conflicts
3342
3343    def NumBranches(self) -> int:
3344        return self.num_branches
3345
3346    def ObjectiveValue(self) -> float:
3347        return self.objective_value
3348
3349    def ResponseProto(self) -> cp_model_pb2.CpSolverResponse:
3350        return self.response_proto
3351
3352    def ResponseStats(self) -> str:
3353        return self.response_stats()
3354
3355    def Solve(
3356        self,
3357        model: CpModel,
3358        solution_callback: Optional["CpSolverSolutionCallback"] = None,
3359    ) -> cp_model_pb2.CpSolverStatus:
3360        return self.solve(model, solution_callback)
3361
3362    def SolutionInfo(self) -> str:
3363        return self.solution_info()
3364
3365    def StatusName(self, status: Optional[Any] = None) -> str:
3366        return self.status_name(status)
3367
3368    def StopSearch(self) -> None:
3369        self.stop_search()
3370
3371    def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]:
3372        return self.sufficient_assumptions_for_infeasibility()
3373
3374    def UserTime(self) -> float:
3375        return self.user_time
3376
3377    def Value(self, expression: LinearExprT) -> int:
3378        return self.value(expression)
3379
3380    def Values(self, variables: _IndexOrSeries) -> pd.Series:
3381        return self.values(variables)
3382
3383    def WallTime(self) -> float:
3384        return self.wall_time
3385
3386    def SolveWithSolutionCallback(
3387        self, model: CpModel, callback: "CpSolverSolutionCallback"
3388    ) -> cp_model_pb2.CpSolverStatus:
3389        """DEPRECATED Use solve() with the callback argument."""
3390        warnings.warn(
3391            "solve_with_solution_callback is deprecated; use solve() with"
3392            + "the callback argument.",
3393            DeprecationWarning,
3394        )
3395        return self.solve(model, callback)
3396
3397    def SearchForAllSolutions(
3398        self, model: CpModel, callback: "CpSolverSolutionCallback"
3399    ) -> cp_model_pb2.CpSolverStatus:
3400        """DEPRECATED Use solve() with the right parameter.
3401
3402        Search for all solutions of a satisfiability problem.
3403
3404        This method searches for all feasible solutions of a given model.
3405        Then it feeds the solution to the callback.
3406
3407        Note that the model cannot contain an objective.
3408
3409        Args:
3410          model: The model to solve.
3411          callback: The callback that will be called at each solution.
3412
3413        Returns:
3414          The status of the solve:
3415
3416          * *FEASIBLE* if some solutions have been found
3417          * *INFEASIBLE* if the solver has proved there are no solution
3418          * *OPTIMAL* if all solutions have been found
3419        """
3420        warnings.warn(
3421            "search_for_all_solutions is deprecated; use solve() with"
3422            + "enumerate_all_solutions = True.",
3423            DeprecationWarning,
3424        )
3425        if model.has_objective():
3426            raise TypeError(
3427                "Search for all solutions is only defined on satisfiability problems"
3428            )
3429        # Store old parameter.
3430        enumerate_all = self.parameters.enumerate_all_solutions
3431        self.parameters.enumerate_all_solutions = True
3432
3433        status: cp_model_pb2.CpSolverStatus = self.solve(model, callback)
3434
3435        # Restore parameter.
3436        self.parameters.enumerate_all_solutions = enumerate_all
3437        return status

Main solver class.

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

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

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 0x7facdb77c500>:
3165    def solve(
3166        self,
3167        model: CpModel,
3168        solution_callback: Optional["CpSolverSolutionCallback"] = None,
3169    ) -> cp_model_pb2.CpSolverStatus:
3170        """Solves a problem and passes each solution to the callback if not null."""
3171        with self.__lock:
3172            self.__solve_wrapper = swig_helper.SolveWrapper()
3173
3174        self.__solve_wrapper.set_parameters(self.parameters)
3175        if solution_callback is not None:
3176            self.__solve_wrapper.add_solution_callback(solution_callback)
3177
3178        if self.log_callback is not None:
3179            self.__solve_wrapper.add_log_callback(self.log_callback)
3180
3181        if self.best_bound_callback is not None:
3182            self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback)
3183
3184        solution: cp_model_pb2.CpSolverResponse = self.__solve_wrapper.solve(
3185            model.proto
3186        )
3187        self.__solution = solution
3188
3189        if solution_callback is not None:
3190            self.__solve_wrapper.clear_solution_callback(solution_callback)
3191
3192        with self.__lock:
3193            self.__solve_wrapper = None
3194
3195        return solution.status

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

def value( self, expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
3203    def value(self, expression: LinearExprT) -> int:
3204        """Returns the value of a linear expression after solve."""
3205        return evaluate_linear_expr(expression, self._solution)

Returns the value of a linear expression after solve.

def values( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
3207    def values(self, variables: _IndexOrSeries) -> pd.Series:
3208        """Returns the values of the input variables.
3209
3210        If `variables` is a `pd.Index`, then the output will be indexed by the
3211        variables. If `variables` is a `pd.Series` indexed by the underlying
3212        dimensions, then the output will be indexed by the same underlying
3213        dimensions.
3214
3215        Args:
3216          variables (Union[pd.Index, pd.Series]): The set of variables from which to
3217            get the values.
3218
3219        Returns:
3220          pd.Series: The values of all variables in the set.
3221        """
3222        solution = self._solution
3223        return _attribute_series(
3224            func=lambda v: solution.solution[v.index],
3225            values=variables,
3226        )

Returns the values of the input variables.

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

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

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

def boolean_value( self, literal: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
3228    def boolean_value(self, literal: LiteralT) -> bool:
3229        """Returns the boolean value of a literal after solve."""
3230        return evaluate_boolean_expression(literal, self._solution)

Returns the boolean value of a literal after solve.

def boolean_values( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
3232    def boolean_values(self, variables: _IndexOrSeries) -> pd.Series:
3233        """Returns the values of the input variables.
3234
3235        If `variables` is a `pd.Index`, then the output will be indexed by the
3236        variables. If `variables` is a `pd.Series` indexed by the underlying
3237        dimensions, then the output will be indexed by the same underlying
3238        dimensions.
3239
3240        Args:
3241          variables (Union[pd.Index, pd.Series]): The set of variables from which to
3242            get the values.
3243
3244        Returns:
3245          pd.Series: The values of all variables in the set.
3246        """
3247        solution = self._solution
3248        return _attribute_series(
3249            func=lambda literal: evaluate_boolean_expression(literal, solution),
3250            values=variables,
3251        )

Returns the values of the input variables.

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

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

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

objective_value: float
3253    @property
3254    def objective_value(self) -> float:
3255        """Returns the value of the objective after solve."""
3256        return self._solution.objective_value

Returns the value of the objective after solve.

best_objective_bound: float
3258    @property
3259    def best_objective_bound(self) -> float:
3260        """Returns the best lower (upper) bound found when min(max)imizing."""
3261        return self._solution.best_objective_bound

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

num_booleans: int
3263    @property
3264    def num_booleans(self) -> int:
3265        """Returns the number of boolean variables managed by the SAT solver."""
3266        return self._solution.num_booleans

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

num_conflicts: int
3268    @property
3269    def num_conflicts(self) -> int:
3270        """Returns the number of conflicts since the creation of the solver."""
3271        return self._solution.num_conflicts

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

num_branches: int
3273    @property
3274    def num_branches(self) -> int:
3275        """Returns the number of search branches explored by the solver."""
3276        return self._solution.num_branches

Returns the number of search branches explored by the solver.

wall_time: float
3278    @property
3279    def wall_time(self) -> float:
3280        """Returns the wall time in seconds since the creation of the solver."""
3281        return self._solution.wall_time

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

user_time: float
3283    @property
3284    def user_time(self) -> float:
3285        """Returns the user time in seconds since the creation of the solver."""
3286        return self._solution.user_time

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

response_proto: ortools.sat.cp_model_pb2.CpSolverResponse
3288    @property
3289    def response_proto(self) -> cp_model_pb2.CpSolverResponse:
3290        """Returns the response object."""
3291        return self._solution

Returns the response object.

def response_stats(self) -> str:
3293    def response_stats(self) -> str:
3294        """Returns some statistics on the solution found as a string."""
3295        return swig_helper.CpSatHelper.solver_response_stats(self._solution)

Returns some statistics on the solution found as a string.

def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]:
3297    def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]:
3298        """Returns the indices of the infeasible assumptions."""
3299        return self._solution.sufficient_assumptions_for_infeasibility

Returns the indices of the infeasible assumptions.

def status_name(self, status: Optional[Any] = None) -> str:
3301    def status_name(self, status: Optional[Any] = None) -> str:
3302        """Returns the name of the status returned by solve()."""
3303        if status is None:
3304            status = self._solution.status
3305        return cp_model_pb2.CpSolverStatus.Name(status)

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

def solution_info(self) -> str:
3307    def solution_info(self) -> str:
3308        """Returns some information on the solve process.
3309
3310        Returns some information on how the solution was found, or the reason
3311        why the model or the parameters are invalid.
3312
3313        Raises:
3314          RuntimeError: if solve() has not been called.
3315        """
3316        return self._solution.solution_info

Returns some information on the solve process.

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

Raises:
  • RuntimeError: if solve() has not been called.
def BestObjectiveBound(self) -> float:
3328    def BestObjectiveBound(self) -> float:
3329        return self.best_objective_bound
def BooleanValue( self, literal: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
3331    def BooleanValue(self, literal: LiteralT) -> bool:
3332        return self.boolean_value(literal)
def BooleanValues( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
3334    def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series:
3335        return self.boolean_values(variables)
def NumBooleans(self) -> int:
3337    def NumBooleans(self) -> int:
3338        return self.num_booleans
def NumConflicts(self) -> int:
3340    def NumConflicts(self) -> int:
3341        return self.num_conflicts
def NumBranches(self) -> int:
3343    def NumBranches(self) -> int:
3344        return self.num_branches
def ObjectiveValue(self) -> float:
3346    def ObjectiveValue(self) -> float:
3347        return self.objective_value
def ResponseProto(self) -> ortools.sat.cp_model_pb2.CpSolverResponse:
3349    def ResponseProto(self) -> cp_model_pb2.CpSolverResponse:
3350        return self.response_proto
def ResponseStats(self) -> str:
3352    def ResponseStats(self) -> str:
3353        return self.response_stats()
def Solve( self, model: CpModel, solution_callback: Optional[CpSolverSolutionCallback] = None) -> <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7facdb77c500>:
3355    def Solve(
3356        self,
3357        model: CpModel,
3358        solution_callback: Optional["CpSolverSolutionCallback"] = None,
3359    ) -> cp_model_pb2.CpSolverStatus:
3360        return self.solve(model, solution_callback)
def SolutionInfo(self) -> str:
3362    def SolutionInfo(self) -> str:
3363        return self.solution_info()
def StatusName(self, status: Optional[Any] = None) -> str:
3365    def StatusName(self, status: Optional[Any] = None) -> str:
3366        return self.status_name(status)
def StopSearch(self) -> None:
3368    def StopSearch(self) -> None:
3369        self.stop_search()
def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]:
3371    def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]:
3372        return self.sufficient_assumptions_for_infeasibility()
def UserTime(self) -> float:
3374    def UserTime(self) -> float:
3375        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:
3377    def Value(self, expression: LinearExprT) -> int:
3378        return self.value(expression)
def Values( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series]) -> pandas.core.series.Series:
3380    def Values(self, variables: _IndexOrSeries) -> pd.Series:
3381        return self.values(variables)
def WallTime(self) -> float:
3383    def WallTime(self) -> float:
3384        return self.wall_time
def SolveWithSolutionCallback( self, model: CpModel, callback: CpSolverSolutionCallback) -> <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7facdb77c500>:
3386    def SolveWithSolutionCallback(
3387        self, model: CpModel, callback: "CpSolverSolutionCallback"
3388    ) -> cp_model_pb2.CpSolverStatus:
3389        """DEPRECATED Use solve() with the callback argument."""
3390        warnings.warn(
3391            "solve_with_solution_callback is deprecated; use solve() with"
3392            + "the callback argument.",
3393            DeprecationWarning,
3394        )
3395        return self.solve(model, callback)

DEPRECATED Use solve() with the callback argument.

def SearchForAllSolutions( self, model: CpModel, callback: CpSolverSolutionCallback) -> <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7facdb77c500>:
3397    def SearchForAllSolutions(
3398        self, model: CpModel, callback: "CpSolverSolutionCallback"
3399    ) -> cp_model_pb2.CpSolverStatus:
3400        """DEPRECATED Use solve() with the right parameter.
3401
3402        Search for all solutions of a satisfiability problem.
3403
3404        This method searches for all feasible solutions of a given model.
3405        Then it feeds the solution to the callback.
3406
3407        Note that the model cannot contain an objective.
3408
3409        Args:
3410          model: The model to solve.
3411          callback: The callback that will be called at each solution.
3412
3413        Returns:
3414          The status of the solve:
3415
3416          * *FEASIBLE* if some solutions have been found
3417          * *INFEASIBLE* if the solver has proved there are no solution
3418          * *OPTIMAL* if all solutions have been found
3419        """
3420        warnings.warn(
3421            "search_for_all_solutions is deprecated; use solve() with"
3422            + "enumerate_all_solutions = True.",
3423            DeprecationWarning,
3424        )
3425        if model.has_objective():
3426            raise TypeError(
3427                "Search for all solutions is only defined on satisfiability problems"
3428            )
3429        # Store old parameter.
3430        enumerate_all = self.parameters.enumerate_all_solutions
3431        self.parameters.enumerate_all_solutions = True
3432
3433        status: cp_model_pb2.CpSolverStatus = self.solve(model, callback)
3434
3435        # Restore parameter.
3436        self.parameters.enumerate_all_solutions = enumerate_all
3437        return status

DEPRECATED Use solve() with the right parameter.

Search for all solutions of a satisfiability problem.

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

Note that the model cannot contain an objective.

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

The status of the solve:

  • FEASIBLE if some solutions have been found
  • INFEASIBLE if the solver has proved there are no solution
  • OPTIMAL if all solutions have been found
class CpSolverSolutionCallback(ortools.sat.python.swig_helper.SolutionCallback):
3443class CpSolverSolutionCallback(swig_helper.SolutionCallback):
3444    """Solution callback.
3445
3446    This class implements a callback that will be called at each new solution
3447    found during search.
3448
3449    The method on_solution_callback() will be called by the solver, and must be
3450    implemented. The current solution can be queried using the boolean_value()
3451    and value() methods.
3452
3453    These methods returns the same information as their counterpart in the
3454    `CpSolver` class.
3455    """
3456
3457    def __init__(self) -> None:
3458        swig_helper.SolutionCallback.__init__(self)
3459
3460    def OnSolutionCallback(self) -> None:
3461        """Proxy for the same method in snake case."""
3462        self.on_solution_callback()
3463
3464    def boolean_value(self, lit: LiteralT) -> bool:
3465        """Returns the boolean value of a boolean literal.
3466
3467        Args:
3468            lit: A boolean variable or its negation.
3469
3470        Returns:
3471            The Boolean value of the literal in the solution.
3472
3473        Raises:
3474            RuntimeError: if `lit` is not a boolean variable or its negation.
3475        """
3476        if not self.has_response():
3477            raise RuntimeError("solve() has not been called.")
3478        if isinstance(lit, IntegralTypes):
3479            return bool(lit)
3480        if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable):
3481            return self.SolutionBooleanValue(
3482                cast(Union[IntVar, _NotBooleanVariable], lit).index
3483            )
3484        if cmh.is_boolean(lit):
3485            return bool(lit)
3486        raise TypeError(f"Cannot interpret {lit} as a boolean expression.")
3487
3488    def value(self, expression: LinearExprT) -> int:
3489        """Evaluates an linear expression in the current solution.
3490
3491        Args:
3492            expression: a linear expression of the model.
3493
3494        Returns:
3495            An integer value equal to the evaluation of the linear expression
3496            against the current solution.
3497
3498        Raises:
3499            RuntimeError: if 'expression' is not a LinearExpr.
3500        """
3501        if not self.has_response():
3502            raise RuntimeError("solve() has not been called.")
3503
3504        value = 0
3505        to_process = [(expression, 1)]
3506        while to_process:
3507            expr, coeff = to_process.pop()
3508            if isinstance(expr, IntegralTypes):
3509                value += int(expr) * coeff
3510            elif isinstance(expr, _ProductCst):
3511                to_process.append((expr.expression(), coeff * expr.coefficient()))
3512            elif isinstance(expr, _Sum):
3513                to_process.append((expr.left(), coeff))
3514                to_process.append((expr.right(), coeff))
3515            elif isinstance(expr, _SumArray):
3516                for e in expr.expressions():
3517                    to_process.append((e, coeff))
3518                    value += expr.constant() * coeff
3519            elif isinstance(expr, _WeightedSum):
3520                for e, c in zip(expr.expressions(), expr.coefficients()):
3521                    to_process.append((e, coeff * c))
3522                value += expr.constant() * coeff
3523            elif isinstance(expr, IntVar):
3524                value += coeff * self.SolutionIntegerValue(expr.index)
3525            elif isinstance(expr, _NotBooleanVariable):
3526                value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index))
3527            else:
3528                raise TypeError(
3529                    f"cannot interpret {expression} as a linear expression."
3530                )
3531
3532        return value
3533
3534    def has_response(self) -> bool:
3535        return self.HasResponse()
3536
3537    def stop_search(self) -> None:
3538        """Stops the current search asynchronously."""
3539        if not self.has_response():
3540            raise RuntimeError("solve() has not been called.")
3541        self.StopSearch()
3542
3543    @property
3544    def objective_value(self) -> float:
3545        """Returns the value of the objective after solve."""
3546        if not self.has_response():
3547            raise RuntimeError("solve() has not been called.")
3548        return self.ObjectiveValue()
3549
3550    @property
3551    def best_objective_bound(self) -> float:
3552        """Returns the best lower (upper) bound found when min(max)imizing."""
3553        if not self.has_response():
3554            raise RuntimeError("solve() has not been called.")
3555        return self.BestObjectiveBound()
3556
3557    @property
3558    def num_booleans(self) -> int:
3559        """Returns the number of boolean variables managed by the SAT solver."""
3560        if not self.has_response():
3561            raise RuntimeError("solve() has not been called.")
3562        return self.NumBooleans()
3563
3564    @property
3565    def num_conflicts(self) -> int:
3566        """Returns the number of conflicts since the creation of the solver."""
3567        if not self.has_response():
3568            raise RuntimeError("solve() has not been called.")
3569        return self.NumConflicts()
3570
3571    @property
3572    def num_branches(self) -> int:
3573        """Returns the number of search branches explored by the solver."""
3574        if not self.has_response():
3575            raise RuntimeError("solve() has not been called.")
3576        return self.NumBranches()
3577
3578    @property
3579    def num_integer_propagations(self) -> int:
3580        """Returns the number of integer propagations done by the solver."""
3581        if not self.has_response():
3582            raise RuntimeError("solve() has not been called.")
3583        return self.NumIntegerPropagations()
3584
3585    @property
3586    def num_boolean_propagations(self) -> int:
3587        """Returns the number of Boolean propagations done by the solver."""
3588        if not self.has_response():
3589            raise RuntimeError("solve() has not been called.")
3590        return self.NumBooleanPropagations()
3591
3592    @property
3593    def deterministic_time(self) -> float:
3594        """Returns the determistic time in seconds since the creation of the solver."""
3595        if not self.has_response():
3596            raise RuntimeError("solve() has not been called.")
3597        return self.DeterministicTime()
3598
3599    @property
3600    def wall_time(self) -> float:
3601        """Returns the wall time in seconds since the creation of the solver."""
3602        if not self.has_response():
3603            raise RuntimeError("solve() has not been called.")
3604        return self.WallTime()
3605
3606    @property
3607    def user_time(self) -> float:
3608        """Returns the user time in seconds since the creation of the solver."""
3609        if not self.has_response():
3610            raise RuntimeError("solve() has not been called.")
3611        return self.UserTime()
3612
3613    @property
3614    def response_proto(self) -> cp_model_pb2.CpSolverResponse:
3615        """Returns the response object."""
3616        if not self.has_response():
3617            raise RuntimeError("solve() has not been called.")
3618        return self.Response()
3619
3620    # Compatibility with pre PEP8
3621    # pylint: disable=invalid-name
3622    Value = value
3623    BooleanValue = boolean_value
3624    # pylint: enable=invalid-name

Solution callback.

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

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

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

CpSolverSolutionCallback()
3457    def __init__(self) -> None:
3458        swig_helper.SolutionCallback.__init__(self)
def OnSolutionCallback(self) -> None:
3460    def OnSolutionCallback(self) -> None:
3461        """Proxy for the same method in snake case."""
3462        self.on_solution_callback()

Proxy for the same method in snake case.

def boolean_value( self, lit: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
3464    def boolean_value(self, lit: LiteralT) -> bool:
3465        """Returns the boolean value of a boolean literal.
3466
3467        Args:
3468            lit: A boolean variable or its negation.
3469
3470        Returns:
3471            The Boolean value of the literal in the solution.
3472
3473        Raises:
3474            RuntimeError: if `lit` is not a boolean variable or its negation.
3475        """
3476        if not self.has_response():
3477            raise RuntimeError("solve() has not been called.")
3478        if isinstance(lit, IntegralTypes):
3479            return bool(lit)
3480        if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable):
3481            return self.SolutionBooleanValue(
3482                cast(Union[IntVar, _NotBooleanVariable], lit).index
3483            )
3484        if cmh.is_boolean(lit):
3485            return bool(lit)
3486        raise TypeError(f"Cannot interpret {lit} as a boolean expression.")

Returns the boolean value of a boolean literal.

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

The Boolean value of the literal in the solution.

Raises:
  • RuntimeError: if lit is not a boolean variable or its negation.
def value( self, expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
3488    def value(self, expression: LinearExprT) -> int:
3489        """Evaluates an linear expression in the current solution.
3490
3491        Args:
3492            expression: a linear expression of the model.
3493
3494        Returns:
3495            An integer value equal to the evaluation of the linear expression
3496            against the current solution.
3497
3498        Raises:
3499            RuntimeError: if 'expression' is not a LinearExpr.
3500        """
3501        if not self.has_response():
3502            raise RuntimeError("solve() has not been called.")
3503
3504        value = 0
3505        to_process = [(expression, 1)]
3506        while to_process:
3507            expr, coeff = to_process.pop()
3508            if isinstance(expr, IntegralTypes):
3509                value += int(expr) * coeff
3510            elif isinstance(expr, _ProductCst):
3511                to_process.append((expr.expression(), coeff * expr.coefficient()))
3512            elif isinstance(expr, _Sum):
3513                to_process.append((expr.left(), coeff))
3514                to_process.append((expr.right(), coeff))
3515            elif isinstance(expr, _SumArray):
3516                for e in expr.expressions():
3517                    to_process.append((e, coeff))
3518                    value += expr.constant() * coeff
3519            elif isinstance(expr, _WeightedSum):
3520                for e, c in zip(expr.expressions(), expr.coefficients()):
3521                    to_process.append((e, coeff * c))
3522                value += expr.constant() * coeff
3523            elif isinstance(expr, IntVar):
3524                value += coeff * self.SolutionIntegerValue(expr.index)
3525            elif isinstance(expr, _NotBooleanVariable):
3526                value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index))
3527            else:
3528                raise TypeError(
3529                    f"cannot interpret {expression} as a linear expression."
3530                )
3531
3532        return value

Evaluates an linear expression in the current solution.

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

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

Raises:
  • RuntimeError: if 'expression' is not a LinearExpr.
def has_response(self) -> bool:
3534    def has_response(self) -> bool:
3535        return self.HasResponse()
objective_value: float
3543    @property
3544    def objective_value(self) -> float:
3545        """Returns the value of the objective after solve."""
3546        if not self.has_response():
3547            raise RuntimeError("solve() has not been called.")
3548        return self.ObjectiveValue()

Returns the value of the objective after solve.

best_objective_bound: float
3550    @property
3551    def best_objective_bound(self) -> float:
3552        """Returns the best lower (upper) bound found when min(max)imizing."""
3553        if not self.has_response():
3554            raise RuntimeError("solve() has not been called.")
3555        return self.BestObjectiveBound()

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

num_booleans: int
3557    @property
3558    def num_booleans(self) -> int:
3559        """Returns the number of boolean variables managed by the SAT solver."""
3560        if not self.has_response():
3561            raise RuntimeError("solve() has not been called.")
3562        return self.NumBooleans()

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

num_conflicts: int
3564    @property
3565    def num_conflicts(self) -> int:
3566        """Returns the number of conflicts since the creation of the solver."""
3567        if not self.has_response():
3568            raise RuntimeError("solve() has not been called.")
3569        return self.NumConflicts()

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

num_branches: int
3571    @property
3572    def num_branches(self) -> int:
3573        """Returns the number of search branches explored by the solver."""
3574        if not self.has_response():
3575            raise RuntimeError("solve() has not been called.")
3576        return self.NumBranches()

Returns the number of search branches explored by the solver.

num_integer_propagations: int
3578    @property
3579    def num_integer_propagations(self) -> int:
3580        """Returns the number of integer propagations done by the solver."""
3581        if not self.has_response():
3582            raise RuntimeError("solve() has not been called.")
3583        return self.NumIntegerPropagations()

Returns the number of integer propagations done by the solver.

num_boolean_propagations: int
3585    @property
3586    def num_boolean_propagations(self) -> int:
3587        """Returns the number of Boolean propagations done by the solver."""
3588        if not self.has_response():
3589            raise RuntimeError("solve() has not been called.")
3590        return self.NumBooleanPropagations()

Returns the number of Boolean propagations done by the solver.

deterministic_time: float
3592    @property
3593    def deterministic_time(self) -> float:
3594        """Returns the determistic time in seconds since the creation of the solver."""
3595        if not self.has_response():
3596            raise RuntimeError("solve() has not been called.")
3597        return self.DeterministicTime()

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

wall_time: float
3599    @property
3600    def wall_time(self) -> float:
3601        """Returns the wall time in seconds since the creation of the solver."""
3602        if not self.has_response():
3603            raise RuntimeError("solve() has not been called.")
3604        return self.WallTime()

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

user_time: float
3606    @property
3607    def user_time(self) -> float:
3608        """Returns the user time in seconds since the creation of the solver."""
3609        if not self.has_response():
3610            raise RuntimeError("solve() has not been called.")
3611        return self.UserTime()

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

response_proto: ortools.sat.cp_model_pb2.CpSolverResponse
3613    @property
3614    def response_proto(self) -> cp_model_pb2.CpSolverResponse:
3615        """Returns the response object."""
3616        if not self.has_response():
3617            raise RuntimeError("solve() has not been called.")
3618        return self.Response()

Returns the response object.

def Value( self, expression: Union[LinearExpr, IntVar, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64]) -> int:
3488    def value(self, expression: LinearExprT) -> int:
3489        """Evaluates an linear expression in the current solution.
3490
3491        Args:
3492            expression: a linear expression of the model.
3493
3494        Returns:
3495            An integer value equal to the evaluation of the linear expression
3496            against the current solution.
3497
3498        Raises:
3499            RuntimeError: if 'expression' is not a LinearExpr.
3500        """
3501        if not self.has_response():
3502            raise RuntimeError("solve() has not been called.")
3503
3504        value = 0
3505        to_process = [(expression, 1)]
3506        while to_process:
3507            expr, coeff = to_process.pop()
3508            if isinstance(expr, IntegralTypes):
3509                value += int(expr) * coeff
3510            elif isinstance(expr, _ProductCst):
3511                to_process.append((expr.expression(), coeff * expr.coefficient()))
3512            elif isinstance(expr, _Sum):
3513                to_process.append((expr.left(), coeff))
3514                to_process.append((expr.right(), coeff))
3515            elif isinstance(expr, _SumArray):
3516                for e in expr.expressions():
3517                    to_process.append((e, coeff))
3518                    value += expr.constant() * coeff
3519            elif isinstance(expr, _WeightedSum):
3520                for e, c in zip(expr.expressions(), expr.coefficients()):
3521                    to_process.append((e, coeff * c))
3522                value += expr.constant() * coeff
3523            elif isinstance(expr, IntVar):
3524                value += coeff * self.SolutionIntegerValue(expr.index)
3525            elif isinstance(expr, _NotBooleanVariable):
3526                value += coeff * (1 - self.SolutionIntegerValue(expr.negated().index))
3527            else:
3528                raise TypeError(
3529                    f"cannot interpret {expression} as a linear expression."
3530                )
3531
3532        return value

Evaluates an linear expression in the current solution.

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

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

Raises:
  • RuntimeError: if 'expression' is not a LinearExpr.
def BooleanValue( self, lit: Union[IntVar, ortools.sat.python.cp_model._NotBooleanVariable, int, numpy.int8, numpy.uint8, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, bool]) -> bool:
3464    def boolean_value(self, lit: LiteralT) -> bool:
3465        """Returns the boolean value of a boolean literal.
3466
3467        Args:
3468            lit: A boolean variable or its negation.
3469
3470        Returns:
3471            The Boolean value of the literal in the solution.
3472
3473        Raises:
3474            RuntimeError: if `lit` is not a boolean variable or its negation.
3475        """
3476        if not self.has_response():
3477            raise RuntimeError("solve() has not been called.")
3478        if isinstance(lit, IntegralTypes):
3479            return bool(lit)
3480        if isinstance(lit, IntVar) or isinstance(lit, _NotBooleanVariable):
3481            return self.SolutionBooleanValue(
3482                cast(Union[IntVar, _NotBooleanVariable], lit).index
3483            )
3484        if cmh.is_boolean(lit):
3485            return bool(lit)
3486        raise TypeError(f"Cannot interpret {lit} as a boolean expression.")

Returns the boolean value of a boolean literal.

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

The Boolean value of the literal in the solution.

Raises:
  • RuntimeError: if lit is not a boolean variable or its negation.
class ObjectiveSolutionPrinter(CpSolverSolutionCallback):
3627class ObjectiveSolutionPrinter(CpSolverSolutionCallback):
3628    """Display the objective value and time of intermediate solutions."""
3629
3630    def __init__(self) -> None:
3631        CpSolverSolutionCallback.__init__(self)
3632        self.__solution_count = 0
3633        self.__start_time = time.time()
3634
3635    def on_solution_callback(self) -> None:
3636        """Called on each new solution."""
3637        current_time = time.time()
3638        obj = self.objective_value
3639        print(
3640            "Solution %i, time = %0.2f s, objective = %i"
3641            % (self.__solution_count, current_time - self.__start_time, obj)
3642        )
3643        self.__solution_count += 1
3644
3645    def solution_count(self) -> int:
3646        """Returns the number of solutions found."""
3647        return self.__solution_count

Display the objective value and time of intermediate solutions.

ObjectiveSolutionPrinter()
3630    def __init__(self) -> None:
3631        CpSolverSolutionCallback.__init__(self)
3632        self.__solution_count = 0
3633        self.__start_time = time.time()
def on_solution_callback(self) -> None:
3635    def on_solution_callback(self) -> None:
3636        """Called on each new solution."""
3637        current_time = time.time()
3638        obj = self.objective_value
3639        print(
3640            "Solution %i, time = %0.2f s, objective = %i"
3641            % (self.__solution_count, current_time - self.__start_time, obj)
3642        )
3643        self.__solution_count += 1

Called on each new solution.

def solution_count(self) -> int:
3645    def solution_count(self) -> int:
3646        """Returns the number of solutions found."""
3647        return self.__solution_count

Returns the number of solutions found.

class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback):
3650class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback):
3651    """Print intermediate solutions (objective, variable values, time)."""
3652
3653    def __init__(self, variables: Sequence[IntVar]) -> None:
3654        CpSolverSolutionCallback.__init__(self)
3655        self.__variables: Sequence[IntVar] = variables
3656        self.__solution_count: int = 0
3657        self.__start_time: float = time.time()
3658
3659    def on_solution_callback(self) -> None:
3660        """Called on each new solution."""
3661        current_time = time.time()
3662        obj = self.objective_value
3663        print(
3664            "Solution %i, time = %0.2f s, objective = %i"
3665            % (self.__solution_count, current_time - self.__start_time, obj)
3666        )
3667        for v in self.__variables:
3668            print("  %s = %i" % (v, self.value(v)), end=" ")
3669        print()
3670        self.__solution_count += 1
3671
3672    @property
3673    def solution_count(self) -> int:
3674        """Returns the number of solutions found."""
3675        return self.__solution_count

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

VarArrayAndObjectiveSolutionPrinter(variables: Sequence[IntVar])
3653    def __init__(self, variables: Sequence[IntVar]) -> None:
3654        CpSolverSolutionCallback.__init__(self)
3655        self.__variables: Sequence[IntVar] = variables
3656        self.__solution_count: int = 0
3657        self.__start_time: float = time.time()
def on_solution_callback(self) -> None:
3659    def on_solution_callback(self) -> None:
3660        """Called on each new solution."""
3661        current_time = time.time()
3662        obj = self.objective_value
3663        print(
3664            "Solution %i, time = %0.2f s, objective = %i"
3665            % (self.__solution_count, current_time - self.__start_time, obj)
3666        )
3667        for v in self.__variables:
3668            print("  %s = %i" % (v, self.value(v)), end=" ")
3669        print()
3670        self.__solution_count += 1

Called on each new solution.

solution_count: int
3672    @property
3673    def solution_count(self) -> int:
3674        """Returns the number of solutions found."""
3675        return self.__solution_count

Returns the number of solutions found.

class VarArraySolutionPrinter(CpSolverSolutionCallback):
3678class VarArraySolutionPrinter(CpSolverSolutionCallback):
3679    """Print intermediate solutions (variable values, time)."""
3680
3681    def __init__(self, variables: Sequence[IntVar]) -> None:
3682        CpSolverSolutionCallback.__init__(self)
3683        self.__variables: Sequence[IntVar] = variables
3684        self.__solution_count: int = 0
3685        self.__start_time: float = time.time()
3686
3687    def on_solution_callback(self) -> None:
3688        """Called on each new solution."""
3689        current_time = time.time()
3690        print(
3691            "Solution %i, time = %0.2f s"
3692            % (self.__solution_count, current_time - self.__start_time)
3693        )
3694        for v in self.__variables:
3695            print("  %s = %i" % (v, self.value(v)), end=" ")
3696        print()
3697        self.__solution_count += 1
3698
3699    @property
3700    def solution_count(self) -> int:
3701        """Returns the number of solutions found."""
3702        return self.__solution_count

Print intermediate solutions (variable values, time).

VarArraySolutionPrinter(variables: Sequence[IntVar])
3681    def __init__(self, variables: Sequence[IntVar]) -> None:
3682        CpSolverSolutionCallback.__init__(self)
3683        self.__variables: Sequence[IntVar] = variables
3684        self.__solution_count: int = 0
3685        self.__start_time: float = time.time()
def on_solution_callback(self) -> None:
3687    def on_solution_callback(self) -> None:
3688        """Called on each new solution."""
3689        current_time = time.time()
3690        print(
3691            "Solution %i, time = %0.2f s"
3692            % (self.__solution_count, current_time - self.__start_time)
3693        )
3694        for v in self.__variables:
3695            print("  %s = %i" % (v, self.value(v)), end=" ")
3696        print()
3697        self.__solution_count += 1

Called on each new solution.

solution_count: int
3699    @property
3700    def solution_count(self) -> int:
3701        """Returns the number of solutions found."""
3702        return self.__solution_count

Returns the number of solutions found.