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#!/usr/bin/env python3
   2# Copyright 2010-2025 Google LLC
   3# Licensed under the Apache License, Version 2.0 (the "License");
   4# you may not use this file except in compliance with the License.
   5# You may obtain a copy of the License at
   6#
   7#     http://www.apache.org/licenses/LICENSE-2.0
   8#
   9# Unless required by applicable law or agreed to in writing, software
  10# distributed under the License is distributed on an "AS IS" BASIS,
  11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12# See the License for the specific language governing permissions and
  13# limitations under the License.
  14
  15"""Methods for building and solving model_builder models.
  16
  17The following two sections describe the main
  18methods for building and solving those models.
  19
  20* [`Model`](#model_builder.Model): Methods for creating
  21models, including variables and constraints.
  22* [`Solver`](#model_builder.Solver): Methods for solving
  23a model and evaluating solutions.
  24
  25Additional methods for solving Model models:
  26
  27* [`Constraint`](#model_builder.Constraint): A few utility methods for modifying
  28  constraints created by `Model`.
  29* [`LinearExpr`](#model_builder.LinearExpr): Methods for creating constraints
  30  and the objective from large arrays of coefficients.
  31
  32Other methods and functions listed are primarily used for developing OR-Tools,
  33rather than for solving specific optimization problems.
  34"""
  35
  36from collections.abc import Callable
  37import math
  38import numbers
  39import typing
  40from typing import Optional, Union
  41
  42import numpy as np
  43import pandas as pd
  44
  45from ortools.linear_solver import linear_solver_pb2
  46from ortools.linear_solver.python import model_builder_helper as mbh
  47from ortools.linear_solver.python import model_builder_numbers as mbn
  48
  49# Custom types.
  50NumberT = Union[int, float, numbers.Real, np.number]
  51IntegerT = Union[int, numbers.Integral, np.integer]
  52LinearExprT = Union[mbh.LinearExpr, NumberT]
  53ConstraintT = Union[mbh.BoundedLinearExpression, bool]
  54_IndexOrSeries = Union[pd.Index, pd.Series]
  55_VariableOrConstraint = Union["LinearConstraint", mbh.Variable]
  56
  57# Forward solve statuses.
  58AffineExpr = mbh.AffineExpr
  59BoundedLinearExpression = mbh.BoundedLinearExpression
  60FlatExpr = mbh.FlatExpr
  61LinearExpr = mbh.LinearExpr
  62SolveStatus = mbh.SolveStatus
  63Variable = mbh.Variable
  64
  65
  66def _add_linear_constraint_to_helper(
  67    bounded_expr: Union[bool, mbh.BoundedLinearExpression],
  68    helper: mbh.ModelBuilderHelper,
  69    name: Optional[str],
  70):
  71    """Creates a new linear constraint in the helper.
  72
  73    It handles boolean values (which might arise in the construction of
  74    BoundedLinearExpressions).
  75
  76    If bounded_expr is a Boolean value, the created constraint is different.
  77    In that case, the constraint will be immutable and marked as under-specified.
  78    It will be always feasible or infeasible whether the value is True or False.
  79
  80    Args:
  81      bounded_expr: The bounded expression used to create the constraint.
  82      helper: The helper to create the constraint.
  83      name: The name of the constraint to be created.
  84
  85    Returns:
  86      LinearConstraint: a constraint in the helper corresponding to the input.
  87
  88    Raises:
  89      TypeError: If constraint is an invalid type.
  90    """
  91    if isinstance(bounded_expr, bool):
  92        c = LinearConstraint(helper, is_under_specified=True)
  93        if name is not None:
  94            helper.set_constraint_name(c.index, name)
  95        if bounded_expr:
  96            # constraint that is always feasible: 0.0 <= nothing <= 0.0
  97            helper.set_constraint_lower_bound(c.index, 0.0)
  98            helper.set_constraint_upper_bound(c.index, 0.0)
  99        else:
 100            # constraint that is always infeasible: +oo <= nothing <= -oo
 101            helper.set_constraint_lower_bound(c.index, 1)
 102            helper.set_constraint_upper_bound(c.index, -1)
 103        return c
 104    if isinstance(bounded_expr, mbh.BoundedLinearExpression):
 105        c = LinearConstraint(helper)
 106        # pylint: disable=protected-access
 107        helper.add_terms_to_constraint(c.index, bounded_expr.vars, bounded_expr.coeffs)
 108        helper.set_constraint_lower_bound(c.index, bounded_expr.lower_bound)
 109        helper.set_constraint_upper_bound(c.index, bounded_expr.upper_bound)
 110        # pylint: enable=protected-access
 111        if name is not None:
 112            helper.set_constraint_name(c.index, name)
 113        return c
 114    raise TypeError(f"invalid type={type(bounded_expr).__name__!r}")
 115
 116
 117def _add_enforced_linear_constraint_to_helper(
 118    bounded_expr: Union[bool, mbh.BoundedLinearExpression],
 119    helper: mbh.ModelBuilderHelper,
 120    var: Variable,
 121    value: bool,
 122    name: Optional[str],
 123):
 124    """Creates a new enforced linear constraint in the helper.
 125
 126    It handles boolean values (which might arise in the construction of
 127    BoundedLinearExpressions).
 128
 129    If bounded_expr is a Boolean value, the linear part of the constraint is
 130    different.
 131    In that case, the constraint will be immutable and marked as under-specified.
 132    Its linear part will be always feasible or infeasible whether the value is
 133    True or False.
 134
 135    Args:
 136      bounded_expr: The bounded expression used to create the constraint.
 137      helper: The helper to create the constraint.
 138      var: the variable used in the indicator
 139      value: the value used in the indicator
 140      name: The name of the constraint to be created.
 141
 142    Returns:
 143      EnforcedLinearConstraint: a constraint in the helper corresponding to the
 144      input.
 145
 146    Raises:
 147      TypeError: If constraint is an invalid type.
 148    """
 149    if isinstance(bounded_expr, bool):
 150        # TODO(user): create indicator variable assignment instead ?
 151        c = EnforcedLinearConstraint(helper, is_under_specified=True)
 152        c.indicator_variable = var
 153        c.indicator_value = value
 154        if name is not None:
 155            helper.set_enforced_constraint_name(c.index, name)
 156        if bounded_expr:
 157            # constraint that is always feasible: 0.0 <= nothing <= 0.0
 158            helper.set_enforced_constraint_lower_bound(c.index, 0.0)
 159            helper.set_enforced_constraint_upper_bound(c.index, 0.0)
 160        else:
 161            # constraint that is always infeasible: +oo <= nothing <= -oo
 162            helper.set_enforced_constraint_lower_bound(c.index, 1)
 163            helper.set_enforced_constraint_upper_bound(c.index, -1)
 164        return c
 165    if isinstance(bounded_expr, mbh.BoundedLinearExpression):
 166        c = EnforcedLinearConstraint(helper)
 167        c.indicator_variable = var
 168        c.indicator_value = value
 169        helper.add_terms_to_enforced_constraint(
 170            c.index, bounded_expr.vars, bounded_expr.coeffs
 171        )
 172        helper.set_enforced_constraint_lower_bound(c.index, bounded_expr.lower_bound)
 173        helper.set_enforced_constraint_upper_bound(c.index, bounded_expr.upper_bound)
 174        if name is not None:
 175            helper.set_constraint_name(c.index, name)
 176        return c
 177
 178    raise TypeError(f"invalid type={type(bounded_expr).__name__!r}")
 179
 180
 181class LinearConstraint:
 182    """Stores a linear equation.
 183
 184    Example:
 185        x = model.new_num_var(0, 10, 'x')
 186        y = model.new_num_var(0, 10, 'y')
 187
 188        linear_constraint = model.add(x + 2 * y == 5)
 189    """
 190
 191    def __init__(
 192        self,
 193        helper: mbh.ModelBuilderHelper,
 194        *,
 195        index: Optional[IntegerT] = None,
 196        is_under_specified: bool = False,
 197    ) -> None:
 198        """LinearConstraint constructor.
 199
 200        Args:
 201          helper: The pybind11 ModelBuilderHelper.
 202          index: If specified, recreates a wrapper to an existing linear constraint.
 203          is_under_specified: indicates if the constraint was created by
 204            model.add(bool).
 205        """
 206        if index is None:
 207            self.__index = helper.add_linear_constraint()
 208        else:
 209            self.__index = index
 210        self.__helper: mbh.ModelBuilderHelper = helper
 211        self.__is_under_specified = is_under_specified
 212
 213    def __hash__(self):
 214        return hash((self.__helper, self.__index))
 215
 216    @property
 217    def index(self) -> IntegerT:
 218        """Returns the index of the constraint in the helper."""
 219        return self.__index
 220
 221    @property
 222    def helper(self) -> mbh.ModelBuilderHelper:
 223        """Returns the ModelBuilderHelper instance."""
 224        return self.__helper
 225
 226    @property
 227    def lower_bound(self) -> np.double:
 228        return self.__helper.constraint_lower_bound(self.__index)
 229
 230    @lower_bound.setter
 231    def lower_bound(self, bound: NumberT) -> None:
 232        self.assert_constraint_is_well_defined()
 233        self.__helper.set_constraint_lower_bound(self.__index, bound)
 234
 235    @property
 236    def upper_bound(self) -> np.double:
 237        return self.__helper.constraint_upper_bound(self.__index)
 238
 239    @upper_bound.setter
 240    def upper_bound(self, bound: NumberT) -> None:
 241        self.assert_constraint_is_well_defined()
 242        self.__helper.set_constraint_upper_bound(self.__index, bound)
 243
 244    @property
 245    def name(self) -> str:
 246        constraint_name = self.__helper.constraint_name(self.__index)
 247        if constraint_name:
 248            return constraint_name
 249        return f"linear_constraint#{self.__index}"
 250
 251    @name.setter
 252    def name(self, name: str) -> None:
 253        return self.__helper.set_constraint_name(self.__index, name)
 254
 255    @property
 256    def is_under_specified(self) -> bool:
 257        """Returns True if the constraint is under specified.
 258
 259        Usually, it means that it was created by model.add(False) or model.add(True)
 260        The effect is that modifying the constraint will raise an exception.
 261        """
 262        return self.__is_under_specified
 263
 264    def assert_constraint_is_well_defined(self) -> None:
 265        """Raises an exception if the constraint is under specified."""
 266        if self.__is_under_specified:
 267            raise ValueError(
 268                f"Constraint {self.index} is under specified and cannot be modified"
 269            )
 270
 271    def __str__(self):
 272        return self.name
 273
 274    def __repr__(self):
 275        return (
 276            f"LinearConstraint({self.name}, lb={self.lower_bound},"
 277            f" ub={self.upper_bound},"
 278            f" var_indices={self.helper.constraint_var_indices(self.index)},"
 279            f" coefficients={self.helper.constraint_coefficients(self.index)})"
 280        )
 281
 282    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
 283        """Sets the coefficient of the variable in the constraint."""
 284        self.assert_constraint_is_well_defined()
 285        self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
 286
 287    def add_term(self, var: Variable, coeff: NumberT) -> None:
 288        """Adds var * coeff to the constraint."""
 289        self.assert_constraint_is_well_defined()
 290        self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)
 291
 292    def clear_terms(self) -> None:
 293        """Clear all terms of the constraint."""
 294        self.assert_constraint_is_well_defined()
 295        self.__helper.clear_constraint_terms(self.__index)
 296
 297
 298class EnforcedLinearConstraint:
 299    """Stores an enforced linear equation, also name indicator constraint.
 300
 301    Example:
 302        x = model.new_num_var(0, 10, 'x')
 303        y = model.new_num_var(0, 10, 'y')
 304        z = model.new_bool_var('z')
 305
 306        enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)
 307    """
 308
 309    def __init__(
 310        self,
 311        helper: mbh.ModelBuilderHelper,
 312        *,
 313        index: Optional[IntegerT] = None,
 314        is_under_specified: bool = False,
 315    ) -> None:
 316        """EnforcedLinearConstraint constructor.
 317
 318        Args:
 319          helper: The pybind11 ModelBuilderHelper.
 320          index: If specified, recreates a wrapper to an existing linear constraint.
 321          is_under_specified: indicates if the constraint was created by
 322            model.add(bool).
 323        """
 324        if index is None:
 325            self.__index = helper.add_enforced_linear_constraint()
 326        else:
 327            if not helper.is_enforced_linear_constraint(index):
 328                raise ValueError(
 329                    f"the given index {index} does not refer to an enforced linear"
 330                    " constraint"
 331                )
 332
 333            self.__index = index
 334        self.__helper: mbh.ModelBuilderHelper = helper
 335        self.__is_under_specified = is_under_specified
 336
 337    @property
 338    def index(self) -> IntegerT:
 339        """Returns the index of the constraint in the helper."""
 340        return self.__index
 341
 342    @property
 343    def helper(self) -> mbh.ModelBuilderHelper:
 344        """Returns the ModelBuilderHelper instance."""
 345        return self.__helper
 346
 347    @property
 348    def lower_bound(self) -> np.double:
 349        return self.__helper.enforced_constraint_lower_bound(self.__index)
 350
 351    @lower_bound.setter
 352    def lower_bound(self, bound: NumberT) -> None:
 353        self.assert_constraint_is_well_defined()
 354        self.__helper.set_enforced_constraint_lower_bound(self.__index, bound)
 355
 356    @property
 357    def upper_bound(self) -> np.double:
 358        return self.__helper.enforced_constraint_upper_bound(self.__index)
 359
 360    @upper_bound.setter
 361    def upper_bound(self, bound: NumberT) -> None:
 362        self.assert_constraint_is_well_defined()
 363        self.__helper.set_enforced_constraint_upper_bound(self.__index, bound)
 364
 365    @property
 366    def indicator_variable(self) -> "Variable":
 367        enforcement_var_index = (
 368            self.__helper.enforced_constraint_indicator_variable_index(self.__index)
 369        )
 370        return Variable(self.__helper, enforcement_var_index)
 371
 372    @indicator_variable.setter
 373    def indicator_variable(self, var: "Variable") -> None:
 374        self.__helper.set_enforced_constraint_indicator_variable_index(
 375            self.__index, var.index
 376        )
 377
 378    @property
 379    def indicator_value(self) -> bool:
 380        return self.__helper.enforced_constraint_indicator_value(self.__index)
 381
 382    @indicator_value.setter
 383    def indicator_value(self, value: bool) -> None:
 384        self.__helper.set_enforced_constraint_indicator_value(self.__index, value)
 385
 386    @property
 387    def name(self) -> str:
 388        constraint_name = self.__helper.enforced_constraint_name(self.__index)
 389        if constraint_name:
 390            return constraint_name
 391        return f"enforced_linear_constraint#{self.__index}"
 392
 393    @name.setter
 394    def name(self, name: str) -> None:
 395        return self.__helper.set_enforced_constraint_name(self.__index, name)
 396
 397    @property
 398    def is_under_specified(self) -> bool:
 399        """Returns True if the constraint is under specified.
 400
 401        Usually, it means that it was created by model.add(False) or model.add(True)
 402        The effect is that modifying the constraint will raise an exception.
 403        """
 404        return self.__is_under_specified
 405
 406    def assert_constraint_is_well_defined(self) -> None:
 407        """Raises an exception if the constraint is under specified."""
 408        if self.__is_under_specified:
 409            raise ValueError(
 410                f"Constraint {self.index} is under specified and cannot be modified"
 411            )
 412
 413    def __str__(self):
 414        return self.name
 415
 416    def __repr__(self):
 417        return (
 418            f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound},"
 419            f" ub={self.upper_bound},"
 420            f" var_indices={self.helper.enforced_constraint_var_indices(self.index)},"
 421            f" coefficients={self.helper.enforced_constraint_coefficients(self.index)},"
 422            f" indicator_variable={self.indicator_variable}"
 423            f" indicator_value={self.indicator_value})"
 424        )
 425
 426    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
 427        """Sets the coefficient of the variable in the constraint."""
 428        self.assert_constraint_is_well_defined()
 429        self.__helper.set_enforced_constraint_coefficient(
 430            self.__index, var.index, coeff
 431        )
 432
 433    def add_term(self, var: Variable, coeff: NumberT) -> None:
 434        """Adds var * coeff to the constraint."""
 435        self.assert_constraint_is_well_defined()
 436        self.__helper.safe_add_term_to_enforced_constraint(
 437            self.__index, var.index, coeff
 438        )
 439
 440    def clear_terms(self) -> None:
 441        """Clear all terms of the constraint."""
 442        self.assert_constraint_is_well_defined()
 443        self.__helper.clear_enforced_constraint_terms(self.__index)
 444
 445
 446class Model:
 447    """Methods for building a linear model.
 448
 449    Methods beginning with:
 450
 451    * ```new_``` create integer, boolean, or interval variables.
 452    * ```add_``` create new constraints and add them to the model.
 453    """
 454
 455    def __init__(self):
 456        self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper()
 457
 458    def clone(self) -> "Model":
 459        """Returns a clone of the current model."""
 460        clone = Model()
 461        clone.helper.overwrite_model(self.helper)
 462        return clone
 463
 464    @typing.overload
 465    def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: ...
 466
 467    @typing.overload
 468    def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: ...
 469
 470    def _get_linear_constraints(
 471        self, constraints: Optional[_IndexOrSeries] = None
 472    ) -> _IndexOrSeries:
 473        if constraints is None:
 474            return self.get_linear_constraints()
 475        return constraints
 476
 477    @typing.overload
 478    def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: ...
 479
 480    @typing.overload
 481    def _get_variables(self, variables: pd.Series) -> pd.Series: ...
 482
 483    def _get_variables(
 484        self, variables: Optional[_IndexOrSeries] = None
 485    ) -> _IndexOrSeries:
 486        if variables is None:
 487            return self.get_variables()
 488        return variables
 489
 490    def get_linear_constraints(self) -> pd.Index:
 491        """Gets all linear constraints in the model."""
 492        return pd.Index(
 493            [self.linear_constraint_from_index(i) for i in range(self.num_constraints)],
 494            name="linear_constraint",
 495        )
 496
 497    def get_linear_constraint_expressions(
 498        self, constraints: Optional[_IndexOrSeries] = None
 499    ) -> pd.Series:
 500        """Gets the expressions of all linear constraints in the set.
 501
 502        If `constraints` is a `pd.Index`, then the output will be indexed by the
 503        constraints. If `constraints` is a `pd.Series` indexed by the underlying
 504        dimensions, then the output will be indexed by the same underlying
 505        dimensions.
 506
 507        Args:
 508          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
 509            constraints from which to get the expressions. If unspecified, all
 510            linear constraints will be in scope.
 511
 512        Returns:
 513          pd.Series: The expressions of all linear constraints in the set.
 514        """
 515        return _attribute_series(
 516            # pylint: disable=g-long-lambda
 517            func=lambda c: mbh.FlatExpr(
 518                # pylint: disable=g-complex-comprehension
 519                [
 520                    Variable(self.__helper, var_id)
 521                    for var_id in c.helper.constraint_var_indices(c.index)
 522                ],
 523                c.helper.constraint_coefficients(c.index),
 524                0.0,
 525            ),
 526            values=self._get_linear_constraints(constraints),
 527        )
 528
 529    def get_linear_constraint_lower_bounds(
 530        self, constraints: Optional[_IndexOrSeries] = None
 531    ) -> pd.Series:
 532        """Gets the lower bounds of all linear constraints in the set.
 533
 534        If `constraints` is a `pd.Index`, then the output will be indexed by the
 535        constraints. If `constraints` is a `pd.Series` indexed by the underlying
 536        dimensions, then the output will be indexed by the same underlying
 537        dimensions.
 538
 539        Args:
 540          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
 541            constraints from which to get the lower bounds. If unspecified, all
 542            linear constraints will be in scope.
 543
 544        Returns:
 545          pd.Series: The lower bounds of all linear constraints in the set.
 546        """
 547        return _attribute_series(
 548            func=lambda c: c.lower_bound,  # pylint: disable=protected-access
 549            values=self._get_linear_constraints(constraints),
 550        )
 551
 552    def get_linear_constraint_upper_bounds(
 553        self, constraints: Optional[_IndexOrSeries] = None
 554    ) -> pd.Series:
 555        """Gets the upper bounds of all linear constraints in the set.
 556
 557        If `constraints` is a `pd.Index`, then the output will be indexed by the
 558        constraints. If `constraints` is a `pd.Series` indexed by the underlying
 559        dimensions, then the output will be indexed by the same underlying
 560        dimensions.
 561
 562        Args:
 563          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
 564            constraints. If unspecified, all linear constraints will be in scope.
 565
 566        Returns:
 567          pd.Series: The upper bounds of all linear constraints in the set.
 568        """
 569        return _attribute_series(
 570            func=lambda c: c.upper_bound,  # pylint: disable=protected-access
 571            values=self._get_linear_constraints(constraints),
 572        )
 573
 574    def get_variables(self) -> pd.Index:
 575        """Gets all variables in the model."""
 576        return pd.Index(
 577            [self.var_from_index(i) for i in range(self.num_variables)],
 578            name="variable",
 579        )
 580
 581    def get_variable_lower_bounds(
 582        self, variables: Optional[_IndexOrSeries] = None
 583    ) -> pd.Series:
 584        """Gets the lower bounds of all variables in the set.
 585
 586        If `variables` is a `pd.Index`, then the output will be indexed by the
 587        variables. If `variables` is a `pd.Series` indexed by the underlying
 588        dimensions, then the output will be indexed by the same underlying
 589        dimensions.
 590
 591        Args:
 592          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
 593            from which to get the lower bounds. If unspecified, all variables will
 594            be in scope.
 595
 596        Returns:
 597          pd.Series: The lower bounds of all variables in the set.
 598        """
 599        return _attribute_series(
 600            func=lambda v: v.lower_bound,  # pylint: disable=protected-access
 601            values=self._get_variables(variables),
 602        )
 603
 604    def get_variable_upper_bounds(
 605        self, variables: Optional[_IndexOrSeries] = None
 606    ) -> pd.Series:
 607        """Gets the upper bounds of all variables in the set.
 608
 609        Args:
 610          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
 611            from which to get the upper bounds. If unspecified, all variables will
 612            be in scope.
 613
 614        Returns:
 615          pd.Series: The upper bounds of all variables in the set.
 616        """
 617        return _attribute_series(
 618            func=lambda v: v.upper_bound,  # pylint: disable=protected-access
 619            values=self._get_variables(variables),
 620        )
 621
 622    # Integer variable.
 623
 624    def new_var(
 625        self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str]
 626    ) -> Variable:
 627        """Create an integer variable with domain [lb, ub].
 628
 629        Args:
 630          lb: Lower bound of the variable.
 631          ub: Upper bound of the variable.
 632          is_integer: Indicates if the variable must take integral values.
 633          name: The name of the variable.
 634
 635        Returns:
 636          a variable whose domain is [lb, ub].
 637        """
 638        if name:
 639            return Variable(self.__helper, lb, ub, is_integer, name)
 640        else:
 641            return Variable(self.__helper, lb, ub, is_integer)
 642
 643    def new_int_var(
 644        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
 645    ) -> Variable:
 646        """Create an integer variable with domain [lb, ub].
 647
 648        Args:
 649          lb: Lower bound of the variable.
 650          ub: Upper bound of the variable.
 651          name: The name of the variable.
 652
 653        Returns:
 654          a variable whose domain is [lb, ub].
 655        """
 656
 657        return self.new_var(lb, ub, True, name)
 658
 659    def new_num_var(
 660        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
 661    ) -> Variable:
 662        """Create an integer variable with domain [lb, ub].
 663
 664        Args:
 665          lb: Lower bound of the variable.
 666          ub: Upper bound of the variable.
 667          name: The name of the variable.
 668
 669        Returns:
 670          a variable whose domain is [lb, ub].
 671        """
 672
 673        return self.new_var(lb, ub, False, name)
 674
 675    def new_bool_var(self, name: Optional[str] = None) -> Variable:
 676        """Creates a 0-1 variable with the given name."""
 677        return self.new_var(0, 1, True, name)  # numpy-scalars
 678
 679    def new_constant(self, value: NumberT) -> Variable:
 680        """Declares a constant variable."""
 681        return self.new_var(value, value, False, None)
 682
 683    def new_var_series(
 684        self,
 685        name: str,
 686        index: pd.Index,
 687        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
 688        upper_bounds: Union[NumberT, pd.Series] = math.inf,
 689        is_integral: Union[bool, pd.Series] = False,
 690    ) -> pd.Series:
 691        """Creates a series of (scalar-valued) variables with the given name.
 692
 693        Args:
 694          name (str): Required. The name of the variable set.
 695          index (pd.Index): Required. The index to use for the variable set.
 696          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
 697            variables in the set. If a `pd.Series` is passed in, it will be based on
 698            the corresponding values of the pd.Series. Defaults to -inf.
 699          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
 700            variables in the set. If a `pd.Series` is passed in, it will be based on
 701            the corresponding values of the pd.Series. Defaults to +inf.
 702          is_integral (bool, pd.Series): Optional. Indicates if the variable can
 703            only take integer values. If a `pd.Series` is passed in, it will be
 704            based on the corresponding values of the pd.Series. Defaults to False.
 705
 706        Returns:
 707          pd.Series: The variable set indexed by its corresponding dimensions.
 708
 709        Raises:
 710          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 711          ValueError: if the `name` is not a valid identifier or already exists.
 712          ValueError: if the `lowerbound` is greater than the `upperbound`.
 713          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
 714          does not match the input index.
 715        """
 716        if not isinstance(index, pd.Index):
 717            raise TypeError("Non-index object is used as index")
 718        if not name.isidentifier():
 719            raise ValueError(f"name={name!r} is not a valid identifier")
 720        if (
 721            mbn.is_a_number(lower_bounds)
 722            and mbn.is_a_number(upper_bounds)
 723            and lower_bounds > upper_bounds
 724        ):
 725            raise ValueError(
 726                f"lower_bound={lower_bounds} is greater than"
 727                f" upper_bound={upper_bounds} for variable set={name!r}"
 728            )
 729        if (
 730            isinstance(is_integral, bool)
 731            and is_integral
 732            and mbn.is_a_number(lower_bounds)
 733            and mbn.is_a_number(upper_bounds)
 734            and math.isfinite(lower_bounds)
 735            and math.isfinite(upper_bounds)
 736            and math.ceil(lower_bounds) > math.floor(upper_bounds)
 737        ):
 738            raise ValueError(
 739                f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}"
 740                f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}"
 741                f" for variable set={name!r}"
 742            )
 743        lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
 744        upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
 745        is_integrals = _convert_to_series_and_validate_index(is_integral, index)
 746        return pd.Series(
 747            index=index,
 748            data=[
 749                # pylint: disable=g-complex-comprehension
 750                Variable(
 751                    self.__helper,
 752                    lower_bounds[i],
 753                    upper_bounds[i],
 754                    is_integrals[i],
 755                    f"{name}[{i}]",
 756                )
 757                for i in index
 758            ],
 759        )
 760
 761    def new_num_var_series(
 762        self,
 763        name: str,
 764        index: pd.Index,
 765        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
 766        upper_bounds: Union[NumberT, pd.Series] = math.inf,
 767    ) -> pd.Series:
 768        """Creates a series of continuous variables with the given name.
 769
 770        Args:
 771          name (str): Required. The name of the variable set.
 772          index (pd.Index): Required. The index to use for the variable set.
 773          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
 774            variables in the set. If a `pd.Series` is passed in, it will be based on
 775            the corresponding values of the pd.Series. Defaults to -inf.
 776          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
 777            variables in the set. If a `pd.Series` is passed in, it will be based on
 778            the corresponding values of the pd.Series. Defaults to +inf.
 779
 780        Returns:
 781          pd.Series: The variable set indexed by its corresponding dimensions.
 782
 783        Raises:
 784          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 785          ValueError: if the `name` is not a valid identifier or already exists.
 786          ValueError: if the `lowerbound` is greater than the `upperbound`.
 787          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
 788          does not match the input index.
 789        """
 790        return self.new_var_series(name, index, lower_bounds, upper_bounds, False)
 791
 792    def new_int_var_series(
 793        self,
 794        name: str,
 795        index: pd.Index,
 796        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
 797        upper_bounds: Union[NumberT, pd.Series] = math.inf,
 798    ) -> pd.Series:
 799        """Creates a series of integer variables with the given name.
 800
 801        Args:
 802          name (str): Required. The name of the variable set.
 803          index (pd.Index): Required. The index to use for the variable set.
 804          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
 805            variables in the set. If a `pd.Series` is passed in, it will be based on
 806            the corresponding values of the pd.Series. Defaults to -inf.
 807          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
 808            variables in the set. If a `pd.Series` is passed in, it will be based on
 809            the corresponding values of the pd.Series. Defaults to +inf.
 810
 811        Returns:
 812          pd.Series: The variable set indexed by its corresponding dimensions.
 813
 814        Raises:
 815          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 816          ValueError: if the `name` is not a valid identifier or already exists.
 817          ValueError: if the `lowerbound` is greater than the `upperbound`.
 818          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
 819          does not match the input index.
 820        """
 821        return self.new_var_series(name, index, lower_bounds, upper_bounds, True)
 822
 823    def new_bool_var_series(
 824        self,
 825        name: str,
 826        index: pd.Index,
 827    ) -> pd.Series:
 828        """Creates a series of Boolean variables with the given name.
 829
 830        Args:
 831          name (str): Required. The name of the variable set.
 832          index (pd.Index): Required. The index to use for the variable set.
 833
 834        Returns:
 835          pd.Series: The variable set indexed by its corresponding dimensions.
 836
 837        Raises:
 838          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 839          ValueError: if the `name` is not a valid identifier or already exists.
 840          ValueError: if the `lowerbound` is greater than the `upperbound`.
 841          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
 842          does not match the input index.
 843        """
 844        return self.new_var_series(name, index, 0, 1, True)
 845
 846    def var_from_index(self, index: IntegerT) -> Variable:
 847        """Rebuilds a variable object from the model and its index."""
 848        return Variable(self.__helper, index)
 849
 850    # Linear constraints.
 851
 852    def add_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
 853        self,
 854        linear_expr: LinearExprT,
 855        lb: NumberT = -math.inf,
 856        ub: NumberT = math.inf,
 857        name: Optional[str] = None,
 858    ) -> LinearConstraint:
 859        """Adds the constraint: `lb <= linear_expr <= ub` with the given name."""
 860        ct = LinearConstraint(self.__helper)
 861        if name:
 862            self.__helper.set_constraint_name(ct.index, name)
 863        if mbn.is_a_number(linear_expr):
 864            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
 865            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
 866        elif isinstance(linear_expr, LinearExpr):
 867            flat_expr = mbh.FlatExpr(linear_expr)
 868            # pylint: disable=protected-access
 869            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset)
 870            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset)
 871            self.__helper.add_terms_to_constraint(
 872                ct.index, flat_expr.vars, flat_expr.coeffs
 873            )
 874        else:
 875            raise TypeError(
 876                "Not supported:"
 877                f" Model.add_linear_constraint({type(linear_expr).__name__!r})"
 878            )
 879        return ct
 880
 881    def add(
 882        self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None
 883    ) -> Union[LinearConstraint, pd.Series]:
 884        """Adds a `BoundedLinearExpression` to the model.
 885
 886        Args:
 887          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
 888          name: An optional name.
 889
 890        Returns:
 891          An instance of the `Constraint` class.
 892
 893        Note that a special treatment is done when the argument does not contain any
 894        variable, and thus evaluates to True or False.
 895
 896        `model.add(True)` will create a constraint 0 <= empty sum <= 0.
 897        The constraint will be marked as under specified, and cannot be modified
 898        thereafter.
 899
 900        `model.add(False)` will create a constraint inf <= empty sum <= -inf. The
 901        constraint will be marked as under specified, and cannot be modified
 902        thereafter.
 903
 904        you can check the if a constraint is under specified by reading the
 905        `LinearConstraint.is_under_specified` property.
 906        """
 907        if isinstance(ct, mbh.BoundedLinearExpression):
 908            return _add_linear_constraint_to_helper(ct, self.__helper, name)
 909        elif isinstance(ct, bool):
 910            return _add_linear_constraint_to_helper(ct, self.__helper, name)
 911        elif isinstance(ct, pd.Series):
 912            return pd.Series(
 913                index=ct.index,
 914                data=[
 915                    _add_linear_constraint_to_helper(
 916                        expr, self.__helper, f"{name}[{i}]"
 917                    )
 918                    for (i, expr) in zip(ct.index, ct)
 919                ],
 920            )
 921        else:
 922            raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})")
 923
 924    def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint:
 925        """Rebuilds a linear constraint object from the model and its index."""
 926        return LinearConstraint(self.__helper, index=index)
 927
 928    # Enforced Linear constraints.
 929
 930    def add_enforced_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
 931        self,
 932        linear_expr: LinearExprT,
 933        ivar: "Variable",
 934        ivalue: bool,
 935        lb: NumberT = -math.inf,
 936        ub: NumberT = math.inf,
 937        name: Optional[str] = None,
 938    ) -> EnforcedLinearConstraint:
 939        """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name."""
 940        ct = EnforcedLinearConstraint(self.__helper)
 941        ct.indicator_variable = ivar
 942        ct.indicator_value = ivalue
 943        if name:
 944            self.__helper.set_constraint_name(ct.index, name)
 945        if mbn.is_a_number(linear_expr):
 946            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
 947            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
 948        elif isinstance(linear_expr, LinearExpr):
 949            flat_expr = mbh.FlatExpr(linear_expr)
 950            # pylint: disable=protected-access
 951            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset)
 952            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset)
 953            self.__helper.add_terms_to_constraint(
 954                ct.index, flat_expr.vars, flat_expr.coeffs
 955            )
 956        else:
 957            raise TypeError(
 958                "Not supported:"
 959                f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})"
 960            )
 961        return ct
 962
 963    def add_enforced(
 964        self,
 965        ct: Union[ConstraintT, pd.Series],
 966        var: Union[Variable, pd.Series],
 967        value: Union[bool, pd.Series],
 968        name: Optional[str] = None,
 969    ) -> Union[EnforcedLinearConstraint, pd.Series]:
 970        """Adds a `ivar == ivalue => BoundedLinearExpression` to the model.
 971
 972        Args:
 973          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
 974          var: The indicator variable
 975          value: the indicator value
 976          name: An optional name.
 977
 978        Returns:
 979          An instance of the `Constraint` class.
 980
 981        Note that a special treatment is done when the argument does not contain any
 982        variable, and thus evaluates to True or False.
 983
 984        model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty
 985        sum <= 0
 986
 987        model.add_enforced(False, var, value) will create a constraint inf <=
 988        empty sum <= -inf
 989
 990        you can check the if a constraint is always false (lb=inf, ub=-inf) by
 991        calling EnforcedLinearConstraint.is_always_false()
 992        """
 993        if isinstance(ct, mbh.BoundedLinearExpression):
 994            return _add_enforced_linear_constraint_to_helper(
 995                ct, self.__helper, var, value, name
 996            )
 997        elif (
 998            isinstance(ct, bool)
 999            and isinstance(var, Variable)
1000            and isinstance(value, bool)
1001        ):
1002            return _add_enforced_linear_constraint_to_helper(
1003                ct, self.__helper, var, value, name
1004            )
1005        elif isinstance(ct, pd.Series):
1006            ivar_series = _convert_to_var_series_and_validate_index(var, ct.index)
1007            ivalue_series = _convert_to_series_and_validate_index(value, ct.index)
1008            return pd.Series(
1009                index=ct.index,
1010                data=[
1011                    _add_enforced_linear_constraint_to_helper(
1012                        expr,
1013                        self.__helper,
1014                        ivar_series[i],
1015                        ivalue_series[i],
1016                        f"{name}[{i}]",
1017                    )
1018                    for (i, expr) in zip(ct.index, ct)
1019                ],
1020            )
1021        else:
1022            raise TypeError(f"Not supported: Model.add_enforced({type(ct).__name__!r}")
1023
1024    def enforced_linear_constraint_from_index(
1025        self, index: IntegerT
1026    ) -> EnforcedLinearConstraint:
1027        """Rebuilds an enforced linear constraint object from the model and its index."""
1028        return EnforcedLinearConstraint(self.__helper, index=index)
1029
1030    # Objective.
1031    def minimize(self, linear_expr: LinearExprT) -> None:
1032        """Minimizes the given objective."""
1033        self.__optimize(linear_expr, False)
1034
1035    def maximize(self, linear_expr: LinearExprT) -> None:
1036        """Maximizes the given objective."""
1037        self.__optimize(linear_expr, True)
1038
1039    def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None:
1040        """Defines the objective."""
1041        self.helper.clear_objective()
1042        self.__helper.set_maximize(maximize)
1043        if mbn.is_a_number(linear_expr):
1044            self.helper.set_objective_offset(linear_expr)
1045        elif isinstance(linear_expr, Variable):
1046            self.helper.set_var_objective_coefficient(linear_expr.index, 1.0)
1047        elif isinstance(linear_expr, LinearExpr):
1048            flat_expr = mbh.FlatExpr(linear_expr)
1049            # pylint: disable=protected-access
1050            self.helper.set_objective_offset(flat_expr.offset)
1051            var_indices = [var.index for var in flat_expr.vars]
1052            self.helper.set_objective_coefficients(var_indices, flat_expr.coeffs)
1053        else:
1054            raise TypeError(
1055                "Not supported:"
1056                f" Model.minimize/maximize({type(linear_expr).__name__!r})"
1057            )
1058
1059    @property
1060    def objective_offset(self) -> np.double:
1061        """Returns the fixed offset of the objective."""
1062        return self.__helper.objective_offset()
1063
1064    @objective_offset.setter
1065    def objective_offset(self, value: NumberT) -> None:
1066        self.__helper.set_objective_offset(value)
1067
1068    def objective_expression(self) -> "LinearExpr":
1069        """Returns the expression to optimize."""
1070        variables: list[Variable] = []
1071        coefficients: list[numbers.Real] = []
1072        for variable in self.get_variables():
1073            coeff = self.__helper.var_objective_coefficient(variable.index)
1074            if coeff != 0.0:
1075                variables.append(variable)
1076                coefficients.append(coeff)
1077        return mbh.FlatExpr(variables, coefficients, self.__helper.objective_offset())
1078
1079    # Hints.
1080    def clear_hints(self):
1081        """Clears all solution hints."""
1082        self.__helper.clear_hints()
1083
1084    def add_hint(self, var: Variable, value: NumberT) -> None:
1085        """Adds var == value as a hint to the model.
1086
1087        Args:
1088          var: The variable of the hint
1089          value: The value of the hint
1090
1091        Note that variables must not appear more than once in the list of hints.
1092        """
1093        self.__helper.add_hint(var.index, value)
1094
1095    # Input/Output
1096    def export_to_lp_string(self, obfuscate: bool = False) -> str:
1097        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1098        options.obfuscate = obfuscate
1099        return self.__helper.export_to_lp_string(options)
1100
1101    def export_to_mps_string(self, obfuscate: bool = False) -> str:
1102        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1103        options.obfuscate = obfuscate
1104        return self.__helper.export_to_mps_string(options)
1105
1106    def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool:
1107        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1108        options.obfuscate = obfuscate
1109        return self.__helper.write_to_mps_file(filename, options)
1110
1111    def export_to_proto(self) -> linear_solver_pb2.MPModelProto:
1112        """Exports the optimization model to a ProtoBuf format."""
1113        return mbh.to_mpmodel_proto(self.__helper)
1114
1115    def import_from_mps_string(self, mps_string: str) -> bool:
1116        """Reads a model from a MPS string."""
1117        return self.__helper.import_from_mps_string(mps_string)
1118
1119    def import_from_mps_file(self, mps_file: str) -> bool:
1120        """Reads a model from a .mps file."""
1121        return self.__helper.import_from_mps_file(mps_file)
1122
1123    def import_from_lp_string(self, lp_string: str) -> bool:
1124        """Reads a model from a LP string.
1125
1126        Note that this code is very limited, and will not support any real lp.
1127        It is only intented to be use to parse test lp problems.
1128
1129        Args:
1130          lp_string: The LP string to import.
1131
1132        Returns:
1133          True if the import was successful.
1134        """
1135        return self.__helper.import_from_lp_string(lp_string)
1136
1137    def import_from_lp_file(self, lp_file: str) -> bool:
1138        """Reads a model from a .lp file.
1139
1140        Note that this code is very limited, and will not support any real lp.
1141        It is only intented to be use to parse test lp problems.
1142
1143        Args:
1144          lp_file: The LP file to import.
1145
1146        Returns:
1147          True if the import was successful.
1148        """
1149        return self.__helper.import_from_lp_file(lp_file)
1150
1151    def import_from_proto_file(self, proto_file: str) -> bool:
1152        """Reads a model from a proto file."""
1153        return self.__helper.read_model_from_proto_file(proto_file)
1154
1155    def export_to_proto_file(self, proto_file: str) -> bool:
1156        """Writes a model to a proto file."""
1157        return self.__helper.write_model_to_proto_file(proto_file)
1158
1159    # Model getters and Setters
1160
1161    @property
1162    def num_variables(self) -> int:
1163        """Returns the number of variables in the model."""
1164        return self.__helper.num_variables()
1165
1166    @property
1167    def num_constraints(self) -> int:
1168        """The number of constraints in the model."""
1169        return self.__helper.num_constraints()
1170
1171    @property
1172    def name(self) -> str:
1173        """The name of the model."""
1174        return self.__helper.name()
1175
1176    @name.setter
1177    def name(self, name: str):
1178        self.__helper.set_name(name)
1179
1180    @property
1181    def helper(self) -> mbh.ModelBuilderHelper:
1182        """Returns the model builder helper."""
1183        return self.__helper
1184
1185
1186class Solver:
1187    """Main solver class.
1188
1189    The purpose of this class is to search for a solution to the model provided
1190    to the solve() method.
1191
1192    Once solve() is called, this class allows inspecting the solution found
1193    with the value() method, as well as general statistics about the solve
1194    procedure.
1195    """
1196
1197    def __init__(self, solver_name: str):
1198        self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name)
1199        self.log_callback: Optional[Callable[[str], None]] = None
1200
1201    def solver_is_supported(self) -> bool:
1202        """Checks whether the requested solver backend was found."""
1203        return self.__solve_helper.solver_is_supported()
1204
1205    # Solver backend and parameters.
1206    def set_time_limit_in_seconds(self, limit: NumberT) -> None:
1207        """Sets a time limit for the solve() call."""
1208        self.__solve_helper.set_time_limit_in_seconds(limit)
1209
1210    def set_solver_specific_parameters(self, parameters: str) -> None:
1211        """Sets parameters specific to the solver backend."""
1212        self.__solve_helper.set_solver_specific_parameters(parameters)
1213
1214    def enable_output(self, enabled: bool) -> None:
1215        """Controls the solver backend logs."""
1216        self.__solve_helper.enable_output(enabled)
1217
1218    def solve(self, model: Model) -> SolveStatus:
1219        """Solves a problem and passes each solution to the callback if not null."""
1220        if self.log_callback is not None:
1221            self.__solve_helper.set_log_callback(self.log_callback)
1222        else:
1223            self.__solve_helper.clear_log_callback()
1224        self.__solve_helper.solve(model.helper)
1225        return SolveStatus(self.__solve_helper.status())
1226
1227    def stop_search(self):
1228        """Stops the current search asynchronously."""
1229        self.__solve_helper.interrupt_solve()
1230
1231    def value(self, expr: LinearExprT) -> np.double:
1232        """Returns the value of a linear expression after solve."""
1233        if not self.__solve_helper.has_solution():
1234            return pd.NA
1235        if mbn.is_a_number(expr):
1236            return expr
1237        elif isinstance(expr, LinearExpr):
1238            return self.__solve_helper.expression_value(expr)
1239        else:
1240            raise TypeError(f"Unknown expression {type(expr).__name__!r}")
1241
1242    def values(self, variables: _IndexOrSeries) -> pd.Series:
1243        """Returns the values of the input variables.
1244
1245        If `variables` is a `pd.Index`, then the output will be indexed by the
1246        variables. If `variables` is a `pd.Series` indexed by the underlying
1247        dimensions, then the output will be indexed by the same underlying
1248        dimensions.
1249
1250        Args:
1251          variables (Union[pd.Index, pd.Series]): The set of variables from which to
1252            get the values.
1253
1254        Returns:
1255          pd.Series: The values of all variables in the set.
1256        """
1257        if not self.__solve_helper.has_solution():
1258            return _attribute_series(func=lambda v: pd.NA, values=variables)
1259        return _attribute_series(
1260            func=lambda v: self.__solve_helper.variable_value(v.index),
1261            values=variables,
1262        )
1263
1264    def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series:
1265        """Returns the reduced cost of the input variables.
1266
1267        If `variables` is a `pd.Index`, then the output will be indexed by the
1268        variables. If `variables` is a `pd.Series` indexed by the underlying
1269        dimensions, then the output will be indexed by the same underlying
1270        dimensions.
1271
1272        Args:
1273          variables (Union[pd.Index, pd.Series]): The set of variables from which to
1274            get the values.
1275
1276        Returns:
1277          pd.Series: The reduced cost of all variables in the set.
1278        """
1279        if not self.__solve_helper.has_solution():
1280            return _attribute_series(func=lambda v: pd.NA, values=variables)
1281        return _attribute_series(
1282            func=lambda v: self.__solve_helper.reduced_cost(v.index),
1283            values=variables,
1284        )
1285
1286    def reduced_cost(self, var: Variable) -> np.double:
1287        """Returns the reduced cost of a linear expression after solve."""
1288        if not self.__solve_helper.has_solution():
1289            return pd.NA
1290        return self.__solve_helper.reduced_cost(var.index)
1291
1292    def dual_values(self, constraints: _IndexOrSeries) -> pd.Series:
1293        """Returns the dual values of the input constraints.
1294
1295        If `constraints` is a `pd.Index`, then the output will be indexed by the
1296        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1297        dimensions, then the output will be indexed by the same underlying
1298        dimensions.
1299
1300        Args:
1301          constraints (Union[pd.Index, pd.Series]): The set of constraints from
1302            which to get the dual values.
1303
1304        Returns:
1305          pd.Series: The dual_values of all constraints in the set.
1306        """
1307        if not self.__solve_helper.has_solution():
1308            return _attribute_series(func=lambda v: pd.NA, values=constraints)
1309        return _attribute_series(
1310            func=lambda v: self.__solve_helper.dual_value(v.index),
1311            values=constraints,
1312        )
1313
1314    def dual_value(self, ct: LinearConstraint) -> np.double:
1315        """Returns the dual value of a linear constraint after solve."""
1316        if not self.__solve_helper.has_solution():
1317            return pd.NA
1318        return self.__solve_helper.dual_value(ct.index)
1319
1320    def activity(self, ct: LinearConstraint) -> np.double:
1321        """Returns the activity of a linear constraint after solve."""
1322        if not self.__solve_helper.has_solution():
1323            return pd.NA
1324        return self.__solve_helper.activity(ct.index)
1325
1326    @property
1327    def objective_value(self) -> np.double:
1328        """Returns the value of the objective after solve."""
1329        if not self.__solve_helper.has_solution():
1330            return pd.NA
1331        return self.__solve_helper.objective_value()
1332
1333    @property
1334    def best_objective_bound(self) -> np.double:
1335        """Returns the best lower (upper) bound found when min(max)imizing."""
1336        if not self.__solve_helper.has_solution():
1337            return pd.NA
1338        return self.__solve_helper.best_objective_bound()
1339
1340    @property
1341    def status_string(self) -> str:
1342        """Returns additional information of the last solve.
1343
1344        It can describe why the model is invalid.
1345        """
1346        return self.__solve_helper.status_string()
1347
1348    @property
1349    def wall_time(self) -> np.double:
1350        return self.__solve_helper.wall_time()
1351
1352    @property
1353    def user_time(self) -> np.double:
1354        return self.__solve_helper.user_time()
1355
1356
1357def _get_index(obj: _IndexOrSeries) -> pd.Index:
1358    """Returns the indices of `obj` as a `pd.Index`."""
1359    if isinstance(obj, pd.Series):
1360        return obj.index
1361    return obj
1362
1363
1364def _attribute_series(
1365    *,
1366    func: Callable[[_VariableOrConstraint], NumberT],
1367    values: _IndexOrSeries,
1368) -> pd.Series:
1369    """Returns the attributes of `values`.
1370
1371    Args:
1372      func: The function to call for getting the attribute data.
1373      values: The values that the function will be applied (element-wise) to.
1374
1375    Returns:
1376      pd.Series: The attribute values.
1377    """
1378    return pd.Series(
1379        data=[func(v) for v in values],
1380        index=_get_index(values),
1381    )
1382
1383
1384def _convert_to_series_and_validate_index(
1385    value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index
1386) -> pd.Series:
1387    """Returns a pd.Series of the given index with the corresponding values.
1388
1389    Args:
1390      value_or_series: the values to be converted (if applicable).
1391      index: the index of the resulting pd.Series.
1392
1393    Returns:
1394      pd.Series: The set of values with the given index.
1395
1396    Raises:
1397      TypeError: If the type of `value_or_series` is not recognized.
1398      ValueError: If the index does not match.
1399    """
1400    if mbn.is_a_number(value_or_series) or isinstance(value_or_series, bool):
1401        result = pd.Series(data=value_or_series, index=index)
1402    elif isinstance(value_or_series, pd.Series):
1403        if value_or_series.index.equals(index):
1404            result = value_or_series
1405        else:
1406            raise ValueError("index does not match")
1407    else:
1408        raise TypeError("invalid type={type(value_or_series).__name!r}")
1409    return result
1410
1411
1412def _convert_to_var_series_and_validate_index(
1413    var_or_series: Union["Variable", pd.Series], index: pd.Index
1414) -> pd.Series:
1415    """Returns a pd.Series of the given index with the corresponding values.
1416
1417    Args:
1418      var_or_series: the variables to be converted (if applicable).
1419      index: the index of the resulting pd.Series.
1420
1421    Returns:
1422      pd.Series: The set of values with the given index.
1423
1424    Raises:
1425      TypeError: If the type of `value_or_series` is not recognized.
1426      ValueError: If the index does not match.
1427    """
1428    if isinstance(var_or_series, Variable):
1429        result = pd.Series(data=var_or_series, index=index)
1430    elif isinstance(var_or_series, pd.Series):
1431        if var_or_series.index.equals(index):
1432            result = var_or_series
1433        else:
1434            raise ValueError("index does not match")
1435    else:
1436        raise TypeError("invalid type={type(value_or_series).__name!r}")
1437    return result
1438
1439
1440# Compatibility.
1441ModelBuilder = Model
1442ModelSolver = Solver
NumberT = int | float | numbers.Real | numpy.number
IntegerT = int | numbers.Integral | numpy.integer
LinearExprT = LinearExpr | int | float | numbers.Real | numpy.number
ConstraintT = BoundedLinearExpression | bool

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)
class BoundedLinearExpression(pybind11_builtins.pybind11_object):

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

