Files
ortools-clone/ortools/math_opt/python/indicator_constraints.py
2025-08-22 14:24:48 +02:00

148 lines
5.6 KiB
Python

#!/usr/bin/env python3
# Copyright 2010-2025 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Linear constraint in a model."""
from typing import Any, Iterator, Optional
from ortools.math_opt.elemental.python import enums
from ortools.math_opt.python import from_model
from ortools.math_opt.python import variables
from ortools.math_opt.python.elemental import elemental
class IndicatorConstraint(from_model.FromModel):
"""An indicator constraint for an optimization model.
An IndicatorConstraint adds the following restriction on feasible solutions to
an optimization model:
if z == 1 then lb <= sum_{i in I} a_i * x_i <= ub
where z is a binary decision variable (or its negation) and x_i are the
decision variables of the problem. Equality constraints lb == ub is allowed,
which models the constraint:
if z == 1 then sum_{i in I} a_i * x_i == b
Setting lb > ub will result in an InvalidArgument error at solve time.
Indicator constraints have limited mutability. You can delete a variable
that the constraint uses, or you can delete the entire constraint. You
currently cannot update bounds or coefficients. This may change in future
versions.
If the indicator variable is deleted or was None at creation time, the
constraint will lead to an invalid model at solve time, unless the constraint
is deleted before solving.
The name is optional, read only, and used only for debugging. Non-empty names
should be distinct.
Do not create an IndicatorConstraint directly, use
Model.add_indicator_constraint() instead. Two IndicatorConstraint objects can
represent the same constraint (for the same model). They will have the same
underlying IndicatorConstraint.elemental for storing the data. The
IndicatorConstraint class is simply a reference to an Elemental.
"""
__slots__ = "_elemental", "_id"
def __init__(self, elem: elemental.Elemental, cid: int) -> None:
"""Internal only, prefer Model functions (add_indicator_constraint() and get_indicator_constraint())."""
if not isinstance(cid, int):
raise TypeError(f"cid type should be int, was:{type(cid).__name__!r}")
self._elemental: elemental.Elemental = elem
self._id: int = cid
@property
def lower_bound(self) -> float:
return self._elemental.get_attr(
enums.DoubleAttr1.INDICATOR_CONSTRAINT_LOWER_BOUND, (self._id,)
)
@property
def upper_bound(self) -> float:
return self._elemental.get_attr(
enums.DoubleAttr1.INDICATOR_CONSTRAINT_UPPER_BOUND, (self._id,)
)
@property
def activate_on_zero(self) -> bool:
return self._elemental.get_attr(
enums.BoolAttr1.INDICATOR_CONSTRAINT_ACTIVATE_ON_ZERO, (self._id,)
)
@property
def indicator_variable(self) -> Optional[variables.Variable]:
var_id = self._elemental.get_attr(
enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR, (self._id,)
)
if var_id < 0:
return None
return variables.Variable(self._elemental, var_id)
@property
def name(self) -> str:
return self._elemental.get_element_name(
enums.ElementType.INDICATOR_CONSTRAINT, self._id
)
@property
def id(self) -> int:
return self._id
@property
def elemental(self) -> elemental.Elemental:
"""Internal use only."""
return self._elemental
def get_coefficient(self, var: variables.Variable) -> float:
from_model.model_is_same(var, self)
return self._elemental.get_attr(
enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT,
(self._id, var.id),
)
def terms(self) -> Iterator[variables.LinearTerm]:
"""Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint."""
keys = self._elemental.slice_attr(
enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, 0, self._id
)
coefs = self._elemental.get_attrs(
enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, keys
)
for i in range(len(keys)):
yield variables.LinearTerm(
variable=variables.Variable(self._elemental, int(keys[i, 1])),
coefficient=float(coefs[i]),
)
def get_implied_constraint(self) -> variables.BoundedLinearExpression:
"""Returns the bounded expression from lower_bound, upper_bound and terms."""
return variables.BoundedLinearExpression(
self.lower_bound, variables.LinearSum(self.terms()), self.upper_bound
)
def __str__(self):
"""Returns the name, or a string containing the id if the name is empty."""
return self.name if self.name else f"linear_constraint_{self.id}"
def __repr__(self):
return f"<LinearConstraint id: {self.id}, name: {self.name!r}>"
def __eq__(self, other: Any) -> bool:
if isinstance(other, IndicatorConstraint):
return self._id == other._id and self._elemental is other._elemental
return False
def __hash__(self) -> int:
return hash(self._id)