Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions assume/world.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import time
from datetime import datetime
from pathlib import Path
import warnings

from mango import (
RoleAgent,
Expand Down Expand Up @@ -698,6 +699,79 @@ def add_market(self, market_operator_id: str, market_config: MarketConfig) -> No
market_operator.markets.append(market_config)
self.markets[f"{market_config.market_id}"] = market_config

def _validate_setup(self):
""" Pre-empt invalid/unclear states, by detecting defective setups. """

# Integrity of schedule.
for market_id, market_config in self.markets.items():
market_start = market_config.opening_hours[0]
market_end = market_config.opening_hours[-1]

if market_start < self.start or market_end > self.end:
msg = (f"Market {market_id} violates world schedule. \n"
f"Market start: {market_start}, end: {market_end}. \n)"
f"World start: {market_start}, end: {market_end}.)")
raise ValueError(msg)
Comment on lines 706 to 714
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe its best to move this to the add_market method so the simulation fails even earlier, when adding an invalid market 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right, that add_market would be the best place for this part if we organize validation locally. Regarding a possible DataValidation class, to me it sounded more like that the validation is intended to be done centrally for all data.


# Integrity of reference.
# For each UnitOperator: strategies must reference existing markets.
unit_operators = list(self.unit_operators.values())
for operator in unit_operators:
for market_id in operator.portfolio_strategies.keys():
if market_id not in list(self.markets.keys()):
msg = (f"Strategies of unit operator {operator} references"
f"market {market_id} which is not known in world."
f"Known markets are:\n{list(self.markets.keys())}.")
raise ValueError(msg)
Comment on lines +718 to +725
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above, this may be moved to the add_unit_operator method


# Each market should be referenced by a market strategy.
referenced_markets = {market
for market in operator.portfolio_strategies.keys()
for operator in unit_operators}
for market_id in self.markets.keys():
if not market_id in referenced_markets:
msg = f"Added market {market_id}, has no participants."
warnings.warn(msg)
Comment on lines +731 to +734
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this part is already ensured by the unit_operator constructor, where a new unit operator references all available markets automatically (i may be wrong though)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. You are right, there is a similar logic in UnitsOperator.__init__. However (and this is why the corresponding test works): You theoretically can add a market to World after you added all UnitsOperators.

This makes we wonder what the purpose of the respective logic in UnitsOperator.__init__ is.

for market in self.available_markets:
            if market.market_id not in self.portfolio_strategies.keys():
                self.portfolio_strategies[market.market_id] = (
                    DirectUnitOperatorStrategy()
                )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You theoretically can add a market to World after you added all UnitsOperators.

Yes, theoretically - it is currently not supported though. So one first has to create the markets and then add the participants.

Fixing this would require to somehow inject the information of available markets prior to the start of the simulation into all UnitsOperators, which would add another layer of magic/complexity to the simulation.

Copy link
Contributor Author

@Graf-Wronski Graf-Wronski Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little confused. Does "Not supported" mean a code like this should raise an Exception?

world = setup_simple_world()

@pytest.fixture
def demand():
    return Demand(
        id="test_unit",
        unit_operator="test_operator",
        min_power=0,
        max_power=-1000,
        technology="demand",
        bidding_strategies={},
        forecaster=DemandForecaster(index, demand=-100),
    )


def test_addition_order(demand):
    """ Add participants before market. """
    
    
    world.add_unit_operator(
        "unit_operator",
        {"dispatch": "naive_eom"})
    world.add_unit_instance(
        operator_id="unit_operator",
        unit=demand)
    
    opening_dispatch = rr.rrule(rr.HOURLY, dtstart=world.start, until=world.end)
    
    market_products= [MarketProduct(
        duration=datetime.timedelta(hours=1),
        count=1,
        first_delivery=datetime.timedelta(hours=1))]
    
    dispatch_config = MarketConfig(
        "dispatch",
        opening_hours=opening_dispatch,
        market_products=market_products)
    
    world.add_market_operator("market_operator")
    world.add_market(
        market_operator_id="market_operator",
        market_config=dispatch_config)
    
    world.run()


# Real-world integrity.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be perfect for the post_init method of the config, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After creation of the market, you do not know if there will be participants.
After creation of a single agent, you do not know if there will be further agents.
So the only place to check if there is any UnitsOperator, which has a strategy for this market configured is right prior to the run execution call of the simulation, by iterating over all units of all UnitsOperators and checking the BiddingStrategies..

# A Re-Dispatch market can only open if an earlier market closed.
dispatch_markets = [config for config in self.markets.values()
if config.market_mechanism != "redispatch"]
redispatch_markets = [config for config in self.markets.values()
if config.market_mechanism == "redispatch"]

if len(redispatch_markets) > 0:
if len(dispatch_markets) == 0:
msg = "Redispatch market but no dispatch market was defined."
raise ValueError(msg)

earliest_dispatch_closing = min(x.opening_hours[-1]
for x in dispatch_markets)
earliest_redispatch_opening = min(x.opening_hours[0]
for x in redispatch_markets)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic will not work for markets that open multiple times.

x.opening_hours[0] + x.opening_duration

seems to be a applicable alterantive.

if earliest_redispatch_opening <= earliest_dispatch_closing:
msg = (f"First redispatch market opens before first dispatch "
f"market has closed.")
raise ValueError(msg)

# Existence of demand implies existence of generation and vice versa.
demand_exists, generation_exists = False, False
for operator in unit_operators:
for unit in operator.units.values():
# ToDo: Are the defintions of demand and generation exhaustive?
if type(unit) in [self.unit_types["demand"]]:
demand_exists = True
elif type(unit) in [self.unit_types["power_plant"],
self.unit_types["hydrogen_plant"]]:
generation_exists = True
if demand_exists and not generation_exists:
msg = "Demand units but no generation units were created. "
warnings.warn(msg)
elif generation_exists and not demand_exists:
msg = "Generation units but no demand units were created. "
warnings.warn(msg)

async def _step(self, container):
"""
Executes a simulation step for the container.
Expand Down Expand Up @@ -737,6 +811,8 @@ async def async_run(self, start_ts: datetime, end_ts: datetime):
start_ts (datetime.datetime): The start timestamp for the simulation run.
end_ts (datetime.datetime): The end timestamp for the simulation run.
"""
self._validate_setup()

logger.debug("activating container")
# agent is implicit added to self.container._agents
async with activate(self.container) as c:
Expand Down Expand Up @@ -784,6 +860,8 @@ def run(self):
container.
"""

self._validate_setup()

start_ts = datetime2timestamp(self.start)
end_ts = datetime2timestamp(self.end)

Expand Down
229 changes: 229 additions & 0 deletions tests/test_world_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# SPDX-FileCopyrightText: ASSUME Developers
#
# SPDX-License-Identifier: AGPL-3.0-or-later

import pandas as pd
import pytest

import datetime

from assume.common.market_objects import MarketConfig, MarketProduct
from assume.strategies.naive_strategies import NaiveSingleBidStrategy
from assume.units.powerplant import PowerPlant

from assume.common.forecaster import DemandForecaster, PowerplantForecaster
from assume.units.demand import Demand
from tests.utils import index, setup_simple_world

import dateutil.rrule as rr

import warnings


world = setup_simple_world()


@pytest.fixture
def demand():
return Demand(
id="test_unit",
unit_operator="test_operator",
min_power=0,
max_power=-1000,
technology="demand",
bidding_strategies={},
forecaster=DemandForecaster(index, demand=-100),
)


@pytest.fixture
def power_plant():
params_dict = {
"bidding_strategies": {"EOM": NaiveSingleBidStrategy()},
"technology": "energy",
"unit_operator": "test_operator",
"max_power": 10,
"min_power": 0,
"forecaster": PowerplantForecaster(index),
}
return PowerPlant("testdemand", **params_dict)


@pytest.fixture
def grid_data():
""" A simple mock grid. """

bus_data = {"name": ["node1"], "v_nom": [1.0]}
buses = pd.DataFrame(bus_data).set_index("name")
line_data = {}
lines = pd.DataFrame(line_data)
generator_data = {"name": "gen1", "node": ["node1"], "max_power": [5.0]}
generators = pd.DataFrame(generator_data).set_index("name")
load_data = {"name": "load1", "node": ["node1"], "max_power": [5.0]}
loads = pd.DataFrame(load_data).set_index("name")

return {"buses": buses,
"lines": lines,
"generators": generators,
"loads": loads}

def test_warning_no_generation(demand):
""" Running a Wworld with demand but no generation raises a Warning. """
world.add_unit_operator("test_operator")
world.add_unit_instance(operator_id="test_operator", unit=demand)
with pytest.warns(UserWarning) as record:
world.run()
assert len(record) > 0


def test_warning_no_demand(power_plant):
""" Running a World with generation but no demand raises a Warning. """
world.add_unit_operator("test_operator")
world.add_unit_instance(operator_id="test_operator", unit=power_plant)
with pytest.warns(UserWarning) as record:
world.run()
assert len(record) > 0


def test_market_too_early():
""" A market that opens before simulation time, raises an Error. """
world.add_market_operator("test_operator")
market_start = world.start - datetime.timedelta(hours=1)
market_end = world.end
market_opening = rr.rrule(rr.HOURLY, dtstart=market_start, until=market_end)
market_products= [MarketProduct(datetime.timedelta(hours=1),
1,
datetime.timedelta(hours=1))]
market_config = MarketConfig(
"test_EOM",
opening_hours=market_opening,
market_products=market_products)

world.add_market(
market_operator_id="test_operator",
market_config=market_config)

with pytest.raises(ValueError):
world.run()


def test_market_too_late():
""" A market that closes after simulation time, raises an Error. """
world.add_market_operator("test_operator")
market_start = world.start
market_end = world.end + datetime.timedelta(hours=1)
market_opening = rr.rrule(rr.HOURLY, dtstart=market_start, until=market_end)
market_products= [MarketProduct(
duration=datetime.timedelta(hours=1),
count=1,
first_delivery=datetime.timedelta(hours=1))]
market_config = MarketConfig(
"test_EOM",
opening_hours=market_opening,
market_products=market_products)

world.add_market(
market_operator_id="test_operator",
market_config=market_config)

with pytest.raises(ValueError):
world.run()


def test_refering_non_existant_market():
"""A UnitOperator referencing a non-existant Market, raises an Error. """

strategies = {"none_existant_market": "naive_eom"}
world.add_unit_operator("unit_operator", strategies)

with pytest.raises(ValueError):
world.run()


def test_non_referred_market():
""" A Market referred by no UnitOperator raises a Warning. """
world.add_market_operator("market_operator")
world.add_unit_operator("unit_operator")
market_opening = rr.rrule(rr.HOURLY, dtstart=world.start, until=world.end)
market_products= [MarketProduct(
duration=datetime.timedelta(hours=1),
count=1,
first_delivery=datetime.timedelta(hours=1))]
market_config = MarketConfig(
"test_EOM",
opening_hours=market_opening,
market_products=market_products)

world.add_market(
market_operator_id="market_operator",
market_config=market_config)

with pytest.warns(UserWarning) as record:
world.run()
assert len(record) > 0


def test_redispatch_no_dispatch(grid_data):
""" A redispatch without a dispatch market raises an Error. """

world.add_market_operator("market_operator")
world.add_unit_operator("unit_operator")
market_opening = rr.rrule(rr.HOURLY, dtstart=world.start, until=world.end)
market_products= [MarketProduct(
duration=datetime.timedelta(hours=1),
count=1,
first_delivery=datetime.timedelta(hours=1))]

redispatch_config = MarketConfig(
"redispatch",
market_mechanism="redispatch",
opening_hours=market_opening,
market_products=market_products,
param_dict={"grid_data": grid_data},
additional_fields=["node", "max_power", "min_power"])

world.add_market(
market_operator_id="market_operator",
market_config=redispatch_config)

with pytest.raises(ValueError):
world.run()


def test_redispatch_too_early(grid_data):
""" A redispatch market opening before dispatch closure raises an Error. """

world.add_market_operator("market_operator")
world.add_unit_operator("unit_operator")
opening_dispatch = rr.rrule(rr.HOURLY, dtstart=world.start, until=world.end)
opening_redispatch = opening_dispatch

market_products= [MarketProduct(
duration=datetime.timedelta(hours=1),
count=1,
first_delivery=datetime.timedelta(hours=1))]

redispatch_config = MarketConfig(
"dispatch",
opening_hours=opening_dispatch,
market_products=market_products)

dispatch_config = MarketConfig(
"redispatch",
market_mechanism="redispatch",
opening_hours=opening_redispatch,
market_products=market_products,
param_dict={"grid_data": grid_data},
additional_fields=["node", "max_power", "min_power"])

world.add_market(
market_operator_id="market_operator",
market_config=dispatch_config)

world.add_market(
market_operator_id="market_operator",
market_config=redispatch_config)

with pytest.raises(ValueError):
world.run()

Loading