Source code for gana.sets.parameter

"""Paramter Set"""

from __future__ import annotations

import logging
from itertools import product
from typing import TYPE_CHECKING, Self
from warnings import warn

from IPython.display import Math, display

from ..utils.draw import draw
from ._element import _E
from .birth import make_T
from .cases import Elem, PCase
from .function import F
from .index import I
from .variable import V

logger = logging.getLogger("gana")

if TYPE_CHECKING:
    from .theta import T

try:
    from pyomo.environ import Param as PyoParam

    has_pyomo = True
except ImportError:
    has_pyomo = False

try:
    from sympy import Idx, IndexedBase, symbols

    has_sympy = True
except ImportError:
    has_sympy = False


[docs] class P(_E): """ Ordered set of parameters. Does not support `inf` or `nan` values. :param index: Indices of the parameter set. :type index: tuple[I], optional :param _: List of parameters. All values are converted to float. :type _: list[int | float], optional :param mutable: If the parameter set is mutable. :type mutable: bool, optional :param tag: Tag/details :type tag: str, optional :ivar index: Index of the parameter set :vartype index: I :ivar _: List of parameters (converted to float) :vartype _: list[int | float] :ivar mutable: If the parameter 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] :ivar case: Special case of the parameter set :vartype case: PCase :raises ValueError: If `!=` operator is used with any type other than `P` :raises ValueError: If the parameter values and the length of indices do not match """ # TODO update Errors in docstring # TODO add examples def __init__( self, *index: I, _: list[float] | float | None = None, mutable: bool = False, tag: str = "", ltx: str = "", name: str = "", ): _E.__init__(self, *index, tag=tag, ltx=ltx, mutable=mutable, name=name) # special case of the parameter self.case: PCase = PCase.SET # No value is provided if _ is None: self._ = [] elif isinstance(_, (int, float)): # if int or float is passed it is a single number # names for these are generated automatically # in __str__() self.name = str(_) # set the name to the number if _ > 0: # postive number self.case = PCase.NUM elif _ < 0: # negative number self.case = PCase.NEGNUM else: # 0 self.case = PCase.ZERO if self.index: # if index is passed # make length equal to index self._ = [float(_)] * len(self.map) else: self.index = (I(size=1, dummy=True),) self._ = [float(_)] else: # if some sort of iterable is passed # preferably a list # set by program if not self.index and _: # if index is not passed # make a dummy index self.index = (I(size=len(_), dummy=True),) # here the map needs to be remade # self.map = {i: None for i in product(*self.index)} self.name = "φ" # set the name to phi self._ = [float(p) for p in _] # fill in the values for n, k in enumerate(self.map): self.map[k] = self._[n] @property def args(self) -> dict[str, str | bool]: """Return the arguments of the parameter set""" return {"tag": self.tag, "ltx": self.ltx, "mutable": self.mutable} # ----------------------------------------------------- # Matrix # ----------------------------------------------------- @property def A(self) -> list[list[float]]: """Generate a diagonal matrix representation of the variable set""" return [[self._[i]] for i in range(len(self))] # ----------------------------------------------------- # Printing # ----------------------------------------------------- @property def ltx(self) -> str: """LaTeX representation""" if not self._ltx: # use name if no LaTeX self._ltx = self.name.replace("_", r"\_") return r"{\mathrm{" + self._ltx + r"}}" @property def index_ltx(self) -> str: """LaTeX representation of the index""" if len(self.index) == 1: return self.index[0].ltx if isinstance(self.index, set): return ( rf"({')|('.join(','.join(i.ltx for i in idx) for idx in self.index)})" ) return rf"{','.join(i.ltx if not isinstance(i, list) else i[0].ltx for i in self.index)}"
[docs] def latex(self) -> str: """ LaTeX representation :returns: LaTeX representation of the parameter set :rtype: str """ if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: return str(self) return self.ltx + r"_{" + self.index_ltx + r"}"
[docs] def show(self, descriptive: bool = False): """Display the variables Args: descriptive (bool, optional): If True, shows all parameters. Defaults to False. """ if descriptive: # just print out the parameters for p in self._: print(p) display(Math(self.latex()))
# ----------------------------------------------------- # Value # ----------------------------------------------------- def __neg__(self): if self.case == PCase.ZERO: # if zero return self return self if self.case in [PCase.NUM, PCase.NEGNUM]: # if number return a negation return P(*self.index, _=-self._[-1]) # else negate the number and return a new parameter set p = P(*self.index, _=[-i for i in self._], **self.args) if self.case == PCase.NEGSET: # if this is already a negated set # make it a normal set now p.name = f"{self.name[1:]}" # remove the negative sign p.case = PCase.SET else: # if this is a normal set # make it a negated set p.case = PCase.NEGSET p.name = f"-{self}" # add the negative sign # return the new parameter set return p def __pos__(self): if self.case == PCase.NEGNUM: # if it is a negative number # return a positive number return P(*self.index, _=-self._[-1], **self.args) if self.case == PCase.NEGSET: p = P(*self.index, _=[-i for i in self._], **self.args) p.name = self.name[1:] # remove the negative sign return p # if self.case in [PCase.ZERO, PCase.SET, PCase.NUM]: # if it is zero, a normal set, or a number # return itself return self def __abs__(self): if self.case == PCase.ZERO: # if zero return self return self if self.case == PCase.SET: # if it is a set return a normal set p = P(*self.index, _=[abs(i) for i in self._], **self.args) p.name = f"|{self.name}|" return p if self.case in [PCase.NEGNUM, PCase.NEGSET]: # if it is a negative number or a negated set # return a positive number or a normal set return -self # else just itself return self def __float__(self): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: return self._[-1] return self # ----------------------------------------------------- # Operators # ----------------------------------------------------- # Handling basic operations---- # if there is a zero on the left, just return P # if the other is a parameter, add the values # if the other is a function/variable, return a function # r<operation> # for the right hand side operations # they only kick in when the left hand side operator # does not have the operation/the operation did not work # in this case, we just do the equivalent self def __add__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: if other is None: # if adding with None return self if self.case == PCase.ZERO: # if self is zero # return other return other if isinstance(other, (int, float)): # if adding to a number if other in [0, 0.0]: # if adding zero, return self return self if self.case in [PCase.NEGNUM, PCase.NUM]: # P (number) - other # the number will be set to NEGNUM, NUM if self._[-1] + other in [0, 0.0]: # if number returns zero, return int 0 return 0 return P(*self.index, _=self._[-1] + other, **self.args) # if set or negative set # just add the number to each value # and return new parameter set p = P(*self.index, _=[i + other for i in self._], **self.args) p.name = f"({self}+{other})" return p if isinstance(other, list): # lengths should match if self.case in [PCase.NUM, PCase.NEGNUM]: p = P(*self.index, _=[self._[-1] + i for i in other], **self.args) else: if len(self) != len(other): warn( f"Index mismatch {self} + {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if list, just zip through the values and add p = P( *self.index, _=[i + j for i, j in zip(self._, other)], **self.args ) # change the name to add that something from a list has been added p.name = f"({self}+φ)" return p if isinstance(other, tuple): # if tuple, make T # and make a function return F( one=make_T(other, index=self.index), add=True, two=self, one_type=Elem.T, two_type=Elem.P, ) if isinstance(other, P): # lengths should match if other.case == PCase.ZERO: # if other is zero return self if self.case in [PCase.NUM, PCase.NEGNUM]: if other.case in [PCase.NUM, PCase.NEGNUM]: # if both are numbers # let P handle the case if self._[-1] + other._[-1] in [0, 0.0]: # if number returns zero, return int 0 return 0 return P(*self.index, _=self._[-1] + other._[-1], **self.args) # else make a new parameter set p = P(*self.index, _=[self._[-1] + i for i in other._], **self.args) p.name = f"({self}+{other})" return p if other.case in [PCase.NUM, PCase.NEGNUM]: # if other is a number, add other to every value in the set p = P(*self.index, _=[i + other._[-1] for i in self._], **self.args) p.name = f"({self}+{other})" return p if len(self) != len(other): warn( f"Index mismatch {self} + {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if either one of them is general parameter p = P(*self.index, _=[i + j for i, j in zip(self._, other._)], **self.args) p.name = f"({self}+{other})" return p # if not parameter, let the other handle elements operator handle the addition if self.case in [PCase.NEGNUM, PCase.NEGSET]: # if negative number, handle using sub, but keep parameter at two # (-P) + E = E - P return F(one=other, sub=True, two=-self, two_type=Elem.P) # keep additive or subtractive parameter at two # P + E = E + P return F(one=other, add=True, two=self, two_type=Elem.P) def __radd__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: # let __add__() handle the addition return self + other def __sub__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P | F: if other is None: # if adding with None return self if self.case == PCase.ZERO: # if self is zero # return negative of other return -other if isinstance(other, (int, float)): # if adding to a number if other in [0, 0.0]: # if adding zero, return self return self if self.case in [PCase.NEGNUM, PCase.NUM]: if self._[-1] - other == 0: # if number returns zero, return int 0 return 0 # let P make a .NUM type return P(*self.index, _=self._[-1] - other, **self.args) p = P(*self.index, _=[i - other for i in self._], **self.args) p.name = f"({self}-{other})" return p if isinstance(other, list): # if list, just zip through the values and add if self.case in [PCase.NUM, PCase.NEGNUM]: p = P(*self.index, _=[self._[-1] - i for i in other], **self.args) else: if len(self) != len(other): warn( f"Index mismatch {self} - {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) p = P( *self.index, _=[i - j for i, j in zip(self._, other)], **self.args ) # change the name to add that something from a list has been added p.name = f"({self}-φ)" return p if isinstance(other, tuple): # if tuple, make T # and make a function return F( one=make_T(other, index=self.index), sub=True, two=self, one_type=Elem.T, two_type=Elem.P, ) if isinstance(other, P): if other.case == PCase.ZERO: # if other is zero return self if self.case in [PCase.NUM, PCase.NEGNUM]: if other.case in [PCase.NUM, PCase.NEGNUM]: # if both are numbers # let P handle the case if self._[-1] - other._[-1] in [0, 0.0]: # if number returns zero, return int 0 return 0 # let P handle the case based on the value that is determines return P(*self.index, _=self._[-1] - other._[-1], **self.args) p = P(*self.index, _=[self._[-1] - i for i in other._], **self.args) p.name = f"({self}-{other.name})" return p if other.case in [PCase.NUM, PCase.NEGNUM]: # if other is a number subtract every value in self with the number p = P(*self.index, _=[i - other._[-1] for i in self._], **self.args) p.name = f"({self}-{other})" return p if len(self) != len(other): raise ValueError( f"Index mismatch {self} - {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if both are just general parameters # zip through the values and subtract p = P(*self.index, _=[i - j for i, j in zip(self._, other._)], **self.args) p.name = f"({self}-{other})" return p # if not parameter, let the other handle elements operator handle the addition if self.case in [PCase.NEGNUM, PCase.NEGSET]: # if negative number, handle using function sub, but keep parameter at two # -P - E = -E - P return F(one=-other, sub=True, two=self, two_type=Elem.P) # keep additive or subtractive parameter at two # P - E = -E + P return F(one=-other, add=True, two=self, two_type=Elem.P) def __rsub__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P | F: # let negation and __add__ handle the subtraction return -self + other def __mul__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: if other is None: # if multiplying by nothing, return nothing return None if self.case == PCase.ZERO: # if self is zero, return zero return 0 if isinstance(other, (int, float)): # if multiplying with a number if other in [1, 1.0]: # by unity, return itself return self if other in [0, 0.0]: # by zero, return 0 return 0.0 if self.case in [PCase.NEGNUM, PCase.NUM]: # if self is a number, just find the product # let P handle the rest return P(*self.index, _=self._[-1] * other, **self.args) # else multiply the number to each value p = P(*self.index, _=[i * other for i in self._], **self.args) p.name = f"{self}*{other}" return p if isinstance(other, list): # if list, just zip through the values and multiply if self.case in [PCase.NUM, PCase.NEGNUM]: p = P(*self.index, _=[self._[-1] * i for i in other], **self.args) else: if len(self) != len(other): warn( f"Index mismatch {self} * {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) p = P( *self.index, _=[i * j for i, j in zip(self._, other)], **self.args ) p.name = f"{self}*φ" return p if isinstance(other, tuple): # if tuple, make T # return a scaled T if self.case == PCase.ZERO: # return int zero return 0 if self.case in [PCase.NUM, PCase.NEGNUM]: # if self is a number, scale the tuple by the number # and make a T t = make_T( tuple([other[0] * self._[-1], other[1] * self._[-1]]), self.index ) t.name = f"{self}*θ" return t # otherwise self is a set # scale the tuple by each value in the set t = make_T( [tuple([i * other[0], i * other[1]]) for i in self._], self.index ) t.name = f"{self}*θ" return t if isinstance(other, P): if other.case == PCase.ZERO: # if self or other is zero, return int zero return 0 if self.case in [PCase.NUM, PCase.NEGNUM]: # if self is a number, just find the product # let P handle the rest if other.case in [PCase.NUM, PCase.NEGNUM]: if other._[0] in [0, 0.0]: # if other is zero, return int zero return 0 return P(*self.index, _=self._[-1] * other._[-1], **self.args) p = P(*self.index, _=[self._[-1] * i for i in other._], **self.args) p.name = f"{self}*{other}" return p if other.case in [PCase.NUM, PCase.NEGNUM]: # if other is a number, find the product of every value in self with the number p = P(*self.index, _=[i * other._[-1] for i in self._], **self.args) p.name = f"{self}*{other}" return p if len(self) != len(other): raise ValueError( f"Index mismatch {self} * {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if both are just general parameters # else zip through the values and multiply p = P(*self.index, _=[i * j for i, j in zip(self._, other._)], **self.args) p.name = rf"{self}*{other}" return p return other * self # TODO - handle multiplication with other types # call multiplication function # if isinstance(other, F): # # if other is a function, use it # # let F handle the multiplication # if other.add: # # if the function is an addition, return a function # if self.case in [PCase.NEGNUM, PCase.NEGSET]: # if other.two_type == Elem.P: # # if the other is a parameter, make it a subtraction # return F(one=self * other.one, sub=True, two=self * other.two) # # make it a subtraction # return F(one=self * other.one, sub=True, two=self * other.two) # return F(one=self * other.one, add=True, two=self * other.two) # return F(one=self, mul=True, two=other, one_type=Elem.P) def __rmul__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: # multiplication is commutative return self * other def __truediv__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: try: if other is None: # if dividing by nothing, return nothing raise ValueError( "Cannot divide by None. Please provide a valid parameter, variable, or number." ) if self.case == PCase.ZERO: # if self is zero, return zero # take every opportunity to return int 0 return 0 if isinstance(other, (int, float)): # if dividing by a number if other in [0, 0.0]: # if dividing by zero, raise an error raise ZeroDivisionError(f"{self} cannot be divided by zero.") if other in [1, 1.0]: return self if self.case in [PCase.NEGNUM, PCase.NUM]: # if self is a number, just find the division # let P handle the rest return P(*self.index, _=self._[-1] / other, **self.args) p = P(*self.index, _=[i / other for i in self._], **self.args) p.name = f"{self}/{other}" return p if isinstance(other, list): if isinstance(other[0], tuple): raise ValueError( "Cannot divide by a list of tuples. Please provide a valid parameter, variable, or number." ) if self.case in [PCase.NUM, PCase.NEGNUM]: # if self is a number, divide the number by each value in the list p = P(*self.index, _=[self._[-1] / i for i in other], **self.args) else: if len(self) != len(other): warn( f"Index mismatch {self} / {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if list, just zip through the values and divide p = P( *self.index, _=[i / j for i, j in zip(self._, other)], **self.args, ) # change the name to add that something from a list has been added p.name = f"{self}/φ" return p if isinstance(other, tuple): # dividing by parametric variable raise ValueError( f"Cannot divide {self} by a tuple. Please provide a valid parameter, variable, or number." ) if isinstance(other, P): if other.case == PCase.ZERO: raise ZeroDivisionError(f"{self} cannot be divided by zero.") if self.case in [PCase.NEGNUM, PCase.NUM]: if other.case in [PCase.NUM, PCase.NEGNUM]: # if both are numbers # let P handle the case return P(*self.index, _=self._[-1] / other._[-1], **self.args) p = P(*self.index, _=[self._[-1] / i for i in other._], **self.args) p.name = f"{self}/{other}" return p if other.case in [PCase.NUM, PCase.NEGNUM]: # if other is a number, divide every value in self by the number p = P(*self.index, _=[i / other._[-1] for i in self._], **self.args) p.name = f"{self}/{other}" return p if len(self) != len(other): raise ValueError( f"Index mismatch {self} / {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if both are just general parameters # zip through the values and divide p = P(*self.index, _=[i / j for i, j in zip(self._, other._)]) p.name = f"{self}/{other}" return p # P / E: not handled yet raise ValueError( f"Cannot divide {self} by {other}. Nonlinear operations are not supported for {type(other).__name__} objects." ) except ZeroDivisionError as e: # handle division by zero raise ZeroDivisionError(f"{self} cannot be divided by zero.") from e def __rtruediv__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: # just do this operation instead return (1 / self) * other def __floordiv__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: if other is None: # if dividing by nothing, return nothing raise ValueError( "Cannot divide by None. Please provide a valid parameter, variable, or number." ) if self.case == PCase.ZERO: # if self is zero, return zero return 0 if isinstance(other, (int, float)): # if dividing by a number if other in [0, 0.0]: # if dividing by zero, raise an error raise ZeroDivisionError(f"{self} cannot be divided by zero.") if other in [1, 1.0]: return self if self.case in [PCase.NEGNUM, PCase.NUM]: # if self is a number, just find the division # let P handle the rest return P(*self.index, _=self._[-1] // other, **self.args) p = P(*self.index, _=[i // other for i in self._], **self.args) p.name = f"{self}//{other}" return p if isinstance(other, list): if isinstance(other[0], tuple): raise ValueError( "Cannot divide by a list of tuples. Please provide a valid parameter, variable, or number." ) if self.case in [PCase.NUM, PCase.NEGNUM]: p = P(*self.index, _=[self._[-1] // i for i in other], **self.args) else: if len(self) != len(other): warn( f"Index mismatch {self} // {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if list, just zip through the values and divide p = P( *self.index, _=[i // j for i, j in zip(self._, other)], **self.args ) # change the name to add that something from a list has been added p.name = f"{self}//φ" return p if isinstance(other, tuple): # dividing by parametric variable raise ValueError( f"Cannot divide {self} by a tuple. Please provide a valid parameter, variable, or number." ) if isinstance(other, P): if other.case == PCase.ZERO: raise ZeroDivisionError(f"{self} cannot be divided by zero.") if self.case in [PCase.NEGNUM, PCase.NUM]: if other.case in [PCase.NUM, PCase.NEGNUM]: # if both are numbers # let P handle the case return P(*self.index, _=self._[-1] // other._[-1], **self.args) p = P(*self.index, _=[self._[-1] // i for i in other._], **self.args) p.name = f"{self}//{other}" return p if other.case in [PCase.NUM, PCase.NEGNUM]: # if other is a number, divide every value in self by the number p = P(*self.index, _=[i // other._[-1] for i in self._], **self.args) p.name = f"{self}//{other}" return p if len(self) != len(other): raise ValueError( f"Index mismatch {self} // {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if both are just general parameters # zip through the values and divide p = P(*self.index, _=[i // j for i, j in zip(self._, other._)], **self.args) p.name = f"{self}//{other}" return p # else let the other handle the division return self // other def __mod__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: if other is None: # if dividing by nothing, return nothing raise ValueError( "Cannot divide by None. Please provide a valid parameter, variable, or number." ) if self.case == PCase.ZERO: # if self is zero, return zero return 0 if isinstance(other, (int, float)): # if dividing by a number if other in [0, 0.0]: # if dividing by zero, raise an error raise ZeroDivisionError(f"{self} cannot be divided by zero.") if other in [1, 1.0]: # if dividing by 1, return self return self if self.case in [PCase.NEGNUM, PCase.NUM]: # if self is a number, just find the modulus # let P handle the rest p = P(*self.index, _=self._[-1] % other, **self.args) p.name = f"{self} % {other}" return p p = P(*self.index, _=[i % other for i in self._], **self.args) p.name = f"{self} % {other}" return p if isinstance(other, list): if isinstance(other[0], tuple): raise ValueError( "Cannot divide by a list of tuples. Please provide a valid parameter, variable, or number." ) if self.case in [PCase.NUM, PCase.NEGNUM]: # if self is a number, find the mod of self and each number in the list p = P(*self.index, _=[self._[-1] % i for i in other], **self.args) else: if len(self) != len(other): warn( f"Index mismatch {self} % {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if list and self is a set, just zip through p = P( *self.index, _=[i % j for i, j in zip(self._, other)], **self.args ) # change the name to add that something from a list has been added p.name = f"{self} % φ" return p if isinstance(other, tuple): # dividing by parametric variable raise ValueError( f"Cannot divide {self} by a tuple. Please provide a valid parameter, variable, or number." ) if isinstance(other, P): if self.case == PCase.ZERO: # if self is zero, return zero return 0 if other.case == PCase.ZERO: raise ZeroDivisionError(f"{self} cannot be divided by zero.") if self.case in [PCase.NEGNUM, PCase.NUM]: if other.case in [PCase.NUM, PCase.NEGNUM]: # if both are numbers # let P handle the case return P(*self.index, _=self._[-1] % other._[-1], **self.args) p = P(*self.index, _=[self._[-1] % i for i in other._], **self.args) p.name = f"{self} % {other}" return p if other.case in [PCase.NUM, PCase.NEGNUM]: # if other is a number, find the modulus of every value in self with the number p = P(*self.index, _=[i % other._[-1] for i in self._], **self.args) p.name = f"{self} % {other}" return p if len(self) != len(other): raise ValueError( f"Index mismatch {self} % {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if both are just general parameters # zip through the values and find the modulus p = P(*self.index, _=[i % j for i, j in zip(self._, other._)], **self.args) p.name = f"{self} % {other}" return p # else let the other handle the modulus return self % other def __pow__( self, other: ( V | Self | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> P: if other is None: # if raising to None, return self return self if self.case == PCase.ZERO: # if self is zero, return zero return 0 if isinstance(other, (int, float)): # if raising to power of a number if other in [0, 0.0]: # if raising to zero, return 1 return 1 if other in [1, 1.0]: # if raising to one, return self return self if self.case in [PCase.NEGNUM, PCase.NUM]: # if self is a number, just find the power # let P handle the rest return P(*self.index, _=self._[-1] ** other, **self.args) # else find the power of each element in set raised to other p = P(*self.index, _=[i**other for i in self._], **self.args) p.name = f"{self}^({other})" return p if isinstance(other, list): if isinstance(other[0], tuple): raise ValueError( "Cannot raise to a list of tuples. Please provide a valid parameter, variable, or number." ) if self.case in [PCase.NUM, PCase.NEGNUM]: p = P(*self.index, _=[self._[-1] ** i for i in other], **self.args) else: if len(self) != len(other): warn( f"Index mismatch {self} ^ {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if list, just zip through the values and raise to power p = P(*self.index, _=[i**j for i, j in zip(self._, other)], **self.args) # change the name to add that something from a list has been added p.name = f"{self}^φ" return p if isinstance(other, tuple): # setting to the power of a parametric variable raise ValueError( f"Cannot raise {self} to a tuple. Please provide a valid parameter, variable, or number." ) if isinstance(other, P): # other is a parameter set if other.case == PCase.ZERO: # if other is 0 return 1 if self.case in [PCase.NEGNUM, PCase.NUM]: # if self is a number, just find the power # let P handle the rest if other.case in [PCase.NUM, PCase.NEGNUM]: return P(*self.index, _=self._[-1] ** other._[-1], **self.args) p = P(*self.index, _=[self._[-1] ** i for i in other._], **self.args) p.name = f"{self}^{other}" return p if other.case in [PCase.NUM, PCase.NEGNUM]: # if other is a number, raise every value in self to the power of the number p = P(*self.index, _=[i ** other._[-1] for i in self._], **self.args) p.name = f"{self}^{other}" return p if len(self) != len(other): raise ValueError( f"Index mismatch {self} ^ {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) # if both are just general parameters # zip through the values and raise to power p = P(*self.index, _=[i**j for i, j in zip(self._, other._)], **self.args) p.name = f"{self}^{other}" return p # else let the other handle the power # not sure if this will ever be called tbh return self**other # ----------------------------------------------------- # Relational # ----------------------------------------------------- def __eq__( self, other: ( Self | P | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> bool: if isinstance(other, (int, float)): if other in [0, 0.0]: if self.case == PCase.ZERO: # if self is zero, return True return True if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value return self._[-1] == other # if not numeric, False raise NotImplementedError( f"{self} == {other}: cannot compare a parameter set with a number" ) if isinstance(other, list): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: raise NotImplementedError( f"{self} == {other}: cannot compare a number with a list." ) # if self is a set, compare it with the list if len(self) != len(other): warn( f"Index mismatch {self} == {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i == j for i, j in zip(self._, other)]) if isinstance(other, P): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: if other.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value of other return self._[-1] == other._[-1] raise NotImplementedError( f"{self} == {other}: cannot compare a number with a parameter set." ) if len(self) != len(other): warn( f"Index mismatch {self} == {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i == j for i, j in zip(self._, other._)]) # else let other handle this return self == other def __le__( self, other: ( Self | P | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> bool: if isinstance(other, (int, float)): if other in [0, 0.0]: if self.case in [PCase.ZERO, PCase.NEGNUM, PCase.NEGSET]: # if self is zero or negative number, return True return True if self.case in [PCase.NUM, PCase.SET]: # if self is a positive number or set, return False return False if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value return self._[-1] <= other # if not numeric, False raise NotImplementedError( f"{self} <= {other}: cannot compare a parameter set with a number" ) if isinstance(other, list): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: raise NotImplementedError( f"{self} <= {other}: cannot compare a number with a list." ) # if self is a set, compare it with the list if len(self) != len(other): warn( f"Index mismatch {self} <= {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i <= j for i, j in zip(self._, other)]) if isinstance(other, P): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: if other.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value of other return self._[-1] <= other._[-1] raise NotImplementedError( f"{self} <= {other}: cannot compare a number with a parameter set." ) if len(self) != len(other): warn( f"Index mismatch {self} <= {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i <= j for i, j in zip(self._, other._)]) # else let other handle this return self <= other def __ge__( self, other: ( Self | P | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> bool: if isinstance(other, (int, float)): if other in [0, 0.0]: if self.case in [PCase.NEGNUM, PCase.NEGSET]: # if self negative number or negated set, return False return False if self.case in [PCase.ZERO, PCase.NUM, PCase.SET]: # if self is a positive number or set, return True return True if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value return self._[-1] >= other # if not numeric, False raise NotImplementedError( f"{self} >= {other}: cannot compare a parameter set with a number" ) if isinstance(other, list): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: raise NotImplementedError( f"{self} >= {other}: cannot compare a number with a list." ) # if self is a set, compare it with the list if len(self) != len(other): warn( f"Index mismatch {self} >= {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i >= j for i, j in zip(self._, other)]) if isinstance(other, P): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: if other.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value of other return self._[-1] >= other._[-1] raise NotImplementedError( f"{self} >= {other}: cannot compare a number with a parameter set." ) if len(self) != len(other): warn( f"Index mismatch {self} >= {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i >= j for i, j in zip(self._, other._)]) # else let other handle this return self >= other def __lt__( self, other: ( Self | P | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> bool: if isinstance(other, (int, float)): if other in [0, 0.0]: if self.case in [PCase.NEGNUM, PCase.NEGSET]: # if self negative number or negated set, return True return True if self.case in [PCase.ZERO, PCase.NUM, PCase.SET]: # if self is a positive number or set, return True return False if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value return self._[-1] < other # if not numeric, False raise NotImplementedError( f"{self} < {other}: cannot compare a parameter set with a number" ) if isinstance(other, list): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: raise NotImplementedError( f"{self} < {other}: cannot compare a number with a list." ) # if self is a set, compare it with the list if len(self) != len(other): warn( f"Index mismatch {self} < {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i < j for i, j in zip(self._, other)]) if isinstance(other, P): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: if other.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value of other return self._[-1] < other._[-1] raise NotImplementedError( f"{self} < {other}: cannot compare a number with a parameter set." ) if len(self) != len(other): warn( f"Index mismatch {self} < {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i < j for i, j in zip(self._, other._)]) # else let other handle this return self < other def __ne__( self, other: ( Self | P | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> bool: if isinstance(other, (int, float)): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value return self._[-1] != other # if not numeric, False raise NotImplementedError( f"{self} != {other}: cannot compare a parameter set with a number" ) if isinstance(other, list): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: raise NotImplementedError( f"{self} != {other}: cannot compare a number with a list." ) # if self is a set, compare it with the list if len(self) != len(other): warn( f"Index mismatch {self} != {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i != j for i, j in zip(self._, other)]) if isinstance(other, P): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: if other.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value of other return self._[-1] != other._[-1] raise NotImplementedError( f"{self} != {other}: cannot compare a number with a parameter set." ) if len(self) != len(other): warn( f"Index mismatch {self} != {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i != j for i, j in zip(self._, other._)]) # else let other handle this return self != other def __gt__( self, other: ( Self | P | T | F | int | float | tuple[int | float] | list[int | float | tuple[int | float]] | None ), ) -> bool: if isinstance(other, (int, float)): if other in [0, 0.0]: if self.case in [PCase.ZERO, PCase.NEGNUM, PCase.NEGSET]: # if self negative number or negated set, return True return False if self.case in [PCase.NUM, PCase.SET]: # if self is a positive number or set, return True return True if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value return self._[-1] > other # if not numeric, False raise NotImplementedError( f"{self} > {other}: cannot compare a parameter set with a number" ) if isinstance(other, list): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: raise NotImplementedError( f"{self} > {other}: cannot compare a number with a list." ) # if self is a set, compare it with the list if len(self) != len(other): warn( f"Index mismatch {self} > {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i > j for i, j in zip(self._, other)]) if isinstance(other, P): if self.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: if other.case in [PCase.NUM, PCase.NEGNUM, PCase.ZERO]: # if self is a number, compare it with the last value of other return self._[-1] > other._[-1] raise NotImplementedError( f"{self} > {other}: cannot compare a number with a parameter set." ) if len(self) != len(other): warn( f"Index mismatch {self} > {other}: len(self) ({len(self)}) != len(other) ({len(other)})" ) return all([i > j for i, j in zip(self._, other._)]) # else let other handle this return self > other def __call__(self, *key: I) -> Self: def lister(inp: tuple[I]) -> tuple[I | list[V]]: return tuple([i] if isinstance(i, V) else i for i in inp) # if a dependent variable is being passed in the key # extract variable from the index (it will be in a list) def delister(inp: tuple[I | list[V]]): return tuple(i[0] if isinstance(i, list) else i for i in inp) if not key or delister(key) == delister(self.index): # if the index is an exact match # or no key is passed return self # the check helps to handle if a variable itself is an index # we do not want to iterate over the entire variable set # but treat the variable as a single index element key: tuple[I] | set[tuple[I]] = lister(key) # if a subset is passed, # first create a product to match # the indices # create a new variable set to return p = P(**self.args) p.name, p.n = self.name, self.n p.index = key # should be able to map these for index in product(*key): # this helps weed out any None indices # i.e. skips if any(i is None for i in index): index = None if index is None: parameter = None else: parameter = self.map[index] p.map[index] = parameter p._.append(parameter) return p # ----------------------------------------------------- # Hashing # ----------------------------------------------------- def __hash__(self): return hash(str(self.name)) # ----------------------------------------------------- # Export # ----------------------------------------------------- # def sympy(self): # """symbolic representation""" # if has_sympy: # return IndexedBase(str(self))[ # symbols(",".join([f"{d}" for d in self.index]), cls=Idx) # ] # logger.warning( # "⚠ sympy is an optional dependency, pip install gana[all] to get optional dependencies ⚠" # ) # def pyomo(self): # """Pyomo representation""" # # idx = [i.pyomo() for i in self.index] # # return PyoParam(*idx, initialize=self._, doc=str(self)) # if has_pyomo: # return PyoParam( # initialize=self._, # doc=str(self), # ) # logger.warning( # "pyomo is an optional dependency, pip install gana[all] to get optional dependencies" # ) # ----------------------------------------------------- # Plotting # -----------------------------------------------------
[docs] def line( self, font_size: float = 16, fig_size: tuple[float, float] = (12, 6), linewidth: float = 0.7, color: str = "blue", grid_alpha: float = 0.3, usetex: bool = True, str_idx_lim: int = 10, ): """ Plot the parameter set :param font_size: Font size for the plot. Defaults to 16. :type font_size: float, optional :param fig_size: Size of the figure. Defaults to (12, 6). :type fig_size: tuple[float, float], optional :param linewidth: Width of the line in the plot. Defaults to 0.7. :type linewidth: float, optional :param color: Color of the line in the plot. Defaults to 'blue'. :type color: str, optional :param grid_alpha: Transparency of the grid lines. Defaults to 0.3. :type grid_alpha: float, optional :param usetex: Use LaTeX for text rendering. Defaults to True. :type usetex: bool, optional :param str_idx_lim: Limit for string indices display. Defaults to 10. :type str_idx_lim: int, optional """ draw( element=self, data=self._, kind="line", font_size=font_size, fig_size=fig_size, linewidth=linewidth, color=color, grid_alpha=grid_alpha, usetex=usetex, str_idx_lim=str_idx_lim, )
[docs] def bar( self, font_size: float = 16, fig_size: tuple[float, float] = (12, 6), linewidth: float = 0.7, color: str = "blue", grid_alpha: float = 0.3, usetex: bool = True, str_idx_lim: int = 10, ): """ Plot the parameter set :param font_size: Font size for the plot. Defaults to 16. :type font_size: float, optional :param fig_size: Size of the figure. Defaults to (12, 6). :type fig_size: tuple[float, float], optional :param linewidth: Width of the line in the plot. Defaults to 0.7. :type linewidth: float, optional :param color: Color of the line in the plot. Defaults to 'blue'. :type color: str, optional :param grid_alpha: Transparency of the grid lines. Defaults to 0.3. :type grid_alpha: float, optional :param usetex: Use LaTeX for text rendering. Defaults to True. :type usetex: bool, optional :param str_idx_lim: Limit for string indices display. Defaults to 10. :type str_idx_lim: int, optional """ draw( element=self, data=self._, kind="bar", font_size=font_size, fig_size=fig_size, linewidth=linewidth, color=color, grid_alpha=grid_alpha, usetex=usetex, str_idx_lim=str_idx_lim, )