(arg0: ortools.linear_solver.python.model_builder_helper.FlatExpr) -> list[operations_research::mb::Variable]

def variable_indices(unknown):
class LinearExpr(pybind11_builtins.pybind11_object):

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

sum(args, *kwargs) -> ortools.linear_solver.python.model_builder_helper.LinearExpr

Creates sum(expressions) [+ constant].

def weighted_sum(unknown):

weighted_sum(expressions: Sequence, coefficients: list[float], *, constant: float = 0.0) -> ortools.linear_solver.python.model_builder_helper.LinearExpr

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

def term(unknown):

term(args, *kwargs) Overloaded function.

  1. term(expr: ortools.linear_solver.python.model_builder_helper.LinearExpr, coeff: float) -> ortools.linear_solver.python.model_builder_helper.LinearExpr

Returns expr * coeff.

  1. term(expr: ortools.linear_solver.python.model_builder_helper.LinearExpr, coeff: float, *, constant: float) -> ortools.linear_solver.python.model_builder_helper.LinearExpr

Returns expr * coeff [+ constant].

  1. term(value: float, coeff: float, *, constant: float) -> ortools.linear_solver.python.model_builder_helper.LinearExpr

Returns value * coeff [+ constant].

def affine(unknown):

affine(args, *kwargs) Overloaded function.

  1. affine(expr: ortools.linear_solver.python.model_builder_helper.LinearExpr, coeff: float, constant: float = 0.0) -> ortools.linear_solver.python.model_builder_helper.LinearExpr

Returns expr * coeff + constant.

  1. affine(value: float, coeff: float, constant: float = 0.0) -> ortools.linear_solver.python.model_builder_helper.LinearExpr

Returns value * coeff + constant.

def constant(unknown):

constant(value: float) -> ortools.linear_solver.python.model_builder_helper.LinearExpr

Returns a constant linear expression.

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>

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

__init__(args, *kwargs) Overloaded function.

  1. __init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: int) -> None

  2. __init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: float, arg2: float, arg3: bool) -> None

  3. __init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: float, arg2: float, arg3: bool, arg4: str) -> None

  4. __init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: int, arg2: int, arg3: bool) -> None

  5. __init__(self: ortools.linear_solver.python.model_builder_helper.Variable, arg0: operations_research::mb::ModelBuilderHelper, arg1: int, arg2: int, arg3: bool, arg4: str) -> None

helper

(arg0: ortools.linear_solver.python.model_builder_helper.Variable) -> operations_research::mb::ModelBuilderHelper

objective_coefficient
class LinearConstraint:
182class LinearConstraint:
183    """Stores a linear equation.
184
185    Example:
186        x = model.new_num_var(0, 10, 'x')
187        y = model.new_num_var(0, 10, 'y')
188
189        linear_constraint = model.add(x + 2 * y == 5)
190    """
191
192    def __init__(
193        self,
194        helper: mbh.ModelBuilderHelper,
195        *,
196        index: Optional[IntegerT] = None,
197        is_under_specified: bool = False,
198    ) -> None:
199        """LinearConstraint constructor.
200
201        Args:
202          helper: The pybind11 ModelBuilderHelper.
203          index: If specified, recreates a wrapper to an existing linear constraint.
204          is_under_specified: indicates if the constraint was created by
205            model.add(bool).
206        """
207        if index is None:
208            self.__index = helper.add_linear_constraint()
209        else:
210            self.__index = index
211        self.__helper: mbh.ModelBuilderHelper = helper
212        self.__is_under_specified = is_under_specified
213
214    def __hash__(self):
215        return hash((self.__helper, self.__index))
216
217    @property
218    def index(self) -> IntegerT:
219        """Returns the index of the constraint in the helper."""
220        return self.__index
221
222    @property
223    def helper(self) -> mbh.ModelBuilderHelper:
224        """Returns the ModelBuilderHelper instance."""
225        return self.__helper
226
227    @property
228    def lower_bound(self) -> np.double:
229        return self.__helper.constraint_lower_bound(self.__index)
230
231    @lower_bound.setter
232    def lower_bound(self, bound: NumberT) -> None:
233        self.assert_constraint_is_well_defined()
234        self.__helper.set_constraint_lower_bound(self.__index, bound)
235
236    @property
237    def upper_bound(self) -> np.double:
238        return self.__helper.constraint_upper_bound(self.__index)
239
240    @upper_bound.setter
241    def upper_bound(self, bound: NumberT) -> None:
242        self.assert_constraint_is_well_defined()
243        self.__helper.set_constraint_upper_bound(self.__index, bound)
244
245    @property
246    def name(self) -> str:
247        constraint_name = self.__helper.constraint_name(self.__index)
248        if constraint_name:
249            return constraint_name
250        return f"linear_constraint#{self.__index}"
251
252    @name.setter
253    def name(self, name: str) -> None:
254        return self.__helper.set_constraint_name(self.__index, name)
255
256    @property
257    def is_under_specified(self) -> bool:
258        """Returns True if the constraint is under specified.
259
260        Usually, it means that it was created by model.add(False) or model.add(True)
261        The effect is that modifying the constraint will raise an exception.
262        """
263        return self.__is_under_specified
264
265    def assert_constraint_is_well_defined(self) -> None:
266        """Raises an exception if the constraint is under specified."""
267        if self.__is_under_specified:
268            raise ValueError(
269                f"Constraint {self.index} is under specified and cannot be modified"
270            )
271
272    def __str__(self):
273        return self.name
274
275    def __repr__(self):
276        return (
277            f"LinearConstraint({self.name}, lb={self.lower_bound},"
278            f" ub={self.upper_bound},"
279            f" var_indices={self.helper.constraint_var_indices(self.index)},"
280            f" coefficients={self.helper.constraint_coefficients(self.index)})"
281        )
282
283    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
284        """Sets the coefficient of the variable in the constraint."""
285        self.assert_constraint_is_well_defined()
286        self.__helper.set_constraint_coefficient(self.__index, var.index, coeff)
287
288    def add_term(self, var: Variable, coeff: NumberT) -> None:
289        """Adds var * coeff to the constraint."""
290        self.assert_constraint_is_well_defined()
291        self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)
292
293    def clear_terms(self) -> None:
294        """Clear all terms of the constraint."""
295        self.assert_constraint_is_well_defined()
296        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: int | numbers.Integral | numpy.integer | None = None, is_under_specified: bool = False)
192    def __init__(
193        self,
194        helper: mbh.ModelBuilderHelper,
195        *,
196        index: Optional[IntegerT] = None,
197        is_under_specified: bool = False,
198    ) -> None:
199        """LinearConstraint constructor.
200
201        Args:
202          helper: The pybind11 ModelBuilderHelper.
203          index: If specified, recreates a wrapper to an existing linear constraint.
204          is_under_specified: indicates if the constraint was created by
205            model.add(bool).
206        """
207        if index is None:
208            self.__index = helper.add_linear_constraint()
209        else:
210            self.__index = index
211        self.__helper: mbh.ModelBuilderHelper = helper
212        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: int | numbers.Integral | numpy.integer
217    @property
218    def index(self) -> IntegerT:
219        """Returns the index of the constraint in the helper."""
220        return self.__index

