Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/dss-extensions/OpenDSSDirect.py/issues/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
-------------------------------
Expand Down
28 changes: 22 additions & 6 deletions pandapower/network_schema/ext_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,63 @@
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,
pa.Check.gt(0),
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,
pa.Check.ge(0),
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,
pa.Check.ge(0),
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,
pa.Check.ge(0),
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,
pa.Check.ge(0),
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,
Expand Down
6 changes: 3 additions & 3 deletions pandapower/network_schema/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,23 @@
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,
pa.Check.ge(0),
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,
pa.Check.ge(0),
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,
Expand Down
7 changes: 7 additions & 0 deletions pandapower/pd2ppc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions pandapower/pd2ppc_zero.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default Values should not be "hidden" in the code. They should be added to the pandera metadata and used from there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the idea, but metadata is on v4 only. I can rebase.



def _pd2ppc_zero(net, k_st, sequence=0):
Expand All @@ -67,6 +76,7 @@
"""
# 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)

Expand Down Expand Up @@ -100,6 +110,81 @@
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.
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use google style docstring.

I am aware that pandapower is currently a mix of different doc string styles, but longterm everything should be using the google style docstring, as this has been the best version for rendering in read the docs so far.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed the style in the rest of the file, I can change for this function only but that will be inconsistent with the rest of the file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file only has one other docstring using this style, please do convert the docstring to google style even if it is inconsistent with the other docstring. These are internal use functions anyway and will never appear in the docs. I would rather have this already in the style that should be applied to all of pandapower than having to convert it later at some point.

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)

@KS-HTK KS-HTK Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a function for checking the existance of a column in the table, it will add the table and column if they do not exist (with default value if available). See pandapower.create._utils.add_column_to_df (on branch release/v4.0.0).

@Houmgaor Houmgaor Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pandapower.create._utils.add_column_to_df adds a column rather that raising the error. The former may add default values (x0x_max/s_sc_max_mva) and create false short-circuits, so I'd rather raise for the code. Also, the function is only on v4 if you still want to use it, so the PR would need a rebase if you want the feature there.


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
Expand Down Expand Up @@ -176,7 +261,7 @@
)

for vector_group, trafos in trafo_df.groupby("vector_group"):
# TODO: check this/expand this

Check warning on line 264 in pandapower/pd2ppc_zero.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=e2nIEE_pandapower&issues=AaAjvRLPrFe4IvmIuPK_&open=AaAjvRLPrFe4IvmIuPK_&pullRequest=3089
ppc_idx = trafos["_ppc_idx"].values.astype(np.int64)

if vector_group.lower() in ["yy", "yd", "dy", "dd"]:
Expand Down Expand Up @@ -609,7 +694,7 @@


def _add_trafo3w_sc_impedance_zero(net, ppc):
# TODO: check this/expand this

Check warning on line 697 in pandapower/pd2ppc_zero.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=e2nIEE_pandapower&issues=AaAjvRLPrFe4IvmIuPLA&open=AaAjvRLPrFe4IvmIuPLA&pullRequest=3089
branch_lookup = net["_pd2ppc_lookups"]["branch"]
if "trafo3w" not in branch_lookup:
return
Expand Down
34 changes: 34 additions & 0 deletions pandapower/test/loadflow/test_runpp_3ph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading