Source code for wip.modules.lparray

"""
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