Note

You can download this example as a Jupyter notebook or try it out directly in Google Colab.

2. Run simulation using configuration and input files#

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.

Prerequisites#

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.

Tutorial outline:#

  • Introduction

  • Setting up the environment

  • Creating input files

    • Power plant units

    • Fuel prices

    • Demand units

    • Demand time series

  • Creating a configuration file

  • Running the simulation

  • Adjusting market configuration

  • Conclusion

Setting up the Environment#

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.

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.

[ ]:
!pip install assume-framework

First things first, let’s import the necessary packages and set up our working directories.

[ ]:
# Import necessary packages
import pandas as pd
import logging
import os
import yaml
import numpy as np

# import the main World class and the load_scenario_folder functions from assume
from assume import World
from assume.scenario.loader_csv import load_scenario_folder

# Set up logging
log = logging.getLogger(__name__)

# Define paths for input and output data
csv_path = "outputs"
input_path = "inputs/example_01"

# Create directories if they don't exist
os.makedirs("local_db", exist_ok=True)
os.makedirs(input_path, exist_ok=True)

# Set the random seed for reproducibility
np.random.seed(0)

Creating Input Files#

Power Plant Units#

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.

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.

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.

[ ]:
# Create the data
powerplant_units_data = {
    "name": ["Unit 1", "Unit 2", "Unit 3", "Unit 4"],
    "technology": ["nuclear", "lignite", "hard coal", "combined cycle gas turbine"],
    "bidding_EOM": ["naive_eom", "naive_eom", "naive_eom", "naive_eom"],
    "fuel_type": ["uranium", "lignite", "hard coal", "natural gas"],
    "emission_factor": [0.0, 0.4, 0.3, 0.2],
    "max_power": [1000.0, 1000.0, 1000.0, 1000.0],
    "min_power": [200.0, 200.0, 200.0, 200.0],
    "efficiency": [0.3, 0.5, 0.4, 0.6],
    "additional_cost": [10.3, 1.65, 1.3, 3.5],
    "unit_operator": ["Operator 1", "Operator 2", "Operator 3", "Operator 4"],
}

# Convert to DataFrame and save as CSV
powerplant_units_df = pd.DataFrame(powerplant_units_data)
powerplant_units_df.to_csv(f"{input_path}/powerplant_units.csv", index=False)

Here is a breakdown of each attribute we are including in our dataset:

  • name: A list of unique identifiers for each power plant unit. These names are used to reference the units throughout the simulation.

  • technology: The type of technology each unit uses to generate electricity.

  • bidding_EOM: The strategy that each power plant unit will use when bidding into the Energy Only 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.

  • 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.

  • emission_factor: A numerical value representing the amount of CO2 (or equivalent) emissions produced per unit of electricity generated.

  • max_power: The maximum power output each unit can deliver. This is the upper limit of the unit’s operational capacity.

  • min_power: The minimum stable level of power that each unit can produce while remaining operational.

  • efficiency: A measure of how effectively each unit converts fuel into electricity. This efficienty represent the final efficiency of converting fuel into electricity.

  • additional_cost: The additional operational costs for each unit, such as maintenance and staffing, expressed in currency units per MWh.

  • 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.

Fuel Prices#

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.

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.

[ ]:
# Create the data
fuel_prices_data = {
    "fuel": ["uranium", "lignite", "hard coal", "natural gas", "oil", "biomass", "co2"],
    "price": [1, 2, 10, 25, 40, 20, 25],
}

# Convert to DataFrame and save as CSV
fuel_prices_df = pd.DataFrame(fuel_prices_data).T
fuel_prices_df.to_csv(f"{input_path}/fuel_prices_df.csv", index=True, header=False)

Demand Units#

We also need to define the demand units for our simulation.

[ ]:
# Create the data
demand_units_data = {
    "name": ["demand_EOM"],
    "technology": ["inflex_demand"],
    "bidding_EOM": ["naive_eom"],
    "max_power": [1000000],
    "min_power": [0],
    "unit_operator": ["eom_de"],
}

# Convert to DataFrame and save as CSV
demand_units_df = pd.DataFrame(demand_units_data)
demand_units_df.to_csv(f"{input_path}/demand_units.csv", index=False)

Demand Time Series#

Lastly, we’ll create a time series for the demand.

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.

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.

[ ]:
# Create a datetime index for a week with hourly resolution
date_range = pd.date_range(start="2021-03-01", periods=8 * 24, freq="H")

# Generate random demand values around 2000
demand_values = np.random.normal(loc=2000, scale=200, size=8 * 24)

