예제 #1
0
    def __init__(self,
                 name: str = None,
                 children: List["Area"] = None,
                 strategy: BaseStrategy = None,
                 appliance: BaseAppliance = None,
                 config: SimulationConfig = None,
                 budget_keeper=None,
                 balancing_spot_trade_ratio=ConstSettings.BalancingSettings.
                 SPOT_TRADE_RATIO):
        self.balancing_spot_trade_ratio = balancing_spot_trade_ratio
        self.active = False
        self.log = TaggedLogWrapper(log, name)
        self.current_tick = 0
        self.name = name
        self.slug = slugify(name, to_lower=True)
        self.area_id = Area._area_id_counter
        Area._area_id_counter += 1
        self.parent = None
        self.children = children if children is not None else []
        for child in self.children:
            child.parent = self
        self.strategy = strategy
        self.appliance = appliance
        self._config = config

        self.budget_keeper = budget_keeper
        if budget_keeper:
            self.budget_keeper.area = self
        self._bc = None  # type: BlockChainInterface
        self._markets = AreaMarkets(self.log)
        self.stats = AreaStats(self._markets)
        self.dispatcher = AreaDispatcher(self)
예제 #2
0
    def setUp(self):
        ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True
        DeviceRegistry.REGISTRY = {
            "H1 General Load": (33, 35),
            "H2 General Load": (33, 35),
            "H1 Storage1": (23, 25),
            "H1 Storage2": (23, 25),
        }

        self.appliance = MagicMock(spec=SimpleAppliance)
        self.strategy = MagicMock(spec=StorageStrategy)
        self.config = MagicMock(spec=SimulationConfig)
        self.config.slot_length = duration(minutes=15)
        self.config.tick_length = duration(seconds=15)
        self.config.start_date = today(tz=TIME_ZONE)
        self.config.sim_duration = duration(days=1)
        self.area = Area("test_area",
                         None,
                         None,
                         self.strategy,
                         self.appliance,
                         self.config,
                         None,
                         transfer_fee_pct=1)
        self.area.parent = self.area
        self.area.children = [self.area]
        self.area.transfer_fee_pct = 1
        self.dispatcher = AreaDispatcher(self.area)
        self.stats = AreaStats(self.area._markets)
예제 #3
0
    def setUp(self):
        ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True
        DeviceRegistry.REGISTRY = {
            "H1 General Load": (33, 35),
            "H2 General Load": (33, 35),
            "H1 Storage1": (23, 25),
            "H1 Storage2": (23, 25),
        }

        self.strategy = MagicMock(spec=StorageStrategy)
        self.config = MagicMock(spec=SimulationConfig)
        self.config.slot_length = duration(minutes=15)
        self.config.tick_length = duration(seconds=15)
        self.config.ticks_per_slot = int(self.config.slot_length.seconds /
                                         self.config.tick_length.seconds)
        self.config.start_date = today(tz=constants.TIME_ZONE)
        GlobalConfig.sim_duration = duration(days=1)
        self.config.sim_duration = duration(days=1)
        self.config.grid_fee_type = 1
        self.config.end_date = self.config.start_date + self.config.sim_duration
        self.area = Area("test_area",
                         None,
                         None,
                         self.strategy,
                         self.config,
                         None,
                         grid_fee_percentage=1)
        self.area_child = Area("test_area_c",
                               None,
                               None,
                               self.strategy,
                               self.config,
                               None,
                               grid_fee_percentage=1)
        self.area_child.parent = self.area
        self.area.children = [self.area_child]
        self.area.grid_fee_percentage = 1
        self.dispatcher = AreaDispatcher(self.area)
        self.stats = AreaStats(self.area._markets, self.area)
