diff --git a/.gitignore b/.gitignore index c655e503..1530b61f 100644 --- a/.gitignore +++ b/.gitignore @@ -136,11 +136,12 @@ dmypy.json *.orig .idea .vscode -examples/inputs/learned_strategies -examples/outputs -examples/local_db/ -workshop/inputs/learned_strategies -workshop/local_db/ +local_db assume-db forecasts_df.csv +examples/inputs/learned_strategies +examples/outputs + +examples/notebooks/inputs +examples/notebooks/outputs diff --git a/assume/common/base.py b/assume/common/base.py index 6cade862..9169b304 100644 --- a/assume/common/base.py +++ b/assume/common/base.py @@ -4,7 +4,7 @@ from collections import defaultdict from datetime import datetime, timedelta -from typing import TypedDict +from typing import Dict, List, Tuple, TypedDict, Union import pandas as pd @@ -18,25 +18,18 @@ class BaseStrategy: class BaseUnit: """ - A base class for a unit. - - :param id: The ID of the unit. - :type id: str - :param unit_operator: The operator of the unit. - :type unit_operator: str - :param technology: The technology of the unit. - :type technology: str - :param bidding_strategies: The bidding strategies of the unit. - :type bidding_strategies: dict[str, BaseStrategy] - :param index: The index of the unit. - :type index: pd.DatetimeIndex - :param outputs: The output of the unit. - :type outputs: dict[str, pd.Series] - :param forecaster: The forecast of the unit. - :type forecaster: Forecaster + A base class for a unit. This class is used as a foundation for all units. + + Args: + id (str): The ID of the unit. + unit_operator (str): The operator of the unit. + technology (str): The technology of the unit. + bidding_strategies (dict[str, BaseStrategy]): The bidding strategies of the unit. + index (pd.DatetimeIndex): The index of the unit. + node (str, optional): The node of the unit. Defaults to "". + forecaster (Forecaster, optional): The forecast of the unit. Defaults to None. + **kwargs: Additional keyword arguments. - Methods - ------- """ def __init__( @@ -73,17 +66,21 @@ def __init__( def calculate_bids( self, market_config: MarketConfig, - product_tuples: list[tuple], + product_tuples: List[Tuple], ) -> Orderbook: """ Calculate the bids for the next time step. - :param market_config: The market configuration. - :type market_config: MarketConfig - :param product_tuples: The product tuples. - :type product_tuples: list[tuple] - :return: The bids. - :rtype: Orderbook + Args: + market_config (MarketConfig): The market configuration. + product_tuples (List[Tuple]): The product tuples. + + Returns: + Orderbook: The bids. + + Raises: + KeyError: If the product type is not found in the bidding strategies. + """ if market_config.product_type not in self.bidding_strategies: @@ -108,14 +105,15 @@ def calculate_bids( def calculate_marginal_cost(self, start: pd.Timestamp, power: float) -> float: """ - calculates the marginal cost for the given power + Calculate the marginal cost for the given power. + + Args: + start (pd.Timestamp): The start time of the dispatch. + power (float): The power output of the unit. + + Returns: + float: The marginal cost for the given power. - :param start: the start time of the dispatch - :type start: pd.Timestamp - :param power: the power output of the unit - :type power: float - :return: the marginal cost for the given power - :rtype: float """ return 0 @@ -125,13 +123,15 @@ def set_dispatch_plan( orderbook: Orderbook, ) -> None: """ - adds dispatch plan from current market result to total dispatch plan + The method iterates through the orderbook, adding the accepted volumes to the corresponding time slots + in the dispatch plan. It then calculates the cashflow and the reward for the bidding strategies. + + Args: + marketconfig (MarketConfig): The market configuration. + orderbook (Orderbook): The orderbook. - :param marketconfig: The market configuration. - :type marketconfig: MarketConfig - :param orderbook: The orderbook. - :type orderbook: Orderbook """ + product_type = marketconfig.product_type for order in orderbook: start = order["start_time"] @@ -155,7 +155,16 @@ def calculate_generation_cost( start: datetime, end: datetime, product_type: str, - ): + ) -> None: + """ + Calculate the generation cost for a specific product type within the given time range. + + Args: + - start (datetime): The start time for the calculation. + - end (datetime): The end time for the calculation. + - product_type (str): The type of product for which the generation cost is to be calculated. + + """ if start not in self.index: return product_type_mc = product_type + "_marginal_costs" @@ -171,43 +180,44 @@ def execute_current_dispatch( end: pd.Timestamp, ) -> pd.Series: """ - check if the total dispatch plan is feasible - This checks if the market feedback is feasible for the given unit. - And sets the closest dispatch if not. - The end param should be inclusive. + Check if the total dispatch plan is feasible. - :param start: the start time of the dispatch - :type start: pd.Timestamp - :param end: the end time of the dispatch - :type end: pd.Timestamp - :return: the volume of the unit within the given time range - :rtype: pd.Series + This method checks if the market feedback is feasible for the given unit and sets the closest dispatch if not. + The end parameter should be inclusive. + + Args: + start: The start time of the dispatch. + end: The end time of the dispatch. + + Returns: + The volume of the unit within the given time range. """ return self.outputs["energy"][start:end] def get_output_before(self, dt: datetime, product_type: str = "energy") -> float: """ - return output before the given datetime. - If datetime is before the start of the index, 0 is returned. + Return output before the given datetime. - :param dt: the datetime - :type dt: datetime - :param product_type: the product type - :type product_type: str - :return: the output before the given datetime - :rtype: float + If the datetime is before the start of the index, 0 is returned. + + Args: + dt: The datetime. + product_type: The product type (default is "energy"). + + Returns: + The output before the given datetime. """ if dt - self.index.freq < self.index[0]: return 0 else: return self.outputs[product_type].at[dt - self.index.freq] - def as_dict(self) -> dict: + def as_dict(self) -> Dict[str, Union[str, int]]: """ Returns a dictionary representation of the unit. - :return: a dictionary representation of the unit - :rtype: dict + Returns: + A dictionary representation of the unit. """ return { "id": self.id, @@ -219,12 +229,11 @@ def as_dict(self) -> dict: def calculate_cashflow(self, product_type: str, orderbook: Orderbook): """ - calculates the cashflow for the given product_type + Calculates the cashflow for the given product_type. - :param product_type: the product type - :type product_type: str - :param orderbook: The orderbook. - :type orderbook: Orderbook + Args: + product_type: The product type. + orderbook: The orderbook. """ for order in orderbook: start = order["start_time"] @@ -246,38 +255,33 @@ def calculate_cashflow(self, product_type: str, orderbook: Orderbook): cashflow * hours ) - def get_starting_costs(self, op_time: int): + def get_starting_costs(self, op_time: int) -> float: """ - op_time is hours running from get_operation_time - returns the costs if start_up is planned - :param op_time: operation time - :type op_time: int - :return: start_costs - :rtype: float + Returns the costs if start-up is planned. + + Args: + op_time: Operation time in hours running from get_operation_time. + + Returns: + Start-up costs. """ return 0 class SupportsMinMax(BaseUnit): """ - Base Class used for Powerplant derived classes - - :param min_power: The minimum power output of the unit. - :type min_power: float - :param max_power: The maximum power output of the unit. - :type max_power: float - :param ramp_down: How much power can be decreased in one time step. - :type ramp_down: float - :param ramp_up: How much power can be increased in one time step. - :type ramp_up: float - :param efficiency: The efficiency of the unit. - :type efficiency: float - :param emission_factor: The emission factor of the unit. - :type emission_factor: float - :param min_operating_time: The minimum time the unit has to be on. - :type min_operating_time: int - :param min_down_time: The minimum time the unit has to be off. - :type min_down_time: int + Base Class used for units supporting continuous dispatch and without energy storage. + This class is best to be used as foundation for classes of power plants and similar units. + + Args: + min_power (float): The minimum power output of the unit. + max_power (float): The maximum power output of the unit. + ramp_down (float): How much power can be decreased in one time step. + ramp_up (float): How much power can be increased in one time step. + efficiency (float): The efficiency of the unit. + emission_factor (float): The emission factor of the unit. + min_operating_time (int): The minimum time the unit has to be on. + min_down_time (int): The minimum time the unit has to be off. Methods ------- @@ -293,19 +297,18 @@ class SupportsMinMax(BaseUnit): min_down_time: int def calculate_min_max_power( - self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" + self, start: pd.Timestamp, end: pd.Timestamp, product_type: str = "energy" ) -> tuple[pd.Series, pd.Series]: """ Calculates the min and max power for the given time period - :param start: the start time of the dispatch - :type start: pd.Timestamp - :param end: the end time of the dispatch - :type end: pd.Timestamp - :param product_type: the product type of the unit - :type product_type: str - :return: the min and max power for the given time period - :rtype: tuple[pd.Series, pd.Series] + Args: + start (pd.Timestamp): the start time of the dispatch + end (pd.Timestamp): the end time of the dispatch + product_type (str): the product type of the unit + + Returns: + tuple[pd.Series, pd.Series]: the min and max power for the given time period """ pass @@ -315,14 +318,13 @@ def calculate_ramp( """ Calculates the ramp for the given power - :param previous_power: the previous power output of the unit - :type previous_power: float - :param power: the power output of the unit - :type power: float - :param current_power: the current power output of the unit - :type current_power: float - :return: the ramp for the given power - :rtype: float + Args: + previous_power (float): the previous power output of the unit + power (float): the power output of the unit + current_power (float): the current power output of the unit + + Returns: + float: the ramp for the given power """ if power == 0: # if less than min_power is required, we run min_power @@ -344,31 +346,29 @@ def calculate_ramp( ) return power - def get_clean_spread(self, prices: pd.DataFrame): + def get_clean_spread(self, prices: pd.DataFrame) -> float: """ - returns the clean spread for the given prices + Returns the clean spread for the given prices + + Args: + prices (pd.DataFrame): the prices - :param prices: the prices - :type prices: pd.DataFrame - :return: the clean spread for the given prices - :rtype: float + Returns: + float: the clean spread for the given prices """ emission_cost = self.emission_factor * prices["co"].mean() fuel_cost = prices[self.technology.replace("_combined", "")].mean() return (fuel_cost + emission_cost) / self.efficiency - def get_operation_time(self, start: datetime): + def get_operation_time(self, start: datetime) -> int: """ - returns the operation time - if unit is on since 4 hours, it returns 4 - if the unit is off since 4 hours, it returns -4 - The value at start is not considered + Returns the operation time - :param start: the start time - :type start: datetime - :return: the operation time - :rtype: int + Args: + start (datetime): the start time + Returns: + int: the operation time """ before = start - self.index.freq # before = start @@ -387,15 +387,15 @@ def get_operation_time(self, start: datetime): runn += 1 return (-1) ** is_off * runn - def get_average_operation_times(self, start: datetime): + def get_average_operation_times(self, start: datetime) -> tuple[float, float]: """ - calculates the average uninterupted operation time - :param start: the current time - :type start: datetime - :return: avg_op_time - :rtype: float - :return: avg_down_time - :rtype: float + Calculates the average uninterrupted operation time + + Args: + start (datetime): the current time + + Returns: + tuple[float, float]: avg_op_time, avg_down_time """ op_series = [] @@ -432,14 +432,18 @@ def get_average_operation_times(self, start: datetime): return max(1, avg_op_time), max(1, avg_down_time) - def get_starting_costs(self, op_time: int): + def get_starting_costs(self, op_time: int) -> float: """ - op_time is hours running from get_operation_time - returns the costs if start_up is planned - :param op_time: operation time - :type op_time: int - :return: start_costs - :rtype: float + Returns the start-up cost for the given operation time. + If operation time is positive, the unit is running, so no start-up costs are returned. + If operation time is negative, the unit is not running, so start-up costs are returned + according to the start-up costs of the unit and the hot/warm/cold start times. + + Args: + op_time (int): operation time + + Returns: + float: start_costs """ if op_time > 0: # unit is running @@ -459,32 +463,21 @@ def get_starting_costs(self, op_time: int): class SupportsMinMaxCharge(BaseUnit): """ - Base Class used for Storage derived classes - - :param initial_soc: The initial state of charge of the storage. - :type initial_soc: float - :param min_power_charge: How much power must be charged at least in one time step. - :type min_power_charge: float - :param max_power_charge: How much power can be charged at most in one time step. - :type max_power_charge: float - :param min_power_discharge: How much power must be discharged at least in one time step. - :type min_power_discharge: float - :param max_power_discharge: How much power can be discharged at most in one time step. - :type max_power_discharge: float - :param ramp_up_discharge: How much power can be increased in discharging in one time step. - :type ramp_up_discharge: float - :param ramp_down_discharge: How much power can be decreased in discharging in one time step. - :type ramp_down_discharge: float - :param ramp_up_charge: How much power can be increased in charging in one time step. - :type ramp_up_charge: float - :param ramp_down_charge: How much power can be decreased in charging in one time step. - :type ramp_down_charge: float - :param max_volume: The maximum volume of the storage. - :type max_volume: float - :param efficiency_charge: The efficiency of charging. - :type efficiency_charge: float - :param efficiency_discharge: The efficiency of discharging. - :type efficiency_discharge: float + Base Class used for units with energy storage. + + Args: + initial_soc (float): The initial state of charge of the storage. + min_power_charge (float): How much power must be charged at least in one time step. + max_power_charge (float): How much power can be charged at most in one time step. + min_power_discharge (float): How much power must be discharged at least in one time step. + max_power_discharge (float): How much power can be discharged at most in one time step. + ramp_up_discharge (float): How much power can be increased in discharging in one time step. + ramp_down_discharge (float): How much power can be decreased in discharging in one time step. + ramp_up_charge (float): How much power can be increased in charging in one time step. + ramp_down_charge (float): How much power can be decreased in charging in one time step. + max_volume (float): The maximum volume of the storage. + efficiency_charge (float): The efficiency of charging. + efficiency_discharge (float): The efficiency of discharging. Methods ------- @@ -505,18 +498,17 @@ class SupportsMinMaxCharge(BaseUnit): def calculate_min_max_charge( self, start: pd.Timestamp, end: pd.Timestamp, product_type="energy" - ) -> tuple[pd.Series, pd.Series]: + ) -> Tuple[pd.Series, pd.Series]: """ calculates the min and max charging power for the given time period - :param start: the start time of the dispatch - :type start: pd.Timestamp - :param end: the end time of the dispatch - :type end: pd.Timestamp - :param product_type: the product type of the unit - :type product_type: str - :return: the min and max charging power for the given time period - :rtype: tuple[pd.Series, pd.Series] + Args: + start (pd.Timestamp): the start time of the dispatch + end (pd.Timestamp): the end time of the dispatch + product_type (str, optional): the product type of the unit. Defaults to "energy". + + Returns: + Tuple[pd.Series, pd.Series]: the min and max charging power for the given time period """ pass @@ -539,28 +531,30 @@ def calculate_min_max_discharge( def get_soc_before(self, dt: datetime) -> float: """ - return SoC before the given datetime. + This method returns the State of Charge (SoC) before the given datetime. If datetime is before the start of the index, the initial SoC is returned. The SoC is a float between 0 and 1. - :param dt: the datetime - :type dt: datetime - :return: the SoC before the given datetime - :rtype: float + Args: + dt (datetime): the datetime + + Returns: + float: the SoC before the given datetime """ if dt - self.index.freq <= self.index[0]: return self.initial_soc else: return self.outputs["soc"].at[dt - self.index.freq] - def get_clean_spread(self, prices: pd.DataFrame): + def get_clean_spread(self, prices: pd.DataFrame) -> float: """ - returns the clean spread for the given prices + Return the clean spread for the given prices - :param prices: the prices - :type prices: pd.DataFrame - :return: the clean spread for the given prices - :rtype: float + Args: + prices (pd.DataFrame): the prices + + Returns: + float: the clean spread for the given prices """ emission_cost = self.emission_factor * prices["co"].mean() fuel_cost = prices[self.technology.replace("_combined", "")].mean() @@ -568,23 +562,20 @@ def get_clean_spread(self, prices: pd.DataFrame): def calculate_ramp_discharge( self, - soc: float, previous_power: float, power_discharge: float, current_power: float = 0, - min_power_discharge: float = 0, ) -> float: """ - calculates the ramp for the given discharging power + Adjusts the discharging power to the ramping constraints. + + Args: + previous_power (float): the previous power output of the unit + power_discharge (float): the discharging power output of the unit + current_power (float, optional): the current power output of the unit. Defaults to 0. - :param previous_power: the previous power output of the unit - :type previous_power: float - :param power_discharge: the discharging power output of the unit - :type power_discharge: float - :param current_power: the current power output of the unit - :type current_power: float - :return: the ramp for the given discharging power - :rtype: float + Returns: + float: the ramp for the given discharging power """ if power_discharge == 0: return power_discharge @@ -614,23 +605,22 @@ def calculate_ramp_discharge( def calculate_ramp_charge( self, - soc: float, previous_power: float, power_charge: float, current_power: float = 0, ) -> float: """ - calculates the ramp for the given charging power + Adjusts the charging power to the ramping constraints. + + Args: + previous_power (float): the previous power output of the unit + power_charge (float): the charging power output of the unit + current_power (float, optional): the current power output of the unit. Defaults to 0. - :param previous_power: the previous power output of the unit - :type previous_power: float - :param power_charge: the charging power output of the unit - :type power_charge: float - :param current_power: the current power output of the unit - :type current_power: float - :return: the ramp for the given charging power - :rtype: float + Returns: + float: the ramp for the given charging power """ + if power_charge == 0: return power_charge @@ -660,16 +650,12 @@ def calculate_ramp_charge( class BaseStrategy: - """A base class for a bidding strategy. - - :param args: The arguments. - :type args: list - :param kwargs: The keyword arguments. - :type kwargs: dict - - Methods - ------- + """ + A base class for a bidding strategy. + Args: + args (list): The arguments. + kwargs (dict): The keyword arguments. """ def __init__(self, *args, **kwargs): @@ -679,38 +665,34 @@ def calculate_bids( self, unit: BaseUnit, market_config: MarketConfig, - product_tuples: list[Product], + product_tuples: List[Product], **kwargs, ) -> Orderbook: """ Calculates the bids for the next time step. - :param unit: The unit. - :type unit: BaseUnit - :param market_config: The market configuration. - :type market_config: MarketConfig - :param product_tuples: The product tuples. - :type product_tuples: list[Product] - :return: The bids - :rtype: Orderbook + Args: + unit (BaseUnit): The unit. + market_config (MarketConfig): The market configuration. + product_tuples (List[Product]): The product tuples. + + Returns: + Orderbook: The bids """ - raise NotImplementedError() def calculate_reward( self, - unit, + unit: BaseUnit, marketconfig: MarketConfig, orderbook: Orderbook, ): """ Calculates the reward for the given unit - :param unit: the unit - :type unit: BaseUnit - :param marketconfig: The market configuration. - :type marketconfig: MarketConfig - :param orderbook: The orderbook. - :type orderbook: Orderbook + Args: + unit (BaseUnit): the unit + marketconfig (MarketConfig): The market configuration. + orderbook (Orderbook): The orderbook. """ pass @@ -719,13 +701,9 @@ class LearningStrategy(BaseStrategy): """ A strategy which provides learning functionality, has a method to calculate the reward. - :param args: The arguments. - :type args: list - :param kwargs: The keyword arguments. - :type kwargs: dict - - Methods - ------- + Args: + args (list): The arguments. + kwargs (dict): The keyword arguments. """ obs_dim: int @@ -741,62 +719,26 @@ class LearningConfig(TypedDict): """ A class for the learning configuration. - :param observation_dimension: The observation dimension. - :type observation_dimension: int - - :param action_dimension: The action dimension. - :type action_dimension: int - - :param continue_learning: Whether to continue learning. - :type continue_learning: bool - - :param load_model_path: The path to the model to load. - :type load_model_path: str - - :param max_bid_price: The maximum bid price. - :type max_bid_price: float - - :param learning_mode: Whether to use learning mode. - :type learning_mode: bool - - :param algorithm: The algorithm to use. - :type algorithm: str - - :param learning_rate: The learning rate. - :type learning_rate: float - - :param training_episodes: The number of training episodes. - :type training_episodes: int - - :param episodes_collecting_initial_experience: The number of episodes collecting initial experience. - :type episodes_collecting_initial_experience: int - - :param train_freq: The training frequency. - :type train_freq: int - - :param gradient_steps: The number of gradient steps. - :type gradient_steps: int - - :param batch_size: The batch size. - :type batch_size: int - - :param gamma: The discount factor. - :type gamma: float - - :param device: The device to use. - :type device: str - - :param noise_sigma : The standard deviation of the noise. - :type noise_sigma: float - - :param noise_scale: Controls the initial strength of the noise. - :type noise_scale: int - - :param noise_dt: Determines how quickly the noise weakens over time. - :type noise_dt: int - - :param trained_actors_path: The path to the learned model to load. - :type trained_actors_path: str + Args: + observation_dimension (int): The observation dimension. + action_dimension (int): The action dimension. + continue_learning (bool): Whether to continue learning. + load_model_path (str): The path to the model to load. + max_bid_price (float): The maximum bid price. + learning_mode (bool): Whether to use learning mode. + algorithm (str): The algorithm to use. + learning_rate (float): The learning rate. + training_episodes (int): The number of training episodes. + episodes_collecting_initial_experience (int): The number of episodes collecting initial experience. + train_freq (int): The training frequency. + gradient_steps (int): The number of gradient steps. + batch_size (int): The batch size. + gamma (float): The discount factor. + device (str): The device to use. + noise_sigma (float): The standard deviation of the noise. + noise_scale (int): Controls the initial strength of the noise. + noise_dt (int): Determines how quickly the noise weakens over time. + trained_actors_path (str): The path to the learned model to load. """ observation_dimension: int diff --git a/assume/common/forecasts.py b/assume/common/forecasts.py index 5bf80d73..b4439339 100644 --- a/assume/common/forecasts.py +++ b/assume/common/forecasts.py @@ -10,16 +10,19 @@ class Forecaster: """ - A Forecaster can provide timeseries for forecasts which are derived either from existing files, - random noise, or actual forecast methods. + Forecaster represents a base class for forecasters based on existing files, + random noise, or actual forecast methods. It initializes with the provided index. It includes methods + to retrieve forecasts for specific columns, availability of units, and prices of fuel types, returning + the corresponding timeseries as pandas Series. Args: - index (pd.Series): The index of the forecasts. - This class represents a forecaster that provides timeseries for forecasts derived from existing files, - random noise, or actual forecast methods. It initializes with the provided index. It includes methods - to retrieve forecasts for specific columns, availability of units, and prices of fuel types, returning - the corresponding timeseries as pandas Series. + Example: + >>> forecaster = Forecaster(index=pd.Series([1, 2, 3])) + >>> forecast = forecaster['temperature'] + >>> print(forecast) + """ def __init__(self, index: pd.Series): @@ -40,9 +43,9 @@ def __getitem__(self, column: str) -> pd.Series: return pd.Series(0, self.index) - def get_availability(self, unit: str): + def get_availability(self, unit: str) -> pd.Series: """ - Returns the availability of a given unit. + Returns the availability of a given unit as a pandas Series based on the provided index. Args: - unit (str): The unit. @@ -50,14 +53,18 @@ def get_availability(self, unit: str): Returns: - pd.Series: The availability of the unit. - This method returns the availability of a given unit as a pandas Series based on the provided index. + Example: + >>> forecaster = Forecaster(index=pd.Series([1, 2, 3])) + >>> availability = forecaster.get_availability('unit_1') + >>> print(availability) """ return self[f"availability_{unit}"] - def get_price(self, fuel_type: str): + def get_price(self, fuel_type: str) -> pd.Series: """ - Returns the price for a given fuel type or zeros if the type does not exist. + Returns the price for a given fuel type as a pandas Series or zeros if the type does + not exist. Args: - fuel_type (str): The fuel type. @@ -65,8 +72,10 @@ def get_price(self, fuel_type: str): Returns: - pd.Series: The price of the fuel. - This method returns the price for a given fuel type as a pandas Series or zeros if the type does - not exist, based on the provided index. + Example: + >>> forecaster = Forecaster(index=pd.Series([1, 2, 3])) + >>> price = forecaster.get_price('lignite') + >>> print(price) """ return self[f"fuel_price_{fuel_type}"] @@ -74,15 +83,19 @@ def get_price(self, fuel_type: str): class CsvForecaster(Forecaster): """ - A Forecaster that reads forecasts from csv files. + This class represents a forecaster that provides timeseries for forecasts derived from existing files. + It initializes with the provided index. It includes methods to retrieve forecasts for specific columns, + availability of units, and prices of fuel types, returning the corresponding timeseries as pandas Series. Args: - index (pd.Series): The index of the forecasts. - powerplants (dict[str, pd.Series]): The power plants. - This class represents a forecaster that provides timeseries for forecasts derived from existing files. - It initializes with the provided index. It includes methods to retrieve forecasts for specific columns, - availability of units, and prices of fuel types, returning the corresponding timeseries as pandas Series. + Example: + >>> forecaster = CsvForecaster(index=pd.Series([1, 2, 3])) + >>> forecast = forecaster['temperature'] + >>> print(forecast) + """ def __init__( @@ -96,6 +109,8 @@ def __init__( def __getitem__(self, column: str) -> pd.Series: """ Returns the forecast for a given column. + If the column does not exist in the forecasts, a Series of zeros is returned. If the column contains "availability", a Series of ones is returned. + Args: - column (str): The column of the forecast. @@ -103,7 +118,6 @@ def __getitem__(self, column: str) -> pd.Series: Returns: - pd.Series: The forecast for the given column. - If the column does not exist in the forecasts, a Series of zeros is returned. If the column contains "availability", a Series of ones is returned. """ if column not in self.forecasts.columns: @@ -115,13 +129,18 @@ def __getitem__(self, column: str) -> pd.Series: def set_forecast(self, data: pd.DataFrame | pd.Series | None, prefix=""): """ Sets the forecast for a given column. + If data is a DataFrame, it sets the forecast for each column in the DataFrame. If data is + a Series, it sets the forecast for the given column. If data is None, no action is taken. + Args: - data (pd.DataFrame | pd.Series | None): The forecast data. - prefix (str): The prefix of the column. - If data is a DataFrame, it sets the forecast for each column in the DataFrame. If data is - a Series, it sets the forecast for the given column. If data is None, no action is taken. + Example: + >>> forecaster = CsvForecaster(index=pd.Series([1, 2, 3])) + >>> forecaster.set_forecast(pd.Series([22, 25, 17], name='temperature'), prefix='location_1_') + >>> print(forecaster['location_1_temperature']) """ if data is None: @@ -149,7 +168,7 @@ def calc_forecast_if_needed(self): Calculates the forecasts if they are not already calculated. This method calculates additional forecasts if they do not already exist, including - "price_EOM" and "residual_load_EOM". + "price_forecast" and "residual_load_forecast". """ cols = [] @@ -170,7 +189,9 @@ def calc_forecast_if_needed(self): def get_registered_market_participants(self, market_id): """ - Retrieves information about market participants for accurate price forecast. + This method retrieves information about market participants to make accurate price forecasts. + Currently, the functionality for using different markets and specified registration for + the price forecast is not implemented, so it returns the power plants as a DataFrame. Args: - market_id (str): The market ID. @@ -178,9 +199,6 @@ def get_registered_market_participants(self, market_id): Returns: - pd.DataFrame: The registered market participants. - This method retrieves information about market participants to make accurate price forecasts. - Currently, the functionality for using different markets and specified registration for - the price forecast is not implemented, so it returns the power plants as a DataFrame. """ self.logger.warn( @@ -190,23 +208,18 @@ def get_registered_market_participants(self, market_id): def calculate_residual_demand_forecast(self): """ - Calculates the residual demand forecast. + This method calculates the residual demand forecast by subtracting the total available power from + renewable energy (VRE) power plants from the overall demand forecast for each time step. Returns: - pd.Series: The residual demand forecast. - This method calculates the residual demand forecast by subtracting the total available power from - renewable energy (VRE) power plants from the overall demand forecast for each time step. - - The calculation involves the following steps: + Notes: 1. Selects VRE power plants (wind_onshore, wind_offshore, solar) from the powerplants data. - 2. Creates a DataFrame, vre_feed_in_df, with columns representing VRE power plants and initializes - it with zeros. + 2. Creates a DataFrame, vre_feed_in_df, with columns representing VRE power plants and initializes it with zeros. 3. Calculates the power feed-in for each VRE power plant based on its availability and maximum power. - 4. Calculates the residual demand by subtracting the total VRE power feed-in from the overall demand - forecast. + 4. Calculates the residual demand by subtracting the total VRE power feed-in from the overall demand forecast. - Returns the resulting residual demand forecast as a pandas Series. """ vre_powerplants = self.powerplants[ @@ -228,28 +241,22 @@ def calculate_residual_demand_forecast(self): def calculate_EOM_price_forecast(self): """ - Calculates the merit order price forecast for the entire time horizon. + This method calculates the merit order price forecast for the entire time horizon at once. + The method considers the infeed of renewables, residual demand, and the marginal costs of power plants to derive the price forecast. Returns: - - pd.Series: The merit order price forecast. - - This function calculates the merit order price forecast, which is provided as a price forecast to - the RL agents, for the entire time horizon at once. The method considers the infeed of renewables, - residual demand, and the marginal costs of power plants to derive the price forecast. + pd.Series: The merit order price forecast. - The calculation involves the following steps: - 1. Calculates the marginal costs for each power plant based on fuel costs, efficiencies, emissions, - and fixed costs. - 2. Sorts the power plants based on their marginal costs and availability. - 3. Computes the cumulative power of available power plants. - 4. Determines the price forecast by iterating through the sorted power plants, setting the price - for times that can still be provided with a specific technology and cheaper ones. + Note: + 1. Calculates the marginal costs for each power plant based on fuel costs, efficiencies, emissions, and fixed costs. + 2. Sorts the power plants based on their marginal costs and availability. + 3. Computes the cumulative power of available power plants. + 4. Determines the price forecast by iterating through the sorted power plants, setting the price for times that can still be provided with a specific technology and cheaper ones. TODO: - - Extend price forecasts for all markets, not just specified for the DAM. - - Consider the inclusion of storages in the price forecast calculation. + Extend price forecasts for all markets, not just specified for the DAM. + Consider the inclusion of storages in the price forecast calculation. - Returns the resulting merit order price forecast as a pandas Series. """ # calculate infeed of renewables and residual demand_df @@ -281,8 +288,10 @@ def calculate_EOM_price_forecast(self): def calculate_marginal_cost(self, pp_series: pd.Series): """ - Calculates the marginal cost of a power plant based on the fuel costs and efficiencies of the - power plant. + This method calculates the marginal cost of a power plant by taking into account the following factors: + - Fuel costs based on the fuel type and efficiency of the power plant. + - Emissions costs considering the CO2 price and emission factor of the power plant. + - Fixed costs, if specified for the power plant. Args: - pp_series (pd.Series): Series containing power plant data. @@ -290,22 +299,14 @@ def calculate_marginal_cost(self, pp_series: pd.Series): Returns: - float: The marginal cost of the power plant. - This method calculates the marginal cost of a power plant by taking into account the following - factors: - - Fuel costs based on the fuel type and efficiency of the power plant. - - Emissions costs considering the CO2 price and emission factor of the power plant. - - Fixed costs, if specified for the power plant. - The calculation involves the following steps: + Notes: 1. Determines the fuel price based on the fuel type of the power plant. 2. Calculates the fuel cost by dividing the fuel price by the efficiency of the power plant. - 3. Calculates the emissions cost based on the CO2 price and emission factor, adjusted by the - efficiency of the power plant. + 3. Calculates the emissions cost based on the CO2 price and emission factor, adjusted by the efficiency of the power plant. 4. Considers any fixed costs specified for the power plant. - 5. Aggregates the fuel cost, emissions cost, and fixed cost to obtain the marginal cost of the - power plant. + 5. Aggregates the fuel cost, emissions cost, and fixed cost to obtain the marginal cost of the power plant. - Returns the resulting marginal cost as a float value. """ fp_column = f"fuel_price_{pp_series.fuel_type}" @@ -327,13 +328,12 @@ def calculate_marginal_cost(self, pp_series: pd.Series): def save_forecasts(self, path): """ - Saves the forecasts to a csv file. + This method saves the forecasts to a csv file located at the specified path. If no + forecasts are provided, an error message is logged. Args: - path (str): The path to save the forecasts to. - This method saves the forecasts to a csv file located at the specified path. If no - forecasts are provided, an error message is logged. """ try: @@ -346,16 +346,20 @@ def save_forecasts(self, path): class RandomForecaster(CsvForecaster): """ - A forecaster that generates forecasts using random noise. - - Args: - - index (pd.Series): The index of the forecasts. - - powerplants (dict[str, pd.Series]): The power plants. - - sigma (float): The standard deviation of the noise. - This class represents a forecaster that generates forecasts using random noise. It inherits from the `CsvForecaster` class and initializes with the provided index, power plants, and standard deviation of the noise. + + Args: + index (pd.Series): The index of the forecasts. + powerplants (dict[str, pd.Series]): The power plants. + sigma (float): The standard deviation of the noise. + + Example: + >>> forecaster = RandomForecaster(index=pd.Series([1, 2, 3])) + >>> forecaster.set_forecast(pd.Series([22, 25, 17], name='temperature'), prefix='location_1_') + >>> print(forecaster['location_1_temperature']) + """ def __init__( @@ -371,7 +375,9 @@ def __init__( def __getitem__(self, column: str) -> pd.Series: """ - Returns the forecast for a given column modified by random noise. + This method returns the forecast for a given column as a pandas Series modified + by random noise based on the provided standard deviation of the noise and the existing + forecasts. If the column does not exist in the forecasts, a Series of zeros is returned. Args: - column (str): The column of the forecast. @@ -379,9 +385,6 @@ def __getitem__(self, column: str) -> pd.Series: Returns: - pd.Series: The forecast modified by random noise. - This method returns the forecast for a given column as a pandas Series modified - by random noise based on the provided standard deviation of the noise and the existing - forecasts. If the column does not exist in the forecasts, a Series of zeros is returned. """ if column not in self.forecasts.columns: @@ -392,7 +395,13 @@ def __getitem__(self, column: str) -> pd.Series: class NaiveForecast(Forecaster): """ - A forecaster that generates forecasts using naive methods. + This class represents a forecaster that generates forecasts using naive methods. It inherits + from the `Forecaster` class and initializes with the provided index and optional parameters + for availability, fuel price, CO2 price, demand, and price forecast. + + If the optional parameters are constant values, they are converted to pandas Series with the + provided index. If the optional parameters are lists, they are converted to pandas Series with + the provided index and the corresponding values. Args: - index (pd.Series): The index of the forecasts. @@ -404,9 +413,12 @@ class NaiveForecast(Forecaster): - demand (float | list): The demand. - price_forecast (float | list): The price forecast. - This class represents a forecaster that generates forecasts using naive methods. It inherits - from the `Forecaster` class and initializes with the provided index and optional parameters - for availability, fuel price, CO2 price, demand, and price forecast. + Example: + >>> forecaster = NaiveForecast(demand=100, co2_price=10, fuel_price=10, availability=1, price_forecast=50) + >>> print(forecaster['demand']) + + >>> forecaster = NaiveForecast(index=pd.Series([1, 2, 3]), demand=[100, 200, 300], co2_price=[10, 20, 30]) + >>> print(forecaster["demand"][2]) """ def __init__( @@ -429,7 +441,10 @@ def __init__( def __getitem__(self, column: str) -> pd.Series: """ - Get the forecasted values for a specific column. + This method retrieves the forecasted values for a specific column based on the + provided parameters such as availability, fuel price, CO2 price, demand, and price + forecast. If the column matches one of the predefined parameters, the corresponding + value is returned as a pandas Series. If the column does not match, a Series of zeros is returned. Args: - column (str): The column for which forecasted values are requested. @@ -437,10 +452,6 @@ def __getitem__(self, column: str) -> pd.Series: Returns: - pd.Series: The forecasted values for the specified column. - This method retrieves the forecasted values for a specific column based on the - provided parameters such as availability, fuel price, CO2 price, demand, and price - forecast. If the column matches one of the predefined parameters, the corresponding - value is returned as a pandas Series. If the column does not match, a Series of zeros is returned. """ if "availability" in column: diff --git a/assume/common/market_objects.py b/assume/common/market_objects.py index 78f99cde..7cbd9323 100644 --- a/assume/common/market_objects.py +++ b/assume/common/market_objects.py @@ -24,29 +24,20 @@ class OnlyHours(NamedTuple): end_hour: int -# describes an order which can be either generation (volume > 0) or demand (volume < 0) class Order(TypedDict): """ Describes an order which can be either generation (volume > 0) or demand (volume < 0) - :param bid_id: the id of the bid - :type bid_id: str - :param start_time: the start time of the order - :type start_time: datetime - :param end_time: the end time of the order - :type end_time: datetime - :param volume: the volume of the order - :type volume: float - :param price: the price of the order - :type price: float - :param accepted_volume: the accepted volume of the order - :type accepted_volume: float - :param accepted_price: the accepted price of the order - :type accepted_price: float - :param only_hours: tuple of hours from which this order is available, on multi day products - :type only_hours: OnlyHours | None - :param agent_id: the id of the agent - :type agent_id: str + Args: + bid_id (str): the id of the bid + start_time (datetime): the start time of the order + end_time (datetime): the end time of the order + volume (Number | dict[datetime, Number]): the volume of the order (positive if generation) + accepted_volume (Number | dict[datetime, Number]): the accepted volume of the order + price (Number): the price of the order + accepted_price (Number | dict[datetime, Number]): the accepted price of the order + agent_id (str): the id of the agent + only_hours (OnlyHours | None): tuple of hours from which this order is available, on multi day products """ bid_id: str @@ -71,40 +62,29 @@ class MarketProduct: """ Describes the configuration of a market product which is available at a market. - :param duration: the duration of the product - :type duration: rd | rr.rrule - :param count: how many future durations can be traded, must be >= 1 - :type count: int - :param first_delivery: when does the first delivery begin, in relation to market start - :type first_delivery: rd - :param only_hours: tuple of hours from which this order is available, on multi day products - :type only_hours: OnlyHours | None - :param eligible_lambda_function: lambda function which determines if an agent is eligible to trade this product - :type eligible_lambda_function: eligible_lambda | None + Args: + duration (rd | rr.rrule): the duration of the product + count (int): how many future durations can be traded, must be >= 1 + first_delivery (rd): when does the first delivery begin, in relation to market start + only_hours (OnlyHours | None): tuple of hours from which this order is available, on multi day products + eligible_lambda_function (eligible_lambda | None): lambda function which determines if an agent is eligible to trade this product """ - duration: rd | rr.rrule # quarter-hourly, half-hourly, hourly, 4hourly, daily, weekly, monthly, quarter-yearly, yearly - count: int # how many future durations can be traded, must be >= 1 - # count can also be given as a rrule with until - first_delivery: rd = ( - rd() - ) # when does the first delivery begin, in relation to market start - # this should be a multiple of duration + duration: rd | rr.rrule + count: int + first_delivery: rd = rd() only_hours: OnlyHours | None = None - # e.g. (8,20) - for peak trade, (20, 8) for off-peak, none for base eligible_lambda_function: eligible_lambda | None = None class Product(NamedTuple): """ - an actual product with start and end - - :param start: the start time of the product - :type start: datetime - :param end: the end time of the product - :type end: datetime - :param only_hours: tuple of hours from which this order is available, on multi day products - :type only_hours: OnlyHours | None + An actual product with start and end. + + Args: + start (datetime): the start time of the product + end (datetime): the end time of the product + only_hours (OnlyHours | None): tuple of hours from which this order is available, on multi day products """ start: datetime @@ -117,60 +97,38 @@ class MarketConfig: """ Describes the configuration of a market. - :param name: the name of the market - :type name: str - :param addr: the address of the market - :type addr: str | None - :param aid: automatic id of the market - :type aid: str | None - :param opening_hours: the opening hours of the market - :type opening_hours: rr.rrule - :param opening_duration: the duration of the opening hours - :type opening_duration: timedelta - :param market_mechanism: name of method used for clearing - :type market_mechanism: str - :param market_products: list of available products to be traded at the market - :type market_products: list[MarketProduct] - :param product_type: energy or capacity or heat - :type product_type: str - :param maximum_bid_volume: the maximum valid bid volume of the market - :type maximum_bid_volume: float - :param maximum_bid_price: the maximum bid price of the market - :type maximum_bid_price: float - :param minimum_bid_price: the minimum bid price of the market - :type minimum_bid_price: float - :param maximum_gradient: max allowed change between bids - :type maximum_gradient: float | None - :param additional_fields: additional fields of the market - :type additional_fields: list[str] - :param volume_unit: the volume unit of the market (e.g. MW) - :type volume_unit: str - :param volume_tick: step increments of volume (e.g. 0.1) - :type volume_tick: float | None - :param price_unit: the price unit of the market (e.g. €/MWh) - :type price_unit: str - :param price_tick: step increments of price (e.g. 0.1) - :type price_tick: float | None - :param supports_get_unmatched: whether the market supports get unmatched - :type supports_get_unmatched: bool - :param eligible_obligations_lambda: lambda function which determines if an agent is eligible to trade this product - :type eligible_obligations_lambda: eligible_lambda | None + Args: + name (str): the name of the market + opening_hours (rr.rrule): the opening hours of the market + opening_duration (timedelta): the duration of the opening hours + market_mechanism (str): name of method used for clearing + market_products (list[MarketProduct]): list of available products to be traded at the market + product_type (str): energy or capacity or heat + maximum_bid_volume (float | None): the maximum valid bid volume of the market + maximum_bid_price (float | None): the maximum bid price of the market + minimum_bid_price (float): the minimum bid price of the market + maximum_gradient (float | None): max allowed change between bids + additional_fields (list[str]): additional fields of the market + volume_unit (str): the volume unit of the market (e.g. MW) + volume_tick (float | None): step increments of volume (e.g. 0.1) + price_unit (str): the price unit of the market (e.g. €/MWh) + price_tick (float | None): step increments of price (e.g. 0.1) + supports_get_unmatched (bool): whether the market supports get unmatched + eligible_obligations_lambda (eligible_lambda): lambda function which determines if an agent is eligible to trade this product + addr (str): the address of the market + aid (str): automatic id of the market """ - name: str - addr = None - aid = None - - # continuous markets are clearing just very fast and keep unmatched orders between clearings - opening_hours: rr.rrule # dtstart is start/introduction of market - opening_duration: timedelta - market_mechanism: str + name: str = "market" + opening_hours: rr.rrule = rr.rrule(rr.HOURLY) + opening_duration: timedelta = timedelta(hours=1) + market_mechanism: str = "pay_as_clear" market_products: list[MarketProduct] = field(default_factory=list) product_type: str = "energy" maximum_bid_volume: float | None = 2000.0 maximum_bid_price: float | None = 3000.0 minimum_bid_price: float = -500.0 - maximum_gradient: float = None # very specific - should be in market clearing + maximum_gradient: float | None = None additional_fields: list[str] = field(default_factory=list) volume_unit: str = "MW" volume_tick: float | None = None # steps in which the amount can be increased @@ -178,25 +136,21 @@ class MarketConfig: price_tick: float | None = None # steps in which the price can be increased supports_get_unmatched: bool = False eligible_obligations_lambda: eligible_lambda = lambda x: True - # lambda: agent.payed_fee - # obligation should be time-based - # only allowed to bid regelenergie if regelleistung was accepted in the same hour for this agent by the market + + addr: str = " " + aid: str = " " class OpeningMessage(TypedDict): """ - Message which is sent from the market to participating agent to open a market - - :param context: the context of the message - :type context: str - :param market_id: the id of the market - :type market_id: str - :param start_time: the start time of the market - :type start_time: float - :param end_time: the stop time of the market - :type end_time: float - :param products: list of products which are available at the market to be traded - :type products: list[Product] + Message which is sent from the market to participating agent to open a market. + + Args: + context (str): the context of the message + market_id (str): the id of the market + start_time (float): the start time of the market + end_time (float): the stop time of the market + products (list[Product]): list of products which are available at the market to be traded """ context: str @@ -208,16 +162,13 @@ class OpeningMessage(TypedDict): class ClearingMessage(TypedDict): """ - Message which is sent from the market to agents to clear a market - - :param context: the context of the message - :type context: str - :param market_id: the id of the market - :type market_id: str - :param accepted_orders: the orders accepted by the market - :type accepted_orders: Orderbook - :param rejected_orders: the orders rejected by the market - :type rejected_orders: Orderbook + Message which is sent from the market to agents to clear a market. + + Args: + context (str): the context of the message + market_id (str): the id of the market + accepted_orders (Orderbook): the orders accepted by the market + rejected_orders (Orderbook): the orders rejected by the market """ context: str @@ -227,24 +178,62 @@ class ClearingMessage(TypedDict): class OrderBookMessage(TypedDict): + """ + Message containing the order book of a market. + + Args: + context (str): the context of the message + market_id (str): the id of the market + orderbook (Orderbook): the order book of the market + """ + context: str market_id: str orderbook: Orderbook class RegistrationMessage(TypedDict): + """ + Message for agent registration at a market. + + Args: + context (str): the context of the message + market_id (str): the id of the market + information (dict): additional information for registration + """ + context: str market_id: str information: dict class RegistrationReplyMessage(TypedDict): + """ + Reply message for agent registration at a market. + + Args: + context (str): the context of the message + market_id (str): the id of the market + accepted (bool): whether the registration is accepted + """ + context: str market_id: str accepted: bool class DataRequestMessage(TypedDict): + """ + Message for requesting data from a market. + + Args: + context (str): the context of the message + market_id (str): the id of the market + metric (str): the specific metric being requested + start_time (datetime): the start time of the data request + end_time (datetime): the end time of the data request + """ + context: str market_id: str metric: str @@ -254,28 +243,36 @@ class DataRequestMessage(TypedDict): class MetaDict(TypedDict): """ - Message Meta of a FIPA ACL Message - http://www.fipa.org/specs/fipa00061/SC00061G.html#_Toc26669700 + Message Meta of a FIPA ACL Message. + + Args: + sender_addr (str | list): the address of the sender + sender_id (str): the id of the sender + reply_to (str): to which agent follow up messages should be sent + conversation_id (str): the id of the conversation + performative (str): the performative of the message + protocol (str): the protocol used + language (str): the language used + encoding (str): the encoding used + ontology (str): the ontology used + reply_with (str): what the answer should contain as in_reply_to + in_reply_to (str): str used to reference an earlier action + reply_by (str): latest time to accept replies """ sender_addr: str | list sender_id: str - reply_to: str # to which agent follow up messages should be sent + reply_to: str conversation_id: str performative: str protocol: str language: str encoding: str ontology: str - reply_with: str # what the answer should contain as in_reply_to - in_reply_to: str # str used to reference an earlier action - reply_by: str # latest time to accept replies + reply_with: str + in_reply_to: str + reply_by: str -# Class for a Smart Contract which can contain something like: -# - Contract for Differences (CfD) -> based on market result -# - Market Subvention -> based on market result -# - Power Purchase Agreements (PPA) -> A buys everything B generates for price x -# - Swing Contract -> contract_type = Callable[[Agent, Agent], None] market_contract_type = Callable[[Agent, Agent, list], None] diff --git a/assume/common/outputs.py b/assume/common/outputs.py index 079bac9b..49cb0a87 100644 --- a/assume/common/outputs.py +++ b/assume/common/outputs.py @@ -26,18 +26,15 @@ class WriteOutput(Role): """ Initializes an instance of the WriteOutput class. - :param simulation_id: The ID of the simulation as a unique calssifier. - :type simulation_id: str - :param start: The start datetime of the simulation run. - :type start: datetime - :param end: The end datetime of the simulation run. - :type end: datetime - :param db_engine: The database engine. Defaults to None. - :type db_engine: optional - :param export_csv_path: The path for exporting CSV files, no path results in not writing the csv. Defaults to "". - :type export_csv_path: str, optional - :param save_frequency_hours: The frequency in hours for storing data in the db and/or csv files. Defaults to None. - :type save_frequency_hours: int + Args: + simulation_id (str): The ID of the simulation as a unique classifier. + start (datetime): The start datetime of the simulation run. + end (datetime): The end datetime of the simulation run. + db_engine: The database engine. Defaults to None. + export_csv_path (str, optional): The path for exporting CSV files, no path results in not writing the csv. Defaults to "". + save_frequency_hours (int): The frequency in hours for storing data in the db and/or csv files. Defaults to None. + learning_mode (bool, optional): Indicates if the simulation is in learning mode. Defaults to False. + evaluation_mode (bool, optional): Indicates if the simulation is in evaluation mode. Defaults to False. """ def __init__( @@ -89,12 +86,12 @@ def __init__( if self.db is not None: self.delete_db_scenario(self.simulation_id) - def delete_db_scenario(self, simulation_id): + def delete_db_scenario(self, simulation_id: str): """ Deletes all data from the database for the given simulation id. - :param simulation_id: The ID of the simulation as a unique calssifier. - :type simulation_id: str + Args: + simulation_id (str): The ID of the simulation as a unique classifier. """ # Loop throuph all database tables @@ -113,6 +110,9 @@ def delete_db_scenario(self, simulation_id): logger.debug("deleted %s rows from %s", rowcount, table_name) def del_similar_runs(self): + """ + Deletes all similar runs from the database based on the simulation ID. This ensures that we overwrite simulations results when restarting one. Please note that a simulation which you also want to keep need to be assigned anew ID. + """ query = text("select distinct simulation from rl_params") try: @@ -148,15 +148,13 @@ def setup(self): ) self.context.schedule_recurrent_task(self.store_dfs, recurrency_task) - def handle_message(self, content, meta: MetaDict): + def handle_message(self, content: dict, meta: MetaDict): """ Handles the incoming messages and performs corresponding actions. - - :param content: The content of the message. - :type content: dict - :param meta: The metadata associated with the message. (not needed yet) - :type meta: any + Args: + content (dict): The content of the message. + meta (MetaDict): The metadata associated with the message. """ if content.get("type") == "store_order_book": @@ -177,12 +175,12 @@ def handle_message(self, content, meta: MetaDict): elif content.get("type") == "rl_learning_params": self.write_rl_params(content.get("data")) - def write_rl_params(self, rl_params): + def write_rl_params(self, rl_params: dict): """ - Writes the RL parameters to the corresponding data frame. + Writes the RL parameters such as reward, regret, and profit to the corresponding data frame. - :param rl_params: The RL parameters. - :type rl_params: any + Args: + rl_params (dict): The RL parameters. """ df = pd.DataFrame.from_records(rl_params, index="datetime") @@ -196,12 +194,12 @@ def write_rl_params(self, rl_params): self.write_dfs["rl_params"].append(df) - def write_market_results(self, market_meta): + def write_market_results(self, market_meta: dict): """ Writes market results to the corresponding data frame. - :param market_meta: The market metadata, which includes the clearing price and volume. - :type market_meta: any + Args: + market_meta (dict): The market metadata, which includes the clearing price and volume. """ df = pd.DataFrame(market_meta) @@ -212,8 +210,7 @@ def write_market_results(self, market_meta): async def store_dfs(self): """ - Stores the data frames to CSV files and the database. - Is scheduled as a recurrent task based on the frequency. + Stores the data frames to CSV files and the database. Is scheduled as a recurrent task based on the frequency. """ for table in self.write_dfs.keys(): @@ -245,10 +242,11 @@ async def store_dfs(self): def check_columns(self, table: str, df: pd.DataFrame): """ - If a simulation before has been started which does not include an additional field - we try to add the field. - For now, this only works for float and text. - An alternative which finds the correct types would be to use + Checks and adds columns to the database table if necessary. + + Args: + table (str): The name of the database table. + df (pd.DataFrame): The DataFrame to be checked. """ with self.db.begin() as db: # Read table into Pandas DataFrame @@ -265,12 +263,12 @@ def check_columns(self, table: str, df: pd.DataFrame): except Exception: logger.exception("Error converting column") - def check_for_tensors(self, data): + def check_for_tensors(self, data: pd.Series): """ Checks if the data contains tensors and converts them to floats. - :param data: The data to be checked. - :type data: any + Args: + data (pd.Series): The data to be checked. """ try: import torch as th @@ -284,15 +282,13 @@ def check_for_tensors(self, data): return data - def write_market_orders(self, market_orders, market_id): + def write_market_orders(self, market_orders: any, market_id: str): """ Writes market orders to the corresponding data frame. - Append new data until it is written to db and csv with store_df function. - :param market_orders: The market orders. - :type market_orders: any - :param market_id: The id of the market. - :type market_id: str + Args: + market_orders (any): The market orders. + market_id (str): The id of the market. """ # check if market results list is empty and skip the funktion and raise a warning if not market_orders: @@ -311,11 +307,10 @@ def write_market_orders(self, market_orders, market_id): def write_units_definition(self, unit_info: dict): """ - Writes unit definitions to the corresponding data frame and directly store it in db and csv. - Since that is only done once, no need for recurrent scheduling arises. + Writes unit definitions to the corresponding data frame and directly stores it in the database and CSV. - :param unit_info: The unit information. - :type unit_info: dict + Args: + unit_info (dict): The unit information. """ table_name = unit_info["unit_type"] + "_meta" @@ -330,32 +325,33 @@ def write_units_definition(self, unit_info: dict): self.write_dfs[table_name].append(pd.DataFrame(u_info).T) - def write_market_dispatch(self, data): + def write_market_dispatch(self, data: any): """ - Writes the planned dispatch of the units after the market clearing to a csv and db - In the case that we have no portfolio optimisation this equals the resulting bids. + Writes the planned dispatch of the units after the market clearing to a CSV and database. - :param data: The records to be put into the table. Formatted like, "datetime, power, market_id, unit_id" - :type data: any + Args: + data (any): The records to be put into the table. Formatted like, "datetime, power, market_id, unit_id". """ df = pd.DataFrame(data, columns=["datetime", "power", "market_id", "unit_id"]) if not df.empty: df["simulation"] = self.simulation_id self.write_dfs["market_dispatch"].append(df) - def write_unit_dispatch(self, data): + def write_unit_dispatch(self, data: any): """ - Writes the actual dispatch of the units to a csv and db + Writes the actual dispatch of the units to a CSV and database. - :param data: The records to be put into the table. Formatted like, "datetime, power, market_id, unit_id" - :type data: any + Args: + data (any): The records to be put into the table. Formatted like, "datetime, power, market_id, unit_id". """ data["simulation"] = self.simulation_id self.write_dfs["unit_dispatch"].append(data) async def on_stop(self): """ - This function makes it possible to calculate Key Performance Indicators + This function makes it possible to calculate Key Performance Indicators. + It is called when the simulation is finished. It collects average price, total cost, total volume and capacity factors + and uses them to calculate the KPIs. The KPIs are then stored in the database and CSV files. """ await super().on_stop() @@ -415,6 +411,12 @@ async def on_stop(self): df.to_sql("kpis", self.db, if_exists="append", index=None) def get_sum_reward(self): + """ + Retrieves the total reward for each learning unit. + + Returns: + np.array: The total reward for each learning unit. + """ query = text( f"select unit, SUM(reward) FROM rl_params where simulation='{self.simulation_id}' GROUP BY unit" ) diff --git a/assume/common/scenario_loader.py b/assume/common/scenario_loader.py index ec218b7a..4578cd59 100644 --- a/assume/common/scenario_loader.py +++ b/assume/common/scenario_loader.py @@ -4,6 +4,7 @@ import logging from datetime import datetime +from typing import Optional, Tuple import dateutil.rrule as rr import numpy as np @@ -30,21 +31,27 @@ def load_file( path: str, config: dict, file_name: str, - index: pd.DatetimeIndex = None, + index: Optional[pd.DatetimeIndex] = None, ) -> pd.DataFrame: """ - This function loads a csv file from a given path and returns a dataframe. - - :param path: the path to the csv file - :type path: str - :param config: the config file - :type config: dict - :param file_name: the name of the csv file - :type file_name: str - :param index: the index of the dataframe - :type index: pd.DatetimeIndex - :return: the dataframe - :rtype: pd.DataFrame + Loads a csv file from the given path and returns a dataframe. + + The config file is used to check if the file name is specified in the config file, + otherwise defaults to the file name. + + If the index is specified, the dataframe is resampled to the index, if possible. If not, None is returned. + + Args: + path (str): The path to the csv file. + config (dict): The config file containing file mappings. + file_name (str): The name of the csv file. + index (pd.DatetimeIndex, optional): The index of the dataframe. Defaults to None. + + Returns: + pd.DataFrame: The dataframe containing the loaded data. + + Raises: + FileNotFoundError: If the specified file is not found, returns None. """ df = None @@ -105,15 +112,15 @@ def load_file( return None -def convert_to_rrule_freq(string): +def convert_to_rrule_freq(string: str) -> Tuple[int, int]: """ - This function converts a string to a rrule frequency and interval. - The string should be in the format of "1h" or "1d" or "1w". + Convert a string to a rrule frequency and interval. + + Args: + string (str): The string to be converted. Should be in the format of "1h" or "1d" or "1w". - :param string: the string to be converted - :type string: str - :return: the rrule frequency and interval - :rtype: tuple[int, int] + Returns: + Tuple[int, int]: The rrule frequency and interval. """ freq = freq_map[string[-1]] interval = int(string[:-1]) @@ -125,20 +132,18 @@ def make_market_config( market_params: dict, world_start: datetime, world_end: datetime, -): +) -> MarketConfig: """ - This function creates a market config from a given dictionary. - - :param id: the id of the market - :type id: str - :param market_params: the market parameters - :type market_params: dict - :param world_start: the start time of the world - :type world_start: datetime - :param world_end: the end time of the world - :type world_end: datetime - :return: the market config - :rtype: MarketConfig + Create a market config from a given dictionary. + + Args: + id (str): The id of the market. + market_params (dict): The market parameters. + world_start (datetime): The start time of the world. + world_end (datetime): The end time of the world. + + Returns: + MarketConfig: The market config. """ freq, interval = convert_to_rrule_freq(market_params["opening_frequency"]) start = market_params.get("start_date") @@ -191,19 +196,20 @@ def add_units( unit_type: str, world: World, forecaster: Forecaster, -): +) -> None: """ - This function adds units to the world from a given dataframe. + Add units to the world from a given dataframe. + The callback is used to adjust unit_params depending on the unit_type, before adding the unit to the world. - :param units_df: the dataframe containing the units - :type units_df: pd.DataFrame - :param unit_type: the type of the unit - :type unit_type: str - :param world: the world - :type world: World - :param forecaster: the forecaster - :type forecaster: Forecaster + Args: + units_df (pd.DataFrame): The dataframe containing the units. + unit_type (str): The type of the unit. + world (World): The world to which the units will be added. + forecaster (Forecaster): The forecaster used for adding the units. + + Returns: + None """ if units_df is None: return @@ -239,17 +245,26 @@ async def load_scenario_folder_async( episode: int = 0, eval_episode: int = 0, trained_actors_path: str = "", -): - """Load a scenario from a given path. Raises: ValueError: If the scenario or study case is not found. - - :param world: The world. - :type world: World - :param inputs_path: Path to the inputs folder. - :type inputs_path: str - :param scenario: Name of the scenario. - :type scenario: str - :param study_case: Name of the study case. - :type study_case: str +) -> None: + """ + Load a scenario from a given path. + + This function loads a scenario within a specified study case from a given path, setting up the world environment for simulation and learning. + + Args: + world (World): An instance of the World class representing the simulation environment. + inputs_path (str): The path to the folder containing input files necessary for the scenario. + scenario (str): The name of the scenario to be loaded. + study_case (str): The specific study case within the scenario to be loaded. + perform_learning (bool, optional): A flag indicating whether learning should be performed. Defaults to True. + perform_evaluation (bool, optional): A flag indicating whether evaluation should be performed. Defaults to False. + episode (int, optional): The episode number for learning. Defaults to 0. + eval_episode (int, optional): The episode number for evaluation. Defaults to 0. + trained_actors_path (str, optional): The path to the trained actors. Defaults to an empty string. + + Raises: + ValueError: If the specified scenario or study case is not found in the provided inputs. + """ # load the config file @@ -464,28 +479,91 @@ async def load_scenario_folder_async( raise ValueError("No RL units/strategies were provided!") +def load_scenario_folder( + world: World, + inputs_path: str, + scenario: str, + study_case: str, + perform_learning: bool = True, + perform_evaluation: bool = False, + episode: int = 1, + eval_episode: int = 1, + trained_actors_path="", +): + """ + Load a scenario from a given path. + + This function loads a scenario within a specified study case from a given path, setting up the world environment for simulation and learning. + + Args: + world (World): An instance of the World class representing the simulation environment. + inputs_path (str): The path to the folder containing input files necessary for the scenario. + scenario (str): The name of the scenario to be loaded. + study_case (str): The specific study case within the scenario to be loaded. + perform_learning (bool, optional): A flag indicating whether learning should be performed. Defaults to True. + perform_evaluation (bool, optional): A flag indicating whether evaluation should be performed. Defaults to False. + episode (int, optional): The episode number for learning. Defaults to 0. + eval_episode (int, optional): The episode number for evaluation. Defaults to 0. + trained_actors_path (str, optional): The path to the trained actors. Defaults to an empty string. + + Raises: + ValueError: If the specified scenario or study case is not found in the provided inputs. + + Example: + >>> load_scenario_folder( + world=world, + inputs_path="/path/to/inputs", + scenario="scenario_name", + study_case="study_case_name", + perform_learning=True, + perform_evaluation=False, + episode=1, + eval_episode=1, + trained_actors_path="", + ) + + Notes: + - The function sets up the world environment based on the provided inputs and configuration files. + - If `perform_learning` is set to True, the function initializes the learning mode with the specified episode number. + - If `perform_evaluation` is set to True, the function performs evaluation using the specified evaluation episode number. + - The function utilizes the specified inputs to configure the simulation environment, including market parameters, unit operators, and forecasting data. + - After calling this function, the world environment is prepared for further simulation and analysis. + + """ + world.loop.run_until_complete( + load_scenario_folder_async( + world=world, + inputs_path=inputs_path, + scenario=scenario, + study_case=study_case, + perform_learning=perform_learning, + perform_evaluation=perform_evaluation, + episode=episode, + eval_episode=eval_episode, + trained_actors_path=trained_actors_path, + ) + ) + + async def async_load_custom_units( world: World, inputs_path: str, scenario: str, file_name: str, unit_type: str, -): - """ - This function loads custom units from a given path. - - :param world: the world - :type world: World - :param inputs_path: the path to the inputs folder - :type inputs_path: str - :param scenario: the name of the scenario - :type scenario: str - :param file_name: the name of the file - :type file_name: str - :param unit_type: the type of the unit - :type unit_type: str +) -> None: """ + Load custom units from a given path. + This function loads custom units of a specified type from a given path within a scenario, adding them to the world environment for simulation. + + Args: + world (World): An instance of the World class representing the simulation environment. + inputs_path (str): The path to the folder containing input files necessary for the custom units. + scenario (str): The name of the scenario from which the custom units are to be loaded. + file_name (str): The name of the file containing the custom units. + unit_type (str): The type of the custom units to be loaded. + """ path = f"{inputs_path}/{scenario}" custom_units = load_file( @@ -516,7 +594,34 @@ def load_custom_units( scenario: str, file_name: str, unit_type: str, -): +) -> None: + """ + Load custom units from a given path. + + This function loads custom units of a specified type from a given path within a scenario, adding them to the world environment for simulation. + + Args: + world (World): An instance of the World class representing the simulation environment. + inputs_path (str): The path to the folder containing input files necessary for the custom units. + scenario (str): The name of the scenario from which the custom units are to be loaded. + file_name (str): The name of the file containing the custom units. + unit_type (str): The type of the custom units to be loaded. + + Example: + >>> load_custom_units( + world=world, + inputs_path="/path/to/inputs", + scenario="scenario_name", + file_name="custom_units.csv", + unit_type="custom_type" + ) + + Notes: + - The function loads custom units from the specified file within the given scenario and adds them to the world environment for simulation. + - If the specified custom units file is not found, a warning is logged. + - Each unique unit operator in the custom units is added to the world's unit operators. + - The custom units are added to the world environment based on their type for use in simulations. + """ world.loop.run_until_complete( async_load_custom_units( world=world, @@ -528,46 +633,27 @@ def load_custom_units( ) -def load_scenario_folder( - world: World, - inputs_path: str, - scenario: str, - study_case: str, - perform_learning: bool = True, - perform_evaluation: bool = False, - episode: int = 1, - eval_episode: int = 1, - trained_actors_path="", -): +def run_learning( + world: World, inputs_path: str, scenario: str, study_case: str +) -> None: """ - Load a scenario from a given path. - - :param world: The world. - :type world: World - :param inputs_path: Path to the inputs folder. - :type inputs_path: str - :param scenario: Name of the scenario. - :type scenario: str - :param study_case: Name of the study case. - :type study_case: str + Train Deep Reinforcement Learning (DRL) agents to act in a simulated market environment. + + This function runs multiple episodes of simulation to train DRL agents, performs evaluation, and saves the best runs. It maintains the buffer and learned agents in memory to avoid resetting them with each new run. + + Args: + world (World): An instance of the World class representing the simulation environment. + inputs_path (str): The path to the folder containing input files necessary for the simulation. + scenario (str): The name of the scenario for the simulation. + study_case (str): The specific study case for the simulation. + + Note: + - The function uses a ReplayBuffer to store experiences for training the DRL agents. + - It iterates through training episodes, updating the agents and evaluating their performance at regular intervals. + - Initial exploration is active at the beginning and is disabled after a certain number of episodes to improve the performance of DRL algorithms. + - Upon completion of training, the function performs an evaluation run using the best policy learned during training. + - The best policies are chosen based on the average reward obtained during the evaluation runs, and they are saved for future use. """ - world.loop.run_until_complete( - load_scenario_folder_async( - world=world, - inputs_path=inputs_path, - scenario=scenario, - study_case=study_case, - perform_learning=perform_learning, - perform_evaluation=perform_evaluation, - episode=episode, - eval_episode=eval_episode, - trained_actors_path=trained_actors_path, - ) - ) - - -def run_learning(world: World, inputs_path: str, scenario: str, study_case: str): - # initiate buffer for rl agent from assume.reinforcement_learning.buffer import ReplayBuffer # remove csv path so that nothing is written while learning @@ -576,7 +662,7 @@ def run_learning(world: World, inputs_path: str, scenario: str, study_case: str) best_reward = -1e10 buffer = ReplayBuffer( - buffer_size=int(5e5), + buffer_size=int(world.learning_config.get("replay_buffer_size", 5e5)), obs_dim=world.learning_role.obs_dim, act_dim=world.learning_role.act_dim, n_rl_units=len(world.learning_role.rl_strats), diff --git a/assume/common/scenario_loader_amiris.py b/assume/common/scenario_loader_amiris.py index ea5cdb51..e9173988 100644 --- a/assume/common/scenario_loader_amiris.py +++ b/assume/common/scenario_loader_amiris.py @@ -89,11 +89,17 @@ def add_agent_to_world( match agent["Type"]: case "EnergyExchange": market_config = MarketConfig( - f"Market_{agent['Id']}", - rr.rrule(rr.HOURLY, interval=1, dtstart=world.start, until=world.end), - timedelta(hours=1), - translate_clearing[agent["Attributes"]["DistributionMethod"]], - [MarketProduct(timedelta(hours=1), 1, timedelta(hours=1))], + name=f"Market_{agent['Id']}", + opening_hours=rr.rrule( + rr.HOURLY, interval=1, dtstart=world.start, until=world.end + ), + opening_duration=timedelta(hours=1), + market_mechanism=translate_clearing[ + agent["Attributes"]["DistributionMethod"] + ], + market_products=[ + MarketProduct(timedelta(hours=1), 1, timedelta(hours=1)) + ], maximum_bid_volume=99999, ) world.add_market_operator(f"Market_{agent['Id']}") diff --git a/assume/common/units_operator.py b/assume/common/units_operator.py index 28c17036..b3bd95a7 100644 --- a/assume/common/units_operator.py +++ b/assume/common/units_operator.py @@ -34,10 +34,9 @@ class UnitsOperator(Role): The UnitsOperator is the agent that manages the units. It receives the opening hours of the market and sends back the bids for the market. - :param available_markets: the available markets - :type available_markets: list[MarketConfig] - :param opt_portfolio: optimized portfolio strategy - :type opt_portfolio: tuple[bool, BaseStrategy] | None + Args: + available_markets (list[MarketConfig]): The available markets. + opt_portfolio (tuple[bool, BaseStrategy] | None, optional): Optimized portfolio strategy. Defaults to None. """ def __init__( @@ -99,12 +98,12 @@ def setup(self): async def add_unit( self, unit: BaseUnit, - ): + ) -> None: """ Create a unit. - :param unit: the unit to be added - :type unit: BaseUnit + Args: + unit (BaseUnit): The unit to be added. """ self.units[unit.id] = unit @@ -127,22 +126,25 @@ async def add_unit( }, ) - def participate(self, market: MarketConfig): + def participate(self, market: MarketConfig) -> bool: """ Method which decides if we want to participate on a given Market. - This always returns true for now + This always returns true for now. + + Args: + market (MarketConfig): The market to participate in. - :param market: the market to participate in - :type market: MarketConfig + Returns: + bool: True if participate, False otherwise. """ return True - async def register_market(self, market: MarketConfig): + async def register_market(self, market: MarketConfig) -> None: """ Register a market. - :param market: the market to register - :type market: MarketConfig + Args: + market (MarketConfig): The market to register. """ await self.context.send_acl_message( @@ -161,32 +163,26 @@ async def register_market(self, market: MarketConfig): ), logger.debug(f"{self.id} sent market registration to {market.name}") - def handle_opening(self, opening: OpeningMessage, meta: MetaDict): + def handle_opening(self, opening: OpeningMessage, meta: MetaDict) -> None: """ - When we receive an opening from the market, we schedule sending back - our list of orders as a response + When we receive an opening from the market, we schedule sending back our list of orders as a response. - :param opening: the opening message - :type opening: OpeningMessage - :param meta: the meta data of the market - :type meta: MetaDict + Args: + opening (OpeningMessage): The opening message. + meta (MetaDict): The meta data of the market. """ logger.debug( f'{self.id} received opening from: {opening["market_id"]} {opening["start_time"]} until: {opening["end_time"]}.' ) self.context.schedule_instant_task(coroutine=self.submit_bids(opening, meta)) - def handle_market_feedback(self, content: ClearingMessage, meta: MetaDict): + def handle_market_feedback(self, content: ClearingMessage, meta: MetaDict) -> None: """ - handles the feedback which is received from a market we did bid at - stores accepted orders, sets the received power - writes result back for the learning - and executes the dispatch, including ramping for times in the past - - :param content: the content of the clearing message - :type content: ClearingMessage - :param meta: the meta data of the market - :type meta: MetaDict + Handles the feedback which is received from a market we did bid at. + + Args: + content (ClearingMessage): The content of the clearing message. + meta (MetaDict): The meta data of the market. """ logger.debug(f"{self.id} got market result: {content}") accepted_orders: Orderbook = content["accepted_orders"] @@ -204,7 +200,14 @@ def handle_market_feedback(self, content: ClearingMessage, meta: MetaDict): def handle_registration_feedback( self, content: RegistrationMessage, meta: MetaDict - ): + ) -> None: + """ + Handles the feedback received from a market regarding registration. + + Args: + content (RegistrationMessage): The content of the registration message. + meta (MetaDict): The meta data of the market. + """ logger.debug("Market %s accepted our registration", content["market_id"]) if content["accepted"]: found = False @@ -220,7 +223,14 @@ def handle_registration_feedback( else: logger.error("Market %s did not accept registration", meta["sender_id"]) - def handle_data_request(self, content: DataRequestMessage, meta: MetaDict): + def handle_data_request(self, content: DataRequestMessage, meta: MetaDict) -> None: + """ + Handles the data request received from other agents. + + Args: + content (DataRequestMessage): The content of the data request message. + meta (MetaDict): The meta data of the market. + """ unit = content["unit"] metric_type = content["metric"] start = content["start_time"] @@ -245,16 +255,15 @@ def handle_data_request(self, content: DataRequestMessage, meta: MetaDict): }, ) - def set_unit_dispatch(self, orderbook: Orderbook, marketconfig: MarketConfig): + def set_unit_dispatch( + self, orderbook: Orderbook, marketconfig: MarketConfig + ) -> None: """ - feeds the current market result back to the units - this does not respect bids from multiple markets - for the same time period, as we only have access to the current orderbook here - - :param orderbook: the orderbook of the market - :type orderbook: Orderbook - :param marketconfig: the market configuration - :type marketconfig: MarketConfig + Feeds the current market result back to the units. + + Args: + orderbook (Orderbook): The orderbook of the market. + marketconfig (MarketConfig): The market configuration. """ orderbook.sort(key=itemgetter("unit_id")) for unit_id, orders in groupby(orderbook, itemgetter("unit_id")): @@ -264,11 +273,12 @@ def set_unit_dispatch(self, orderbook: Orderbook, marketconfig: MarketConfig): orderbook=orderbook, ) - def write_actual_dispatch(self, product_type: str): + def write_actual_dispatch(self, product_type: str) -> None: """ - sends the actual aggregated dispatch curve - works across multiple markets - sends dispatch at or after it actually happens + Sends the actual aggregated dispatch curve. + + Args: + product_type (str): The type of the product. """ last = self.last_sent_dispatch @@ -334,13 +344,14 @@ def write_actual_dispatch(self, product_type: str): }, ) - async def submit_bids(self, opening: OpeningMessage, meta: MetaDict): + async def submit_bids(self, opening: OpeningMessage, meta: MetaDict) -> None: """ - formulates an orderbook and sends it to the market. - This will handle optional portfolio processing + Formulates an orderbook and sends it to the market. + This function will accomodate the portfolio optimization in the future. - :param opening: the opening message - :type opening: OpeningMessage + Args: + opening (OpeningMessage): The opening message. + meta (MetaDict): The meta data of the market. """ products = opening["products"] @@ -384,18 +395,15 @@ async def formulate_bids_portfolio( self, market: MarketConfig, products: list[tuple] ) -> Orderbook: """ - Takes information from all units that the unit operator manages and - formulates the bid to the market from that according to the bidding strategy of the unit operator. - - This is the portfolio optimization version + Formulates the bid to the market according to the bidding strategy of the unit operator. + Placeholder for future portfolio optimization. - :param market: the market to formulate bids for - :type market: MarketConfig - :param products: the products to formulate bids for - :type products: list[tuple] + Args: + market (MarketConfig): The market to formulate bids for. + products (list[tuple]): The products to formulate bids for. - :return: OrderBook that is submitted as a bid to the market - :rtype: OrderBook + Returns: + OrderBook: The orderbook that is submitted as a bid to the market. """ orderbook: Orderbook = [] # TODO sort units by priority @@ -410,16 +418,14 @@ async def formulate_bids( self, market: MarketConfig, products: list[tuple] ) -> Orderbook: """ - Takes information from all units that the unit operator manages and - formulates the bid to the market from that according to the bidding strategy of the unit itself. + Formulates the bid to the market according to the bidding strategy of the each unit individually. - :param market: the market to formulate bids for - :type market: MarketConfig - :param products: the products to formulate bids for - :type products: list[tuple] + Args: + market (MarketConfig): The market to formulate bids for. + products (list[tuple]): The products to formulate bids for. - :return OrderBook that is submitted as a bid to the market - :rtype OrderBook + Returns: + OrderBook: The orderbook that is submitted as a bid to the market. """ orderbook: Orderbook = [] @@ -447,7 +453,16 @@ async def formulate_bids( return orderbook - def write_learning_to_output(self, start: datetime, marketconfig: MarketConfig): + def write_learning_to_output( + self, start: datetime, marketconfig: MarketConfig + ) -> None: + """ + Sends the current rl_strategy update to the output agent. + + Args: + start (datetime): The start time. + marketconfig (MarketConfig): The market configuration. + """ output_agent_list = [] for unit_id, unit in self.units.items(): # rl only for energy market for now! @@ -493,7 +508,18 @@ def write_to_learning( act_dim: int, device: str, learning_unit_count: int, - ): + ) -> None: + """ + Writes learning results to the learning agent. + + Args: + start (datetime): The start time. + marketconfig (MarketConfig): The market configuration. + obs_dim (int): The observation dimension. + act_dim (int): The action dimension. + device (str): The device used for learning. + learning_unit_count (int): The count of learning units. + """ all_observations = [] all_rewards = [] try: @@ -538,15 +564,17 @@ def write_to_learning( }, ) - def write_learning_params(self, orderbook: Orderbook, marketconfig: MarketConfig): + def write_learning_params( + self, orderbook: Orderbook, marketconfig: MarketConfig + ) -> None: """ - sends the current rl_strategy update to the output agent + Sends the current rl_strategy update to the output agent. - :param orderbook: the orderbook of the market - :type orderbook: Orderbook - :param marketconfig: the market configuration - :type marketconfig: MarketConfig + Args: + orderbook (Orderbook): The orderbook of the market. + marketconfig (MarketConfig): The market configuration. """ + learning_strategies = [] for unit in self.units.values(): diff --git a/assume/common/utils.py b/assume/common/utils.py index e6f3b720..fbd71148 100644 --- a/assume/common/utils.py +++ b/assume/common/utils.py @@ -22,13 +22,21 @@ def initializer(func): """ Automatically assigns the parameters. - >>> class process: - ... @initializer - ... def __init__(self, cmd, reachable=False, user='root'): - ... pass - >>> p = process('halt', True) - >>> p.cmd, p.reachable, p.user - ('halt', True, 'root') + + Args: + func (callable): The function to be initialized. + + Returns: + callable: The wrapper function. + + Examples: + >>> class process: + ... @initializer + ... def __init__(self, cmd, reachable=False, user='root'): + ... pass + >>> p = process('halt', True) + >>> p.cmd, p.reachable, p.user + ('halt', True, 'root') """ names, varargs, keywords, defaults, *_ = inspect.getfullargspec(func) @@ -48,14 +56,14 @@ def wrapper(self, *args, **kargs): def get_available_products(market_products: list[MarketProduct], startdate: datetime): """ - Get all available products for a given startdate - - :param market_products: list of market products - :type market_products: list[MarketProduct] - :param startdate: the startdate - :type startdate: datetime - :return: list of available products - :rtype: list[MarketProduct] + Get all available products for a given startdate. + + Args: + market_products (list[MarketProduct]): List of market products. + startdate (datetime): The startdate. + + Returns: + list[MarketProduct]: List of available products. """ options = [] for product in market_products: @@ -76,15 +84,16 @@ def get_available_products(market_products: list[MarketProduct], startdate: date def plot_orderbook(orderbook: Orderbook, results: list[dict]): """ - Plot the merit order of bids for each node in a separate subplot - - :param orderbook: the orderbook - :type orderbook: Orderbook - :param results: the results of the clearing - :type results: list[dict] - :return: the figure and axes of the plot - :rtype: tuple[matplotlib.figure.Figure, matplotlib.axes.Axes] + Plot the merit order of bids for each node in a separate subplot. + + Args: + orderbook (Orderbook): The orderbook. + results (list[dict]): The results of the clearing. + + Returns: + tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]: The figure and axes of the plot. """ + import matplotlib.pyplot as plt from matplotlib.lines import Line2D @@ -197,11 +206,12 @@ def plot_orderbook(orderbook: Orderbook, results: list[dict]): def visualize_orderbook(order_book: Orderbook): """ - Visualize the orderbook + Visualize the orderbook. - :param order_book: the orderbook - :type order_book: Orderbook + Args: + order_book (Orderbook): The orderbook. """ + import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap @@ -235,27 +245,21 @@ def visualize_orderbook(order_book: Orderbook): def aggregate_step_amount(orderbook: Orderbook, begin=None, end=None, groupby=None): """ - step function with bought volume - allows setting timeframe through begin and end - and group by columns in groupby. - This allows to have separate time series per market and bid_id/unit_id. - The orderbook must contain all relevant orders. - E.g. to calculate the current volume from 01.06 to 02.06, a yearly base - order from 01.01-31.12 must also be given, to be considered. - - If called without groupby, this returns the aggregated orderbook timeseries - - :param orderbook: the orderbook - :type orderbook: Orderbook - :param begin: the begin time - :type begin: datetime | None - :param end: the end time - :type end: datetime | None - :param groupby: the columns to group by - :type groupby: list[str] | None - :return: the aggregated orderbook timeseries - :rtype: list[tuple[datetime, float, str, str]] + Step function with bought volume, allows setting timeframe through begin and end, and group by columns in groupby. + + Args: + orderbook (Orderbook): The orderbook. + begin (datetime, optional): The begin time. Defaults to None. + end (datetime, optional): The end time. Defaults to None. + groupby (list[str], optional): The columns to group by. Defaults to None. + + Returns: + list[tuple[datetime, float, str, str]]: The aggregated orderbook timeseries. + + Examples: + If called without groupby, this returns the aggregated orderbook timeseries """ + if groupby is None: groupby = [] deltas = [] @@ -318,6 +322,20 @@ def aggregate_step_amount(orderbook: Orderbook, begin=None, end=None, groupby=No def get_test_demand_orders(power: np.array): + """ + Get test demand orders. + + Args: + power (np.array): Power array. + + Returns: + pd.DataFrame: DataFrame of demand orders. + + Examples: + >>> power = np.array([100, 200, 150]) + >>> get_test_demand_orders(power) + """ + order_book = {} for t in range(len(power)): order_book[t] = dict( @@ -328,7 +346,20 @@ def get_test_demand_orders(power: np.array): return demand_order -def separate_orders(orderbook): +def separate_orders(orderbook: Orderbook): + """ + Separate orders with several hours into single hour orders. + + Args: + orderbook (Orderbook): The orderbook. + + Returns: + list: The updated orderbook. + + Notes: + This function separates orders with several hours into single hour orders and modifies the orderbook in place. + """ + # separate orders with several hours into single hour orders delete_orders = [] for order in orderbook: diff --git a/assume/units/storage.py b/assume/units/storage.py index 8b63e55c..a510e7c1 100644 --- a/assume/units/storage.py +++ b/assume/units/storage.py @@ -406,11 +406,9 @@ def calculate_ramp_discharge( min_power_discharge: float = 0, ) -> float: power_discharge = super().calculate_ramp_discharge( - soc, previous_power, power_discharge, current_power, - min_power_discharge, ) # restrict according to min_SOC @@ -430,7 +428,6 @@ def calculate_ramp_charge( min_power_charge: float = 0, ) -> float: power_charge = super().calculate_ramp_charge( - soc, previous_power, power_charge, current_power, diff --git a/assume/world.py b/assume/world.py index eae41cc7..f69452c1 100644 --- a/assume/world.py +++ b/assume/world.py @@ -9,6 +9,7 @@ import time from datetime import datetime from sys import platform +from typing import Any, Optional, Tuple, Union import nest_asyncio import pandas as pd @@ -44,31 +45,28 @@ class World: def __init__( self, - addr: tuple[str, int] | str = "world", + addr: Union[Tuple[str, int], str] = "world", database_uri: str = "", export_csv_path: str = "", log_level: str = "INFO", - distributed_role: bool = None, - ): + distributed_role: Optional[bool] = None, + ) -> None: """ - Initialize a World instance. + Initializes a World instance with the provided address, database URI, export CSV path, log level, and distributed role settings. + + If a database URI is provided, it establishes a database connection. Additionally, it sets up various dictionaries and attributes for market operators, + markets, unit operators, unit types, bidding strategies, and clearing mechanisms. If available, it imports learning strategies and handles any potential import errors. + Finally, it sets up the event loop for asynchronous operations. Args: - - addr (tuple[str, int] | str): The address of the world, represented as a tuple of string and int or a string. - - database_uri (str): The URI for the database connection. - - export_csv_path (str): The path for exporting CSV data. - - log_level (str): The logging level for the world instance. - - distributed_role (bool): A boolean indicating whether distributed roles are enabled. + addr (Union[Tuple[str, int], str]): The address of the world, represented as a tuple of string and int or a string. + database_uri (str): The URI for the database connection. + export_csv_path (str): The path for exporting CSV data. + log_level (str): The logging level for the world instance. + distributed_role (Optional[bool]): A boolean indicating whether distributed roles are enabled. Returns: - - None - - This method initializes a World instance with the provided address, database URI, - export CSV path, log level, and distributed role settings. It establishes a database - connection if a database URI is provided. Additionally, it sets up various dictionaries - and attributes for market operators, markets, unit operators, unit types, bidding strategies, - and clearing mechanisms. If available, it imports learning strategies and handles any - potential import errors. Finally, it sets up the event loop for asynchronous operations. + None """ logging.getLogger("assume").setLevel(log_level) @@ -133,34 +131,30 @@ async def setup( save_frequency_hours: int = 24, bidding_params: dict = {}, learning_config: LearningConfig = {}, - forecaster: Forecaster = None, - manager_address=None, - **kwargs, - ): + forecaster: Optional[Forecaster] = None, + manager_address: Optional[Any] = None, + **kwargs: Any, + ) -> None: """ - Set up the environment for the simulation. + Set up the environment for the simulation, initializing various parameters and components required for the simulation run. Args: - - self: The instance of the class. - - start (datetime): The start datetime for the simulation. - - end (datetime): The end datetime for the simulation. - - simulation_id (str): The unique identifier for the simulation. - - index (pd.Series): The index for the simulation. - - save_frequency_hours (int, optional): The frequency (in hours) at which to save simulation data. Defaults to 24. - - bidding_params (dict, optional): Parameters for bidding. Defaults to an empty dictionary. - - learning_config (LearningConfig, optional): Configuration for the learning process. Defaults to an empty configuration. - - forecaster (Forecaster, optional): The forecaster used for custom unit types. Defaults to None. - - manager_address: The address of the manager. + self: The instance of the class. + start (datetime): The start datetime for the simulation. + end (datetime): The end datetime for the simulation. + simulation_id (str): The unique identifier for the simulation. + index (pd.Series): The index for the simulation. + save_frequency_hours (int, optional): The frequency (in hours) at which to save simulation data. Defaults to 24. + bidding_params (dict, optional): Parameters for bidding. Defaults to an empty dictionary. + learning_config (LearningConfig, optional): Configuration for the learning process. Defaults to an empty configuration. + forecaster (Forecaster, optional): The forecaster used for custom unit types. Defaults to None. + manager_address (Any, optional): The address of the manager. Other Parameters: - - kwargs: Additional keyword arguments. + **kwargs: Additional keyword arguments. Returns: - - None - - This method sets up the environment for the simulation. It initializes various parameters and components - required for the simulation run, including the clock, learning configuration, forecaster, container, - connection type, and output agents. It also handles the setup for distributed roles if applicable. + None """ self.clock = ExternalClock(0) @@ -213,20 +207,17 @@ async def setup( self.container, receiver_clock_addresses=self.addresses ) - async def setup_learning(self): + async def setup_learning(self) -> None: """ - Set up the learning process for the simulation. + Set up the learning process for the simulation, updating bidding parameters with the learning configuration + and initializing the reinforcement learning (RL) learning role with the specified parameters. It also sets up + the RL agent and adds the learning role to it for further processing. Args: - - self: The instance of the class. + self: The instance of the class. Returns: - - None - - This method sets up the learning process for the simulation. It updates the bidding parameters - with the learning configuration and initializes the reinforcement learning (RL) learning role - with the specified parameters. It also sets up the RL agent and adds the learning role to it - for further processing. + None """ self.bidding_params.update(self.learning_config) @@ -247,22 +238,21 @@ async def setup_learning(self): ) rl_agent.add_role(self.learning_role) - async def setup_output_agent(self, simulation_id: str, save_frequency_hours: int): + async def setup_output_agent( + self, simulation_id: str, save_frequency_hours: int + ) -> None: """ - Set up the output agent for the simulation. + Set up the output agent for the simulation, creating an output role responsible for writing simulation output, + including data storage and export settings. Depending on the platform (currently supported only on Linux), + it adds the output agent to the container's processes, or directly adds the output role to the output agent. Args: - - self: The instance of the class. - - simulation_id (str): The unique identifier for the simulation. - - save_frequency_hours (int): The frequency (in hours) at which to save simulation data. + self: The instance of the class. + simulation_id (str): The unique identifier for the simulation. + save_frequency_hours (int): The frequency (in hours) at which to save simulation data. Returns: - - None - - This method sets up the output agent for the simulation. It creates an output role responsible - for writing simulation output, including data storage and export settings. Depending on the - platform (currently supported only on Linux), it adds the output agent to the container's - processes, or directly adds the output role to the output agent. + None """ self.logger.debug(f"creating output agent {self.db=} {self.export_csv_path=}") @@ -294,24 +284,18 @@ def creator(container): ) output_agent.add_role(self.output_role) - def add_unit_operator( - self, - id: str, - ) -> None: + def add_unit_operator(self, id: str) -> None: """ - Add a unit operator to the simulation. + Add a unit operator to the simulation, creating a new role agent and applying the role of a unit operator to it. + The unit operator is then added to the list of existing operators. If in learning mode, additional context parameters + related to learning and output agents are set for the unit operator's role context. Args: - - self: The instance of the class. - - id (str): The identifier for the unit operator. + self: The instance of the class. + id (str): The identifier for the unit operator. Returns: - - None - - This method adds a unit operator to the simulation. It creates a new role agent and - applies the role of a units operator to it. The unit operator is then added to the - list of operators currently existing. If in learning mode, additional context parameters - related to learning and output agents are set for the unit operator's role context. + None """ if self.unit_operators.get(id): @@ -351,23 +335,20 @@ async def async_add_unit( forecaster: Forecaster, ) -> None: """ - Asynchronously add a unit to the simulation. + Asynchronously adds a unit to the simulation, checking if the unit operator exists, verifying the unit type, + and ensuring that the unit operator does not already have a unit with the same id. It then creates bidding + strategies for the unit and adds the unit within the associated unit operator. Args: - - self: The instance of the class. - - id (str): The identifier for the unit. - - unit_type (str): The type of unit to be added. - - unit_operator_id (str): The identifier of the unit operator to which the unit will be added. - - unit_params (dict): Parameters for configuring the unit. - - forecaster (Forecaster): The forecaster used by the unit. + self: The instance of the class. + id (str): The identifier for the unit. + unit_type (str): The type of unit to be added. + unit_operator_id (str): The identifier of the unit operator to which the unit will be added. + unit_params (dict): Parameters for configuring the unit. + forecaster (Forecaster): The forecaster used by the unit. Returns: - - None - - This method asynchronously adds a unit to the simulation. It checks if the unit operator - exists, verifies the unit type, and ensures that the unit operator does not already have a - unit with the same id. It then creates bidding strategies for the unit and creates the unit - within the associated unit operator. + None """ # check if unit operator exists @@ -416,24 +397,18 @@ async def async_add_unit( ) await self.unit_operators[unit_operator_id].add_unit(unit) - def add_market_operator( - self, - id: str, - ) -> None: + def add_market_operator(self, id: str) -> None: """ - Add a market operator to the simulation. + Add a market operator to the simulation by creating a new role agent for the market operator + and setting additional context parameters. If not in learning mode and not in evaluation mode, + it includes the output agent address and ID in the role context data dictionary. Args: - - self: The instance of the class. - - id (str): The identifier for the market operator. + self: The instance of the class. + id (str): The identifier for the market operator. Returns: - - None - - This method adds a market operator to the simulation. It creates a new role agent for - the market operator and sets additional context parameters. If not in learning mode and - not in evaluation mode, it includes output agent address and ID in the role context data - dictionary. + None """ if self.market_operators.get(id): @@ -455,26 +430,19 @@ def add_market_operator( ) self.market_operators[id] = market_operator_agent - def add_market( - self, - market_operator_id: str, - market_config: MarketConfig, - ) -> None: + def add_market(self, market_operator_id: str, market_config: MarketConfig) -> None: """ - Add a market to the simulation. + Add a market to the simulation by creating a market role based on the specified market mechanism in the market + configuration. Then, add this role to the specified market operator and append the market configuration to the list + of markets within the market operator. Additionally, store the market configuration in the simulation's markets dictionary. Args: - - self: The instance of the class. - - market_operator_id (str): The identifier of the market operator to which the market will be added. - - market_config (MarketConfig): The configuration for the market to be added. + self: The instance of the class. + market_operator_id (str): The identifier of the market operator to which the market will be added. + market_config (MarketConfig): The configuration for the market to be added. Returns: - - None - - This method adds a market to the simulation. It creates a market role based on the specified - market mechanism in the market configuration. It then adds this role to the specified market - operator and appends the market configuration to the list of markets within the market - operator. Additionally, it stores the market configuration in the simulation's markets dictionary. + None """ if mm_class := self.clearing_mechanisms.get(market_config.market_mechanism): @@ -506,24 +474,19 @@ async def _step(self): async def async_run(self, start_ts, end_ts): """ - Run the simulation asynchronously. + Run the simulation asynchronously, progressing the simulation time from the start timestamp to the end timestamp, + allowing registration before the first opening. If distributed roles are enabled, broadcast the simulation time. + Iterate through the simulation time, updating the progress bar and the simulation description. Once the simulation + time reaches the end timestamp, close the progress bar and shut down the simulation container. Args: - - self: The instance of the class. - - start_ts: The start timestamp for the simulation run. - - end_ts: The end timestamp for the simulation run. + self: The instance of the class. + start_ts: The start timestamp for the simulation run. + end_ts: The end timestamp for the simulation run. Returns: - - None - - This method runs the simulation asynchronously. It progresses the simulation time from - the start timestamp to the end timestamp, allowing registration before the first opening. - If distributed roles are enabled, it broadcasts the simulation time. The method then iterates - through the simulation time, updating the progress bar and the simulation description. Once - the simulation time reaches the end timestamp, the method closes the progress bar and shuts - down the simulation container. + None """ - # agent is implicit added to self.container._agents pbar = tqdm(total=end_ts - start_ts) @@ -549,17 +512,15 @@ def run(self): """ Run the simulation. - Returns: - - None - - This method converts the start and end timestamps to UTC time and then runs - the asynchronous simulation using the `async_run` method. It progresses the simulation time from - the start timestamp to the end timestamp, allowing registration before the first opening. - If distributed roles are enabled, it broadcasts the simulation time. The method then iterates - through the simulation time, updating the progress bar and the simulation description. Once - the simulation time reaches the end timestamp, the method closes the progress bar and shuts - down the simulation container. + This method converts the start and end timestamps to UTC time and then runs the asynchronous simulation using + the `async_run` method. It progresses the simulation time from the start timestamp to the end timestamp, allowing + registration before the first opening. If distributed roles are enabled, it broadcasts the simulation time. The + method then iterates through the simulation time, updating the progress bar and the simulation description. Once + the simulation time reaches the end timestamp, the method closes the progress bar and shuts down the simulation + container. + Returns: + None """ start_ts = calendar.timegm(self.start.utctimetuple()) @@ -574,15 +535,11 @@ def run(self): def reset(self): """ - Reset the World instance. + Reset the market operators, markets, unit operators, and forecast providers to empty dictionaries. Returns: - - None - - This method resets the market operators, markets, unit operators, and forecast - providers to empty dictionaries. + None """ - self.market_operators = {} self.markets = {} self.unit_operators = {} @@ -599,20 +556,19 @@ def add_unit( """ Add a unit to the World instance. + This method checks if the unit operator exists, verifies the unit type, and ensures that the unit operator + does not already have a unit with the same id. It then creates bidding strategies for the unit and creates + the unit within the associated unit operator. + Args: - - id (str): The identifier for the unit. - - unit_type (str): The type of the unit. - - unit_operator_id (str): The identifier of the unit operator. - - unit_params (dict): Parameters specific to the unit. - - forecaster (Forecaster): The forecaster associated with the unit. + id (str): The identifier for the unit. + unit_type (str): The type of the unit. + unit_operator_id (str): The identifier of the unit operator. + unit_params (dict): Parameters specific to the unit. + forecaster (Forecaster): The forecaster associated with the unit. Returns: - - None - - This method adds a unit to the World instance. It checks if the unit operator - exists, verifies the unit type, and ensures that the unit operator does not already have a - unit with the same id. It then creates bidding strategies for the unit and creates the unit - within the associated unit operator. + None """ return self.loop.run_until_complete( diff --git a/docs/source/conf.py b/docs/source/conf.py index 45fdfe7a..250933a1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -48,6 +48,7 @@ "repository_url": "https://github.com/assume-framework/assume.git", "use_repository_button": True, "show_navbar_depth": 2, + "navigation_with_keys": False, } # The name for this set of Sphinx documents. If None, it defaults to diff --git a/docs/source/examples/02_automated_run_example.nblink b/docs/source/examples/02_automated_run_example.nblink new file mode 100644 index 00000000..129b5b50 --- /dev/null +++ b/docs/source/examples/02_automated_run_example.nblink @@ -0,0 +1 @@ +{"path": "../../../examples/notebooks/02_automated_run_example.ipynb"} diff --git a/examples/notebooks/01_minimal_manual_example.ipynb copy.license b/docs/source/examples/02_automated_run_example.nblink.license similarity index 100% rename from examples/notebooks/01_minimal_manual_example.ipynb copy.license rename to docs/source/examples/02_automated_run_example.nblink.license diff --git a/docs/source/examples_basic.rst b/docs/source/examples_basic.rst index 3b9d914e..a029427d 100644 --- a/docs/source/examples_basic.rst +++ b/docs/source/examples_basic.rst @@ -2,16 +2,18 @@ .. .. SPDX-License-Identifier: AGPL-3.0-or-later -############ -Basic Usage -############ +########## +Tutorials +########## - -Here you can find several examples for usage of ASSUME framework to get you started: +The tutorials gradually become more complex, as more features and customizations are introduced. +We start with explaining the general concept of ASSUME in tutorial 01. If you are just starting to use ASSUME we advise you to use this one first and proceed with the following. +Here you can find several tutorials on how to use ASSUME framework to get you started: .. toctree:: :maxdepth: 1 - examples/01_minimal_manual_example.ipynb - examples/04_Reinforcement_learning_example.ipynb + examples/01_minimal_manual_example.nblink + examples/02_automated_run_example.nblink + examples/04_Reinforcement_learning_example.nblink diff --git a/examples/distributed_simulation/config.py b/examples/distributed_simulation/config.py index c731359b..8e535d2c 100644 --- a/examples/distributed_simulation/config.py +++ b/examples/distributed_simulation/config.py @@ -46,11 +46,11 @@ marketdesign = [ MarketConfig( - "EOM", - rr.rrule(rr.HOURLY, interval=24, dtstart=start, until=end), - timedelta(hours=1), - "pay_as_clear", - [MarketProduct(timedelta(hours=1), 24, timedelta(hours=1))], + name="EOM", + opening_hours=rr.rrule(rr.HOURLY, interval=24, dtstart=start, until=end), + opening_duration=timedelta(hours=1), + market_mechanism="pay_as_clear", + market_products=[MarketProduct(timedelta(hours=1), 24, timedelta(hours=1))], additional_fields=["block_id", "link", "exclusive_id"], ) ] diff --git a/examples/notebooks/01_minimal_manual_example.ipynb b/examples/notebooks/01_minimal_manual_example.ipynb index a074ccf7..7678443f 100644 --- a/examples/notebooks/01_minimal_manual_example.ipynb +++ b/examples/notebooks/01_minimal_manual_example.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Minimal manual example\n", + "# 1. Minimal manual tutorial\n", "In this notebook, we will walk through a minimal example of how to use the ASSUME framework. We will first initialize the world instance, next we will create a single market and its operator, afterwards we wll add a generation and a demand agents, and finally start the simulation." ] }, @@ -326,7 +326,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# The whole code as a single cell\n" + "## The whole code as a single cell\n" ] }, { @@ -447,7 +447,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.4" }, "nbsphinx": { "execute": "never" diff --git a/examples/notebooks/02_automated_run_example.ipynb b/examples/notebooks/02_automated_run_example.ipynb new file mode 100644 index 00000000..d8ba2c75 --- /dev/null +++ b/examples/notebooks/02_automated_run_example.ipynb @@ -0,0 +1,524 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 2. Run simulation using configuration and input files\n", + "\n", + "Welcome to the second tutorial in the ASSUME framework series. In the previous tutorial, we learned how to manually set up and execute simulations. However, for larger simulations involving multiple agents and demand series, it's more efficient to automate the process using configuration files and input files. This tutorial will guide you through the steps of creating these files and using them to run simulations in ASSUME.\n", + "\n", + "## Prerequisites\n", + "\n", + "Before you begin, make sure you have completed the first tutorial, which covers the basics of setting up and running a simple simulation manually. You should also have the ASSUME framework installed on your system.\n", + "\n", + "## Tutorial outline:\n", + "\n", + "- Introduction\n", + "- Setting up the environment\n", + "- Creating input files\n", + " - Power plant units\n", + " - Fuel prices\n", + " - Demand units\n", + " - Demand time series\n", + "- Creating a configuration file\n", + "- Running the simulation\n", + "- Adjusting market configuration\n", + "- Conclusion" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up the Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we just install the ASSUME core package via pip. The instructions for an installation can be found here: https://assume.readthedocs.io/en/latest/installation.html.\n", + "\n", + "This step is only required if you are working with this notebook in collab. If you are working locally and you have installed the assume package, you can skip this step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "shellscript" + } + }, + "outputs": [], + "source": [ + "!pip install assume-framework" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "First things first, let's import the necessary packages and set up our working directories." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import necessary packages\n", + "import pandas as pd\n", + "import logging\n", + "import os\n", + "import yaml\n", + "import numpy as np\n", + "\n", + "# import the main World class and the load_scenario_folder functions from assume\n", + "from assume import World, load_scenario_folder\n", + "\n", + "# Set up logging\n", + "log = logging.getLogger(__name__)\n", + "\n", + "# Define paths for input and output data\n", + "csv_path = \"outputs\"\n", + "input_path = \"inputs/example_01\"\n", + "\n", + "# Create directories if they don't exist\n", + "os.makedirs(\"local_db\", exist_ok=True)\n", + "os.makedirs(input_path, exist_ok=True)\n", + "\n", + "# Set the random seed for reproducibility\n", + "np.random.seed(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating Input Files\n", + "\n", + "### Power Plant Units\n", + "\n", + "In this section, we will create an input file that contains the details of our power plant units. Each power plant unit is represented by a set of attributes that define its operational and economic characteristics. The data is organized into a structured format that can be easily read and processed by the ASSUME framework.\n", + "\n", + "Once we have defined our data, we convert it into a pandas DataFrame. This is a convenient format for handling tabular data in Python and allows for easy manipulation and analysis. Finally, we save this DataFrame to a CSV file, which will serve as an input file for our simulation.\n", + "\n", + "Users can also create CSV files directly and save them to the input directory. This approach serves purely for demonstration purposes. Users can also adjust the input files manually to suit their needs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the data\n", + "powerplant_units_data = {\n", + " \"name\": [\"Unit 1\", \"Unit 2\", \"Unit 3\", \"Unit 4\"],\n", + " \"technology\": [\"nuclear\", \"lignite\", \"hard coal\", \"combined cycle gas turbine\"],\n", + " \"bidding_energy\": [\"naive\", \"naive\", \"naive\", \"naive\"],\n", + " \"fuel_type\": [\"uranium\", \"lignite\", \"hard coal\", \"natural gas\"],\n", + " \"emission_factor\": [0.0, 0.4, 0.3, 0.2],\n", + " \"max_power\": [1000.0, 1000.0, 1000.0, 1000.0],\n", + " \"min_power\": [200.0, 200.0, 200.0, 200.0],\n", + " \"efficiency\": [0.3, 0.5, 0.4, 0.6],\n", + " \"fixed_cost\": [10.3, 1.65, 1.3, 3.5],\n", + " \"unit_operator\": [\"Operator 1\", \"Operator 2\", \"Operator 3\", \"Operator 4\"],\n", + "}\n", + "\n", + "# Convert to DataFrame and save as CSV\n", + "powerplant_units_df = pd.DataFrame(powerplant_units_data)\n", + "powerplant_units_df.to_csv(f\"{input_path}/powerplant_units.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is a breakdown of each attribute we are including in our dataset:\n", + "\n", + "- `name`: A list of unique identifiers for each power plant unit. These names are used to reference the units throughout the simulation.\n", + "\n", + "- `technology`: The type of technology each unit uses to generate electricity.\n", + "\n", + "- `bidding_energy`: The strategy that each power plant unit will use when bidding into the energy market. In this example, all units are using a `naive` strategy, which bids at the marginal cost of production. If there are two markets in your simulation, for example a capacity market for reserves, you can also specify a `bidding_capacity` column, which will be used when bidding into the reserve market.\n", + "\n", + "- `fuel_type`: The primary fuel source used by each unit. This information is crucial as it relates to fuel costs and availability, as well as emissions.\n", + "\n", + "- `emission_factor`: A numerical value representing the amount of CO2 (or equivalent) emissions produced per unit of electricity generated.\n", + "\n", + "- `max_power`: The maximum power output each unit can deliver. This is the upper limit of the unit's operational capacity.\n", + "\n", + "- `min_power`: The minimum stable level of power that each unit can produce while remaining operational.\n", + "\n", + "- `efficiency`: A measure of how effectively each unit converts fuel into electricity. This efficienty represent the final efficiency of converting fuel into electricity.\n", + "\n", + "- `fixed_cost`: The fixed operational costs for each unit, such as maintenance and staffing, expressed in currency units per time period.\n", + "\n", + "- `unit_operator`: The entity responsible for operating each power plant unit. This could be a utility company, a private operator, or another type of organization." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Fuel Prices\n", + "\n", + "Now, we will create a DataFrame for fuel prices and save it as a CSV file. In this case we are using constant values for fuel prices, but users can also define time series for fuel prices. This is useful for simulating scenarios where fuel prices are volatile and change over time.\n", + "\n", + "The framework automatically recognizes if fuel prices are constant or time-varying. If fuel prices are time-varying, the correct price will be used for each time step in the simulation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the data\n", + "fuel_prices_data = {\n", + " \"fuel\": [\"uranium\", \"lignite\", \"hard coal\", \"natural gas\", \"oil\", \"biomass\", \"co2\"],\n", + " \"price\": [1, 2, 10, 25, 40, 20, 25],\n", + "}\n", + "\n", + "# Convert to DataFrame and save as CSV\n", + "fuel_prices_df = pd.DataFrame(fuel_prices_data).T\n", + "fuel_prices_df.to_csv(f\"{input_path}/fuel_prices_df.csv\", index=True, header=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Demand Units\n", + "\n", + "We also need to define the demand units for our simulation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the data\n", + "demand_units_data = {\n", + " \"name\": [\"demand_EOM\"],\n", + " \"technology\": [\"inflex_demand\"],\n", + " \"bidding_energy\": [\"naive\"],\n", + " \"max_power\": [1000000],\n", + " \"min_power\": [0],\n", + " \"unit_operator\": [\"eom_de\"],\n", + "}\n", + "\n", + "# Convert to DataFrame and save as CSV\n", + "demand_units_df = pd.DataFrame(demand_units_data)\n", + "demand_units_df.to_csv(f\"{input_path}/demand_units.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Demand Time Series\n", + "\n", + "Lastly, we'll create a time series for the demand. \n", + "\n", + "You might notice, that the column name we use if demand_EOM, which is similar to the name of our demand unit. The framework is designed in such way, that multiple demand units can be defined in the same file. The column name is used to match the demand time series with the correct demand unit. Afterwards, each demand unit following a naive bidding strategy will bid the respecrive demand value into the market.\n", + "\n", + "Also, the length of the demand time series must be at least as long as the simulation time horizon. If the time series is longer than the simulation time horizon, the framework will automatically truncate it to the correct length. If the resolution of the time series is higher than the simulation time step, the framework will automatically resample the time series to match the simulation time step. If it is shorter, an error will be raised." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a datetime index for a week with hourly resolution\n", + "date_range = pd.date_range(start=\"2021-03-01\", periods=8 * 24, freq=\"H\")\n", + "\n", + "# Generate random demand values around 2000\n", + "demand_values = np.random.normal(loc=2000, scale=200, size=8 * 24)\n", + "\n", + "# Create a DataFrame for the demand profile and save as CSV\n", + "demand_profile = pd.DataFrame({\"datetime\": date_range, \"demand_EOM\": demand_values})\n", + "demand_profile.to_csv(f\"{input_path}/demand_df.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's what each attribute in our dataset represents:\n", + "\n", + "- `name`: This is the identifier for the demand unit. In our case, we have a single demand unit named `demand_EOM`, which could represent the total electricity demand of an entire market or a specific region within the market.\n", + "\n", + "- `technology`: Indicates the type of demand. Here, `inflex_demand` is used to denote inelastic demand, meaning that the demand does not change in response to price fluctuations within the short term. This is a typical assumption for electricity markets within a short time horizon.\n", + "\n", + "- `bidding_energy`: Specifies the bidding strategy for the demand unit. Even though demand is typically price-inelastic in the short term, it still needs to be represented in the market. The `naive` strategy here bids the demand value into the market at price of 3000 EUR/MWh.\n", + "\n", + "- `max_power`: The maximum power that the demand unit can request. In this example, we've set it to 1,000,000 MW, which is a placeholder. This value can be used for more sophisticated bidding strategies.\n", + "\n", + "- `min_power`: The minimum power level that the demand unit can request. In this case also serves as a placeholder for more sophisticated bidding strategies.\n", + "\n", + "- `unit_operator`: The entity responsible for the demand unit. In this example, `eom_de` could represent an electricity market operator in Germany." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating a Configuration File\n", + "\n", + "With our input files ready, we'll now create a configuration file that ASSUME will use to load the simulation. The confi file allows easy customization of the simulation parameters, such as the simulation time horizon, the time step, and the market configuration. The configuration file is written in YAML format, which is a human-readable markup language that is commonly used for configuration files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the config as a dictionary\n", + "config_data = {\n", + " \"hourly_market\": {\n", + " \"start_date\": \"2021-03-01 00:00\",\n", + " \"end_date\": \"2021-03-08 00:00\",\n", + " \"time_step\": \"1h\",\n", + " \"save_frequency_hours\": 24,\n", + " \"markets_config\": {\n", + " \"EOM\": {\n", + " \"operator\": \"EOM_operator\",\n", + " \"product_type\": \"energy\",\n", + " \"opening_frequency\": \"1h\",\n", + " \"opening_duration\": \"1h\",\n", + " \"products\": [{\"duration\": \"1h\", \"count\": 1, \"first_delivery\": \"1h\"}],\n", + " \"volume_unit\": \"MWh\",\n", + " \"price_unit\": \"EUR/MWh\",\n", + " \"market_mechanism\": \"pay_as_clear\",\n", + " }\n", + " },\n", + " }\n", + "}\n", + "\n", + "# Save the configuration as YAML\n", + "with open(f\"{input_path}/config.yaml\", \"w\") as file:\n", + " yaml.dump(config_data, file, sort_keys=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is a breakdown of each key in our configuration file:\n", + "\n", + "- `start_date`: This key specifies the starting date and time for the market simulation. In this example, the simulation will start on March 1st, 2021, at midnight. The date and time are in the format \"YYYY-MM-DD HH:MM\".\n", + "\n", + "- `end_date`: This key defines the ending date and time for the market simulation, which is set to March 8th, 2021, at midnight. The simulation will run for one week.\n", + "\n", + "- `time_step`: This key defines the granularity of the market operation. Here, it is set to \"1h\", which means the simulation internal clock operates in one-hour intervals.\n", + "\n", + "- `save_frequency_hours`: This key indicates how often the simulation data should be saved. In this case, the data will be saved every 24 hours. This is helpful when you have a long simulation and want to observe the results at regular intervals (when using docker and database). Alternatively, you can remove this parameter to save all results at the end of the simulation.\n", + "\n", + "- `markets_config`: This key contains a nested dictionary with configurations for specific markets within the hourly market.\n", + "\n", + " - `EOM`: This is a sub-key representing a specific market, named EOM.\n", + "\n", + " - `operator`: This key specifies the operator of the EOM market, which is \"EOM_operator\" in this case.\n", + "\n", + " - `product_type`: This key defines the type of product being traded in the market. Here, the product type is \"energy\".\n", + "\n", + " - `opening_frequency`: This key indicates how often the market opens for trading. It is set to \"1h\", meaning the market opens every hour.\n", + "\n", + " - `opening_duration`: This key specifies the duration for which the market is open during each trading session. It is also set to \"1h\".\n", + "\n", + " - `products`: This key holds a list of products available for trading in the market. Each product is represented as a dictionary with its own set of configurations.\n", + "\n", + " - `duration`: This key defines the delivery duration of the product, which is \"1h\" in this example.\n", + "\n", + " - `count`: This key specifies the number of products available for each trading session. In this case, there is only one product per session.\n", + "\n", + " - `first_delivery`: This key indicates the time until the first delivery of the product after trading. It is set to \"1h\" after the market closes.\n", + "\n", + " - `volume_unit`: This key defines the unit of measurement for the volume of the product, which is \"MWh\" (megawatt-hour) in this example.\n", + "\n", + " - `price_unit`: This key specifies the unit of measurement for the price of the product, which is \"EUR/MWh\" (Euros per megawatt-hour).\n", + "\n", + " - `market_mechanism`: This key describes the market mechanism used to clear the market. \"pay_as_clear\" means that all participants pay the clearing price, which is the highest accepted bid price.\n", + "\n", + "To read more about available market configuration, please refer to https://assume.readthedocs.io/en/latest/market_config.html." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running the Simulation\n", + "\n", + "Now that we have our input files and configuration set up, we can run the simulation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# define the database uri. In this case we are using a local sqlite database\n", + "db_uri = f\"sqlite:///local_db/assume_db_example_02.db\"\n", + "\n", + "# create world instance\n", + "world = World(database_uri=db_uri, export_csv_path=csv_path)\n", + "\n", + "# load scenario by providing the world instance\n", + "# the path to the inputs folder and the scenario name (subfolder in inputs)\n", + "# and the study case name (which config to use for the simulation)\n", + "load_scenario_folder(\n", + " world,\n", + " inputs_path=\"inputs\",\n", + " scenario=\"example_01\",\n", + " study_case=\"hourly_market\",\n", + ")\n", + "\n", + "# run the simulation\n", + "world.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adjusting Market Configuration\n", + "\n", + "You can easily adjust the market design by changing a few lines in the configuration file. Let's add a new market configuration. Let's say we would like to switch from an hourly market to a day-ahead market with hourly intervals. All we need to do is to change the `opening_frequency` to \"24h\" and `count` to 24. This means, that market opens every 24 hours and each participant needs to submit 24 hourly products." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the new market config\n", + "new_market_config = {\n", + " \"daily_market\": {\n", + " \"start_date\": \"2021-03-01 00:00\",\n", + " \"end_date\": \"2021-03-08 00:00\",\n", + " \"time_step\": \"1h\",\n", + " \"save_frequency_hours\": 24,\n", + " \"markets_config\": {\n", + " \"EOM\": {\n", + " \"operator\": \"EOM_operator\",\n", + " \"product_type\": \"energy\",\n", + " \"opening_frequency\": \"24h\",\n", + " \"opening_duration\": \"1h\",\n", + " \"products\": [{\"duration\": \"1h\", \"count\": 24, \"first_delivery\": \"1h\"}],\n", + " \"volume_unit\": \"MWh\",\n", + " \"price_unit\": \"EUR/MWh\",\n", + " \"market_mechanism\": \"pay_as_clear\",\n", + " }\n", + " },\n", + " }\n", + "}\n", + "\n", + "# Update the existing configuration\n", + "config_data.update(new_market_config)\n", + "\n", + "# Save the updated configuration\n", + "with open(f\"{input_path}/config.yaml\", \"w\") as file:\n", + " yaml.dump(config_data, file, sort_keys=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Running the Simulation Again\n", + "\n", + "With the updated configuration, we can run the simulation for a different study case, in this case for daily_market configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_format = \"local_db\" # \"local_db\" or \"timescale\"\n", + "\n", + "if data_format == \"local_db\":\n", + " db_uri = f\"sqlite:///./local_db/assume_db_example_02.db\"\n", + "\n", + "# create world\n", + "world = World(database_uri=db_uri, export_csv_path=csv_path)\n", + "\n", + "# load scenario by providing the world instance\n", + "# the path to the inputs folder and the scenario name (subfolder in inputs)\n", + "# and the study case name (which config to use for the simulation)\n", + "load_scenario_folder(\n", + " world,\n", + " inputs_path=\"inputs\",\n", + " scenario=\"example_01\",\n", + " study_case=\"daily_market\",\n", + ")\n", + "\n", + "# run the simulation\n", + "world.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simulation results\n", + "\n", + "After all the simulations are complete, you might want to analyze the results. The results are stored in the database. But they are also written to CSV files at the end of the simulation. The CSV files are stored in the outputs directory, which you are invited to explore. In the next tutorial, we will take a closer look at the simulation results and learn how to visualize them." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "Congratulations! You've learned how to automate the setup and execution of simulations in ASSUME using configuration files and input files. This approach is particularly useful for handling large and complex simulations. \n", + "\n", + "You are welcome to experiment with different configurations and variying input data. For example, you can try changing the bidding strategy for the power plant units to a more sophisticated strategy, such as a `flexable_eom`" + ] + } + ], + "metadata": { + "colab": { + "include_colab_link": true, + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "assume-framework", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + }, + "nbsphinx": { + "execute": "never" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/02_automated_run_example.ipynb.license b/examples/notebooks/02_automated_run_example.ipynb.license new file mode 100644 index 00000000..a6ae0636 --- /dev/null +++ b/examples/notebooks/02_automated_run_example.ipynb.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: ASSUME Developers + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/examples/notebooks/04_Reinforcement_learning_example.ipynb b/examples/notebooks/04_Reinforcement_learning_example.ipynb index 2054861a..c7a25556 100644 --- a/examples/notebooks/04_Reinforcement_learning_example.ipynb +++ b/examples/notebooks/04_Reinforcement_learning_example.ipynb @@ -6,7 +6,7 @@ "id": "4JeBorbE6FYr" }, "source": [ - "# Reinforcement learning example\n", + "# 4. Reinforcement learning tutorial\n", "\n", "This tutorial will introduce users into ASSUME and its ways of using reinforcement leanring (RL). The main objective of this tutorial is to ensure participants grasp the steps required to equip a new unit with RL strategies or modify the action dimensions.\n", "Our emphasis lies in the bidding strategy class, with less emphasis on the algorithm and role. The latter are usable as a plug and play solution in the framework. The following coding tasks will highlight the key aspects to be adjusted, as already outlined in the learning_strategies.py file.\n", @@ -30,7 +30,7 @@ "id": "bj2C4ElILNNv" }, "source": [ - "# 1. ASSUME & LEARNING BASICS\n", + "## 1. ASSUME & Learning Basics\n", "\n", "ASSUME in general is intended for researchers, planners, utilities and everyone searching to understand market dynamics of energy markets. It provides an easy-to-use tool-box as a free software that can be tailored to the specific use case of the user.\n", "\n", @@ -58,7 +58,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 1.1 Single-Agent Learning\n", + "### Single-Agent Learning\n", "\n", "We use the actor-critic approach to train the learning agent. The actor-critic approach is a popular RL algorithm that uses two neural networks: an actor network and a critic network. The actor network is responsible for selecting actions, while the critic network evaluates the quality of the actions taken by the actor.\n", "\n", @@ -91,7 +91,7 @@ "id": "OMvIl2xLVi1l" }, "source": [ - "## 1.2 Multi-Agent Learning\n", + "### Multi-Agent Learning\n", "\n", "While in a single-agent setup, the state transition and respective reward depend only on the actions of a single agent, the state transitions and rewards depend on the actions of all learning agents in a multi-agent setup. This makes the environment non-stationary for a single agent, which violates the Markov property. Hence, the convergence guarantees of single-agent RL algorithms are no longer valid. Therefore, we utilize the framework of centralized training and decentralized execution and expand upon the MADDPG algorithm. The main idea of this approach is to use a centralized critic during the training phase, which has access to the entire state $\\textbf{S}$, and all actions $a_1, ..., a_N$, thus resolving the issue of non-stationarity, as changes in state transitions and rewards can be explained by the actions of other agents. Meanwhile, during both training and execution, the actor has access only to its local observations $o_i$ derived from the entire state $\\textbf{S}$.\n", "\n", @@ -118,7 +118,7 @@ "id": "OeeZDtIFmmhn" }, "source": [ - "# 2. GET ASSUME RUNNING\n", + "## 2. Get ASSUME running\n", "Here we just install the ASSUME core package via pip. In general the instructions for an installation can be found here: https://assume.readthedocs.io/en/latest/installation.html. All the required steps are executed here and since we are working in colab the generation of a venv is not necessary. \n" ] }, @@ -175,7 +175,9 @@ "id": "Fg7DyNjLuvSb" }, "source": [ - "**Let the magic happen.** Now you can run your first ever simulation in ASSUME. The following code naviagtes to the respective assume folder and starts the simulation example example_01b using the local database here in colab." + "**Let the magic happen.** Now you can run your first ever simulation in ASSUME. The following code navigates to the respective assume folder and starts the simulation example example_01b using the local database here in colab.\n", + "\n", + "When running locally, you can also just run `assume -s example_01b -db \"sqlite:///./examples/local_db/assume_db_example_01b.db\"` in a shell" ] }, { @@ -202,7 +204,7 @@ "id": "zMyZhaNM7NRP" }, "source": [ - "# 3. MAKE ASSUME LEARN\n", + "## 3. Make your agents learn\n", "\n", "Now it is time to get your hands dirty and actually dive into coding in ASSUME. The main objective of this session is to ensure participants grasp the steps required to equip a new unit with RL strategies or modify the action dimensions. Our emphasis lies in the bidding strategy class, with less emphasis on the algorithm and role. Coding tasks will highlight the key aspects to be a djusted, as already outlined in the learning_strategies.py file. Subsequent\n", "sections will present the tasks and provide the correct answers for the coding exercises.\n", @@ -327,10 +329,7 @@ " )\n", "\n", " elif Path(load_path=kwargs[\"trained_actors_path\"]).is_dir():\n", - " self.load_actor_params(load_path=kwargs[\"trained_actors_path\"])\n", - "\n", - " def testfunction():\n", - " return None" + " self.load_actor_params(load_path=kwargs[\"trained_actors_path\"])" ] }, { @@ -339,7 +338,7 @@ "id": "8UM1QPZrIdqK" }, "source": [ - "## 3.1 The \"Step Function\"\n", + "### 3.1 The \"Step Function\"\n", "\n", "The key function in an RL problem is the step that is taken in the so called environment. It consist the following parts:\n", "\n", @@ -364,6 +363,7 @@ "# magic to enable class definitions across colab cells\n", "%%add_to RLStrategy\n", "\n", + "\n", "def calculate_bids(\n", " self,\n", " unit: SupportsMinMax,\n", @@ -425,6 +425,7 @@ "# magic to enable class definitions across colab cells\n", "%%add_to RLStrategy\n", "\n", + "\n", "def calculate_reward(\n", " self,\n", " unit,\n", @@ -452,7 +453,7 @@ "id": "Jgjx14997Y9s" }, "source": [ - "## 3.2 Get an observation\n", + "### 3.2 Get an observation\n", "\n", "The decision about the observations received by each agent plays a crucial role when designing a multi-agent RL setup. The following describes the task of learning agents representing profit-maximizing electricity market participants who either sell a generating unit's output or optimize a storage unit's operation. They are represented through their plants' techno-economic parameters, such as minimal operational capacity $P^{min}$, start-up $c^{su}$, and shut-down $c^{sd}$ costs. This information is all know by the unit istself and, hence, also accessible in the bidding strategy.\n", "\n", @@ -467,7 +468,7 @@ "id": "PngYyvs72UxB" }, "source": [ - "### **Task 1**\n", + "#### **Task 1**\n", "**Goal**: With the help of the *unit*, the *starttime* and the *endtime* we want to create the Observations for the unit.\n", "\n", "There are 4 different observations:\n", @@ -606,7 +607,7 @@ "id": "kDYKZGERKJ6V" }, "source": [ - "### **Solution 1**\n", + "#### **Solution 1**\n", "\n", "First why do we scale?\n", "\n", @@ -652,7 +653,7 @@ "id": "rW_1op6fCTV-" }, "source": [ - "## 3.3 Choose an action\n", + "### 3.3 Choose an action\n", "\n", "To differentiate between the inflexible and flexible parts of a plant's generation capacity, we split the bids into two parts. The first bid part allows agents to bid a very low or even negative price for the inflexible capacity; this reflects the agent's motivation to stay infra-marginal during periods of very low net load (e.g., in periods of high solar and wind power generation) to avoid the cost of a shut-down and subsequent start-up of the plant. The flexible part of the capacity can be offered at a higher price to provide chances for higher profits. The actions of agent $i$ at time-step $t$ are defined as $a_{i,t} = [ep^\\mathrm{inflex}_{i,t}, ep^\\mathrm{flex}_{i,t}] \\in [ep^{min},ep^{max}]$, where $ep^\\mathrm{inflex}_{i,t}$ and $ep^\\mathrm{flex}_{i,t}$ are bid prices for the inflexible and flexible capacities, and $ep^{min},ep^{max}$ are minimal and maximal bid prices, respectively.\n", "\n", @@ -672,7 +673,7 @@ "id": "Cho84Pqs2N2G" }, "source": [ - "### **Task 2.1**\n", + "#### **Task 2.1**\n", "**Goal**: With the observations and noise we generate actions\n", "\n", "In the following task we define the actions for the initial exploration mode. As described before we can guide it by not letting it choose random actions but defining a base-bid on which we add a good amount of noise. In this way the initial strategy starts from a solution that we know works somewhat well. Define the respective base bid in the followin code. Remeber we are defining bids for a conventional power plant bidding in an Energy-Only-Market with a uniform pricing auction. " @@ -748,7 +749,7 @@ "id": "OTaqkwV3xcf6" }, "source": [ - "### **Solution 2.1**\n", + "#### **Solution 2.1**\n", "\n", "So how do we define the base bid?\n", "\n", @@ -780,8 +781,8 @@ "id": "B5Hgh88Vz0wD" }, "source": [ - "### **Task 2.2**\n", - "**Goal: Define the actual bids with the outputs of the actors\n", + "#### **Task 2.2**\n", + "**Goal: Define the actual bids with the outputs of the actors**\n", "\n", "Similarly to every other output of a neuronal network, the actions are in the range of 0-1. These values need to be translated into the actual bids $a_{i,t} = [ep^\\mathrm{inflex}_{i,t}, ep^\\mathrm{flex}_{i,t}] \\in [ep^{min},ep^{max}]$. This can be done in a way that further helps the RL agent to learn, if we put some thought into.\n", "\n", @@ -897,11 +898,11 @@ "id": "3n-kJeOFCfRB" }, "source": [ - "### **Solution 2.2**\n", + "#### **Solution 2.2**\n", "\n", "So how do we define the actual bid from the action?\n", "\n", - "We have the bid price for the minimum power (inflex) and the rest of the power. As the power plant needs to run at minimal the minum power in order to offer generation in general, it makes sense to offer this generation at a lower price than the rest of the power. Hence, we can alocate the actions to the bid prices in the following way. In addition, the actions need to be rescaled of course.\n" + "We have the bid price for the minimum power (inflex) and the rest of the power. As the power plant needs to run at minimal the minum power in order to offer generation in general, it makes sense to offer this generation at a lower price than the rest of the power. Hence, we can allocate the actions to the bid prices in the following way. In addition, the actions need to be rescaled of course.\n" ] }, { @@ -938,7 +939,7 @@ "id": "hr15xKuGCkbn" }, "source": [ - "## 3.4 Get a reward\n", + "### 3.4 Get a reward\n", "This step is done in the *calculate_reward*()-function, which is called after the market is cleared and we get the market feedback, so we can calculate the profit. In RL, the design of a reward function is as important as the choice of the correct algorithm. During the initial phase of the work, pure economic reward in the form of the agent's profit was used. Typically, electricity market models consider only a single restart cost. Still, in the case of using RL, the split into shut-down and start-up costs allow the agents to better differentiate between these two events and learn a better policy.\n", "\n", "\n", @@ -956,7 +957,13 @@ "\\end{equation}\n", "\n", "\n", - "In this equation, $P^\\text{conf}$ is the confirmed capacity on the market, $P^{min}$ --- minimal stable capacity, $M$ --- market clearing price, $mc$ --- marginal generation cost, $dt$ --- market time resolution, $c^{su}, c^{sd}$ --- start-up and shut-down costs, respectively.\n", + "In this equation, the variables are:\n", + "* $P^\\text{conf}$ the confirmed capacity on the market\n", + "* $P^{min}$ the minimal stable capacity\n", + "* $M$ the market clearing price\n", + "* $mc$ the marginal generation cost\n", + "* $dt$ the market time resolution\n", + "* $c^{su}, c^{sd}$ the start-up and shut-down costs, respectively\n", "\n", "The profit-driven reward function was sufficient for a few agents, but the learning performance decreased significantly with more agents. Therefore, we add an additional regret term $cm$." ] @@ -967,7 +974,7 @@ "id": "aGyaOUgo3Y8Q" }, "source": [ - "### **Task 3**\n", + "#### **Task 3**\n", "**Goal**: Define the reward guiding the learning process of the agent.\n", "\n", "As the reward plays such a crucial role in the learning think of ways how to integrate further signals exceeding the monetary profit. One example could be integrating a regret term, namely the opportunity costs. Your task is to define the rewrad using the opportunity costs and to scale it." @@ -1090,7 +1097,7 @@ "id": "gWF7D4QA2-kz" }, "source": [ - "### **Solution 3**\n", + "#### **Solution 3**\n", "\n", "So how do we define the actual reward?\n", "\n", @@ -1139,7 +1146,7 @@ "id": "L3flH5iY4x7Z" }, "source": [ - "## 3.5 Start the simulation\n", + "### 3.5 Start the simulation\n", "\n", "We are almost done with all the changes to actually be able to make ASSUME learn here in google colab. If you would rather like to load our pretrained strategies, we need a function for loading parameters, which can be found below. \n", "\n" @@ -1156,6 +1163,7 @@ "# magic to enable class definitions across colab cells\n", "%%add_to RLStrategy\n", "\n", + "\n", "def load_actor_params(self, load_path):\n", " \"\"\"\n", " Load actor parameters\n", diff --git a/examples/world_script.py b/examples/world_script.py index 76d03a9a..c71bd2c6 100644 --- a/examples/world_script.py +++ b/examples/world_script.py @@ -39,11 +39,11 @@ async def init(): marketdesign = [ MarketConfig( - "EOM", - rr.rrule(rr.HOURLY, interval=24, dtstart=start, until=end), - timedelta(hours=1), - "pay_as_clear", - [MarketProduct(timedelta(hours=1), 24, timedelta(hours=1))], + name="EOM", + opening_hours=rr.rrule(rr.HOURLY, interval=24, dtstart=start, until=end), + opening_duration=timedelta(hours=1), + market_mechanism="pay_as_clear", + market_products=[MarketProduct(timedelta(hours=1), 24, timedelta(hours=1))], additional_fields=["block_id", "link", "exclusive_id"], ) ] diff --git a/tests/test_clearing_paper_examples.py b/tests/test_clearing_paper_examples.py index fc03ab8a..01f718c4 100644 --- a/tests/test_clearing_paper_examples.py +++ b/tests/test_clearing_paper_examples.py @@ -15,7 +15,7 @@ from .utils import extend_orderbook simple_dayahead_auction_config = MarketConfig( - "simple_dayahead_auction", + name="simple_dayahead_auction", market_products=[MarketProduct(rd(hours=+1), 1, rd(hours=1))], additional_fields=["node_id"], opening_hours=rr.rrule( diff --git a/tests/test_complex_market_mechanisms.py b/tests/test_complex_market_mechanisms.py index e410f391..a912ec53 100644 --- a/tests/test_complex_market_mechanisms.py +++ b/tests/test_complex_market_mechanisms.py @@ -15,7 +15,7 @@ from .utils import extend_orderbook simple_dayahead_auction_config = MarketConfig( - "simple_dayahead_auction", + name="simple_dayahead_auction", market_products=[MarketProduct(rd(hours=+1), 1, rd(hours=1))], additional_fields=["node_id"], opening_hours=rr.rrule( diff --git a/tests/test_data_request_mechanism.py b/tests/test_data_request_mechanism.py index 600b5207..8fa1a1e7 100644 --- a/tests/test_data_request_mechanism.py +++ b/tests/test_data_request_mechanism.py @@ -51,11 +51,11 @@ def handle_message(self, content, meta): async def test_request_messages(): market_name = "Test" marketconfig = MarketConfig( - market_name, - rr.rrule(rr.HOURLY, dtstart=start, until=end), - rd(hours=1), - "pay_as_clear", - [MarketProduct(rd(hours=1), 1, rd(hours=1))], + name=market_name, + opening_hours=rr.rrule(rr.HOURLY, dtstart=start, until=end), + opening_duration=rd(hours=1), + market_mechanism="pay_as_clear", + market_products=[MarketProduct(rd(hours=1), 1, rd(hours=1))], ) clock = ExternalClock(0) container = await create_container( diff --git a/tests/test_demand.py b/tests/test_demand.py index 7108bd67..35ea182d 100644 --- a/tests/test_demand.py +++ b/tests/test_demand.py @@ -46,11 +46,11 @@ def test_demand(): assert dem.calculate_marginal_cost(start, max_power.max()) == 2000 mc = MarketConfig( - "Test", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), 1, timedelta(hours=1))], + name="Test", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[MarketProduct(timedelta(hours=1), 1, timedelta(hours=1))], ) bids = dem.calculate_bids(mc, [product_tuple]) @@ -107,11 +107,11 @@ def test_demand_series(): assert dem.calculate_marginal_cost(end, max_power) == 1000 mc = MarketConfig( - "Test", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), 1, timedelta(hours=1))], + name="Test", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[MarketProduct(timedelta(hours=1), 1, timedelta(hours=1))], ) bids = dem.calculate_bids(mc, [product_tuple]) diff --git a/tests/test_dmas_market.py b/tests/test_dmas_market.py index bf227322..52fadc38 100644 --- a/tests/test_dmas_market.py +++ b/tests/test_dmas_market.py @@ -17,7 +17,7 @@ end = datetime(2020, 12, 2) simple_dayahead_auction_config = MarketConfig( - "simple_dayahead_auction", + name="simple_dayahead_auction", market_products=[MarketProduct(rd(hours=+1), 24, rd(hours=1))], additional_fields=["exclusive_id", "link", "block_id"], opening_hours=rr.rrule( diff --git a/tests/test_dmas_powerplant.py b/tests/test_dmas_powerplant.py index e76f633c..09596597 100644 --- a/tests/test_dmas_powerplant.py +++ b/tests/test_dmas_powerplant.py @@ -97,11 +97,13 @@ def test_dmas_calc(power_plant_1): hour_count = len(power_plant_1.index) // 2 mc = MarketConfig( - "Test", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0))], + name="Test", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[ + MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0)) + ], additional_fields=["link", "block_id"], ) start = power_plant_1.index[0] @@ -122,11 +124,13 @@ def test_dmas_day(power_plant_day): assert hour_count == 24 mc = MarketConfig( - "Test", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0))], + name="Test", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[ + MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0)) + ], additional_fields=["link", "block_id"], ) start = power_plant_day.index[0] @@ -155,11 +159,13 @@ def test_dmas_prevent_start(power_plant_day): power_plant_day.forecaster.price_forecast.iloc[10:11] = -10 mc = MarketConfig( - "Test", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0))], + name="Test", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[ + MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0)) + ], additional_fields=["link", "block_id"], ) start = power_plant_day.index[0] @@ -187,11 +193,13 @@ def test_dmas_prevent_start_end(power_plant_day): power_plant_day.forecaster.price_forecast.iloc[20:24] = -10 mc = MarketConfig( - "Test", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0))], + name="Test", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[ + MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0)) + ], additional_fields=["link", "block_id"], ) start = power_plant_day.index[0] diff --git a/tests/test_dmas_storage.py b/tests/test_dmas_storage.py index bba6883d..b84dc616 100644 --- a/tests/test_dmas_storage.py +++ b/tests/test_dmas_storage.py @@ -93,11 +93,13 @@ def test_dmas_calc(storage_unit): hour_count = len(storage_unit.index) // 2 mc = MarketConfig( - "Test", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0))], + name="Test", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[ + MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0)) + ], additional_fields=["exclusive_id"], ) start = storage_unit.index[0] @@ -116,11 +118,13 @@ def test_dmas_day(storage_day): assert hour_count == 24 mc = MarketConfig( - "Test", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0))], + name="Test", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[ + MarketProduct(timedelta(hours=1), hour_count, timedelta(hours=0)) + ], additional_fields=["exclusive_id"], ) start = storage_day.index[0] diff --git a/tests/test_extended.py b/tests/test_extended.py index 84666759..833d51c9 100644 --- a/tests/test_extended.py +++ b/tests/test_extended.py @@ -43,11 +43,11 @@ def test_otc_strategy_scaled(scale, mock_supports_minmax): strategy = OTCStrategy(scale_firm_power_capacity=scale) mc = MarketConfig( - "OTC", - rr.rrule(rr.HOURLY), - timedelta(hours=1), - "not needed", - [MarketProduct(timedelta(hours=1), 1, timedelta(hours=1))], + name="OTC", + opening_hours=rr.rrule(rr.HOURLY), + opening_duration=timedelta(hours=1), + market_mechanism="not needed", + market_products=[MarketProduct(timedelta(hours=1), 1, timedelta(hours=1))], ) unit = mock_supports_minmax diff --git a/tests/test_market.py b/tests/test_market.py index 54fe7a29..d3bea89d 100644 --- a/tests/test_market.py +++ b/tests/test_market.py @@ -24,11 +24,11 @@ async def market_role() -> MarketRole: market_name = "Test" marketconfig = MarketConfig( - market_name, - rr.rrule(rr.HOURLY, dtstart=start, until=end), - rd(hours=1), - "pay_as_clear", - [MarketProduct(rd(hours=1), 1, rd(hours=1))], + name=market_name, + opening_hours=rr.rrule(rr.HOURLY, dtstart=start, until=end), + opening_duration=rd(hours=1), + market_mechanism="pay_as_clear", + market_products=[MarketProduct(rd(hours=1), 1, rd(hours=1))], ) clock = ExternalClock(0) container = await create_container(addr=("0.0.0.0", 9098), clock=clock) diff --git a/tests/test_simple_market_mechanisms.py b/tests/test_simple_market_mechanisms.py index 106a7962..3ad038f7 100644 --- a/tests/test_simple_market_mechanisms.py +++ b/tests/test_simple_market_mechanisms.py @@ -14,7 +14,7 @@ from .utils import create_orderbook, extend_orderbook simple_dayahead_auction_config = MarketConfig( - "simple_dayahead_auction", + name="simple_dayahead_auction", market_products=[MarketProduct(rd(hours=+1), 1, rd(hours=1))], additional_fields=["node_id"], opening_hours=rr.rrule( diff --git a/tests/test_units.py b/tests/test_units.py index 8609d851..708c59cf 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -60,16 +60,14 @@ def test_minmaxcharge(): # stay turned off assert ( - mmc.calculate_ramp_charge( - 0.5, previous_power=0, power_charge=0, current_power=0 - ) + mmc.calculate_ramp_charge(previous_power=0, power_charge=0, current_power=0) == 0 ) # stay turned off assert ( mmc.calculate_ramp_discharge( - 0.5, previous_power=0, power_discharge=0, current_power=0 + previous_power=0, power_discharge=0, current_power=0 ) == 0 ) diff --git a/tests/test_units_operator.py b/tests/test_units_operator.py index 925db076..06e5cfe6 100644 --- a/tests/test_units_operator.py +++ b/tests/test_units_operator.py @@ -27,11 +27,11 @@ async def units_operator() -> UnitsOperator: market_name = "Test" marketconfig = MarketConfig( - market_name, - rr.rrule(rr.HOURLY, dtstart=start, until=end), - rd(hours=1), - "pay_as_clear", - [MarketProduct(rd(hours=1), 1, rd(hours=1))], + name=market_name, + opening_hours=rr.rrule(rr.HOURLY, dtstart=start, until=end), + opening_duration=rd(hours=1), + market_mechanism="pay_as_clear", + market_products=[MarketProduct(rd(hours=1), 1, rd(hours=1))], ) clock = ExternalClock(0) container = await create_container(addr=("0.0.0.0", 9098), clock=clock) diff --git a/tests/test_utils.py b/tests/test_utils.py index 643c60cf..774fde6a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -41,11 +41,13 @@ def test_make_market_config(): start = datetime(2020, 1, 1) end = datetime(2020, 12, 2) mc = MarketConfig( - market_name, - rr.rrule(rr.HOURLY, dtstart=start, until=end), - pd.Timedelta(hours=1), - "pay_as_clear", - [MarketProduct(pd.Timedelta(hours=1), 1, pd.Timedelta(hours=1))], + name=market_name, + opening_hours=rr.rrule(rr.HOURLY, dtstart=start, until=end), + opening_duration=pd.Timedelta(hours=1), + market_mechanism="pay_as_clear", + market_products=[ + MarketProduct(pd.Timedelta(hours=1), 1, pd.Timedelta(hours=1)) + ], ) market_params = { "operator": "EOM_operator",