# Create a DataFrame for the demand profile and save as CSV
demand_profile = pd.DataFrame({"datetime": date_range, "demand_EOM": demand_values})
demand_profile.to_csv(f"{input_path}/demand_df.csv", index=False)

Here’s what each attribute in our dataset represents:

  • 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.

  • 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.

  • bidding_EOM: 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.

  • 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.

  • 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.

  • unit_operator: The entity responsible for the demand unit. In this example, eom_de could represent an electricity market operator in Germany.

Creating a Configuration File#

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.

[ ]:
# Define the config as a dictionary
config_data = {
    "hourly_market": {
        "start_date": "2021-03-01 00:00",
        "end_date": "2021-03-07 00:00",
        "time_step": "1h",
        "save_frequency_hours": 24,
        "markets_config": {
            "EOM": {
                "operator": "EOM_operator",
                "product_type": "energy",
                "opening_frequency": "1h",
                "opening_duration": "1h",
                "products": [{"duration": "1h", "count": 1, "first_delivery": "1h"}],
                "volume_unit": "MWh",
                "price_unit": "EUR/MWh",
                "market_mechanism": "pay_as_clear",
            }
        },
    }
}

# Save the configuration as YAML
with open(f"{input_path}/config.yaml", "w") as file:
    yaml.dump(config_data, file, sort_keys=False)

Here is a breakdown of each key in our configuration file:

  • 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”.

  • 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.

  • 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.

  • 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.

  • markets_config: This key contains a nested dictionary with configurations for specific markets within the hourly market.

    • EOM: This is a sub-key representing a specific market, named EOM.

      • operator: This key specifies the operator of the EOM market, which is “EOM_operator” in this case.

      • product_type: This key defines the type of product being traded in the market. Here, the product type is “energy”.

      • opening_frequency: This key indicates how often the market opens for trading. It is set to “1h”, meaning the market opens every hour.

      • opening_duration: This key specifies the duration for which the market is open during each trading session. It is also set to “1h”.

      • 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.

        • duration: This key defines the delivery duration of the product, which is “1h” in this example.

        • count: This key specifies the number of products available for each trading session. In this case, there is only one product per session.

        • 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.

      • volume_unit: This key defines the unit of measurement for the volume of the product, which is “MWh” (megawatt-hour) in this example.

      • price_unit: This key specifies the unit of measurement for the price of the product, which is “EUR/MWh” (Euros per megawatt-hour).

      • 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.

To read more about available market configuration, please refer to https://assume.readthedocs.io/en/latest/market_config.html.

Running the Simulation#

Now that we have our input files and configuration set up, we can run the simulation.

[ ]:
# define the database uri. In this case we are using a local sqlite database
db_uri = f"sqlite:///local_db/assume_db_example_02.db"

# create world instance
world = World(database_uri=db_uri, export_csv_path=csv_path)

# load scenario by providing the world instance
# the path to the inputs folder and the scenario name (subfolder in inputs)
# and the study case name (which config to use for the simulation)
load_scenario_folder(
    world,
    inputs_path="inputs",
    scenario="example_01",
    study_case="hourly_market",
)

# run the simulation
world.run()

Adjusting Market Configuration#

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.

[ ]:
# Define the new market config
new_market_config = {
    "daily_market": {
        "start_date": "2021-03-01 00:00",
        "end_date": "2021-03-07 00:00",
        "time_step": "1h",
        "save_frequency_hours": 24,
        "markets_config": {
            "EOM": {
                "operator": "EOM_operator",
                "product_type": "energy",
                "opening_frequency": "24h",
                "opening_duration": "1h",
                "products": [{"duration": "1h", "count": 24, "first_delivery": "1h"}],
                "volume_unit": "MWh",
                "price_unit": "EUR/MWh",
                "market_mechanism": "pay_as_clear",
            }
        },
    }
}

# Update the existing configuration
config_data.update(new_market_config)

# Save the updated configuration
with open(f"{input_path}/config.yaml", "w") as file:
    yaml.dump(config_data, file, sort_keys=False)

Running the Simulation Again#

With the updated configuration, we can run the simulation for a different study case, in this case for daily_market configuration.

[ ]:
data_format = "local_db"  # "local_db" or "timescale"

if data_format == "local_db":
    db_uri = f"sqlite:///./local_db/assume_db_example_02.db"

# create world
world = World(database_uri=db_uri, export_csv_path=csv_path)

# load scenario by providing the world instance
# the path to the inputs folder and the scenario name (subfolder in inputs)
# and the study case name (which config to use for the simulation)
load_scenario_folder(
    world,
    inputs_path="inputs",
    scenario="example_01",
    study_case="daily_market",
)

# run the simulation
world.run()

Simulation results#

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.

Conclusion#

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.

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