diff --git a/src/power_grid_model_io/converters/pandapower_converter.py b/src/power_grid_model_io/converters/pandapower_converter.py index f5873c3a..1c22dac4 100644 --- a/src/power_grid_model_io/converters/pandapower_converter.py +++ b/src/power_grid_model_io/converters/pandapower_converter.py @@ -342,13 +342,13 @@ def _create_output_data(self): self._pp_buses_output() self._pp_lines_output() self._pp_ext_grids_output() - self._pp_loads_output() + self._pp_load_elements_output(element="load", symmetric=True) + self._pp_load_elements_output(element="ward", symmetric=True) + self._pp_load_elements_output(element="motor", symmetric=True) self._pp_shunts_output() self._pp_trafos_output() self._pp_sgens_output() self._pp_trafos3w_output() - self._pp_ward_output() - self._pp_motor_output() self._pp_asym_gens_output() self._pp_asym_loads_output() # Switches derive results from branches pp_output_data and pgm_output_data of links. Hence, placed in the end. @@ -360,13 +360,14 @@ def _create_output_data_3ph(self): Furthermore, creates a global node lookup table, which stores nodes' voltage magnitude per unit and the voltage angle in degrees """ - # TODO create output_data_3ph for remaining components - # Although Pandapower itself did not implmenet res_shunt_3ph - # Since results are avaiable in PGM output, these should be converted. + # TODO create output_data_3ph for trafos3w, switches self._pp_buses_output_3ph() self._pp_lines_output_3ph() self._pp_ext_grids_output_3ph() - self._pp_loads_output_3ph() + self._pp_load_elements_output(element="load", symmetric=False) + self._pp_load_elements_output(element="ward", symmetric=False) + self._pp_load_elements_output(element="motor", symmetric=False) + self._pp_shunts_output_3ph() self._pp_trafos_output_3ph() self._pp_sgens_output_3ph() self._pp_asym_gens_output_3ph() @@ -1560,64 +1561,35 @@ def _pp_asym_gens_output(self): self.pp_output_data["res_asymmetric_sgen"] = pp_output_asym_gens - def _pp_loads_output(self): + def _pp_load_elements_output(self, element, symmetric): """ - This function converts a power-grid-model Symmetrical Load output array to a Load Dataframe of PandaPower. - - Returns: - a PandaPower Dataframe for the Load component + Utility function to convert output of elements represented as load + in power grid model. + element: "load", "motor" or "ward" + symmetric: True or False """ - load_id_names = ["const_power", "const_impedance", "const_current"] - assert "res_load" not in self.pp_output_data - - if ( - ComponentType.sym_load not in self.pgm_output_data - or self.pgm_output_data[ComponentType.sym_load].size == 0 - or ("load", load_id_names[0]) not in self.idx - ): - return - - # Store the results, while assuring that we are not overwriting any data - assert "res_load" not in self.pp_output_data - self.pp_output_data["res_load"] = self._pp_load_result_accumulate( - pp_component_name="load", load_id_names=load_id_names - ) - - def _pp_ward_output(self): - load_id_names = ["ward_const_power_load", "ward_const_impedance_load"] - assert "res_ward" not in self.pp_output_data - - if ( - ComponentType.sym_load not in self.pgm_output_data - or self.pgm_output_data[ComponentType.sym_load].size == 0 - or ("ward", load_id_names[0]) not in self.idx - ): - return - - accumulated_loads = self._pp_load_result_accumulate(pp_component_name="ward", load_id_names=load_id_names) - # TODO Find a better way for mapping vm_pu from bus - # accumulated_loads["vm_pu"] = np.nan - - # Store the results, while assuring that we are not overwriting any data - assert "res_ward" not in self.pp_output_data - self.pp_output_data["res_ward"] = accumulated_loads - - def _pp_motor_output(self): - load_id_names = ["motor_load"] + if symmetric: + res_table = "res_" + element + else: + res_table = "res_" + element + "_3ph" - assert "res_motor" not in self.pp_output_data + if element == "load": + load_id_names = ["const_power", "const_impedance", "const_current"] + elif element == "ward": + load_id_names = ["ward_const_power_load", "ward_const_impedance_load"] + elif element == "motor": + load_id_names = ["motor_load"] if ( ComponentType.sym_load not in self.pgm_output_data or self.pgm_output_data[ComponentType.sym_load].size == 0 - or ("motor", load_id_names[0]) not in self.idx + or (element, load_id_names[0]) not in self.idx ): return - # Store the results, while assuring that we are not overwriting any data - assert "res_motor" not in self.pp_output_data - self.pp_output_data["res_motor"] = self._pp_load_result_accumulate( - pp_component_name="motor", load_id_names=load_id_names + assert res_table not in self.pp_output_data + self.pp_output_data[res_table] = self._pp_load_result_accumulate( + pp_component_name=element, load_id_names=load_id_names ) def _pp_load_result_accumulate(self, pp_component_name: str, load_id_names: List[str]) -> pd.DataFrame: @@ -2131,26 +2103,30 @@ def _pp_trafos_output_3ph(self): # pylint: disable=too-many-statements assert "res_trafo_3ph" not in self.pp_output_data self.pp_output_data["res_trafo_3ph"] = pp_output_trafos_3ph - def _pp_loads_output_3ph(self): + def _pp_shunts_output_3ph(self): """ - This function converts a power-grid-model Symmetrical Load output array to a Load Dataframe of PandaPower. + This function converts a power-grid-model Shunt output array to a Shunt Dataframe of PandaPower. Returns: - a PandaPower Dataframe for the Load component + a PandaPower Dataframe for the Shunt component """ - load_id_names = ["const_power", "const_impedance", "const_current"] - if ( - ComponentType.sym_load not in self.pgm_output_data - or self.pgm_output_data[ComponentType.sym_load].size == 0 - or ("load", load_id_names[0]) not in self.idx - ): + # TODO: create unit tests for the function + assert "res_shunt_3ph" not in self.pp_output_data + + if ComponentType.shunt not in self.pgm_output_data or self.pgm_output_data[ComponentType.shunt].size == 0: return - # Store the results, while assuring that we are not overwriting any data - assert "res_load_3ph" not in self.pp_output_data - self.pp_output_data["res_load_3ph"] = self._pp_load_result_accumulate( - pp_component_name="load", load_id_names=load_id_names + pgm_output_shunts = self.pgm_output_data[ComponentType.shunt] + + pp_output_shunts = pd.DataFrame( + columns=["p_mw", "q_mvar", "vm_pu"], + index=self._get_pp_ids("shunt", pgm_output_shunts["id"]), ) + pp_output_shunts["p_mw"] = pgm_output_shunts["p"].sum() * 1e-6 + pp_output_shunts["q_mvar"] = pgm_output_shunts["q"].sum() * 1e-6 + # TODO Find a better way for mapping vm_pu from bus + # pp_output_shunts["vm_pu"] = np.nan + self.pp_output_data["res_shunt_3ph"] = pp_output_shunts def _pp_asym_loads_output_3ph(self): """ diff --git a/tests/unit/converters/test_pandapower_converter_output.py b/tests/unit/converters/test_pandapower_converter_output.py index 85d616f8..c73a7ae9 100644 --- a/tests/unit/converters/test_pandapower_converter_output.py +++ b/tests/unit/converters/test_pandapower_converter_output.py @@ -2,8 +2,8 @@ # # SPDX-License-Identifier: MPL-2.0 -from typing import Callable, List -from unittest.mock import ANY, MagicMock, patch +from typing import Any, Callable, Dict, List +from unittest.mock import ANY, MagicMock, call, patch import numpy as np import pandas as pd @@ -38,11 +38,14 @@ def test_create_output_data(): converter._pp_sgens_output.assert_called_once_with() converter._pp_trafos_output.assert_called_once_with() converter._pp_trafos3w_output.assert_called_once_with() - converter._pp_loads_output.assert_called_once_with() + expected_calls = [ + call(element="load", symmetric=True), + call(element="ward", symmetric=True), + call(element="motor", symmetric=True), + ] + converter._pp_load_elements_output.assert_has_calls(expected_calls) converter._pp_asym_loads_output.assert_called_once_with() converter._pp_asym_gens_output.assert_called_once_with() - converter._pp_motor_output.assert_called_once_with() - converter._pp_ward_output.assert_called_once_with() converter._pp_switches_output.assert_called_once_with() @@ -54,50 +57,57 @@ def test_create_output_data_3ph(): PandaPowerConverter._create_output_data_3ph(self=converter) # type: ignore # Assert - assert len(converter.method_calls) == 8 + assert len(converter.method_calls) == 11 converter._pp_buses_output_3ph.assert_called_once_with() converter._pp_lines_output_3ph.assert_called_once_with() converter._pp_ext_grids_output_3ph.assert_called_once_with() converter._pp_sgens_output_3ph.assert_called_once_with() converter._pp_trafos_output_3ph.assert_called_once_with() - converter._pp_loads_output_3ph.assert_called_once_with() + expected_calls = [ + call(element="load", symmetric=False), + call(element="ward", symmetric=False), + call(element="motor", symmetric=False), + ] + converter._pp_load_elements_output.assert_has_calls(expected_calls) converter._pp_asym_loads_output_3ph.assert_called_once_with() converter._pp_asym_gens_output_3ph.assert_called_once_with() @pytest.mark.parametrize( - ("create_fn", "table"), + ("create_fn", "table", "create_fn_kwargs"), [ - (PandaPowerConverter._pp_buses_output, "node"), - (PandaPowerConverter._pp_lines_output, "line"), - (PandaPowerConverter._pp_ext_grids_output, "source"), - (PandaPowerConverter._pp_shunts_output, "shunt"), - (PandaPowerConverter._pp_sgens_output, "sym_gen"), - (PandaPowerConverter._pp_trafos_output, "transformer"), - (PandaPowerConverter._pp_trafos3w_output, "three_winding_transformer"), - (PandaPowerConverter._pp_loads_output, "sym_load"), - (PandaPowerConverter._pp_asym_loads_output, "asym_load"), - (PandaPowerConverter._pp_asym_gens_output, "asym_gen"), - (PandaPowerConverter._pp_ward_output, "ward"), - (PandaPowerConverter._pp_motor_output, "motor"), - (PandaPowerConverter._pp_switches_output, "link"), - (PandaPowerConverter._pp_buses_output_3ph, "node"), - (PandaPowerConverter._pp_lines_output_3ph, "line"), - (PandaPowerConverter._pp_ext_grids_output_3ph, "source"), - (PandaPowerConverter._pp_sgens_output_3ph, "sym_gen"), - (PandaPowerConverter._pp_trafos_output_3ph, "transformer"), - (PandaPowerConverter._pp_loads_output_3ph, "sym_load"), - (PandaPowerConverter._pp_asym_loads_output_3ph, "asym_load"), - (PandaPowerConverter._pp_asym_gens_output_3ph, "asym_gen"), + (PandaPowerConverter._pp_buses_output, "node", {}), + (PandaPowerConverter._pp_lines_output, "line", {}), + (PandaPowerConverter._pp_ext_grids_output, "source", {}), + (PandaPowerConverter._pp_shunts_output, "shunt", {}), + (PandaPowerConverter._pp_sgens_output, "sym_gen", {}), + (PandaPowerConverter._pp_trafos_output, "transformer", {}), + (PandaPowerConverter._pp_trafos3w_output, "three_winding_transformer", {}), + (PandaPowerConverter._pp_load_elements_output, "load", {"symmetric": True, "element": "sym_load"}), + (PandaPowerConverter._pp_load_elements_output, "ward", {"symmetric": True, "element": "ward"}), + (PandaPowerConverter._pp_load_elements_output, "motor", {"symmetric": True, "element": "motor"}), + (PandaPowerConverter._pp_asym_loads_output, "asym_load", {}), + (PandaPowerConverter._pp_asym_gens_output, "asym_gen", {}), + (PandaPowerConverter._pp_switches_output, "link", {}), + (PandaPowerConverter._pp_buses_output_3ph, "node", {}), + (PandaPowerConverter._pp_lines_output_3ph, "line", {}), + (PandaPowerConverter._pp_ext_grids_output_3ph, "source", {}), + (PandaPowerConverter._pp_sgens_output_3ph, "sym_gen", {}), + (PandaPowerConverter._pp_trafos_output_3ph, "transformer", {}), + (PandaPowerConverter._pp_load_elements_output, "load", {"symmetric": False, "element": "sym_load"}), + (PandaPowerConverter._pp_load_elements_output, "ward", {"symmetric": False, "element": "ward"}), + (PandaPowerConverter._pp_load_elements_output, "motor", {"symmetric": False, "element": "motor"}), + (PandaPowerConverter._pp_asym_loads_output_3ph, "asym_load", {}), + (PandaPowerConverter._pp_asym_gens_output_3ph, "asym_gen", {}), ], ) -def test_create_pp_output_object__empty(create_fn: Callable[[PandaPowerConverter], None], table: str): +def test_create_pp_output_object__empty(create_fn: Callable[..., None], table: str, create_fn_kwargs: Dict[str, Any]): # Arrange: No table converter = PandaPowerConverter() # Act / Assert with patch("power_grid_model_io.converters.pandapower_converter.pd.DataFrame") as mock_df: - create_fn(converter) + create_fn(converter, **create_fn_kwargs) mock_df.assert_not_called() # Arrange: Empty table @@ -105,7 +115,7 @@ def test_create_pp_output_object__empty(create_fn: Callable[[PandaPowerConverter # Act / Assert with patch("power_grid_model_io.converters.pandapower_converter.pd.DataFrame") as mock_df: - create_fn(converter) + create_fn(converter, **create_fn_kwargs) mock_df.assert_not_called() @@ -532,15 +542,34 @@ def test_pp_load_result_accumulate__asym(): @pytest.mark.parametrize( - ("output_fn", "table", "load_id_names", "result_suffix"), + ("output_fn", "element", "symmetric", "table", "load_id_names", "result_suffix"), [ - (PandaPowerConverter._pp_loads_output, "load", ["const_power", "const_impedance", "const_current"], ""), - (PandaPowerConverter._pp_motor_output, "motor", ["motor_load"], ""), - (PandaPowerConverter._pp_loads_output_3ph, "load", ["const_power", "const_impedance", "const_current"], "_3ph"), + ( + PandaPowerConverter._pp_load_elements_output, + "load", + True, + "load", + ["const_power", "const_impedance", "const_current"], + "", + ), + (PandaPowerConverter._pp_load_elements_output, "motor", True, "motor", ["motor_load"], ""), + ( + PandaPowerConverter._pp_load_elements_output, + "load", + False, + "load", + ["const_power", "const_impedance", "const_current"], + "_3ph", + ), ], ) def test_output_load_types( - output_fn: Callable[[PandaPowerConverter], None], table: str, load_id_names: List[str], result_suffix: str + output_fn: Callable[[PandaPowerConverter, str, bool], None], + element: str, + symmetric: bool, + table: str, + load_id_names: List[str], + result_suffix: str, ): # Arrange converter = PandaPowerConverter() @@ -551,7 +580,7 @@ def test_output_load_types( converter._pp_load_result_accumulate = MagicMock() # type: ignore # Act - output_fn(converter) + output_fn(converter, element, symmetric) # Assert converter._pp_load_result_accumulate.assert_called_once_with(pp_component_name=table, load_id_names=load_id_names) @@ -570,7 +599,7 @@ def test_output_load_ward(): converter._pp_load_result_accumulate = MagicMock() # Act - converter._pp_ward_output() + converter._pp_load_elements_output(element="ward", symmetric=True) # Assert converter._pp_load_result_accumulate.assert_called_once_with(pp_component_name="ward", load_id_names=load_id_names) diff --git a/tests/validation/converters/test_pandapower_converter_output.py b/tests/validation/converters/test_pandapower_converter_output.py index 6916b133..d9d8bc77 100644 --- a/tests/validation/converters/test_pandapower_converter_output.py +++ b/tests/validation/converters/test_pandapower_converter_output.py @@ -8,8 +8,12 @@ from pathlib import Path from typing import Tuple +import numpy as np +import pandapower.networks as pp_networks import pandas as pd import pytest +from pandapower.results import reset_results +from power_grid_model import PowerGridModel from power_grid_model.validation import assert_valid_input_data from power_grid_model_io.converters import PandaPowerConverter @@ -278,3 +282,163 @@ def test_attributes_3ph(output_data_3ph: Tuple[PandaPowerData, PandaPowerData], # Assert pd.testing.assert_series_equal(actual_values, expected_values, atol=5e-4, rtol=1e-4) + + +def _get_total_powers_3ph(net): + """ + Calculates total complex power for sources, loads and losses + Input: Pandapower Network + Output: [s_ext_grid, s_load, s_loss] + """ + s_ext_grid = ( + net.res_ext_grid_3ph.loc[:, ["p_a_mw", "p_b_mw", "p_c_mw"]].sum().sum() + + 1j * net.res_ext_grid_3ph.loc[:, ["q_a_mvar", "q_b_mvar", "q_c_mvar"]].sum().sum() + ) + + if "res_asymmetric_load_3ph" in net: + s_load_asym = ( + net.res_asymmetric_load_3ph.loc[:, ["p_a_mw", "p_b_mw", "p_c_mw"]].sum().sum() + + 1j * net.res_asymmetric_load_3ph.loc[:, ["q_a_mvar", "q_b_mvar", "q_c_mvar"]].sum().sum() + ) + else: + s_load_asym = np.complex128() + + if "res_load_3ph" in net: + s_load_sym = net.res_load_3ph.loc[:, "p_mw"].sum() + 1j * net.res_load_3ph.loc[:, "q_mvar"].sum() + else: + s_load_sym = np.complex128() + + if "res_motor_3ph" in net: + s_motor = net.res_motor_3ph.loc[:, "p_mw"].sum() + 1j * net.res_motor_3ph.loc[:, "q_mvar"].sum() + else: + s_motor = np.complex128() + + if ("res_ward_3ph" in net) and (not net.res_ward_3ph.empty): + s_ward = net.res_ward_3ph.loc[:, "p_mw"].sum() + 1j * net.res_ward_3ph.loc[:, "q_mvar"].sum() + else: + s_ward = np.complex128() + + if ("res_shunt_3ph" in net) and (not net.res_shunt_3ph.empty): + s_shunt = net.res_shunt_3ph.loc[:, "p_mw"].sum() + 1j * net.res_shunt_3ph.loc[:, "q_mvar"].sum() + else: + s_shunt = np.complex128() + + s_load = s_load_sym + s_load_asym + s_motor + s_ward + s_shunt + + if "res_line_3ph" in net: + s_loss_line = ( + net.res_line_3ph.loc[:, ["p_a_l_mw", "p_b_l_mw", "p_c_l_mw"]].sum().sum() + + 1j * net.res_line_3ph.loc[:, ["q_a_l_mvar", "q_b_l_mvar", "q_c_l_mvar"]].sum().sum() + ) + else: + s_loss_line = np.complex128() + + if "res_trafo_3ph" in net: + s_loss_trafo = ( + net.res_trafo_3ph.loc[:, ["p_a_l_mw", "p_b_l_mw", "p_c_l_mw"]].sum().sum() + + 1j * net.res_trafo_3ph.loc[:, ["q_a_l_mvar", "q_b_l_mvar", "q_c_l_mvar"]].sum().sum() + ) + else: + s_loss_trafo = np.complex128() + + s_loss = s_loss_line + s_loss_trafo + return [s_ext_grid, s_load, s_loss] + + +def test_output_data_3ph__powers(): + def run_pf_asym_with_pgm(net): + reset_results(net, "pf_3ph") + pgm_converter = PandaPowerConverter() + input_data, _ = pgm_converter.load_input_data(net, make_extra_info=False) + pgm = PowerGridModel(input_data) + output_data = pgm.calculate_power_flow(symmetric=False) + output_tables = pgm_converter.convert(output_data) + for table in output_tables.keys(): + net[table] = output_tables[table] + + net = pp_networks.ieee_european_lv_asymmetric() + pp.create_load(net, 50, 0.1, 0.05) + run_pf_asym_with_pgm(net) + s_ext_grid, s_load, s_loss = _get_total_powers_3ph(net) + assert np.isclose(s_ext_grid, (s_load + s_loss)) + + pp.create_motor(net, 100, 0.1, 0.9) + pp.create_ward(net, 200, 0.1, 0.05, 0.1, 0.05) + pp.create_shunt_as_capacitor(net, 150, 0.09, 0) + run_pf_asym_with_pgm(net) + s_ext_grid, s_load, s_loss = _get_total_powers_3ph(net) + assert np.isclose(s_ext_grid, (s_load + s_loss)) + + +def _get_total_powers(net): + """ + Calculates total complex power for sources, loads and losses + Input: Pandapower Network + Output: [s_ext_grid, s_load, s_loss] + """ + s_ext_grid = net.res_ext_grid.loc[:, "p_mw"].sum() + 1j * net.res_ext_grid.loc[:, "q_mvar"].sum() + + if "res_asymmetric_load" in net: + s_load_asym = net.res_asymmetric_load.loc[:, "p_mw"].sum() + 1j * net.res_asymmetric_load.loc[:, "q_mvar"].sum() + else: + s_load_asym = np.complex128() + + if "res_load" in net: + s_load_sym = net.res_load.loc[:, "p_mw"].sum() + 1j * net.res_load.loc[:, "q_mvar"].sum() + else: + s_load_sym = np.complex128() + + if "res_motor" in net: + s_motor = net.res_motor.loc[:, "p_mw"].sum() + 1j * net.res_motor.loc[:, "q_mvar"].sum() + else: + s_motor = np.complex128() + + if "res_ward" in net: + s_ward = net.res_ward.loc[:, "p_mw"].sum() + 1j * net.res_ward.loc[:, "q_mvar"].sum() + else: + s_ward = np.complex128() + + if "res_shunt" in net: + s_shunt = net.res_shunt.loc[:, "p_mw"].sum() + 1j * net.res_shunt.loc[:, "q_mvar"].sum() + else: + s_shunt = np.complex128() + + s_load = s_load_sym + s_load_asym + s_motor + s_ward + s_shunt + + if "res_line" in net: + s_loss_line = net.res_line.loc[:, "pl_mw"].sum() + 1j * net.res_line.loc[:, "ql_mvar"].sum() + else: + s_loss_line = np.complex128() + + if "res_trafo" in net: + s_loss_trafo = net.res_trafo.loc[:, "pl_mw"].sum() + 1j * net.res_trafo.loc[:, "ql_mvar"].sum() + else: + s_loss_trafo = np.complex128() + + s_loss = s_loss_line + s_loss_trafo + return [s_ext_grid, s_load, s_loss] + + +def test_output_data__powers(): + def run_pf_sym_with_pgm(net): + reset_results(net, "pf") + pgm_converter = PandaPowerConverter() + input_data, _ = pgm_converter.load_input_data(net, make_extra_info=False) + pgm = PowerGridModel(input_data) + output_data = pgm.calculate_power_flow(symmetric=True) + output_tables = pgm_converter.convert(output_data) + for table in output_tables.keys(): + net[table] = output_tables[table] + + net = pp_networks.ieee_european_lv_asymmetric() + pp.create_load(net, 50, 0.1, 0.05) + run_pf_sym_with_pgm(net) + s_ext_grid, s_load, s_loss = _get_total_powers(net) + assert np.isclose(s_ext_grid, (s_load + s_loss)) + + pp.create_motor(net, 100, 0.1, 0.9) + pp.create_ward(net, 200, 0.1, 0.05, 0.1, 0.05) + pp.create_shunt_as_capacitor(net, 150, 0.09, 0) + run_pf_sym_with_pgm(net) + s_ext_grid, s_load, s_loss = _get_total_powers(net) + assert np.isclose(s_ext_grid, (s_load + s_loss))