"""
Module provides objects that can be used to manipulate linear programming problems.
The module provides a set of classes and functions that can be used to manipulate
linear programming problems.
These objects are:
- `lparray` : Numpy array with homogeneous LpVariables, LpAffineExpression or LpConstraints.
- `lp_minmax` : Get the minimum or maximum from a list of linear expressions.
- `addAbs`: Add absolute value constraints to a linear programming problem.
- `lp_multiply`: Multiplication of a binary and continuous variables.
"""
from __future__ import annotations
import logging
from typing import Any
from typing import Collection
from typing import Generic
from typing import Iterable
from typing import List
from typing import Literal
from typing import NoReturn
from typing import Optional
from typing import Protocol
from typing import Tuple
from typing import TypeVar
from typing import Union
import numpy as np
import pulp
from numpy import ndarray
from pulp import LpAffineExpression
from pulp import LpBinary
from pulp import LpConstraint
from pulp import LpContinuous
from pulp import LpProblem
from pulp import LpVariable
Axes = Collection[Any]
RecordT = TypeVar("RecordT") # noqa
LpComparable = Union["lparray", LpVariable, int, float]
# noinspection PyTypeHints
LpVarType = Literal["Binary", "Integer", "Continuous"]
LpSenseType = Literal[1, -1] # noqa
Number = Union[int, float]
LP = TypeVar("LP", LpVariable, LpAffineExpression, LpComparable)
LPV = TypeVar("LPV", LpVariable, LpAffineExpression)
[docs]class HasShape(Protocol): # pylint: disable=too-few-public-methods
"""
Protocol for objects that have a shape attribute.
This protocol defines a contract that classes can adhere to without having to
inherit from a common base class. It's useful when writing functions or classes
that expect an object with a specific attribute, in this case, `shape`.
Attributes
----------
shape : Tuple[int, ...]
Tuple indicating the dimensions of the object.
e.g., (2, 3) for a 2x3 matrix.
Notes
-----
The `HasShape` protocol can be used in type annotations to indicate that a
function or method expects an object with a `shape` attribute.
"""
shape: Tuple[int, ...]
[docs]def count_out(iterable: Iterable[Any]) -> List[int]:
"""
Return indices of items in the given iterable.
Given some iterable, this function generates a list of indices corresponding
to each item in the iterable.
Parameters
----------
iterable : Iterable[Any]
Input iterable of any type.
Returns
-------
List[int]
List of indices for each item in the input iterable.
Examples
--------
>>> count_out(["a", "b", "c"])
[0, 1, 2]
>>> count_out((1, 2, 3, 4))
[0, 1, 2, 3]
"""
iterable = iterable if isinstance(iterable, Iterable) else [iterable]
return [ix for ix, _ in enumerate(iterable)]
# noinspection PyPep8Naming
[docs]class lparray(ndarray, Generic[LP]): # type: ignore
"""
Numpy array with homogeneous LpVariables, LpAffineExpression or
LpConstraints.
All variables in the array will have the same:
* Lp* type
* (intrinsic) upper bound
* (intrinsic) lower bound
Also, all vectorized operations will preserve this invariant. External
manipulations that break this invariant will lead to wrong behavior.
All variables in an array are named, and share the same base name, which is
extended by indexes into a collection of index sets whose product spans the
elements of the array. These index sets can be named or anonymous --
anonymous index sets are int ranges.
Implements vectorized versions of various LP of
LpConstraint-type lparrays support the `constrain` method, which will
bind the constraints to an LpProblem.
In addition, a number of more sophisticated mathematical operations are
supported, many of which involve the creation of auxiliary lparrays
with variables behind the scenes.
"""
BINARY_UB: int = 1
"""Upper bound value for binary LpVariables."""
BINARY_LB: int = 0
"""Lower bound value for binary LpVariables."""
[docs] @classmethod
def create(
cls,
name: str,
index_sets: tuple[Collection[Any], ...],
*,
lowBound: Optional[Number] = None,
upBound: Optional[Number] = None,
cat: LpVarType = "Continuous",
) -> lparray[LpVariable]:
"""
Creates a lparray with shape from a cartesian product of index sets.
Each LpVariable in the array at an index [i_0, ..., i_n] will be named
as "{name}_(ix_sets[0][i_0], ..., ix_sets[n][i_n])"
Parameters
----------
name : str
Base names for the underlying LpVariables
index_sets : tuple[Collection[Any], ...]
An iterable of iterables containing the dimension names
for the array.
lowBound : Optional[Number]
Passed to LpVariable, uniform for an array
upBound : Optional[Number]
Passed as to `LpVariable`, uniform for the array
cat : LpVarType
Passed to `LpVariable`, uniform for an array defining the category
of each `LpVariable`. Defaults to "Integer"
Returns
-------
lparray[LpVariable]
Array of LP Variables
"""
cls.base_name = name
if len(index_sets) == 0:
return ( # type: ignore
np.array([
LpVariable(name,
cat=cat,
upBound=upBound,
lowBound=lowBound)
]).squeeze().view(lparray))
if len(index_sets) == 1:
name += "("
def recursive_worker(
r_name: str,
plane: np.ndarray,
r_index_sets: Tuple[Iterable[Any], ...],
) -> None:
if len(r_index_sets) == 1:
close_paren = r_name and (")" if "(" in r_name else "")
for ix in count_out(r_index_sets[0]):
plane[ix] = LpVariable(
f"{r_name}{ix}{close_paren}",
cat=cat,
upBound=upBound,
lowBound=lowBound,
)
else:
open_paren = r_name and ("(" if "(" not in r_name else "")
for ix in count_out(r_index_sets[0]):
recursive_worker(
f"{r_name}{open_paren}{ix},",
plane[ix],
r_index_sets[1:],
)
arr = np.zeros(tuple(len(ixset) for ixset in index_sets), dtype=object)
recursive_worker(name, arr, index_sets)
return arr.view(lparray) # type: ignore
[docs] @classmethod
def create_like(
cls,
name: str,
like: HasShape,
**kwargs: Any,
) -> lparray[LpVariable]:
"""Create a lparray with the same shape as the passed array.
Parameters
----------
name : str
Base names for the LpVariables in the array
like : HasShape
An object supporting `.shape` whose shape will be used
Returns
-------
lparray[LpVariable]
Anonymous lparray with the same shape as a passed array.
"""
return cls.create_anon(name, like.shape, **kwargs)
[docs] @classmethod
def create_anon(
cls,
name: str,
shape: Tuple[int, ...],
**kwargs: Any,
) -> lparray[LpVariable]:
"""Create a lparray with a given shape and nameless index sets.
Parameters
----------
name : str
Base names for the underlying LpVariables
shape : tuple[int, ...]
Array shape, same as for numpy arrays
**kwargs: Any
Pulp LpVariable extra arguments that you want to define
Returns
-------
lparray[LpVariable]
Array of LpVariables
"""
index_sets = tuple(list(range(d)) for d in shape)
return cls.create(name, index_sets, **kwargs)
def __ge__(self, other: LpComparable) -> lparray[LpConstraint]:
return np.greater_equal(self, other, dtype=object) # type: ignore
def __le__(self, other: LpComparable) -> lparray[LpConstraint]:
return np.less_equal(self, other, dtype=object) # type: ignore
def __lt__(self, other: LpComparable) -> NoReturn:
raise NotImplementedError("lparrays support only <=, >=, and ==")
def __gt__(self, other: LpComparable) -> NoReturn:
raise NotImplementedError("lparrays support only <=, >=, and ==")
def __eq__(self, other: LpComparable) -> lparray[LpConstraint]:
return np.equal(self, other, dtype=object) # type: ignore
@property
def values(self: lparray[LPV]) -> np.ndarray:
"""
Return the underlying values of the PuLP variables.
Values are returned by calling `pulp.value` on each element of the `lparray`.
If the problem is not solved, all entries are returned as `None`.
Returns
-------
np.ndarray
Numpy array with variable values
"""
return np.vectorize(pulp.value)(self).view(np.ndarray)
[docs] def constrain(self, prob: LpProblem, name: str) -> None:
"""Apply the constraints contained in `lparray` to the problem.
Parameters
----------
prob: LpProblem
Lp Problem to add the `lp_constraints` to.
name: str
Name of to the `lp_constraints`.
..Versionadded:: 0.4.0
Add "try/except" clause that adds lp_constraints to prob instance
without using the specified name if name is already taken.
"""
if not isinstance(prob, LpProblem):
raise TypeError(
f"Trying to constrain a {type(prob)}. Did you pass prob?")
if self.ndim == 0:
cons = self.item()
try:
prob += cons, name
except pulp.PulpError as exc:
logging.warning(exc)
prob += cons
return
if name and self.ndim == 1:
name += "("
def recursive_worker(r_prob: LpProblem, plane: np.ndarray,
r_name: str) -> None:
if plane.ndim == 1:
close_paren = r_name and (")" if "(" in r_name else "")
for cx, const in enumerate(plane):
if not isinstance(const, LpConstraint):
raise TypeError("Attempting to constrain problem with "
f"non-lp_constraints {const}")
const.name = r_name and f"{r_name}{cx}{close_paren}"
r_prob += const
else:
open_paren = r_name and ("(" if "(" not in r_name else "")
for px, subplane in enumerate(plane):
subname = r_name and f"{r_name}{open_paren}{px},"
recursive_worker(r_prob, subplane, subname)
# noinspection PydanticTypeChecker, PyTypeChecker
recursive_worker(prob, self, name)
[docs] def abs_decompose(
self: lparray[LPV],
prob: LpProblem,
name: str,
*,
bigM: Number = 1000.0,
**kwargs: Any,
) -> Tuple[lparray[LpVariable], lparray[LpVariable]]:
r"""
Constraint that generates two arrays, xp and xm, that sum to abs(self).
Constraint uses the following properties:
.. math::
\begin{array}{}
xp \geq 0 \\
xm \geq 0 \\
xp == 0 \or xm == 0 \\
\Sum_{i=1}^{n} (xp_i + xm_i) = |self| = X \\
\text{where:} \\
xp \rightarrow \text{Positive half of X} \\
xm \rightarrow \text{Negative half of X} \\
\end{array}
xp >= 0
xm >= 0
xp == 0 XOR xm == 0
Use the big M method.
Generates `2 * self.size` visible new variables.
Generates `1 * self.size` binary auxiliary variables.
Parameters
----------
prob : LpProblem
Problem to bind aux variables to
name : str
Base names for generated variables
bigM : Number
The -lower and upper bound on self to assume. Default value = 1000.0
kwargs : Any
Extra arguments to `create`
Returns
-------
Tuple[lparray[LpVariable], lparray[LpVariable]]
Arrays, xp and xm, that sum to |self|
Examples
--------
Let xb == binary variable (domain: {0, 1})
and x == integer expression comprised of
multiple variables with (domain: {-1000, 1000})
If we create the following constraints:
x <= 10000 * (1 - xb) -> c_lb
x >= -10000 * xb -> c_ub
If x is negative, then both c_lb and c_ub constraints
will only be True, when xb == 1
On the other hand, if x is positive, xb must be equal to 0.
"""
# w == 1 <=> self <= 0
w = lparray.create_like(
f"{name}_abs_aux",
self,
lowBound=self.BINARY_LB,
upBound=self.BINARY_UB,
cat=LpBinary,
)
# binding if self >= 0
(self <= bigM * (1 - w)).constrain(prob, f"{name}_lb")
# binding if self <= 0
(self >= -bigM * w).constrain(prob, f"{name}_ub")
# xp is the positive half of X, xm is the negative half of X
xp = lparray.create_like(f"{name}_absp", self, **kwargs)
xm = lparray.create_like(f"{name}_absm", self, **kwargs)
(xp >= 0).constrain(prob, f"{name}_abs_xplb")
(xm >= 0).constrain(prob, f"{name}_abs_xmlb")
(xp - xm == self).constrain(prob, f"{name}_absdecomp")
# xp >= 0 <=> xm == 0 and vice versa
(xp <= bigM * (1 - w)).constrain(prob, f"{name}_absxpexcl")
(xm <= bigM * w).constrain(prob, f"{name}_absxmexcl")
return xp, xm
[docs] def abs(
self,
prob: LpProblem,
name: str,
**kwargs: Any,
) -> lparray[LpAffineExpression]:
"""
Return variable equal to |self|.
Thin wrapper around `abs_decompose`
Parameters
----------
prob: LpProblem :
name: str
Base name of extra constraints `xm` and `xp` that'll be created
Returns
-------
lparray[LpAffineExpression]
Array of linear expressions that are always bigger or equal to 0
"""
xp, xm = self.abs_decompose(prob, name, **kwargs)
return xp + xm # type: ignore
[docs] def logical_clip(
self,
prob: LpProblem,
name: str,
bigM: Number = 1_000,
) -> lparray[LpVariable]:
"""
Assumes self is an integer >= 0.
Returns an array of the same shape as self-containing:
z_... = max(self_..., 1)
Generates `self.size` new variables.
Parameters
----------
prob : LpProblem
Problem to bind aux variables to
name : str
Base names for generated variables
bigM : Number
The -lower and upper bound on self to assume. Default value = 1000.0
Returns
-------
lparray[LpVariable]
Array of the same shape as self-containing max value between
problem variables and 1.
.. warning::
It is extremely important to correctly configure the value for bigM.
This value should be greater than the module of every lpVariable,
to be compared. If the problem variables have values in range of
-100,000,000 to 50,000,000, then bigM must be AT LEAST equal to
100,000,001.
On the other hand, setting bigM to values far greater than the
variables' range might result in considerable processing time.
"""
z = self.__class__.create(name,
tuple(range(x) for x in self.shape),
cat=LpBinary)
(self >= z).constrain(prob, f"{name}_lb")
(self <= bigM * z).constrain(prob, f"{name}_ub")
return z
[docs] def sumit(self, *args: Any, **kwargs: Any) -> LpVariable:
"""Equivalent to `self.sum().item()`."""
out = self.sum(*args, **kwargs)
return out.item()
# noinspection PyTypeHints
[docs] def _lp_minmax(
self,
prob: LpProblem,
name: str,
which: Literal["min", "max"],
cat: LpVarType,
*,
lb: Optional[Number] = None,
ub: Optional[Number] = None,
bigM: Number = 1000,
axis: Optional[Union[int, tuple[int, ...]]] = None,
) -> lparray[LpVariable]:
"""
Return lparray with its min/max along an axis.
The Axis can be multidimensional.
Parameters
----------
prob : LpProblem
Problem instance to which to apply the constraints.
name : str
Base LpVariable name for the min/max output array.
which : Literal["min", "max"]
Choice of operation. Can be either "min"
or "max" -- determines the operation
cat : LpVarType
LpCategory of the output lparray
lb : Optional[Number]
Lower bound on the output array
ub : Optional[Number]
Upper bound on the output array
bigM : Number
big M value used for auxiliary variable inequalities.
It Should be larger than any value that can appear in self in
a feasible solution.
axis : Optional[Union[int, tuple[int, ...]]]
Axes along which to take the maximum
Returns
-------
lparray[LpVariable]
lparray with min/max along a given axis.
Raises
------
ValueError
- If `axis` is None and `self` is a scalar.
- If `self.ndim` is not equal to `len(axis)`.
- If `cat` is not "Binary" and `lb` and `ub` are not provided.
- If `which` is not "min" or "max".
TypeError
If `axis` values are not integers.
"""
if not np.product(self.shape):
raise ValueError("No variables given!")
if axis is None:
axis = tuple(range(self.ndim))
elif isinstance(axis, int):
axis = (axis, )
elif (not isinstance(axis, tuple) or not axis
or any(not isinstance(ax, int) or not self.ndim > ax >= 0
for ax in axis)):
raise TypeError("Axis must be a tuple of positive integers")
if cat == LpBinary:
lb, ub = self.BINARY_LB, self.BINARY_UB
elif lb is None and ub is None:
raise ValueError(
"Need to supply constraints for non-binary variables!")
if which not in ("min", "max"):
raise ValueError(
f"{which} is not valid operation. Choose between min, max,")
aux_name = f"{name}_{which}_aux"
# Axes of self-which the max is indexed by
keep_axis = tuple(ix for ix in range(self.ndim) if ix not in axis)
# Array of max values
target_shape = sum((self.shape[ax:ax + 1] for ax in keep_axis), ())
target: lparray[LpVariable] = lparray.create_anon(name,
target_shape,
lowBound=lb,
upBound=ub,
cat=cat)
# "broadcastable" version for comparison with self
br = tuple((slice(None, None, None) if ax in keep_axis else None)
for ax in range(self.ndim))
target_br = target[br]
# indicator variable array.
# w[ixs ∈ span(axis), ~axis] == 1 <=> self[ixs, ~axis] is binding
w = self.create_like(aux_name,
self,
lowBound=0,
upBound=1,
cat=LpBinary)
(w.sum(axis=axis) == 1).constrain(prob, f"{name}_aux_sum")
if which == "max":
(target_br >= self).constrain(prob, f"{name}_lt_max")
(target_br
<= self + bigM * (1 - w)).constrain(prob, f"{name}_attains_max")
elif which == "min":
(target_br <= self).constrain(prob, f"{name}_gt_min")
(target_br
>= self - bigM * (1 - w)).constrain(prob, f"{name}_attains_min")
return target
# noinspection PyTypeHints
[docs] def _lp_int_minmax(
self,
prob: LpProblem,
name: str,
which: Literal["min", "max"],
lb: int,
ub: int,
**kwargs: Any,
) -> lparray[LpVariable]:
"""Internal method for `lparray.lp_int_max`."""
# Variable can be of type integer, but what's the difference
# between an integer variable with values between 0 and 1 and a
# binary variable? - There's No difference at all!
if lb == self.BINARY_LB and ub == self.BINARY_UB:
cat = pulp.LpBinary
else:
cat = pulp.LpInteger
return self._lp_minmax(prob,
name,
which=which,
cat=cat,
lb=lb,
ub=ub,
**kwargs)
# noinspection Pylint
[docs] def lp_int_max(
self,
prob: LpProblem,
name: str,
lb: int,
ub: int,
**kwargs: Any,
) -> lparray[LpVariable]:
"""
Return an array corresponding to the maximum value along an axis.
The array corresponds to the maximum value along specified axes.
"""
return self._lp_int_minmax(prob,
name,
which="max",
lb=lb,
ub=ub,
**kwargs)
[docs] def lp_int_min( # noqa
self,
prob: LpProblem,
name: str,
lb: int,
ub: int,
**kwargs: Any,
) -> lparray[LpVariable]:
"""
Return an array corresponding to the maximum value along an axis.
The Method can be used on **Integer** variables.
Parameters
----------
prob : LpProblem
Problem instance to which to apply the constraints
name : str
Base-name for the output array
lb : Optional[Number]
Lower bound on the output array
ub : Optional[Number]
Upper bound on the output array
Returns
-------
lparray[LpVariable]
Array with min along one of the axes.
"""
return self._lp_int_minmax(prob,
name,
which="min",
lb=lb,
ub=ub,
**kwargs)
[docs] def lp_bin_max(self, prob: LpProblem, name: str,
**kwargs: Any) -> lparray[LpVariable]:
"""
Return an array corresponding to the maximum value along an axis.
Binary variable type.
Parameters
----------
prob : LpProblem
Problem instance to which to apply the constraints.
name : str
Base-name for the output array.
**kwargs: Any
Returns
-------
lparray[LpVariable]
Array with the max along one of the axes.
"""
return self._lp_int_minmax(prob,
name,
lb=0,
ub=1,
which="max",
**kwargs)
[docs] def lp_bin_min(self, prob: LpProblem, name: str,
**kwargs: Any) -> lparray[LpVariable]: # noqa
"""
Return an array corresponding to the minimum value along an axis.
Binary variable type.
Parameters
----------
prob : LpProblem
Problem instance to which to apply the constraints.
name : str
Base-name for the output array,
**kwargs: Any
Returns
-------
lparray[LpVariable]
Array with min along one of the axes.
"""
return self._lp_int_minmax(prob,
name,
lb=0,
ub=1,
which="min",
**kwargs)
[docs] def lp_real_max(self, prob: LpProblem, name: str,
**kwargs: Any) -> lparray[LpVariable]: # noqa
"""
Return an array corresponding to the maximum value along an axis.
Continuous variable type.
Parameters
----------
prob : LpProblem
Problem instance to which to apply the constraints.
name : str
Base name for the output array
**kwargs: Any
Returns
-------
lparray[LpVariable]
Array with max along one of the axes.
"""
return self._lp_minmax(prob, name, "max", LpContinuous, **kwargs)
[docs] def lp_real_min(self, prob: LpProblem, name: str,
**kwargs: Any) -> lparray[LpVariable]: # noqa
"""
Return an array corresponding to the minimum value along an axis.
Continuous variable type.
Parameters
----------
prob : LpProblem
Problem instance to which to apply the constraints.
name : str
Base name for the output array
**kwargs: Any
Returns
-------
lparray[LpVariable]
Array with min along one of the axes.
"""
return self._lp_minmax(prob, name, "min", LpContinuous, **kwargs)
[docs] def lp_bin_and(
self: lparray[LPV],
prob: LpProblem,
name: str,
*ins: Union[lparray[LpVariable], lparray[LpAffineExpression], ndarray],
) -> lparray[LPV]:
"""Constrain the array using logical AND operation of binary inputs.
Parameters
----------
prob : LpProblem
Problem instance to which to apply the constraints.
name : str
Base name for the lp_constraints.
*ins: Union[lparray[LpVariable], lparray[LpAffineExpression], ndarray]
Returns
-------
lparray[LPV]
Array with implemented logic AND gate.
"""
for ix, _in in enumerate(ins):
(self <= _in).constrain(prob, f"{name}_and_ub{ix}")
# empty and = 1
(self >= sum(ins, 1 - len(ins))).constrain(prob, f"{name}_and_lb")
return self
[docs] def lp_bin_or(
self: lparray[LPV],
prob: LpProblem,
name: str,
*ins: Union[lparray[LpVariable], lparray[LpAffineExpression], ndarray],
) -> lparray[LPV]:
"""Constrain the array using the logical OR operation of binary inputs"""
for ix, _in in enumerate(ins):
(self >= _in).constrain(prob, f"{name}_or_lb{ix}")
# empty or = 0
(self <= sum(ins)).constrain(prob, f"{name}_and_ub")
return self
[docs]def lp_minmax(
lp_expression,
prob: LpProblem,
name: str,
which,
cat,
lb=None,
ub=None,
bigM=1000,
):
if cat == pulp.LpBinary:
lb = 0
ub = 1
elif lb is None or ub is None:
assert 0, "Need to supply constraints for non-binary variables!"
assert which in ("min", "max")
aux_name = f"{name}_{which}_aux"
target = pulp.LpVariable(name, lowBound=lb, upBound=ub, cat=cat)
w = [
pulp.LpVariable(f"{aux_name}_{idx}",
lowBound=0,
upBound=1,
cat=LpBinary) for idx, _ in enumerate(lp_expression)
]
prob += pulp.lpSum(w) == 1, f"{name}_aux_sum"
for idx, (wi, lpx) in enumerate(zip(w, lp_expression)):
if which == "max":
prob += target >= lpx, f"{name}_lt_max_{idx}"
prob += target <= lpx + bigM * (1
- wi), f"{name}_attains_max_{idx}"
elif which == "min":
prob += target <= lpx, f"{name}_gt_min_{idx}"
prob += target >= lpx - bigM * (1
- wi), f"{name}_attains_min_{idx}"
return target
[docs]def find_unique_name(prob: pulp.LpProblem, name: str) -> str:
"""Find a unique variable name for the problem.
If a variable with the given name exists, append '_<number>' to it,
where <number> starts from 1 and increases until a unique name is found.
Parameters
----------
prob : pulp.LpProblem
The problem to check for existing variable names.
name : str
The base name to check for uniqueness.
Returns
-------
str
A unique variable name.
"""
if name not in prob.variablesDict():
return name
counter = 1
while f"{name}_{counter}" in prob.variablesDict():
counter += 1
return f"{name}_{counter}"
[docs]def add_abs(
prob: pulp.LpProblem,
var: pulp.LpVariable | pulp.LpAffineExpression,
big_m: float | int = 100_000,
abs_var_name: str | None = None,
) -> pulp.LpVariable:
"""
Create an LP variable with the absolute value of a variable or expression.
This function introduces an auxiliary variable to the linear programming
problem that represents the absolute value of a provided variable or
expression. It also adds the necessary constraints to ensure this auxiliary
variable correctly captures the absolute value.
Parameters
----------
prob : pulp.LpProblem
The optimization problem to which the absolute value variable is added.
var : pulp.LpVariable | pulp.LpAffineExpression
The variable or expression whose absolute value is to be represented.
big_m : float | int, default=100000
A large constant required to create the auxiliary constraints needed to
create the variable that equals the absolute value of :param:`var`.
The value needs to be greater than any value that :param:`var` can have.
abs_var_name : str | None, optional
The name for the absolute value variable. If None, a name is generated
automatically.
Returns
-------
pulp.LpVariable
The auxiliary variable representing the absolute value of the provided
variable or expression.
Examples
--------
The following example demonstrates how the :func:`add_abs` can be used,
to find the absolute value for the `x` variable, that has a range of:
:math:`-10 \\leq{} \\text{x} \\leq{} 0`:
>>> prob = pulp.LpProblem("MyProblem", sense=pulp.LpMaximize)
>>> x = pulp.LpVariable("x", lowBound=-10, upBound=0)
>>> abs_x = add_abs(prob, x)
>>> prob.setObjective(abs_x)
>>> print(pulp.LpStatus[prob.solve(pulp.PULP_CBC_CMD(msg=False))])
'Optimal'
>>> print(f"Objective Value: {prob.objective.value():.0f}")
'Objective Value: 10'
>>> for name, lpvar in prob.variablesDict().items():
... print(f"{name} = {lpvar.value():.0f}")
abs(x) = 10
x = -10
binary_var(x) = 0
"""
var_name = "UNKNOWN_NAME"
if isinstance(var, pulp.LpAffineExpression):
var_name = var.getName()
if var_name is None:
var_name = var.__str__()
elif isinstance(var, pulp.LpVariable):
var_name = var.name
if abs_var_name is None:
abs_var_name = f"abs({var_name})"
# Create the absolute value variable
abs_var_name = find_unique_name(prob, abs_var_name)
abs_var = pulp.LpVariable(abs_var_name, lowBound=0)
# Binary variable to determine the sign of 'var'
binary_var_name = find_unique_name(prob, f"binary_var({var_name})")
binary_var = pulp.LpVariable(binary_var_name, cat=pulp.LpBinary)
# Add constraints
prob += abs_var >= var
prob += abs_var >= -var
prob += abs_var <= var + big_m * (1 - binary_var)
prob += abs_var <= -var + big_m * binary_var
return abs_var
[docs]def lp_multiply(
bin_lpvar: pulp.LpVariable,
cont_lpvar: pulp.LpVariable,
prob: pulp.LpProblem,
big_m: int | float = 100_000,
) -> pulp.LpProblem:
"""
Multiply a binary with a continuous `pulp.LpVariable`.
Introduces constraints to a given `pulp.LpProblem` that represent
the multiplication between a binary and a continuous `pulp.LpVariable`,
and returns the modified `pulp.LpProblem`.
Parameters
----------
bin_lpvar : pulp.LpVariable
Binary linear programming variable.
cont_lpvar : pulp.LpVariable
Continuous linear programming variable,
which should have both its lower- and upper-bounds set.
prob : pulp.LpProblem
The linear programming problem to which the
multiplication constraints will be added.
big_m : int | float, default=100_000
A value that is greater than the lower and upper bounds of `cont_lpvar`.
Returns
-------
pulp.LpProblem
The modified `pulp.LpProblem` with the added constraints.
Examples
--------
To illustrate how to use the function with pulp:
>>> import pulp
>>> prob = pulp.LpProblem("ExampleProblem", pulp.LpMaximize)
>>> bin_var = pulp.LpVariable("binary", 0, 1, pulp.LpBinary)
>>> cont_var = pulp.LpVariable("continuous", 0, 10)
>>> prob = lp_multiply(bin_var, cont_var, prob)
>>> prob += bin_var == 0
>>> prob.setObjective(cont_var)
>>> prob.solve(pulp.PULP_CBC_CMD(msg=False))
>>> print(cont_var.value())
0.0
"""
# Derive y_min and y_max from continuous variable's bounds
y_min = cont_lpvar.lowBound if cont_lpvar.lowBound is not None else -big_m
y_max = cont_lpvar.upBound if cont_lpvar.upBound is not None else big_m
# Introduce a new variable for the product
mult_lpvar = pulp.LpVariable(f"{bin_lpvar}_times_{cont_lpvar}", y_min,
y_max)
# Add constraints representing the multiplication
prob += mult_lpvar >= y_min * bin_lpvar
prob += mult_lpvar <= y_max * bin_lpvar
prob += mult_lpvar >= cont_lpvar - y_max * (1 - bin_lpvar)
prob += mult_lpvar <= cont_lpvar - y_min * (1 - bin_lpvar)
prob += mult_lpvar == cont_lpvar
return prob
[docs]def lp_define_or_constraint(
prob: pulp.LpProblem,
lp_expression: pulp.LpVariable | pulp.LpAffineExpression,
lp_binary_var: pulp.LpVariable,
low_bound: float,
up_bound: float,
):
"""
Define or constrain a linear programming problem in PuLP.
This function adds constraints to a given linear programming (LP) problem
based on a specified LP expression, some specified boundary conditions and
a binary variable. It uses the binary variable to impose the lower and upper bounds
on the LP expression, or set it to 0, otherwise. The function handles both single
`pulp.LpVariables` and `pulp.LpAffineExpressions`.
Parameters
----------
prob : pulp.LpProblem
The linear programming problem to which the constraints will be added.
lp_expression : pulp.LpVariable | pulp.LpAffineExpression
The linear programming expression (either a variable or an affine
expression) that is to be constrained.
lp_binary_var : pulp.LpVariable
The binary variable to link with the expression, to constraint the expression
from :param:`lp_expression` to be equal to 0, when this binary variable is also
0, or a value between :param:`low_bound` and :param:`up_bound` otherwise.
low_bound : float
The lower bound for the LP expression. The expression will be
constrained to be greater than or equal to this value times a binary
auxiliary variable.
up_bound : float
The upper bound for the LP expression. The expression will be
constrained to be less than or equal to this value times a binary
auxiliary variable.
Examples
--------
>>> prob = pulp.LpProblem("Example_Problem", pulp.LpMaximize)
>>> x = pulp.LpVariable("x", 0, 3)
>>> xb = pulp.LpVariable("xb", cat=pulp.LpBinary)
>>> lp_define_or_constraint(prob, x, xb, 0.8, 1)
This example adds constraints to the problem 'prob' that enforce variable 'x'
to be between 0.8 and 1 when a certain condition (represented by the binary
auxiliary variable) is true.
"""
prob += lp_expression >= low_bound * lp_binary_var
prob += lp_expression <= up_bound * lp_binary_var