Source code for energia.modeling.indices.domain

"""Domain"""

from __future__ import annotations

from dataclasses import dataclass, field
from functools import cached_property
from operator import is_, is_not
from typing import TYPE_CHECKING, Self

from ..._core._hash import _Hash

if TYPE_CHECKING:
    from gana import I as Idx
    from gana import V

    from ..._core._x import _X
    from ...components.commodities.commodity import Commodity
    from ...components.game.couple import Interact
    from ...components.game.player import Player
    from ...components.impact.indicator import Indicator
    from ...components.operations.process import Process
    from ...components.operations.storage import Storage
    from ...components.operations.transport import Transport
    from ...components.spatial.linkage import Linkage
    from ...components.spatial.location import Location
    from ...components.temporal.lag import Lag
    from ...components.temporal.modes import Modes
    from ...components.temporal.periods import Periods
    from ...represent.model import Model
    from ..variables.aspect import Aspect
    from .sample import Sample


[docs] @dataclass class Domain(_Hash): """ Point represented by a tuple of indices :param indicator: Indicates the impact of some activity through an equivalency, e.g. GWP, ODP. :type indicator: Indicator | None :param commodity: Represents the flow of any stream, measured using some basis, e.g. water, Rupee, carbon-dioxide. :type commodity: Commodity | None :param process: Process that is being considered, e.g. dam, farming. :type process: Process | None :param storage: Storage that is being considered, e.g. reservoir. :type storage: Storage | None :param transport: Transport that is being considered, e.g. pipeline, road. :type transport: Transport | None :param player: Actor that takes decisions, e.g. me, you. :type player: Player | None :param couple: Other actor that might be paired with the player. :type couple: Couple | None :param location: Spatial aspect of the domain, e.g. Goa, Texas. :type location: Location | None :param linkage: Linkage aspect of the domain, e.g. pipeline, road. :type linkage: Linkage | None :param periods: Temporal aspect of the domain, e.g. year, month. :type periods: Periods | None :param lag: Indicates whether the temporal element is lagged or not. :type lag: Lag | None :param modes: Modes applicable to the domain. :type modes: Modes | None :param samples: List of samples that can be summed over. :type samples: list[Bind] | None :ivar model: Model to which the Domain belongs. :vartype model: Model """ # the reason I keep these individual # instead of directly adding primary or stream # is because we do an instance check in Aspect # this helps relay the checks # primary component (one of these is needed) indicator: Indicator | None = None commodity: Commodity | None = None process: Process | None = None storage: Storage | None = None transport: Transport | None = None # decision - maker and other decision-maker player: Player | None = None couple: Interact | None = None # compulsory space and time elements location: Location | None = None linkage: Linkage | None = None periods: Periods | None = None lag: Lag | None = None modes: Modes | None = None # These can be summed over samples: list[Sample] = field(default_factory=list) def __post_init__(self): # Domains are structured something like this: # (primary_component ...aspect_n, secondary_component_n....,decision-makers, space, time # primary_component can be an indicator, commodity, or operation (process | storage | transport) # {aspect_n: secondary_component_n} is given in self.samples # decision-makers = player | couple # space = location | linkage # time = period | lag # primary index being modeled in some spatiotemporal context self.model: Model | None = next((i.model for i in self.index_short if i), None) # ----------------------------------------------------- # Components # ----------------------------------------------------- # These are kept as properties # because domains are updated on the fly # look at change() and call() @property def stream(self) -> Indicator | Commodity | None: """Stream""" return self.indicator or self.commodity @property def operation(self) -> Process | Storage | Transport | None: """Operation""" return self.process or self.storage or self.transport @property def primary( self, ) -> Indicator | Commodity | Process | Storage | Transport | list[Sample]: """Primary component""" _primary = self.stream or self.operation or self.samples if not _primary: raise ValueError("Domain must have at least one primary index") return _primary @property def sample(self) -> Sample | None: """Sample""" if self.samples: return self.samples[0] return None @property def space(self) -> Location | Linkage | None: """Space""" return self.linkage or self.location @property def maker(self) -> Player | Interact | None: """Decision-maker""" return self.couple or self.player @property def time(self) -> Periods | Lag: """Time""" if self.periods is not None: return self.periods if self.lag is not None: return self.lag return self.model.horizon @property def linked(self) -> bool: """Linked""" return True if self.linkage else False @property def lagged(self) -> bool: """Lagged""" return True if self.lag else False # ----------------------------------------------------- # Disposition # ----------------------------------------------------- @property def isroot(self) -> bool: """ This implies that the domain is of the form <object, space, time> """ if not self.lag and self.size == 3: return True return False @property def isrootroot(self) -> bool: """ This implies that the domain is of the form <object, network, horizon> Thus, an element attached to this domain has the lowest possible dimensionality """ if self.isroot: if is_(self.time, self.time.horizon): if is_(self.space, self.space.network): return True return False @property def disposition(self) -> tuple[str, ...]: """Disposition""" return tuple(self._.keys()) # ----------------------------------------------------- # Naming # ----------------------------------------------------- @property def name(self): """Name""" return f"{tuple(self.index)}" @property def idxname(self): """Name of the index""" return "_" + "_".join(f"{i}" for i in self.index) # ----------------------------------------------------- # Dictionaries # ----------------------------------------------------- @property def index(self) -> list[Aspect | _X]: """list of _Index elements""" return self.index_primary + self.index_binds + self.index_modes @property def index_primary( self, ) -> list[Indicator | Commodity | Process | Storage | Transport]: """Primary index :returns: list of primary indices :rtype: list[X] """ return [self.primary] + [ i for i in [self.space, self.periods, self.lag] if i is not None ] @property def index_spatiotemporal(self) -> list[Aspect | _X]: """List of indices with modes :returns: list of indices with modes :rtype: list[X] """ return self.index_primary[1:] + self.index_modes @property def index_binds(self) -> list[Aspect | _X]: """List of bind indices :returns: list of bind indices :rtype: list[X] """ return [x for b in self.samples for x in (b.aspect, b.domain.primary)] @property def index_modes(self) -> list[Modes]: """Set of mode indices""" return [self.modes] if self.modes else [] @property def index_short( self, ) -> list[Indicator | Commodity | Process | Storage | Transport | Sample | Modes]: """Set of indices""" return self.index_primary + self.samples + self.index_modes @property def tree(self) -> dict: """Convert index into tree""" tree = {} node = tree for key in self.index: node[key] = {} node = node[key] return tree @property def aspects(self) -> list[Aspect]: """Aspects""" return [b.aspect for b in self.samples] @property def args( self, ) -> dict[ str, Indicator | Commodity | Player | Process | Storage | Transport | Location | Linkage | Periods | Lag | Modes | list[Sample] | None, ]: """Dictionary of indices""" return { "indicator": self.indicator, "commodity": self.commodity, "player": self.player, "process": self.process, "storage": self.storage, "transport": self.transport, "location": self.location, "linkage": self.linkage, "periods": self.periods, "lag": self.lag, "modes": self.modes, "samples": self.samples, } @property def dictionary( self, ) -> dict[ str, Indicator | Commodity | Process | Storage | Transport | Player | Location | Linkage | Periods | Lag | Modes | list[Sample] | None, ]: """Dictionary of Components""" return { "primary": self.primary, "player": self.player, "space": self.space, "time": self.time, "modes": self.modes, "samples": self.samples, } # ----------------------------------------------------- # Iterables # ----------------------------------------------------- @property def _(self): """Dictionary of indices that are not None""" return {i: j for i, j in self.dictionary.items() if j is not None} @property def tup(self): """Tuple of objects""" return tuple(self._.values()) @property def lst(self): """Tuple of objects""" return list(self._.values())
[docs] @cached_property def I(self) -> list[Idx | list[V]]: """List of I""" _I = [] for idx in self.index: if isinstance(idx, list): # this is how variables are handled in gana _I.append(idx) elif isinstance(idx.I, tuple): _I.extend(idx.I) else: _I.append(idx.I) return tuple(_I)
@property def size(self) -> int: """Size of the domain""" return len(self.index) # ----------------------------------------------------- # Helpers # -----------------------------------------------------
[docs] def inform_components_of_cons(self, cons_name: str): """Update the constraints declared at every index""" for idx in self.index: idx.constraints.add(cons_name)
[docs] def inform_components_of_domain(self, aspect: Aspect): """ Update all components in the domains with the aspects that they have been modeled in :param aspect: Aspect being modeled :type aspect: Aspect """ for i, j in self._.items(): if i == "samples" or (self.lag and i == "time"): # lags disappear anyway, so dont bother continue # these are dependent variables, so do not update them if self not in j.domains: # check and update the domains at each index j.domains.append(self) # update the domain for the aspect for components try: j.aspects[aspect].add(self) except KeyError: j.aspects[aspect] = {self}
[docs] def copy(self) -> Self: """Make a copy of self""" return Domain(**self.args)
[docs] def edit(self, what: dict[str, _X]) -> Self: """Change some aspects and return a new Domain""" return Domain(**{**self.args, **what})
[docs] def param_tree(self, parameter: float | list[float], rel: str) -> dict: """Tree representation of the Domain""" tree = {} node = tree n_last = len(self.index) - 1 for n, key in enumerate(self.index): if n == n_last: if not node: node[key] = {} node[key][rel] = parameter else: node[key] = {} node = node[key] return tree
# ----------------------------------------------------- # Vector # ----------------------------------------------------- def __getitem__(self, index: str) -> _X: """Get the index by name""" return self._[index] def __iter__(self): """Iterate over the indices""" return iter(self.index_short) def __call__(self, *args: str) -> Self: return Domain(**{i: j for i, j in self.args.items() if i in args}) def __len__(self): return len(self.disposition) # ----------------------------------------------------- # Operators # ----------------------------------------------------- def __truediv__(self, other: list[str]) -> Self: """ Will give you the Domain minus a particular index :param other: index you wish to remove :type other: str | list[str] :returns: lower dimensional domain :rtype: Domain .. example:: >>> domain = Domain(commodity=water, operation=dam, space=goa, time=year) (water, dam, goa, year) >>> domain/'operation' (water, goa, year) >>> domain/['commodity', 'operation'] (goa, year) """ return Domain(**{i: j for i, j in self.args.items() if i not in other}) def __sub__(self, other: Self) -> list[str]: """ Will give you a list of indices that are not common between two domains :param other: another Domain object :type other: Self .. example:: >>> domain1 = Domain(commodity=water, operation=dam, space=goa, time=year) >>> domain2 = Domain(commodity=water, space=mumbai, time=year) >>> domain1 - domain2 ['operation', 'space'] """ notcommon = [] for i, j in self._.items(): # append if disposition is same but value is not if i in other._: if is_not(other._[i], j): notcommon.append(i) else: # append if disposition is not in other notcommon.append(i) # The first loop will not check for the keys that are not in self._ for k in other._.keys(): if k not in self._: notcommon.append(k) return notcommon def __add__(self, other: Self) -> Self: return Domain(**{**self.args, **other.args}) # ----------------------------------------------------- # Relational # ----------------------------------------------------- def __eq__(self, other): return self.name == str(other) def __lt__(self, other: Self) -> bool: """Less than comparison based on the number of indices""" if len(self) < len(other): return True return False def __gt__(self, other: Self) -> bool: """Greater than comparison based on the number of indices""" return other < self