diff --git a/assume/common/base.py b/assume/common/base.py index 7a449944..6cade862 100644 --- a/assume/common/base.py +++ b/assume/common/base.py @@ -743,40 +743,58 @@ class LearningConfig(TypedDict): :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 gamme: The discount factor. + + :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 """ diff --git a/assume/common/forecasts.py b/assume/common/forecasts.py index ebb3ac01..5bf80d73 100644 --- a/assume/common/forecasts.py +++ b/assume/common/forecasts.py @@ -10,13 +10,16 @@ class Forecaster: """ - A Forecaster can provide timeseries for forecasts which are derived either from existing files, random noise or actual forecast methods. + A Forecaster can provide timeseries for forecasts which are derived either from existing files, + random noise, or actual forecast methods. - :param index: the index of the forecasts - :type index: pd.Series + Args: + - index (pd.Series): The index of the forecasts. - Methods - ------- + 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. """ def __init__(self, index: pd.Series): @@ -26,34 +29,46 @@ def __getitem__(self, column: str) -> pd.Series: """ Returns the forecast for a given column. - :param column: the column of the forecast - :type column: str - :return: the forecast - :rtype: pd.Series + Args: + - column (str): The column of the forecast. + + Returns: + - pd.Series: The forecast. + + This method returns the forecast for a given column as a pandas Series based on the provided index. """ + return pd.Series(0, self.index) def get_availability(self, unit: str): """ Returns the availability of a given unit. - :param unit: the unit - :type unit: str - :return: the availability of the unit - :rtype: pd.Series + Args: + - unit (str): The unit. + + 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. """ + return self[f"availability_{unit}"] def get_price(self, fuel_type: str): """ - Returns the price for a given fuel_type - or zeros if type does not exist + Returns the price for a given fuel type or zeros if the type does not exist. + + Args: + - fuel_type (str): The fuel type. - :param fuel_type: the fuel type - :type fuel_type: str - :return: the price of the fuel - :rtype: pd.Series + 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. """ + return self[f"fuel_price_{fuel_type}"] @@ -61,13 +76,13 @@ class CsvForecaster(Forecaster): """ A Forecaster that reads forecasts from csv files. - :param index: the index of the forecasts - :type index: pd.Series - :param powerplants: the powerplants - :type powerplants: dict[str, pd.Series] + Args: + - index (pd.Series): The index of the forecasts. + - powerplants (dict[str, pd.Series]): The power plants. - Methods - ------- + 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. """ def __init__( @@ -79,6 +94,18 @@ def __init__( self.forecasts = pd.DataFrame(index=index) def __getitem__(self, column: str) -> pd.Series: + """ + Returns the forecast for a given column. + + Args: + - column (str): The column of the forecast. + + 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: if "availability" in column: return pd.Series(1, self.index) @@ -89,11 +116,14 @@ def set_forecast(self, data: pd.DataFrame | pd.Series | None, prefix=""): """ Sets the forecast for a given column. - :param data: the forecast - :type data: pd.DataFrame | pd.Series | None - :param prefix: the prefix of the column - :type prefix: str + 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. """ + if data is None: return elif isinstance(data, pd.DataFrame): @@ -117,7 +147,11 @@ def set_forecast(self, data: pd.DataFrame | pd.Series | None, prefix=""): 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". """ + cols = [] for pp in self.powerplants.index: col = f"availability_{pp}" @@ -136,13 +170,19 @@ def calc_forecast_if_needed(self): def get_registered_market_participants(self, market_id): """ - get information about market participants to make accurate price forecast + Retrieves information about market participants for accurate price forecast. + + Args: + - market_id (str): The market ID. + + Returns: + - pd.DataFrame: The registered market participants. - :param market_id: the market id - :type market_id: str - :return: the registered market participants - :rtype: pd.DataFrame + 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( "Functionality of using the different markets and specified registration for the price forecast is not implemented yet" ) @@ -152,9 +192,23 @@ def calculate_residual_demand_forecast(self): """ Calculates the residual demand forecast. - :return: the residual demand forecast - :rtype: pd.Series + 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: + 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. + 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. + + Returns the resulting residual demand forecast as a pandas Series. """ + vre_powerplants = self.powerplants[ self.powerplants["technology"].isin( ["wind_onshore", "wind_offshore", "solar"] @@ -174,13 +228,28 @@ def calculate_residual_demand_forecast(self): def calculate_EOM_price_forecast(self): """ - Function that calculates the merit order price, which is given as a price forecast to the Rl agents - Here for the entire time horizon at once - TODO make price forecasts for all markets, not just specified for the DAM like here - TODO consider storages? + Calculates the merit order price forecast for the entire time horizon. + + 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. + + 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. + + TODO: + - Extend price forecasts for all markets, not just specified for the DAM. + - Consider the inclusion of storages in the price forecast calculation. - :return: the merit order price forecast - :rtype: pd.Series + Returns the resulting merit order price forecast as a pandas Series. """ # calculate infeed of renewables and residual demand_df @@ -212,13 +281,33 @@ 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. - - :param pp_series: Series with power plant data - :type pp_series: pd.Series - :return: the marginal cost of the power plant - :rtype: float + Calculates the marginal cost of a power plant based on the fuel costs and efficiencies of the + power plant. + + Args: + - pp_series (pd.Series): Series containing power plant data. + + 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: + 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. + 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. + + Returns the resulting marginal cost as a float value. """ + fp_column = f"fuel_price_{pp_series.fuel_type}" if fp_column in self.forecasts.columns: fuel_price = self.forecasts[fp_column] @@ -240,9 +329,13 @@ def save_forecasts(self, path): """ Saves the forecasts to a csv file. - :param path: the path to save the forecasts to - :type path: str + 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: self.forecasts.to_csv(f"{path}/forecasts_df.csv", index=True) except ValueError: @@ -255,15 +348,14 @@ class RandomForecaster(CsvForecaster): """ A forecaster that generates forecasts using random noise. - :param index: the index of the forecasts - :type index: pd.Series - :param powerplants: the powerplants - :type powerplants: dict[str, pd.Series] - :param sigma: the standard deviation of the noise - :type sigma: float + 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. - Methods - ------- + 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. """ def __init__( @@ -278,6 +370,20 @@ def __init__( super().__init__(index, powerplants, *args, **kwargs) def __getitem__(self, column: str) -> pd.Series: + """ + Returns the forecast for a given column modified by random noise. + + Args: + - column (str): The column of the forecast. + + 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: return pd.Series(0, self.index) noise = np.random.normal(0, self.sigma, len(self.index)) @@ -288,21 +394,19 @@ class NaiveForecast(Forecaster): """ A forecaster that generates forecasts using naive methods. - :param index: the index of the forecasts - :type index: pd.Series - :param availability: the availability of the power plants - :type availability: float | list - :param fuel_price: the fuel price - :type fuel_price: float | list - :param co2_price: the co2 price - :type co2_price: float | list - :param demand: the demand - :type demand: float | list - :param price_forecast: the price forecast - :type price_forecast: float | list - - Methods - ------- + Args: + - index (pd.Series): The index of the forecasts. + + Optional Args: + - availability (float | list): The availability of the power plants. + - fuel_price (float | list): The fuel price. + - co2_price (float | list): The CO2 price. + - 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. """ def __init__( @@ -324,6 +428,21 @@ def __init__( self.price_forecast = price_forecast def __getitem__(self, column: str) -> pd.Series: + """ + Get the forecasted values for a specific column. + + Args: + - column (str): The column for which forecasted values are requested. + + 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: value = self.availability elif column == "fuel_price_co2": diff --git a/assume/units/storage.py b/assume/units/storage.py index 6ed64902..8b63e55c 100644 --- a/assume/units/storage.py +++ b/assume/units/storage.py @@ -71,8 +71,6 @@ class Storage(SupportsMinMaxCharge): The minimum operating time of the storage unit in hours. min_down_time : float, optional The minimum down time of the storage unit in hours. - min_down_time : float, optional - The minimum down time of the storage unit in hours. is_active: bool Defines whether or not the unit bids itself or is portfolio optimized. bidding_startegy: str diff --git a/assume/world.py b/assume/world.py index a91f86aa..eae41cc7 100644 --- a/assume/world.py +++ b/assume/world.py @@ -50,6 +50,27 @@ def __init__( log_level: str = "INFO", distributed_role: bool = None, ): + """ + Initialize a World instance. + + 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. + + 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. + """ + logging.getLogger("assume").setLevel(log_level) self.logger = logging.getLogger(__name__) self.addr = addr @@ -116,6 +137,32 @@ async def setup( manager_address=None, **kwargs, ): + """ + Set up the environment for the simulation. + + 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. + + Other Parameters: + - 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. + """ + self.clock = ExternalClock(0) self.start = start self.end = end @@ -167,6 +214,21 @@ async def setup( ) async def setup_learning(self): + """ + Set up the learning process for the simulation. + + Args: + - 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. + """ + self.bidding_params.update(self.learning_config) if self.learning_mode: @@ -186,7 +248,23 @@ 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): - # Add output agent to world + """ + Set up the output agent for the simulation. + + 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. + + 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. + """ + self.logger.debug(f"creating output agent {self.db=} {self.export_csv_path=}") self.output_role = WriteOutput( simulation_id=simulation_id, @@ -221,13 +299,21 @@ def add_unit_operator( id: str, ) -> None: """ - Create and add a new unit operator to the world. + Add a unit operator to the simulation. + + Args: + - self: The instance of the class. + - id (str): The identifier for the unit operator. - Params - ------ - id: str or int + 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. """ + if self.unit_operators.get(id): raise ValueError(f"Unit operator {id} already exists") @@ -265,15 +351,23 @@ async def async_add_unit( forecaster: Forecaster, ) -> None: """ - Create and add a new unit to the world. - - Params - ------ - id: str - unit_type: str - unit_operator_id: str - unit_params: dict - + Asynchronously add a unit to the simulation. + + 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. + + 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. """ # check if unit operator exists @@ -325,15 +419,23 @@ async def async_add_unit( def add_market_operator( self, id: str, - ): + ) -> None: """ - creates the market operator + Add a market operator to the simulation. + + Args: + - self: The instance of the class. + - id (str): The identifier for the market operator. - Params - ------ - id = int - market operator id is associated with the market its participating + 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. """ + if self.market_operators.get(id): raise ValueError(f"MarketOperator {id} already exists") market_operator_agent = RoleAgent( @@ -357,17 +459,24 @@ def add_market( self, market_operator_id: str, market_config: MarketConfig, - ): + ) -> None: """ - including the markets in the market container - - Params - ------ - id = int - ID of the operator - marketconfig = - describes the configuration of a market + Add a market to the simulation. + + 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. + + 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. """ + if mm_class := self.clearing_mechanisms.get(market_config.market_mechanism): market_role = mm_class(market_config) else: @@ -397,12 +506,25 @@ async def _step(self): async def async_run(self, start_ts, end_ts): """ - Run the simulation. - either in learning mode where we run multiple times or in normal mode + Run the simulation asynchronously. + + 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. + + 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. """ # agent is implicit added to self.container._agents - pbar = tqdm(total=end_ts - start_ts) # allow registration before first opening @@ -424,6 +546,22 @@ async def async_run(self, start_ts, end_ts): await self.container.shutdown() 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. + + """ + start_ts = calendar.timegm(self.start.utctimetuple()) end_ts = calendar.timegm(self.end.utctimetuple()) @@ -435,6 +573,16 @@ def run(self): pass def reset(self): + """ + Reset the World instance. + + Returns: + - None + + This method resets the market operators, markets, unit operators, and forecast + providers to empty dictionaries. + """ + self.market_operators = {} self.markets = {} self.unit_operators = {} @@ -448,6 +596,25 @@ def add_unit( unit_params: dict, forecaster: Forecaster, ) -> None: + """ + Add a unit to the World instance. + + 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. + + 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. + """ + return self.loop.run_until_complete( self.async_add_unit( id=id, diff --git a/docs/source/_static/template.ccs b/docs/source/_static/template.ccs new file mode 100644 index 00000000..75ef5102 --- /dev/null +++ b/docs/source/_static/template.ccs @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: ASSUME Developers + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +.wy-side-nav-search { + background-color: #eeeeee; +} + +.wy-side-nav-search .wy-dropdown>a, +.wy-side-nav-search>a { + color: rgb(34, 97, 156) +} + +.wy-side-nav-search>div.version { + color: rgb(34, 97, 156) +} + +.wy-menu-vertical header, +.wy-menu-vertical p.caption, +.rst-versions a { + color: #999999; +} + +.wy-menu-vertical a.reference:hover, +.wy-menu-vertical a.reference.internal:hover { + background: #dddddd; + color: #fff; +} + +.wy-nav-side { + background: #efefef; +} + +.wy-menu-vertical a.reference { + color: #000; +} + +.rst-versions .rst-current-version, +.wy-nav-top, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a:hover { + background: #002221; +} + +.wy-nav-content .highlight { + background: #ffffff; +} + +.rst-content code.literal, +.rst-content tt.literal { + color: rgb(34, 97, 156) +} + +.wy-nav-content a.reference { + color: rgb(34, 97, 156); +} + + +/* override table width restrictions */ + +@media screen and (min-width: 767px) { + .wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from overriding + this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + background: rgb(250, 250, 250) !important; + } + .wy-table-responsive { + max-width: 100%; + overflow: visible !important; + } + .wy-nav-content { + max-width: 910px !important; + } +} diff --git a/docs/source/assume.common.rst b/docs/source/assume.common.rst index 2362513b..f994ba8b 100644 --- a/docs/source/assume.common.rst +++ b/docs/source/assume.common.rst @@ -2,8 +2,8 @@ .. .. SPDX-License-Identifier: AGPL-3.0-or-later -assume.common package -===================== +Common components and methods +============================= Submodules ---------- diff --git a/docs/source/assume.markets.clearing_algorithms.rst b/docs/source/assume.markets.clearing_algorithms.rst index 7f58853c..e3cf1501 100644 --- a/docs/source/assume.markets.clearing_algorithms.rst +++ b/docs/source/assume.markets.clearing_algorithms.rst @@ -2,8 +2,8 @@ .. .. SPDX-License-Identifier: AGPL-3.0-or-later -assume.markets.clearing\_algorithms package -=========================================== +Market clearing algorithms +========================== Submodules ---------- diff --git a/docs/source/assume.markets.rst b/docs/source/assume.markets.rst index 66e69e4e..d7f53231 100644 --- a/docs/source/assume.markets.rst +++ b/docs/source/assume.markets.rst @@ -2,8 +2,8 @@ .. .. SPDX-License-Identifier: AGPL-3.0-or-later -assume.markets package -====================== +Market Classes +============== Subpackages ----------- diff --git a/docs/source/assume.reinforcement_learning.rst b/docs/source/assume.reinforcement_learning.rst index cb7915ef..4daee7dd 100644 --- a/docs/source/assume.reinforcement_learning.rst +++ b/docs/source/assume.reinforcement_learning.rst @@ -2,8 +2,8 @@ .. .. SPDX-License-Identifier: AGPL-3.0-or-later -assume.reinforcement\_learning package -====================================== +Reinforcement learning algorithms +=================================== Submodules ---------- diff --git a/docs/source/assume.rst b/docs/source/assume.rst index c1536baa..78bdba4b 100644 --- a/docs/source/assume.rst +++ b/docs/source/assume.rst @@ -2,44 +2,47 @@ .. .. SPDX-License-Identifier: AGPL-3.0-or-later -assume package +API Reference ============== -Subpackages ------------ +World module +------------ +.. toctree:: + :maxdepth: 4 + + assume.world +Market classes +-------------- .. toctree:: :maxdepth: 4 - assume.common assume.markets - assume.reinforcement_learning - assume.strategies - assume.units -Submodules ----------- +Units +------ +.. toctree:: + :maxdepth: 4 + + assume.units -assume.cli module ------------------ +Bidding strategies +------------------ +.. toctree:: + :maxdepth: 4 -.. automodule:: assume.cli - :members: - :undoc-members: - :show-inheritance: + assume.strategies -assume.world module -------------------- +Reinforcement learning algorithms +---------------------------------- +.. toctree:: + :maxdepth: 4 -.. automodule:: assume.world - :members: - :undoc-members: - :show-inheritance: + assume.reinforcement_learning -Module contents ---------------- +Common components and methods +----------------------------- +.. toctree:: + :maxdepth: 4 -.. automodule:: assume - :members: - :undoc-members: - :show-inheritance: + assume.common diff --git a/docs/source/assume.strategies.rst b/docs/source/assume.strategies.rst index 00020e6f..0e366b29 100644 --- a/docs/source/assume.strategies.rst +++ b/docs/source/assume.strategies.rst @@ -2,8 +2,8 @@ .. .. SPDX-License-Identifier: AGPL-3.0-or-later -assume.strategies package -========================= +Bidding strategies +=================== Submodules ---------- diff --git a/docs/source/assume.units.rst b/docs/source/assume.units.rst index 3a4944ff..928910d8 100644 --- a/docs/source/assume.units.rst +++ b/docs/source/assume.units.rst @@ -2,8 +2,8 @@ .. .. SPDX-License-Identifier: AGPL-3.0-or-later -assume.units package -==================== +Units +===== Submodules ---------- diff --git a/docs/source/assume.world.rst b/docs/source/assume.world.rst new file mode 100644 index 00000000..51f4f30a --- /dev/null +++ b/docs/source/assume.world.rst @@ -0,0 +1,11 @@ +.. SPDX-FileCopyrightText: ASSUME Developers +.. +.. SPDX-License-Identifier: AGPL-3.0-or-later + +World module +============ + +.. automodule:: assume.world + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index 4d438e45..2f4da38f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,8 +10,8 @@ copyright = "2022-2023 ASSUME Developers" author = "ASSUME Developers" -release = "0.1" -version = "0.1.0" +release = "0.2" +version = "0.2.1" # -- General configuration @@ -22,6 +22,12 @@ "sphinx.ext.autosummary", "sphinx.ext.intersphinx", "sphinxcontrib.mermaid", + "sphinx.ext.todo", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "nbsphinx", + "nbsphinx_link", + "sphinx.ext.imgconverter", # for SVG conversion ] intersphinx_mapping = { @@ -61,3 +67,14 @@ # -- Options for EPUB output epub_show_urls = "footnote" + +# -- Options for nbsphinx ------------------------------------------------- +# nbsphinx_kernel_name = 'assume' +nbsphinx_prolog = """ +{% set docname = env.doc2path(env.docname, base=None).replace("nblink", "ipynb").replace("examples/", "examples/notebooks/") %} +.. note:: + + You can `download `_ this example as a Jupyter notebook +""" + +nbsphinx_allow_errors = True diff --git a/docs/source/examples/01_minimal_manual_example.nblink b/docs/source/examples/01_minimal_manual_example.nblink new file mode 100644 index 00000000..a54c02ed --- /dev/null +++ b/docs/source/examples/01_minimal_manual_example.nblink @@ -0,0 +1 @@ +{"path": "../../../examples/notebooks/01_minimal_manual_example.ipynb"} diff --git a/docs/source/examples/01_minimal_manual_example.nblink.license b/docs/source/examples/01_minimal_manual_example.nblink.license new file mode 100644 index 00000000..a6ae0636 --- /dev/null +++ b/docs/source/examples/01_minimal_manual_example.nblink.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: ASSUME Developers + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/docs/source/examples_basic.rst b/docs/source/examples_basic.rst new file mode 100644 index 00000000..7c494fbf --- /dev/null +++ b/docs/source/examples_basic.rst @@ -0,0 +1,16 @@ +.. SPDX-FileCopyrightText: ASSUME Developers +.. +.. SPDX-License-Identifier: AGPL-3.0-or-later + +############ +Basic Usage +############ + + +Here you can find several examples for basic usage of ASSUME framework to get you started: + + +.. toctree:: + :maxdepth: 1 + + examples/01_minimal_manual_example.ipynb diff --git a/docs/source/index.rst b/docs/source/index.rst index 101f8b0f..5335807d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -41,33 +41,55 @@ how to :doc:`installation` the project. Documentation ============= + **Getting Started** * :doc:`introduction` * :doc:`installation` * :doc:`quick_start` + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Getting Started + + introduction + installation + quick_start + +**Examples** + +* :doc:`examples_basic` +* :doc:`example_simulations` + +.. toctree:: + :hidden: + :maxdepth: 2 + :caption: Examples + + examples_basic + example_simulations + +**User Guide** + * :doc:`market_config` * :doc:`market_mechanism` * :doc:`bidding_agents` * :doc:`learning` -* :doc:`example_simulations` * :doc:`assume` .. toctree:: :hidden: - :maxdepth: 4 - :caption: Contents + :maxdepth: 2 + :caption: User Guide - introduction - installation - quick_start market_config market_mechanism bidding_agents - example_simulations learning assume + Indices and tables ================== diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 3449a587..695754c1 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -17,7 +17,7 @@ ASSUME and train your agents, make sure to install Torch. Detailed installation instructions can be found `here `_. Using Pip/Python -============== +================ In general most people use an environment manager for python. Here, Install instructions for Conda and Venv are given. @@ -76,7 +76,7 @@ To install with testing capabilities:: pip install assume-framework[test] Install Tab-Completion ------------------ +---------------------- ASSUME uses `argcomplete` for argument completion on the CLI. diff --git a/examples/examples.py b/examples/examples.py index 3a1b3903..412124e3 100644 --- a/examples/examples.py +++ b/examples/examples.py @@ -58,8 +58,8 @@ - local_db: without database and grafana - timescale: with database and grafana (note: you need docker installed) """ - data_format = "timescale" # "local_db" or "timescale" - example = "learning_small" + data_format = "local_db" # "local_db" or "timescale" + example = "small" if data_format == "local_db": db_uri = f"sqlite:///./examples/local_db/assume_db_{example}.db" diff --git a/examples/notebooks/01_minimal_manual_example.ipynb b/examples/notebooks/01_minimal_manual_example.ipynb new file mode 100644 index 00000000..ce1e6b26 --- /dev/null +++ b/examples/notebooks/01_minimal_manual_example.ipynb @@ -0,0 +1,446 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Minimal manual example\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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting Up the Simulation Environment\n", + "\n", + "First, let's set up the necessary environment and import the required libraries." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import os\n", + "from datetime import datetime, timedelta\n", + "\n", + "import pandas as pd\n", + "from dateutil import rrule as rr\n", + "\n", + "from assume import World\n", + "from assume.common.forecasts import NaiveForecast\n", + "from assume.common.market_objects import MarketConfig, MarketProduct\n", + "\n", + "log = logging.getLogger(__name__)\n", + "\n", + "os.makedirs(\"./local_db\", exist_ok=True)\n", + "\n", + "db_uri = \"sqlite:///./local_db/assume_db_min_example.db\"\n", + "\n", + "world = World(database_uri=db_uri)\n", + "\n", + "start = datetime(2023, 10, 4)\n", + "end = datetime(2023, 12, 5)\n", + "index = pd.date_range(\n", + " start=start,\n", + " end=end + timedelta(hours=24),\n", + " freq=\"H\",\n", + ")\n", + "sim_id = \"world_script_simulation\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this section, we begin by importing the necessary libraries and modules. Additionally, we define the database URI. For this instance, we will utilize a local SQLite database to store our results. In subsequent notebooks, we will transition to using a timescaledb database to store the results, which can then be visualized using the included Grafana dashboards. \n", + "\n", + "Subsequently, we instantiate the `World` class, the primary class responsible for managing the simulation. We also establish the simulation's start and end dates, define the simulation index and step size, and assign a simulation ID. This unique identifier is crucial for referencing the simulation in the database." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initializing the Simulation\n", + "Next, we initialize the simulation by executing the setup function. The setup function 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." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "await world.setup(\n", + " start=start,\n", + " end=end,\n", + " save_frequency_hours=48,\n", + " simulation_id=sim_id,\n", + " index=index,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuring market\n", + "Here, we define a market configuration, set up a market operator, and add the configured market to the simulation world." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "marketdesign = [\n", + " MarketConfig(\n", + " name=\"EOM\",\n", + " opening_hours=rr.rrule(rr.HOURLY, interval=24, dtstart=start, until=end),\n", + " opening_duration=timedelta(hours=1),\n", + " market_mechanism=\"pay_as_clear\",\n", + " market_products=[MarketProduct(timedelta(hours=1), 24, timedelta(hours=1))],\n", + " additional_fields=[\"block_id\", \"link\", \"exclusive_id\"],\n", + " )\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This code segment sets up a market configuration named \"EOM\" with specific opening hours, market mechanism, products, and additional fields, providing the foundation for simulating and analyzing the behavior of this particular electricity market.\n", + "\n", + "In this code:\n", + "- `marketdesign` is a list containing a single market configuration.\n", + "\n", + "- `MarketConfig(...)` defines the configuration for a specific market. In this case, it's named \"EOM\" (End of Month).\n", + "\n", + " - `name=\"EOM\"` - Specifies the name of the market configuration as \"EOM\".\n", + "\n", + " - `opening_hours=rr.rrule(rr.HOURLY, interval=24, dtstart=start, until=end)` - Defines the opening hours for the market using a rule that repeats hourly with a 24-hour interval, starting at `start` and ending at `end`. This indicates that the market operates on a daily basis.\n", + "\n", + " - `opening_duration=timedelta(hours=1)` - Specifies the duration of each market opening as 1 hour.\n", + "\n", + " - `market_mechanism=\"pay_as_clear\"` - Indicates the market mechanism used, in this case, \"pay as clear\", which is a common mechanism in electricity markets where all accepted bids are paid the market-clearing price.\n", + "\n", + " - `market_products=[MarketProduct(timedelta(hours=1), 24, timedelta(hours=1))]` - Defines the market products available. In this case, it seems to be a single product with a duration of 1 hour, 24 periods, and a period duration of 1 hour.\n", + "\n", + " - `additional_fields=[\"block_id\", \"link\", \"exclusive_id\"]` - Specifies additional fields associated with this market configuration, such as \"block_id\", \"link\", and \"exclusive_id\"." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "mo_id = \"market_operator\"\n", + "world.add_market_operator(id=mo_id)\n", + "\n", + "for market_config in marketdesign:\n", + " world.add_market(market_operator_id=mo_id, market_config=market_config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this section, we add a market operator to the simulation world and create a market with previously defined configuration.\n", + "\n", + "In this code:\n", + "- `mo_id = \"market_operator\"` assigns the identifier \"market_operator\" to the market operator.\n", + "\n", + "- `world.add_market_operator(id=mo_id)` adds a market operator to the simulation world with the specified identifier \"market_operator\". A market operator in this context represents an entity responsible for operating and managing one or more markets within the simulation.\n", + "\n", + "- The loop `for market_config in marketdesign:` iterates over the market configurations defined in the `marketdesign` list.\n", + "\n", + " - `world.add_market(market_operator_id=mo_id, market_config=market_config)` associates each market configuration with the market operator identified by \"market_operator\". This effectively adds the specified market configuration to the simulation world under the management of the market operator." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Unit Operators and Units\n", + "\n", + "After initializing the simulation, and creating a market, we add unit operators and units to the simulation world." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "world.add_unit_operator(\"demand_operator\")\n", + "\n", + "demand_forecast = NaiveForecast(index, demand=100)\n", + "\n", + "world.add_unit(\n", + " id=\"demand_unit\",\n", + " unit_type=\"demand\",\n", + " unit_operator_id=\"demand_operator\",\n", + " unit_params={\n", + " \"min_power\": 0,\n", + " \"max_power\": 1000,\n", + " \"bidding_strategies\": {\"energy\": \"naive\"},\n", + " \"technology\": \"demand\",\n", + " },\n", + " forecaster=demand_forecast,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This code segment sets up a demand unit managed by the \"my_demand\" unit operator, equipped with a naive demand forecast, and establishes its operational parameters within the electricity market simulation framework.\n", + "\n", + "In this code:\n", + "- `world.add_unit_operator(\"demand_operator\")` adds a unit operator with the identifier \"my_demand\" to the simulation world. A unit operator manages a group of similar units within the simulation.\n", + "\n", + "- `demand_forecast = NaiveForecast(index, demand=100)` creates a naive demand forecast object named `demand_forecast`. This forecast is initialized with an index and a constant demand value of 100.\n", + "\n", + "- `world.add_unit(...)` adds a demand unit to the simulation world with the following specifications:\n", + "\n", + " - `id=\"demand_unit\"` assigns the identifier \"demand1\" to the demand unit.\n", + "\n", + " - `unit_type=\"demand\"` specifies that this unit is of type \"demand\", indicating that it represents a consumer of electricity.\n", + "\n", + " - `unit_operator_id=\"demand_operator\"` associates the unit with the unit operator identified as \"my_demand\".\n", + "\n", + " - `unit_params` provides various parameters for the demand unit, including minimum and maximum power, bidding strategies, and technology type.\n", + "\n", + " - `forecaster=demand_forecast` associates the demand forecast (`demand_forecast`) with the demand unit, allowing the unit to utilize this forecast for its behavior within the simulation." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "world.add_unit_operator(\"unit_operator\")\n", + "\n", + "nuclear_forecast = NaiveForecast(index, availability=1, fuel_price=3, co2_price=0.1)\n", + "\n", + "world.add_unit(\n", + " id=\"nuclear_unit\",\n", + " unit_type=\"power_plant\",\n", + " unit_operator_id=\"unit_operator\",\n", + " unit_params={\n", + " \"min_power\": 200,\n", + " \"max_power\": 1000,\n", + " \"bidding_strategies\": {\"energy\": \"naive\"},\n", + " \"technology\": \"nuclear\",\n", + " },\n", + " forecaster=nuclear_forecast,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This code segment sets up a nuclear power plant unit managed by the \"unit_operator\" unit operator, equipped with a naive availability and cost forecast, and establishes its operational parameters within the electricity market simulation framework.\n", + "\n", + "In this code:\n", + "- `world.add_unit_operator(\"unit_operator\")` adds a unit operator with the identifier \"unit_operator\" to the simulation world. This unit operator will manage a group of similar units within the simulation.\n", + "\n", + "- `nuclear_forecast = NaiveForecast(index, availability=1, fuel_price=3, co2_price=0.1)` creates a naive forecast for the nuclear power plant. This forecast is initialized with an index, a constant availability of 1, a fuel price of 3, and a CO2 price of 0.1.\n", + "\n", + "- `world.add_unit(...)` adds a nuclear power plant unit to the simulation world with the following specifications:\n", + "\n", + " - `id=\"nuclear_unit\"` assigns the identifier \"nuclear_unit\" to the nuclear power plant unit.\n", + "\n", + " - `unit_type=\"power_plant\"` specifies that this unit is of type \"power_plant\", indicating that it represents a power generation facility.\n", + "\n", + " - `unit_operator_id=\"unit_operator\"` associates the unit with the unit operator identified as \"unit_operator\".\n", + "\n", + " - `unit_params` provides various parameters for the nuclear power plant unit, including minimum and maximum power, bidding strategies, and technology type.\n", + "\n", + " - `forecaster=nuclear_forecast` associates the nuclear forecast (`nuclear_forecast`) with the nuclear power plant unit, allowing the unit to utilize this forecast for its behavior within the simulation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running the Simulation\n", + "\n", + "Finally, we run the simulation to observe the market behaviors and outcomes." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "world_script_simulation 2023-12-05 00:00:00: : 5356801.0it [00:03, 1534875.35it/s] \n" + ] + } + ], + "source": [ + "world.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "In this notebook, we have demonstrated the basic steps involved in setting up and running a simulation using the ASSUME framework for simulating electricity markets. This example is intended to provide a detailed overview of internal workings of the framework and its components. This approach can be used for small simulations with a few agents and markets. In the next notebook we will explore how this process is automated for large scale simulation using input files." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The whole code as a single cell\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 0%| | 0/7689600 [00:00