Skip to content
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

Adding CongestionResult. #200

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
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
Next Next commit
Adding CongestionResult.
staudtMarius committed Jun 19, 2024
commit 333c23f38bbabc2aad1f989c34b70cfac39274b5
5 changes: 3 additions & 2 deletions pypsdm/io/utils.py
Original file line number Diff line number Diff line change
@@ -89,14 +89,15 @@ 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.
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]):
16 changes: 13 additions & 3 deletions pypsdm/models/enums.py
Original file line number Diff line number Diff line change
@@ -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,18 +67,21 @@ 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!"
)
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):
4 changes: 4 additions & 0 deletions pypsdm/models/gwr.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions pypsdm/models/input/container/mixins.py
Original file line number Diff line number Diff line change
@@ -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,
2 changes: 2 additions & 0 deletions pypsdm/models/input/container/raw_grid.py
Original file line number Diff line number Diff line change
@@ -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}")

4 changes: 4 additions & 0 deletions pypsdm/models/result/container/grid.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions pypsdm/models/result/container/raw_grid.py
Original file line number Diff line number Diff line change
@@ -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}
2 changes: 2 additions & 0 deletions pypsdm/models/result/grid/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
]
90 changes: 90 additions & 0 deletions pypsdm/models/result/grid/congestions.py
Original file line number Diff line number Diff line change
@@ -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
180 changes: 144 additions & 36 deletions pypsdm/models/result/participant/dict.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

import os
import uuid
from abc import abstractmethod
from abc import abstractmethod, ABC
from datetime import datetime
from typing import TYPE_CHECKING, Self, Tuple, Type

@@ -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
@@ -47,13 +45,148 @@ def from_csv(
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,
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, 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")
@@ -87,11 +220,11 @@ def from_csv(
)

def to_csv(
self,
path: str,
delimiter=",",
mkdirs=False,
resample_rate: str | None = None,
self,
path: str,
delimiter=",",
mkdirs=False,
resample_rate: str | None = None,
):
if mkdirs:
os.makedirs(path, exist_ok=True)
@@ -117,31 +250,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]):
31 changes: 29 additions & 2 deletions pypsdm/models/ts/base.py
Original file line number Diff line number Diff line change
@@ -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():
36 changes: 31 additions & 5 deletions tests/models/result/container/test_raw_grid.py
Original file line number Diff line number Diff line change
@@ -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

58 changes: 58 additions & 0 deletions tests/models/result/grid/test_congestion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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