Source code for gana.sets.index

"""A set of index elements (X)"""

import logging
from operator import is_
from typing import Self

from IPython.display import Math, display

from .cases import ICase

logger = logging.getLogger("gana")

try:
    from pyomo.environ import RangeSet as PyoRangeSet
    from pyomo.environ import Set as PyoSet

    has_pyomo = True
except ImportError:
    has_pyomo = False

try:
    from sympy import FiniteSet

    has_sympy = True
except ImportError:
    has_sympy = False


[docs] class I: """ Set of index elements (X). :param members: Members of the Index set. :type members: str | int, optional :param size: Size of the Index set, creates an ordered set if given. :type size: int, optional :param mutable: If the Index set is mutable. Defaults to False. :type mutable: bool, optional :param tag: Tag/details. Defaults to None. :type tag: str, optional :param dummy: If the Index set is a dummy set, elements are created immediately. Defaults to False. :type dummy: bool, optional :ivar _: Elements of the index set :vartype _: list[X] :ivar tag: Tag/details :vartype tag: str :ivar ordered: Ordered set, True if size is given :vartype ordered: bool :ivar name: Name, set by the program :vartype name: str :ivar n: Number id, set by the program :vartype n: int :ivar ltx: LaTeX representation :vartype ltx: str :raises ValueError: If both members and size are given :raises ValueError: If indices of elements (P, V) are not compatible :raises ValueError: If index set is not ordered and step is given .. admonition:: Example .. code-block:: python p = Program() p.s1 = I('a', 'b', 'c') p.s2 = I('a', 'd', 'e', 'f') # Intersection p.s1 & p.s2 # I('a') # Union p.s1 | p.s2 # I('a', 'b', 'c', 'd', 'e', 'f') # Symmetric difference p.s1 ^ p.s2 # I('b', 'c', 'd', 'e', 'f') # Difference p.s1 - p.s2 # I('b', 'c') """ def __init__( self, *members: str | int, size: int = None, start: int = 0, mutable: bool = False, tag: str = None, ltx: str = None, dummy: bool = False, ): self.tag = tag self.mutable = mutable # set by program self.name = "" self.n = None self.start = start # this is when children single element sets are created # These will be set in ._ self.parent: list[Self] = [] self.pos: list[int] = [] self._: list[Self] = [] # if it is a slice self.slice: slice = None # hash passed on along with name # self._hash = '' if size: if members: raise ValueError( "An index set can either be defined by members or size, not both" ) self.size = size self.members = [] self.ordered = True elif members: self.size = len(members) self.members = members self.ordered = False else: self._ = [] self.members = [] self.ordered = False # # compound sets collect a list of children sets # # from which they are made # self.children: list[Self] = [] # # These are used for index arrays for function (F) sets # self.one: I = None # self.two: I = None self.parameters = [] self.variables = [] self.functions = [] self.constraints = [] # if latex name is given self._ltx = ltx if dummy: self.case = ICase.DUMMY self.birth_elements() else: self.case: ICase = None
[docs] def birth_elements(self): """Create elements for the index set""" # if self.ordered: self.size = int(self.size) for n in range(self.size): # this is called from outside # (once the name is set) # for an ordered index set # create new index index = I() # append parent index.parent.append(self) # update position in parent index.pos.append(n) # set that this is ordered index.ordered = True # give the name index.name = rf"{self}[{self.start + n}]" index._hash = hash(index.name) # the only element in element (index set of size one) # is itself index._ = [index] index.size = 1 index.members = [index.name] self._.append(index)
# index.ltx = r"{" + rf"{self.ltx}_{n}" + r"}" # ----------------------------------------------------- # Modifiers # -----------------------------------------------------
[docs] def step(self, n: int) -> list[Self]: """ Step up or down the index set :param n: Step size :type n: int :returns: New index set stepped up or down :rtype: I """ if not self.ordered: raise ValueError( "Index set is not ordered, cannot step up or down the index set" ) if not n: # if no step (0) return self # else create a new index set index = I() # if step is negative if n < 0: in_index = self._[:n] index._ = [None] * -n + in_index # the negative sign will come with n index.name = f"{self.name}{n}" index._ltx = rf"{self.ltx}{n}" else: in_index = self._[n:] index._ = in_index + [None] * n # + needs to be provided index.name = f"{self.name}+{n}" index._ltx = rf"{self.ltx}+{n}" # update the members index.members = [i.name for i in in_index] # note that this is a subset of self index.parent = self # the size is still the same index.size = self.size # only done for index set index.ordered = True return index
# ----------------------------------------------------- # Printing # ----------------------------------------------------- @property def ltx(self) -> str: """LaTeX representation""" if self.ordered: # this is a true subset, a single index point # not a splice or a step if self.parent and isinstance(self.parent, list): self._ltx = ( r"{" + rf"{self.parent[0].ltx}" + r"_{" + rf"{self.pos[0]}" + r"}}" ) elif not self._ltx: self._ltx = self.name.replace("_", r"\_") else: self._ltx = self.name.replace("_", r"\_") return r"{" + self._ltx + r"}"
[docs] def latex( self, descriptive: bool = True, int_not: bool = False, dots_limit: int = 5 ) -> str: """ LaTeX representation :param descriptive: print members of the index set :type descriptive: bool, optional :param int_not: Whether to display the set in integer notation. :type int_not: bool, optional :param ddot_limit: Maximum size over which ... is used to represent members. :type ddot_limit: int, optional :returns: LaTeX representation of the index set :rtype: str """ if not self.name: return "" if self.parent: return self.ltx if self.case == ICase.SELF: # if this is a self contained index return "" if descriptive: if self.ordered and int_not: members = ( rf"\{{ i = \mathbb{{{self.ltx}}} \mid " rf"{self._[0].ltx} \leq i \leq {self._[-1].ltx} \}}" ) else: members = ( r"{" + ( r", ".join(x.latex() for x in self._) if len(self) < dots_limit else rf"{self._[0].latex()}, \dots ,{self._[-1].latex()}" ) + r"}" ) return rf"{self.ltx} = \{{ {members} \}}" return self.ltx
[docs] def show( self, descriptive: bool = True, int_not: bool = False, dots_limit: int = 5 ): """ Display the set :param descriptive: Print members of the index set :type descriptive: bool, optional """ display(Math(self.latex(descriptive, int_not, dots_limit)))
[docs] def mps(self, pos: int) -> str: """ MPS representation :param pos: Position of the member in the set :type pos: int :returns: MPS representation of the member at position pos :rtype: str """ return rf"_{self[pos]}".upper()
[docs] def lp(self, pos: int) -> str: """ LP representation :param pos: Position of the member in the set :type pos: int """ return rf"_{self[pos]}"
# ----------------------------------------------------- # Birth # -----------------------------------------------------
[docs] def birth_index(self, name: str, members: list[Self]) -> Self: """ Updates the parent, sets new positions and mutable/ordered attributes :param name: Name of the new index set :type name: str :param members: Members of the new index set :type members: list[I] :returns: New index set :rtype: I """ # set new members for the index index = I() # set a name for the new index index.name = name # update the members of the index # doing this from outside avoids creating # element index sets (X) again index.members = members index.ordered = self.ordered index.size = len(members) index.mutable = self.mutable return index
# ----------------------------------------------------- # Operators # ----------------------------------------------------- # Avoid running instance checks def __eq__(self, other: Self): # equality checks for index sets are only done # on the basis of names return is_(self, other) def __and__(self, other: Self): # Members that exist in both Index sets _and = [i for i in self.members if i in other.members] return self.birth_index(rf"{self.name} & {other.name}", _and) def __or__(self, other: Self): # members that exist in either self or other # make a copy of the members in self _or = list(self.members) # if a member in other is not included for i in other.members: if i not in _or: # append it to the list _or.append(i) # mutable sets will have the same name # and are mutated using | (__or__) # repeated names are not allowed, # thus if the same name is coming in, # the index is definitely being mutated if self.name == other.name: return self.birth_index(self.name, _or) # else create a new name that reflects the operation return self.birth_index(rf"{self.name} | {other.name}", _or) def __xor__(self, other: Self): # members that exist in either self or other, but not both # create an empty list to be updated _xor: list[Self] = [] # if something is in self but not in other for i in self.members: if i not in other.members: _xor.append(i) # if something is in other but not in self for i in other.members: if i not in self.members: _xor.append(i) return self.birth_index(rf"{self.name} ^ {other.name}", _xor) def __sub__(self, other: int | Self): # other is an integer, step down the index set if isinstance(other, int): if len(self) == 1: return return self.step(-other) # members from other are removed from self # if other is some type of an Index set # create an empty list _sub = [] # if a member of self is in the other set for i in self.members: if i not in other.members: # do not append _sub.append(i) return self.birth_index(rf"{self.name} - {other.name}", _sub) def __add__(self, other: int | Self): # if other is an integer, step up the index set if isinstance(other, int): return self.step(other) raise NotImplementedError( 'Addition of Index sets is not implemented. Use | or the "or" operator for union.\n' "+ can be used to step up the index set by an integer." ) def __mul__(self, other: Self | tuple | None): # product of two Index sets if other is None: # This will likely be used mostly for element indices # allowing indices to be skipped while generating elements return None # if other is a tuple, return a tuple with self as the first element if isinstance(other, tuple): return (self,) + other # if other is an Index set, return a tuple of index sets return (self, other) def __rmul__(self, other: Self | tuple | None): if other is None: return None if isinstance(other, int): # This allows the use of math.prod if other == 1: return self # the only other allowed instance for which this # is called is tuple # Not running an instance check to save time return other + (self,) # ----------------------------------------------------- # Vector # ----------------------------------------------------- def __len__(self) -> int: return len(self._) # return len([i for i in self._ if not isinstance(i, Skip)]) def __iter__(self) -> Self: return iter(self._) def __getitem__(self, key: int | str | slice) -> Self: if isinstance(key, slice): # if this is a slice [start:stop] # generate a new index set for that stretch index = I(*self._[key], mutable=self.mutable, tag=self.tag) # mark this as a subset of self index.parent = self index.slice = key # note the start and stops if key.start is None: index.name = rf"{self.name}[0:{key.stop}]" else: index.name = rf"{self.name}[{key.start}:{key.stop}]" index.ordered = self.ordered index._ = self._[key] return index return self._[key] def __contains__(self, other: Self): return True if other in self._ else False # ----------------------------------------------------- # Hashing # ----------------------------------------------------- def __str__(self): return self.name def __repr__(self): return str(self) def __hash__(self): # this seems to work the fastest # not sure how pythonic this is though try: return self._hash except AttributeError: self._hash = hash(self.name) # self._hash = hash(self.name) return self.__hash__()
# ----------------------------------------------------- # Export # ----------------------------------------------------- # def sympy(self): # """Sympy representation""" # if has_sympy: # return FiniteSet(*[str(s) for s in self._]) # logger.warning( # "⚠ sympy is an optional dependency, pip install gana[all] to get optional dependencies ⚠" # ) # def pyomo(self): # """Pyomo representation""" # if has_pyomo: # if self.ordered: # return PyoRangeSet(len(self), doc=self.tag) # return PyoSet(initialize=[i.name for i in self._], doc=self.tag) # logger.warning( # "⚠ pyomo is an optional dependency, pip install gana[all] to get optional dependencies ⚠" # )