"""Conversion"""
from __future__ import annotations
from collections.abc import Mapping
from functools import cached_property
from typing import TYPE_CHECKING, Self
from ..._core._hash import _Hash
from ...components.temporal.lag import Lag
from ...components.temporal.modes import Modes
if TYPE_CHECKING:
from gana import Prg
from ...components.commodities.commodity import Commodity
from ...components.operations.operation import Operation
from ...components.operations.storage import Storage
from ...components.spatial.linkage import Linkage
from ...components.spatial.location import Location
from ...components.temporal.periods import Periods
from ...represent.model import Model
from ..indices.sample import Sample
[docs]
class Conversion(Mapping, _Hash):
"""
Processes convert one Commodity to another Commodity
Conversion provides the conversion of resources
:param resource: Commodity that is balanced
:type resource: Commodity
:param operation: Process or Storage that serves as primary spatial index
:type operation: Process | Storage
:param bind: Sample that is used to define the conversion
:type bind: Sample
:ivar name: Name of the component, generated based on the operation.
:vartype name: str
:ivar base: Basis Commodity, usually has a value 1 in the conversion matrix, set using __call__. Defaults to None.
:vartype base: Commodity
:ivar conversion: {Commoditys: conversion}. Defaults to {}, set using __eq__.
:vartype conversion: dict[Commodity : int | float]
:ivar lag: Temporal lag (processing time) for conversion, set using __getitem__.
:vartype lag: Lag
:ivar periods: Periods over which the conversion is defined. Defaults to None.
:vartype periods: Periods
:raises ValueError: If conversion lists are of inconsistent lengths.
.. note::
- name and operation are generated post init
- base (__call__), conversion (__eq__), lag (__getitem__) are defined as the program is built
- Storage contains two processes (charge and discharge), hence is provided separately
"""
def __init__(
self,
aspect: str = "",
add: str = "",
sub: str = "",
operation: Operation | Storage | None = None,
resource: Commodity | None = None,
balance: dict[Commodity, float | list[float]] | None = None,
hold: int | float | None = None,
attr_name: str = "",
symbol: str = "η",
use_max_time: bool = False,
):
self.resource = resource
self.operation = operation
self.symbol = symbol
# * Aspect that elicits the conversion
self.aspect = aspect
# * Aspects corresponding to positive and negative conversion
self.add = add
self.sub = sub
if balance:
self.balance = balance
else:
self.balance = {}
# value to hold, will be applied later
# occurs when Conversion/Commodity == parameter is used
# the parameter is held until a dummy resource is created
self.hold = hold
# used if a resource is expected to be inventoried
self.expect: Commodity | None = None
self.lag: Lag | None = None
# this is carried forth incase, piece wise linear conversion is used
self.attr_name = attr_name
self.use_max_time = use_max_time
@property
def args(self) -> dict[str, str | Operation | Commodity | None]:
"""Arguments of the conversion"""
return {
'by': self.aspect,
'add': self.add,
'sub': self.sub,
'operation': self.operation,
'basis': self.resource,
}
[docs]
@classmethod
def from_balance(
cls,
balance: dict[Commodity, float | list[float]],
by: str = "",
add: str = "",
sub: str = "",
operation: Operation | None = None,
basis: Commodity | None = None,
) -> Self:
"""Creates Conversion from balance dict"""
conv = cls()
# set first resource as the basis
conv.balance = balance
conv.operation = operation
conv.aspect = by
conv.add = add
conv.sub = sub
conv.resource = basis
return conv
@property
def name(self) -> str:
"""Name"""
if self.resource:
return f"{self.symbol}({self.operation}, {self.resource})"
return f"{self.symbol}({self.operation})"
[docs]
@cached_property
def model(self) -> Model | None:
"""energia Model"""
return next((i.model for i in self.balance), None)
[docs]
@cached_property
def program(self) -> Prg | None:
"""gana Program"""
return self.operation.program
[docs]
def balancer(self):
"""
Checks if there is a list in the conversion
If yes, tries to make everything consistent
"""
def _balancer(conversion: dict):
# check if lists are provided
check_list = dict.fromkeys(conversion.keys(), False)
# check lengths of the list, for parameter the length is 1
check_len = dict.fromkeys(conversion.keys(), 1)
for res, par in conversion.items():
if isinstance(par, list):
check_list[res] = True
check_len[res] = len(par)
# check if all the list lens are the same
lengths = {i for i in check_len.values() if i > 1}
if len(lengths) > 1:
# if there are different lengths, raise an error
raise ValueError(
f"Conversion: {self.name} has inconsistent list lengths: {lengths}",
)
if any(check_list.values()):
length = next(iter(lengths))
# if any of the values are a list
#
for res, par in conversion.items():
if isinstance(par, (float, int)):
conversion[res] = [par] * length
return conversion
self.balance = _balancer(self.balance)
[docs]
def time_checker(
self, commodity: Commodity, space: Location | Linkage, time: Periods
):
"""This checks if it is actually necessary
to write conversion at denser temporal scales
"""
# This checks whether some other aspect is defined at
# a lower temporal scale
if space not in self.model.balances[commodity]:
# if not defined for that location, check for a lower order location
# i.e. location at a lower hierarchy,
# e.g. say if space being passed is a city, and a grb has not been defined for it
# then we need to check at a higher order
parent = self.model.space.split(space)[
1
] # get location at one hierarchy above
if parent:
# if that indeed exists, then make the parent the space
# the conversion Balance variables will feature in grb for parent location
space = parent
_ = self.model.balances[commodity][space][time]
if commodity.inv_of:
# for inventoried resources, the conversion is written
# using the time of the base resource's grb
commodity = commodity.inv_of
try:
times = list(
[
t
for t in self.model.balances[commodity][space]
if self.model.balances[commodity][space][t]
],
)
except KeyError:
times = []
# write the conversion balance at
# densest temporal scale in that space
if times:
if self.use_max_time:
return max(times)
return min(times)
return time.horizon
[docs]
def write(
self, space: Location | Linkage, time: Periods | Lag, modes: Modes | None = None
):
"""Writes equations for conversion balance"""
for res, par in self.items():
if res in self.model.balances:
time = self.time_checker(res, space, time)
_ = self.model.balances[res].get(space, {})
eff = par if isinstance(par, list) else [par]
decision = getattr(self.operation, self.aspect)
if eff[0] < 0:
# Resources are consumed (expendend by Process) immediately
dependent = getattr(res, self.sub)
eff = [-e for e in eff]
else:
# Production — may occur after lag
time = self.lag.of if self.lag else time
dependent = getattr(res, self.add)
if modes:
rhs = dependent(decision, space, modes, time)
lhs = decision(space, modes, time)
else:
rhs = dependent(decision, space, time)
lhs = decision(space, time)
_ = lhs[rhs] == eff
[docs]
def items(self):
"""Items of the conversion balance"""
return self.balance.items()
[docs]
def keys(self):
"""Keys of the conversion balance"""
return self.balance.keys()
[docs]
def values(self):
"""Values of the conversion balance"""
return self.balance.values()
def __getitem__(self, key: Commodity) -> float | list[float]:
"""Used to define mode based conversions"""
return self.balance[key]
def __setitem__(self, key: Commodity, value: float | list[float]):
self.balance[key] = value
def __call__(self, basis: Commodity | Conversion, lag: Lag | None = None) -> Self:
# sets the basis
if isinstance(basis, Conversion):
# if a Conversion is provided (parameter*Commodity)
# In this case the associated conversion is not 1
# especially useful if Process is scaled to consumption of a commodity
# i.e. basis = -1*Commodity
self.balance = {**self, **basis}
self.resource = next(iter(self))
else:
# if a Commodity is provided
# implies that the conversion is 1
# i.e the Process is scaled to one unit of this Commodity produced
self.balance = {basis: 1.0, **self}
if lag:
self.lag = lag
return self
def __eq__(
self,
other: Conversion | list[Conversion] | int | float | dict[Modes, Conversion],
):
if isinstance(other, dict):
collect_parents = []
for mode, conv in other.items():
conv.operation = self.operation
collect_parents.append(mode.parent)
other[mode] = Conversion.from_balance({**self, **conv}, **self.args)
if len(set(collect_parents)) > 1:
raise ValueError(
f"{self}: PWL Conversion modes must belong to the same parent Modes",
)
if len(collect_parents[0]) != len(other):
raise ValueError(
f"{self}: PWL Conversion modes must account for all modes in {collect_parents[0]}",
)
setattr(
self.operation,
self.attr_name,
PWLConversion.from_balance(
balance=other, sample=getattr(self.operation, self.aspect)
),
)
return getattr(self.operation, self.attr_name)
if isinstance(other, list):
if self.resource:
for i, o in enumerate(other):
other[i] = Conversion.from_balance(
{**self, **o},
)
setattr(
self.operation,
self.attr_name,
PWLConversion(
conversions=other,
sample=(
getattr(self.operation.stored, self.aspect)
if hasattr(self.operation, "stored")
else getattr(self.operation, self.aspect)
),
aspect=self.aspect,
add=self.add,
sub=self.sub,
),
)
return getattr(self.operation, self.attr_name)
if isinstance(other, (int, float)):
# this is used for inventory conversion
# when not other resource besides the one being inventoried is involved
self.balance = {**self, self.expect: 1 / -float(other)}
else:
self.balance = {**self, **other}
self.model.convmatrix[self.operation] = self.balance
return self
def __neg__(self) -> Self:
self.balance = {res: -par for res, par in self.balance.items()}
return self
def __add__(self, other: Conversion) -> Self:
self.balance = {**self, **other}
return self
def __sub__(self, other: Conversion) -> Self:
self.balance = {**self, **-other}
return self
def __mul__(self, times: int | float | list) -> Self:
if isinstance(times, list):
self.balance = {res: [par * i for i in times] for res, par in self.items()}
else:
self.balance = {res: par * times for res, par in self.items()}
return self
def __rmul__(self, times) -> Self:
return self * times
def __len__(self):
"""Length of the conversion balance"""
return len(self.balance)
def __iter__(self):
return iter(self.balance)
[docs]
class PWLConversion(Mapping, _Hash):
"""Piece Wise Linear Conversion"""
def __init__(
self,
conversions: list[Conversion],
sample: Sample,
aspect: str = "",
add: str = "",
sub: str = "",
):
self.sample = sample
self.operation = self.sample.domain.operation or self.sample.domain.primary
self.model = self.sample.model
if conversions:
self.modes = self.model.Modes(size=len(conversions), sample=sample)
self.balance: dict[Modes, Conversion] = dict(zip(self.modes, conversions))
for conv in conversions:
conv.operation = self.operation
conv.aspect = aspect or conv.aspect
conv.add = add or conv.add
conv.sub = sub or conv.sub
self._aspect = aspect
self._add = add
self._sub = sub
[docs]
@classmethod
def from_balance(
cls,
balance: dict[Modes, Conversion],
sample: Sample,
) -> Self:
"""Creates PWLConversion from balance dict"""
conv = cls([], sample)
conv.operation = sample.domain.operation
conv.balance = balance
conv.modes = (next(iter(balance))).parent
return conv
@property
def aspect(self) -> str:
if self._aspect:
return self._aspect
return self[0].aspect
@property
def add(self) -> str:
if self._add:
return self._add
return self[0].add
@property
def sub(self) -> str:
if self._sub:
return self._sub
return self[0].sub
@property
def lag(self) -> str:
return self[0].lag
@property
def name(self) -> str:
"""Name"""
return f"η_PWL({self.operation}, {self.modes})"
[docs]
def balancer(self):
"""Balances all conversions"""
for conv in self.balance.values():
conv.balancer()
[docs]
def items(self):
"""Items of the conversion balance"""
return self.balance.items()
[docs]
def keys(self):
"""Keys of the conversion balance"""
return self.balance.keys()
[docs]
def values(self):
"""Values of the conversion balance"""
return self.balance.values()
[docs]
def box(self):
"""Consolidates the conversion dict into {resource: par} format"""
resources = list(set().union(*self.values()))
_box = {r: [] for r in resources}
for conv in self.values():
for resource in resources:
if resource in conv:
_box[resource].append(conv[resource])
else:
_box[resource].append(None)
return _box
[docs]
def write(self, space: Location | Linkage, time: Periods | Lag):
"""Writes equations for conversion balance"""
for mode, conv in self.items():
conv.write(space, time, mode)
def __len__(self):
"""Length of the conversion balance"""
return len(self.balance)
def __iter__(self):
return iter(self.balance)
def __setitem__(self, key: int | str | Modes, value: Conversion):
if isinstance(key, int):
key = self.modes[key]
self.balance[key] = value
def __getitem__(self, key: int | str) -> Conversion:
"""Used to define mode based conversions"""
if isinstance(key, int):
key = self.modes[key]
return self.balance[key]