Skip to content

Commit decffda

Browse files
committed
Merge branch 'main' into feature/sim-bench
Signed-off-by: Bram Stoeller <bram.stoeller@alliander.com>
2 parents 431f1a9 + 63b0b94 commit decffda

File tree

12 files changed

+275
-59
lines changed

12 files changed

+275
-59
lines changed

src/power_grid_model_io/converters/pandapower_converter.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ class PandaPowerConverter(BaseConverter[PandaPowerData]):
2929

3030
__slots__ = ("pp_input_data", "pgm_input_data", "idx", "idx_lookup", "next_idx", "system_frequency")
3131

32-
def __init__(self, system_frequency: float = 50.0):
32+
def __init__(self, system_frequency: float = 50.0, trafo_loading: str = "current"):
3333
"""
3434
Prepare some member variables
3535
3636
Args:
3737
system_frequency: fundamental frequency of the alternating current and voltage in the Network measured in Hz
3838
"""
3939
super().__init__(source=None, destination=None)
40+
self.trafo_loading = trafo_loading
4041
self.system_frequency: float = system_frequency
4142
self.pp_input_data: PandaPowerData = {}
4243
self.pgm_input_data: SingleDataset = {}
@@ -1060,16 +1061,29 @@ def _pp_trafos_output(self):
10601061
# TODO: create unit tests for the function
10611062
assert "res_trafo" not in self.pp_output_data
10621063

1063-
if "transformer" not in self.pgm_output_data or self.pgm_output_data["transformer"].size == 0:
1064+
if ("transformer" not in self.pgm_output_data or self.pgm_output_data["transformer"].size == 0) or (
1065+
"trafo" not in self.pp_input_data or len(self.pp_input_data["trafo"]) == 0
1066+
):
10641067
return
10651068

10661069
pgm_input_transformers = self.pgm_input_data["transformer"]
1067-
1070+
pp_input_transformers = self.pp_input_data["trafo"]
10681071
pgm_output_transformers = self.pgm_output_data["transformer"]
10691072

10701073
from_nodes = self.pgm_nodes_lookup.loc[pgm_input_transformers["from_node"]]
10711074
to_nodes = self.pgm_nodes_lookup.loc[pgm_input_transformers["to_node"]]
10721075