Returns the index of the constraint in the helper.

222    @property
223    def helper(self) -> mbh.ModelBuilderHelper:
224        """Returns the ModelBuilderHelper instance."""
225        return self.__helper

Returns the ModelBuilderHelper instance.

lower_bound: numpy.float64
227    @property
228    def lower_bound(self) -> np.double:
229        return self.__helper.constraint_lower_bound(self.__index)
upper_bound: numpy.float64
236    @property
237    def upper_bound(self) -> np.double:
238        return self.__helper.constraint_upper_bound(self.__index)
name: str
245    @property
246    def name(self) -> str:
247        constraint_name = self.__helper.constraint_name(self.__index)
248        if constraint_name:
249            return constraint_name
250        return f"linear_constraint#{self.__index}"
is_under_specified: bool
256    @property
257    def is_under_specified(self) -> bool:
258        """Returns True if the constraint is under specified.
259
260        Usually, it means that it was created by model.add(False) or model.add(True)
261        The effect is that modifying the constraint will raise an exception.
262        """
263        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:
265    def assert_constraint_is_well_defined(self) -> None:
266        """Raises an exception if the constraint is under specified."""
267        if self.__is_under_specified:
268            raise ValueError(
269                f"Constraint {self.index} is under specified and cannot be modified"
270            )

Raises an exception if the constraint is under specified.

def set_coefficient( self, var: Variable, coeff: int | float | numbers.Real | numpy.number) -> None:
283    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
284        """Sets the coefficient of the variable in the constraint."""
285        self.assert_constraint_is_well_defined()
286        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: int | float | numbers.Real | numpy.number) -> None:
288    def add_term(self, var: Variable, coeff: NumberT) -> None:
289        """Adds var * coeff to the constraint."""
290        self.assert_constraint_is_well_defined()
291        self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff)

Adds var * coeff to the constraint.

def clear_terms(self) -> None:
293    def clear_terms(self) -> None:
294        """Clear all terms of the constraint."""
295        self.assert_constraint_is_well_defined()
296        self.__helper.clear_constraint_terms(self.__index)

Clear all terms of the constraint.

class EnforcedLinearConstraint:
299class EnforcedLinearConstraint:
300    """Stores an enforced linear equation, also name indicator constraint.
301
302    Example:
303        x = model.new_num_var(0, 10, 'x')
304        y = model.new_num_var(0, 10, 'y')
305        z = model.new_bool_var('z')
306
307        enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False)
308    """
309
310    def __init__(
311        self,
312        helper: mbh.ModelBuilderHelper,
313        *,
314        index: Optional[IntegerT] = None,
315        is_under_specified: bool = False,
316    ) -> None:
317        """EnforcedLinearConstraint constructor.
318
319        Args:
320          helper: The pybind11 ModelBuilderHelper.
321          index: If specified, recreates a wrapper to an existing linear constraint.
322          is_under_specified: indicates if the constraint was created by
323            model.add(bool).
324        """
325        if index is None:
326            self.__index = helper.add_enforced_linear_constraint()
327        else:
328            if not helper.is_enforced_linear_constraint(index):
329                raise ValueError(
330                    f"the given index {index} does not refer to an enforced linear"
331                    " constraint"
332                )
333
334            self.__index = index
335        self.__helper: mbh.ModelBuilderHelper = helper
336        self.__is_under_specified = is_under_specified
337
338    @property
339    def index(self) -> IntegerT:
340        """Returns the index of the constraint in the helper."""
341        return self.__index
342
343    @property
344    def helper(self) -> mbh.ModelBuilderHelper:
345        """Returns the ModelBuilderHelper instance."""
346        return self.__helper
347
348    @property
349    def lower_bound(self) -> np.double:
350        return self.__helper.enforced_constraint_lower_bound(self.__index)
351
352    @lower_bound.setter
353    def lower_bound(self, bound: NumberT) -> None:
354        self.assert_constraint_is_well_defined()
355        self.__helper.set_enforced_constraint_lower_bound(self.__index, bound)
356
357    @property
358    def upper_bound(self) -> np.double:
359        return self.__helper.enforced_constraint_upper_bound(self.__index)
360
361    @upper_bound.setter
362    def upper_bound(self, bound: NumberT) -> None:
363        self.assert_constraint_is_well_defined()
364        self.__helper.set_enforced_constraint_upper_bound(self.__index, bound)
365
366    @property
367    def indicator_variable(self) -> "Variable":
368        enforcement_var_index = (
369            self.__helper.enforced_constraint_indicator_variable_index(self.__index)
370        )
371        return Variable(self.__helper, enforcement_var_index)
372
373    @indicator_variable.setter
374    def indicator_variable(self, var: "Variable") -> None:
375        self.__helper.set_enforced_constraint_indicator_variable_index(
376            self.__index, var.index
377        )
378
379    @property
380    def indicator_value(self) -> bool:
381        return self.__helper.enforced_constraint_indicator_value(self.__index)
382
383    @indicator_value.setter
384    def indicator_value(self, value: bool) -> None:
385        self.__helper.set_enforced_constraint_indicator_value(self.__index, value)
386
387    @property
388    def name(self) -> str:
389        constraint_name = self.__helper.enforced_constraint_name(self.__index)
390        if constraint_name:
391            return constraint_name
392        return f"enforced_linear_constraint#{self.__index}"
393
394    @name.setter
395    def name(self, name: str) -> None:
396        return self.__helper.set_enforced_constraint_name(self.__index, name)
397
398    @property
399    def is_under_specified(self) -> bool:
400        """Returns True if the constraint is under specified.
401
402        Usually, it means that it was created by model.add(False) or model.add(True)
403        The effect is that modifying the constraint will raise an exception.
404        """
405        return self.__is_under_specified
406
407    def assert_constraint_is_well_defined(self) -> None:
408        """Raises an exception if the constraint is under specified."""
409        if self.__is_under_specified:
410            raise ValueError(
411                f"Constraint {self.index} is under specified and cannot be modified"
412            )
413
414    def __str__(self):
415        return self.name
416
417    def __repr__(self):
418        return (
419            f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound},"
420            f" ub={self.upper_bound},"
421            f" var_indices={self.helper.enforced_constraint_var_indices(self.index)},"
422            f" coefficients={self.helper.enforced_constraint_coefficients(self.index)},"
423            f" indicator_variable={self.indicator_variable}"
424            f" indicator_value={self.indicator_value})"
425        )
426
427    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
428        """Sets the coefficient of the variable in the constraint."""
429        self.assert_constraint_is_well_defined()
430        self.__helper.set_enforced_constraint_coefficient(
431            self.__index, var.index, coeff
432        )
433
434    def add_term(self, var: Variable, coeff: NumberT) -> None:
435        """Adds var * coeff to the constraint."""
436        self.assert_constraint_is_well_defined()
437        self.__helper.safe_add_term_to_enforced_constraint(
438            self.__index, var.index, coeff
439        )
440
441    def clear_terms(self) -> None:
442        """Clear all terms of the constraint."""
443        self.assert_constraint_is_well_defined()
444        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: int | numbers.Integral | numpy.integer | None = None, is_under_specified: bool = False)
310    def __init__(
311        self,
312        helper: mbh.ModelBuilderHelper,
313        *,
314        index: Optional[IntegerT] = None,
315        is_under_specified: bool = False,
316    ) -> None:
317        """EnforcedLinearConstraint constructor.
318
319        Args:
320          helper: The pybind11 ModelBuilderHelper.
321          index: If specified, recreates a wrapper to an existing linear constraint.
322          is_under_specified: indicates if the constraint was created by
323            model.add(bool).
324        """
325        if index is None:
326            self.__index = helper.add_enforced_linear_constraint()
327        else:
328            if not helper.is_enforced_linear_constraint(index):
329                raise ValueError(
330                    f"the given index {index} does not refer to an enforced linear"
331                    " constraint"
332                )
333
334            self.__index = index
335        self.__helper: mbh.ModelBuilderHelper = helper
336        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: int | numbers.Integral | numpy.integer
338    @property
339    def index(self) -> IntegerT:
340        """Returns the index of the constraint in the helper."""
341        return self.__index

Returns the index of the constraint in the helper.

343    @property
344    def helper(self) -> mbh.ModelBuilderHelper:
345        """Returns the ModelBuilderHelper instance."""
346        return self.__helper

Returns the ModelBuilderHelper instance.

lower_bound: numpy.float64
348    @property
349    def lower_bound(self) -> np.double:
350        return self.__helper.enforced_constraint_lower_bound(self.__index)
upper_bound: numpy.float64
357    @property
358    def upper_bound(self) -> np.double:
359        return self.__helper.enforced_constraint_upper_bound(self.__index)
indicator_variable: Variable
366    @property
367    def indicator_variable(self) -> "Variable":
368        enforcement_var_index = (
369            self.__helper.enforced_constraint_indicator_variable_index(self.__index)
370        )
371        return Variable(self.__helper, enforcement_var_index)
indicator_value: bool
379    @property
380    def indicator_value(self) -> bool:
381        return self.__helper.enforced_constraint_indicator_value(self.__index)
name: str
387    @property
388    def name(self) -> str:
389        constraint_name = self.__helper.enforced_constraint_name(self.__index)
390        if constraint_name:
391            return constraint_name
392        return f"enforced_linear_constraint#{self.__index}"
is_under_specified: bool
398    @property
399    def is_under_specified(self) -> bool:
400        """Returns True if the constraint is under specified.
401
402        Usually, it means that it was created by model.add(False) or model.add(True)
403        The effect is that modifying the constraint will raise an exception.
404        """
405        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:
407    def assert_constraint_is_well_defined(self) -> None:
408        """Raises an exception if the constraint is under specified."""
409        if self.__is_under_specified:
410            raise ValueError(
411                f"Constraint {self.index} is under specified and cannot be modified"
412            )

Raises an exception if the constraint is under specified.

def set_coefficient( self, var: Variable, coeff: int | float | numbers.Real | numpy.number) -> None:
427    def set_coefficient(self, var: Variable, coeff: NumberT) -> None:
428        """Sets the coefficient of the variable in the constraint."""
429        self.assert_constraint_is_well_defined()
430        self.__helper.set_enforced_constraint_coefficient(
431            self.__index, var.index, coeff
432        )

Sets the coefficient of the variable in the constraint.

def add_term( self, var: Variable, coeff: int | float | numbers.Real | numpy.number) -> None:
434    def add_term(self, var: Variable, coeff: NumberT) -> None:
435        """Adds var * coeff to the constraint."""
436        self.assert_constraint_is_well_defined()
437        self.__helper.safe_add_term_to_enforced_constraint(
438            self.__index, var.index, coeff
439        )

Adds var * coeff to the constraint.

def clear_terms(self) -> None:
441    def clear_terms(self) -> None:
442        """Clear all terms of the constraint."""
443        self.assert_constraint_is_well_defined()
444        self.__helper.clear_enforced_constraint_terms(self.__index)

Clear all terms of the constraint.