예제 #4
0
class Area:
    _area_id_counter = 1

    def __init__(self,
                 name: str = None,
                 children: List["Area"] = None,
                 strategy: BaseStrategy = None,
                 appliance: BaseAppliance = None,
                 config: SimulationConfig = None,
                 budget_keeper=None,
                 balancing_spot_trade_ratio=ConstSettings.BalancingSettings.
                 SPOT_TRADE_RATIO):
        self.balancing_spot_trade_ratio = balancing_spot_trade_ratio
        self.active = False
        self.log = TaggedLogWrapper(log, name)
        self.current_tick = 0
        self.name = name
        self.slug = slugify(name, to_lower=True)
        self.area_id = Area._area_id_counter
        Area._area_id_counter += 1
        self.parent = None
        self.children = children if children is not None else []
        for child in self.children:
            child.parent = self
        self.strategy = strategy
        self.appliance = appliance
        self._config = config

        self.budget_keeper = budget_keeper
        if budget_keeper:
            self.budget_keeper.area = self
        self._bc = None  # type: BlockChainInterface
        self._markets = AreaMarkets(self.log)
        self.stats = AreaStats(self._markets)
        self.dispatcher = AreaDispatcher(self)

    def activate(self, bc=None):
        if bc:
            self._bc = bc
        for attr, kind in [(self.strategy, 'Strategy'),
                           (self.appliance, 'Appliance')]:
            if attr:
                if self.parent:
                    attr.area = self.parent
                    attr.owner = self
                else:
                    raise AreaException("{kind} {attr.__class__.__name__} "
                                        "on area {s} without parent!".format(
                                            kind=kind, attr=attr, s=self))

            if self.budget_keeper:
                self.budget_keeper.activate()

        # Cycle markets without triggering it's own event chain.
        self._cycle_markets(_trigger_event=False)

        if not self.strategy and self.parent is not None:
            self.log.warning("No strategy. Using inter area agent.")
        self.log.info('Activating area')
        self.active = True
        self.dispatcher.broadcast_activate()

    def _cycle_markets(self, _trigger_event=True, _market_cycle=False):
        """
        Remove markets for old time slots, add markets for new slots.
        Trigger `MARKET_CYCLE` event to allow child markets to also cycle.

        It's important for this to happen from top to bottom of the `Area` tree
        in order for the `InterAreaAgent`s to be connected correctly

        `_trigger_event` is used internally to avoid multiple event chains during
        initial area activation.
        """
        if not self.children:
            # Since children trade in markets we only need to populate them if there are any
            return

        if self.budget_keeper and _market_cycle:
            self.budget_keeper.process_market_cycle()

        now = self.now
        time_in_hour = duration(minutes=now.minute, seconds=now.second)
        now = now.at(now.hour, minute=0, second=0) + \
            ((time_in_hour // self.config.slot_length) * self.config.slot_length)

        self.log.info("Cycling markets")
        self._markets.rotate_markets(now, self.stats, self.dispatcher)

        # Clear `current_market` cache
        self.__dict__.pop('current_market', None)

        # Markets range from one slot to market_count into the future
        changed = self._markets.create_future_markets(now, True, self)

        if ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET and \
                len(DeviceRegistry.REGISTRY.keys()) != 0:
            changed_balancing_market = self._markets.create_future_markets(
                now, False, self)
        else:
            changed_balancing_market = None

        # Force market cycle event in case this is the first market slot
        if (changed or len(self._markets.past_markets.keys())
                == 0) and _trigger_event:
            self.dispatcher.broadcast_market_cycle()

        # Force balancing_market cycle event in case this is the first market slot
        if (changed_balancing_market or len(self._markets.past_balancing_markets.keys()) == 0) \
                and _trigger_event and ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET:
            self.dispatcher.broadcast_balancing_market_cycle()

    def tick(self, is_root_area=False):
        if self.current_tick % self.config.ticks_per_slot == 0 and is_root_area:
            self._cycle_markets()
        self.dispatcher.broadcast_tick(area=self)
        self.current_tick += 1

    def __repr__(self):
        return "<Area '{s.name}' markets: {markets}>".format(
            s=self,
            markets=[
                t.strftime(TIME_FORMAT) for t in self._markets.markets.keys()
            ])

    @property
    def current_slot(self):
        return self.current_tick // self.config.ticks_per_slot

    @property
    def current_tick_in_slot(self):
        return self.current_tick % self.config.ticks_per_slot

    @property
    def config(self):
        if self._config:
            return self._config
        if self.parent:
            return self.parent.config
        return DEFAULT_CONFIG

    @property
    def bc(self) -> Optional[BlockChainInterface]:
        if self._bc is not None:
            return self._bc
        if self.parent:
            return self.parent.bc
        return None

    @cached_property
    def child_by_slug(self):
        slug_map = {}
        areas = [self]
        while areas:
            for area in list(areas):
                slug_map[area.slug] = area
                areas.remove(area)
                areas.extend(area.children)
        return slug_map

    def get_now(self) -> DateTime:
        """Compatibility wrapper"""
        warnings.warn(
            "The '.get_now()' method has been replaced by the '.now' property. "
            "Please use that in the future.")
        return self.now

    @property
    def now(self) -> DateTime:
        """
        Return the 'current time' as a `DateTime` object.
        Can be overridden in subclasses to change the meaning of 'now'.

        In this default implementation 'current time' is defined by the number of ticks that
        have passed.
        """
        return DateTime.now(tz=TIME_ZONE).start_of('day').add(
            seconds=self.config.tick_length.seconds * self.current_tick)

    @property
    def all_markets(self):
        return list(self._markets.markets.values())

    @property
    def past_markets(self):
        return list(self._markets.past_markets.values())

    def get_market(self, timeslot):
        return self._markets.markets[timeslot]

    def get_past_market(self, timeslot):
        return self._markets.past_markets[timeslot]

    def get_balancing_market(self, timeslot):
        return self._markets.balancing_markets[timeslot]

    @property
    def balancing_markets(self):
        return list(self._markets.balancing_markets.values())

    @property
    def past_balancing_markets(self):
        return list(self._markets.past_balancing_markets.values())

    @property
    def market_with_most_expensive_offer(self):
        # In case of a tie, max returns the first market occurrence in order to
        # satisfy the most recent market slot
        return max(
            self.all_markets,
            key=lambda m: m.sorted_offers[0].price / m.sorted_offers[0].energy)

    @property
    def next_market(self):
        """Returns the 'current' market (i.e. the one currently 'running')"""
        try:
            return list(self._markets.markets.values())[0]
        except IndexError:
            return None

    @cached_property
    def current_market(self):
        """Returns the 'current' market (i.e. the one currently 'running')"""
        try:
            return list(self._markets.past_markets.values())[-1]
        except IndexError:
            return None

    def get_future_market_from_id(self, _id):
        try:
            return [m for m in self._markets.markets.values()
                    if m.id == _id][0]
        except IndexError:
            return None

    @property
    def last_past_market(self):
        try:
            return list(self._markets.past_markets.values())[-1]
        except IndexError:
            return None

    @cached_property
    def available_triggers(self):
        triggers = []
        if isinstance(self.strategy, TriggerMixin):
            triggers.extend(self.strategy.available_triggers)
        if isinstance(self.appliance, TriggerMixin):
            triggers.extend(self.appliance.available_triggers)
        return {t.name: t for t in triggers}

    def _fire_trigger(self, trigger_name, **params):
        for target in (self.strategy, self.appliance):
            if isinstance(target, TriggerMixin):
                for trigger in target.available_triggers:
                    if trigger.name == trigger_name:
                        return target.fire_trigger(trigger_name, **params)
예제 #5
0
 def __init__(self, area):
     self.event_dispatching_via_redis = \
         ConstSettings.GeneralSettings.EVENT_DISPATCHING_VIA_REDIS
     self.dispatcher = RedisAreaDispatcher(area, mock_redis, mock_redis_market) \
         if self.event_dispatching_via_redis else AreaDispatcher(area)