Note

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

1. Minimal manual tutorial#

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.

Setting Up the Simulation 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, let’s set up the necessary environment and import the required libraries.

[ ]:
import logging
import os
from datetime import datetime, timedelta

import pandas as pd
from dateutil import rrule as rr

from assume import World
from assume.common.forecasts import NaiveForecast
from assume.common.market_objects import MarketConfig, MarketProduct

log = logging.getLogger(__name__)

os.makedirs("./local_db", exist_ok=True)

db_uri = "sqlite:///./local_db/assume_db_min_example.db"

world = World(database_uri=db_uri)

start = datetime(2023, 10, 4)
end = datetime(2023, 12, 5)
index = pd.date_range(
    start=start,
    end=end + timedelta(hours=24),
    freq="H",
)
sim_id = "world_script_simulation"

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.

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.

Initializing the Simulation#

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.

[ ]:
await world.setup(
    start=start,
    end=end,
    save_frequency_hours=48,
    simulation_id=sim_id,
    index=index,
)

Configuring market#

Here, we define a market configuration, set up a market operator, and add the configured market to the simulation world.

[ ]:
marketdesign = [
    MarketConfig(
        market_id="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"],
    )
]

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.

In this code: - marketdesign is a list containing a single market configuration.

  • MarketConfig(...) defines the configuration for a specific market. In this case, it’s named “EOM” (End of Month).

    • name="EOM" - Specifies the name of the market configuration as “EOM”.

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

    • opening_duration=timedelta(hours=1) - Specifies the duration of each market opening as 1 hour.

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

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

    • additional_fields=["block_id", "link", "exclusive_id"] - Specifies additional fields associated with this market configuration, such as “block_id”, “link”, and “exclusive_id”.

[ ]:
mo_id = "market_operator"
world.add_market_operator(id=mo_id)

for market_config in marketdesign:
    world.add_market(market_operator_id=mo_id, market_config=market_config)

In this section, we add a market operator to the simulation world and create a market with previously defined configuration.

In this code: - mo_id = "market_operator" assigns the identifier “market_operator” to the market operator.

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

  • The loop for market_config in marketdesign: iterates over the market configurations defined in the marketdesign list.

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

Adding Unit Operators and Units#

After initializing the simulation, and creating a market, we add unit operators and units to the simulation world.

[ ]:
world.add_unit_operator("demand_operator")

demand_forecast = NaiveForecast(index, demand=100)

world.add_unit(
    id="demand_unit",
    unit_type="demand",
    unit_operator_id="demand_operator",
    unit_params={
        "min_power": 0,
        "max_power": 1000,
        "bidding_strategies": {"EOM": "naive_eom"},
        "technology": "demand",
    },
    forecaster=demand_forecast,
)

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.

In this code: - 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.

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

  • world.add_unit(...) adds a demand unit to the simulation world with the following specifications:

    • id="demand_unit" assigns the identifier “demand1” to the demand unit.

    • unit_type="demand" specifies that this unit is of type “demand”, indicating that it represents a consumer of electricity.

    • unit_operator_id="demand_operator" associates the unit with the unit operator identified as “my_demand”.

    • unit_params provides various parameters for the demand unit, including minimum and maximum power, bidding strategies, and technology type.

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

[ ]:
world.add_unit_operator("unit_operator")

nuclear_forecast = NaiveForecast(index, availability=1, fuel_price=3, co2_price=0.1)

world.add_unit(
    id="nuclear_unit",
    unit_type="power_plant",
    unit_operator_id="unit_operator",
    unit_params={
        "min_power": 200,
        "max_power": 1000,
        "bidding_strategies": {"EOM": "naive_eom"},
        "technology": "nuclear",
    },
    forecaster=nuclear_forecast,
)

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.

In this code: - 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.

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

  • world.add_unit(...) adds a nuclear power plant unit to the simulation world with the following specifications:

    • id="nuclear_unit" assigns the identifier “nuclear_unit” to the nuclear power plant unit.

    • unit_type="power_plant" specifies that this unit is of type “power_plant”, indicating that it represents a power generation facility.

    • unit_operator_id="unit_operator" associates the unit with the unit operator identified as “unit_operator”.

    • unit_params provides various parameters for the nuclear power plant unit, including minimum and maximum power, bidding strategies, and technology type.

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

Running the Simulation#

Finally, we run the simulation to observe the market behaviors and outcomes.

[ ]:
world.run()

Conclusion#

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.

The whole code as a single cell#

[ ]:
import logging
import os
from datetime import datetime, timedelta

import pandas as pd
from dateutil import rrule as rr

from assume import World
from assume.common.forecasts import NaiveForecast
from assume.common.market_objects import MarketConfig, MarketProduct

log = logging.getLogger(__name__)

os.makedirs("./local_db", exist_ok=True)

db_uri = "sqlite:///./local_db/assume_db_min_example.db"

world = World(database_uri=db_uri)

start = datetime(2023, 1, 1)
end = datetime(2023, 3, 31)
index = pd.date_range(
    start=start,
    end=end + timedelta(hours=24),
    freq="H",
)
sim_id = "world_script_simulation"

await world.setup(
    start=start,
    end=end,
    save_frequency_hours=48,
    simulation_id=sim_id,
    index=index,
)

marketdesign = [
    MarketConfig(
        market_id="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"],
    )
]

mo_id = "market_operator"
world.add_market_operator(id=mo_id)

for market_config in marketdesign:
    world.add_market(market_operator_id=mo_id, market_config=market_config)

    world.add_unit_operator("demand_operator")

demand_forecast = NaiveForecast(index, demand=100)

world.add_unit(
    id="demand_unit",
    unit_type="demand",
    unit_operator_id="demand_operator",
    unit_params={
        "min_power": 0,
        "max_power": 1000,
        "bidding_strategies": {"EOM": "naive_eom"},
        "technology": "demand",
    },
    forecaster=demand_forecast,
)

world.add_unit_operator("unit_operator")

nuclear_forecast = NaiveForecast(index, availability=1, fuel_price=3, co2_price=0.1)

world.add_unit(
    id="nuclear_unit",
    unit_type="power_plant",
    unit_operator_id="unit_operator",
    unit_params={
        "min_power": 200,
        "max_power": 1000,
        "bidding_strategies": {"EOM": "naive_eom"},
        "technology": "nuclear",
    },
    forecaster=nuclear_forecast,
)

world.run()