class Model:
 447class Model:
 448    """Methods for building a linear model.
 449
 450    Methods beginning with:
 451
 452    * ```new_``` create integer, boolean, or interval variables.
 453    * ```add_``` create new constraints and add them to the model.
 454    """
 455
 456    def __init__(self):
 457        self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper()
 458
 459    def clone(self) -> "Model":
 460        """Returns a clone of the current model."""
 461        clone = Model()
 462        clone.helper.overwrite_model(self.helper)
 463        return clone
 464
 465    @typing.overload
 466    def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: ...
 467
 468    @typing.overload
 469    def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: ...
 470
 471    def _get_linear_constraints(
 472        self, constraints: Optional[_IndexOrSeries] = None
 473    ) -> _IndexOrSeries:
 474        if constraints is None:
 475            return self.get_linear_constraints()
 476        return constraints
 477
 478    @typing.overload
 479    def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: ...
 480
 481    @typing.overload
 482    def _get_variables(self, variables: pd.Series) -> pd.Series: ...
 483
 484    def _get_variables(
 485        self, variables: Optional[_IndexOrSeries] = None
 486    ) -> _IndexOrSeries:
 487        if variables is None:
 488            return self.get_variables()
 489        return variables
 490
 491    def get_linear_constraints(self) -> pd.Index:
 492        """Gets all linear constraints in the model."""
 493        return pd.Index(
 494            [self.linear_constraint_from_index(i) for i in range(self.num_constraints)],
 495            name="linear_constraint",
 496        )
 497
 498    def get_linear_constraint_expressions(
 499        self, constraints: Optional[_IndexOrSeries] = None
 500    ) -> pd.Series:
 501        """Gets the expressions of all linear constraints in the set.
 502
 503        If `constraints` is a `pd.Index`, then the output will be indexed by the
 504        constraints. If `constraints` is a `pd.Series` indexed by the underlying
 505        dimensions, then the output will be indexed by the same underlying
 506        dimensions.
 507
 508        Args:
 509          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
 510            constraints from which to get the expressions. If unspecified, all
 511            linear constraints will be in scope.
 512
 513        Returns:
 514          pd.Series: The expressions of all linear constraints in the set.
 515        """
 516        return _attribute_series(
 517            # pylint: disable=g-long-lambda
 518            func=lambda c: mbh.FlatExpr(
 519                # pylint: disable=g-complex-comprehension
 520                [
 521                    Variable(self.__helper, var_id)
 522                    for var_id in c.helper.constraint_var_indices(c.index)
 523                ],
 524                c.helper.constraint_coefficients(c.index),
 525                0.0,
 526            ),
 527            values=self._get_linear_constraints(constraints),
 528        )
 529
 530    def get_linear_constraint_lower_bounds(
 531        self, constraints: Optional[_IndexOrSeries] = None
 532    ) -> pd.Series:
 533        """Gets the lower bounds of all linear constraints in the set.
 534
 535        If `constraints` is a `pd.Index`, then the output will be indexed by the
 536        constraints. If `constraints` is a `pd.Series` indexed by the underlying
 537        dimensions, then the output will be indexed by the same underlying
 538        dimensions.
 539
 540        Args:
 541          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
 542            constraints from which to get the lower bounds. If unspecified, all
 543            linear constraints will be in scope.
 544
 545        Returns:
 546          pd.Series: The lower bounds of all linear constraints in the set.
 547        """
 548        return _attribute_series(
 549            func=lambda c: c.lower_bound,  # pylint: disable=protected-access
 550            values=self._get_linear_constraints(constraints),
 551        )
 552
 553    def get_linear_constraint_upper_bounds(
 554        self, constraints: Optional[_IndexOrSeries] = None
 555    ) -> pd.Series:
 556        """Gets the upper bounds of all linear constraints in the set.
 557
 558        If `constraints` is a `pd.Index`, then the output will be indexed by the
 559        constraints. If `constraints` is a `pd.Series` indexed by the underlying
 560        dimensions, then the output will be indexed by the same underlying
 561        dimensions.
 562
 563        Args:
 564          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
 565            constraints. If unspecified, all linear constraints will be in scope.
 566
 567        Returns:
 568          pd.Series: The upper bounds of all linear constraints in the set.
 569        """
 570        return _attribute_series(
 571            func=lambda c: c.upper_bound,  # pylint: disable=protected-access
 572            values=self._get_linear_constraints(constraints),
 573        )
 574
 575    def get_variables(self) -> pd.Index:
 576        """Gets all variables in the model."""
 577        return pd.Index(
 578            [self.var_from_index(i) for i in range(self.num_variables)],
 579            name="variable",
 580        )
 581
 582    def get_variable_lower_bounds(
 583        self, variables: Optional[_IndexOrSeries] = None
 584    ) -> pd.Series:
 585        """Gets the lower bounds of all variables in the set.
 586
 587        If `variables` is a `pd.Index`, then the output will be indexed by the
 588        variables. If `variables` is a `pd.Series` indexed by the underlying
 589        dimensions, then the output will be indexed by the same underlying
 590        dimensions.
 591
 592        Args:
 593          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
 594            from which to get the lower bounds. If unspecified, all variables will
 595            be in scope.
 596
 597        Returns:
 598          pd.Series: The lower bounds of all variables in the set.
 599        """
 600        return _attribute_series(
 601            func=lambda v: v.lower_bound,  # pylint: disable=protected-access
 602            values=self._get_variables(variables),
 603        )
 604
 605    def get_variable_upper_bounds(
 606        self, variables: Optional[_IndexOrSeries] = None
 607    ) -> pd.Series:
 608        """Gets the upper bounds of all variables in the set.
 609
 610        Args:
 611          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
 612            from which to get the upper bounds. If unspecified, all variables will
 613            be in scope.
 614
 615        Returns:
 616          pd.Series: The upper bounds of all variables in the set.
 617        """
 618        return _attribute_series(
 619            func=lambda v: v.upper_bound,  # pylint: disable=protected-access
 620            values=self._get_variables(variables),
 621        )
 622
 623    # Integer variable.
 624
 625    def new_var(
 626        self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str]
 627    ) -> Variable:
 628        """Create an integer variable with domain [lb, ub].
 629
 630        Args:
 631          lb: Lower bound of the variable.
 632          ub: Upper bound of the variable.
 633          is_integer: Indicates if the variable must take integral values.
 634          name: The name of the variable.
 635
 636        Returns:
 637          a variable whose domain is [lb, ub].
 638        """
 639        if name:
 640            return Variable(self.__helper, lb, ub, is_integer, name)
 641        else:
 642            return Variable(self.__helper, lb, ub, is_integer)
 643
 644    def new_int_var(
 645        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
 646    ) -> Variable:
 647        """Create an integer variable with domain [lb, ub].
 648
 649        Args:
 650          lb: Lower bound of the variable.
 651          ub: Upper bound of the variable.
 652          name: The name of the variable.
 653
 654        Returns:
 655          a variable whose domain is [lb, ub].
 656        """
 657
 658        return self.new_var(lb, ub, True, name)
 659
 660    def new_num_var(
 661        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
 662    ) -> Variable:
 663        """Create an integer variable with domain [lb, ub].
 664
 665        Args:
 666          lb: Lower bound of the variable.
 667          ub: Upper bound of the variable.
 668          name: The name of the variable.
 669
 670        Returns:
 671          a variable whose domain is [lb, ub].
 672        """
 673
 674        return self.new_var(lb, ub, False, name)
 675
 676    def new_bool_var(self, name: Optional[str] = None) -> Variable:
 677        """Creates a 0-1 variable with the given name."""
 678        return self.new_var(0, 1, True, name)  # numpy-scalars
 679
 680    def new_constant(self, value: NumberT) -> Variable:
 681        """Declares a constant variable."""
 682        return self.new_var(value, value, False, None)
 683
 684    def new_var_series(
 685        self,
 686        name: str,
 687        index: pd.Index,
 688        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
 689        upper_bounds: Union[NumberT, pd.Series] = math.inf,
 690        is_integral: Union[bool, pd.Series] = False,
 691    ) -> pd.Series:
 692        """Creates a series of (scalar-valued) variables with the given name.
 693
 694        Args:
 695          name (str): Required. The name of the variable set.
 696          index (pd.Index): Required. The index to use for the variable set.
 697          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
 698            variables in the set. If a `pd.Series` is passed in, it will be based on
 699            the corresponding values of the pd.Series. Defaults to -inf.
 700          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
 701            variables in the set. If a `pd.Series` is passed in, it will be based on
 702            the corresponding values of the pd.Series. Defaults to +inf.
 703          is_integral (bool, pd.Series): Optional. Indicates if the variable can
 704            only take integer values. If a `pd.Series` is passed in, it will be
 705            based on the corresponding values of the pd.Series. Defaults to False.
 706
 707        Returns:
 708          pd.Series: The variable set indexed by its corresponding dimensions.
 709
 710        Raises:
 711          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 712          ValueError: if the `name` is not a valid identifier or already exists.
 713          ValueError: if the `lowerbound` is greater than the `upperbound`.
 714          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
 715          does not match the input index.
 716        """
 717        if not isinstance(index, pd.Index):
 718            raise TypeError("Non-index object is used as index")
 719        if not name.isidentifier():
 720            raise ValueError(f"name={name!r} is not a valid identifier")
 721        if (
 722            mbn.is_a_number(lower_bounds)
 723            and mbn.is_a_number(upper_bounds)
 724            and lower_bounds > upper_bounds
 725        ):
 726            raise ValueError(
 727                f"lower_bound={lower_bounds} is greater than"
 728                f" upper_bound={upper_bounds} for variable set={name!r}"
 729            )
 730        if (
 731            isinstance(is_integral, bool)
 732            and is_integral
 733            and mbn.is_a_number(lower_bounds)
 734            and mbn.is_a_number(upper_bounds)
 735            and math.isfinite(lower_bounds)
 736            and math.isfinite(upper_bounds)
 737            and math.ceil(lower_bounds) > math.floor(upper_bounds)
 738        ):
 739            raise ValueError(
 740                f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}"
 741                f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}"
 742                f" for variable set={name!r}"
 743            )
 744        lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
 745        upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
 746        is_integrals = _convert_to_series_and_validate_index(is_integral, index)
 747        return pd.Series(
 748            index=index,
 749            data=[
 750                # pylint: disable=g-complex-comprehension
 751                Variable(
 752                    self.__helper,
 753                    lower_bounds[i],
 754                    upper_bounds[i],
 755                    is_integrals[i],
 756                    f"{name}[{i}]",
 757                )
 758                for i in index
 759            ],
 760        )
 761
 762    def new_num_var_series(
 763        self,
 764        name: str,
 765        index: pd.Index,
 766        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
 767        upper_bounds: Union[NumberT, pd.Series] = math.inf,
 768    ) -> pd.Series:
 769        """Creates a series of continuous variables with the given name.
 770
 771        Args:
 772          name (str): Required. The name of the variable set.
 773          index (pd.Index): Required. The index to use for the variable set.
 774          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
 775            variables in the set. If a `pd.Series` is passed in, it will be based on
 776            the corresponding values of the pd.Series. Defaults to -inf.
 777          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
 778            variables in the set. If a `pd.Series` is passed in, it will be based on
 779            the corresponding values of the pd.Series. Defaults to +inf.
 780
 781        Returns:
 782          pd.Series: The variable set indexed by its corresponding dimensions.
 783
 784        Raises:
 785          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 786          ValueError: if the `name` is not a valid identifier or already exists.
 787          ValueError: if the `lowerbound` is greater than the `upperbound`.
 788          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
 789          does not match the input index.
 790        """
 791        return self.new_var_series(name, index, lower_bounds, upper_bounds, False)
 792
 793    def new_int_var_series(
 794        self,
 795        name: str,
 796        index: pd.Index,
 797        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
 798        upper_bounds: Union[NumberT, pd.Series] = math.inf,
 799    ) -> pd.Series:
 800        """Creates a series of integer variables with the given name.
 801
 802        Args:
 803          name (str): Required. The name of the variable set.
 804          index (pd.Index): Required. The index to use for the variable set.
 805          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
 806            variables in the set. If a `pd.Series` is passed in, it will be based on
 807            the corresponding values of the pd.Series. Defaults to -inf.
 808          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
 809            variables in the set. If a `pd.Series` is passed in, it will be based on
 810            the corresponding values of the pd.Series. Defaults to +inf.
 811
 812        Returns:
 813          pd.Series: The variable set indexed by its corresponding dimensions.
 814
 815        Raises:
 816          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 817          ValueError: if the `name` is not a valid identifier or already exists.
 818          ValueError: if the `lowerbound` is greater than the `upperbound`.
 819          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
 820          does not match the input index.
 821        """
 822        return self.new_var_series(name, index, lower_bounds, upper_bounds, True)
 823
 824    def new_bool_var_series(
 825        self,
 826        name: str,
 827        index: pd.Index,
 828    ) -> pd.Series:
 829        """Creates a series of Boolean variables with the given name.
 830
 831        Args:
 832          name (str): Required. The name of the variable set.
 833          index (pd.Index): Required. The index to use for the variable set.
 834
 835        Returns:
 836          pd.Series: The variable set indexed by its corresponding dimensions.
 837
 838        Raises:
 839          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
 840          ValueError: if the `name` is not a valid identifier or already exists.
 841          ValueError: if the `lowerbound` is greater than the `upperbound`.
 842          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
 843          does not match the input index.
 844        """
 845        return self.new_var_series(name, index, 0, 1, True)
 846
 847    def var_from_index(self, index: IntegerT) -> Variable:
 848        """Rebuilds a variable object from the model and its index."""
 849        return Variable(self.__helper, index)
 850
 851    # Linear constraints.
 852
 853    def add_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
 854        self,
 855        linear_expr: LinearExprT,
 856        lb: NumberT = -math.inf,
 857        ub: NumberT = math.inf,
 858        name: Optional[str] = None,
 859    ) -> LinearConstraint:
 860        """Adds the constraint: `lb <= linear_expr <= ub` with the given name."""
 861        ct = LinearConstraint(self.__helper)
 862        if name:
 863            self.__helper.set_constraint_name(ct.index, name)
 864        if mbn.is_a_number(linear_expr):
 865            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
 866            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
 867        elif isinstance(linear_expr, LinearExpr):
 868            flat_expr = mbh.FlatExpr(linear_expr)
 869            # pylint: disable=protected-access
 870            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset)
 871            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset)
 872            self.__helper.add_terms_to_constraint(
 873                ct.index, flat_expr.vars, flat_expr.coeffs
 874            )
 875        else:
 876            raise TypeError(
 877                "Not supported:"
 878                f" Model.add_linear_constraint({type(linear_expr).__name__!r})"
 879            )
 880        return ct
 881
 882    def add(
 883        self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None
 884    ) -> Union[LinearConstraint, pd.Series]:
 885        """Adds a `BoundedLinearExpression` to the model.
 886
 887        Args:
 888          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
 889          name: An optional name.
 890
 891        Returns:
 892          An instance of the `Constraint` class.
 893
 894        Note that a special treatment is done when the argument does not contain any
 895        variable, and thus evaluates to True or False.
 896
 897        `model.add(True)` will create a constraint 0 <= empty sum <= 0.
 898        The constraint will be marked as under specified, and cannot be modified
 899        thereafter.
 900
 901        `model.add(False)` will create a constraint inf <= empty sum <= -inf. The
 902        constraint will be marked as under specified, and cannot be modified
 903        thereafter.
 904
 905        you can check the if a constraint is under specified by reading the
 906        `LinearConstraint.is_under_specified` property.
 907        """
 908        if isinstance(ct, mbh.BoundedLinearExpression):
 909            return _add_linear_constraint_to_helper(ct, self.__helper, name)
 910        elif isinstance(ct, bool):
 911            return _add_linear_constraint_to_helper(ct, self.__helper, name)
 912        elif isinstance(ct, pd.Series):
 913            return pd.Series(
 914                index=ct.index,
 915                data=[
 916                    _add_linear_constraint_to_helper(
 917                        expr, self.__helper, f"{name}[{i}]"
 918                    )
 919                    for (i, expr) in zip(ct.index, ct)
 920                ],
 921            )
 922        else:
 923            raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})")
 924
 925    def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint:
 926        """Rebuilds a linear constraint object from the model and its index."""
 927        return LinearConstraint(self.__helper, index=index)
 928
 929    # Enforced Linear constraints.
 930
 931    def add_enforced_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
 932        self,
 933        linear_expr: LinearExprT,
 934        ivar: "Variable",
 935        ivalue: bool,
 936        lb: NumberT = -math.inf,
 937        ub: NumberT = math.inf,
 938        name: Optional[str] = None,
 939    ) -> EnforcedLinearConstraint:
 940        """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name."""
 941        ct = EnforcedLinearConstraint(self.__helper)
 942        ct.indicator_variable = ivar
 943        ct.indicator_value = ivalue
 944        if name:
 945            self.__helper.set_constraint_name(ct.index, name)
 946        if mbn.is_a_number(linear_expr):
 947            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
 948            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
 949        elif isinstance(linear_expr, LinearExpr):
 950            flat_expr = mbh.FlatExpr(linear_expr)
 951            # pylint: disable=protected-access
 952            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset)
 953            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset)
 954            self.__helper.add_terms_to_constraint(
 955                ct.index, flat_expr.vars, flat_expr.coeffs
 956            )
 957        else:
 958            raise TypeError(
 959                "Not supported:"
 960                f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})"
 961            )
 962        return ct
 963
 964    def add_enforced(
 965        self,
 966        ct: Union[ConstraintT, pd.Series],
 967        var: Union[Variable, pd.Series],
 968        value: Union[bool, pd.Series],
 969        name: Optional[str] = None,
 970    ) -> Union[EnforcedLinearConstraint, pd.Series]:
 971        """Adds a `ivar == ivalue => BoundedLinearExpression` to the model.
 972
 973        Args:
 974          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
 975          var: The indicator variable
 976          value: the indicator value
 977          name: An optional name.
 978
 979        Returns:
 980          An instance of the `Constraint` class.
 981
 982        Note that a special treatment is done when the argument does not contain any
 983        variable, and thus evaluates to True or False.
 984
 985        model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty
 986        sum <= 0
 987
 988        model.add_enforced(False, var, value) will create a constraint inf <=
 989        empty sum <= -inf
 990
 991        you can check the if a constraint is always false (lb=inf, ub=-inf) by
 992        calling EnforcedLinearConstraint.is_always_false()
 993        """
 994        if isinstance(ct, mbh.BoundedLinearExpression):
 995            return _add_enforced_linear_constraint_to_helper(
 996                ct, self.__helper, var, value, name
 997            )
 998        elif (
 999            isinstance(ct, bool)
1000            and isinstance(var, Variable)
1001            and isinstance(value, bool)
1002        ):
1003            return _add_enforced_linear_constraint_to_helper(
1004                ct, self.__helper, var, value, name
1005            )
1006        elif isinstance(ct, pd.Series):
1007            ivar_series = _convert_to_var_series_and_validate_index(var, ct.index)
1008            ivalue_series = _convert_to_series_and_validate_index(value, ct.index)
1009            return pd.Series(
1010                index=ct.index,
1011                data=[
1012                    _add_enforced_linear_constraint_to_helper(
1013                        expr,
1014                        self.__helper,
1015                        ivar_series[i],
1016                        ivalue_series[i],
1017                        f"{name}[{i}]",
1018                    )
1019                    for (i, expr) in zip(ct.index, ct)
1020                ],
1021            )
1022        else:
1023            raise TypeError(f"Not supported: Model.add_enforced({type(ct).__name__!r}")
1024
1025    def enforced_linear_constraint_from_index(
1026        self, index: IntegerT
1027    ) -> EnforcedLinearConstraint:
1028        """Rebuilds an enforced linear constraint object from the model and its index."""
1029        return EnforcedLinearConstraint(self.__helper, index=index)
1030
1031    # Objective.
1032    def minimize(self, linear_expr: LinearExprT) -> None:
1033        """Minimizes the given objective."""
1034        self.__optimize(linear_expr, False)
1035
1036    def maximize(self, linear_expr: LinearExprT) -> None:
1037        """Maximizes the given objective."""
1038        self.__optimize(linear_expr, True)
1039
1040    def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None:
1041        """Defines the objective."""
1042        self.helper.clear_objective()
1043        self.__helper.set_maximize(maximize)
1044        if mbn.is_a_number(linear_expr):
1045            self.helper.set_objective_offset(linear_expr)
1046        elif isinstance(linear_expr, Variable):
1047            self.helper.set_var_objective_coefficient(linear_expr.index, 1.0)
1048        elif isinstance(linear_expr, LinearExpr):
1049            flat_expr = mbh.FlatExpr(linear_expr)
1050            # pylint: disable=protected-access
1051            self.helper.set_objective_offset(flat_expr.offset)
1052            var_indices = [var.index for var in flat_expr.vars]
1053            self.helper.set_objective_coefficients(var_indices, flat_expr.coeffs)
1054        else:
1055            raise TypeError(
1056                "Not supported:"
1057                f" Model.minimize/maximize({type(linear_expr).__name__!r})"
1058            )
1059
1060    @property
1061    def objective_offset(self) -> np.double:
1062        """Returns the fixed offset of the objective."""
1063        return self.__helper.objective_offset()
1064
1065    @objective_offset.setter
1066    def objective_offset(self, value: NumberT) -> None:
1067        self.__helper.set_objective_offset(value)
1068
1069    def objective_expression(self) -> "LinearExpr":
1070        """Returns the expression to optimize."""
1071        variables: list[Variable] = []
1072        coefficients: list[numbers.Real] = []
1073        for variable in self.get_variables():
1074            coeff = self.__helper.var_objective_coefficient(variable.index)
1075            if coeff != 0.0:
1076                variables.append(variable)
1077                coefficients.append(coeff)
1078        return mbh.FlatExpr(variables, coefficients, self.__helper.objective_offset())
1079
1080    # Hints.
1081    def clear_hints(self):
1082        """Clears all solution hints."""
1083        self.__helper.clear_hints()
1084
1085    def add_hint(self, var: Variable, value: NumberT) -> None:
1086        """Adds var == value as a hint to the model.
1087
1088        Args:
1089          var: The variable of the hint
1090          value: The value of the hint
1091
1092        Note that variables must not appear more than once in the list of hints.
1093        """
1094        self.__helper.add_hint(var.index, value)
1095
1096    # Input/Output
1097    def export_to_lp_string(self, obfuscate: bool = False) -> str:
1098        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1099        options.obfuscate = obfuscate
1100        return self.__helper.export_to_lp_string(options)
1101
1102    def export_to_mps_string(self, obfuscate: bool = False) -> str:
1103        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1104        options.obfuscate = obfuscate
1105        return self.__helper.export_to_mps_string(options)
1106
1107    def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool:
1108        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1109        options.obfuscate = obfuscate
1110        return self.__helper.write_to_mps_file(filename, options)
1111
1112    def export_to_proto(self) -> linear_solver_pb2.MPModelProto:
1113        """Exports the optimization model to a ProtoBuf format."""
1114        return mbh.to_mpmodel_proto(self.__helper)
1115
1116    def import_from_mps_string(self, mps_string: str) -> bool:
1117        """Reads a model from a MPS string."""
1118        return self.__helper.import_from_mps_string(mps_string)
1119
1120    def import_from_mps_file(self, mps_file: str) -> bool:
1121        """Reads a model from a .mps file."""
1122        return self.__helper.import_from_mps_file(mps_file)
1123
1124    def import_from_lp_string(self, lp_string: str) -> bool:
1125        """Reads a model from a LP string.
1126
1127        Note that this code is very limited, and will not support any real lp.
1128        It is only intented to be use to parse test lp problems.
1129
1130        Args:
1131          lp_string: The LP string to import.
1132
1133        Returns:
1134          True if the import was successful.
1135        """
1136        return self.__helper.import_from_lp_string(lp_string)
1137
1138    def import_from_lp_file(self, lp_file: str) -> bool:
1139        """Reads a model from a .lp file.
1140
1141        Note that this code is very limited, and will not support any real lp.
1142        It is only intented to be use to parse test lp problems.
1143
1144        Args:
1145          lp_file: The LP file to import.
1146
1147        Returns:
1148          True if the import was successful.
1149        """
1150        return self.__helper.import_from_lp_file(lp_file)
1151
1152    def import_from_proto_file(self, proto_file: str) -> bool:
1153        """Reads a model from a proto file."""
1154        return self.__helper.read_model_from_proto_file(proto_file)
1155
1156    def export_to_proto_file(self, proto_file: str) -> bool:
1157        """Writes a model to a proto file."""
1158        return self.__helper.write_model_to_proto_file(proto_file)
1159
1160    # Model getters and Setters
1161
1162    @property
1163    def num_variables(self) -> int:
1164        """Returns the number of variables in the model."""
1165        return self.__helper.num_variables()
1166
1167    @property
1168    def num_constraints(self) -> int:
1169        """The number of constraints in the model."""
1170        return self.__helper.num_constraints()
1171
1172    @property
1173    def name(self) -> str:
1174        """The name of the model."""
1175        return self.__helper.name()
1176
1177    @name.setter
1178    def name(self, name: str):
1179        self.__helper.set_name(name)
1180
1181    @property
1182    def helper(self) -> mbh.ModelBuilderHelper:
1183        """Returns the model builder helper."""
1184        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:
459    def clone(self) -> "Model":
460        """Returns a clone of the current model."""
461        clone = Model()
462        clone.helper.overwrite_model(self.helper)
463        return clone

Returns a clone of the current model.

def get_linear_constraints(self) -> pandas.core.indexes.base.Index:
491    def get_linear_constraints(self) -> pd.Index:
492        """Gets all linear constraints in the model."""
493        return pd.Index(
494            [self.linear_constraint_from_index(i) for i in range(self.num_constraints)],
495            name="linear_constraint",
496        )

Gets all linear constraints in the model.

def get_linear_constraint_expressions( self, constraints: pandas.core.indexes.base.Index | pandas.core.series.Series | None = None) -> pandas.core.series.Series:
498    def get_linear_constraint_expressions(
499        self, constraints: Optional[_IndexOrSeries] = None
500    ) -> pd.Series:
501        """Gets the expressions of all linear constraints in the set.
502
503        If `constraints` is a `pd.Index`, then the output will be indexed by the
504        constraints. If `constraints` is a `pd.Series` indexed by the underlying
505        dimensions, then the output will be indexed by the same underlying
506        dimensions.
507
508        Args:
509          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
510            constraints from which to get the expressions. If unspecified, all
511            linear constraints will be in scope.
512
513        Returns:
514          pd.Series: The expressions of all linear constraints in the set.
515        """
516        return _attribute_series(
517            # pylint: disable=g-long-lambda
518            func=lambda c: mbh.FlatExpr(
519                # pylint: disable=g-complex-comprehension
520                [
521                    Variable(self.__helper, var_id)
522                    for var_id in c.helper.constraint_var_indices(c.index)
523                ],
524                c.helper.constraint_coefficients(c.index),
525                0.0,
526            ),
527            values=self._get_linear_constraints(constraints),
528        )

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: pandas.core.indexes.base.Index | pandas.core.series.Series | None = None) -> pandas.core.series.Series:
530    def get_linear_constraint_lower_bounds(
531        self, constraints: Optional[_IndexOrSeries] = None
532    ) -> pd.Series:
533        """Gets the lower bounds of all linear constraints in the set.
534
535        If `constraints` is a `pd.Index`, then the output will be indexed by the
536        constraints. If `constraints` is a `pd.Series` indexed by the underlying
537        dimensions, then the output will be indexed by the same underlying
538        dimensions.
539
540        Args:
541          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
542            constraints from which to get the lower bounds. If unspecified, all
543            linear constraints will be in scope.
544
545        Returns:
546          pd.Series: The lower bounds of all linear constraints in the set.
547        """
548        return _attribute_series(
549            func=lambda c: c.lower_bound,  # pylint: disable=protected-access
550            values=self._get_linear_constraints(constraints),
551        )

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: pandas.core.indexes.base.Index | pandas.core.series.Series | None = None) -> pandas.core.series.Series:
553    def get_linear_constraint_upper_bounds(
554        self, constraints: Optional[_IndexOrSeries] = None
555    ) -> pd.Series:
556        """Gets the upper bounds of all linear constraints in the set.
557
558        If `constraints` is a `pd.Index`, then the output will be indexed by the
559        constraints. If `constraints` is a `pd.Series` indexed by the underlying
560        dimensions, then the output will be indexed by the same underlying
561        dimensions.
562
563        Args:
564          constraints (Union[pd.Index, pd.Series]): Optional. The set of linear
565            constraints. If unspecified, all linear constraints will be in scope.
566
567        Returns:
568          pd.Series: The upper bounds of all linear constraints in the set.
569        """
570        return _attribute_series(
571            func=lambda c: c.upper_bound,  # pylint: disable=protected-access
572            values=self._get_linear_constraints(constraints),
573        )

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:
575    def get_variables(self) -> pd.Index:
576        """Gets all variables in the model."""
577        return pd.Index(
578            [self.var_from_index(i) for i in range(self.num_variables)],
579            name="variable",
580        )

Gets all variables in the model.

def get_variable_lower_bounds( self, variables: pandas.core.indexes.base.Index | pandas.core.series.Series | None = None) -> pandas.core.series.Series:
582    def get_variable_lower_bounds(
583        self, variables: Optional[_IndexOrSeries] = None
584    ) -> pd.Series:
585        """Gets the lower bounds of all variables in the set.
586
587        If `variables` is a `pd.Index`, then the output will be indexed by the
588        variables. If `variables` is a `pd.Series` indexed by the underlying
589        dimensions, then the output will be indexed by the same underlying
590        dimensions.
591
592        Args:
593          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
594            from which to get the lower bounds. If unspecified, all variables will
595            be in scope.
596
597        Returns:
598          pd.Series: The lower bounds of all variables in the set.
599        """
600        return _attribute_series(
601            func=lambda v: v.lower_bound,  # pylint: disable=protected-access
602            values=self._get_variables(variables),
603        )

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: pandas.core.indexes.base.Index | pandas.core.series.Series | None = None) -> pandas.core.series.Series:
605    def get_variable_upper_bounds(
606        self, variables: Optional[_IndexOrSeries] = None
607    ) -> pd.Series:
608        """Gets the upper bounds of all variables in the set.
609
610        Args:
611          variables (Union[pd.Index, pd.Series]): Optional. The set of variables
612            from which to get the upper bounds. If unspecified, all variables will
613            be in scope.
614
615        Returns:
616          pd.Series: The upper bounds of all variables in the set.
617        """
618        return _attribute_series(
619            func=lambda v: v.upper_bound,  # pylint: disable=protected-access
620            values=self._get_variables(variables),
621        )

Gets the upper bounds of all variables in the set.

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

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

def new_var( self, lb: int | float | numbers.Real | numpy.number, ub: int | float | numbers.Real | numpy.number, is_integer: bool, name: str | None) -> Variable:
625    def new_var(
626        self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str]
627    ) -> Variable:
628        """Create an integer variable with domain [lb, ub].
629
630        Args:
631          lb: Lower bound of the variable.
632          ub: Upper bound of the variable.
633          is_integer: Indicates if the variable must take integral values.
634          name: The name of the variable.
635
636        Returns:
637          a variable whose domain is [lb, ub].
638        """
639        if name:
640            return Variable(self.__helper, lb, ub, is_integer, name)
641        else:
642            return Variable(self.__helper, lb, ub, is_integer)

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

Arguments:
  • lb: Lower bound of the variable.
  • ub: Upper bound of the variable.
  • is_integer: Indicates if the variable must take integral values.
  • name: The name of the variable.
Returns:

a variable whose domain is [lb, ub].

def new_int_var( self, lb: int | float | numbers.Real | numpy.number, ub: int | float | numbers.Real | numpy.number, name: str | None = None) -> Variable:
644    def new_int_var(
645        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
646    ) -> Variable:
647        """Create an integer variable with domain [lb, ub].
648
649        Args:
650          lb: Lower bound of the variable.
651          ub: Upper bound of the variable.
652          name: The name of the variable.
653
654        Returns:
655          a variable whose domain is [lb, ub].
656        """
657
658        return self.new_var(lb, ub, True, name)

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

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

a variable whose domain is [lb, ub].

def new_num_var( self, lb: int | float | numbers.Real | numpy.number, ub: int | float | numbers.Real | numpy.number, name: str | None = None) -> Variable:
660    def new_num_var(
661        self, lb: NumberT, ub: NumberT, name: Optional[str] = None
662    ) -> Variable:
663        """Create an integer variable with domain [lb, ub].
664
665        Args:
666          lb: Lower bound of the variable.
667          ub: Upper bound of the variable.
668          name: The name of the variable.
669
670        Returns:
671          a variable whose domain is [lb, ub].
672        """
673
674        return self.new_var(lb, ub, False, name)

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

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

a variable whose domain is [lb, ub].

def new_bool_var( self, name: str | None = None) -> Variable:
676    def new_bool_var(self, name: Optional[str] = None) -> Variable:
677        """Creates a 0-1 variable with the given name."""
678        return self.new_var(0, 1, True, name)  # numpy-scalars

Creates a 0-1 variable with the given name.

def new_constant( self, value: int | float | numbers.Real | numpy.number) -> Variable:
680    def new_constant(self, value: NumberT) -> Variable:
681        """Declares a constant variable."""
682        return self.new_var(value, value, False, None)

Declares a constant variable.

def new_var_series( self, name: str, index: pandas.core.indexes.base.Index, lower_bounds: int | float | numbers.Real | numpy.number | pandas.core.series.Series = -inf, upper_bounds: int | float | numbers.Real | numpy.number | pandas.core.series.Series = inf, is_integral: bool | pandas.core.series.Series = False) -> pandas.core.series.Series:
684    def new_var_series(
685        self,
686        name: str,
687        index: pd.Index,
688        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
689        upper_bounds: Union[NumberT, pd.Series] = math.inf,
690        is_integral: Union[bool, pd.Series] = False,
691    ) -> pd.Series:
692        """Creates a series of (scalar-valued) variables with the given name.
693
694        Args:
695          name (str): Required. The name of the variable set.
696          index (pd.Index): Required. The index to use for the variable set.
697          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
698            variables in the set. If a `pd.Series` is passed in, it will be based on
699            the corresponding values of the pd.Series. Defaults to -inf.
700          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
701            variables in the set. If a `pd.Series` is passed in, it will be based on
702            the corresponding values of the pd.Series. Defaults to +inf.
703          is_integral (bool, pd.Series): Optional. Indicates if the variable can
704            only take integer values. If a `pd.Series` is passed in, it will be
705            based on the corresponding values of the pd.Series. Defaults to False.
706
707        Returns:
708          pd.Series: The variable set indexed by its corresponding dimensions.
709
710        Raises:
711          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
712          ValueError: if the `name` is not a valid identifier or already exists.
713          ValueError: if the `lowerbound` is greater than the `upperbound`.
714          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
715          does not match the input index.
716        """
717        if not isinstance(index, pd.Index):
718            raise TypeError("Non-index object is used as index")
719        if not name.isidentifier():
720            raise ValueError(f"name={name!r} is not a valid identifier")
721        if (
722            mbn.is_a_number(lower_bounds)
723            and mbn.is_a_number(upper_bounds)
724            and lower_bounds > upper_bounds
725        ):
726            raise ValueError(
727                f"lower_bound={lower_bounds} is greater than"
728                f" upper_bound={upper_bounds} for variable set={name!r}"
729            )
730        if (
731            isinstance(is_integral, bool)
732            and is_integral
733            and mbn.is_a_number(lower_bounds)
734            and mbn.is_a_number(upper_bounds)
735            and math.isfinite(lower_bounds)
736            and math.isfinite(upper_bounds)
737            and math.ceil(lower_bounds) > math.floor(upper_bounds)
738        ):
739            raise ValueError(
740                f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}"
741                f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}"
742                f" for variable set={name!r}"
743            )
744        lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
745        upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
746        is_integrals = _convert_to_series_and_validate_index(is_integral, index)
747        return pd.Series(
748            index=index,
749            data=[
750                # pylint: disable=g-complex-comprehension
751                Variable(
752                    self.__helper,
753                    lower_bounds[i],
754                    upper_bounds[i],
755                    is_integrals[i],
756                    f"{name}[{i}]",
757                )
758                for i in index
759            ],
760        )

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

