Source code for gana.sets.theta

# """Parametric variable"""

from __future__ import annotations

from itertools import product
# from functools import reduce
from math import prod
from typing import TYPE_CHECKING, Self
from warnings import warn

from IPython.display import Math, display

from ._element import _E
from .birth import make_P
from .cases import Elem
from .function import F
from .index import I
from .variable import V

if TYPE_CHECKING:
    from .parameter import P


[docs] class T(_E): """ Ordered set of parametric variables (theta). :param index: Index for the theta set. :type index: I :param _: Values for the theta set. :type _: list[tuple[float]] | tuple[float] :param mutable: If True, the theta set can be modified. Defaults to False. :type mutable: bool, optional :param tag: Tag for the theta set. Defaults to None. :type tag: str, optional :ivar index: Index of the parametric variable set :vartype index: I :ivar _: List of parametric variables :vartype _: list[int | float] :ivar mutable: If the parametric variable set is mutable :vartype mutable: bool :ivar tag: Tag/details :vartype tag: str :ivar name: Name, set by the program :vartype name: str :ivar n: Number id, set by the program :vartype n: int :ivar map: Index to parameter mapping :vartype map: dict[X | Idx, Var] """ def __init__( self, *index: I, _: list[tuple[float]] | tuple[float] | None = None, tag: str | None = None, mutable: bool = False, ltx: str | None = None, name: str = "", ): _E.__init__(self, *index, tag=tag, ltx=ltx, mutable=mutable, name=name) # name will be set by the program later # if dummy index, the name is set to 'φ' (phi) self.name = self.name or "θ" # containt the set of parameteric variables self._: list[Self] = _ # always a list of parameteric variables self.lb: float | int | None = None self.ub: float | int | None = None # flag to check if the set has been birthed self.birthed = False # ----------------------------------------------------- # Helpers # ----------------------------------------------------- @property def args(self) -> dict[str, str | bool]: """Return the arguments of the parametric variable set :returns: Dictionary of arguments :rtype: dict """ return {"tag": self.tag, "ltx": self.ltx, "mutable": self.mutable}
[docs] def create_map(self): """Create a map of indices to parameters""" self.map: dict[tuple[I], Self] = { prod(i): self._[n] for n, i in enumerate(list(product(*self.index))) }
# ----------------------------------------------------- # Birthing # -----------------------------------------------------
[docs] def birth_thetas(self, mutating: bool = False, n_start=0): """Births a parametric variable (Theta) at every index in the index set :param mutating: If the variable set is being mutated. Defaults to False. :type mutating: bool, optional :param n_start: The starting number for positioning the variables. Defaults to 0. :type n_start: int, optional """ for pos, idx in enumerate(self.map): # create a theta at every index theta = T(**self.args) # set parent to self theta.parent = self # this is the nth parametric variable set declared theta.n = n_start + pos if not mutating: # give self + pos as name theta.name = f"{self.name}[{pos}]" # give position theta.pos = pos # give the index theta.index = idx # kind of recursive, but for elemental sets # the element set only contains a tuple theta.lb = self._[pos][0] theta.ub = self._[pos][1] theta._ = [theta] # theta._ = self._[pos] # append the new parametric variable to the set self._[pos] = theta # update the maps for both self and theta self.map[idx] = theta theta.map[idx] = theta # set the birthing flag self.birthed = True
# ----------------------------------------------------- # Matrix # ----------------------------------------------------- @property def CrA(self): """A matric of critical region""" CrA_UB = [[0] * len(self) for _ in range(len(self))] CrA_LB = [[0] * len(self) for _ in range(len(self))] for n in range(len(self)): CrA_UB[n][n] = 1.0 CrA_LB[n][n] = -1.0 CrA_ = [] for n in range(len(self)): CrA_.append(CrA_UB[n]) CrA_.append(CrA_LB[n]) return CrA_ @property def CrB(self): """B matrix of critical region""" CrB_ = [] for t in self._: CrB_.append(t._[1]) CrB_.append(-t._[0]) return CrB_ # ----------------------------------------------------- # Printing # ----------------------------------------------------- @property def ltx(self) -> str: """LaTeX representation of the parametric variable set""" if self._ltx: return r"{" + self._ltx + r"}" # if user has not set the LaTeX representation # the name becomes the latex representation if self.name: return r"{" + self.name.replace("_", r"\_") + r"}" return self._ltx @property def longname(self) -> str: """Long name representation""" if self.parent: return f"{self.parent.name}(" + ",".join([i.name for i in self.index]) + ")" return f"{self.name}(" + ",".join([i.name for i in self.index]) + ")"
[docs] def latex(self) -> str: """LaTeX representation""" index = ( r"_{" + rf"{self.index}".replace("(", "") .replace(")", "") .replace("[", "{") .replace("]", "}") + r"}" ) if len(self.index) == 1: # if there is a single index element # then a comma will show up in the end, replace that return self.ltx + index.replace(",", "") # type: ignore return self.ltx + index
[docs] def show(self, descriptive: bool = False): """Display the variables :param descriptive: Print members of the index set :type descriptive: bool, optional """ if descriptive: for pv in self._: if pv: display(Math(pv.latex())) else: display(Math(self.latex()))
# ----------------------------------------------------- # Operations # ----------------------------------------------------- def __neg__(self): return self * -1 def __add__(self, other: Self | P | V): # type of instance of other two_type = None if isinstance(other, (int, float)): # if adding with numeric if other in [0, 0.0]: # return self is zero return self # else make parameter other = make_P(other, index=self.index) two_type = Elem.P if isinstance(other, tuple): # if tuple, make T other = T(*self.index, _=other, **self.args) two_type = Elem.T if isinstance(other, list): # if list.. # check first element type if isinstance(other[0], tuple): # if tuple # make T other = T(_=other, **self.args) two_type = Elem.T else: # assume it is a list of numbers other = make_P(other) two_type = Elem.P # irrespective just make a function, # if type not passed function.types will figure out return F(one=self, add=True, two=other, one_type=Elem.T, two_type=two_type) def __sub__(self, other: Self | P | V): # type of instance of other two_type = None if isinstance(other, (int, float)): # if adding with numeric if other in [0, 0.0]: # return self is zero return self # else make parameter other = make_P(other, index=self.index) two_type = Elem.P if isinstance(other, tuple): # if tuple, make T other = T(*self.index, _=other, **self.args) two_type = Elem.T if isinstance(other, list): # if list.. # check first element type if isinstance(other[0], tuple): # if tuple # make T other = T(_=other, **self.args) two_type = Elem.T else: # assume it is a list of numbers other = make_P(other) two_type = Elem.P # irrespective just make a function, # if type not passed function.types will figure out return F(one=self, sub=True, two=other, one_type=Elem.T, two_type=two_type) def __radd__(self, other: Self | P | V | F): return other + self def __rsub__(self, other: Self | P | V | F): return -self + other def __mul__(self, other: Self | F): if isinstance(other, (int, float)): if other in [1, 1.0]: # if one return self return self # else scale the bounds t = T( *self.index, _=[tuple([other * j for j in i]) for i in self._], **self.args, ) t.name = f"{other}*{self}" return t if isinstance(other, tuple): # if tuple scale the bounds again t = T( *self.index, _=[(other[0] * i[0], other[1] * i[1]) for i in self._], **self.args, ) t.name = f"{self}*θ" if isinstance(other, list): # if list.. # scale and return # check length match if len(self) != len(other): warn( f"Index mismatch for {self} * {other}: ({len(self)} != {len(other)})" ) # check first element type if isinstance(other[0], tuple): # if tuple # make T t = T( *self.index, _=[(i[0] * j[0], i[1] * j[1]) for i, j in zip(self, other)], **self.args, ) t.name = f"{self}*θ" return t # otherwise assume numeric t = T( *self.index, _=[(i[0] * j, i[1] * j) for i, j in zip(self, other)], **self.args, ) # else let other handle the operation return other * self def __rmul__(self, other: Self | F | int): return self * other def __truediv__(self, other: Self | F | int): if isinstance(other, (int, float)): if other in [1, 1.0]: # if one return self return self # else scale the bounds t = T( *self.index, _=[tuple([i / other for i in j]) for j in self._], **self.args, ) t.name = f"{self}/{other}" return t if isinstance(other, tuple): # theta\theta not allowed raise NotImplementedError( "Division by tuple is not implemented for theta sets." ) if isinstance(other, list): # if list.. # scale and return # check length match if len(self) != len(other): warn( f"Index mismatch for {self} / {other}: ({len(self)} != {len(other)})" ) # check first element type if isinstance(other[0], tuple): # if tuple raise NotImplementedError( "Division by list of tuples is not implemented for theta sets." ) # otherwise assume numeric t = T( *self.index, _=[(i[0] / j, i[1] / j) for i, j in zip(self, other)], **self.args, ) raise NotImplementedError( "Division by anything other than numeric is not implemented for theta sets." ) def __call__(self, *key: I) -> Self: if not key or (key == self.index): # if the index is an exact match # or no key is passed return self # if a subset is passed, # first create a product to match # the indices indices = list(product(*key)) # create a new variable set to return t = T(**self.args) t.name, t.n = self.name, self.n t.index = key # should be able to map these for index in indices: # this helps weed out any None indices # i.e. skips index = prod(index) if index is None: theta = None else: theta = self.map[index] t.map[index] = theta t._.append(theta) return t