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