diff --git a/assume/__init__.py b/assume/__init__.py index 359e409ba..22583ff51 100644 --- a/assume/__init__.py +++ b/assume/__init__.py @@ -5,11 +5,6 @@ from importlib.metadata import version from assume.common import MarketConfig, MarketProduct -from assume.scenario.loader_csv import ( - load_custom_units, - load_scenario_folder, - run_learning, -) from assume.world import World __version__ = version("assume-framework") diff --git a/assume/scenario/__init__.py b/assume/scenario/__init__.py new file mode 100644 index 000000000..2f0e04a39 --- /dev/null +++ b/assume/scenario/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: ASSUME Developers +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from assume.scenario.loader_amiris import read_amiris_yaml +from assume.scenario.loader_csv import load_scenario_folder, run_learning +from assume.scenario.exporter_json import to_gui_json +from assume.scenario.loader_json import load_world_from_gui_json diff --git a/assume/scenario/exporter_json.py b/assume/scenario/exporter_json.py new file mode 100644 index 000000000..0e042cfd7 --- /dev/null +++ b/assume/scenario/exporter_json.py @@ -0,0 +1,259 @@ +# SPDX-FileCopyrightText: ASSUME Developers +# +# SPDX-License-Identifier: AGPL-3.0-or-later +import base64 +import json +from random import randbytes + +from assume import World +from assume.common.base import BaseStrategy, BaseUnit +from assume.common.market_objects import lambda_functions +from assume.strategies import bidding_strategies +from assume.units import ( + Building, + Demand, + Exchange, + HydrogenPlant, + PowerPlant, + SteelPlant, +) + + +def identify_strategy(strategy: BaseStrategy) -> str: + for name, s in bidding_strategies.items(): + if isinstance(strategy, s): + return name + return "" + + +def unit_type(u: BaseUnit) -> str: + if isinstance(u, Demand): + return "demand" + if isinstance(u, PowerPlant): + return "powerplant" + if isinstance(u, Building): + return "building" + if isinstance(u, HydrogenPlant): + return "hydrogen_plant" + if isinstance(u, SteelPlant): + return "steel_plant" + if isinstance(u, Exchange): + return "exchange" + return "storage" + + +def lambda_fn(fn) -> str: + for k, v in lambda_functions.items(): + if fn == v: + return k + return "" + + +def position_left(depth=0) -> dict: + return {"x": 0, "y": depth * 100} + + +def position_right(depth=0) -> dict: + return {"x": 500, "y": depth * 100} + + +def get_id(node_type: str) -> str: + bytes = randbytes(3) + return f"{node_type}_{base64.b64encode(bytes).decode('ascii')}" + + +def _val(field): + if field is None: + return None + if isinstance(field, str) or isinstance(field, int) or isinstance(field, float): + return field + return "TODO" + + +def to_gui_json(w: World) -> str: + nodes, edges = [], [] + tmp_edges = {} + nodes.append( + { + "id": "world", + "type": "world", + "deletable": False, + "data": { + "name": "world", + "save_frequency_hours": w.output_role.save_frequency_hours, + "start": str(w.start), + "end": str(w.end), + "simulation_id": w.simulation_id, + }, + "position": {"x": 250, "y": 0}, + } + ) + for operator_name, operator in w.unit_operators.items(): + operator_id = get_id("unitOperator") + nodes.append( + { + "id": operator_id, + "type": "unitOperator", + "data": { + "name": operator_name, + }, + "position": position_right(1), + } + ) + edges.append( + { + "id": f"world#unitOperator_handle#{operator_id}#world_handle", + "source": "world", + "sourceHandle": "unitOperator_handle", + "target": operator_id, + "targetHandle": "world_handle", + "type": "default", + "data": {"name": f"world-{operator_id}"}, + } + ) + for unit in operator.units.values(): + unit_id = get_id("unit") + nodes.append( + { + "id": unit_id, + "type": "unit", + "data": { + "name": unit.id, + "unitType": unit_type(unit), + "technology": unit.technology, + "min_power": _val(getattr(unit, "min_power", 0)), + "max_power": _val(getattr(unit, "max_power", 0)), + "price": _val(getattr(unit, "price", 0)), + "efficiency": _val(getattr(unit, "efficiency", 1.0)), + "ramp_up": _val(getattr(unit, "ramp_up", 0)), + "ramp_down": _val(getattr(unit, "ramp_down", 0)), + "emission_factor": _val(getattr(unit, "emission_factor", 0)), + "min_operating_time": _val( + getattr(unit, "min_operating_time", 0) + ), + "min_downtime": _val(getattr(unit, "min_downtime", 0)), + "max_power_charge": _val(getattr(unit, "max_power_charge", 0)), + "max_power_discharge": _val( + getattr(unit, "max_power_discharge", 0) + ), + "max_soc": _val(getattr(unit, "max_soc", 0)), + "volume_import": _val(getattr(unit, "volume_import", 0)), + "volume_export": _val(getattr(unit, "volume_export", 0)), + }, + "position": position_right(2), + } + ) + edges.append( + { + "id": f"{operator_id}#unit_handle#{unit_id}#unitOperator_handle", + "source": operator_id, + "sourceHandle": "unit_handle", + "target": unit_id, + "targetHandle": "unitOperator_handle", + "type": "default", + "data": {"name": f"{operator_id}-{unit_id}"}, + } + ) + for market, strategy in unit.bidding_strategies.items(): + e = tmp_edges.get(market, []) + e.append( + { + "unit_id": unit_id, + "strategy": identify_strategy(strategy), + } + ) + tmp_edges[market] = e + for provider_name, provider in w.market_operators.items(): + provider_id = get_id("marketProvider") + nodes.append( + { + "id": provider_id, + "type": "marketProvider", + "data": { + "name": provider_name, + }, + "position": position_left(1), + } + ) + edges.append( + { + "id": f"world#marketProvider_handle#{provider_id}#world_handle", + "source": "world", + "sourceHandle": "marketProvider_handle", + "target": provider_id, + "targetHandle": "world_handle", + "type": "default", + "data": {"name": f"world-{provider_id}"}, + } + ) + for market in provider.markets: + market_id = get_id(market.market_id) + nodes.append( + { + "id": market_id, + "type": "market", + "data": { + "name": market.market_id, + "opening_duration": str(market.opening_duration.seconds // 60), + "market_mechanism": market.market_mechanism, + }, + "position": position_left(2), + } + ) + edges.append( + { + "id": f"{provider_id}#market_handle#{market_id}#marketProvider_handle", + "source": provider_id, + "sourceHandle": "market_handle", + "target": market_id, + "targetHandle": "marketProvider_handle", + "type": "default", + "data": {"name": f"{provider_id}-{market_id}"}, + } + ) + for t in tmp_edges.get(market.market_id, []): + edges.append( + { + "id": f"{t['unit_id']}#market_handle#{market_id}#unit_handle", + "source": t["unit_id"], + "sourceHandle": "market_handle", + "target": market_id, + "targetHandle": "unit_handle", + "type": "unit-market", + "data": { + "name": f"{t['unit_id']}-{market_id}", + "strategy": t["strategy"], + }, + } + ) + for product in market.market_products: + id = get_id("marketProduct") + nodes.append( + { + "id": id, + "type": "marketProduct", + "data": { + "name": id, + "duration": str(product.duration.seconds // 60), + "count": product.count, + "first_delivery": str(product.first_delivery.seconds // 60), + "eligible_lambda_function": lambda_fn( + product.eligible_lambda_function + ), + }, + "position": position_left(3), + } + ) + edges.append( + { + "id": f"{market_id}#marketProduct_handle#{id}#market_handle", + "source": market_id, + "sourceHandle": "marketProduct_handle", + "target": id, + "targetHandle": "market_handle", + "type": "default", + "data": {"name": f"{market_id}-{id}"}, + } + ) + + return json.dumps({"nodes": nodes, "edges": edges}) diff --git a/assume/scenario/loader_json.py b/assume/scenario/loader_json.py new file mode 100644 index 000000000..d84d66006 --- /dev/null +++ b/assume/scenario/loader_json.py @@ -0,0 +1,176 @@ +# SPDX-FileCopyrightText: ASSUME Developers +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import datetime, timedelta + +import pandas as pd +from dateutil import rrule as rr +from dateutil.relativedelta import relativedelta + +from assume import MarketConfig, MarketProduct, World +from assume.common.forecaster import ( + DemandForecaster, + ExchangeForecaster, + PowerplantForecaster, + SteelplantForecaster, + UnitForecaster, +) +from assume.common.market_objects import OnlyHours + + +def source_target(connection: str): + return connection.split("#")[0], connection.split("#")[2] + + +def getType(id: str): + return id.split("_")[0] + + +def load_world_from_gui_json(data: dict, world: World) -> World: + nodes = {i["id"]: i for i in data["nodes"]} + edges = {} + for i in data["edges"]: + source, target = source_target(i["id"]) + i["target"] = target + edges.setdefault(source, {}).setdefault(getType(target), []).append(i) + worldData = nodes["world"]["data"] + start = datetime.fromisoformat(worldData["start"]) + end = datetime.fromisoformat(worldData["end"]) + index = pd.date_range( + start=start, + end=end + datetime.timedelta(hours=24), + freq=worldData["frequency"], + ) + world.setup( + start=start, + end=end, + save_frequency_hours=int(worldData["save_frequency_hours"]), + simulation_id=worldData["simulation_id"], + ) + + add_markets(world, edges, nodes) + add_units(world, edges, nodes, index) + return world + + +def add_markets(world: World, edges: dict, nodes: dict): + # add markets + for market_operator in edges["world"]["marketProvider"]: + target_market_operator = market_operator["target"] + world.add_market_operator(target_market_operator) + for market in edges[target_market_operator]["market"]: + target_market = market["target"] + market_products = [] + for market_product in edges[target_market]["marketProduct"]: + target_market_product = market_product["target"] + productData = nodes[target_market_product]["data"] + print(productData) + market_products.append( + MarketProduct( + duration=relativedelta(minutes=int(productData["duration"])), + count=int(productData["count"]), + first_delivery=relativedelta( + minutes=int(productData["first_delivery"]) + ), + only_hours=_only_hours(productData.get("only_hours", "")), + eligible_lambda_function=_optional_string( + productData.get("eligible_lambda_function") + ), + ) + ) + data = nodes[target_market]["data"] + world.add_market( + market_operator_id=target_market_operator, + market_config=MarketConfig( + market_id=target_market, + market_mechanism=data["market_mechanism"], + opening_hours=rr.rrule( + rr.HOURLY, interval=24, dtstart=world.start, until=world.end + ), + opening_duration=timedelta(minutes=int(data["opening_duration"])), + market_products=market_products, + ), + ) + + +def add_units(world: World, edges: dict, nodes: dict, index): + for unit_operator in edges["world"]["unitOperator"]: + target_unit_operator = unit_operator["target"] + world.add_unit_operator(target_unit_operator) + for unit in edges[target_unit_operator]["unit"]: + target_unit = unit["target"] + bidding_strategies = {} + for connection in edges[target_unit]["market"]: + bidding_strategies[connection["target"]] = connection["data"][ + "strategy" + ] + unitData = nodes[target_unit]["data"] + world.add_unit( + id=target_unit, + unit_operator_id=target_unit_operator, + unit_type=unitData["unitType"], + unit_params={ + "bidding_strategies": bidding_strategies, + "technology": unitData["technology"], + "min_power": int(unitData.get("min_power", 0)), + "max_power": int(unitData.get("max_power", 0)), + "price": float(unitData.get("price", 0)), + "efficiency": float(unitData.get("efficiency", 1.0)), + "ramp_up": int(unitData.get("ramp_up", 0)), + "ramp_down": int(unitData.get("ramp_down", 0)), + "emission_factor": float(unitData.get("emission_factor", 0)), + "min_operating_time": int(unitData.get("min_operating_time", 0)), + "min_downtime": int(unitData.get("min_downtime", 0)), + "max_power_charge": int(unitData.get("max_power_charge", 0)), + "max_power_discharge": int(unitData.get("max_power_discharge", 0)), + "max_soc": int(unitData.get("max_soc", 0)), + "volume_import": int(unitData.get("volume_import", 0)), + "volume_export": int(unitData.get("volume_export", 0)), + }, + forecaster=forecaster_for_type(unitData, index), + ) + return world + + +def forecaster_for_type(data: dict, index: pd.DatetimeIndex) -> UnitForecaster: + default_args = { + "availability": data.get("forecast_availability", 1.0), + "market_prices": data.get("forecast_price", 50.0), + } + fuel_prices = { + "co2": data.get("forecast_co2_price", 10.0), + data.get("fuel_type", "others"): data.get("forecast_fuel_price", 10.0), + } + match data["unitType"]: + case "power_plant": + return PowerplantForecaster(index, fuel_prices=fuel_prices, **default_args) + case "storage": + return UnitForecaster(index, **default_args) + case "demand": + return DemandForecaster( + index, **default_args, demand=data["forecast_demand"] + ) + case "exchange": + return ExchangeForecaster(index, **default_args) + case "steel_plant": + return SteelplantForecaster(index, fuel_prices=fuel_prices, **default_args) + case "building": + return UnitForecaster(index, **default_args) # TODO + case "hydrogen_plant": + return UnitForecaster(index, **default_args) # TODO + case "steam_generation": + return UnitForecaster(index, **default_args) # TODO + raise ValueError(f"Unknown unit type {data['unitType']}") + + +def _only_hours(s: str) -> OnlyHours | None: + if s is None or s == "" or len(s.split(",")) != 2: + return None + return OnlyHours(int(s.split(",")[0]), int(s.split(",")[1])) + + +def _optional_string(s: str) -> str | None: + if s is None or s == "" or s.lower() == "none": + return None + return s diff --git a/examples/notebooks/03_custom_unit_example.ipynb b/examples/notebooks/03_custom_unit_example.ipynb index a848d5b52..adbe61c14 100644 --- a/examples/notebooks/03_custom_unit_example.ipynb +++ b/examples/notebooks/03_custom_unit_example.ipynb @@ -1163,8 +1163,8 @@ "source": [ "# import the main World class and the load_scenario_folder functions from assume\n", "# import the function to load custom units\n", - "from assume import World, load_custom_units\n", - "from assume.scenario.loader_csv import load_scenario_folder\n", + "from assume import World\n", + "from assume.scenario.loader_csv import load_custom_units, load_scenario_folder\n", "\n", "# Set up logging\n", "log = logging.getLogger(__name__)\n",