def __init__( self, avg_power_W, hrs_per_day=None, hrs_of_day=None, fit_to_limit=True, energy_rate_increase_per_update=None, update_interval=None, initial_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, final_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, balancing_energy_ratio: tuple = ( ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), use_market_maker_rate: bool = False): """ Constructor of LoadHoursStrategy :param avg_power_W: Power rating of load device :param hrs_per_day: Daily energy usage :param hrs_of_day: hours of day energy is needed :param fit_to_limit: if set to True, it will make a linear curve following following initial_buying_rate & final_buying_rate :param energy_rate_increase_per_update: Slope of Load bids change per update :param update_interval: Interval after which Load will update its offer :param initial_buying_rate: Starting point of load's preferred buying rate :param final_buying_rate: Ending point of load's preferred buying rate :param use_market_maker_rate: If set to True, Load would track its final buying rate as per utility's trading rate """ LoadValidator.validate_energy(avg_power_W=avg_power_W, hrs_per_day=hrs_per_day, hrs_of_day=hrs_of_day) self.state = LoadState() self.avg_power_W = avg_power_W # consolidated_cycle is KWh energy consumed for the entire year self.daily_energy_required = None # Energy consumed during the day ideally should not exceed daily_energy_required self.energy_per_slot_Wh = None self.hrs_per_day = {} # type: Dict[int, int] self.assign_hours_of_per_day(hrs_of_day, hrs_per_day) self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio) self._init_price_update(fit_to_limit, energy_rate_increase_per_update, update_interval, use_market_maker_rate, initial_buying_rate, final_buying_rate) self._calculate_active_markets() self._cycled_market = set() self._simulation_start_timestamp = None
def __init__(self, avg_power_W, hrs_per_day=None, hrs_of_day=None, daily_budget=None, min_energy_rate: Union[float, dict, str] = ConstSettings.LoadSettings.MIN_ENERGY_RATE, max_energy_rate: Union[float, dict, str] = ConstSettings.LoadSettings.MAX_ENERGY_RATE, balancing_energy_ratio: tuple = (ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO)): BaseStrategy.__init__(self) self.min_energy_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, min_energy_rate) self.max_energy_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, max_energy_rate) BidUpdateFrequencyMixin.__init__(self, initial_rate_profile=self.min_energy_rate, final_rate_profile=self.max_energy_rate) self.state = LoadState() self.avg_power_W = avg_power_W # consolidated_cycle is KWh energy consumed for the entire year self.daily_energy_required = None # Budget for a single day in eur self.daily_budget = daily_budget * 100 if daily_budget is not None else None # Energy consumed during the day ideally should not exceed daily_energy_required self.energy_per_slot_Wh = None self.energy_requirement_Wh = {} # type: Dict[Time, float] self.hrs_per_day = {} # type: Dict[int, int] if hrs_of_day is None: hrs_of_day = list(range(24)) # be a parameter on the constructor or if we want to deal in percentages if hrs_per_day is None: hrs_per_day = len(hrs_of_day) if hrs_of_day is None: hrs_of_day = list(range(24)) self.hrs_of_day = hrs_of_day self._initial_hrs_per_day = hrs_per_day self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio) if not all([0 <= h <= 23 for h in hrs_of_day]): raise ValueError("Hrs_of_day list should contain integers between 0 and 23.") if len(hrs_of_day) < hrs_per_day: raise ValueError("Length of list 'hrs_of_day' must be greater equal 'hrs_per_day'")
def __init__( self, avg_power_W, hrs_per_day=None, hrs_of_day=None, fit_to_limit=True, energy_rate_increase_per_update=1, update_interval=duration( minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL), initial_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.INITIAL_BUYING_RATE, final_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.FINAL_BUYING_RATE, balancing_energy_ratio: tuple = ( ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), use_market_maker_rate: bool = False): """ Constructor of LoadHoursStrategy :param avg_power_W: Power rating of load device :param hrs_per_day: Daily energy usage :param hrs_of_day: hours of day energy is needed :param fit_to_limit: if set to True, it will make a linear curve following following initial_buying_rate & final_buying_rate :param energy_rate_increase_per_update: Slope of Load bids change per update :param update_interval: Interval after which Load will update its offer :param initial_buying_rate: Starting point of load's preferred buying rate :param final_buying_rate: Ending point of load's preferred buying rate :param use_market_maker_rate: If set to True, Load would track its final buying rate as per utility's trading rate """ # If use_market_maker_rate is true, overwrite final_buying_rate to market maker rate if use_market_maker_rate: final_buying_rate = GlobalConfig.market_maker_rate if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.bid_update = \ UpdateFrequencyMixin(initial_rate=initial_buying_rate, final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, rate_limit_object=min) try: validate_load_device(avg_power_W=avg_power_W, hrs_per_day=hrs_per_day, hrs_of_day=hrs_of_day) except D3ADeviceException as e: raise D3ADeviceException(str(e)) for time_slot in generate_market_slot_list(): rate_change = self.bid_update.energy_rate_change_per_update[ time_slot] try: validate_load_device( initial_buying_rate=self.bid_update. initial_rate[time_slot], final_buying_rate=self.bid_update.final_rate[time_slot], energy_rate_increase_per_update=rate_change) except D3ADeviceException as e: raise D3ADeviceException(str(e)) self.state = LoadState() self.avg_power_W = avg_power_W # consolidated_cycle is KWh energy consumed for the entire year self.daily_energy_required = None # Energy consumed during the day ideally should not exceed daily_energy_required self.energy_per_slot_Wh = None self.energy_requirement_Wh = {} # type: Dict[Time, float] self.hrs_per_day = {} # type: Dict[int, int] self.assign_hours_of_per_day(hrs_of_day, hrs_per_day) self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio)
class LoadHoursStrategy(BidEnabledStrategy): parameters = ('avg_power_W', 'hrs_per_day', 'hrs_of_day', 'fit_to_limit', 'energy_rate_increase_per_update', 'update_interval', 'initial_buying_rate', 'final_buying_rate', 'balancing_energy_ratio', 'use_market_maker_rate') def __init__( self, avg_power_W, hrs_per_day=None, hrs_of_day=None, fit_to_limit=True, energy_rate_increase_per_update=None, update_interval=None, initial_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, final_buying_rate: Union[ float, dict, str] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, balancing_energy_ratio: tuple = ( ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), use_market_maker_rate: bool = False): """ Constructor of LoadHoursStrategy :param avg_power_W: Power rating of load device :param hrs_per_day: Daily energy usage :param hrs_of_day: hours of day energy is needed :param fit_to_limit: if set to True, it will make a linear curve following following initial_buying_rate & final_buying_rate :param energy_rate_increase_per_update: Slope of Load bids change per update :param update_interval: Interval after which Load will update its offer :param initial_buying_rate: Starting point of load's preferred buying rate :param final_buying_rate: Ending point of load's preferred buying rate :param use_market_maker_rate: If set to True, Load would track its final buying rate as per utility's trading rate """ LoadValidator.validate_energy(avg_power_W=avg_power_W, hrs_per_day=hrs_per_day, hrs_of_day=hrs_of_day) self.state = LoadState() self.avg_power_W = avg_power_W # consolidated_cycle is KWh energy consumed for the entire year self.daily_energy_required = None # Energy consumed during the day ideally should not exceed daily_energy_required self.energy_per_slot_Wh = None self.hrs_per_day = {} # type: Dict[int, int] self.assign_hours_of_per_day(hrs_of_day, hrs_per_day) self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio) self._init_price_update(fit_to_limit, energy_rate_increase_per_update, update_interval, use_market_maker_rate, initial_buying_rate, final_buying_rate) self._calculate_active_markets() self._cycled_market = set() self._simulation_start_timestamp = None def _init_price_update(self, fit_to_limit, energy_rate_increase_per_update, update_interval, use_market_maker_rate, initial_buying_rate, final_buying_rate): # Instantiate instance variables that should not be shared with child classes self.use_market_maker_rate = use_market_maker_rate self.fit_to_limit = fit_to_limit LoadValidator.validate_rate( fit_to_limit=fit_to_limit, energy_rate_increase_per_update=energy_rate_increase_per_update) if update_interval is None: update_interval = \ duration(minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.bid_update = \ TemplateStrategyBidUpdater( initial_rate=initial_buying_rate, final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, rate_limit_object=min) @staticmethod def _validate_rates(initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit): # all parameters have to be validated for each time slot here for time_slot in initial_rate.keys(): rate_change = None if fit_to_limit else \ find_object_of_same_weekday_and_time(energy_rate_change_per_update, time_slot) LoadValidator.validate_rate( initial_buying_rate=initial_rate[time_slot], energy_rate_increase_per_update=rate_change, final_buying_rate=find_object_of_same_weekday_and_time( final_rate, time_slot), fit_to_limit=fit_to_limit) def event_activate(self, **kwargs): self._calculate_active_markets() self.event_activate_price() self.bid_update.update_and_populate_price_settings(self.area) self.event_activate_energy() def event_market_cycle(self): if self.use_market_maker_rate: self._area_reconfigure_prices( final_buying_rate=get_market_maker_rate_from_config( self.area.next_market, 0) + self.owner.get_path_to_root_fees(), validate=False) super().event_market_cycle() self.add_entry_in_hrs_per_day() self.bid_update.update_and_populate_price_settings(self.area) if ConstSettings.IAASettings.MARKET_TYPE == 1: self.bid_update.reset(self) self._calculate_active_markets() self._update_energy_requirement_future_markets() self._set_alternative_pricing_scheme() self.update_state() def add_entry_in_hrs_per_day(self, overwrite=False): for market in self.area.all_markets: current_day = self._get_day_of_timestamp(market.time_slot) if current_day not in self.hrs_per_day or overwrite: self.hrs_per_day[current_day] = self._initial_hrs_per_day def update_state(self): self._post_first_bid() if self.area.current_market: self._cycled_market.add(self.area.current_market.time_slot) # Provide energy values for the past market slot, to be used in the settlement market self._set_energy_measurement_of_last_market() self._delete_past_state() def _set_energy_measurement_of_last_market(self): """Set the (simulated) actual energy of the device in the previous market slot.""" if self.area.current_market: self._set_energy_measurement_kWh( self.area.current_market.time_slot) def _set_energy_measurement_kWh(self, time_slot: DateTime) -> None: """Set the (simulated) actual energy consumed by the device in a market slot.""" energy_forecast_kWh = self.state.get_desired_energy_Wh( time_slot) / 1000 simulated_measured_energy_kWh = utils.compute_altered_energy( energy_forecast_kWh) self.state.set_energy_measurement_kWh(simulated_measured_energy_kWh, time_slot) def _delete_past_state(self): if (constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True or self.area.current_market is None): return self.state.delete_past_state_values(self.area.current_market.time_slot) self.bid_update.delete_past_state_values( self.area.current_market.time_slot) def _area_reconfigure_prices(self, **kwargs): if key_in_dict_and_not_none(kwargs, 'initial_buying_rate'): initial_rate = read_arbitrary_profile( InputProfileTypes.IDENTITY, kwargs['initial_buying_rate']) else: initial_rate = self.bid_update.initial_rate_profile_buffer if key_in_dict_and_not_none(kwargs, 'final_buying_rate'): final_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs['final_buying_rate']) else: final_rate = self.bid_update.final_rate_profile_buffer if key_in_dict_and_not_none(kwargs, 'energy_rate_increase_per_update'): energy_rate_change_per_update = \ read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs['energy_rate_increase_per_update']) else: energy_rate_change_per_update = \ self.bid_update.energy_rate_change_per_update_profile_buffer if key_in_dict_and_not_none(kwargs, 'fit_to_limit'): fit_to_limit = kwargs['fit_to_limit'] else: fit_to_limit = self.bid_update.fit_to_limit if key_in_dict_and_not_none(kwargs, 'update_interval'): if isinstance(kwargs['update_interval'], int): update_interval = duration(minutes=kwargs['update_interval']) else: update_interval = kwargs['update_interval'] else: update_interval = self.bid_update.update_interval if key_in_dict_and_not_none(kwargs, 'use_market_maker_rate'): self.use_market_maker_rate = kwargs['use_market_maker_rate'] try: self._validate_rates(initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit) except Exception as e: log.error( f"LoadHours._area_reconfigure_prices failed. Exception: {e}. " f"Traceback: {traceback.format_exc()}") return self.bid_update.set_parameters( initial_rate_profile_buffer=initial_rate, final_rate_profile_buffer=final_rate, energy_rate_change_per_update_profile_buffer= energy_rate_change_per_update, fit_to_limit=fit_to_limit, update_interval=update_interval) def area_reconfigure_event(self, **kwargs): """Reconfigure the device properties at runtime using the provided arguments.""" if key_in_dict_and_not_none(kwargs, 'hrs_per_day') or \ key_in_dict_and_not_none(kwargs, 'hrs_of_day'): self.assign_hours_of_per_day(kwargs['hrs_of_day'], kwargs['hrs_per_day']) self.add_entry_in_hrs_per_day(overwrite=True) if key_in_dict_and_not_none(kwargs, 'avg_power_W'): self.avg_power_W = kwargs['avg_power_W'] self._update_energy_requirement_future_markets() self._area_reconfigure_prices(**kwargs) self.bid_update.update_and_populate_price_settings(self.area) def event_activate_price(self): # If use_market_maker_rate is true, overwrite final_buying_rate to market maker rate if self.use_market_maker_rate: self._area_reconfigure_prices( final_buying_rate=get_market_maker_rate_from_config( self.area.next_market, 0) + self.owner.get_path_to_root_fees(), validate=False) self._validate_rates( self.bid_update.initial_rate_profile_buffer, self.bid_update.final_rate_profile_buffer, self.bid_update.energy_rate_change_per_update_profile_buffer, self.bid_update.fit_to_limit) @staticmethod def _find_acceptable_offer(market): offers = market.most_affordable_offers return random.choice(offers) def _offer_rate_can_be_accepted(self, offer: Offer, market_slot: Market): """Check if the offer rate is less than what the device wants to pay.""" max_affordable_offer_rate = self.bid_update.get_updated_rate( market_slot.time_slot) return (round(offer.energy_rate, DEFAULT_PRECISION) <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE) def _one_sided_market_event_tick(self, market, offer=None): if not self.state.can_buy_more_energy(market.time_slot): return try: if offer is None: if not market.offers: return acceptable_offer = self._find_acceptable_offer(market) else: if offer.id not in market.offers: return acceptable_offer = offer time_slot = market.time_slot current_day = self._get_day_of_timestamp(time_slot) if acceptable_offer and \ self.hrs_per_day[current_day] > FLOATING_POINT_TOLERANCE and \ self._offer_rate_can_be_accepted(acceptable_offer, market): energy_Wh = self.state.calculate_energy_to_accept( acceptable_offer.energy * 1000.0, time_slot) self.accept_offer(market, acceptable_offer, energy=energy_Wh / 1000.0, buyer_origin=self.owner.name, buyer_origin_id=self.owner.uuid, buyer_id=self.owner.uuid) self.state.decrement_energy_requirement( energy_Wh, time_slot, self.owner.name) self.hrs_per_day[current_day] -= self._operating_hours( energy_Wh / 1000.0) except MarketException: self.log.exception("An Error occurred while buying an offer") def _double_sided_market_event_tick(self, market): self.bid_update.update(market, self) def event_tick(self): """Post bids on market tick. This method is triggered by the TICK event.""" for market in self.active_markets: if ConstSettings.IAASettings.MARKET_TYPE == 1: self._one_sided_market_event_tick(market) elif ConstSettings.IAASettings.MARKET_TYPE == 2 or \ ConstSettings.IAASettings.MARKET_TYPE == 3: self._double_sided_market_event_tick(market) self.bid_update.increment_update_counter_all_markets(self) def event_offer(self, *, market_id, offer): """Automatically react to offers in single-sided markets. This method is triggered by the OFFER event. """ super().event_offer(market_id=market_id, offer=offer) # In two-sided markets, the device doesn't automatically react to offers (it actively bids) if ConstSettings.IAASettings.MARKET_TYPE != 1: return market = self.area.get_future_market_from_id(market_id) # TODO: do we really need self._cycled_market ? if market.time_slot not in self._cycled_market: return if self._can_buy_in_market( market) and self._offer_comes_from_different_seller(offer): self._one_sided_market_event_tick(market, offer) def _can_buy_in_market(self, market): return self._is_market_active( market) and self.state.can_buy_more_energy(market.time_slot) def _offer_comes_from_different_seller(self, offer): return offer.seller != self.owner.name and offer.seller != self.area.name def _set_alternative_pricing_scheme(self): if ConstSettings.IAASettings.AlternativePricing.PRICING_SCHEME != 0: for market in self.area.all_markets: time_slot = market.time_slot final_rate = self.area.config.market_maker_rate[time_slot] self.bid_update.reassign_mixin_arguments(time_slot, initial_rate=0, final_rate=final_rate) def _post_first_bid(self): if ConstSettings.IAASettings.MARKET_TYPE == 1: return for market in self.active_markets: if (self.state.can_buy_more_energy(market.time_slot) and not self.are_bids_posted(market.id)): bid_energy = self.state.get_energy_requirement_Wh( market.time_slot) if self.is_eligible_for_balancing_market: bid_energy -= self.state.get_desired_energy(market.time_slot) * \ self.balancing_energy_ratio.demand try: self.post_first_bid(market, bid_energy) except MarketException: pass def event_balancing_market_cycle(self): for market in self.active_markets: self._demand_balancing_offer(market) def event_bid_traded(self, *, market_id, bid_trade): """Register the bid traded by the device and its effects. Extends the superclass method. This method is triggered by the MarketEvent.BID_TRADED event. """ super().event_bid_traded(market_id=market_id, bid_trade=bid_trade) market = self.area.get_future_market_from_id(market_id) if bid_trade.offer_bid.buyer == self.owner.name: self.state.decrement_energy_requirement( bid_trade.offer_bid.energy * 1000, market.time_slot, self.owner.name) market_day = self._get_day_of_timestamp(market.time_slot) if self.hrs_per_day != {} and market_day in self.hrs_per_day: self.hrs_per_day[market_day] -= self._operating_hours( bid_trade.offer_bid.energy) def event_trade(self, *, market_id, trade): market = self.area.get_future_market_from_id(market_id) assert market is not None self.assert_if_trade_bid_price_is_too_high(market, trade) if ConstSettings.BalancingSettings.FLEXIBLE_LOADS_SUPPORT: # Load can only put supply_balancing_offers only when there is a trade in spot_market self._supply_balancing_offer(market, trade) super().event_trade(market_id=market_id, trade=trade) # committing to increase its consumption when required def _demand_balancing_offer(self, market): if not self.is_eligible_for_balancing_market: return ramp_up_energy = \ self.balancing_energy_ratio.demand * \ self.state.get_desired_energy_Wh(market.time_slot) self.state.decrement_energy_requirement(ramp_up_energy, market.time_slot, self.owner.name) ramp_up_price = DeviceRegistry.REGISTRY[ self.owner.name][0] * ramp_up_energy if ramp_up_energy != 0 and ramp_up_price != 0: self.area.get_balancing_market(market.time_slot). \ balancing_offer(ramp_up_price, -ramp_up_energy, self.owner.name) # committing to reduce its consumption when required def _supply_balancing_offer(self, market, trade): if not self.is_eligible_for_balancing_market: return if trade.buyer != self.owner.name: return ramp_down_energy = self.balancing_energy_ratio.supply * trade.offer_bid.energy ramp_down_price = DeviceRegistry.REGISTRY[ self.owner.name][1] * ramp_down_energy self.area.get_balancing_market(market.time_slot).balancing_offer( ramp_down_price, ramp_down_energy, self.owner.name) def event_activate_energy(self): self.hrs_per_day = {0: self._initial_hrs_per_day} self._simulation_start_timestamp = self.area.now self._update_energy_requirement_future_markets() @property def active_markets(self): """Return market slots in which the load device is active. Active market slots are specific to the LoadHoursStrategy and depend on the hours of day in which the device will be active (selected by the user). """ return self._active_markets def _calculate_active_markets(self): self._active_markets = [ market for market in self.area.all_markets if self._is_market_active(market) ] if self.area else [] def _is_market_active(self, market): return self._allowed_operating_hours(market.time_slot) and \ market.in_sim_duration and \ (not self.area.current_market or market.time_slot >= self.area.current_market.time_slot) def assign_hours_of_per_day(self, hrs_of_day, hrs_per_day): if hrs_of_day is None: hrs_of_day = list(range(24)) # be a parameter on the constructor or if we want to deal in percentages if hrs_per_day is None: hrs_per_day = len(hrs_of_day) if hrs_of_day is None: hrs_of_day = list(range(24)) self.hrs_of_day = hrs_of_day self._initial_hrs_per_day = hrs_per_day if not all([0 <= h <= 23 for h in hrs_of_day]): raise ValueError( "Hrs_of_day list should contain integers between 0 and 23.") if len(hrs_of_day) < hrs_per_day: raise ValueError( "Length of list 'hrs_of_day' must be greater equal 'hrs_per_day'" ) def _update_energy_requirement_future_markets(self): self.energy_per_slot_Wh = convert_W_to_Wh(self.avg_power_W, self.area.config.slot_length) for market in self.area.all_markets: desired_energy_Wh = self.energy_per_slot_Wh \ if self._allowed_operating_hours(market.time_slot) else 0.0 self.state.set_desired_energy(desired_energy_Wh, market.time_slot) for market in self.active_markets: current_day = self._get_day_of_timestamp(market.time_slot) if current_day not in self.hrs_per_day or \ self.hrs_per_day[current_day] <= FLOATING_POINT_TOLERANCE: # Overwrite desired energy to 0 in case the previous step has populated the # desired energy by the hrs_per_day have been exhausted. self.state.set_desired_energy(0.0, market.time_slot, True) if self.area.current_market: self.state.update_total_demanded_energy( self.area.current_market.time_slot) def _allowed_operating_hours(self, time): return time.hour in self.hrs_of_day def _operating_hours(self, energy_kWh): return (((energy_kWh * 1000) / self.energy_per_slot_Wh) * (self.area.config.slot_length / duration(hours=1))) def _get_day_of_timestamp(self, time_slot: DateTime): """Return the number of days passed from the simulation start date to the time slot.""" if self._simulation_start_timestamp is None: return 0 return (time_slot - self._simulation_start_timestamp).days