1076+
# Only derating factor used here. Sn is already being multiplied by parallel
1077+
loading_multiplier = pp_input_transformers["df"]
1078+
if self.trafo_loading == "current":
1079+
ui_from = pgm_output_transformers["i_from"] * pgm_input_transformers["u1"]
1080+
ui_to = pgm_output_transformers["i_to"] * pgm_input_transformers["u2"]
1081+
loading = np.maximum(ui_from, ui_to) / pgm_input_transformers["sn"] * loading_multiplier * 1e2
1082+
elif self.trafo_loading == "power":
1083+
loading = pgm_output_transformers["loading"] * loading_multiplier * 1e2
1084+
else:
1085+
raise ValueError(f"Invalid transformer loading type: {str(self.trafo_loading)}")
1086+
10731087
pp_output_trafos = pd.DataFrame(
10741088
columns=[
10751089
"p_hv_mw",
@@ -1100,7 +1114,7 @@ def _pp_trafos_output(self):
11001114
pp_output_trafos["vm_lv_pu"] = to_nodes["u_pu"].values
11011115
pp_output_trafos["va_hv_degree"] = from_nodes["u_degree"].values
11021116
pp_output_trafos["va_lv_degree"] = to_nodes["u_degree"].values
1103-
pp_output_trafos["loading_percent"] = pgm_output_transformers["loading"] * 1e2
1117+
pp_output_trafos["loading_percent"] = loading
11041118

11051119
self.pp_output_data["res_trafo"] = pp_output_trafos
11061120

src/power_grid_model_io/data_stores/csv_dir_store.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
# SPDX-FileCopyrightText: 2022 Contributors to the Power Grid Model project <dynamic.grid.calculation@alliander.com>
1+
# SPDX-FileCopyrightText: 2022 Contributors to the Power Grid Model IO project <dynamic.grid.calculation@alliander.com>
22
#
33
# SPDX-License-Identifier: MPL-2.0
44
"""
55
CSV Directory Store
66
"""
77

88
from pathlib import Path
9-
from typing import Any, Callable, Dict, List
9+
from typing import Any, Dict, List
1010

1111
import pandas as pd
1212

1313
from power_grid_model_io.data_stores.base_data_store import BaseDataStore
14-
from power_grid_model_io.data_types import TabularData
14+
from power_grid_model_io.data_types import LazyDataFrame, TabularData
1515

1616

1717
class CsvDirStore(BaseDataStore[TabularData]):
@@ -35,13 +35,13 @@ def load(self) -> TabularData:
3535
Create a lazy loader for all CSV files in a directory and store them in a TabularData instance.
3636
"""
3737

38-
def lazy_csv_loader(csv_path: Path) -> Callable[[], pd.DataFrame]:
38+
def lazy_csv_loader(csv_path: Path) -> LazyDataFrame:
3939
def csv_loader():
4040
return pd.read_csv(filepath_or_buffer=csv_path, header=self._header_rows, **self._csv_kwargs)
4141

4242
return csv_loader
4343

44-
data: Dict[str, Callable[[], pd.DataFrame]] = {}
44+
data: Dict[str, LazyDataFrame] = {}
4545
for path in self._dir_path.glob("*.csv"):
4646
data[path.stem] = lazy_csv_loader(path)
4747

src/power_grid_model_io/data_stores/excel_file_store.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pandas as pd
1313

1414
from power_grid_model_io.data_stores.base_data_store import BaseDataStore
15-
from power_grid_model_io.data_types import TabularData
15+
from power_grid_model_io.data_types import LazyDataFrame, TabularData
1616

1717

1818
class ExcelFileStore(BaseDataStore[TabularData]):
@@ -24,7 +24,7 @@ class ExcelFileStore(BaseDataStore[TabularData]):
2424
same values) or renamed.
2525
"""
2626

27-
__slots__ = ("_file_paths", "_header_rows")
27+
__slots__ = ("_file_paths", "_excel_files", "_header_rows")
2828

2929
_unnamed_pattern: re.Pattern = re.compile(r"Unnamed: \d+_level_\d+")
3030

@@ -62,19 +62,26 @@ def load(self) -> TabularData:
6262
have no prefix, while the tables of all the extra files will be prefixed with the name of the key word argument
6363
as supplied in the constructor.
6464
"""
65-
data: Dict[str, pd.DataFrame] = {}
66-
for name, path in self._file_paths.items():
67-
with path.open(mode="rb") as file_pointer:
68-
spreadsheet = pd.read_excel(io=file_pointer, sheet_name=None, header=self._header_rows)
69-
for sheet_name, sheet_data in spreadsheet.items():
65+
66+
def lazy_sheet_loader(xls_file: pd.ExcelFile, xls_sheet_name: str):
67+
def sheet_loader():
68+
sheet_data = xls_file.parse(xls_sheet_name, header=self._header_rows)
7069
sheet_data = self._remove_unnamed_column_placeholders(data=sheet_data)
71-
sheet_data = self._handle_duplicate_columns(data=sheet_data, sheet_name=sheet_name)
72-
if name:
70+
sheet_data = self._handle_duplicate_columns(data=sheet_data, sheet_name=xls_sheet_name)
71+
return sheet_data
72+
73+
return sheet_loader
74+
75+
data: Dict[str, LazyDataFrame] = {}
76+
for name, path in self._file_paths.items():
77+
excel_file = pd.ExcelFile(path)
78+
for sheet_name in excel_file.sheet_names:
79+
loader = lazy_sheet_loader(excel_file, sheet_name)
80+
if name != "": # If the Excel file is not the main file, prefix the sheet name with the file name
7381
sheet_name = f"{name}.{sheet_name}"
7482
if sheet_name in data:
7583
raise ValueError(f"Duplicate sheet name '{sheet_name}'")
76-
data[sheet_name] = sheet_data
77-
84+
data[sheet_name] = loader
7885
return TabularData(**data)
7986

8087
def save(self, data: TabularData) -> None:

src/power_grid_model_io/data_types/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"""
77

88
from power_grid_model_io.data_types._data_types import ExtraInfo, ExtraInfoLookup, StructuredData
9-
from power_grid_model_io.data_types.tabular_data import TabularData
9+
from power_grid_model_io.data_types.tabular_data import LazyDataFrame, TabularData

src/power_grid_model_io/data_types/tabular_data.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class TabularData:
2424
which supports unit conversions and value substitutions
2525
"""
2626

27-
def __init__(self, **tables: Union[pd.DataFrame, LazyDataFrame, np.ndarray]):
27+
def __init__(self, **tables: Union[pd.DataFrame, np.ndarray, LazyDataFrame]):
2828
"""
2929
Tabular data can either be a collection of pandas DataFrames and/or numpy structured arrays.
3030
The key word arguments will define the keys of the data.
@@ -41,7 +41,7 @@ def __init__(self, **tables: Union[pd.DataFrame, LazyDataFrame, np.ndarray]):
4141
f"Invalid data type for table '{table_name}'; "
4242
f"expected a pandas DataFrame or NumPy array, got {type(table_data).__name__}."
4343
)
44-
self._data: Dict[str, Union[pd.DataFrame, LazyDataFrame, np.ndarray]] = tables
44+
self._data: Dict[str, Union[pd.DataFrame, np.ndarray, LazyDataFrame]] = tables
4545
self._units: Optional[UnitMapping] = None
4646
self._substitution: Optional[ValueMapping] = None
4747
self._log = structlog.get_logger(type(self).__name__)
@@ -156,6 +156,14 @@ def _apply_unit_conversion(self, table_data: pd.DataFrame, table: str, field: st
156156

157157
return table_data[pd.MultiIndex.from_tuples([(field, si_unit)])[0]]
158158

159+
def __len__(self) -> int:
160+
"""
161+
Return the number of tables (regardless of if they are already loaded or not)
162+
163+
Returns: The number of tables
164+
"""
165+
return len(self._data)
166+
159167
def __contains__(self, table_name: str) -> bool:
160168
"""
161169
Mimic the dictionary 'in' operator
@@ -195,7 +203,10 @@ def items(self) -> Generator[Tuple[str, Union[pd.DataFrame, np.ndarray]], None,
195203
"""
196204
Mimic the dictionary .items() function
197205
198-
Returns: An iterator over the table names and the raw table data
206+
Returns: A generator of the table names and the raw table data
199207
"""
208+
209+
# Note: PyCharm complains about the type, but it is correct, as an ItemsView extends from
210+
# AbstractSet[Tuple[_KT_co, _VT_co]], which actually is compatible with Iterable[_KT_co, _VT_co]
200211
for key in self._data:
201212
yield key, self[key]

tests/unit/converters/test_pandapower_converter_output.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from power_grid_model import initialize_array
1212

1313
from power_grid_model_io.converters.pandapower_converter import PandaPowerConverter
14+
from tests.utils import MockDf
1415

1516

1617
@pytest.fixture
@@ -286,9 +287,11 @@ def test_output_sgen(converter):
286287
converter.pp_output_data.__setitem__.assert_called_once_with("res_sgen", mock_pp_df.return_value)
287288

288289

289-
def test_output_trafos(converter):
290+
def test_output_trafos__current(converter):
290291
# Arrange
291292
mock_pgm_array = MagicMock()
293+
converter.trafo_loading = "current"
294+
converter.pp_input_data["trafo"] = MockDf(2)
292295
converter.pgm_input_data["transformer"] = mock_pgm_array
293296
converter.pgm_output_data["transformer"] = mock_pgm_array
294297
converter.pgm_nodes_lookup = pd.DataFrame(
@@ -314,9 +317,8 @@ def test_output_trafos(converter):
314317
mock_pgm_array.__getitem__.assert_any_call("q_to")
315318
mock_pgm_array.__getitem__.assert_any_call("i_from")
316319
mock_pgm_array.__getitem__.assert_any_call("i_to")
317-
# mock_pgm_array.__getitem__.assert_any_call("u_pu")
318-
# mock_pgm_array.__getitem__.assert_any_call("u_degree")
319-
mock_pgm_array.__getitem__.assert_any_call("loading")
320+
# current loading retrieval
321+
mock_pgm_array.__getitem__.assert_any_call("sn")
320322

321323
# assignment
322324
mock_pp_df.return_value.__setitem__.assert_any_call("p_hv_mw", ANY)
@@ -337,6 +339,42 @@ def test_output_trafos(converter):
337339
converter.pp_output_data.__setitem__.assert_called_once_with("res_trafo", mock_pp_df.return_value)
338340

339341

342+
def test_output_trafos__power(converter):
343+
# Arrange
344+
mock_pgm_array = MagicMock()
345+
converter.trafo_loading = "power"
346+
converter.pp_input_data["trafo"] = MockDf(2)
347+
converter.pgm_input_data["transformer"] = mock_pgm_array
348+
converter.pgm_output_data["transformer"] = mock_pgm_array
349+
converter.pgm_nodes_lookup = pd.DataFrame(
350+
{"u_pu": mock_pgm_array, "u_degree": mock_pgm_array}, index=mock_pgm_array
351+
)
352+
353+
with patch("power_grid_model_io.converters.pandapower_converter.pd.DataFrame") as mock_pp_df:
354+
# Act
355+
converter._pp_trafos_output()
356+
357+
mock_pgm_array.__getitem__.assert_any_call("loading")
358+
mock_pp_df.return_value.__setitem__.assert_any_call("loading_percent", ANY)
359+
# result
360+
converter.pp_output_data.__setitem__.assert_called_once_with("res_trafo", mock_pp_df.return_value)
361+
362+
363+
def test_output_trafos__invalid_trafo_loading(converter):
364+
# Arrange
365+
mock_pgm_array = MagicMock()
366+
converter.trafo_loading = "abcd"
367+
converter.pp_input_data["trafo"] = MockDf(2)
368+
converter.pgm_input_data["transformer"] = mock_pgm_array
369+
converter.pgm_output_data["transformer"] = mock_pgm_array
370+
converter.pgm_nodes_lookup = pd.DataFrame(
371+
{"u_pu": mock_pgm_array, "u_degree": mock_pgm_array}, index=mock_pgm_array
372+
)
373+
374+
with pytest.raises(ValueError, match="Invalid transformer loading type: abcd"):
375+
converter._pp_trafos_output()
376+
377+
340378
def test_output_trafo3w(converter):
341379
# Arrange
342380
mock_pgm_array = MagicMock()

tests/unit/converters/test_tabular_converter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ def test_serialize_data(converter: TabularConverter, pgm_node_empty: SingleDatas
266266
pgm_node_empty["node"]["id"] = [1, 2]
267267
pgm_node_empty["node"]["u_rated"] = [3.0, 4.0]
268268
tabular_data = converter._serialize_data(data=pgm_node_empty, extra_info=None)
269-
assert len(tabular_data._data) == 1
270-
assert (tabular_data.get_column("node", "id") == np.array([1, 2])).all()
271-
assert (tabular_data.get_column("node", "u_rated") == np.array([3.0, 4.0])).all()
269+
assert len(tabular_data) == 1
270+
assert (tabular_data["node"]["id"] == np.array([1, 2])).all()
271+
assert (tabular_data["node"]["u_rated"] == np.array([3.0, 4.0])).all()
272272

273273

274274
def test_parse_col_def(converter: TabularConverter, tabular_data_no_units_no_substitutions: TabularData):
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# SPDX-FileCopyrightText: 2022 Contributors to the Power Grid Model project <dynamic.grid.calculation@alliander.com>
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
import tempfile
6+
from pathlib import Path
7+
from unittest.mock import MagicMock, patch
8+
9+
import pandas as pd
10+
import pytest
11+
12+
from power_grid_model_io.data_stores.csv_dir_store import CsvDirStore
13+
from power_grid_model_io.data_types import TabularData
14+
15+
16+
@pytest.fixture()
17+
def temp_dir():
18+
with tempfile.TemporaryDirectory() as tmp:
19+
yield Path(tmp).resolve()
20+
21+
22+
def touch(file_path: Path):
23+
open(file_path, "wb").close()
24+
25+
26+
@patch("power_grid_model_io.data_stores.csv_dir_store.pd.read_csv")
27+
def test_load(mock_read_csv: MagicMock, temp_dir: Path):
28+
# Arrange
29+
foo_data = MagicMock()
30+
bar_data = MagicMock()
31+
touch(temp_dir / "foo.csv")
32+
touch(temp_dir / "bar.csv")
33+
mock_read_csv.side_effect = (foo_data, bar_data)
34+
csv_dir = CsvDirStore(temp_dir, bla=True)
35+
36+
# Act
37+
csv_data = csv_dir.load()
38+
39+
# Assert
40+
mock_read_csv.assert_not_called() # The csv data is not yet loaded
41+
assert csv_data["foo"] == foo_data
42+
assert csv_data["bar"] == bar_data
43+
mock_read_csv.assert_any_call(filepath_or_buffer=temp_dir / "foo.csv", header=[0], bla=True)
44+
mock_read_csv.assert_any_call(filepath_or_buffer=temp_dir / "bar.csv", header=[0], bla=True)
45+
46+
47+
@patch("power_grid_model_io.data_stores.csv_dir_store.pd.DataFrame.to_csv")
48+
def test_save(mock_to_csv: MagicMock, temp_dir):
49+
# Arrange
50+
foo_data = pd.DataFrame()
51+
bar_data = pd.DataFrame()
52+
data = TabularData(foo=foo_data, bar=bar_data)
53+
csv_dir = CsvDirStore(temp_dir, bla=True)
54+
55+
# Act
56+
csv_dir.save(data)
57+
58+
# Assert
59+
mock_to_csv.assert_any_call(path_or_buf=temp_dir / "foo.csv", bla=True)
60+
mock_to_csv.assert_any_call(path_or_buf=temp_dir / "bar.csv", bla=True)

0 commit comments

Comments
 (0)