Arguments:
  • name (str): Required. The name of the variable set.
  • index (pd.Index): Required. The index to use for the variable set.
  • lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for variables in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series. Defaults to -inf.
  • upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for variables in the set. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series. Defaults to +inf.
  • is_integral (bool, pd.Series): Optional. Indicates if the variable can only take integer values. If a pd.Series is passed in, it will be based on the corresponding values of the pd.Series. Defaults to False.
Returns:

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

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the lowerbound is greater than the upperbound.
  • ValueError: if the index of lower_bound, upper_bound, or is_integer
  • does not match the input index.
def new_num_var_series( self, name: str, index: pandas.core.indexes.base.Index, lower_bounds: int | float | numbers.Real | numpy.number | pandas.core.series.Series = -inf, upper_bounds: int | float | numbers.Real | numpy.number | pandas.core.series.Series = inf) -> pandas.core.series.Series:
762    def new_num_var_series(
763        self,
764        name: str,
765        index: pd.Index,
766        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
767        upper_bounds: Union[NumberT, pd.Series] = math.inf,
768    ) -> pd.Series:
769        """Creates a series of continuous variables with the given name.
770
771        Args:
772          name (str): Required. The name of the variable set.
773          index (pd.Index): Required. The index to use for the variable set.
774          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
775            variables in the set. If a `pd.Series` is passed in, it will be based on
776            the corresponding values of the pd.Series. Defaults to -inf.
777          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
778            variables in the set. If a `pd.Series` is passed in, it will be based on
779            the corresponding values of the pd.Series. Defaults to +inf.
780
781        Returns:
782          pd.Series: The variable set indexed by its corresponding dimensions.
783
784        Raises:
785          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
786          ValueError: if the `name` is not a valid identifier or already exists.
787          ValueError: if the `lowerbound` is greater than the `upperbound`.
788          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
789          does not match the input index.
790        """
791        return self.new_var_series(name, index, lower_bounds, upper_bounds, False)

Creates a series of continuous variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the lowerbound is greater than the upperbound.
  • ValueError: if the index of lower_bound, upper_bound, or is_integer
  • does not match the input index.
def new_int_var_series( self, name: str, index: pandas.core.indexes.base.Index, lower_bounds: int | float | numbers.Real | numpy.number | pandas.core.series.Series = -inf, upper_bounds: int | float | numbers.Real | numpy.number | pandas.core.series.Series = inf) -> pandas.core.series.Series:
793    def new_int_var_series(
794        self,
795        name: str,
796        index: pd.Index,
797        lower_bounds: Union[NumberT, pd.Series] = -math.inf,
798        upper_bounds: Union[NumberT, pd.Series] = math.inf,
799    ) -> pd.Series:
800        """Creates a series of integer variables with the given name.
801
802        Args:
803          name (str): Required. The name of the variable set.
804          index (pd.Index): Required. The index to use for the variable set.
805          lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for
806            variables in the set. If a `pd.Series` is passed in, it will be based on
807            the corresponding values of the pd.Series. Defaults to -inf.
808          upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for
809            variables in the set. If a `pd.Series` is passed in, it will be based on
810            the corresponding values of the pd.Series. Defaults to +inf.
811
812        Returns:
813          pd.Series: The variable set indexed by its corresponding dimensions.
814
815        Raises:
816          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
817          ValueError: if the `name` is not a valid identifier or already exists.
818          ValueError: if the `lowerbound` is greater than the `upperbound`.
819          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
820          does not match the input index.
821        """
822        return self.new_var_series(name, index, lower_bounds, upper_bounds, True)

Creates a series of integer variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the lowerbound is greater than the upperbound.
  • ValueError: if the index of lower_bound, upper_bound, or is_integer
  • does not match the input index.
def new_bool_var_series( self, name: str, index: pandas.core.indexes.base.Index) -> pandas.core.series.Series:
824    def new_bool_var_series(
825        self,
826        name: str,
827        index: pd.Index,
828    ) -> pd.Series:
829        """Creates a series of Boolean variables with the given name.
830
831        Args:
832          name (str): Required. The name of the variable set.
833          index (pd.Index): Required. The index to use for the variable set.
834
835        Returns:
836          pd.Series: The variable set indexed by its corresponding dimensions.
837
838        Raises:
839          TypeError: if the `index` is invalid (e.g. a `DataFrame`).
840          ValueError: if the `name` is not a valid identifier or already exists.
841          ValueError: if the `lowerbound` is greater than the `upperbound`.
842          ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer`
843          does not match the input index.
844        """
845        return self.new_var_series(name, index, 0, 1, True)

Creates a series of Boolean variables with the given name.

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

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

Raises:
  • TypeError: if the index is invalid (e.g. a DataFrame).
  • ValueError: if the name is not a valid identifier or already exists.
  • ValueError: if the lowerbound is greater than the upperbound.
  • ValueError: if the index of lower_bound, upper_bound, or is_integer
  • does not match the input index.
def var_from_index( self, index: int | numbers.Integral | numpy.integer) -> Variable:
847    def var_from_index(self, index: IntegerT) -> Variable:
848        """Rebuilds a variable object from the model and its index."""
849        return Variable(self.__helper, index)

Rebuilds a variable object from the model and its index.

def add_linear_constraint( self, linear_expr: LinearExpr | int | float | numbers.Real | numpy.number, lb: int | float | numbers.Real | numpy.number = -inf, ub: int | float | numbers.Real | numpy.number = inf, name: str | None = None) -> LinearConstraint:
853    def add_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
854        self,
855        linear_expr: LinearExprT,
856        lb: NumberT = -math.inf,
857        ub: NumberT = math.inf,
858        name: Optional[str] = None,
859    ) -> LinearConstraint:
860        """Adds the constraint: `lb <= linear_expr <= ub` with the given name."""
861        ct = LinearConstraint(self.__helper)
862        if name:
863            self.__helper.set_constraint_name(ct.index, name)
864        if mbn.is_a_number(linear_expr):
865            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
866            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
867        elif isinstance(linear_expr, LinearExpr):
868            flat_expr = mbh.FlatExpr(linear_expr)
869            # pylint: disable=protected-access
870            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset)
871            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset)
872            self.__helper.add_terms_to_constraint(
873                ct.index, flat_expr.vars, flat_expr.coeffs
874            )
875        else:
876            raise TypeError(
877                "Not supported:"
878                f" Model.add_linear_constraint({type(linear_expr).__name__!r})"
879            )
880        return ct

Adds the constraint: lb <= linear_expr <= ub with the given name.

def add( self, ct: BoundedLinearExpression | bool | pandas.core.series.Series, name: str | None = None) -> LinearConstraint | pandas.core.series.Series:
882    def add(
883        self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None
884    ) -> Union[LinearConstraint, pd.Series]:
885        """Adds a `BoundedLinearExpression` to the model.
886
887        Args:
888          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
889          name: An optional name.
890
891        Returns:
892          An instance of the `Constraint` class.
893
894        Note that a special treatment is done when the argument does not contain any
895        variable, and thus evaluates to True or False.
896
897        `model.add(True)` will create a constraint 0 <= empty sum <= 0.
898        The constraint will be marked as under specified, and cannot be modified
899        thereafter.
900
901        `model.add(False)` will create a constraint inf <= empty sum <= -inf. The
902        constraint will be marked as under specified, and cannot be modified
903        thereafter.
904
905        you can check the if a constraint is under specified by reading the
906        `LinearConstraint.is_under_specified` property.
907        """
908        if isinstance(ct, mbh.BoundedLinearExpression):
909            return _add_linear_constraint_to_helper(ct, self.__helper, name)
910        elif isinstance(ct, bool):
911            return _add_linear_constraint_to_helper(ct, self.__helper, name)
912        elif isinstance(ct, pd.Series):
913            return pd.Series(
914                index=ct.index,
915                data=[
916                    _add_linear_constraint_to_helper(
917                        expr, self.__helper, f"{name}[{i}]"
918                    )
919                    for (i, expr) in zip(ct.index, ct)
920                ],
921            )
922        else:
923            raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})")

Adds a BoundedLinearExpression to the model.

Arguments:
Returns:

An instance of the Constraint class.

Note that a special treatment is done when the argument does not contain any variable, and thus evaluates to True or False.

model.add(True) will create a constraint 0 <= empty sum <= 0. The constraint will be marked as under specified, and cannot be modified thereafter.

model.add(False) will create a constraint inf <= empty sum <= -inf. The constraint will be marked as under specified, and cannot be modified thereafter.

you can check the if a constraint is under specified by reading the LinearConstraint.is_under_specified property.

def linear_constraint_from_index( self, index: int | numbers.Integral | numpy.integer) -> LinearConstraint:
925    def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint:
926        """Rebuilds a linear constraint object from the model and its index."""
927        return LinearConstraint(self.__helper, index=index)

Rebuilds a linear constraint object from the model and its index.

def add_enforced_linear_constraint( self, linear_expr: LinearExpr | int | float | numbers.Real | numpy.number, ivar: Variable, ivalue: bool, lb: int | float | numbers.Real | numpy.number = -inf, ub: int | float | numbers.Real | numpy.number = inf, name: str | None = None) -> EnforcedLinearConstraint:
931    def add_enforced_linear_constraint(  # pytype: disable=annotation-type-mismatch  # numpy-scalars
932        self,
933        linear_expr: LinearExprT,
934        ivar: "Variable",
935        ivalue: bool,
936        lb: NumberT = -math.inf,
937        ub: NumberT = math.inf,
938        name: Optional[str] = None,
939    ) -> EnforcedLinearConstraint:
940        """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name."""
941        ct = EnforcedLinearConstraint(self.__helper)
942        ct.indicator_variable = ivar
943        ct.indicator_value = ivalue
944        if name:
945            self.__helper.set_constraint_name(ct.index, name)
946        if mbn.is_a_number(linear_expr):
947            self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr)
948            self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr)
949        elif isinstance(linear_expr, LinearExpr):
950            flat_expr = mbh.FlatExpr(linear_expr)
951            # pylint: disable=protected-access
952            self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset)
953            self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset)
954            self.__helper.add_terms_to_constraint(
955                ct.index, flat_expr.vars, flat_expr.coeffs
956            )
957        else:
958            raise TypeError(
959                "Not supported:"
960                f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})"
961            )
962        return ct

Adds the constraint: ivar == ivalue => lb <= linear_expr <= ub with the given name.

def add_enforced( self, ct: BoundedLinearExpression | bool | pandas.core.series.Series, var: Variable | pandas.core.series.Series, value: bool | pandas.core.series.Series, name: str | None = None) -> EnforcedLinearConstraint | pandas.core.series.Series:
 964    def add_enforced(
 965        self,
 966        ct: Union[ConstraintT, pd.Series],
 967        var: Union[Variable, pd.Series],
 968        value: Union[bool, pd.Series],
 969        name: Optional[str] = None,
 970    ) -> Union[EnforcedLinearConstraint, pd.Series]:
 971        """Adds a `ivar == ivalue => BoundedLinearExpression` to the model.
 972
 973        Args:
 974          ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
 975          var: The indicator variable
 976          value: the indicator value
 977          name: An optional name.
 978
 979        Returns:
 980          An instance of the `Constraint` class.
 981
 982        Note that a special treatment is done when the argument does not contain any
 983        variable, and thus evaluates to True or False.
 984
 985        model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty
 986        sum <= 0
 987
 988        model.add_enforced(False, var, value) will create a constraint inf <=
 989        empty sum <= -inf
 990
 991        you can check the if a constraint is always false (lb=inf, ub=-inf) by
 992        calling EnforcedLinearConstraint.is_always_false()
 993        """
 994        if isinstance(ct, mbh.BoundedLinearExpression):
 995            return _add_enforced_linear_constraint_to_helper(
 996                ct, self.__helper, var, value, name
 997            )
 998        elif (
 999            isinstance(ct, bool)
1000            and isinstance(var, Variable)
1001            and isinstance(value, bool)
1002        ):
1003            return _add_enforced_linear_constraint_to_helper(
1004                ct, self.__helper, var, value, name
1005            )
1006        elif isinstance(ct, pd.Series):
1007            ivar_series = _convert_to_var_series_and_validate_index(var, ct.index)
1008            ivalue_series = _convert_to_series_and_validate_index(value, ct.index)
1009            return pd.Series(
1010                index=ct.index,
1011                data=[
1012                    _add_enforced_linear_constraint_to_helper(
1013                        expr,
1014                        self.__helper,
1015                        ivar_series[i],
1016                        ivalue_series[i],
1017                        f"{name}[{i}]",
1018                    )
1019                    for (i, expr) in zip(ct.index, ct)
1020                ],
1021            )
1022        else:
1023            raise TypeError(f"Not supported: Model.add_enforced({type(ct).__name__!r}")

Adds a ivar == ivalue => BoundedLinearExpression to the model.

Arguments:
Returns:

An instance of the Constraint class.

Note that a special treatment is done when the argument does not contain any variable, and thus evaluates to True or False.

model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty sum <= 0

model.add_enforced(False, var, value) will create a constraint inf <= empty sum <= -inf

you can check the if a constraint is always false (lb=inf, ub=-inf) by calling EnforcedLinearConstraint.is_always_false()

def enforced_linear_constraint_from_index( self, index: int | numbers.Integral | numpy.integer) -> EnforcedLinearConstraint:
1025    def enforced_linear_constraint_from_index(
1026        self, index: IntegerT
1027    ) -> EnforcedLinearConstraint:
1028        """Rebuilds an enforced linear constraint object from the model and its index."""
1029        return EnforcedLinearConstraint(self.__helper, index=index)

Rebuilds an enforced linear constraint object from the model and its index.

def minimize( self, linear_expr: LinearExpr | int | float | numbers.Real | numpy.number) -> None:
1032    def minimize(self, linear_expr: LinearExprT) -> None:
1033        """Minimizes the given objective."""
1034        self.__optimize(linear_expr, False)

Minimizes the given objective.

def maximize( self, linear_expr: LinearExpr | int | float | numbers.Real | numpy.number) -> None:
1036    def maximize(self, linear_expr: LinearExprT) -> None:
1037        """Maximizes the given objective."""
1038        self.__optimize(linear_expr, True)

Maximizes the given objective.

objective_offset: numpy.float64
1060    @property
1061    def objective_offset(self) -> np.double:
1062        """Returns the fixed offset of the objective."""
1063        return self.__helper.objective_offset()

Returns the fixed offset of the objective.

def objective_expression(self) -> LinearExpr:
1069    def objective_expression(self) -> "LinearExpr":
1070        """Returns the expression to optimize."""
1071        variables: list[Variable] = []
1072        coefficients: list[numbers.Real] = []
1073        for variable in self.get_variables():
1074            coeff = self.__helper.var_objective_coefficient(variable.index)
1075            if coeff != 0.0:
1076                variables.append(variable)
1077                coefficients.append(coeff)
1078        return mbh.FlatExpr(variables, coefficients, self.__helper.objective_offset())

Returns the expression to optimize.

def clear_hints(self):
1081    def clear_hints(self):
1082        """Clears all solution hints."""
1083        self.__helper.clear_hints()

Clears all solution hints.

def add_hint( self, var: Variable, value: int | float | numbers.Real | numpy.number) -> None:
1085    def add_hint(self, var: Variable, value: NumberT) -> None:
1086        """Adds var == value as a hint to the model.
1087
1088        Args:
1089          var: The variable of the hint
1090          value: The value of the hint
1091
1092        Note that variables must not appear more than once in the list of hints.
1093        """
1094        self.__helper.add_hint(var.index, value)

Adds var == value as a hint to the model.

Arguments:
  • var: The variable of the hint
  • value: The value of the hint

Note that variables must not appear more than once in the list of hints.

def export_to_lp_string(self, obfuscate: bool = False) -> str:
1097    def export_to_lp_string(self, obfuscate: bool = False) -> str:
1098        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1099        options.obfuscate = obfuscate
1100        return self.__helper.export_to_lp_string(options)
def export_to_mps_string(self, obfuscate: bool = False) -> str:
1102    def export_to_mps_string(self, obfuscate: bool = False) -> str:
1103        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1104        options.obfuscate = obfuscate
1105        return self.__helper.export_to_mps_string(options)
def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool:
1107    def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool:
1108        options: mbh.MPModelExportOptions = mbh.MPModelExportOptions()
1109        options.obfuscate = obfuscate
1110        return self.__helper.write_to_mps_file(filename, options)
def export_to_proto(self) -> ortools.linear_solver.linear_solver_pb2.MPModelProto:
1112    def export_to_proto(self) -> linear_solver_pb2.MPModelProto:
1113        """Exports the optimization model to a ProtoBuf format."""
1114        return mbh.to_mpmodel_proto(self.__helper)

