-
Notifications
You must be signed in to change notification settings - Fork 29
Config validation #684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Config validation #684
Changes from 5 commits
a88540a
82c00f6
9db67a4
eebd534
ed42671
2a2a499
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| import time | ||
| from datetime import datetime | ||
| from pathlib import Path | ||
| import warnings | ||
|
|
||
| from mango import ( | ||
| RoleAgent, | ||
|
|
@@ -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) | ||
|
|
||
| # 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see above, this may be moved to the |
||
|
|
||
| # 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. You are right, there is a similar logic in This makes we wonder what the purpose of the respective logic in for market in self.available_markets:
if market.market_id not in self.portfolio_strategies.keys():
self.portfolio_strategies[market.market_id] = (
DirectUnitOperatorStrategy()
)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| # 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) | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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: | ||
|
|
@@ -784,6 +860,8 @@ def run(self): | |
| container. | ||
| """ | ||
|
|
||
| self._validate_setup() | ||
kim-mskw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| start_ts = datetime2timestamp(self.start) | ||
| end_ts = datetime2timestamp(self.end) | ||
|
|
||
|
|
||
| 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() | ||
|
|
There was a problem hiding this comment.
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_marketmethod so the simulation fails even earlier, when adding an invalid market 🤔There was a problem hiding this comment.
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_marketwould be the best place for this part if we organize validation locally. Regarding a possibleDataValidationclass, to me it sounded more like that the validation is intended to be done centrally for all data.