ortools.linear_solver.python.model_builder

Methods for building and solving model_builder models.

The following two sections describe the main methods for building and solving those models.

  • .Model">Model: Methods for creating models, including variables and constraints.
  • .Solver">Solver: Methods for solving a model and evaluating solutions.

Additional methods for solving Model models:

  • Constraint: A few utility methods for modifying constraints created by Model.
  • .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 model_builder models.
  15
  16The following two sections describe the main
  17methods for building and solving those models.
  18
  19* [`Model`](#model_builder.Model): Methods for creating
  20models, including variables and constraints.
  21* [`Solver`](#model_builder.Solver): Methods for solving
  22a model and evaluating solutions.
  23
  24Additional methods for solving Model models:
  25
  26* [`Constraint`](#model_builder.Constraint): A few utility methods for modifying
  27  constraints created by `Model`.
  28* [`LinearExpr`](#model_builder.LinearExpr): Methods for creating constraints
  29  and the objective from large arrays of coefficients.
  30
  31Other methods and functions listed are primarily used for developing OR-Tools,
  32rather than for solving specific optimization problems.
  33"""
  34
  35import abc
  36import dataclasses
  37import math
  38import numbers
  39import typing
  40from typing import Callable, List, Optional, Sequence, Tuple, Union, cast
  41
  42import numpy as np
  43from numpy import typing as npt
  44import pandas as pd
  45
  46from ortools.linear_solver import linear_solver_pb2
  47from ortools.linear_solver.python import model_builder_helper as mbh
  48from ortools.linear_solver.python import model_builder_numbers as mbn
  49
  50
  51# Custom types.
  52NumberT = Union[int, float, numbers.Real, np.number]
  53IntegerT = Union[int, numbers.Integral, np.integer]
  54LinearExprT = Union["LinearExpr", NumberT]
  55ConstraintT = Union["_BoundedLinearExpr", bool]
  56_IndexOrSeries = Union[pd.Index, pd.Series]
  57_VariableOrConstraint = Union["LinearConstraint", "Variable"]
  58
  59# Forward solve statuses.
  60SolveStatus = mbh.SolveStatus
  61
  62# pylint: disable=protected-access
  63
  64
  65class LinearExpr(metaclass=abc.ABCMeta):
  66    """Holds an linear expression.
  67
  68    A linear expression is built from constants and variables.
  69    For example, `x + 2.0 * (y - z + 1.0)`.
  70
  71    Linear expressions are used in Model models in constraints and in the
  72    objective:
  73
  74    * You can define linear constraints as in:
  75
  76    ```
  77    model.add(x + 2 * y <= 5.0)
  78    model.add(sum(array_of_vars) == 5.0)
  79    ```
  80
  81    * In Model, the objective is a linear expression:
  82
  83    ```
  84    model.minimize(x + 2.0 * y + z)
  85    ```
  86
  87    * For large arrays, using the LinearExpr class is faster that using the python
  88    `sum()` function. You can create constraints and the objective from lists of
  89    linear expressions or coefficients as follows:
  90
  91    ```
  92    model.minimize(model_builder.LinearExpr.sum(expressions))
  93    model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
  94    ```
  95    """
  96
  97    @classmethod
  98    def sum(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
  99        cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0
 100    ) -> LinearExprT:
 101        """Creates `sum(expressions) + constant`.
 102
 103        It can perform simple simplifications and returns different objects,
 104        including the input.
 105
 106        Args:
 107          expressions: a sequence of linear expressions or constants.
 108          constant: a numerical constant.
 109
 110        Returns:
 111          a LinearExpr instance or a numerical constant.
 112        """
 113        checked_constant: np.double = mbn.assert_is_a_number(constant)
 114        if not expressions:
 115            return checked_constant
 116        if len(expressions) == 1 and mbn.is_zero(checked_constant):
 117            return expressions[0]
 118
 119        return LinearExpr.weighted_sum(
 120            expressions, np.ones(len(expressions)), constant=checked_constant
 121        )
 122
 123    @classmethod
 124    def weighted_sum(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
 125        cls,
 126        expressions: Sequence[LinearExprT],
 127        coefficients: Sequence[NumberT],
 128        *,
 129        constant: NumberT = 0.0,
 130    ) -> Union[NumberT, "_LinearExpression"]:
 131        """Creates `sum(expressions[i] * coefficients[i]) + constant`.
 132
 133        It can perform simple simplifications and returns different object,
 134        including the input.
 135
 136        Args:
 137          expressions: a sequence of linear expressions or constants.
 138          coefficients: a sequence of numerical constants.
 139          constant: a numerical constant.
 140
 141        Returns:
 142          a _LinearExpression instance or a numerical constant.
 143        """
 144        if len(expressions) != len(coefficients):
 145            raise ValueError(
 146                "LinearExpr.weighted_sum: expressions and coefficients have"
 147                " different lengths"
 148            )
 149        checked_constant: np.double = mbn.assert_is_a_number(constant)
 150        if not expressions:
 151            return checked_constant
 152        return _sum_as_flat_linear_expression(
 153            to_process=list(zip(expressions, coefficients)), offset=checked_constant
 154        )
 155
 156    @classmethod
 157    def term(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
 158        cls,
 159        expression: LinearExprT,
 160        coefficient: NumberT,
 161        *,
 162        constant: NumberT = 0.0,
 163    ) -> LinearExprT:
 164        """Creates `expression * coefficient + constant`.
 165
 166        It can perform simple simplifications and returns different object,
 167        including the input.
 168        Args:
 169          expression: a linear expression or a constant.
 170          coefficient: a numerical constant.
 171          constant: a numerical constant.
 172
 173        Returns:
 174          a LinearExpr instance or a numerical constant.
 175        """
 176        checked_coefficient: np.double = mbn.assert_is_a_number(coefficient)
 177        checked_constant: np.double = mbn.assert_is_a_number(constant)
 178
 179        if mbn.is_zero(checked_coefficient):
 180            return checked_constant
 181        if mbn.is_one(checked_coefficient) and mbn.is_zero(checked_constant):
 182            return expression
 183        if mbn.is_a_number(expression):
 184            return np.double(expression) * checked_coefficient + checked_constant
 185        if isinstance(expression, LinearExpr):
 186            return _as_flat_linear_expression(
 187                expression * checked_coefficient + checked_constant
 188            )
 189        raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}")
 190
 191    def __hash__(self):
 192        return object.__hash__(self)
 193
 194    def __add__(self, arg: LinearExprT) -> "_Sum":
 195        return _Sum(self, arg)
 196
 197    def __radd__(self, arg: LinearExprT) -> "_Sum":
 198        return self.__add__(arg)
 199
 200    def __sub__(self, arg: LinearExprT) -> "_Sum":
 201        return _Sum(self, -arg)
 202
 203    def __rsub__(self, arg: LinearExprT) -> "_Sum":
 204        return _Sum(-self, arg)
 205
 206    def __mul__(self, arg: NumberT) -> "_Product":
 207        return _Product(self, arg)
 208
 209    def __rmul__(self, arg: NumberT) -> "_Product":
 210        return self.__mul__(arg)
 211
 212    def __truediv__(self, coeff: NumberT) -> "_Product":
 213        return self.__mul__(1.0 / coeff)
 214
 215    def __neg__(self) -> "_Product":
 216        return _Product(self, -1)
 217
 218    def __bool__(self):
 219        raise NotImplementedError(f"Cannot use a LinearExpr {self} as a Boolean value")
 220
 221    def __eq__(self, arg: LinearExprT) -> "BoundedLinearExpression":
 222        return BoundedLinearExpression(self - arg, 0, 0)
 223
 224    def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression":
 225        return BoundedLinearExpression(
 226            self - arg, 0, math.inf
 227        )  # pytype: disable=wrong-arg-types  # numpy-scalars
 228
 229    def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression":
 230        return BoundedLinearExpression(
 231            self - arg, -math.inf, 0
 232        )  # pytype: disable=wrong-arg-types  # numpy-scalars
 233
 234
 235class Variable(LinearExpr):
 236    """A variable (continuous or integral).
 237
 238    A Variable is an object that can take on any integer value within defined
 239    ranges. Variables appear in constraint like:
 240
 241        x + y >= 5
 242
 243    Solving a model is equivalent to finding, for each variable, a single value
 244    from the set of initial values (called the initial domain), such that the
 245    model is feasible, or optimal if you provided an objective function.
 246    """
 247
 248    def __init__(
 249        self,
 250        helper: mbh.ModelBuilderHelper,
 251        lb: NumberT,
 252        ub: Optional[NumberT],
 253        is_integral: Optional[bool],
 254        name: Optional[str],
 255    ) -> None:
 256        """See Model.new_var below."""
 257        LinearExpr.__init__(self)
 258        self.__helper: mbh.ModelBuilderHelper = helper
 259        # Python do not support multiple __init__ methods.
 260        # This method is only called from the Model class.
 261        # We hack the parameter to support the two cases:
 262        # case 1:
 263        #     helper is a ModelBuilderHelper, lb is a double value, ub is a double
 264        #     value, is_integral is a Boolean value, and name is a string.
 265        # case 2:
 266        #     helper is a ModelBuilderHelper, lb is an index (int), ub is None,
 267        #     is_integral is None, and name is None.
 268        if mbn.is_integral(lb) and ub is None and is_integral is None:
 269            self.__index: np.int32 = np.int32(lb)
 270            self.__helper: mbh.ModelBuilderHelper = helper
 271        else:
 272            index: np.int32 = helper.add_var()
 273            self.__index: np.int32 = np.int32(index)
 274            self.__helper: mbh.ModelBuilderHelper = helper
 275            helper.set_var_lower_bound(index, lb)
 276            helper.set_var_upper_bound(index, ub)
 277            helper.set_var_integrality(index, is_integral)
 278            if name:
 279                helper.set_var_name(index, name)
 280
 281    @property
 282    def index(self) -> np.int32:
 283        """Returns the index of the variable in the helper."""
 284        return self.__index
 285
 286    @property
 287    def helper(self) -> mbh.ModelBuilderHelper:
 288        """Returns the underlying ModelBuilderHelper."""
 289        return self.__helper
 290
 291    def is_equal_to(self, other: LinearExprT) -> bool:
 292        """Returns true if self == other in the python sense."""
 293        if not isinstance(other, Variable):
 294            return False
 295        return self.index == other.index and self.helper == other.helper
 296
 297    def __str__(self) -> str:
 298        return self.name
 299
 300    def __repr__(self) -> str:
 301        return self.__str__()
 302
 303    @property
 304    def name(self) -> str:
 305        """Returns the name of the variable."""
 306        var_name = self.__helper.var_name(self.__index)
 307        if var_name:
 308            return var_name
 309        return f"variable#{self.index}"
 310
 311    @name.setter
 312    def name(self, name: str) -> None:
 313        """Sets the name of the variable."""
 314        self.__helper.set_var_name(self.__index, name)
 315
 316    @property
 317    def lower_bound(self) -> np.double:
 318        """Returns the lower bound of the variable."""
 319        return self.__helper.var_lower_bound(self.__index)
 320
 321    @lower_bound.setter
 322    def lower_bound(self, bound: NumberT) -> None:
 323        """Sets the lower bound of the variable."""
 324        self.__helper.set_var_lower_bound(self.__index, bound)
 325
 326    @property
 327    def upper_bound(self) -> np.double:
 328        """Returns the upper bound of the variable."""
 329        return self.__helper.var_upper_bound(self.__index)
 330
 331    @upper_bound.setter
 332    def upper_bound(self, bound: NumberT) -> None:
 333        """Sets the upper bound of the variable."""
 334        self.__helper.set_var_upper_bound(self.__index, bound)
 335
 336    @property
 337    def is_integral(self) -> bool:
 338        """Returns whether the variable is integral."""
 339        return self.__helper.var_is_integral(self.__index)
 340
 341    @is_integral.setter
 342    def integrality(self, is_integral: bool) -> None:
 343        """Sets the integrality of the variable."""
 344        self.__helper.set_var_integrality(self.__index, is_integral)
 345
 346    @property
 347    def objective_coefficient(self) -> NumberT:
 348        return self.__helper.var_objective_coefficient(self.__index)
 349
 350    @objective_coefficient.setter
 351    def objective_coefficient(self, coeff: NumberT) -> None:
 352        self.__helper.set_var_objective_coefficient(self.__index, coeff)
 353
 354    def __eq__(self, arg: Optional[LinearExprT]) -> ConstraintT:
 355        if arg is None:
 356            return False
 357        if isinstance(arg, Variable):
 358            return VarEqVar(self, arg)
 359        return BoundedLinearExpression(
 360            self - arg, 0.0, 0.0
 361        )  # pytype: disable=wrong-arg-types  # numpy-scalars
 362
 363    def __hash__(self):
 364        return hash((self.__helper, self.__index))
 365
 366
 367class _BoundedLinearExpr(metaclass=abc.ABCMeta):
 368    """Interface for types that can build bounded linear (boolean) expressions.
 369
 370    Classes derived from _BoundedLinearExpr are used to build linear constraints
 371    to be satisfied.
 372
 373      * BoundedLinearExpression: a linear expression with upper and lower bounds.
 374      * VarEqVar: an equality comparison between two variables.
 375    """
 376
 377    @abc.abstractmethod
 378    def _add_linear_constraint(
 379        self, helper: mbh.ModelBuilderHelper, name: str
 380    ) -> "LinearConstraint":
 381        """Creates a new linear constraint in the helper.
 382
 383        Args:
 384          helper (mbh.ModelBuilderHelper): The helper to create the constraint.
 385          name (str): The name of the linear constraint.
 386
 387        Returns:
 388          LinearConstraint: A reference to the linear constraint in the helper.
 389        """
 390
 391    @abc.abstractmethod
 392    def _add_enforced_linear_constraint(
 393        self,
 394        helper: mbh.ModelBuilderHelper,
 395        var: Variable,
 396        value: bool,
 397        name: str,
 398    ) -> "EnforcedLinearConstraint":
 399        """Creates a new enforced linear constraint in the helper.
 400
 401        Args:
 402          helper (mbh.ModelBuilderHelper): The helper to create the constraint.
 403          var (Variable): The indicator variable of the constraint.
 404          value (bool): The indicator value of the constraint.
 405          name (str): The name of the linear constraint.
 406
 407        Returns:
 408          Enforced LinearConstraint: A reference to the linear constraint in the
 409          helper.
 410        """
 411
 412
 413def _add_linear_constraint_to_helper(
 414    bounded_expr: Union[bool, _BoundedLinearExpr],
 415    helper: mbh.ModelBuilderHelper,
 416    name: Optional[str],
 417):
 418    """Creates a new linear constraint in the helper.
 419
 420    It handles boolean values (which might arise in the construction of
 421    BoundedLinearExpressions).
 422
 423    If bounded_expr is a Boolean value, the created constraint is different.
 424    In that case, the constraint will be immutable and marked as under-specified.
 425    It will be always feasible or infeasible whether the value is True or False.
 426
 427    Args:
 428      bounded_expr: The bounded expression used to create the constraint.
 429      helper: The helper to create the constraint.
 430      name: The name of the constraint to be created.
 431
 432    Returns:
 433      LinearConstraint: a constraint in the helper corresponding to the input.
 434
 435    Raises:
 436      TypeError: If constraint is an invalid type.
 437    """
 438    if isinstance(bounded_expr, bool):
 439        c = LinearConstraint(helper, is_under_specified=True)
 440        if name is not None:
 441            helper.set_constraint_name(c.index, name)
 442        if bounded_expr:
 443            # constraint that is always feasible: 0.0 <= nothing <= 0.0
 444            helper.set_constraint_lower_bound(c.index, 0.0)
 445            helper.set_constraint_upper_bound(c.index, 0.0)
 446        else:
 447            # constraint that is always infeasible: +oo <= nothing <= -oo
 448            helper.set_constraint_lower_bound(c.index, 1)
 449            helper.set_constraint_upper_bound(c.index, -1)
 450        return c
 451    if isinstance(bounded_expr, _BoundedLinearExpr):
 452        # pylint: disable=protected-access
 453        return bounded_expr._add_linear_constraint(helper, name)
 454    raise TypeError("invalid type={}".format(type(bounded_expr)))
 455
 456
 457def _add_enforced_linear_constraint_to_helper(
 458    bounded_expr: Union[bool, _BoundedLinearExpr],
 459    helper: mbh.ModelBuilderHelper,
 460    var: Variable,
 461    value: bool,
 462    name: Optional[str],
 463):
 464    """Creates a new enforced linear constraint in the helper.
 465
 466    It handles boolean values (which might arise in the construction of
 467    BoundedLinearExpressions).
 468
 469    If bounded_expr is a Boolean value, the linear part of the constraint is
 470    different.
 471    In that case, the constraint will be immutable and marked as under-specified.
 472    Its linear part will be always feasible or infeasible whether the value is
 473    True or False.
 474
 475    Args:
 476      bounded_expr: The bounded expression used to create the constraint.
 477      helper: The helper to create the constraint.
 478      var: the variable used in the indicator
 479      value: the value used in the indicator
 480      name: The name of the constraint to be created.
 481
 482    Returns:
 483      EnforcedLinearConstraint: a constraint in the helper corresponding to the
 484      input.
 485
 486    Raises:
 487      TypeError: If constraint is an invalid type.
 488    """
 489    if isinstance(bounded_expr, bool):
 490        # TODO(user): create indicator variable assignment instead ?
 491        c = EnforcedLinearConstraint(helper, is_under_specified=True)
 492        c.indicator_variable = var
 493        c.indicator_value = value
 494        if name is not None:
 495            helper.set_enforced_constraint_name(c.index, name)
 496        if bounded_expr:
 497            # constraint that is always feasible: 0.0 <= nothing <= 0.0
 498            helper.set_enforced_constraint_lower_bound(c.index, 0.0)
 499            helper.set_enforced_constraint_upper_bound(c.index, 0.0)
 500        else:
 501            # constraint that is always infeasible: +oo <= nothing <= -oo
 502            helper.set_enforced_constraint_lower_bound(c.index, 1)
 503            helper.set_enforced_constraint_upper_bound(c.index, -1)
 504        return c
 505    if isinstance(bounded_expr, _BoundedLinearExpr):
 506        # pylint: disable=protected-access
 507        return bounded_expr._add_enforced_linear_constraint(helper, var, value, name)
 508    raise TypeError("invalid type={}".format(type(bounded_expr)))
 509
 510
 511@dataclasses.dataclass(repr=False, eq=False, frozen=True)
 512class VarEqVar(_BoundedLinearExpr):
 513    """Represents var == var."""
 514
 515    __slots__ = ("left", "right")
 516
 517    left: Variable
 518    right: Variable
 519
 520    def __str__(self):
 521        return f"{self.left} == {self.right}"
 522
 523    def __repr__(self):
 524        return self.__str__()
 525
 526    def __bool__(self) -> bool:
 527        return hash(self.left) == hash(self.right)
 528
 529    def _add_linear_constraint(
 530        self, helper: mbh.ModelBuilderHelper, name: str
 531    ) -> "LinearConstraint":
 532        c = LinearConstraint(helper)
 533        helper.set_constraint_lower_bound(c.index, 0.0)
 534        helper.set_constraint_upper_bound(c.index, 0.0)
 535        # pylint: disable=protected-access
 536        helper.add_term_to_constraint(c.index, self.left.index, 1.0)
 537        helper.add_term_to_constraint(c.index, self.right.index, -1.0)
 538        # pylint: enable=protected-access
 539        helper.set_constraint_name(c.index, name)
 540        return c
 541
 542    def _add_enforced_linear_constraint(
 543        self,
 544        helper: mbh.ModelBuilderHelper,
 545        var: Variable,
 546        value: bool,
 547        name: str,
 548    ) -> "EnforcedLinearConstraint":
 549        """Adds an enforced linear constraint to the model."""
 550        c = EnforcedLinearConstraint(helper)
 551        c.indicator_variable = var
 552        c.indicator_value = value
 553        helper.set_enforced_constraint_lower_bound(c.index, 0.0)
 554        helper.set_enforced_constraint_upper_bound(c.index, 0.0)
 555        # pylint: disable=protected-access
 556        helper.add_term_to_enforced_constraint(c.index, self.left.index, 1.0)
 557        helper.add_term_to_enforced_constraint(c.index, self.right.index, -1.0)
 558        # pylint: enable=protected-access
 559        helper.set_enforced_constraint_name(c.index, name)
 560        return c
 561
 562
 563class BoundedLinearExpression(_BoundedLinearExpr):
 564    """Represents a linear constraint: `lb <= linear expression <= ub`.
 565
 566    The only use of this class is to be added to the Model through
 567    `Model.add(bounded expression)`, as in:
 568
 569        model.Add(x + 2 * y -1 >= z)
 570    """
 571
 572    def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT) -> None:
 573        self.__expr: LinearExprT = expr
 574        self.__lb: np.double = mbn.assert_is_a_number(lb)
 575        self.__ub: np.double = mbn.assert_is_a_number(ub)
 576
 577    def __str__(self) -> str:
 578        if self.__lb > -math.inf and self.__ub < math.inf:
 579            if self.__lb == self.__ub:
 580                return f"{self.__expr} == {self.__lb}"
 581            else:
 582                return f"{self.__lb} <= {self.__expr} <= {self.__ub}"
 583        elif self.__lb > -math.inf:
 584            return f"{self.__expr} >= {self.__lb}"
 585        elif self.__ub < math.inf:
 586            return f"{self.__expr} <= {self.__ub}"
 587        else:
 588            return f"{self.__expr} free"
 589
 590    def __repr__(self):
 591        return self.__str__()
 592
 593    @property
 594    def expression(self) -> LinearExprT:
 595        return self.__expr
 596
 597    @property
 598    def lower_bound(self) -> np.double:
 599        return self.__lb
 600
 601    @property
 602    def upper_bound(self) -> np.double:
 603        return self.__ub
 604
 605    def __bool__(self) -> bool:
 606        raise NotImplementedError(
 607            f"Cannot use a BoundedLinearExpression {self} as a Boolean value"
 608        )
 609
 610    def _add_linear_constraint(
 611        self, helper: mbh.ModelBuilderHelper, name: Optional[str]
 612    ) -> "LinearConstraint":
 613        c = LinearConstraint(helper)
 614        flat_expr = _as_flat_linear_expression(self.__expr)
 615        # pylint: disable=protected-access
 616        helper.add_terms_to_constraint(
 617            c.index, flat_expr._variable_indices, flat_expr._coefficients
 618        )
 619        helper.set_constraint_lower_bound(c.index, self.__lb - flat_expr._offset)
 620        helper.set_constraint_upper_bound(c.index, self.__ub - flat_expr._offset)
 621        # pylint: enable=protected-access
 622        if name is not None:
 623            helper.set_constraint_name(c.index, name)
 624        return c
 625
 626    def _add_enforced_linear_constraint(
 627        self,
 628        helper: mbh.ModelBuilderHelper,
 629        var: Variable,
 630        value: bool,
 631        name: Optional[str],
 632    ) -> "EnforcedLinearConstraint":
 633        """Adds an enforced linear constraint to the model."""
 634        c = EnforcedLinearConstraint(helper)
 635        c.indicator_variable = var
 636        c.indicator_value = value
 637        flat_expr = _as_flat_linear_expression(self.__expr)
 638        # pylint: disable=protected-access
 639        helper.add_terms_to_enforced_constraint(
 640            c.index, flat_expr._variable_indices, flat_expr._coefficients
 641        )
 642        helper.set_enforced_constraint_lower_bound(
 643            c.index, self.__lb - flat_expr._offset
 644        )
 645        helper.set_enforced_constraint_upper_bound(
 646            c.index, self.__ub - flat_expr._offset
 647        )
 648        # pylint: enable=protected-access
 649        if name is not None:
 650            helper.set_enforced_constraint_name(c.index, name)
 651        return c
 652
 653
 654class LinearConstraint:
 655    """Stores a linear equation.
 656
 657    Example:
 658        x = model.new_num_var(0, 10, 'x')
 659        y = model.new_num_var(0, 10, 'y')
 660
 661        linear_constraint = model.add(x + 2 * y == 5)
 662    """
 663
 664    def __init__(
 665        self,
 666        helper: mbh.ModelBuilderHelper,
 667        *,
 668        index: Optional[IntegerT] = None,
 669        is_under_specified: bool = False,
 670    ) -> None:
 671        """LinearConstraint constructor.
 672
 673        Args:
 674          helper: The pybind11 ModelBuilderHelper.
 675          index: If specified, recreates a wrapper to an existing linear constraint.
 676          is_under_specified: indicates if the constraint was created by
 677            model.add(bool).
 678        """
 679        if index is None:
 680            self.__index = helper.add_linear_constraint()
 681        else:
 682            self.__index = index
 683        self.__helper: mbh.ModelBuilderHelper = helper
 684        self.__is_under_specified = is_under_specified
 685
 686    @property
 687    def index(self) -> IntegerT:
 688        """Returns the index of the constraint in the helper."""
 689        return self.__index
 690
 691    @property
 692    def helper(self) -> mbh.ModelBuilderHelper:
 693        """Returns the ModelBuilderHelper instance."""
 694        return self.__helper
 695
 696    @property
 697    def lower_bound(self) -> np.double:
 698        return self.__helper.constraint_lower_bound(self.__index)
 699
 700    @lower_bound.setter
 701    def lower_bound(self, bound: NumberT) -> None:
 702        self.assert_constraint_is_well_defined()
 703        self.__helper.set_constraint_lower_bound(self.__index, bound)
 704
 705    @property
 706    def upper_bound(self) -> np.double:
 707        return self.__helper.constraint_upper_bound(self.__index)
 708
 709    @upper_bound.setter
 710    def upper_bound(self, bound: NumberT) -> None:
 711        self.assert_constraint_is_well_defined()
 712        self.__helper.set_constraint_upper_bound(self.__index, bound)
 713
 714    @property
 715    def name(self) -> str:
 716        constraint_name = self.__helper.constraint_name(self.__index)
 717        if constraint_name:
 718            return constraint_name
 719        return f"linear_constraint#{self.__index}"
 720
 721    @name.setter
 722    def name(self, name: str) -> None:
 723        return self.__helper.set_constraint_name(self.__index, name)
 724
 725    @property
 726    def is_under_specified(self) -> bool:
 727        """Returns True if the constraint is under specified.
 728
 729        Usually, it means that it was created by model.add(False) or model.add(True)
 730        The effect is that modifying the constraint will raise an exception.
 731        """
 732        return self.__is_under_specified
 733
 734    def assert_constraint_is_well_defined(self) -> None:
 735        """Raises an exception if the constraint is under specified."""
 736        if self.__is_under_specified:
 737            raise ValueError(
 738                f"Constraint {self.index} is under specified and cannot be modified"
 739            )
 740
 741    def __str__(self):
 742        return self.name
 743
 744    def __repr__(self):
 745        return (
 746            f"LinearConstraint({self.name}, lb={self.lower_bound},"
 747            f" ub={self.upper_bound},"
 748            f" var_indices={self.helper.constraint_var_indices(self.index)},"
 749            f" coefficients={self.helper.constraint_coefficients(self.index)})"
 750        )
 751
 752    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
 753        """Sets the coefficient of the variable in the constraint."""
 754        self.assert_constraint_is_well_defined()
 755        self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
 756
 757    def add_term(self, var: Variable, coeff: NumberT) -> None:
 758        """Adds var * coeff to the constraint."""
 759        self.assert_constraint_is_well_defined()
 760        self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)
 761
 762    def clear_terms(self) -> None:
 763        """Clear all terms of the constraint."""
 764        self.assert_constraint_is_well_defined()
 765        self.__helper.clear_constraint_terms(self.__index)
 766
 767
 768class EnforcedLinearConstraint:
 769    """Stores an enforced linear equation, also name indicator constraint.
 770
 771    Example:
 772        x = model.new_num_var(0, 10, 'x')
 773        y = model.new_num_var(0, 10, 'y')
 774        z = model.new_bool_var('z')
 775
 776        enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)
 777    """
 778
 779    def __init__(
 780        self,
 781        helper: mbh.ModelBuilderHelper,
 782        *,
 783        index: Optional[IntegerT] = None,
 784        is_under_specified: bool = False,
 785    ) -> None:
 786        """EnforcedLinearConstraint constructor.
 787
 788        Args:
 789          helper: The pybind11 ModelBuilderHelper.
 790          index: If specified, recreates a wrapper to an existing linear constraint.
 791          is_under_specified: indicates if the constraint was created by
 792            model.add(bool).
 793        """
 794        if index is None:
 795            self.__index = helper.add_enforced_linear_constraint()
 796        else:
 797            if not helper.is_enforced_linear_constraint(index):
 798                raise ValueError(
 799                    f"the given index {index} does not refer to an enforced linear"
 800                    " constraint"
 801                )
 802
 803            self.__index = index
 804        self.__helper: mbh.ModelBuilderHelper = helper
 805        self.__is_under_specified = is_under_specified
 806
 807    @property
 808    def index(self) -> IntegerT:
 809        """Returns the index of the constraint in the helper."""
 810        return self.__index
 811
 812    @property
 813    def helper(self) -> mbh.ModelBuilderHelper:
 814        """Returns the ModelBuilderHelper instance."""
 815        return self.__helper
 816
 817    @property
 818    def lower_bound(self) -> np.double:
 819        return self.__helper.enforced_constraint_lower_bound(self.__index)
 820
 821    @lower_bound.setter
 822    def lower_bound(self, bound: NumberT) -> None:
 823        self.assert_constraint_is_well_defined()
 824        self.__helper.set_enforced_constraint_lower_bound(self.__index, bound)
 825
 826    @property
 827    def upper_bound(self) -> np.double:
 828        return self.__helper.enforced_constraint_upper_bound(self.__index)
 829
 830    @upper_bound.setter
 831    def upper_bound(self, bound: NumberT) -> None:
 832        self.assert_constraint_is_well_defined()
 833        self.__helper.set_enforced_constraint_upper_bound(self.__index, bound)
 834
 835    @property
 836    def indicator_variable(self) -> "Variable":
 837        enforcement_var_index = (
 838            self.__helper.enforced_constraint_indicator_variable_index(self.__index)
 839        )
 840        return Variable(self.__helper, enforcement_var_index, None, None, None)
 841
 842    @indicator_variable.setter
 843    def indicator_variable(self, var: "Variable") -> None:
 844        self.__helper.set_enforced_constraint_indicator_variable_index(
 845            self.__index, var.index
 846        )
 847
 848    @property
 849    def indicator_value(self) -> bool:
 850        return self.__helper.enforced_constraint_indicator_value(self.__index)
 851
 852    @indicator_value.setter
 853    def indicator_value(self, value: bool) -> None:
 854        self.__helper.set_enforced_constraint_indicator_value(self.__index, value)
 855
 856    @property
 857    def name(self) -> str:
 858        constraint_name = self.__helper.enforced_constraint_name(self.__index)
 859        if constraint_name:
 860            return constraint_name
 861        return f"enforced_linear_constraint#{self.__index}"
 862
 863    @name.setter
 864    def name(self, name: str) -> None:
 865        return self.__helper.set_enforced_constraint_name(self.__index, name)
 866
 867    @property
 868    def is_under_specified(self) -> bool:
 869        """Returns True if the constraint is under specified.
 870
 871        Usually, it means that it was created by model.add(False) or model.add(True)
 872        The effect is that modifying the constraint will raise an exception.
 873        """
 874        return self.__is_under_specified
 875
 876    def assert_constraint_is_well_defined(self) -> None:
 877        """Raises an exception if the constraint is under specified."""
 878        if self.__is_under_specified:
 879            raise ValueError(
 880                f"Constraint {self.index} is under specified and cannot be modified"
 881            )
 882
 883    def __str__(self):
 884        return self.name
 885
 886    def __repr__(self):
 887        return (
 888            f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound},"
 889            f" ub={self.upper_bound},"
 890            f" var_indices={self.helper.enforced_constraint_var_indices(self.index)},"
 891            f" coefficients={self.helper.enforced_constraint_coefficients(self.index)},"
 892            f" indicator_variable={self.indicator_variable}"
 893            f" indicator_value={self.indicator_value})"
 894        )
 895
 896    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
 897        """Sets the coefficient of the variable in the constraint."""
 898        self.assert_constraint_is_well_defined()
 899        self.__helper.set_enforced_constraint_coefficient(
 900            self.__index, var.index, coeff
 901        )
 902
 903    def add_term(self, var: Variable, coeff: NumberT) -> None:
 904        """Adds var * coeff to the constraint."""
 905        self.assert_constraint_is_well_defined()
 906        self.__helper.safe_add_term_to_enforced_constraint(
 907            self.__index, var.index, coeff
 908        )
 909
 910    def clear_terms(self) -> None:
 911        """Clear all terms of the constraint."""
 912        self.assert_constraint_is_well_defined()
 913        self.__helper.clear_enforced_constraint_terms(self.__index)
 914
 915
 916class Model:
 917    """Methods for building a linear model.
 918
 919    Methods beginning with:
 920
 921    * ```new_``` create integer, boolean, or interval variables.
 922    * ```add_``` create new constraints and add them to the model.
 923    """
 924
 925    def __init__(self):
 926        self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper()
 927
 928    def clone(self) -> "Model":
 929        """Returns a clone of the current model."""
 930        clone = Model()
 931        clone.helper.overwrite_model(self.helper)
 932        return clone
 933
 934    @typing.overload
 935    def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: ...
 936
 937    @typing.overload
 938    def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: ...
 939
 940    def _get_linear_constraints(
 941        self, constraints: Optional[_IndexOrSeries] = None
 942    ) -> _IndexOrSeries:
 943        if constraints is None:
 944            return self.get_linear_constraints()
 945        return constraints
 946
 947    @typing.overload
 948    def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: ...
 949
 950    @typing.overload
 951    def _get_variables(self, variables: pd.Series) -> pd.Series: ...
 952
 953    def _get_variables(
 954        self, variables: Optional[_IndexOrSeries] = None
 955    ) -> _IndexOrSeries:
 956        if variables is None:
 957            return self.get_variables()
 958        return variables
 959
 960    def get_linear_constraints(self) -> pd.Index:
 961        """Gets all linear constraints in the model."""
 962        return pd.Index(
 963            [self.linear_constraint_from_index(i) for i in range(self.num_constraints)],
 964            name="linear_constraint",
 965        )
 966
 967    def get_linear_constraint_expressions(
 968        self, constraints: Optional[_IndexOrSeries] = None
 969    ) -> pd.Series:
 970        """Gets the expressions of all linear constraints in the set.
 971
 972        If `constraints` is a `pd.Index`, then the output will be indexed by the
 973        constraints. If `constraints` is a `pd.Series` indexed by the underlying
 974        dimensions, then the output will be indexed by the same underlying
 975        dimensions.
 976
 977        Args:
 978          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
 979            constraints from which to get the expressions. If unspecified, all
 980            linear constraints will be in scope.
 981
 982        Returns:
 983          pd.Series: The expressions of all linear constraints in the set.
 984        """
 985        return _attribute_series(
 986            # pylint: disable=g-long-lambda
 987            func=lambda c: _as_flat_linear_expression(
 988                # pylint: disable=g-complex-comprehension
 989                sum(
 990                    coeff * Variable(self.__helper, var_id, None, None, None)
 991                    for var_id, coeff in zip(
 992                        c.helper.constraint_var_indices(c.index),
 993                        c.helper.constraint_coefficients(c.index),
 994                    )
 995                )
 996            ),
 997            values=self._get_linear_constraints(constraints),
 998        )
 999
1000    def get_linear_constraint_lower_bounds(
1001        self, constraints: Optional[_IndexOrSeries] = None
1002    ) -> pd.Series:
1003        """Gets the lower bounds of all linear constraints in the set.
1004
1005        If `constraints` is a `pd.Index`, then the output will be indexed by the
1006        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1007        dimensions, then the output will be indexed by the same underlying
1008        dimensions.
1009
1010        Args:
1011          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1012            constraints from which to get the lower bounds. If unspecified, all
1013            linear constraints will be in scope.
1014
1015        Returns:
1016          pd.Series: The lower bounds of all linear constraints in the set.
1017        """
1018        return _attribute_series(
1019            func=lambda c: c.lower_bound,  # pylint: disable=protected-access
1020            values=self._get_linear_constraints(constraints),
1021        )
1022
1023    def get_linear_constraint_upper_bounds(
1024        self, constraints: Optional[_IndexOrSeries] = None
1025    ) -> pd.Series:
1026        """Gets the upper bounds of all linear constraints in the set.
1027
1028        If `constraints` is a `pd.Index`, then the output will be indexed by the
1029        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1030        dimensions, then the output will be indexed by the same underlying
1031        dimensions.
1032
1033        Args:
1034          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1035            constraints. If unspecified, all linear constraints will be in scope.
1036
1037        Returns:
1038          pd.Series: The upper bounds of all linear constraints in the set.
1039        """
1040        return _attribute_series(
1041            func=lambda c: c.upper_bound,  # pylint: disable=protected-access
1042            values=self._get_linear_constraints(constraints),
1043        )
1044
1045    def get_variables(self) -> pd.Index:
1046        """Gets all variables in the model."""
1047        return pd.Index(
1048            [self.var_from_index(i) for i in range(self.num_variables)],
1049            name="variable",
1050        )
1051
1052    def get_variable_lower_bounds(
1053        self, variables: Optional[_IndexOrSeries] = None
1054    ) -> pd.Series:
1055        """Gets the lower bounds of all variables in the set.
1056
1057        If `variables` is a `pd.Index`, then the output will be indexed by the
1058        variables. If `variables` is a `pd.Series` indexed by the underlying
1059        dimensions, then the output will be indexed by the same underlying
1060        dimensions.
1061
1062        Args:
1063          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
1064            from which to get the lower bounds. If unspecified, all variables will
1065            be in scope.
1066
1067        Returns:
1068          pd.Series: The lower bounds of all variables in the set.
1069        """
1070        return _attribute_series(
1071            func=lambda v: v.lower_bound,  # pylint: disable=protected-access
1072            values=self._get_variables(variables),
1073        )
1074
1075    def get_variable_upper_bounds(
1076        self, variables: Optional[_IndexOrSeries] = None
1077    ) -> pd.Series:
1078        """Gets the upper bounds of all variables in the set.
1079
1080        Args:
1081          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
1082            from which to get the upper bounds. If unspecified, all variables will
1083            be in scope.
1084
1085        Returns:
1086          pd.Series: The upper bounds of all variables in the set.
1087        """
1088        return _attribute_series(
1089            func=lambda v: v.upper_bound,  # pylint: disable=protected-access
1090            values=self._get_variables(variables),
1091        )
1092
1093    # Integer variable.
1094
1095    def new_var(
1096        self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str]
1097    ) -> Variable:
1098        """Create an integer variable with domain [lb, ub].
1099
1100        Args:
1101          lb: Lower bound of the variable.
1102          ub: Upper bound of the variable.
1103          is_integer: Indicates if the variable must take integral values.
1104          name: The name of the variable.
1105
1106        Returns:
1107          a variable whose domain is [lb, ub].
1108        """
1109
1110        return Variable(self.__helper, lb, ub, is_integer, name)
1111
1112    def new_int_var(
1113        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
1114    ) -> Variable:
1115        """Create an integer variable with domain [lb, ub].
1116
1117        Args:
1118          lb: Lower bound of the variable.
1119          ub: Upper bound of the variable.
1120          name: The name of the variable.
1121
1122        Returns:
1123          a variable whose domain is [lb, ub].
1124        """
1125
1126        return self.new_var(lb, ub, True, name)
1127
1128    def new_num_var(
1129        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
1130    ) -> Variable:
1131        """Create an integer variable with domain [lb, ub].
1132
1133        Args:
1134          lb: Lower bound of the variable.
1135          ub: Upper bound of the variable.
1136          name: The name of the variable.
1137
1138        Returns:
1139          a variable whose domain is [lb, ub].
1140        """
1141
1142        return self.new_var(lb, ub, False, name)
1143
1144    def new_bool_var(self, name: Optional[str] = None) -> Variable:
1145        """Creates a 0-1 variable with the given name."""
1146        return self.new_var(
1147            0, 1, True, name
1148        )  # pytype: disable=wrong-arg-types  # numpy-scalars
1149
1150    def new_constant(self, value: NumberT) -> Variable:
1151        """Declares a constant variable."""
1152        return self.new_var(value, value, False, None)
1153
1154    def new_var_series(
1155        self,
1156        name: str,
1157        index: pd.Index,
1158        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1159        upper_bounds: Union[NumberT, pd.Series] = math.inf,
1160        is_integral: Union[bool, pd.Series] = False,
1161    ) -> pd.Series:
1162        """Creates a series of (scalar-valued) variables with the given name.
1163
1164        Args:
1165          name (str): Required. The name of the variable set.
1166          index (pd.Index): Required. The index to use for the variable set.
1167          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1168            variables in the set. If a `pd.Series` is passed in, it will be based on
1169            the corresponding values of the pd.Series. Defaults to -inf.
1170          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1171            variables in the set. If a `pd.Series` is passed in, it will be based on
1172            the corresponding values of the pd.Series. Defaults to +inf.
1173          is_integral (bool, pd.Series): Optional. Indicates if the variable can
1174            only take integer values. If a `pd.Series` is passed in, it will be
1175            based on the corresponding values of the pd.Series. Defaults to False.
1176
1177        Returns:
1178          pd.Series: The variable set indexed by its corresponding dimensions.
1179
1180        Raises:
1181          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1182          ValueError: if the `name` is not a valid identifier or already exists.
1183          ValueError: if the `lowerbound` is greater than the `upperbound`.
1184          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1185          does not match the input index.
1186        """
1187        if not isinstance(index, pd.Index):
1188            raise TypeError("Non-index object is used as index")
1189        if not name.isidentifier():
1190            raise ValueError("name={} is not a valid identifier".format(name))
1191        if (
1192            mbn.is_a_number(lower_bounds)
1193            and mbn.is_a_number(upper_bounds)
1194            and lower_bounds > upper_bounds
1195        ):
1196            raise ValueError(
1197                "lower_bound={} is greater than upper_bound={} for variable set={}".format(
1198                    lower_bounds, upper_bounds, name
1199                )
1200            )
1201        if (
1202            isinstance(is_integral, bool)
1203            and is_integral
1204            and mbn.is_a_number(lower_bounds)
1205            and mbn.is_a_number(upper_bounds)
1206            and math.isfinite(lower_bounds)
1207            and math.isfinite(upper_bounds)
1208            and math.ceil(lower_bounds) > math.floor(upper_bounds)
1209        ):
1210            raise ValueError(
1211                "ceil(lower_bound={})={}".format(lower_bounds, math.ceil(lower_bounds))
1212                + " is greater than floor("
1213                + "upper_bound={})={}".format(upper_bounds, math.floor(upper_bounds))
1214                + " for variable set={}".format(name)
1215            )
1216        lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
1217        upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
1218        is_integrals = _convert_to_series_and_validate_index(is_integral, index)
1219        return pd.Series(
1220            index=index,
1221            data=[
1222                # pylint: disable=g-complex-comprehension
1223                Variable(
1224                    helper=self.__helper,
1225                    name=f"{name}[{i}]",
1226                    lb=lower_bounds[i],
1227                    ub=upper_bounds[i],
1228                    is_integral=is_integrals[i],
1229                )
1230                for i in index
1231            ],
1232        )
1233
1234    def new_num_var_series(
1235        self,
1236        name: str,
1237        index: pd.Index,
1238        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1239        upper_bounds: Union[NumberT, pd.Series] = math.inf,
1240    ) -> pd.Series:
1241        """Creates a series of continuous variables with the given name.
1242
1243        Args:
1244          name (str): Required. The name of the variable set.
1245          index (pd.Index): Required. The index to use for the variable set.
1246          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1247            variables in the set. If a `pd.Series` is passed in, it will be based on
1248            the corresponding values of the pd.Series. Defaults to -inf.
1249          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1250            variables in the set. If a `pd.Series` is passed in, it will be based on
1251            the corresponding values of the pd.Series. Defaults to +inf.
1252
1253        Returns:
1254          pd.Series: The variable set indexed by its corresponding dimensions.
1255
1256        Raises:
1257          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1258          ValueError: if the `name` is not a valid identifier or already exists.
1259          ValueError: if the `lowerbound` is greater than the `upperbound`.
1260          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1261          does not match the input index.
1262        """
1263        return self.new_var_series(name, index, lower_bounds, upper_bounds, False)
1264
1265    def new_int_var_series(
1266        self,
1267        name: str,
1268        index: pd.Index,
1269        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1270        upper_bounds: Union[NumberT, pd.Series] = math.inf,
1271    ) -> pd.Series:
1272        """Creates a series of integer variables with the given name.
1273
1274        Args:
1275          name (str): Required. The name of the variable set.
1276          index (pd.Index): Required. The index to use for the variable set.
1277          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1278            variables in the set. If a `pd.Series` is passed in, it will be based on
1279            the corresponding values of the pd.Series. Defaults to -inf.
1280          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1281            variables in the set. If a `pd.Series` is passed in, it will be based on
1282            the corresponding values of the pd.Series. Defaults to +inf.
1283
1284        Returns:
1285          pd.Series: The variable set indexed by its corresponding dimensions.
1286
1287        Raises:
1288          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1289          ValueError: if the `name` is not a valid identifier or already exists.
1290          ValueError: if the `lowerbound` is greater than the `upperbound`.
1291          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1292          does not match the input index.
1293        """
1294        return self.new_var_series(name, index, lower_bounds, upper_bounds, True)
1295
1296    def new_bool_var_series(
1297        self,
1298        name: str,
1299        index: pd.Index,
1300    ) -> pd.Series:
1301        """Creates a series of Boolean variables with the given name.
1302
1303        Args:
1304          name (str): Required. The name of the variable set.
1305          index (pd.Index): Required. The index to use for the variable set.
1306
1307        Returns:
1308          pd.Series: The variable set indexed by its corresponding dimensions.
1309
1310        Raises:
1311          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1312          ValueError: if the `name` is not a valid identifier or already exists.
1313          ValueError: if the `lowerbound` is greater than the `upperbound`.
1314          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1315          does not match the input index.
1316        """
1317        return self.new_var_series(name, index, 0, 1, True)
1318
1319    def var_from_index(self, index: IntegerT) -> Variable:
1320        """Rebuilds a variable object from the model and its index."""
1321        return Variable(self.__helper, index, None, None, None)
1322
1323    # Linear constraints.
1324
1325    def add_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
1326        self,
1327        linear_expr: LinearExprT,
1328        lb: NumberT = -math.inf,
1329        ub: NumberT = math.inf,
1330        name: Optional[str] = None,
1331    ) -> LinearConstraint:
1332        """Adds the constraint: `lb <= linear_expr <= ub` with the given name."""
1333        ct = LinearConstraint(self.__helper)
1334        if name:
1335            self.__helper.set_constraint_name(ct.index, name)
1336        if mbn.is_a_number(linear_expr):
1337            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
1338            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
1339        elif isinstance(linear_expr, Variable):
1340            self.__helper.set_constraint_lower_bound(ct.index, lb)
1341            self.__helper.set_constraint_upper_bound(ct.index, ub)
1342            self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
1343        elif isinstance(linear_expr, LinearExpr):
1344            flat_expr = _as_flat_linear_expression(linear_expr)
1345            # pylint: disable=protected-access
1346            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset)
1347            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset)
1348            self.__helper.add_terms_to_constraint(
1349                ct.index, flat_expr._variable_indices, flat_expr._coefficients
1350            )
1351        else:
1352            raise TypeError(
1353                f"Not supported: Model.add_linear_constraint({linear_expr})"
1354                f" with type {type(linear_expr)}"
1355            )
1356        return ct
1357
1358    def add(
1359        self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None
1360    ) -> Union[LinearConstraint, pd.Series]:
1361        """Adds a `BoundedLinearExpression` to the model.
1362
1363        Args:
1364          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1365          name: An optional name.
1366
1367        Returns:
1368          An instance of the `Constraint` class.
1369
1370        Note that a special treatment is done when the argument does not contain any
1371        variable, and thus evaluates to True or False.
1372
1373        `model.add(True)` will create a constraint 0 <= empty sum <= 0.
1374        The constraint will be marked as under specified, and cannot be modified
1375        thereafter.
1376
1377        `model.add(False)` will create a constraint inf <= empty sum <= -inf. The
1378        constraint will be marked as under specified, and cannot be modified
1379        thereafter.
1380
1381        you can check the if a constraint is under specified by reading the
1382        `LinearConstraint.is_under_specified` property.
1383        """
1384        if isinstance(ct, _BoundedLinearExpr):
1385            return ct._add_linear_constraint(self.__helper, name)
1386        elif isinstance(ct, bool):
1387            return _add_linear_constraint_to_helper(ct, self.__helper, name)
1388        elif isinstance(ct, pd.Series):
1389            return pd.Series(
1390                index=ct.index,
1391                data=[
1392                    _add_linear_constraint_to_helper(
1393                        expr, self.__helper, f"{name}[{i}]"
1394                    )
1395                    for (i, expr) in zip(ct.index, ct)
1396                ],
1397            )
1398        else:
1399            raise TypeError("Not supported: Model.add(" + str(ct) + ")")
1400
1401    def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint:
1402        """Rebuilds a linear constraint object from the model and its index."""
1403        return LinearConstraint(self.__helper, index=index)
1404
1405    # EnforcedLinear constraints.
1406
1407    def add_enforced_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
1408        self,
1409        linear_expr: LinearExprT,
1410        ivar: "Variable",
1411        ivalue: bool,
1412        lb: NumberT = -math.inf,
1413        ub: NumberT = math.inf,
1414        name: Optional[str] = None,
1415    ) -> EnforcedLinearConstraint:
1416        """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name."""
1417        ct = EnforcedLinearConstraint(self.__helper)
1418        ct.indicator_variable = ivar
1419        ct.indicator_value = ivalue
1420        if name:
1421            self.__helper.set_constraint_name(ct.index, name)
1422        if mbn.is_a_number(linear_expr):
1423            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
1424            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
1425        elif isinstance(linear_expr, Variable):
1426            self.__helper.set_constraint_lower_bound(ct.index, lb)
1427            self.__helper.set_constraint_upper_bound(ct.index, ub)
1428            self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
1429        elif isinstance(linear_expr, LinearExpr):
1430            flat_expr = _as_flat_linear_expression(linear_expr)
1431            # pylint: disable=protected-access
1432            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset)
1433            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset)
1434            self.__helper.add_terms_to_constraint(
1435                ct.index, flat_expr._variable_indices, flat_expr._coefficients
1436            )
1437        else:
1438            raise TypeError(
1439                "Not supported:"
1440                f" Model.add_enforced_linear_constraint({linear_expr}) with"
1441                f" type {type(linear_expr)}"
1442            )
1443        return ct
1444
1445    def add_enforced(
1446        self,
1447        ct: Union[ConstraintT, pd.Series],
1448        var: Union[Variable, pd.Series],
1449        value: Union[bool, pd.Series],
1450        name: Optional[str] = None,
1451    ) -> Union[EnforcedLinearConstraint, pd.Series]:
1452        """Adds a `ivar == ivalue => BoundedLinearExpression` to the model.
1453
1454        Args:
1455          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1456          var: The indicator variable
1457          value: the indicator value
1458          name: An optional name.
1459
1460        Returns:
1461          An instance of the `Constraint` class.
1462
1463        Note that a special treatment is done when the argument does not contain any
1464        variable, and thus evaluates to True or False.
1465
1466        model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty
1467        sum <= 0
1468
1469        model.add_enforced(False, var, value) will create a constraint inf <=
1470        empty sum <= -inf
1471
1472        you can check the if a constraint is always false (lb=inf, ub=-inf) by
1473        calling EnforcedLinearConstraint.is_always_false()
1474        """
1475        if isinstance(ct, _BoundedLinearExpr):
1476            return ct._add_enforced_linear_constraint(self.__helper, var, value, name)
1477        elif (
1478            isinstance(ct, bool)
1479            and isinstance(var, Variable)
1480            and isinstance(value, bool)
1481        ):
1482            return _add_enforced_linear_constraint_to_helper(
1483                ct, self.__helper, var, value, name
1484            )
1485        elif isinstance(ct, pd.Series):
1486            ivar_series = _convert_to_var_series_and_validate_index(var, ct.index)
1487            ivalue_series = _convert_to_series_and_validate_index(value, ct.index)
1488            return pd.Series(
1489                index=ct.index,
1490                data=[
1491                    _add_enforced_linear_constraint_to_helper(
1492                        expr,
1493                        self.__helper,
1494                        ivar_series[i],
1495                        ivalue_series[i],
1496                        f"{name}[{i}]",
1497                    )
1498                    for (i, expr) in zip(ct.index, ct)
1499                ],
1500            )
1501        else:
1502            raise TypeError("Not supported: Model.add_enforced(" + str(ct) + ")")
1503
1504    def enforced_linear_constraint_from_index(
1505        self, index: IntegerT
1506    ) -> EnforcedLinearConstraint:
1507        """Rebuilds an enforced linear constraint object from the model and its index."""
1508        return EnforcedLinearConstraint(self.__helper, index=index)
1509
1510    # Objective.
1511    def minimize(self, linear_expr: LinearExprT) -> None:
1512        """Minimizes the given objective."""
1513        self.__optimize(linear_expr, False)
1514
1515    def maximize(self, linear_expr: LinearExprT) -> None:
1516        """Maximizes the given objective."""
1517        self.__optimize(linear_expr, True)
1518
1519    def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None:
1520        """Defines the objective."""
1521        self.helper.clear_objective()
1522        self.__helper.set_maximize(maximize)
1523        if mbn.is_a_number(linear_expr):
1524            self.helper.set_objective_offset(linear_expr)
1525        elif isinstance(linear_expr, Variable):
1526            self.helper.set_var_objective_coefficient(linear_expr.index, 1.0)
1527        elif isinstance(linear_expr, LinearExpr):
1528            flat_expr = _as_flat_linear_expression(linear_expr)
1529            # pylint: disable=protected-access
1530            self.helper.set_objective_offset(flat_expr._offset)
1531            self.helper.set_objective_coefficients(
1532                flat_expr._variable_indices, flat_expr._coefficients
1533            )
1534        else:
1535            raise TypeError(f"Not supported: Model.minimize/maximize({linear_expr})")
1536
1537    @property
1538    def objective_offset(self) -> np.double:
1539        """Returns the fixed offset of the objective."""
1540        return self.__helper.objective_offset()
1541
1542    @objective_offset.setter
1543    def objective_offset(self, value: NumberT) -> None:
1544        self.__helper.set_objective_offset(value)
1545
1546    def objective_expression(self) -> "_LinearExpression":
1547        """Returns the expression to optimize."""
1548        return _as_flat_linear_expression(
1549            sum(
1550                variable * self.__helper.var_objective_coefficient(variable.index)
1551                for variable in self.get_variables()
1552                if self.__helper.var_objective_coefficient(variable.index) != 0.0
1553            )
1554            + self.__helper.objective_offset()
1555        )
1556
1557    # Hints.
1558    def clear_hints(self):
1559        """Clears all solution hints."""
1560        self.__helper.clear_hints()
1561
1562    def add_hint(self, var: Variable, value: NumberT) -> None:
1563        """Adds var == value as a hint to the model.
1564
1565        Args:
1566          var: The variable of the hint
1567          value: The value of the hint
1568
1569        Note that variables must not appear more than once in the list of hints.
1570        """
1571        self.__helper.add_hint(var.index, value)
1572
1573    # Input/Output
1574    def export_to_lp_string(self, obfuscate: bool = False) -> str:
1575        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1576        options.obfuscate = obfuscate
1577        return self.__helper.export_to_lp_string(options)
1578
1579    def export_to_mps_string(self, obfuscate: bool = False) -> str:
1580        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1581        options.obfuscate = obfuscate
1582        return self.__helper.export_to_mps_string(options)
1583
1584    def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool:
1585        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1586        options.obfuscate = obfuscate
1587        return self.__helper.write_to_mps_file(filename, options)
1588
1589    def export_to_proto(self) -> linear_solver_pb2.MPModelProto:
1590        """Exports the optimization model to a ProtoBuf format."""
1591        return mbh.to_mpmodel_proto(self.__helper)
1592
1593    def import_from_mps_string(self, mps_string: str) -> bool:
1594        """Reads a model from a MPS string."""
1595        return self.__helper.import_from_mps_string(mps_string)
1596
1597    def import_from_mps_file(self, mps_file: str) -> bool:
1598        """Reads a model from a .mps file."""
1599        return self.__helper.import_from_mps_file(mps_file)
1600
1601    def import_from_lp_string(self, lp_string: str) -> bool:
1602        """Reads a model from a LP string."""
1603        return self.__helper.import_from_lp_string(lp_string)
1604
1605    def import_from_lp_file(self, lp_file: str) -> bool:
1606        """Reads a model from a .lp file."""
1607        return self.__helper.import_from_lp_file(lp_file)
1608
1609    def import_from_proto_file(self, proto_file: str) -> bool:
1610        """Reads a model from a proto file."""
1611        return self.__helper.read_model_from_proto_file(proto_file)
1612
1613    def export_to_proto_file(self, proto_file: str) -> bool:
1614        """Writes a model to a proto file."""
1615        return self.__helper.write_model_to_proto_file(proto_file)
1616
1617    # Model getters and Setters
1618
1619    @property
1620    def num_variables(self) -> int:
1621        """Returns the number of variables in the model."""
1622        return self.__helper.num_variables()
1623
1624    @property
1625    def num_constraints(self) -> int:
1626        """The number of constraints in the model."""
1627        return self.__helper.num_constraints()
1628
1629    @property
1630    def name(self) -> str:
1631        """The name of the model."""
1632        return self.__helper.name()
1633
1634    @name.setter
1635    def name(self, name: str):
1636        self.__helper.set_name(name)
1637
1638    @property
1639    def helper(self) -> mbh.ModelBuilderHelper:
1640        """Returns the model builder helper."""
1641        return self.__helper
1642
1643
1644class Solver:
1645    """Main solver class.
1646
1647    The purpose of this class is to search for a solution to the model provided
1648    to the solve() method.
1649
1650    Once solve() is called, this class allows inspecting the solution found
1651    with the value() method, as well as general statistics about the solve
1652    procedure.
1653    """
1654
1655    def __init__(self, solver_name: str):
1656        self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name)
1657        self.log_callback: Optional[Callable[[str], None]] = None
1658
1659    def solver_is_supported(self) -> bool:
1660        """Checks whether the requested solver backend was found."""
1661        return self.__solve_helper.solver_is_supported()
1662
1663    # Solver backend and parameters.
1664    def set_time_limit_in_seconds(self, limit: NumberT) -> None:
1665        """Sets a time limit for the solve() call."""
1666        self.__solve_helper.set_time_limit_in_seconds(limit)
1667
1668    def set_solver_specific_parameters(self, parameters: str) -> None:
1669        """Sets parameters specific to the solver backend."""
1670        self.__solve_helper.set_solver_specific_parameters(parameters)
1671
1672    def enable_output(self, enabled: bool) -> None:
1673        """Controls the solver backend logs."""
1674        self.__solve_helper.enable_output(enabled)
1675
1676    def solve(self, model: Model) -> SolveStatus:
1677        """Solves a problem and passes each solution to the callback if not null."""
1678        if self.log_callback is not None:
1679            self.__solve_helper.set_log_callback(self.log_callback)
1680        else:
1681            self.__solve_helper.clear_log_callback()
1682        self.__solve_helper.solve(model.helper)
1683        return SolveStatus(self.__solve_helper.status())
1684
1685    def stop_search(self):
1686        """Stops the current search asynchronously."""
1687        self.__solve_helper.interrupt_solve()
1688
1689    def value(self, expr: LinearExprT) -> np.double:
1690        """Returns the value of a linear expression after solve."""
1691        if not self.__solve_helper.has_solution():
1692            return pd.NA
1693        if mbn.is_a_number(expr):
1694            return expr
1695        elif isinstance(expr, Variable):
1696            return self.__solve_helper.var_value(expr.index)
1697        elif isinstance(expr, LinearExpr):
1698            flat_expr = _as_flat_linear_expression(expr)
1699            return self.__solve_helper.expression_value(
1700                flat_expr._variable_indices,
1701                flat_expr._coefficients,
1702                flat_expr._offset,
1703            )
1704        else:
1705            raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}")
1706
1707    def values(self, variables: _IndexOrSeries) -> pd.Series:
1708        """Returns the values of the input variables.
1709
1710        If `variables` is a `pd.Index`, then the output will be indexed by the
1711        variables. If `variables` is a `pd.Series` indexed by the underlying
1712        dimensions, then the output will be indexed by the same underlying
1713        dimensions.
1714
1715        Args:
1716          variables (Union[pd.Index, pd.Series]): The set of variables from which to
1717            get the values.
1718
1719        Returns:
1720          pd.Series: The values of all variables in the set.
1721        """
1722        if not self.__solve_helper.has_solution():
1723            return _attribute_series(func=lambda v: pd.NA, values=variables)
1724        return _attribute_series(
1725            func=lambda v: self.__solve_helper.var_value(v.index),
1726            values=variables,
1727        )
1728
1729    def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series:
1730        """Returns the reduced cost of the input variables.
1731
1732        If `variables` is a `pd.Index`, then the output will be indexed by the
1733        variables. If `variables` is a `pd.Series` indexed by the underlying
1734        dimensions, then the output will be indexed by the same underlying
1735        dimensions.
1736
1737        Args:
1738          variables (Union[pd.Index, pd.Series]): The set of variables from which to
1739            get the values.
1740
1741        Returns:
1742          pd.Series: The reduced cost of all variables in the set.
1743        """
1744        if not self.__solve_helper.has_solution():
1745            return _attribute_series(func=lambda v: pd.NA, values=variables)
1746        return _attribute_series(
1747            func=lambda v: self.__solve_helper.reduced_cost(v.index),
1748            values=variables,
1749        )
1750
1751    def reduced_cost(self, var: Variable) -> np.double:
1752        """Returns the reduced cost of a linear expression after solve."""
1753        if not self.__solve_helper.has_solution():
1754            return pd.NA
1755        return self.__solve_helper.reduced_cost(var.index)
1756
1757    def dual_values(self, constraints: _IndexOrSeries) -> pd.Series:
1758        """Returns the dual values of the input constraints.
1759
1760        If `constraints` is a `pd.Index`, then the output will be indexed by the
1761        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1762        dimensions, then the output will be indexed by the same underlying
1763        dimensions.
1764
1765        Args:
1766          constraints (Union[pd.Index, pd.Series]): The set of constraints from
1767            which to get the dual values.
1768
1769        Returns:
1770          pd.Series: The dual_values of all constraints in the set.
1771        """
1772        if not self.__solve_helper.has_solution():
1773            return _attribute_series(func=lambda v: pd.NA, values=constraints)
1774        return _attribute_series(
1775            func=lambda v: self.__solve_helper.dual_value(v.index),
1776            values=constraints,
1777        )
1778
1779    def dual_value(self, ct: LinearConstraint) -> np.double:
1780        """Returns the dual value of a linear constraint after solve."""
1781        if not self.__solve_helper.has_solution():
1782            return pd.NA
1783        return self.__solve_helper.dual_value(ct.index)
1784
1785    def activity(self, ct: LinearConstraint) -> np.double:
1786        """Returns the activity of a linear constraint after solve."""
1787        if not self.__solve_helper.has_solution():
1788            return pd.NA
1789        return self.__solve_helper.activity(ct.index)
1790
1791    @property
1792    def objective_value(self) -> np.double:
1793        """Returns the value of the objective after solve."""
1794        if not self.__solve_helper.has_solution():
1795            return pd.NA
1796        return self.__solve_helper.objective_value()
1797
1798    @property
1799    def best_objective_bound(self) -> np.double:
1800        """Returns the best lower (upper) bound found when min(max)imizing."""
1801        if not self.__solve_helper.has_solution():
1802            return pd.NA
1803        return self.__solve_helper.best_objective_bound()
1804
1805    @property
1806    def status_string(self) -> str:
1807        """Returns additional information of the last solve.
1808
1809        It can describe why the model is invalid.
1810        """
1811        return self.__solve_helper.status_string()
1812
1813    @property
1814    def wall_time(self) -> np.double:
1815        return self.__solve_helper.wall_time()
1816
1817    @property
1818    def user_time(self) -> np.double:
1819        return self.__solve_helper.user_time()
1820
1821
1822# The maximum number of terms to display in a linear expression's repr.
1823_MAX_LINEAR_EXPRESSION_REPR_TERMS = 5
1824
1825
1826@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1827class _LinearExpression(LinearExpr):
1828    """For variables x, an expression: offset + sum_{i in I} coeff_i * x_i."""
1829
1830    __slots__ = ("_variable_indices", "_coefficients", "_offset", "_helper")
1831
1832    _variable_indices: npt.NDArray[np.int32]
1833    _coefficients: npt.NDArray[np.double]
1834    _offset: float
1835    _helper: Optional[mbh.ModelBuilderHelper]
1836
1837    @property
1838    def variable_indices(self) -> npt.NDArray[np.int32]:
1839        return self._variable_indices
1840
1841    @property
1842    def coefficients(self) -> npt.NDArray[np.double]:
1843        return self._coefficients
1844
1845    @property
1846    def constant(self) -> float:
1847        return self._offset
1848
1849    @property
1850    def helper(self) -> Optional[mbh.ModelBuilderHelper]:
1851        return self._helper
1852
1853    def __repr__(self):
1854        return self.__str__()
1855
1856    def __str__(self):
1857        if self._helper is None:
1858            return str(self._offset)
1859
1860        result = []
1861        for index, coeff in zip(self.variable_indices, self.coefficients):
1862            if len(result) >= _MAX_LINEAR_EXPRESSION_REPR_TERMS:
1863                result.append(" + ...")
1864                break
1865            var_name = Variable(self._helper, index, None, None, None).name
1866            if not result and mbn.is_one(coeff):
1867                result.append(var_name)
1868            elif not result and mbn.is_minus_one(coeff):
1869                result.append(f"-{var_name}")
1870            elif not result:
1871                result.append(f"{coeff} * {var_name}")
1872            elif mbn.is_one(coeff):
1873                result.append(f" + {var_name}")
1874            elif mbn.is_minus_one(coeff):
1875                result.append(f" - {var_name}")
1876            elif coeff > 0.0:
1877                result.append(f" + {coeff} * {var_name}")
1878            elif coeff < 0.0:
1879                result.append(f" - {-coeff} * {var_name}")
1880
1881        if not result:
1882            return f"{self.constant}"
1883        if self.constant > 0:
1884            result.append(f" + {self.constant}")
1885        elif self.constant < 0:
1886            result.append(f" - {-self.constant}")
1887        return "".join(result)
1888
1889
1890def _sum_as_flat_linear_expression(
1891    to_process: List[Tuple[LinearExprT, float]], offset: float = 0.0
1892) -> _LinearExpression:
1893    """Creates a _LinearExpression as the sum of terms."""
1894    indices = []
1895    coeffs = []
1896    helper = None
1897    while to_process:  # Flatten AST of LinearTypes.
1898        expr, coeff = to_process.pop()
1899        if isinstance(expr, _Sum):
1900            to_process.append((expr._left, coeff))
1901            to_process.append((expr._right, coeff))
1902        elif isinstance(expr, Variable):
1903            indices.append([expr.index])
1904            coeffs.append([coeff])
1905            if helper is None:
1906                helper = expr.helper
1907        elif mbn.is_a_number(expr):
1908            offset += coeff * cast(NumberT, expr)
1909        elif isinstance(expr, _Product):
1910            to_process.append((expr._expression, coeff * expr._coefficient))
1911        elif isinstance(expr, _LinearExpression):
1912            offset += coeff * expr._offset
1913            if expr._helper is not None:
1914                indices.append(expr.variable_indices)
1915                coeffs.append(np.multiply(expr.coefficients, coeff))
1916                if helper is None:
1917                    helper = expr._helper
1918        else:
1919            raise TypeError(
1920                "Unrecognized linear expression: " + str(expr) + f" {type(expr)}"
1921            )
1922
1923    if helper is not None:
1924        all_indices: npt.NDArray[np.int32] = np.concatenate(indices, axis=0)
1925        all_coeffs: npt.NDArray[np.double] = np.concatenate(coeffs, axis=0)
1926        sorted_indices, sorted_coefficients = helper.sort_and_regroup_terms(
1927            all_indices, all_coeffs
1928        )
1929        return _LinearExpression(sorted_indices, sorted_coefficients, offset, helper)
1930    else:
1931        assert not indices
1932        assert not coeffs
1933        return _LinearExpression(
1934            _variable_indices=np.zeros(dtype=np.int32, shape=[0]),
1935            _coefficients=np.zeros(dtype=np.double, shape=[0]),
1936            _offset=offset,
1937            _helper=None,
1938        )
1939
1940
1941def _as_flat_linear_expression(base_expr: LinearExprT) -> _LinearExpression:
1942    """Converts floats, ints and Linear objects to a LinearExpression."""
1943    if isinstance(base_expr, _LinearExpression):
1944        return base_expr
1945    return _sum_as_flat_linear_expression(to_process=[(base_expr, 1.0)], offset=0.0)
1946
1947
1948@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1949class _Sum(LinearExpr):
1950    """Represents the (deferred) sum of two expressions."""
1951
1952    __slots__ = ("_left", "_right")
1953
1954    _left: LinearExprT
1955    _right: LinearExprT
1956
1957    def __repr__(self):
1958        return self.__str__()
1959
1960    def __str__(self):
1961        return str(_as_flat_linear_expression(self))
1962
1963
1964@dataclasses.dataclass(repr=False, eq=False, frozen=True)
1965class _Product(LinearExpr):
1966    """Represents the (deferred) product of an expression by a constant."""
1967
1968    __slots__ = ("_expression", "_coefficient")
1969
1970    _expression: LinearExpr
1971    _coefficient: NumberT
1972
1973    def __repr__(self):
1974        return self.__str__()
1975
1976    def __str__(self):
1977        return str(_as_flat_linear_expression(self))
1978
1979
1980def _get_index(obj: _IndexOrSeries) -> pd.Index:
1981    """Returns the indices of `obj` as a `pd.Index`."""
1982    if isinstance(obj, pd.Series):
1983        return obj.index
1984    return obj
1985
1986
1987def _attribute_series(
1988    *,
1989    func: Callable[[_VariableOrConstraint], NumberT],
1990    values: _IndexOrSeries,
1991) -> pd.Series:
1992    """Returns the attributes of `values`.
1993
1994    Args:
1995      func: The function to call for getting the attribute data.
1996      values: The values that the function will be applied (element-wise) to.
1997
1998    Returns:
1999      pd.Series: The attribute values.
2000    """
2001    return pd.Series(
2002        data=[func(v) for v in values],
2003        index=_get_index(values),
2004    )
2005
2006
2007def _convert_to_series_and_validate_index(
2008    value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index
2009) -> pd.Series:
2010    """Returns a pd.Series of the given index with the corresponding values.
2011
2012    Args:
2013      value_or_series: the values to be converted (if applicable).
2014      index: the index of the resulting pd.Series.
2015
2016    Returns:
2017      pd.Series: The set of values with the given index.
2018
2019    Raises:
2020      TypeError: If the type of `value_or_series` is not recognized.
2021      ValueError: If the index does not match.
2022    """
2023    if mbn.is_a_number(value_or_series) or isinstance(value_or_series, bool):
2024        result = pd.Series(data=value_or_series, index=index)
2025    elif isinstance(value_or_series, pd.Series):
2026        if value_or_series.index.equals(index):
2027            result = value_or_series
2028        else:
2029            raise ValueError("index does not match")
2030    else:
2031        raise TypeError("invalid type={}".format(type(value_or_series)))
2032    return result
2033
2034
2035def _convert_to_var_series_and_validate_index(
2036    var_or_series: Union["Variable", pd.Series], index: pd.Index
2037) -> pd.Series:
2038    """Returns a pd.Series of the given index with the corresponding values.
2039
2040    Args:
2041      var_or_series: the variables to be converted (if applicable).
2042      index: the index of the resulting pd.Series.
2043
2044    Returns:
2045      pd.Series: The set of values with the given index.
2046
2047    Raises:
2048      TypeError: If the type of `value_or_series` is not recognized.
2049      ValueError: If the index does not match.
2050    """
2051    if isinstance(var_or_series, Variable):
2052        result = pd.Series(data=var_or_series, index=index)
2053    elif isinstance(var_or_series, pd.Series):
2054        if var_or_series.index.equals(index):
2055            result = var_or_series
2056        else:
2057            raise ValueError("index does not match")
2058    else:
2059        raise TypeError("invalid type={}".format(type(var_or_series)))
2060    return result
2061
2062
2063# Compatibility.
2064ModelBuilder = Model
2065ModelSolver = Solver
NumberT = typing.Union[int, float, numbers.Real, numpy.number]
IntegerT = typing.Union[int, numbers.Integral, numpy.integer]
LinearExprT = typing.Union[ForwardRef('LinearExpr'), int, float, numbers.Real, numpy.number]
ConstraintT = typing.Union[ForwardRef('_BoundedLinearExpr'), bool]
class SolveStatus(pybind11_builtins.pybind11_object):

Members:

OPTIMAL

FEASIBLE

INFEASIBLE

UNBOUNDED

ABNORMAL

NOT_SOLVED

MODEL_IS_VALID

CANCELLED_BY_USER

UNKNOWN_STATUS

MODEL_INVALID

INVALID_SOLVER_PARAMETERS

SOLVER_TYPE_UNAVAILABLE

INCOMPATIBLE_OPTIONS

SolveStatus()
name

name(self: object) -> str

OPTIMAL = <SolveStatus.OPTIMAL: 0>
FEASIBLE = <SolveStatus.FEASIBLE: 1>
INFEASIBLE = <SolveStatus.INFEASIBLE: 2>
UNBOUNDED = <SolveStatus.UNBOUNDED: 3>
ABNORMAL = <SolveStatus.ABNORMAL: 4>
NOT_SOLVED = <SolveStatus.NOT_SOLVED: 5>
MODEL_IS_VALID = <SolveStatus.MODEL_IS_VALID: 6>
CANCELLED_BY_USER = <SolveStatus.CANCELLED_BY_USER: 7>
UNKNOWN_STATUS = <SolveStatus.UNKNOWN_STATUS: 8>
MODEL_INVALID = <SolveStatus.MODEL_INVALID: 9>
INVALID_SOLVER_PARAMETERS = <SolveStatus.INVALID_SOLVER_PARAMETERS: 10>
SOLVER_TYPE_UNAVAILABLE = <SolveStatus.SOLVER_TYPE_UNAVAILABLE: 11>
INCOMPATIBLE_OPTIONS = <SolveStatus.INCOMPATIBLE_OPTIONS: 12>
class LinearExpr:
 66class LinearExpr(metaclass=abc.ABCMeta):
 67    """Holds an linear expression.
 68
 69    A linear expression is built from constants and variables.
 70    For example, `x + 2.0 * (y - z + 1.0)`.
 71
 72    Linear expressions are used in Model models in constraints and in the
 73    objective:
 74
 75    * You can define linear constraints as in:
 76
 77    ```
 78    model.add(x + 2 * y <= 5.0)
 79    model.add(sum(array_of_vars) == 5.0)
 80    ```
 81
 82    * In Model, the objective is a linear expression:
 83
 84    ```
 85    model.minimize(x + 2.0 * y + z)
 86    ```
 87
 88    * For large arrays, using the LinearExpr class is faster that using the python
 89    `sum()` function. You can create constraints and the objective from lists of
 90    linear expressions or coefficients as follows:
 91
 92    ```
 93    model.minimize(model_builder.LinearExpr.sum(expressions))
 94    model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
 95    ```
 96    """
 97
 98    @classmethod
 99    def sum(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
100        cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0
101    ) -> LinearExprT:
102        """Creates `sum(expressions) + constant`.
103
104        It can perform simple simplifications and returns different objects,
105        including the input.
106
107        Args:
108          expressions: a sequence of linear expressions or constants.
109          constant: a numerical constant.
110
111        Returns:
112          a LinearExpr instance or a numerical constant.
113        """
114        checked_constant: np.double = mbn.assert_is_a_number(constant)
115        if not expressions:
116            return checked_constant
117        if len(expressions) == 1 and mbn.is_zero(checked_constant):
118            return expressions[0]
119
120        return LinearExpr.weighted_sum(
121            expressions, np.ones(len(expressions)), constant=checked_constant
122        )
123
124    @classmethod
125    def weighted_sum(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
126        cls,
127        expressions: Sequence[LinearExprT],
128        coefficients: Sequence[NumberT],
129        *,
130        constant: NumberT = 0.0,
131    ) -> Union[NumberT, "_LinearExpression"]:
132        """Creates `sum(expressions[i] * coefficients[i]) + constant`.
133
134        It can perform simple simplifications and returns different object,
135        including the input.
136
137        Args:
138          expressions: a sequence of linear expressions or constants.
139          coefficients: a sequence of numerical constants.
140          constant: a numerical constant.
141
142        Returns:
143          a _LinearExpression instance or a numerical constant.
144        """
145        if len(expressions) != len(coefficients):
146            raise ValueError(
147                "LinearExpr.weighted_sum: expressions and coefficients have"
148                " different lengths"
149            )
150        checked_constant: np.double = mbn.assert_is_a_number(constant)
151        if not expressions:
152            return checked_constant
153        return _sum_as_flat_linear_expression(
154            to_process=list(zip(expressions, coefficients)), offset=checked_constant
155        )
156
157    @classmethod
158    def term(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
159        cls,
160        expression: LinearExprT,
161        coefficient: NumberT,
162        *,
163        constant: NumberT = 0.0,
164    ) -> LinearExprT:
165        """Creates `expression * coefficient + constant`.
166
167        It can perform simple simplifications and returns different object,
168        including the input.
169        Args:
170          expression: a linear expression or a constant.
171          coefficient: a numerical constant.
172          constant: a numerical constant.
173
174        Returns:
175          a LinearExpr instance or a numerical constant.
176        """
177        checked_coefficient: np.double = mbn.assert_is_a_number(coefficient)
178        checked_constant: np.double = mbn.assert_is_a_number(constant)
179
180        if mbn.is_zero(checked_coefficient):
181            return checked_constant
182        if mbn.is_one(checked_coefficient) and mbn.is_zero(checked_constant):
183            return expression
184        if mbn.is_a_number(expression):
185            return np.double(expression) * checked_coefficient + checked_constant
186        if isinstance(expression, LinearExpr):
187            return _as_flat_linear_expression(
188                expression * checked_coefficient + checked_constant
189            )
190        raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}")
191
192    def __hash__(self):
193        return object.__hash__(self)
194
195    def __add__(self, arg: LinearExprT) -> "_Sum":
196        return _Sum(self, arg)
197
198    def __radd__(self, arg: LinearExprT) -> "_Sum":
199        return self.__add__(arg)
200
201    def __sub__(self, arg: LinearExprT) -> "_Sum":
202        return _Sum(self, -arg)
203
204    def __rsub__(self, arg: LinearExprT) -> "_Sum":
205        return _Sum(-self, arg)
206
207    def __mul__(self, arg: NumberT) -> "_Product":
208        return _Product(self, arg)
209
210    def __rmul__(self, arg: NumberT) -> "_Product":
211        return self.__mul__(arg)
212
213    def __truediv__(self, coeff: NumberT) -> "_Product":
214        return self.__mul__(1.0 / coeff)
215
216    def __neg__(self) -> "_Product":
217        return _Product(self, -1)
218
219    def __bool__(self):
220        raise NotImplementedError(f"Cannot use a LinearExpr {self} as a Boolean value")
221
222    def __eq__(self, arg: LinearExprT) -> "BoundedLinearExpression":
223        return BoundedLinearExpression(self - arg, 0, 0)
224
225    def __ge__(self, arg: LinearExprT) -> "BoundedLinearExpression":
226        return BoundedLinearExpression(
227            self - arg, 0, math.inf
228        )  # pytype: disable=wrong-arg-types  # numpy-scalars
229
230    def __le__(self, arg: LinearExprT) -> "BoundedLinearExpression":
231        return BoundedLinearExpression(
232            self - arg, -math.inf, 0
233        )  # pytype: disable=wrong-arg-types  # numpy-scalars

Holds an linear expression.

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

Linear expressions are used in Model models in constraints and in the objective:

  • You can define linear constraints as in:
model.add(x + 2 * y <= 5.0)
model.add(sum(array_of_vars) == 5.0)
  • In Model, the objective is a linear expression:
model.minimize(x + 2.0 * 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(model_builder.LinearExpr.sum(expressions))
model.add(model_builder.LinearExpr.weighted_sum(expressions, coeffs) >= 0)
@classmethod
def sum( cls, expressions: Sequence[Union[LinearExpr, int, float, numbers.Real, numpy.number]], *, constant: Union[int, float, numbers.Real, numpy.number] = 0.0) -> Union[LinearExpr, int, float, numbers.Real, numpy.number]:
 98    @classmethod
 99    def sum(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
100        cls, expressions: Sequence[LinearExprT], *, constant: NumberT = 0.0
101    ) -> LinearExprT:
102        """Creates `sum(expressions) + constant`.
103
104        It can perform simple simplifications and returns different objects,
105        including the input.
106
107        Args:
108          expressions: a sequence of linear expressions or constants.
109          constant: a numerical constant.
110
111        Returns:
112          a LinearExpr instance or a numerical constant.
113        """
114        checked_constant: np.double = mbn.assert_is_a_number(constant)
115        if not expressions:
116            return checked_constant
117        if len(expressions) == 1 and mbn.is_zero(checked_constant):
118            return expressions[0]
119
120        return LinearExpr.weighted_sum(
121            expressions, np.ones(len(expressions)), constant=checked_constant
122        )

Creates sum(expressions) + constant.

It can perform simple simplifications and returns different objects, including the input.

Arguments:
  • expressions: a sequence of linear expressions or constants.
  • constant: a numerical constant.
Returns:

a LinearExpr instance or a numerical constant.

@classmethod
def weighted_sum( cls, expressions: Sequence[Union[LinearExpr, int, float, numbers.Real, numpy.number]], coefficients: Sequence[Union[int, float, numbers.Real, numpy.number]], *, constant: Union[int, float, numbers.Real, numpy.number] = 0.0) -> Union[int, float, numbers.Real, numpy.number, ortools.linear_solver.python.model_builder._LinearExpression]:
124    @classmethod
125    def weighted_sum(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
126        cls,
127        expressions: Sequence[LinearExprT],
128        coefficients: Sequence[NumberT],
129        *,
130        constant: NumberT = 0.0,
131    ) -> Union[NumberT, "_LinearExpression"]:
132        """Creates `sum(expressions[i] * coefficients[i]) + constant`.
133
134        It can perform simple simplifications and returns different object,
135        including the input.
136
137        Args:
138          expressions: a sequence of linear expressions or constants.
139          coefficients: a sequence of numerical constants.
140          constant: a numerical constant.
141
142        Returns:
143          a _LinearExpression instance or a numerical constant.
144        """
145        if len(expressions) != len(coefficients):
146            raise ValueError(
147                "LinearExpr.weighted_sum: expressions and coefficients have"
148                " different lengths"
149            )
150        checked_constant: np.double = mbn.assert_is_a_number(constant)
151        if not expressions:
152            return checked_constant
153        return _sum_as_flat_linear_expression(
154            to_process=list(zip(expressions, coefficients)), offset=checked_constant
155        )

Creates sum(expressions[i] * coefficients[i]) + constant.

It can perform simple simplifications and returns different object, including the input.

Arguments:
  • expressions: a sequence of linear expressions or constants.
  • coefficients: a sequence of numerical constants.
  • constant: a numerical constant.
Returns:

a _LinearExpression instance or a numerical constant.

@classmethod
def term( cls, expression: Union[LinearExpr, int, float, numbers.Real, numpy.number], coefficient: Union[int, float, numbers.Real, numpy.number], *, constant: Union[int, float, numbers.Real, numpy.number] = 0.0) -> Union[LinearExpr, int, float, numbers.Real, numpy.number]:
157    @classmethod
158    def term(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
159        cls,
160        expression: LinearExprT,
161        coefficient: NumberT,
162        *,
163        constant: NumberT = 0.0,
164    ) -> LinearExprT:
165        """Creates `expression * coefficient + constant`.
166
167        It can perform simple simplifications and returns different object,
168        including the input.
169        Args:
170          expression: a linear expression or a constant.
171          coefficient: a numerical constant.
172          constant: a numerical constant.
173
174        Returns:
175          a LinearExpr instance or a numerical constant.
176        """
177        checked_coefficient: np.double = mbn.assert_is_a_number(coefficient)
178        checked_constant: np.double = mbn.assert_is_a_number(constant)
179
180        if mbn.is_zero(checked_coefficient):
181            return checked_constant
182        if mbn.is_one(checked_coefficient) and mbn.is_zero(checked_constant):
183            return expression
184        if mbn.is_a_number(expression):
185            return np.double(expression) * checked_coefficient + checked_constant
186        if isinstance(expression, LinearExpr):
187            return _as_flat_linear_expression(
188                expression * checked_coefficient + checked_constant
189            )
190        raise TypeError(f"Unknown expression {expression!r} of type {type(expression)}")

Creates expression * coefficient + constant.

It can perform simple simplifications and returns different object, including the input.

Arguments:
  • expression: a linear expression or a constant.
  • coefficient: a numerical constant.
  • constant: a numerical constant.
Returns:

a LinearExpr instance or a numerical constant.

class Variable(LinearExpr):
236class Variable(LinearExpr):
237    """A variable (continuous or integral).
238
239    A Variable is an object that can take on any integer value within defined
240    ranges. Variables appear in constraint like:
241
242        x + y >= 5
243
244    Solving a model is equivalent to finding, for each variable, a single value
245    from the set of initial values (called the initial domain), such that the
246    model is feasible, or optimal if you provided an objective function.
247    """
248
249    def __init__(
250        self,
251        helper: mbh.ModelBuilderHelper,
252        lb: NumberT,
253        ub: Optional[NumberT],
254        is_integral: Optional[bool],
255        name: Optional[str],
256    ) -> None:
257        """See Model.new_var below."""
258        LinearExpr.__init__(self)
259        self.__helper: mbh.ModelBuilderHelper = helper
260        # Python do not support multiple __init__ methods.
261        # This method is only called from the Model class.
262        # We hack the parameter to support the two cases:
263        # case 1:
264        #     helper is a ModelBuilderHelper, lb is a double value, ub is a double
265        #     value, is_integral is a Boolean value, and name is a string.
266        # case 2:
267        #     helper is a ModelBuilderHelper, lb is an index (int), ub is None,
268        #     is_integral is None, and name is None.
269        if mbn.is_integral(lb) and ub is None and is_integral is None:
270            self.__index: np.int32 = np.int32(lb)
271            self.__helper: mbh.ModelBuilderHelper = helper
272        else:
273            index: np.int32 = helper.add_var()
274            self.__index: np.int32 = np.int32(index)
275            self.__helper: mbh.ModelBuilderHelper = helper
276            helper.set_var_lower_bound(index, lb)
277            helper.set_var_upper_bound(index, ub)
278            helper.set_var_integrality(index, is_integral)
279            if name:
280                helper.set_var_name(index, name)
281
282    @property
283    def index(self) -> np.int32:
284        """Returns the index of the variable in the helper."""
285        return self.__index
286
287    @property
288    def helper(self) -> mbh.ModelBuilderHelper:
289        """Returns the underlying ModelBuilderHelper."""
290        return self.__helper
291
292    def is_equal_to(self, other: LinearExprT) -> bool:
293        """Returns true if self == other in the python sense."""
294        if not isinstance(other, Variable):
295            return False
296        return self.index == other.index and self.helper == other.helper
297
298    def __str__(self) -> str:
299        return self.name
300
301    def __repr__(self) -> str:
302        return self.__str__()
303
304    @property
305    def name(self) -> str:
306        """Returns the name of the variable."""
307        var_name = self.__helper.var_name(self.__index)
308        if var_name:
309            return var_name
310        return f"variable#{self.index}"
311
312    @name.setter
313    def name(self, name: str) -> None:
314        """Sets the name of the variable."""
315        self.__helper.set_var_name(self.__index, name)
316
317    @property
318    def lower_bound(self) -> np.double:
319        """Returns the lower bound of the variable."""
320        return self.__helper.var_lower_bound(self.__index)
321
322    @lower_bound.setter
323    def lower_bound(self, bound: NumberT) -> None:
324        """Sets the lower bound of the variable."""
325        self.__helper.set_var_lower_bound(self.__index, bound)
326
327    @property
328    def upper_bound(self) -> np.double:
329        """Returns the upper bound of the variable."""
330        return self.__helper.var_upper_bound(self.__index)
331
332    @upper_bound.setter
333    def upper_bound(self, bound: NumberT) -> None:
334        """Sets the upper bound of the variable."""
335        self.__helper.set_var_upper_bound(self.__index, bound)
336
337    @property
338    def is_integral(self) -> bool:
339        """Returns whether the variable is integral."""
340        return self.__helper.var_is_integral(self.__index)
341
342    @is_integral.setter
343    def integrality(self, is_integral: bool) -> None:
344        """Sets the integrality of the variable."""
345        self.__helper.set_var_integrality(self.__index, is_integral)
346
347    @property
348    def objective_coefficient(self) -> NumberT:
349        return self.__helper.var_objective_coefficient(self.__index)
350
351    @objective_coefficient.setter
352    def objective_coefficient(self, coeff: NumberT) -> None:
353        self.__helper.set_var_objective_coefficient(self.__index, coeff)
354
355    def __eq__(self, arg: Optional[LinearExprT]) -> ConstraintT:
356        if arg is None:
357            return False
358        if isinstance(arg, Variable):
359            return VarEqVar(self, arg)
360        return BoundedLinearExpression(
361            self - arg, 0.0, 0.0
362        )  # pytype: disable=wrong-arg-types  # numpy-scalars
363
364    def __hash__(self):
365        return hash((self.__helper, self.__index))

A variable (continuous or integral).

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

x + y >= 5

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.

Variable( helper: ortools.linear_solver.python.model_builder_helper.ModelBuilderHelper, lb: Union[int, float, numbers.Real, numpy.number], ub: Union[int, float, numbers.Real, numpy.number, NoneType], is_integral: Optional[bool], name: Optional[str])
249    def __init__(
250        self,
251        helper: mbh.ModelBuilderHelper,
252        lb: NumberT,
253        ub: Optional[NumberT],
254        is_integral: Optional[bool],
255        name: Optional[str],
256    ) -> None:
257        """See Model.new_var below."""
258        LinearExpr.__init__(self)
259        self.__helper: mbh.ModelBuilderHelper = helper
260        # Python do not support multiple __init__ methods.
261        # This method is only called from the Model class.
262        # We hack the parameter to support the two cases:
263        # case 1:
264        #     helper is a ModelBuilderHelper, lb is a double value, ub is a double
265        #     value, is_integral is a Boolean value, and name is a string.
266        # case 2:
267        #     helper is a ModelBuilderHelper, lb is an index (int), ub is None,
268        #     is_integral is None, and name is None.
269        if mbn.is_integral(lb) and ub is None and is_integral is None:
270            self.__index: np.int32 = np.int32(lb)
271            self.__helper: mbh.ModelBuilderHelper = helper
272        else:
273            index: np.int32 = helper.add_var()
274            self.__index: np.int32 = np.int32(index)
275            self.__helper: mbh.ModelBuilderHelper = helper
276            helper.set_var_lower_bound(index, lb)
277            helper.set_var_upper_bound(index, ub)
278            helper.set_var_integrality(index, is_integral)
279            if name:
280                helper.set_var_name(index, name)

See Model.new_var below.

index: numpy.int32
282    @property
283    def index(self) -> np.int32:
284        """Returns the index of the variable in the helper."""
285        return self.__index

Returns the index of the variable in the helper.

287    @property
288    def helper(self) -> mbh.ModelBuilderHelper:
289        """Returns the underlying ModelBuilderHelper."""
290        return self.__helper

Returns the underlying ModelBuilderHelper.

def is_equal_to( self, other: Union[LinearExpr, int, float, numbers.Real, numpy.number]) -> bool:
292    def is_equal_to(self, other: LinearExprT) -> bool:
293        """Returns true if self == other in the python sense."""
294        if not isinstance(other, Variable):
295            return False
296        return self.index == other.index and self.helper == other.helper

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

name: str
304    @property
305    def name(self) -> str:
306        """Returns the name of the variable."""
307        var_name = self.__helper.var_name(self.__index)
308        if var_name:
309            return var_name
310        return f"variable#{self.index}"

Returns the name of the variable.

lower_bound: numpy.float64
317    @property
318    def lower_bound(self) -> np.double:
319        """Returns the lower bound of the variable."""
320        return self.__helper.var_lower_bound(self.__index)

Returns the lower bound of the variable.

upper_bound: numpy.float64
327    @property
328    def upper_bound(self) -> np.double:
329        """Returns the upper bound of the variable."""
330        return self.__helper.var_upper_bound(self.__index)

Returns the upper bound of the variable.

is_integral: bool
337    @property
338    def is_integral(self) -> bool:
339        """Returns whether the variable is integral."""
340        return self.__helper.var_is_integral(self.__index)

Returns whether the variable is integral.

integrality: bool
337    @property
338    def is_integral(self) -> bool:
339        """Returns whether the variable is integral."""
340        return self.__helper.var_is_integral(self.__index)

Returns whether the variable is integral.

objective_coefficient: Union[int, float, numbers.Real, numpy.number]
347    @property
348    def objective_coefficient(self) -> NumberT:
349        return self.__helper.var_objective_coefficient(self.__index)
Inherited Members
LinearExpr
sum
weighted_sum
term
@dataclasses.dataclass(repr=False, eq=False, frozen=True)
class VarEqVar(_BoundedLinearExpr):
512@dataclasses.dataclass(repr=False, eq=False, frozen=True)
513class VarEqVar(_BoundedLinearExpr):
514    """Represents var == var."""
515
516    __slots__ = ("left", "right")
517
518    left: Variable
519    right: Variable
520
521    def __str__(self):
522        return f"{self.left} == {self.right}"
523
524    def __repr__(self):
525        return self.__str__()
526
527    def __bool__(self) -> bool:
528        return hash(self.left) == hash(self.right)
529
530    def _add_linear_constraint(
531        self, helper: mbh.ModelBuilderHelper, name: str
532    ) -> "LinearConstraint":
533        c = LinearConstraint(helper)
534        helper.set_constraint_lower_bound(c.index, 0.0)
535        helper.set_constraint_upper_bound(c.index, 0.0)
536        # pylint: disable=protected-access
537        helper.add_term_to_constraint(c.index, self.left.index, 1.0)
538        helper.add_term_to_constraint(c.index, self.right.index, -1.0)
539        # pylint: enable=protected-access
540        helper.set_constraint_name(c.index, name)
541        return c
542
543    def _add_enforced_linear_constraint(
544        self,
545        helper: mbh.ModelBuilderHelper,
546        var: Variable,
547        value: bool,
548        name: str,
549    ) -> "EnforcedLinearConstraint":
550        """Adds an enforced linear constraint to the model."""
551        c = EnforcedLinearConstraint(helper)
552        c.indicator_variable = var
553        c.indicator_value = value
554        helper.set_enforced_constraint_lower_bound(c.index, 0.0)
555        helper.set_enforced_constraint_upper_bound(c.index, 0.0)
556        # pylint: disable=protected-access
557        helper.add_term_to_enforced_constraint(c.index, self.left.index, 1.0)
558        helper.add_term_to_enforced_constraint(c.index, self.right.index, -1.0)
559        # pylint: enable=protected-access
560        helper.set_enforced_constraint_name(c.index, name)
561        return c

Represents var == var.

VarEqVar( left: Variable, right: Variable)
left: Variable
right: Variable
class BoundedLinearExpression(_BoundedLinearExpr):
564class BoundedLinearExpression(_BoundedLinearExpr):
565    """Represents a linear constraint: `lb <= linear expression <= ub`.
566
567    The only use of this class is to be added to the Model through
568    `Model.add(bounded expression)`, as in:
569
570        model.Add(x + 2 * y -1 >= z)
571    """
572
573    def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT) -> None:
574        self.__expr: LinearExprT = expr
575        self.__lb: np.double = mbn.assert_is_a_number(lb)
576        self.__ub: np.double = mbn.assert_is_a_number(ub)
577
578    def __str__(self) -> str:
579        if self.__lb > -math.inf and self.__ub < math.inf:
580            if self.__lb == self.__ub:
581                return f"{self.__expr} == {self.__lb}"
582            else:
583                return f"{self.__lb} <= {self.__expr} <= {self.__ub}"
584        elif self.__lb > -math.inf:
585            return f"{self.__expr} >= {self.__lb}"
586        elif self.__ub < math.inf:
587            return f"{self.__expr} <= {self.__ub}"
588        else:
589            return f"{self.__expr} free"
590
591    def __repr__(self):
592        return self.__str__()
593
594    @property
595    def expression(self) -> LinearExprT:
596        return self.__expr
597
598    @property
599    def lower_bound(self) -> np.double:
600        return self.__lb
601
602    @property
603    def upper_bound(self) -> np.double:
604        return self.__ub
605
606    def __bool__(self) -> bool:
607        raise NotImplementedError(
608            f"Cannot use a BoundedLinearExpression {self} as a Boolean value"
609        )
610
611    def _add_linear_constraint(
612        self, helper: mbh.ModelBuilderHelper, name: Optional[str]
613    ) -> "LinearConstraint":
614        c = LinearConstraint(helper)
615        flat_expr = _as_flat_linear_expression(self.__expr)
616        # pylint: disable=protected-access
617        helper.add_terms_to_constraint(
618            c.index, flat_expr._variable_indices, flat_expr._coefficients
619        )
620        helper.set_constraint_lower_bound(c.index, self.__lb - flat_expr._offset)
621        helper.set_constraint_upper_bound(c.index, self.__ub - flat_expr._offset)
622        # pylint: enable=protected-access
623        if name is not None:
624            helper.set_constraint_name(c.index, name)
625        return c
626
627    def _add_enforced_linear_constraint(
628        self,
629        helper: mbh.ModelBuilderHelper,
630        var: Variable,
631        value: bool,
632        name: Optional[str],
633    ) -> "EnforcedLinearConstraint":
634        """Adds an enforced linear constraint to the model."""
635        c = EnforcedLinearConstraint(helper)
636        c.indicator_variable = var
637        c.indicator_value = value
638        flat_expr = _as_flat_linear_expression(self.__expr)
639        # pylint: disable=protected-access
640        helper.add_terms_to_enforced_constraint(
641            c.index, flat_expr._variable_indices, flat_expr._coefficients
642        )
643        helper.set_enforced_constraint_lower_bound(
644            c.index, self.__lb - flat_expr._offset
645        )
646        helper.set_enforced_constraint_upper_bound(
647            c.index, self.__ub - flat_expr._offset
648        )
649        # pylint: enable=protected-access
650        if name is not None:
651            helper.set_enforced_constraint_name(c.index, name)
652        return c

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

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

model.Add(x + 2 * y -1 >= z)
BoundedLinearExpression( expr: Union[LinearExpr, int, float, numbers.Real, numpy.number], lb: Union[int, float, numbers.Real, numpy.number], ub: Union[int, float, numbers.Real, numpy.number])
573    def __init__(self, expr: LinearExprT, lb: NumberT, ub: NumberT) -> None:
574        self.__expr: LinearExprT = expr
575        self.__lb: np.double = mbn.assert_is_a_number(lb)
576        self.__ub: np.double = mbn.assert_is_a_number(ub)
expression: Union[LinearExpr, int, float, numbers.Real, numpy.number]
594    @property
595    def expression(self) -> LinearExprT:
596        return self.__expr
lower_bound: numpy.float64
598    @property
599    def lower_bound(self) -> np.double:
600        return self.__lb
upper_bound: numpy.float64
602    @property
603    def upper_bound(self) -> np.double:
604        return self.__ub
class LinearConstraint:
655class LinearConstraint:
656    """Stores a linear equation.
657
658    Example:
659        x = model.new_num_var(0, 10, 'x')
660        y = model.new_num_var(0, 10, 'y')
661
662        linear_constraint = model.add(x + 2 * y == 5)
663    """
664
665    def __init__(
666        self,
667        helper: mbh.ModelBuilderHelper,
668        *,
669        index: Optional[IntegerT] = None,
670        is_under_specified: bool = False,
671    ) -> None:
672        """LinearConstraint constructor.
673
674        Args:
675          helper: The pybind11 ModelBuilderHelper.
676          index: If specified, recreates a wrapper to an existing linear constraint.
677          is_under_specified: indicates if the constraint was created by
678            model.add(bool).
679        """
680        if index is None:
681            self.__index = helper.add_linear_constraint()
682        else:
683            self.__index = index
684        self.__helper: mbh.ModelBuilderHelper = helper
685        self.__is_under_specified = is_under_specified
686
687    @property
688    def index(self) -> IntegerT:
689        """Returns the index of the constraint in the helper."""
690        return self.__index
691
692    @property
693    def helper(self) -> mbh.ModelBuilderHelper:
694        """Returns the ModelBuilderHelper instance."""
695        return self.__helper
696
697    @property
698    def lower_bound(self) -> np.double:
699        return self.__helper.constraint_lower_bound(self.__index)
700
701    @lower_bound.setter
702    def lower_bound(self, bound: NumberT) -> None:
703        self.assert_constraint_is_well_defined()
704        self.__helper.set_constraint_lower_bound(self.__index, bound)
705
706    @property
707    def upper_bound(self) -> np.double:
708        return self.__helper.constraint_upper_bound(self.__index)
709
710    @upper_bound.setter
711    def upper_bound(self, bound: NumberT) -> None:
712        self.assert_constraint_is_well_defined()
713        self.__helper.set_constraint_upper_bound(self.__index, bound)
714
715    @property
716    def name(self) -> str:
717        constraint_name = self.__helper.constraint_name(self.__index)
718        if constraint_name:
719            return constraint_name
720        return f"linear_constraint#{self.__index}"
721
722    @name.setter
723    def name(self, name: str) -> None:
724        return self.__helper.set_constraint_name(self.__index, name)
725
726    @property
727    def is_under_specified(self) -> bool:
728        """Returns True if the constraint is under specified.
729
730        Usually, it means that it was created by model.add(False) or model.add(True)
731        The effect is that modifying the constraint will raise an exception.
732        """
733        return self.__is_under_specified
734
735    def assert_constraint_is_well_defined(self) -> None:
736        """Raises an exception if the constraint is under specified."""
737        if self.__is_under_specified:
738            raise ValueError(
739                f"Constraint {self.index} is under specified and cannot be modified"
740            )
741
742    def __str__(self):
743        return self.name
744
745    def __repr__(self):
746        return (
747            f"LinearConstraint({self.name}, lb={self.lower_bound},"
748            f" ub={self.upper_bound},"
749            f" var_indices={self.helper.constraint_var_indices(self.index)},"
750            f" coefficients={self.helper.constraint_coefficients(self.index)})"
751        )
752
753    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
754        """Sets the coefficient of the variable in the constraint."""
755        self.assert_constraint_is_well_defined()
756        self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
757
758    def add_term(self, var: Variable, coeff: NumberT) -> None:
759        """Adds var * coeff to the constraint."""
760        self.assert_constraint_is_well_defined()
761        self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)
762
763    def clear_terms(self) -> None:
764        """Clear all terms of the constraint."""
765        self.assert_constraint_is_well_defined()
766        self.__helper.clear_constraint_terms(self.__index)

Stores a linear equation.

Example:

x = model.new_num_var(0, 10, 'x') y = model.new_num_var(0, 10, 'y')

linear_constraint = model.add(x + 2 * y == 5)

LinearConstraint( helper: ortools.linear_solver.python.model_builder_helper.ModelBuilderHelper, *, index: Union[int, numbers.Integral, numpy.integer, NoneType] = None, is_under_specified: bool = False)
665    def __init__(
666        self,
667        helper: mbh.ModelBuilderHelper,
668        *,
669        index: Optional[IntegerT] = None,
670        is_under_specified: bool = False,
671    ) -> None:
672        """LinearConstraint constructor.
673
674        Args:
675          helper: The pybind11 ModelBuilderHelper.
676          index: If specified, recreates a wrapper to an existing linear constraint.
677          is_under_specified: indicates if the constraint was created by
678            model.add(bool).
679        """
680        if index is None:
681            self.__index = helper.add_linear_constraint()
682        else:
683            self.__index = index
684        self.__helper: mbh.ModelBuilderHelper = helper
685        self.__is_under_specified = is_under_specified

LinearConstraint constructor.

Arguments:
  • helper: The pybind11 ModelBuilderHelper.
  • index: If specified, recreates a wrapper to an existing linear constraint.
  • is_under_specified: indicates if the constraint was created by model.add(bool).
index: Union[int, numbers.Integral, numpy.integer]
687    @property
688    def index(self) -> IntegerT:
689        """Returns the index of the constraint in the helper."""
690        return self.__index

Returns the index of the constraint in the helper.

692    @property
693    def helper(self) -> mbh.ModelBuilderHelper:
694        """Returns the ModelBuilderHelper instance."""
695        return self.__helper

Returns the ModelBuilderHelper instance.

lower_bound: numpy.float64
697    @property
698    def lower_bound(self) -> np.double:
699        return self.__helper.constraint_lower_bound(self.__index)
upper_bound: numpy.float64
706    @property
707    def upper_bound(self) -> np.double:
708        return self.__helper.constraint_upper_bound(self.__index)
name: str
715    @property
716    def name(self) -> str:
717        constraint_name = self.__helper.constraint_name(self.__index)
718        if constraint_name:
719            return constraint_name
720        return f"linear_constraint#{self.__index}"
is_under_specified: bool
726    @property
727    def is_under_specified(self) -> bool:
728        """Returns True if the constraint is under specified.
729
730        Usually, it means that it was created by model.add(False) or model.add(True)
731        The effect is that modifying the constraint will raise an exception.
732        """
733        return self.__is_under_specified

Returns True if the constraint is under specified.

Usually, it means that it was created by model.add(False) or model.add(True) The effect is that modifying the constraint will raise an exception.

def assert_constraint_is_well_defined(self) -> None:
735    def assert_constraint_is_well_defined(self) -> None:
736        """Raises an exception if the constraint is under specified."""
737        if self.__is_under_specified:
738            raise ValueError(
739                f"Constraint {self.index} is under specified and cannot be modified"
740            )

Raises an exception if the constraint is under specified.

def set_coefficient( self, var: Variable, coeff: Union[int, float, numbers.Real, numpy.number]) -> None:
753    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
754        """Sets the coefficient of the variable in the constraint."""
755        self.assert_constraint_is_well_defined()
756        self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)

Sets the coefficient of the variable in the constraint.

def add_term( self, var: Variable, coeff: Union[int, float, numbers.Real, numpy.number]) -> None:
758    def add_term(self, var: Variable, coeff: NumberT) -> None:
759        """Adds var * coeff to the constraint."""
760        self.assert_constraint_is_well_defined()
761        self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)

Adds var * coeff to the constraint.

def clear_terms(self) -> None:
763    def clear_terms(self) -> None:
764        """Clear all terms of the constraint."""
765        self.assert_constraint_is_well_defined()
766        self.__helper.clear_constraint_terms(self.__index)

Clear all terms of the constraint.

class EnforcedLinearConstraint:
769class EnforcedLinearConstraint:
770    """Stores an enforced linear equation, also name indicator constraint.
771
772    Example:
773        x = model.new_num_var(0, 10, 'x')
774        y = model.new_num_var(0, 10, 'y')
775        z = model.new_bool_var('z')
776
777        enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)
778    """
779
780    def __init__(
781        self,
782        helper: mbh.ModelBuilderHelper,
783        *,
784        index: Optional[IntegerT] = None,
785        is_under_specified: bool = False,
786    ) -> None:
787        """EnforcedLinearConstraint constructor.
788
789        Args:
790          helper: The pybind11 ModelBuilderHelper.
791          index: If specified, recreates a wrapper to an existing linear constraint.
792          is_under_specified: indicates if the constraint was created by
793            model.add(bool).
794        """
795        if index is None:
796            self.__index = helper.add_enforced_linear_constraint()
797        else:
798            if not helper.is_enforced_linear_constraint(index):
799                raise ValueError(
800                    f"the given index {index} does not refer to an enforced linear"
801                    " constraint"
802                )
803
804            self.__index = index
805        self.__helper: mbh.ModelBuilderHelper = helper
806        self.__is_under_specified = is_under_specified
807
808    @property
809    def index(self) -> IntegerT:
810        """Returns the index of the constraint in the helper."""
811        return self.__index
812
813    @property
814    def helper(self) -> mbh.ModelBuilderHelper:
815        """Returns the ModelBuilderHelper instance."""
816        return self.__helper
817
818    @property
819    def lower_bound(self) -> np.double:
820        return self.__helper.enforced_constraint_lower_bound(self.__index)
821
822    @lower_bound.setter
823    def lower_bound(self, bound: NumberT) -> None:
824        self.assert_constraint_is_well_defined()
825        self.__helper.set_enforced_constraint_lower_bound(self.__index, bound)
826
827    @property
828    def upper_bound(self) -> np.double:
829        return self.__helper.enforced_constraint_upper_bound(self.__index)
830
831    @upper_bound.setter
832    def upper_bound(self, bound: NumberT) -> None:
833        self.assert_constraint_is_well_defined()
834        self.__helper.set_enforced_constraint_upper_bound(self.__index, bound)
835
836    @property
837    def indicator_variable(self) -> "Variable":
838        enforcement_var_index = (
839            self.__helper.enforced_constraint_indicator_variable_index(self.__index)
840        )
841        return Variable(self.__helper, enforcement_var_index, None, None, None)
842
843    @indicator_variable.setter
844    def indicator_variable(self, var: "Variable") -> None:
845        self.__helper.set_enforced_constraint_indicator_variable_index(
846            self.__index, var.index
847        )
848
849    @property
850    def indicator_value(self) -> bool:
851        return self.__helper.enforced_constraint_indicator_value(self.__index)
852
853    @indicator_value.setter
854    def indicator_value(self, value: bool) -> None:
855        self.__helper.set_enforced_constraint_indicator_value(self.__index, value)
856
857    @property
858    def name(self) -> str:
859        constraint_name = self.__helper.enforced_constraint_name(self.__index)
860        if constraint_name:
861            return constraint_name
862        return f"enforced_linear_constraint#{self.__index}"
863
864    @name.setter
865    def name(self, name: str) -> None:
866        return self.__helper.set_enforced_constraint_name(self.__index, name)
867
868    @property
869    def is_under_specified(self) -> bool:
870        """Returns True if the constraint is under specified.
871
872        Usually, it means that it was created by model.add(False) or model.add(True)
873        The effect is that modifying the constraint will raise an exception.
874        """
875        return self.__is_under_specified
876
877    def assert_constraint_is_well_defined(self) -> None:
878        """Raises an exception if the constraint is under specified."""
879        if self.__is_under_specified:
880            raise ValueError(
881                f"Constraint {self.index} is under specified and cannot be modified"
882            )
883
884    def __str__(self):
885        return self.name
886
887    def __repr__(self):
888        return (
889            f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound},"
890            f" ub={self.upper_bound},"
891            f" var_indices={self.helper.enforced_constraint_var_indices(self.index)},"
892            f" coefficients={self.helper.enforced_constraint_coefficients(self.index)},"
893            f" indicator_variable={self.indicator_variable}"
894            f" indicator_value={self.indicator_value})"
895        )
896
897    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
898        """Sets the coefficient of the variable in the constraint."""
899        self.assert_constraint_is_well_defined()
900        self.__helper.set_enforced_constraint_coefficient(
901            self.__index, var.index, coeff
902        )
903
904    def add_term(self, var: Variable, coeff: NumberT) -> None:
905        """Adds var * coeff to the constraint."""
906        self.assert_constraint_is_well_defined()
907        self.__helper.safe_add_term_to_enforced_constraint(
908            self.__index, var.index, coeff
909        )
910
911    def clear_terms(self) -> None:
912        """Clear all terms of the constraint."""
913        self.assert_constraint_is_well_defined()
914        self.__helper.clear_enforced_constraint_terms(self.__index)

Stores an enforced linear equation, also name indicator constraint.

Example:

x = model.new_num_var(0, 10, 'x') y = model.new_num_var(0, 10, 'y') z = model.new_bool_var('z')

enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)

EnforcedLinearConstraint( helper: ortools.linear_solver.python.model_builder_helper.ModelBuilderHelper, *, index: Union[int, numbers.Integral, numpy.integer, NoneType] = None, is_under_specified: bool = False)
780    def __init__(
781        self,
782        helper: mbh.ModelBuilderHelper,
783        *,
784        index: Optional[IntegerT] = None,
785        is_under_specified: bool = False,
786    ) -> None:
787        """EnforcedLinearConstraint constructor.
788
789        Args:
790          helper: The pybind11 ModelBuilderHelper.
791          index: If specified, recreates a wrapper to an existing linear constraint.
792          is_under_specified: indicates if the constraint was created by
793            model.add(bool).
794        """
795        if index is None:
796            self.__index = helper.add_enforced_linear_constraint()
797        else:
798            if not helper.is_enforced_linear_constraint(index):
799                raise ValueError(
800                    f"the given index {index} does not refer to an enforced linear"
801                    " constraint"
802                )
803
804            self.__index = index
805        self.__helper: mbh.ModelBuilderHelper = helper
806        self.__is_under_specified = is_under_specified

EnforcedLinearConstraint constructor.

Arguments:
  • helper: The pybind11 ModelBuilderHelper.
  • index: If specified, recreates a wrapper to an existing linear constraint.
  • is_under_specified: indicates if the constraint was created by model.add(bool).
index: Union[int, numbers.Integral, numpy.integer]
808    @property
809    def index(self) -> IntegerT:
810        """Returns the index of the constraint in the helper."""
811        return self.__index

Returns the index of the constraint in the helper.

813    @property
814    def helper(self) -> mbh.ModelBuilderHelper:
815        """Returns the ModelBuilderHelper instance."""
816        return self.__helper

Returns the ModelBuilderHelper instance.

lower_bound: numpy.float64
818    @property
819    def lower_bound(self) -> np.double:
820        return self.__helper.enforced_constraint_lower_bound(self.__index)
upper_bound: numpy.float64
827    @property
828    def upper_bound(self) -> np.double:
829        return self.__helper.enforced_constraint_upper_bound(self.__index)
indicator_variable: Variable
836    @property
837    def indicator_variable(self) -> "Variable":
838        enforcement_var_index = (
839            self.__helper.enforced_constraint_indicator_variable_index(self.__index)
840        )
841        return Variable(self.__helper, enforcement_var_index, None, None, None)
indicator_value: bool
849    @property
850    def indicator_value(self) -> bool:
851        return self.__helper.enforced_constraint_indicator_value(self.__index)
name: str
857    @property
858    def name(self) -> str:
859        constraint_name = self.__helper.enforced_constraint_name(self.__index)
860        if constraint_name:
861            return constraint_name
862        return f"enforced_linear_constraint#{self.__index}"
is_under_specified: bool
868    @property
869    def is_under_specified(self) -> bool:
870        """Returns True if the constraint is under specified.
871
872        Usually, it means that it was created by model.add(False) or model.add(True)
873        The effect is that modifying the constraint will raise an exception.
874        """
875        return self.__is_under_specified

Returns True if the constraint is under specified.

Usually, it means that it was created by model.add(False) or model.add(True) The effect is that modifying the constraint will raise an exception.

def assert_constraint_is_well_defined(self) -> None:
877    def assert_constraint_is_well_defined(self) -> None:
878        """Raises an exception if the constraint is under specified."""
879        if self.__is_under_specified:
880            raise ValueError(
881                f"Constraint {self.index} is under specified and cannot be modified"
882            )

Raises an exception if the constraint is under specified.

def set_coefficient( self, var: Variable, coeff: Union[int, float, numbers.Real, numpy.number]) -> None:
897    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
898        """Sets the coefficient of the variable in the constraint."""
899        self.assert_constraint_is_well_defined()
900        self.__helper.set_enforced_constraint_coefficient(
901            self.__index, var.index, coeff
902        )

Sets the coefficient of the variable in the constraint.

def add_term( self, var: Variable, coeff: Union[int, float, numbers.Real, numpy.number]) -> None:
904    def add_term(self, var: Variable, coeff: NumberT) -> None:
905        """Adds var * coeff to the constraint."""
906        self.assert_constraint_is_well_defined()
907        self.__helper.safe_add_term_to_enforced_constraint(
908            self.__index, var.index, coeff
909        )

Adds var * coeff to the constraint.

def clear_terms(self) -> None:
911    def clear_terms(self) -> None:
912        """Clear all terms of the constraint."""
913        self.assert_constraint_is_well_defined()
914        self.__helper.clear_enforced_constraint_terms(self.__index)

Clear all terms of the constraint.

class Model:
 917class Model:
 918    """Methods for building a linear model.
 919
 920    Methods beginning with:
 921
 922    * ```new_``` create integer, boolean, or interval variables.
 923    * ```add_``` create new constraints and add them to the model.
 924    """
 925
 926    def __init__(self):
 927        self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper()
 928
 929    def clone(self) -> "Model":
 930        """Returns a clone of the current model."""
 931        clone = Model()
 932        clone.helper.overwrite_model(self.helper)
 933        return clone
 934
 935    @typing.overload
 936    def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: ...
 937
 938    @typing.overload
 939    def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: ...
 940
 941    def _get_linear_constraints(
 942        self, constraints: Optional[_IndexOrSeries] = None
 943    ) -> _IndexOrSeries:
 944        if constraints is None:
 945            return self.get_linear_constraints()
 946        return constraints
 947
 948    @typing.overload
 949    def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: ...
 950
 951    @typing.overload
 952    def _get_variables(self, variables: pd.Series) -> pd.Series: ...
 953
 954    def _get_variables(
 955        self, variables: Optional[_IndexOrSeries] = None
 956    ) -> _IndexOrSeries:
 957        if variables is None:
 958            return self.get_variables()
 959        return variables
 960
 961    def get_linear_constraints(self) -> pd.Index:
 962        """Gets all linear constraints in the model."""
 963        return pd.Index(
 964            [self.linear_constraint_from_index(i) for i in range(self.num_constraints)],
 965            name="linear_constraint",
 966        )
 967
 968    def get_linear_constraint_expressions(
 969        self, constraints: Optional[_IndexOrSeries] = None
 970    ) -> pd.Series:
 971        """Gets the expressions of all linear constraints in the set.
 972
 973        If `constraints` is a `pd.Index`, then the output will be indexed by the
 974        constraints. If `constraints` is a `pd.Series` indexed by the underlying
 975        dimensions, then the output will be indexed by the same underlying
 976        dimensions.
 977
 978        Args:
 979          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
 980            constraints from which to get the expressions. If unspecified, all
 981            linear constraints will be in scope.
 982
 983        Returns:
 984          pd.Series: The expressions of all linear constraints in the set.
 985        """
 986        return _attribute_series(
 987            # pylint: disable=g-long-lambda
 988            func=lambda c: _as_flat_linear_expression(
 989                # pylint: disable=g-complex-comprehension
 990                sum(
 991                    coeff * Variable(self.__helper, var_id, None, None, None)
 992                    for var_id, coeff in zip(
 993                        c.helper.constraint_var_indices(c.index),
 994                        c.helper.constraint_coefficients(c.index),
 995                    )
 996                )
 997            ),
 998            values=self._get_linear_constraints(constraints),
 999        )
1000
1001    def get_linear_constraint_lower_bounds(
1002        self, constraints: Optional[_IndexOrSeries] = None
1003    ) -> pd.Series:
1004        """Gets the lower bounds of all linear constraints in the set.
1005
1006        If `constraints` is a `pd.Index`, then the output will be indexed by the
1007        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1008        dimensions, then the output will be indexed by the same underlying
1009        dimensions.
1010
1011        Args:
1012          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1013            constraints from which to get the lower bounds. If unspecified, all
1014            linear constraints will be in scope.
1015
1016        Returns:
1017          pd.Series: The lower bounds of all linear constraints in the set.
1018        """
1019        return _attribute_series(
1020            func=lambda c: c.lower_bound,  # pylint: disable=protected-access
1021            values=self._get_linear_constraints(constraints),
1022        )
1023
1024    def get_linear_constraint_upper_bounds(
1025        self, constraints: Optional[_IndexOrSeries] = None
1026    ) -> pd.Series:
1027        """Gets the upper bounds of all linear constraints in the set.
1028
1029        If `constraints` is a `pd.Index`, then the output will be indexed by the
1030        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1031        dimensions, then the output will be indexed by the same underlying
1032        dimensions.
1033
1034        Args:
1035          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1036            constraints. If unspecified, all linear constraints will be in scope.
1037
1038        Returns:
1039          pd.Series: The upper bounds of all linear constraints in the set.
1040        """
1041        return _attribute_series(
1042            func=lambda c: c.upper_bound,  # pylint: disable=protected-access
1043            values=self._get_linear_constraints(constraints),
1044        )
1045
1046    def get_variables(self) -> pd.Index:
1047        """Gets all variables in the model."""
1048        return pd.Index(
1049            [self.var_from_index(i) for i in range(self.num_variables)],
1050            name="variable",
1051        )
1052
1053    def get_variable_lower_bounds(
1054        self, variables: Optional[_IndexOrSeries] = None
1055    ) -> pd.Series:
1056        """Gets the lower bounds of all variables in the set.
1057
1058        If `variables` is a `pd.Index`, then the output will be indexed by the
1059        variables. If `variables` is a `pd.Series` indexed by the underlying
1060        dimensions, then the output will be indexed by the same underlying
1061        dimensions.
1062
1063        Args:
1064          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
1065            from which to get the lower bounds. If unspecified, all variables will
1066            be in scope.
1067
1068        Returns:
1069          pd.Series: The lower bounds of all variables in the set.
1070        """
1071        return _attribute_series(
1072            func=lambda v: v.lower_bound,  # pylint: disable=protected-access
1073            values=self._get_variables(variables),
1074        )
1075
1076    def get_variable_upper_bounds(
1077        self, variables: Optional[_IndexOrSeries] = None
1078    ) -> pd.Series:
1079        """Gets the upper bounds of all variables in the set.
1080
1081        Args:
1082          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
1083            from which to get the upper bounds. If unspecified, all variables will
1084            be in scope.
1085
1086        Returns:
1087          pd.Series: The upper bounds of all variables in the set.
1088        """
1089        return _attribute_series(
1090            func=lambda v: v.upper_bound,  # pylint: disable=protected-access
1091            values=self._get_variables(variables),
1092        )
1093
1094    # Integer variable.
1095
1096    def new_var(
1097        self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str]
1098    ) -> Variable:
1099        """Create an integer variable with domain [lb, ub].
1100
1101        Args:
1102          lb: Lower bound of the variable.
1103          ub: Upper bound of the variable.
1104          is_integer: Indicates if the variable must take integral values.
1105          name: The name of the variable.
1106
1107        Returns:
1108          a variable whose domain is [lb, ub].
1109        """
1110
1111        return Variable(self.__helper, lb, ub, is_integer, name)
1112
1113    def new_int_var(
1114        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
1115    ) -> Variable:
1116        """Create an integer variable with domain [lb, ub].
1117
1118        Args:
1119          lb: Lower bound of the variable.
1120          ub: Upper bound of the variable.
1121          name: The name of the variable.
1122
1123        Returns:
1124          a variable whose domain is [lb, ub].
1125        """
1126
1127        return self.new_var(lb, ub, True, name)
1128
1129    def new_num_var(
1130        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
1131    ) -> Variable:
1132        """Create an integer variable with domain [lb, ub].
1133
1134        Args:
1135          lb: Lower bound of the variable.
1136          ub: Upper bound of the variable.
1137          name: The name of the variable.
1138
1139        Returns:
1140          a variable whose domain is [lb, ub].
1141        """
1142
1143        return self.new_var(lb, ub, False, name)
1144
1145    def new_bool_var(self, name: Optional[str] = None) -> Variable:
1146        """Creates a 0-1 variable with the given name."""
1147        return self.new_var(
1148            0, 1, True, name
1149        )  # pytype: disable=wrong-arg-types  # numpy-scalars
1150
1151    def new_constant(self, value: NumberT) -> Variable:
1152        """Declares a constant variable."""
1153        return self.new_var(value, value, False, None)
1154
1155    def new_var_series(
1156        self,
1157        name: str,
1158        index: pd.Index,
1159        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1160        upper_bounds: Union[NumberT, pd.Series] = math.inf,
1161        is_integral: Union[bool, pd.Series] = False,
1162    ) -> pd.Series:
1163        """Creates a series of (scalar-valued) variables with the given name.
1164
1165        Args:
1166          name (str): Required. The name of the variable set.
1167          index (pd.Index): Required. The index to use for the variable set.
1168          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1169            variables in the set. If a `pd.Series` is passed in, it will be based on
1170            the corresponding values of the pd.Series. Defaults to -inf.
1171          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1172            variables in the set. If a `pd.Series` is passed in, it will be based on
1173            the corresponding values of the pd.Series. Defaults to +inf.
1174          is_integral (bool, pd.Series): Optional. Indicates if the variable can
1175            only take integer values. If a `pd.Series` is passed in, it will be
1176            based on the corresponding values of the pd.Series. Defaults to False.
1177
1178        Returns:
1179          pd.Series: The variable set indexed by its corresponding dimensions.
1180
1181        Raises:
1182          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1183          ValueError: if the `name` is not a valid identifier or already exists.
1184          ValueError: if the `lowerbound` is greater than the `upperbound`.
1185          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1186          does not match the input index.
1187        """
1188        if not isinstance(index, pd.Index):
1189            raise TypeError("Non-index object is used as index")
1190        if not name.isidentifier():
1191            raise ValueError("name={} is not a valid identifier".format(name))
1192        if (
1193            mbn.is_a_number(lower_bounds)
1194            and mbn.is_a_number(upper_bounds)
1195            and lower_bounds > upper_bounds
1196        ):
1197            raise ValueError(
1198                "lower_bound={} is greater than upper_bound={} for variable set={}".format(
1199                    lower_bounds, upper_bounds, name
1200                )
1201            )
1202        if (
1203            isinstance(is_integral, bool)
1204            and is_integral
1205            and mbn.is_a_number(lower_bounds)
1206            and mbn.is_a_number(upper_bounds)
1207            and math.isfinite(lower_bounds)
1208            and math.isfinite(upper_bounds)
1209            and math.ceil(lower_bounds) > math.floor(upper_bounds)
1210        ):
1211            raise ValueError(
1212                "ceil(lower_bound={})={}".format(lower_bounds, math.ceil(lower_bounds))
1213                + " is greater than floor("
1214                + "upper_bound={})={}".format(upper_bounds, math.floor(upper_bounds))
1215                + " for variable set={}".format(name)
1216            )
1217        lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
1218        upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
1219        is_integrals = _convert_to_series_and_validate_index(is_integral, index)
1220        return pd.Series(
1221            index=index,
1222            data=[
1223                # pylint: disable=g-complex-comprehension
1224                Variable(
1225                    helper=self.__helper,
1226                    name=f"{name}[{i}]",
1227                    lb=lower_bounds[i],
1228                    ub=upper_bounds[i],
1229                    is_integral=is_integrals[i],
1230                )
1231                for i in index
1232            ],
1233        )
1234
1235    def new_num_var_series(
1236        self,
1237        name: str,
1238        index: pd.Index,
1239        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1240        upper_bounds: Union[NumberT, pd.Series] = math.inf,
1241    ) -> pd.Series:
1242        """Creates a series of continuous variables with the given name.
1243
1244        Args:
1245          name (str): Required. The name of the variable set.
1246          index (pd.Index): Required. The index to use for the variable set.
1247          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1248            variables in the set. If a `pd.Series` is passed in, it will be based on
1249            the corresponding values of the pd.Series. Defaults to -inf.
1250          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1251            variables in the set. If a `pd.Series` is passed in, it will be based on
1252            the corresponding values of the pd.Series. Defaults to +inf.
1253
1254        Returns:
1255          pd.Series: The variable set indexed by its corresponding dimensions.
1256
1257        Raises:
1258          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1259          ValueError: if the `name` is not a valid identifier or already exists.
1260          ValueError: if the `lowerbound` is greater than the `upperbound`.
1261          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1262          does not match the input index.
1263        """
1264        return self.new_var_series(name, index, lower_bounds, upper_bounds, False)
1265
1266    def new_int_var_series(
1267        self,
1268        name: str,
1269        index: pd.Index,
1270        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
1271        upper_bounds: Union[NumberT, pd.Series] = math.inf,
1272    ) -> pd.Series:
1273        """Creates a series of integer variables with the given name.
1274
1275        Args:
1276          name (str): Required. The name of the variable set.
1277          index (pd.Index): Required. The index to use for the variable set.
1278          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
1279            variables in the set. If a `pd.Series` is passed in, it will be based on
1280            the corresponding values of the pd.Series. Defaults to -inf.
1281          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
1282            variables in the set. If a `pd.Series` is passed in, it will be based on
1283            the corresponding values of the pd.Series. Defaults to +inf.
1284
1285        Returns:
1286          pd.Series: The variable set indexed by its corresponding dimensions.
1287
1288        Raises:
1289          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1290          ValueError: if the `name` is not a valid identifier or already exists.
1291          ValueError: if the `lowerbound` is greater than the `upperbound`.
1292          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1293          does not match the input index.
1294        """
1295        return self.new_var_series(name, index, lower_bounds, upper_bounds, True)
1296
1297    def new_bool_var_series(
1298        self,
1299        name: str,
1300        index: pd.Index,
1301    ) -> pd.Series:
1302        """Creates a series of Boolean variables with the given name.
1303
1304        Args:
1305          name (str): Required. The name of the variable set.
1306          index (pd.Index): Required. The index to use for the variable set.
1307
1308        Returns:
1309          pd.Series: The variable set indexed by its corresponding dimensions.
1310
1311        Raises:
1312          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1313          ValueError: if the `name` is not a valid identifier or already exists.
1314          ValueError: if the `lowerbound` is greater than the `upperbound`.
1315          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
1316          does not match the input index.
1317        """
1318        return self.new_var_series(name, index, 0, 1, True)
1319
1320    def var_from_index(self, index: IntegerT) -> Variable:
1321        """Rebuilds a variable object from the model and its index."""
1322        return Variable(self.__helper, index, None, None, None)
1323
1324    # Linear constraints.
1325
1326    def add_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
1327        self,
1328        linear_expr: LinearExprT,
1329        lb: NumberT = -math.inf,
1330        ub: NumberT = math.inf,
1331        name: Optional[str] = None,
1332    ) -> LinearConstraint:
1333        """Adds the constraint: `lb <= linear_expr <= ub` with the given name."""
1334        ct = LinearConstraint(self.__helper)
1335        if name:
1336            self.__helper.set_constraint_name(ct.index, name)
1337        if mbn.is_a_number(linear_expr):
1338            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
1339            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
1340        elif isinstance(linear_expr, Variable):
1341            self.__helper.set_constraint_lower_bound(ct.index, lb)
1342            self.__helper.set_constraint_upper_bound(ct.index, ub)
1343            self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
1344        elif isinstance(linear_expr, LinearExpr):
1345            flat_expr = _as_flat_linear_expression(linear_expr)
1346            # pylint: disable=protected-access
1347            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset)
1348            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset)
1349            self.__helper.add_terms_to_constraint(
1350                ct.index, flat_expr._variable_indices, flat_expr._coefficients
1351            )
1352        else:
1353            raise TypeError(
1354                f"Not supported: Model.add_linear_constraint({linear_expr})"
1355                f" with type {type(linear_expr)}"
1356            )
1357        return ct
1358
1359    def add(
1360        self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None
1361    ) -> Union[LinearConstraint, pd.Series]:
1362        """Adds a `BoundedLinearExpression` to the model.
1363
1364        Args:
1365          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1366          name: An optional name.
1367
1368        Returns:
1369          An instance of the `Constraint` class.
1370
1371        Note that a special treatment is done when the argument does not contain any
1372        variable, and thus evaluates to True or False.
1373
1374        `model.add(True)` will create a constraint 0 <= empty sum <= 0.
1375        The constraint will be marked as under specified, and cannot be modified
1376        thereafter.
1377
1378        `model.add(False)` will create a constraint inf <= empty sum <= -inf. The
1379        constraint will be marked as under specified, and cannot be modified
1380        thereafter.
1381
1382        you can check the if a constraint is under specified by reading the
1383        `LinearConstraint.is_under_specified` property.
1384        """
1385        if isinstance(ct, _BoundedLinearExpr):
1386            return ct._add_linear_constraint(self.__helper, name)
1387        elif isinstance(ct, bool):
1388            return _add_linear_constraint_to_helper(ct, self.__helper, name)
1389        elif isinstance(ct, pd.Series):
1390            return pd.Series(
1391                index=ct.index,
1392                data=[
1393                    _add_linear_constraint_to_helper(
1394                        expr, self.__helper, f"{name}[{i}]"
1395                    )
1396                    for (i, expr) in zip(ct.index, ct)
1397                ],
1398            )
1399        else:
1400            raise TypeError("Not supported: Model.add(" + str(ct) + ")")
1401
1402    def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint:
1403        """Rebuilds a linear constraint object from the model and its index."""
1404        return LinearConstraint(self.__helper, index=index)
1405
1406    # EnforcedLinear constraints.
1407
1408    def add_enforced_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
1409        self,
1410        linear_expr: LinearExprT,
1411        ivar: "Variable",
1412        ivalue: bool,
1413        lb: NumberT = -math.inf,
1414        ub: NumberT = math.inf,
1415        name: Optional[str] = None,
1416    ) -> EnforcedLinearConstraint:
1417        """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name."""
1418        ct = EnforcedLinearConstraint(self.__helper)
1419        ct.indicator_variable = ivar
1420        ct.indicator_value = ivalue
1421        if name:
1422            self.__helper.set_constraint_name(ct.index, name)
1423        if mbn.is_a_number(linear_expr):
1424            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
1425            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
1426        elif isinstance(linear_expr, Variable):
1427            self.__helper.set_constraint_lower_bound(ct.index, lb)
1428            self.__helper.set_constraint_upper_bound(ct.index, ub)
1429            self.__helper.add_term_to_constraint(ct.index, linear_expr.index, 1.0)
1430        elif isinstance(linear_expr, LinearExpr):
1431            flat_expr = _as_flat_linear_expression(linear_expr)
1432            # pylint: disable=protected-access
1433            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr._offset)
1434            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr._offset)
1435            self.__helper.add_terms_to_constraint(
1436                ct.index, flat_expr._variable_indices, flat_expr._coefficients
1437            )
1438        else:
1439            raise TypeError(
1440                "Not supported:"
1441                f" Model.add_enforced_linear_constraint({linear_expr}) with"
1442                f" type {type(linear_expr)}"
1443            )
1444        return ct
1445
1446    def add_enforced(
1447        self,
1448        ct: Union[ConstraintT, pd.Series],
1449        var: Union[Variable, pd.Series],
1450        value: Union[bool, pd.Series],
1451        name: Optional[str] = None,
1452    ) -> Union[EnforcedLinearConstraint, pd.Series]:
1453        """Adds a `ivar == ivalue => BoundedLinearExpression` to the model.
1454
1455        Args:
1456          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
1457          var: The indicator variable
1458          value: the indicator value
1459          name: An optional name.
1460
1461        Returns:
1462          An instance of the `Constraint` class.
1463
1464        Note that a special treatment is done when the argument does not contain any
1465        variable, and thus evaluates to True or False.
1466
1467        model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty
1468        sum <= 0
1469
1470        model.add_enforced(False, var, value) will create a constraint inf <=
1471        empty sum <= -inf
1472
1473        you can check the if a constraint is always false (lb=inf, ub=-inf) by
1474        calling EnforcedLinearConstraint.is_always_false()
1475        """
1476        if isinstance(ct, _BoundedLinearExpr):
1477            return ct._add_enforced_linear_constraint(self.__helper, var, value, name)
1478        elif (
1479            isinstance(ct, bool)
1480            and isinstance(var, Variable)
1481            and isinstance(value, bool)
1482        ):
1483            return _add_enforced_linear_constraint_to_helper(
1484                ct, self.__helper, var, value, name
1485            )
1486        elif isinstance(ct, pd.Series):
1487            ivar_series = _convert_to_var_series_and_validate_index(var, ct.index)
1488            ivalue_series = _convert_to_series_and_validate_index(value, ct.index)
1489            return pd.Series(
1490                index=ct.index,
1491                data=[
1492                    _add_enforced_linear_constraint_to_helper(
1493                        expr,
1494                        self.__helper,
1495                        ivar_series[i],
1496                        ivalue_series[i],
1497                        f"{name}[{i}]",
1498                    )
1499                    for (i, expr) in zip(ct.index, ct)
1500                ],
1501            )
1502        else:
1503            raise TypeError("Not supported: Model.add_enforced(" + str(ct) + ")")
1504
1505    def enforced_linear_constraint_from_index(
1506        self, index: IntegerT
1507    ) -> EnforcedLinearConstraint:
1508        """Rebuilds an enforced linear constraint object from the model and its index."""
1509        return EnforcedLinearConstraint(self.__helper, index=index)
1510
1511    # Objective.
1512    def minimize(self, linear_expr: LinearExprT) -> None:
1513        """Minimizes the given objective."""
1514        self.__optimize(linear_expr, False)
1515
1516    def maximize(self, linear_expr: LinearExprT) -> None:
1517        """Maximizes the given objective."""
1518        self.__optimize(linear_expr, True)
1519
1520    def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None:
1521        """Defines the objective."""
1522        self.helper.clear_objective()
1523        self.__helper.set_maximize(maximize)
1524        if mbn.is_a_number(linear_expr):
1525            self.helper.set_objective_offset(linear_expr)
1526        elif isinstance(linear_expr, Variable):
1527            self.helper.set_var_objective_coefficient(linear_expr.index, 1.0)
1528        elif isinstance(linear_expr, LinearExpr):
1529            flat_expr = _as_flat_linear_expression(linear_expr)
1530            # pylint: disable=protected-access
1531            self.helper.set_objective_offset(flat_expr._offset)
1532            self.helper.set_objective_coefficients(
1533                flat_expr._variable_indices, flat_expr._coefficients
1534            )
1535        else:
1536            raise TypeError(f"Not supported: Model.minimize/maximize({linear_expr})")
1537
1538    @property
1539    def objective_offset(self) -> np.double:
1540        """Returns the fixed offset of the objective."""
1541        return self.__helper.objective_offset()
1542
1543    @objective_offset.setter
1544    def objective_offset(self, value: NumberT) -> None:
1545        self.__helper.set_objective_offset(value)
1546
1547    def objective_expression(self) -> "_LinearExpression":
1548        """Returns the expression to optimize."""
1549        return _as_flat_linear_expression(
1550            sum(
1551                variable * self.__helper.var_objective_coefficient(variable.index)
1552                for variable in self.get_variables()
1553                if self.__helper.var_objective_coefficient(variable.index) != 0.0
1554            )
1555            + self.__helper.objective_offset()
1556        )
1557
1558    # Hints.
1559    def clear_hints(self):
1560        """Clears all solution hints."""
1561        self.__helper.clear_hints()
1562
1563    def add_hint(self, var: Variable, value: NumberT) -> None:
1564        """Adds var == value as a hint to the model.
1565
1566        Args:
1567          var: The variable of the hint
1568          value: The value of the hint
1569
1570        Note that variables must not appear more than once in the list of hints.
1571        """
1572        self.__helper.add_hint(var.index, value)
1573
1574    # Input/Output
1575    def export_to_lp_string(self, obfuscate: bool = False) -> str:
1576        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1577        options.obfuscate = obfuscate
1578        return self.__helper.export_to_lp_string(options)
1579
1580    def export_to_mps_string(self, obfuscate: bool = False) -> str:
1581        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1582        options.obfuscate = obfuscate
1583        return self.__helper.export_to_mps_string(options)
1584
1585    def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool:
1586        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1587        options.obfuscate = obfuscate
1588        return self.__helper.write_to_mps_file(filename, options)
1589
1590    def export_to_proto(self) -> linear_solver_pb2.MPModelProto:
1591        """Exports the optimization model to a ProtoBuf format."""
1592        return mbh.to_mpmodel_proto(self.__helper)
1593
1594    def import_from_mps_string(self, mps_string: str) -> bool:
1595        """Reads a model from a MPS string."""
1596        return self.__helper.import_from_mps_string(mps_string)
1597
1598    def import_from_mps_file(self, mps_file: str) -> bool:
1599        """Reads a model from a .mps file."""
1600        return self.__helper.import_from_mps_file(mps_file)
1601
1602    def import_from_lp_string(self, lp_string: str) -> bool:
1603        """Reads a model from a LP string."""
1604        return self.__helper.import_from_lp_string(lp_string)
1605
1606    def import_from_lp_file(self, lp_file: str) -> bool:
1607        """Reads a model from a .lp file."""
1608        return self.__helper.import_from_lp_file(lp_file)
1609
1610    def import_from_proto_file(self, proto_file: str) -> bool:
1611        """Reads a model from a proto file."""
1612        return self.__helper.read_model_from_proto_file(proto_file)
1613
1614    def export_to_proto_file(self, proto_file: str) -> bool:
1615        """Writes a model to a proto file."""
1616        return self.__helper.write_model_to_proto_file(proto_file)
1617
1618    # Model getters and Setters
1619
1620    @property
1621    def num_variables(self) -> int:
1622        """Returns the number of variables in the model."""
1623        return self.__helper.num_variables()
1624
1625    @property
1626    def num_constraints(self) -> int:
1627        """The number of constraints in the model."""
1628        return self.__helper.num_constraints()
1629
1630    @property
1631    def name(self) -> str:
1632        """The name of the model."""
1633        return self.__helper.name()
1634
1635    @name.setter
1636    def name(self, name: str):
1637        self.__helper.set_name(name)
1638
1639    @property
1640    def helper(self) -> mbh.ModelBuilderHelper:
1641        """Returns the model builder helper."""
1642        return self.__helper

Methods for building a linear model.

Methods beginning with:

  • new_ create integer, boolean, or interval variables.
  • add_ create new constraints and add them to the model.
def clone(self) -> Model:
929    def clone(self) -> "Model":
930        """Returns a clone of the current model."""
931        clone = Model()
932        clone.helper.overwrite_model(self.helper)
933        return clone

Returns a clone of the current model.

def get_linear_constraints(self) -> pandas.core.indexes.base.Index:
961    def get_linear_constraints(self) -> pd.Index:
962        """Gets all linear constraints in the model."""
963        return pd.Index(
964            [self.linear_constraint_from_index(i) for i in range(self.num_constraints)],
965            name="linear_constraint",
966        )

Gets all linear constraints in the model.

def get_linear_constraint_expressions( self, constraints: Union[pandas.core.indexes.base.Index, pandas.core.series.Series, NoneType] = None) -> pandas.core.series.Series:
968    def get_linear_constraint_expressions(
969        self, constraints: Optional[_IndexOrSeries] = None
970    ) -> pd.Series:
971        """Gets the expressions of all linear constraints in the set.
972
973        If `constraints` is a `pd.Index`, then the output will be indexed by the
974        constraints. If `constraints` is a `pd.Series` indexed by the underlying
975        dimensions, then the output will be indexed by the same underlying
976        dimensions.
977
978        Args:
979          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
980            constraints from which to get the expressions. If unspecified, all
981            linear constraints will be in scope.
982
983        Returns:
984          pd.Series: The expressions of all linear constraints in the set.
985        """
986        return _attribute_series(
987            # pylint: disable=g-long-lambda
988            func=lambda c: _as_flat_linear_expression(
989                # pylint: disable=g-complex-comprehension
990                sum(
991                    coeff * Variable(self.__helper, var_id, None, None, None)
992                    for var_id, coeff in zip(
993                        c.helper.constraint_var_indices(c.index),
994                        c.helper.constraint_coefficients(c.index),
995                    )
996                )
997            ),
998            values=self._get_linear_constraints(constraints),
999        )

Gets the expressions of all linear constraints in the set.

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

Arguments:
  • constraints (Union[pd.Index, pd.Series]): Optional. The set of linear constraints from which to get the expressions. If unspecified, all linear constraints will be in scope.
Returns:

pd.Series: The expressions of all linear constraints in the set.

def get_linear_constraint_lower_bounds( self, constraints: Union[pandas.core.indexes.base.Index, pandas.core.series.Series, NoneType] = None) -> pandas.core.series.Series:
1001    def get_linear_constraint_lower_bounds(
1002        self, constraints: Optional[_IndexOrSeries] = None
1003    ) -> pd.Series:
1004        """Gets the lower bounds of all linear constraints in the set.
1005
1006        If `constraints` is a `pd.Index`, then the output will be indexed by the
1007        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1008        dimensions, then the output will be indexed by the same underlying
1009        dimensions.
1010
1011        Args:
1012          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1013            constraints from which to get the lower bounds. If unspecified, all
1014            linear constraints will be in scope.
1015
1016        Returns:
1017          pd.Series: The lower bounds of all linear constraints in the set.
1018        """
1019        return _attribute_series(
1020            func=lambda c: c.lower_bound,  # pylint: disable=protected-access
1021            values=self._get_linear_constraints(constraints),
1022        )

Gets the lower bounds of all linear constraints in the set.

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

Arguments:
  • constraints (Union[pd.Index, pd.Series]): Optional. The set of linear constraints from which to get the lower bounds. If unspecified, all linear constraints will be in scope.
Returns:

pd.Series: The lower bounds of all linear constraints in the set.

def get_linear_constraint_upper_bounds( self, constraints: Union[pandas.core.indexes.base.Index, pandas.core.series.Series, NoneType] = None) -> pandas.core.series.Series:
1024    def get_linear_constraint_upper_bounds(
1025        self, constraints: Optional[_IndexOrSeries] = None
1026    ) -> pd.Series:
1027        """Gets the upper bounds of all linear constraints in the set.
1028
1029        If `constraints` is a `pd.Index`, then the output will be indexed by the
1030        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1031        dimensions, then the output will be indexed by the same underlying
1032        dimensions.
1033
1034        Args:
1035          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
1036            constraints. If unspecified, all linear constraints will be in scope.
1037
1038        Returns:
1039          pd.Series: The upper bounds of all linear constraints in the set.
1040        """
1041        return _attribute_series(
1042            func=lambda c: c.upper_bound,  # pylint: disable=protected-access
1043            values=self._get_linear_constraints(constraints),
1044        )

Gets the upper bounds of all linear constraints in the set.

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

Arguments:
  • constraints (Union[pd.Index, pd.Series]): Optional. The set of linear constraints. If unspecified, all linear constraints will be in scope.
Returns:

pd.Series: The upper bounds of all linear constraints in the set.

def get_variables(self) -> pandas.core.indexes.base.Index:
1046    def get_variables(self) -> pd.Index:
1047        """Gets all variables in the model."""
1048        return pd.Index(
1049            [self.var_from_index(i) for i in range(self.num_variables)],
1050            name="variable",
1051        )

Gets all variables in the model.

def get_variable_lower_bounds( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series, NoneType] = None) -> pandas.core.series.Series:
1053    def get_variable_lower_bounds(
1054        self, variables: Optional[_IndexOrSeries] = None
1055    ) -> pd.Series:
1056        """Gets the lower bounds of all variables in the set.
1057
1058        If `variables` is a `pd.Index`, then the output will be indexed by the
1059        variables. If `variables` is a `pd.Series` indexed by the underlying
1060        dimensions, then the output will be indexed by the same underlying
1061        dimensions.
1062
1063        Args:
1064          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
1065            from which to get the lower bounds. If unspecified, all variables will
1066            be in scope.
1067
1068        Returns:
1069          pd.Series: The lower bounds of all variables in the set.
1070        """
1071        return _attribute_series(
1072            func=lambda v: v.lower_bound,  # pylint: disable=protected-access
1073            values=self._get_variables(variables),
1074        )

Gets the lower bounds of all variables in the set.

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]): Optional. The set of variables from which to get the lower bounds. If unspecified, all variables will be in scope.
Returns:

pd.Series: The lower bounds of all variables in the set.

def get_variable_upper_bounds( self, variables: Union[pandas.core.indexes.base.Index, pandas.core.series.Series, NoneType] = None) -> pandas.core.series.Series:
1076    def get_variable_upper_bounds(
1077        self, variables: Optional[_IndexOrSeries] = None
1078    ) -> pd.Series:
1079        """Gets the upper bounds of all variables in the set.
1080
1081        Args:
1082          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
1083            from which to get the upper bounds. If unspecified, all variables will
1084            be in scope.
1085
1086        Returns:
1087          pd.Series: The upper bounds of all variables in the set.
1088        """
1089        return _attribute_series(
1090            func=lambda v: v.upper_bound,  # pylint: disable=protected-access
1091            values=self._get_variables(variables),