Exports the optimization model to a ProtoBuf format.

def import_from_mps_string(self, mps_string: str) -> bool:
1116    def import_from_mps_string(self, mps_string: str) -> bool:
1117        """Reads a model from a MPS string."""
1118        return self.__helper.import_from_mps_string(mps_string)

Reads a model from a MPS string.

def import_from_mps_file(self, mps_file: str) -> bool:
1120    def import_from_mps_file(self, mps_file: str) -> bool:
1121        """Reads a model from a .mps file."""
1122        return self.__helper.import_from_mps_file(mps_file)

Reads a model from a .mps file.

def import_from_lp_string(self, lp_string: str) -> bool:
1124    def import_from_lp_string(self, lp_string: str) -> bool:
1125        """Reads a model from a LP string.
1126
1127        Note that this code is very limited, and will not support any real lp.
1128        It is only intented to be use to parse test lp problems.
1129
1130        Args:
1131          lp_string: The LP string to import.
1132
1133        Returns:
1134          True if the import was successful.
1135        """
1136        return self.__helper.import_from_lp_string(lp_string)

Reads a model from a LP string.

Note that this code is very limited, and will not support any real lp. It is only intented to be use to parse test lp problems.

Arguments:
  • lp_string: The LP string to import.
Returns:

True if the import was successful.

def import_from_lp_file(self, lp_file: str) -> bool:
1138    def import_from_lp_file(self, lp_file: str) -> bool:
1139        """Reads a model from a .lp file.
1140
1141        Note that this code is very limited, and will not support any real lp.
1142        It is only intented to be use to parse test lp problems.
1143
1144        Args:
1145          lp_file: The LP file to import.
1146
1147        Returns:
1148          True if the import was successful.
1149        """
1150        return self.__helper.import_from_lp_file(lp_file)

Reads a model from a .lp file.

Note that this code is very limited, and will not support any real lp. It is only intented to be use to parse test lp problems.

Arguments:
  • lp_file: The LP file to import.
Returns:

True if the import was successful.

def import_from_proto_file(self, proto_file: str) -> bool:
1152    def import_from_proto_file(self, proto_file: str) -> bool:
1153        """Reads a model from a proto file."""
1154        return self.__helper.read_model_from_proto_file(proto_file)

Reads a model from a proto file.

def export_to_proto_file(self, proto_file: str) -> bool:
1156    def export_to_proto_file(self, proto_file: str) -> bool:
1157        """Writes a model to a proto file."""
1158        return self.__helper.write_model_to_proto_file(proto_file)

Writes a model to a proto file.

num_variables: int
1162    @property
1163    def num_variables(self) -> int:
1164        """Returns the number of variables in the model."""
1165        return self.__helper.num_variables()

Returns the number of variables in the model.

num_constraints: int
1167    @property
1168    def num_constraints(self) -> int:
1169        """The number of constraints in the model."""
1170        return self.__helper.num_constraints()

The number of constraints in the model.

name: str
1172    @property
1173    def name(self) -> str:
1174        """The name of the model."""
1175        return self.__helper.name()

The name of the model.

1181    @property
1182    def helper(self) -> mbh.ModelBuilderHelper:
1183        """Returns the model builder helper."""
1184        return self.__helper

Returns the model builder helper.

class Solver:
1187class Solver:
1188    """Main solver class.
1189
1190    The purpose of this class is to search for a solution to the model provided
1191    to the solve() method.
1192
1193    Once solve() is called, this class allows inspecting the solution found
1194    with the value() method, as well as general statistics about the solve
1195    procedure.
1196    """
1197
1198    def __init__(self, solver_name: str):
1199        self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name)
1200        self.log_callback: Optional[Callable[[str], None]] = None
1201
1202    def solver_is_supported(self) -> bool:
1203        """Checks whether the requested solver backend was found."""
1204        return self.__solve_helper.solver_is_supported()
1205
1206    # Solver backend and parameters.
1207    def set_time_limit_in_seconds(self, limit: NumberT) -> None:
1208        """Sets a time limit for the solve() call."""
1209        self.__solve_helper.set_time_limit_in_seconds(limit)
1210
1211    def set_solver_specific_parameters(self, parameters: str) -> None:
1212        """Sets parameters specific to the solver backend."""
1213        self.__solve_helper.set_solver_specific_parameters(parameters)
1214
1215    def enable_output(self, enabled: bool) -> None:
1216        """Controls the solver backend logs."""
1217        self.__solve_helper.enable_output(enabled)
1218
1219    def solve(self, model: Model) -> SolveStatus:
1220        """Solves a problem and passes each solution to the callback if not null."""
1221        if self.log_callback is not None:
1222            self.__solve_helper.set_log_callback(self.log_callback)
1223        else:
1224            self.__solve_helper.clear_log_callback()
1225        self.__solve_helper.solve(model.helper)
1226        return SolveStatus(self.__solve_helper.status())
1227
1228    def stop_search(self):
1229        """Stops the current search asynchronously."""
1230        self.__solve_helper.interrupt_solve()
1231
1232    def value(self, expr: LinearExprT) -> np.double:
1233        """Returns the value of a linear expression after solve."""
1234        if not self.__solve_helper.has_solution():
1235            return pd.NA
1236        if mbn.is_a_number(expr):
1237            return expr
1238        elif isinstance(expr, LinearExpr):
1239            return self.__solve_helper.expression_value(expr)
1240        else:
1241            raise TypeError(f"Unknown expression {type(expr).__name__!r}")
1242
1243    def values(self, variables: _IndexOrSeries) -> pd.Series:
1244        """Returns the values of the input variables.
1245
1246        If `variables` is a `pd.Index`, then the output will be indexed by the
1247        variables. If `variables` is a `pd.Series` indexed by the underlying
1248        dimensions, then the output will be indexed by the same underlying
1249        dimensions.
1250
1251        Args:
1252          variables (Union[pd.Index, pd.Series]): The set of variables from which to
1253            get the values.
1254
1255        Returns:
1256          pd.Series: The values of all variables in the set.
1257        """
1258        if not self.__solve_helper.has_solution():
1259            return _attribute_series(func=lambda v: pd.NA, values=variables)
1260        return _attribute_series(
1261            func=lambda v: self.__solve_helper.variable_value(v.index),
1262            values=variables,
1263        )
1264
1265    def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series:
1266        """Returns the reduced cost of the input variables.
1267
1268        If `variables` is a `pd.Index`, then the output will be indexed by the
1269        variables. If `variables` is a `pd.Series` indexed by the underlying
1270        dimensions, then the output will be indexed by the same underlying
1271        dimensions.
1272
1273        Args:
1274          variables (Union[pd.Index, pd.Series]): The set of variables from which to
1275            get the values.
1276
1277        Returns:
1278          pd.Series: The reduced cost of all variables in the set.
1279        """
1280        if not self.__solve_helper.has_solution():
1281            return _attribute_series(func=lambda v: pd.NA, values=variables)
1282        return _attribute_series(
1283            func=lambda v: self.__solve_helper.reduced_cost(v.index),
1284            values=variables,
1285        )
1286
1287    def reduced_cost(self, var: Variable) -> np.double:
1288        """Returns the reduced cost of a linear expression after solve."""
1289        if not self.__solve_helper.has_solution():
1290            return pd.NA
1291        return self.__solve_helper.reduced_cost(var.index)
1292
1293    def dual_values(self, constraints: _IndexOrSeries) -> pd.Series:
1294        """Returns the dual values of the input constraints.
1295
1296        If `constraints` is a `pd.Index`, then the output will be indexed by the
1297        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1298        dimensions, then the output will be indexed by the same underlying
1299        dimensions.
1300
1301        Args:
1302          constraints (Union[pd.Index, pd.Series]): The set of constraints from
1303            which to get the dual values.
1304
1305        Returns:
1306          pd.Series: The dual_values of all constraints in the set.
1307        """
1308        if not self.__solve_helper.has_solution():
1309            return _attribute_series(func=lambda v: pd.NA, values=constraints)
1310        return _attribute_series(
1311            func=lambda v: self.__solve_helper.dual_value(v.index),
1312            values=constraints,
1313        )
1314
1315    def dual_value(self, ct: LinearConstraint) -> np.double:
1316        """Returns the dual value of a linear constraint after solve."""
1317        if not self.__solve_helper.has_solution():
1318            return pd.NA
1319        return self.__solve_helper.dual_value(ct.index)
1320
1321    def activity(self, ct: LinearConstraint) -> np.double:
1322        """Returns the activity of a linear constraint after solve."""
1323        if not self.__solve_helper.has_solution():
1324            return pd.NA
1325        return self.__solve_helper.activity(ct.index)
1326
1327    @property
1328    def objective_value(self) -> np.double:
1329        """Returns the value of the objective after solve."""
1330        if not self.__solve_helper.has_solution():
1331            return pd.NA
1332        return self.__solve_helper.objective_value()
1333
1334    @property
1335    def best_objective_bound(self) -> np.double:
1336        """Returns the best lower (upper) bound found when min(max)imizing."""
1337        if not self.__solve_helper.has_solution():
1338            return pd.NA
1339        return self.__solve_helper.best_objective_bound()
1340
1341    @property
1342    def status_string(self) -> str:
1343        """Returns additional information of the last solve.
1344
1345        It can describe why the model is invalid.
1346        """
1347        return self.__solve_helper.status_string()
1348
1349    @property
1350    def wall_time(self) -> np.double:
1351        return self.__solve_helper.wall_time()
1352
1353    @property
1354    def user_time(self) -> np.double:
1355        return self.__solve_helper.user_time()

Main solver class.

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

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

Solver(solver_name: str)
1198    def __init__(self, solver_name: str):
1199        self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name)
1200        self.log_callback: Optional[Callable[[str], None]] = None
log_callback: Callable[[str], None] | None
def solver_is_supported(self) -> bool:
1202    def solver_is_supported(self) -> bool:
1203        """Checks whether the requested solver backend was found."""
1204        return self.__solve_helper.solver_is_supported()

Checks whether the requested solver backend was found.

def set_time_limit_in_seconds(self, limit: int | float | numbers.Real | numpy.number) -> None:
1207    def set_time_limit_in_seconds(self, limit: NumberT) -> None:
1208        """Sets a time limit for the solve() call."""
1209        self.__solve_helper.set_time_limit_in_seconds(limit)

Sets a time limit for the solve() call.

def set_solver_specific_parameters(self, parameters: str) -> None:
1211    def set_solver_specific_parameters(self, parameters: str) -> None:
1212        """Sets parameters specific to the solver backend."""
1213        self.__solve_helper.set_solver_specific_parameters(parameters)

Sets parameters specific to the solver backend.

def enable_output(self, enabled: bool) -> None:
1215    def enable_output(self, enabled: bool) -> None:
1216        """Controls the solver backend logs."""
1217        self.__solve_helper.enable_output(enabled)

Controls the solver backend logs.

def solve( self, model: Model) -> SolveStatus:
1219    def solve(self, model: Model) -> SolveStatus:
1220        """Solves a problem and passes each solution to the callback if not null."""
1221        if self.log_callback is not None:
1222            self.__solve_helper.set_log_callback(self.log_callback)
1223        else:
1224            self.__solve_helper.clear_log_callback()
1225        self.__solve_helper.solve(model.helper)
1226        return SolveStatus(self.__solve_helper.status())

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

def value( self, expr: LinearExpr | int | float | numbers.Real | numpy.number) -> numpy.float64:
1232    def value(self, expr: LinearExprT) -> np.double:
1233        """Returns the value of a linear expression after solve."""
1234        if not self.__solve_helper.has_solution():
1235            return pd.NA
1236        if mbn.is_a_number(expr):
1237            return expr
1238        elif isinstance(expr, LinearExpr):
1239            return self.__solve_helper.expression_value(expr)
1240        else:
1241            raise TypeError(f"Unknown expression {type(expr).__name__!r}")

Returns the value of a linear expression after solve.

def values( self, variables: pandas.core.indexes.base.Index | pandas.core.series.Series) -> pandas.core.series.Series:
1243    def values(self, variables: _IndexOrSeries) -> pd.Series:
1244        """Returns the values of the input variables.
1245
1246        If `variables` is a `pd.Index`, then the output will be indexed by the
1247        variables. If `variables` is a `pd.Series` indexed by the underlying
1248        dimensions, then the output will be indexed by the same underlying
1249        dimensions.
1250
1251        Args:
1252          variables (Union[pd.Index, pd.Series]): The set of variables from which to
1253            get the values.
1254
1255        Returns:
1256          pd.Series: The values of all variables in the set.
1257        """
1258        if not self.__solve_helper.has_solution():
1259            return _attribute_series(func=lambda v: pd.NA, values=variables)
1260        return _attribute_series(
1261            func=lambda v: self.__solve_helper.variable_value(v.index),
1262            values=variables,
1263        )

Returns the values of the input variables.

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

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

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

def reduced_costs( self, variables: pandas.core.indexes.base.Index | pandas.core.series.Series) -> pandas.core.series.Series:
1265    def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series:
1266        """Returns the reduced cost of the input variables.
1267
1268        If `variables` is a `pd.Index`, then the output will be indexed by the
1269        variables. If `variables` is a `pd.Series` indexed by the underlying
1270        dimensions, then the output will be indexed by the same underlying
1271        dimensions.
1272
1273        Args:
1274          variables (Union[pd.Index, pd.Series]): The set of variables from which to
1275            get the values.
1276
1277        Returns:
1278          pd.Series: The reduced cost of all variables in the set.
1279        """
1280        if not self.__solve_helper.has_solution():
1281            return _attribute_series(func=lambda v: pd.NA, values=variables)
1282        return _attribute_series(
1283            func=lambda v: self.__solve_helper.reduced_cost(v.index),
1284            values=variables,
1285        )

Returns the reduced cost of the input variables.

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

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

pd.Series: The reduced cost of all variables in the set.

def reduced_cost( self, var: Variable) -> numpy.float64:
1287    def reduced_cost(self, var: Variable) -> np.double:
1288        """Returns the reduced cost of a linear expression after solve."""
1289        if not self.__solve_helper.has_solution():
1290            return pd.NA
1291        return self.__solve_helper.reduced_cost(var.index)

Returns the reduced cost of a linear expression after solve.

def dual_values( self, constraints: pandas.core.indexes.base.Index | pandas.core.series.Series) -> pandas.core.series.Series:
1293    def dual_values(self, constraints: _IndexOrSeries) -> pd.Series:
1294        """Returns the dual values of the input constraints.
1295
1296        If `constraints` is a `pd.Index`, then the output will be indexed by the
1297        constraints. If `constraints` is a `pd.Series` indexed by the underlying
1298        dimensions, then the output will be indexed by the same underlying
1299        dimensions.
1300
1301        Args:
1302          constraints (Union[pd.Index, pd.Series]): The set of constraints from
1303            which to get the dual values.
1304
1305        Returns:
1306          pd.Series: The dual_values of all constraints in the set.
1307        """
1308        if not self.__solve_helper.has_solution():
1309            return _attribute_series(func=lambda v: pd.NA, values=constraints)
1310        return _attribute_series(
1311            func=lambda v: self.__solve_helper.dual_value(v.index),
1312            values=constraints,
1313        )

Returns the dual values of the input constraints.

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]): The set of constraints from which to get the dual values.
Returns:

pd.Series: The dual_values of all constraints in the set.

def dual_value( self, ct: LinearConstraint) -> numpy.float64:
1315    def dual_value(self, ct: LinearConstraint) -> np.double:
1316        """Returns the dual value of a linear constraint after solve."""
1317        if not self.__solve_helper.has_solution():
1318            return pd.NA
1319        return self.__solve_helper.dual_value(ct.index)

Returns the dual value of a linear constraint after solve.

def activity( self, ct: LinearConstraint) -> numpy.float64:
1321    def activity(self, ct: LinearConstraint) -> np.double:
1322        """Returns the activity of a linear constraint after solve."""
1323        if not self.__solve_helper.has_solution():
1324            return pd.NA
1325        return self.__solve_helper.activity(ct.index)

Returns the activity of a linear constraint after solve.

objective_value: numpy.float64
1327    @property
1328    def objective_value(self) -> np.double:
1329        """Returns the value of the objective after solve."""
1330        if not self.__solve_helper.has_solution():
1331            return pd.NA
1332        return self.__solve_helper.objective_value()

Returns the value of the objective after solve.

best_objective_bound: numpy.float64
1334    @property
1335    def best_objective_bound(self) -> np.double:
1336        """Returns the best lower (upper) bound found when min(max)imizing."""
1337        if not self.__solve_helper.has_solution():
1338            return pd.NA
1339        return self.__solve_helper.best_objective_bound()

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

status_string: str
1341    @property
1342    def status_string(self) -> str:
1343        """Returns additional information of the last solve.
1344
1345        It can describe why the model is invalid.
1346        """
1347        return self.__solve_helper.status_string()

Returns additional information of the last solve.

It can describe why the model is invalid.

wall_time: numpy.float64
1349    @property
1350    def wall_time(self) -> np.double:
1351        return self.__solve_helper.wall_time()
user_time: numpy.float64
1353    @property
1354    def user_time(self) -> np.double:
1355        return self.__solve_helper.user_time()
ModelBuilder = <class 'Model'>
ModelSolver = <class 'Solver'>