Source code for energia.modeling.constraints.balance

"""Balance"""

from __future__ import annotations

import logging
from functools import cached_property
from operator import is_
from typing import TYPE_CHECKING, Self

from ..._core._hash import _Hash
from ...components.operations.storage import Stored
from ...utils.decorators import timer

logger = logging.getLogger("energia")

if TYPE_CHECKING:
    from gana import V
    from gana.sets.constraint import C
    from gana.sets.function import F

    from ..._core._x import _X
    from ...components.spatial.linkage import Linkage
    from ...components.spatial.location import Location
    from ..indices.domain import Domain
    from ..variables.aspect import Aspect


[docs] class Balance(_Hash): """Performs a general commodity balance :param aspect: Aspect to which the constraint is applied :type aspect: Aspect :param domain: Domain over which the aspect is defined :type domain: Domain """ def __init__(self, aspect: Aspect, domain: Domain): self.aspect = aspect self.domain = domain self._handshake() self.write()
[docs] def write(self) -> tuple[Domain, Aspect] | Domain | bool | None: """Writes the stream balance constraint""" if self._check_existing(): return False if not self.stored: self._init_sample() if self.existing_aspects: # if exists, update return self._update_constraint() # else, create new return self._birth_constraint()
[docs] @cached_property def stored(self): """If the commodity is a Stored commodity""" return isinstance(self.commodity, Stored)
[docs] @cached_property def space(self) -> Location | Linkage | None: """Location or Linkage of the constraint""" if ( self._space.isin and self.balances[self.commodity][self._space.isin][self.time] ): # if a balance is written for a parent location, write for parent return self._space.isin return self._space
@cached_property def _space(self): """Location where balance is applied""" if self.domain.linkage: # if outgoing, apply at sink return ( self.domain.linkage.sink if self.aspect.sign == 1 else self.domain.linkage.source ) return self.domain.location
[docs] @cached_property def name(self) -> str: """Name of the constraint""" return f"{self.aspect.name}{self.domain}"
[docs] @cached_property def cons_name(self) -> str: """Name of the constraint""" return f"{self.commodity}_{self.space}_{self.time}_grb"
@property def existing_aspects(self): """Exisiting Aspects""" return self.balances[self.commodity][self.space][self.time]
[docs] @cached_property def updated_part(self) -> V | F | int: """Returns the part of the constraint that is new""" if self.stored and self.aspect == "inventory": # if inventory is being add to GRB if len(self.time) == 1: # cannot lag a single time period return 0 return ( self(*self.domain).V() - self(*self.domain.edit({"lag": -1 * self.time, "periods": None})).V() ) return self(*self.domain).V()
@timer(logger, "balance-init") def _birth_constraint(self) -> Domain | bool: """ Births a new General Resource Balance constraint :returns: Domain if constraint is created, else False :rtype: Domain | bool """ cons_grb = ( self.updated_part == 0 if self.aspect.ispos else -self.updated_part == 0 ) if cons_grb is True: # this catches the case where 0 is returned by fresh_part # making cons_grb, 0 == 0, i.e. True return False cons_grb.categorize("Balance") setattr( self.program, self.cons_name, cons_grb, ) self._inform() return self.domain @timer(logger, "balance-update") def _update_constraint( self, ) -> tuple[Domain, Aspect]: """ Updates an existing General Resource Balance constraint :returns: The domain and aspect of the updated constraint for logging purposes :rtype: tuple[Domain, Aspect] """ cons_grb: C = getattr(self.program, self.cons_name) setattr( self.program, self.cons_name, ( cons_grb + self.updated_part if self.aspect.ispos else cons_grb - self.updated_part ), ) self._inform() # this is returned for logging purposes return self.domain, self.aspect def _check_existing(self) -> bool: """Checks if the balance constraint already exists""" if ( (not self.samples and self.commodity) or (self.aspect(self.commodity, self.time) not in self.existing_aspects) or (self.commodity.insitu) ): return False return True def _inform(self): """ Updates the constraints in all the indices of self.domain Add constraint name to aspect """ self.domain.inform_components_of_cons(self.cons_name) self.aspect.constraints.add(self.cons_name) # update the GRB aspects self.existing_aspects.append(self) def _handshake(self): """Borrow attributes from aspect and domain""" # take from aspect self.model = self.aspect.model # take from program self.program = self.model.program self.balances = self.model.balances # take from domain self.commodity = self.domain.commodity self.samples = self.domain.samples self.time = self.domain.time def _init_sample(self): """Initializes the sample for the balance constraint if needed """ _balances = self.balances[self.commodity][self.space] lower_times = [t for t in _balances if t > self.time] if _balances else False if lower_times: _ = self.aspect(self.commodity, self.space, lower_times[0]) >= 0 def __eq__(self, other: Self): return is_(self.aspect, other.aspect) and self.domain == other.domain def __call__(self, *index: _X): """Returns the variable for the aspect at the given index""" return self.aspect(*index)