diff --git a/pypsdm/io/utils.py b/pypsdm/io/utils.py index 172f123f..453e7697 100644 --- a/pypsdm/io/utils.py +++ b/pypsdm/io/utils.py @@ -89,7 +89,7 @@ def to_date_time(zoned_date_time: str) -> datetime: def csv_to_grpd_df( - file_name: str, simulation_data_path: str, delimiter: str | None = None + file_name: str, simulation_data_path: str, by: str, delimiter: str | None = None ) -> DataFrameGroupBy: """ Reads in a PSDM csv results file cleans it up and groups it by input_archive model. @@ -97,6 +97,7 @@ def csv_to_grpd_df( Args: file_name: name of the file to read simulation_data_path: base directory of the result data + by: the column to group by delimiter: the csv delimiter Returns: @@ -106,7 +107,7 @@ def csv_to_grpd_df( if "uuid" in data.columns: data = data.drop(columns=["uuid"]) - return data.groupby(by="input_model") + return data.groupby(by=by) def check_filter(filter_start: Optional[datetime], filter_end: Optional[datetime]): diff --git a/pypsdm/models/enums.py b/pypsdm/models/enums.py index 0067d16e..9b1c71a6 100644 --- a/pypsdm/models/enums.py +++ b/pypsdm/models/enums.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Type, TypeVar if TYPE_CHECKING: - from pypsdm.models.result.participant.dict import EntitiesResultDictMixin + from pypsdm.models.result.participant.dict import ResultDictMixin from pypsdm.models.ts.base import TimeSeries @@ -28,7 +28,10 @@ def get_csv_input_file_name(self): return self.value + "_input.csv" def get_csv_result_file_name(self): - return self.value + "_res.csv" + if self.value == "subgrid": + return "congestion_res.csv" + else: + return self.value + "_res.csv" def get_type_file_name(self): assert self.has_type() is True @@ -42,6 +45,7 @@ def get_result_type(self) -> type[TimeSeries]: from pypsdm.models.result.grid.connector import ConnectorCurrent from pypsdm.models.result.grid.switch import SwitchResult from pypsdm.models.result.grid.transformer import Transformer2WResult + from pypsdm.models.result.grid.congestions import CongestionResult from pypsdm.models.ts.types import ( ComplexPower, ComplexPowerWithSoc, @@ -63,6 +67,8 @@ def get_result_type(self) -> type[TimeSeries]: return ConnectorCurrent case RawGridElementsEnum.SWITCH: return SwitchResult + case RawGridElementsEnum.SUBGRID: + return CongestionResult case _: raise NotImplementedError( f"Result type {self} not implemented yet!" @@ -70,11 +76,12 @@ def get_result_type(self) -> type[TimeSeries]: else: raise ValueError(f"Entity type {self} not supported!") - def get_result_dict_type(self) -> Type["EntitiesResultDictMixin"]: + def get_result_dict_type(self) -> Type["ResultDictMixin"]: from pypsdm.models.result.grid.line import LinesResult from pypsdm.models.result.grid.node import NodesResult from pypsdm.models.result.grid.switch import SwitchesResult from pypsdm.models.result.grid.transformer import Transformers2WResult + from pypsdm.models.result.grid.congestions import CongestionsResult from pypsdm.models.result.participant.dict import ( EmsResult, EvcsResult, @@ -97,6 +104,8 @@ def get_result_dict_type(self) -> Type["EntitiesResultDictMixin"]: return Transformers2WResult case RawGridElementsEnum.SWITCH: return SwitchesResult + case RawGridElementsEnum.SUBGRID: + return CongestionsResult case SystemParticipantsEnum.ELECTRIC_VEHICLE: return EvsResult case SystemParticipantsEnum.EV_CHARGING_STATION: @@ -158,6 +167,7 @@ class RawGridElementsEnum(EntitiesEnum): TRANSFROMER_3_W = "transformer_3_w" SWITCH = "switch" MEASUREMENT_UNIT = "measurement_unit" + SUBGRID = "subgrid" class ThermalGridElementsEnum(EntitiesEnum): diff --git a/pypsdm/models/gwr.py b/pypsdm/models/gwr.py index 6c2c5cae..36857203 100644 --- a/pypsdm/models/gwr.py +++ b/pypsdm/models/gwr.py @@ -106,6 +106,10 @@ def transformers_2_w_res(self): def switches_res(self): return self.raw_grid_res.switches + @property + def congestions_res(self): + return self.results.raw_grid.congestions + @property def participants_res(self): return self.results.participants diff --git a/pypsdm/models/input/container/mixins.py b/pypsdm/models/input/container/mixins.py index c5673417..70dcb880 100644 --- a/pypsdm/models/input/container/mixins.py +++ b/pypsdm/models/input/container/mixins.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from pypsdm.models.input.container.grid import GridContainer - from pypsdm.models.result.participant.dict import EntitiesResultDictMixin + from pypsdm.models.result.participant.dict import ResultDictMixin class ContainerMixin(ABC): @@ -118,8 +118,8 @@ def entities_from_csv( delimiter: str | None = None, filter_start: datetime | None = None, filter_end: datetime | None = None, - ) -> dict[EntitiesEnum, EntitiesResultDictMixin]: - from pypsdm.models.result.participant.dict import EntitiesResultDictMixin + ) -> dict[EntitiesEnum, ResultDictMixin]: + from pypsdm.models.result.participant.dict import ResultDictMixin res_files = [ f for f in os.listdir(simulation_data_path) if f.endswith("_res.csv") @@ -135,7 +135,7 @@ def entities_from_csv( with concurrent.futures.ProcessPoolExecutor() as executor: # warning: Breakpoints in the underlying method might not work when started from ipynb pa_from_csv_for_participant = partial( - EntitiesResultDictMixin.from_csv_for_entity, + ResultDictMixin.from_csv_for_entity, simulation_data_path, simulation_end, grid_container, diff --git a/pypsdm/models/input/container/raw_grid.py b/pypsdm/models/input/container/raw_grid.py index 09768cd9..fa928c67 100644 --- a/pypsdm/models/input/container/raw_grid.py +++ b/pypsdm/models/input/container/raw_grid.py @@ -77,6 +77,8 @@ def get_with_enum(self, enum: RawGridElementsEnum): return self.transformers_2_w case RawGridElementsEnum.SWITCH: return self.switches + case RawGridElementsEnum.SUBGRID: + return None case _: raise ValueError(f"Unknown enum {enum}") diff --git a/pypsdm/models/result/container/grid.py b/pypsdm/models/result/container/grid.py index 150cfd80..057ea8da 100644 --- a/pypsdm/models/result/container/grid.py +++ b/pypsdm/models/result/container/grid.py @@ -40,6 +40,10 @@ def transformers_2w(self): def switches(self): return self.raw_grid.switches + @property + def congestions(self): + return self.raw_grid.congestions + @property def ems(self): return self.participants.ems diff --git a/pypsdm/models/result/container/raw_grid.py b/pypsdm/models/result/container/raw_grid.py index 57ce3f15..4ddc361f 100644 --- a/pypsdm/models/result/container/raw_grid.py +++ b/pypsdm/models/result/container/raw_grid.py @@ -9,6 +9,7 @@ from pypsdm.models.result.grid.node import NodesResult from pypsdm.models.result.grid.switch import SwitchesResult from pypsdm.models.result.grid.transformer import Transformers2WResult +from pypsdm.models.result.grid.congestions import CongestionsResult from pypsdm.models.ts.base import EntityKey @@ -18,6 +19,7 @@ class RawGridResultContainer(ResultContainerMixin): lines: LinesResult transformers_2w: Transformers2WResult switches: SwitchesResult + congestions: CongestionsResult def __init__(self, dct): def get_or_empty(key: RawGridElementsEnum, dict_type): @@ -36,6 +38,7 @@ def get_or_empty(key: RawGridElementsEnum, dict_type): RawGridElementsEnum.TRANSFORMER_2_W, Transformers2WResult ) self.switches = get_or_empty(RawGridElementsEnum.SWITCH, SwitchesResult) + self.congestions = get_or_empty(RawGridElementsEnum.SUBGRID, CongestionsResult) def __len__(self): return sum(len(v) for v in self.to_dict().values()) @@ -54,6 +57,7 @@ def to_dict(self, include_empty: bool = False) -> dict: RawGridElementsEnum.LINE: self.lines, RawGridElementsEnum.TRANSFORMER_2_W: self.transformers_2w, RawGridElementsEnum.SWITCH: self.switches, + RawGridElementsEnum.SUBGRID: self.congestions, } if not include_empty: res = {k: v for k, v in res.items() if v} diff --git a/pypsdm/models/result/grid/__init__.py b/pypsdm/models/result/grid/__init__.py index 6d974433..35526dbf 100644 --- a/pypsdm/models/result/grid/__init__.py +++ b/pypsdm/models/result/grid/__init__.py @@ -3,6 +3,7 @@ from .node import NodesResult from .switch import SwitchesResult, SwitchResult from .transformer import Transformer2WResult, Transformers2WResult +from .congestions import CongestionResult, CongestionsResult __all__ = [ "ConnectorCurrent", @@ -13,4 +14,5 @@ "LinesResult", "SwitchResult", "SwitchesResult", + "CongestionsResult", ] diff --git a/pypsdm/models/result/grid/congestions.py b/pypsdm/models/result/grid/congestions.py new file mode 100644 index 00000000..8114f908 --- /dev/null +++ b/pypsdm/models/result/grid/congestions.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass +from datetime import datetime + +from pandas import DataFrame + +from pypsdm.models.enums import RawGridElementsEnum +from pypsdm.models.result.participant.dict import SubgridResultDictMixin +from pypsdm.models.ts.base import ( + SubGridKey, + TimeSeries, + TimeSeriesDict, + TimeSeriesDictMixin, +) + + +@dataclass +class CongestionResult(TimeSeries): + def __init__(self, data: DataFrame, end: datetime | None = None): + super().__init__(data, end) + + def __eq__(self, other: object) -> bool: + return super().__eq__(other) + + def __add__(self, _): + return NotImplemented + + @property + def vMin(self) -> float: + return self.data["vMin"].drop_duplicates()[0] + + @property + def vMax(self) -> float: + return self.data["vMax"].drop_duplicates()[0] + + @property + def subnet(self) -> int: + return self.data["subgrid"].drop_duplicates()[0] + + @property + def voltage(self): + return self.data["voltage"] + + @property + def line(self): + return self.data["line"] + + @property + def transformer(self): + return self.data["transformer"] + + @staticmethod + def attributes() -> list[str]: + return ["vMin", "vMax", "subgrid", "voltage", "line", "transformer"] + + +class CongestionsResult( + TimeSeriesDict[SubGridKey, CongestionResult], + TimeSeriesDictMixin, + SubgridResultDictMixin, +): + def __eq__(self, other: object) -> bool: + return super().__eq__(other) + + @property + def vMin(self) -> DataFrame: + return self.attr_df("vMin") + + @property + def vMax(self) -> DataFrame: + return self.attr_df("vMax") + + @property + def subnet(self) -> DataFrame: + return self.attr_df("subgrid") + + @property + def voltage(self) -> DataFrame: + return self.attr_df("voltage") + + @property + def line(self) -> DataFrame: + return self.attr_df("line") + + @property + def transformer(self) -> DataFrame: + return self.attr_df("transformer") + + @classmethod + def entity_type(cls) -> RawGridElementsEnum: + return RawGridElementsEnum.SUBGRID diff --git a/pypsdm/models/result/participant/dict.py b/pypsdm/models/result/participant/dict.py index dc6bd9d4..70397738 100644 --- a/pypsdm/models/result/participant/dict.py +++ b/pypsdm/models/result/participant/dict.py @@ -12,7 +12,7 @@ from pypsdm.io.utils import check_filter, csv_to_grpd_df, get_file_path, to_date_time from pypsdm.models.enums import EntitiesEnum, SystemParticipantsEnum from pypsdm.models.input.entity import Entities -from pypsdm.models.ts.base import EntityKey, TimeSeries +from pypsdm.models.ts.base import EntityKey, SubGridKey, TimeSeries from pypsdm.models.ts.types import ( ComplexPower, ComplexPowerDict, @@ -24,9 +24,7 @@ from pypsdm.models.input.container.grid import GridContainer -class EntitiesResultDictMixin: - def uuids(self) -> set[str]: - return {key.uuid for key in self.keys()} # type: ignore +class ResultDictMixin: @classmethod @abstractmethod @@ -37,6 +35,59 @@ def entity_type(cls) -> EntitiesEnum: def result_type(cls) -> Type[TimeSeries]: return cls.entity_type().get_result_type() + @classmethod + def from_csv( + cls, + simulation_data_path: str, + delimiter: str | None = None, + simulation_end: datetime | None = None, + input_entities: Entities | None = None, + filter_start: datetime | None = None, + filter_end: datetime | None = None, + must_exist: bool = True, + ) -> Self: + raise NotImplementedError + + @abstractmethod + def to_csv( + self, + path: str, + delimiter=",", + mkdirs=False, + resample_rate: str | None = None, + ): + return NotImplemented + + @staticmethod + def from_csv_for_entity( + simulation_data_path: str, + simulation_end: datetime | None, + grid_container: GridContainer | None, + entity: EntitiesEnum, + delimiter: str | None = None, + ) -> "ResultDictMixin" | Tuple[Exception, EntitiesEnum]: + try: + if grid_container: + input_entities = grid_container.get_with_enum(entity) + else: + input_entities = None + dict_type = entity.get_result_dict_type() + return dict_type.from_csv( + simulation_data_path, + delimiter=delimiter, + simulation_end=simulation_end, + input_entities=input_entities, + must_exist=False, + ) + + except Exception as e: + return e, entity + + +class SubgridResultDictMixin(ResultDictMixin): + def subgrids(self) -> set[int]: + return {key.subgrid for key in self.keys()} # type: ignore + @classmethod def from_csv( cls, @@ -53,7 +104,93 @@ def from_csv( file_name = cls.entity_type().get_csv_result_file_name() path = get_file_path(simulation_data_path, file_name) if path.exists(): - grpd_df = csv_to_grpd_df(file_name, simulation_data_path, delimiter) + grpd_df = csv_to_grpd_df( + file_name, simulation_data_path, "subgrid", delimiter + ) + else: + if must_exist: + raise FileNotFoundError(f"File {path} does not exist") + else: + return cls.empty() # type: ignore + + if len(grpd_df) == 0: + return cls.empty() # type: ignore + + if simulation_end is None: + simulation_end = to_date_time(grpd_df["time"].max().max()) # type: ignore + + ts_dict = {} + for key, grp in grpd_df: + result_key = None + + if isinstance(key, int): + result_key = SubGridKey(key, None) + else: + logger.warning("Entity {} is not a subgrid result.".format(key)) + + ts = cls.result_type()(grp, simulation_end) + ts_dict[result_key] = ts + + res = cls(ts_dict) + return ( + res + if not filter_start + else res.filter_for_time_interval(filter_start, filter_end) # type: ignore + ) + + def to_csv( + self, + path: str, + delimiter=",", + mkdirs=False, + resample_rate: str | None = None, + ): + if mkdirs: + os.makedirs(path, exist_ok=True) + + file_name = self.entity_type().get_csv_result_file_name() + + def prepare_data(data: pd.DataFrame, subgrid: int): + data = data.copy() + data = ( + data.resample("60s").ffill().resample(resample_rate).mean() + if resample_rate + else data + ) + data.index.name = "time" + return data + + dfs = [ + prepare_data(participant.data, entity_key.subgrid) + for entity_key, participant in self.items() # type: ignore + ] + df = pd.concat(dfs) + df.to_csv(os.path.join(path, file_name), sep=delimiter, index=True) + + +class EntitiesResultDictMixin(ResultDictMixin): + def uuids(self) -> set[str]: + return {key.uuid for key in self.keys()} # type: ignore + + @classmethod + def from_csv( + cls, + simulation_data_path: str, + delimiter: str | None = None, + simulation_end: datetime | None = None, + input_entities: Entities | None = None, + filter_start: datetime | None = None, + filter_end: datetime | None = None, + must_exist: bool = True, + ) -> Self: + check_filter(filter_start, filter_end) + + file_name = cls.entity_type().get_csv_result_file_name() + path = get_file_path(simulation_data_path, file_name) + if path.exists(): + grpd_df = csv_to_grpd_df( + file_name, simulation_data_path, "input_model", delimiter + ) else: if must_exist: raise FileNotFoundError(f"File {path} does not exist") @@ -117,31 +254,6 @@ def prepare_data(data: pd.DataFrame, input_model: str): df = pd.concat(dfs) df.to_csv(os.path.join(path, file_name), sep=delimiter, index=True) - @staticmethod - def from_csv_for_entity( - simulation_data_path: str, - simulation_end: datetime | None, - grid_container: GridContainer | None, - entity: EntitiesEnum, - delimiter: str | None = None, - ) -> "EntitiesResultDictMixin" | Tuple[Exception, EntitiesEnum]: - try: - if grid_container: - input_entities = grid_container.get_with_enum(entity) - else: - input_entities = None - dict_type = entity.get_result_dict_type() - return dict_type.from_csv( - simulation_data_path, - delimiter=delimiter, - simulation_end=simulation_end, - input_entities=input_entities, - must_exist=False, - ) - - except Exception as e: - return (e, entity) - class EmsResult(ComplexPowerDict[EntityKey], EntitiesResultDictMixin): def __init__(self, data: dict[EntityKey, ComplexPower]): diff --git a/pypsdm/models/ts/base.py b/pypsdm/models/ts/base.py index 55b47dad..bac1ccc3 100644 --- a/pypsdm/models/ts/base.py +++ b/pypsdm/models/ts/base.py @@ -1,5 +1,5 @@ import copy -from abc import ABC +from abc import ABC, abstractmethod from collections import UserDict from dataclasses import dataclass from datetime import datetime @@ -200,7 +200,14 @@ def interval(self, start: datetime, end: datetime) -> Self: @dataclass(frozen=True) -class EntityKey: +class ResultKey: + @abstractmethod + def id(self): + return NotImplemented + + +@dataclass(frozen=True) +class EntityKey(ResultKey): uuid: str name: str | None = None @@ -219,6 +226,26 @@ def id(self) -> str: return self.name if self.name else self.uuid +@dataclass(frozen=True) +class SubGridKey(ResultKey): + subgrid: int + name: str | None = None + + def __eq__(self, other: object) -> bool: + if isinstance(other, SubGridKey): + return self.subgrid == other.subgrid + if isinstance(other, int): + return self.subgrid == other + return False + + def __hash__(self) -> int: + return hash(self.subgrid) + + @property + def id(self) -> str: + return self.name if self.name else self.subgrid + + class TimeSeriesDict(UserDict[K, V]): def __init__(self, data: dict[K, V]): for ts in data.values(): diff --git a/tests/models/result/container/test_grid.py b/tests/models/result/container/test_grid.py index db02023f..7a4efd7d 100644 --- a/tests/models/result/container/test_grid.py +++ b/tests/models/result/container/test_grid.py @@ -2,13 +2,14 @@ import pandas as pd -from pypsdm.models.enums import EntitiesEnum +from pypsdm.models.enums import EntitiesEnum, RawGridElementsEnum from pypsdm.models.result.container.grid import GridResultContainer from pypsdm.models.result.container.participants import ( SystemParticipantsResultContainer, ) from pypsdm.models.result.container.raw_grid import RawGridResultContainer -from pypsdm.models.ts.base import TIME_COLUMN_NAME, EntityKey +from pypsdm.models.result.grid import CongestionResult +from pypsdm.models.ts.base import TIME_COLUMN_NAME, EntityKey, SubGridKey def get_entities_test_data(entity: EntitiesEnum): @@ -27,10 +28,35 @@ def get_data(attributes): data[attr] = [0.0, 1.0, 2.0, 3.0] return data + def get_congestion_data(subgrid: int): + data = pd.DataFrame( + { + TIME_COLUMN_NAME: [ + "2021-01-01", + "2021-01-02", + "2021-01-03", + "2021-01-04", + ], + } + ) + data["vMin"] = [0.9, 0.9, 0.9, 0.9] + data["vMax"] = [1.1, 1.1, 1.1, 1.1] + data["subgrid"] = [subgrid, subgrid, subgrid, subgrid] + + for attr in ["voltage", "line", "transformer"]: + data[attr] = [False, False, False, False] + return data + entities = {} - for e in ["a", "b", "c"]: - res_type = entity.get_result_type() - entities[EntityKey(e)] = res_type(get_data(res_type.attributes())) + + if entity is RawGridElementsEnum.SUBGRID: + for s in [1, 2, 3]: + entities[SubGridKey(s)] = CongestionResult(get_congestion_data(s)) + + else: + for e in ["a", "b", "c"]: + res_type = entity.get_result_type() + entities[EntityKey(e)] = res_type(get_data(res_type.attributes())) return entity.get_result_dict_type()(entities) # type: ignore diff --git a/tests/models/result/container/test_raw_grid.py b/tests/models/result/container/test_raw_grid.py index f1fbbc51..ddc15630 100644 --- a/tests/models/result/container/test_raw_grid.py +++ b/tests/models/result/container/test_raw_grid.py @@ -1,8 +1,9 @@ import pandas as pd -from pypsdm.models.enums import EntitiesEnum +from pypsdm.models.enums import EntitiesEnum, RawGridElementsEnum from pypsdm.models.result.container.raw_grid import RawGridResultContainer -from pypsdm.models.ts.base import TIME_COLUMN_NAME, EntityKey +from pypsdm.models.result.grid import CongestionResult +from pypsdm.models.ts.base import TIME_COLUMN_NAME, EntityKey, SubGridKey def get_entities_test_data(entity: EntitiesEnum): @@ -21,10 +22,35 @@ def get_data(attributes): data[attr] = [0.0, 1.0, 2.0, 3.0] return data + def get_congestion_data(subgrid: int): + data = pd.DataFrame( + { + TIME_COLUMN_NAME: [ + "2021-01-01", + "2021-01-02", + "2021-01-03", + "2021-01-04", + ], + } + ) + data["vMin"] = [0.9, 0.9, 0.9, 0.9] + data["vMax"] = [1.1, 1.1, 1.1, 1.1] + data["subgrid"] = [subgrid, subgrid, subgrid, subgrid] + + for attr in ["voltage", "line", "transformer"]: + data[attr] = [False, False, False, False] + return data + entities = {} - for e in ["a", "b", "c"]: - res_type = entity.get_result_type() - entities[EntityKey(e)] = res_type(get_data(res_type.attributes())) + + if entity is RawGridElementsEnum.SUBGRID: + for s in [1, 2, 3]: + entities[SubGridKey(s)] = CongestionResult(get_congestion_data(s)) + + else: + for e in ["a", "b", "c"]: + res_type = entity.get_result_type() + entities[EntityKey(e)] = res_type(get_data(res_type.attributes())) return entity.get_result_dict_type()(entities) # type: ignore diff --git a/tests/models/result/grid/test_congestion.py b/tests/models/result/grid/test_congestion.py new file mode 100644 index 00000000..dfa104d9 --- /dev/null +++ b/tests/models/result/grid/test_congestion.py @@ -0,0 +1,72 @@ +import pandas as pd + +from pypsdm.models.result.grid.congestions import CongestionsResult, CongestionResult +from pypsdm.models.ts.base import SubGridKey, TIME_COLUMN_NAME + + +def test_from_csv(tmp_path): + data_str = """ +v_max,v_min,line,subgrid,time,transformer,voltage +1.05,0.95,false,1,2016-07-24T01:00:00Z,false,false +1.05,0.95,false,4,2016-07-24T01:00:00Z,false,false +1.05,0.95,false,2,2016-07-24T01:00:00Z,false,false +1.05,0.95,false,3,2016-07-24T01:00:00Z,false,true +1.05,0.95,false,5,2016-07-24T01:00:00Z,false,false +1.05,0.95,false,1,2016-07-24T02:00:00Z,false,false +1.05,0.95,false,2,2016-07-24T02:00:00Z,false,false +1.05,0.95,false,3,2016-07-24T02:00:00Z,false,true +1.05,0.95,false,4,2016-07-24T02:00:00Z,false,false +1.05,0.95,false,5,2016-07-24T02:00:00Z,false,false +""" + file_path = tmp_path / "congestion_res.csv" + with open(file_path, "w") as f: + f.write(data_str) + + congestions = CongestionsResult.from_csv(tmp_path) + assert set(congestions.keys()) == { + SubGridKey(1), + SubGridKey(2), + SubGridKey(3), + SubGridKey(4), + SubGridKey(5), + } + print(set(congestions[1].data.columns)) + assert set(congestions[1].data.columns) == { + "v_max", + "transformer", + "voltage", + "subgrid", + "v_min", + "line", + } + + +def test_to_csv(tmp_path): + def get_congestion_data(subgrid: int): + data = pd.DataFrame( + { + TIME_COLUMN_NAME: [ + "2021-01-01", + "2021-01-02", + "2021-01-03", + "2021-01-04", + ], + } + ) + data["vMin"] = [0.9, 0.9, 0.9, 0.9] + data["vMax"] = [1.1, 1.1, 1.1, 1.1] + data["subgrid"] = [subgrid, subgrid, subgrid, subgrid] + + for attr in ["voltage", "line", "transformer"]: + data[attr] = [False, False, False, False] + return data + + res_dict = { + SubGridKey(s): CongestionResult(get_congestion_data(s)) for s in [1, 2, 3] + } + + congestions = CongestionsResult(res_dict) + + congestions.to_csv(tmp_path) + congestions2 = CongestionsResult.from_csv(tmp_path) + assert congestions == congestions2