diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 461f2a3d7..fc5f305d1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,8 @@ Change Log ------------------------------- - [FIXED] restored ``OpenDSSDirect.py`` to the ``all``/``dev`` extras so the OpenDSS converter is exercised (and its coverage reported) in CI again; it was dropped in #3062 because installing it alongside ``pytest~=9.1`` crashed the pytest process on Windows. Root cause (see `dss-extensions/OpenDSSDirect.py#148 `_): pytest enables Python's ``faulthandler`` by default, which intercepts a first-chance Windows structured exception that OpenDSSDirect.py's native backend raises -- and normally handles itself -- during import, and misreports it as fatal. Bracketing the import with ``faulthandler.disable()``/``.enable()`` avoids the false crash while leaving ``faulthandler`` protecting the rest of the test run; the converter now runs on Windows instead of merely skipping there. - [ADDED] OpenDSS converter: series (bus-to-bus) ``Reactor`` elements are now imported as a fixed-impedance ``line``, the pattern some feeder libraries (e.g. EPRI's Ckt5/Ckt7) use to model the substation's Thevenin-equivalent source impedance instead of a ``Transformer``. +- [CHANGED] missing zero sequence parameters of ext_grid and line are reported with a descriptive error instead of a KeyError +- [ADDED] r0x0_min and x0x_min to the ext_grid schema and documentation [3.5.4] - 2026-07-08 ------------------------------- diff --git a/pandapower/network_schema/ext_grid.py b/pandapower/network_schema/ext_grid.py index eabf23451..5f56f3e89 100644 --- a/pandapower/network_schema/ext_grid.py +++ b/pandapower/network_schema/ext_grid.py @@ -26,7 +26,7 @@ nullable=True, required=False, description="maximum short circuit power provision [MVA]", - metadata={"sc": True, "cim": True}, + metadata={"sc": True, "3ph": True, "cim": True, "example": "1000"}, ), "s_sc_min_mva": pa.Column( float, @@ -34,7 +34,7 @@ nullable=True, required=False, description="minimum short circuit power provision [MVA]", - metadata={"sc": True, "cim": True}, + metadata={"sc": True, "cim": True, "example": "1000"}, ), "rx_max": pa.Column( float, @@ -42,7 +42,7 @@ nullable=True, required=False, description="maxium R/X ratio of short-circuit impedance", - metadata={"sc": True, "3ph": True, "cim": True}, + metadata={"sc": True, "3ph": True, "cim": True, "example": "0.1"}, ), "rx_min": pa.Column( float, @@ -50,7 +50,7 @@ nullable=True, required=False, description="minimum R/X ratio of short-circuit impedance", - metadata={"sc": True, "3ph": True, "cim": True}, + metadata={"sc": True, "3ph": True, "cim": True, "example": "0.1"}, ), "r0x0_max": pa.Column( float, @@ -58,7 +58,7 @@ nullable=True, required=False, description="maximal R/X-ratio to calculate Zero sequence internal impedance of ext_grid", - metadata={"sc": True, "3ph": True, "cim": True}, + metadata={"sc": True, "3ph": True, "cim": True, "example": "0.1"}, ), "x0x_max": pa.Column( float, @@ -66,7 +66,23 @@ nullable=True, required=False, description="maximal X0/X-ratio to calculate Zero sequence internal impedance of ext_grid", - metadata={"sc": True, "3ph": True, "cim": True}, + metadata={"sc": True, "3ph": True, "cim": True, "example": "1.0"}, + ), + "r0x0_min": pa.Column( + float, + pa.Check.ge(0), + nullable=True, + required=False, + description="minimal R/X-ratio to calculate Zero sequence internal impedance of ext_grid", + metadata={"sc": True, "example": "0.1"}, + ), + "x0x_min": pa.Column( + float, + pa.Check.ge(0), + nullable=True, + required=False, + description="minimal X0/X-ratio to calculate Zero sequence internal impedance of ext_grid", + metadata={"sc": True, "example": "1.0"}, ), "slack_weight": pa.Column( float, diff --git a/pandapower/network_schema/line.py b/pandapower/network_schema/line.py index 09b8f5412..5ccd97664 100644 --- a/pandapower/network_schema/line.py +++ b/pandapower/network_schema/line.py @@ -39,7 +39,7 @@ nullable=True, required=False, description="zero sequence resistance of the line [Ohm per km]", - metadata={"sc": True, "3ph": True, "cim": True}, + metadata={"sc": True, "3ph": True, "cim": True, "example": "0.1"}, ), "x0_ohm_per_km": pa.Column( float, @@ -47,7 +47,7 @@ nullable=True, required=False, description="zero sequence reactance of the line [Ohm per km]", - metadata={"sc": True, "3ph": True, "cim": True}, + metadata={"sc": True, "3ph": True, "cim": True, "example": "0.1"}, ), "c0_nf_per_km": pa.Column( float, @@ -55,7 +55,7 @@ nullable=True, required=False, description="zero sequence capacitance of the line [nano Farad per km]", - metadata={"sc": True, "3ph": True, "cim": True}, + metadata={"sc": True, "3ph": True, "cim": True, "example": "0.0"}, ), "g0_us_per_km": pa.Column( float, diff --git a/pandapower/pd2ppc.py b/pandapower/pd2ppc.py index be7f970f8..d34eb8b9b 100644 --- a/pandapower/pd2ppc.py +++ b/pandapower/pd2ppc.py @@ -135,6 +135,13 @@ def _pd2ppc(net, sequence=None, **kwargs): check_connectivity = net["_options"]["check_connectivity"] calculate_voltage_angles = net["_options"]["calculate_voltage_angles"] + if sequence == 0 or mode == "pf_3ph": + # the zero sequence network is built from parameters that the balanced power flow does not + # need. They are checked here, i.e. before the positive sequence network of an unbalanced + # power flow is built, so that all of them are reported at once + from pandapower.pd2ppc_zero import _check_zero_sequence_parameters + _check_zero_sequence_parameters(net) + ppc = _init_ppc(net, mode=mode, sequence=sequence) # generate ppc['bus'] and the bus lookup diff --git a/pandapower/pd2ppc_zero.py b/pandapower/pd2ppc_zero.py index c1fb71149..d0aef8374 100644 --- a/pandapower/pd2ppc_zero.py +++ b/pandapower/pd2ppc_zero.py @@ -56,6 +56,15 @@ _end_temperature_correction_factor, ) from pandapower.pd2ppc import _ppc2ppci, _init_ppc +from pandapower.network_schema.tools.helper import get_element_schema + +# parameters that are needed to build the zero sequence network. The case dependent ones are +# formatted with the short-circuit case, the example values reported for them by +# _check_zero_sequence_parameters come from the "example" metadata of the pandera schemas. +_ZERO_SEQUENCE_PARAMETERS = { + "ext_grid": ("s_sc_{case}_mva", "rx_{case}", "x0x_{case}", "r0x0_{case}"), + "line": ("r0_ohm_per_km", "x0_ohm_per_km", "c0_nf_per_km"), +} def _pd2ppc_zero(net, k_st, sequence=0): @@ -67,6 +76,7 @@ def _pd2ppc_zero(net, k_st, sequence=0): """ # select elements in service (time consuming, so we do it once) net["_is_elements"] = _select_is_elements_numba(net, sequence=sequence) + _check_zero_sequence_parameters(net) ppc = _init_ppc(net, sequence) @@ -100,6 +110,81 @@ def _pd2ppc_zero(net, k_st, sequence=0): return ppc, ppci +def _example_value(element, parameter): + """ + Read the example value of a parameter from the pandera schema of its element. + + Args: + element: name of the element table, e.g. "ext_grid". + parameter: name of the column in that table, e.g. "x0x_max". + + Returns: + The value shown for the parameter in the hint of _check_zero_sequence_parameters, taken + from the "example" metadata of the schema column, or "..." if the schema does not define + one. + """ + schema = get_element_schema(element) + if schema is None or parameter not in schema.columns: + return "..." + return (schema.columns[parameter].metadata or {}).get("example", "...") + + +def _check_zero_sequence_parameters(net): + """ + Check that the parameters needed to build the zero sequence network are available. + + The unbalanced power flow and the unbalanced short-circuit calculation need zero sequence + parameters of ext_grid and line that the balanced power flow does not require. They are + collected here in one place, so that all of them are reported at once with a hint on how to + add them, instead of raising a KeyError on the first one that happens to be used. + + Args: + net: the pandapower network whose zero sequence parameters are checked. + + Raises: + ValueError: if a parameter is missing from the element table, or is undefined for an + element that is in service. + """ + case = net["_options"]["case"] if net["_options"]["mode"] == "sc" else "max" + # the ext_grid impedance is only added for ext_grids in service, while the line impedances are + # read from the whole line table as soon as it holds any line. The values are only used for the + # elements in service though, so undefined values are reported for those only. + is_element = { + "ext_grid": net["_is_elements"]["ext_grid"], + "line": net["line"].index.isin(net["_is_elements"]["line_is_idx"]), + } + missing, incomplete = {}, {} + for element, parameters in _ZERO_SEQUENCE_PARAMETERS.items(): + if len(net[element]) == 0 or (element == "ext_grid" and not is_element[element].any()): + continue + for parameter in parameters: + parameter = parameter.format(case=case) + if parameter not in net[element].columns: + missing.setdefault(element, []).append(parameter) + elif net[element][parameter].isnull().values[is_element[element]].any(): + incomplete.setdefault(element, []).append(parameter) + + if not missing and not incomplete: + return + + message = "Zero sequence parameters are needed for unbalanced calculations:" + for element, parameters in missing.items(): + message += f"\n net.{element} is missing the column(s) {parameters}" + for element, parameters in incomplete.items(): + message += f"\n net.{element} has undefined values in the column(s) {parameters}" + if missing: + message += "\n Try:" + for element, parameters in missing.items(): + for parameter in parameters: + message += f"\n net.{element}['{parameter}'] = {_example_value(element, parameter)}" + message += ( + "\n For lines and transformers that have a standard type, the zero sequence parameters " + "can be taken from the standard type library with " + "pandapower.add_zero_impedance_parameters(net)." + ) + raise ValueError(message) + + def _build_branch_ppc_zero(net, ppc, k_st=None): """ Takes the empty ppc network and fills it with the zero imepdance branch values. The branch diff --git a/pandapower/test/loadflow/test_runpp_3ph.py b/pandapower/test/loadflow/test_runpp_3ph.py index 5deda1dc5..afe02eaa0 100644 --- a/pandapower/test/loadflow/test_runpp_3ph.py +++ b/pandapower/test/loadflow/test_runpp_3ph.py @@ -138,6 +138,40 @@ def check_it(net): assert np.max(np.abs(line_pp - line_pf)) < 1.1e-4 +@pytest.mark.parametrize( + "dropped_parameters, element", + [ + (["s_sc_max_mva", "rx_max"], "ext_grid"), + (["r0x0_max", "x0x_max"], "ext_grid"), + (["r0_ohm_per_km", "x0_ohm_per_km", "c0_nf_per_km"], "line"), + ], +) +def test_2bus_network_missing_zero_sequence_parameters(test_net, dropped_parameters, element): + # every zero sequence parameter is reported with the same error, see issue #3069 + add_zero_impedance_parameters(test_net) + test_net[element] = test_net[element].drop(columns=dropped_parameters) + with pytest.raises(ValueError) as excinfo: + runpp_3ph(test_net) + for parameter in dropped_parameters: + assert f"net.{element}['{parameter}'] = " in str(excinfo.value) + + +def test_2bus_network_undefined_zero_sequence_parameters(test_net): + # -o---o + # \---o + b = create_bus(test_net, vn_kv=110) + second_line = create_line(test_net, from_bus=1, to_bus=b, length_km=50.0, std_type="example_type") + add_zero_impedance_parameters(test_net) + test_net.line.loc[second_line, "x0_ohm_per_km"] = np.nan + with pytest.raises(ValueError, match="undefined values in the column"): + runpp_3ph(test_net) + + # values of elements out of service are not used, so they are not required either + test_net.line.loc[second_line, "in_service"] = False + runpp_3ph(test_net) + assert test_net["converged"] + + def test_2bus_network(test_net): # -o---o add_zero_impedance_parameters(test_net)