Skip to content

[BUG] Effect of transformation ratio and u_rated on short circuit calculation #1371

@nitbharambe

Description

@nitbharambe

IEC standard specifies usage of rated voltage of the node to do short circuit calculations. This is because it makes no assumption on actual voltage but assumes that nominally it would be 1.0 p.u. for a bus and at extreme, 1.1.

PGM is not 100% conformant with IEC 60909.
We do not calculate short circuit currents using rated voltage of fault nodes: c * u_rated. But we use c * u_rated * k_eq which includes any transformation ratio k_eq involved from source to the fault node. This k factor is statically present in the grid and not dependent on load conditions hence it is present in exact form in the short circuit situation too.
The design decision was that since our approach would be more accurate compared to the standard, we do not have to stick with certain assumptions.
The other decisions like inclusion of magnetisation current, tap positions and capcitance in the short circuit calculations are easily managed on the user side by setting i0, c1, c0, tap_pos, tap_nom as 0. In the case of voltage, determining k_eq especially with multiple sources becomes more complex to bypass.

Many of MV grids have varied transformer secondary ratings compared to a fixed 10kV node voltage rating, hence this issue concerns those cases.
As expected, we also deviate from other calculation engines in this regard (See in the script below). Hence our validation does not become straightforward.

Given these points, let us revisit this decision again.

Comparison script

Reproduces PGM calculations, calculations inside PGM replicated by hand, hand calculations via IEC standard, vision (Calculated externally) and pandapower. (TLDR to relevant functions or output directly.)

The parameters are unrealistic but suitable for understanding the calculation.

source -- node_1 -- transformer -- node_2
import warnings
from pprint import pprint
import numpy as np

from power_grid_model import (
    ComponentType,
    CalculationMethod,
    PowerGridModel,
    initialize_array,
    DatasetType,
    AttributeType as AT,
    FaultType,
    ShortCircuitVoltageScaling,
    FaultPhase
)
import pandapower as pp


# Constants
s_base = 1e6
u_rated_source_node = 100e3
z_base_source_node = u_rated_source_node * u_rated_source_node / s_base
i_base_source_node = s_base / u_rated_source_node
# u_rated_transformer_node will be set in the function calls below. Not a constant

# Transformer params
u1 = 100e3
u2 = 10e3
sn = 1000e6
uk = 0.1


def calculate_pgm_engine(skss, c, fault_node, u_rated_transformer_node):
    skss_adj = skss / c  # Assuming #1352 is resolved

    # node
    node = initialize_array(DatasetType.input, ComponentType.node, 2)
    node[AT.id] = [1, 2]
    node[AT.u_rated] = [u_rated_source_node, u_rated_transformer_node]

    # source
    source = initialize_array(DatasetType.input, ComponentType.source, 1)
    source[AT.id] = [3]
    source[AT.node] = [1]
    source[AT.status] = [1]
    source[AT.u_ref] = [1.0]
    source[AT.sk] = [skss_adj]
    source[AT.rx_ratio] = [1e-4]

    # transformer
    transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)
    transformer[AT.id] = [4]
    transformer[AT.from_node] = [1]
    transformer[AT.to_node] = [2]
    transformer[AT.from_status] = [1]
    transformer[AT.to_status] = [1]
    transformer[AT.u1] = [u1]
    transformer[AT.u2] = [u2]
    transformer[AT.sn] = [sn]
    transformer[AT.uk] = [uk]
    transformer[AT.pk] = [0.0]
    transformer[AT.p0] = [0.0]
    transformer[AT.i0] = [0.0]
    transformer[AT.tap_side] = [0]
    transformer[AT.tap_pos] = [0]
    transformer[AT.tap_nom] = [0]
    transformer[AT.tap_min] = [-10]
    transformer[AT.tap_max] = [10]
    transformer[AT.tap_size] = [1000]
    transformer[AT.clock] = [0]
    transformer[AT.winding_from] = [0]
    transformer[AT.winding_to] = [0]

    # fault
    fault = initialize_array(DatasetType.input, ComponentType.fault, 1)
    fault[AT.id] = [5]
    fault[AT.fault_object] = [fault_node]
    fault[AT.status] = [1]
    fault[AT.fault_type] = FaultType.three_phase
    fault[AT.fault_phase] = FaultPhase.abc
    fault[AT.r_f] = [0.0]
    fault[AT.x_f] = [0.0]

    # all
    input_data = {
        ComponentType.node: node,
        ComponentType.source: source,
        ComponentType.transformer: transformer,
        ComponentType.fault: fault,
    }

    model = PowerGridModel(input_data)

    short_circuit_voltage_scaling = (
        ShortCircuitVoltageScaling.minimum
        if np.isclose(c, 1.0)
        else ShortCircuitVoltageScaling.maximum
    )
    output_data = model.calculate_short_circuit(
        calculation_method=CalculationMethod.iec60909,
        short_circuit_voltage_scaling=short_circuit_voltage_scaling,
    )

    return {
        "i_f": output_data[ComponentType.fault][AT.i_f][0, 0]
    }


def calculate_pandapower(skss, c, fault_node, u_rated_transformer_node):
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")

        #create empty net
        net = pp.create_empty_network()

        #create buses
        bus1 = pp.create_bus(net, vn_kv=u_rated_source_node, name="Bus 1")
        bus2 = pp.create_bus(net, vn_kv=u_rated_transformer_node, name="Bus 2")

        pp.create_ext_grid(net, bus=bus1, vm_pu=1.0, s_sc_max_mva=skss, s_sc_min_mva=skss, rx_max=1e-4, rx_min=1e-4)
        pp.create_transformer_from_parameters(net, hv_bus=bus1, lv_bus=bus2, sn_mva=sn, vn_hv_kv=u1, vn_lv_kv=u2, vk_percent=uk*100, vkr_percent=0.0, pfe_kw=0.0, i0_percent=0.0)

        case = "min" if np.isclose(c, 1.0) else "max"
        fault_bus = bus1 if fault_node == 1 else bus2

        pp.shortcircuit.calc_sc(net, case=case, ip=False, r_fault_ohm=0.0)
        i_f_ka = net.res_bus_sc["ikss_ka"][fault_bus] # ka is actually amperes
        return {"i_f": i_f_ka}


def calculate_vision(skss, c, fault_node, u_rated_transformer_node):
    vision_results = {
        (1000e6, 1.0, 1, 10e3): 5774,
        (1000e6, 1.1, 1, 10e3): 5774,
        (1000e6, 1.0, 2, 10e3): 52490,
        (1000e6, 1.1, 2, 10e3): 52990,
        (1000e6, 1.0, 2, 10.5e3): 55110,
        (1000e6, 1.1, 2, 10.5e3): 55640,
    }
    return {"i_f": vision_results.get((skss, c, fault_node, u_rated_transformer_node), None)}


def calculate_hand(skss, c, fault_node, u_rated_transformer_node):
    z_base_transformer_node = u_rated_transformer_node * u_rated_transformer_node / s_base
    i_base_transformer_node = s_base / u_rated_transformer_node

    z_k_pu = c / (skss / s_base)
    if fault_node == 1:
        i_f_pu = c * 1.0 / (z_k_pu * np.sqrt(3))
        i_f = i_f_pu * i_base_source_node
        return {
            "i_f_iec": i_f,
        }
    elif fault_node == 2:
        k = (u1 / u2) / (u_rated_source_node / u_rated_transformer_node)

        z_k_lv_pu = z_k_pu * (1 / k / k)
        z_k_lv_ohms = z_k_lv_pu * z_base_transformer_node

        z_base_transformer_rating = u2 * u2 / sn
        z_t_ohms = uk * z_base_transformer_rating
        z_t_pu = z_t_ohms / z_base_transformer_node

        z_total_pu = z_k_lv_pu + z_t_pu
        z_total_ohms = z_k_lv_ohms + z_t_ohms

        i_f_pu_pgm = (c * 1.0 / k) / (z_total_pu * np.sqrt(3))
        i_f_pgm = i_f_pu_pgm * i_base_transformer_node

        # Transformer impedance correction factor
        if np.isclose(c, 1.1):
            k_t_correction_factor = (0.95 * c) / ( 1 + 0.6 * uk)
            z_total_ohms = z_k_lv_ohms + z_t_ohms * k_t_correction_factor

        i_f_iec = c * u_rated_transformer_node/ (z_total_ohms * np.sqrt(3))

        return {
            "i_f_pgm": i_f_pgm,
            "i_f_iec": i_f_iec,
        }
    

def to_float(d):
    return {k: (f"{float(v):.2f}" if isinstance(v, np.floating) else f"{v:.2f}") for k, v in d.items()}

def run():
    cases = [
        (1000e6, 1.0, 1, 10e3),
        (1000e6, 1.1, 1, 10e3),
        (1000e6, 1.0, 2, 10e3),
        (1000e6, 1.0, 2, 10.5e3),
        (1000e6, 1.1, 2, 10e3),
        (1000e6, 1.1, 2, 10.5e3),
    ]

    results = {}
    for skss, c, fault_node, u_rated_transformer_node in cases:
        results[(u_rated_transformer_node, c, fault_node)] = {
            "pgm":        to_float(calculate_pgm_engine(skss=skss, c=c, fault_node=fault_node, u_rated_transformer_node=u_rated_transformer_node)),
            "hand":       to_float(calculate_hand(skss=skss, c=c, fault_node=fault_node, u_rated_transformer_node=u_rated_transformer_node)),
            "pandapower": to_float(calculate_pandapower(skss=skss, c=c, fault_node=fault_node, u_rated_transformer_node=u_rated_transformer_node)),
            "vision":     to_float(calculate_vision(skss=skss, c=c, fault_node=fault_node, u_rated_transformer_node=u_rated_transformer_node)),
        }

    print("\nCase header: (u_rated_transformer_node, c, fault_node)\n")
    pprint(results, indent=4, sort_dicts=False, width=200)

if __name__ == "__main__":
    run()

gives output

Case header: (u_rated_transformer_node, c, fault_node)

{   (10000.0, 1.0, 1): {   'pgm': {'i_f': '5773.50'},
                           'hand': {'i_f_iec': '5773.50'},
                           'pandapower': {'i_f': '5773.50'},
                           'vision': {'i_f': 5774}},
    (10000.0, 1.1, 1): {   'pgm': {'i_f': '5773.50'},
                           'hand': {'i_f_iec': '5773.50'},
                           'pandapower': {'i_f': '5773.50'},
                           'vision': {'i_f': 5774}},
    (10000.0, 1.0, 2): {   'pgm': {'i_f': '52486.39'},
                           'hand': {'i_f_pgm': '52486.39', 'i_f_iec': '52486.39'},
                           'pandapower': {'i_f': '52554.00'},
                           'vision': {'i_f': 52490}},
    (10500.0, 1.0, 2): {   'pgm': {'i_f': '52486.39'},
                           'hand': {'i_f_pgm': '52486.39', 'i_f_iec': '55110.71'},
                           'pandapower': {'i_f': '55181.70'},
                           'vision': {'i_f': 55110}},
    (10000.0, 1.1, 2): {   'pgm': {'i_f': '52923.77'},
                           'hand': {'i_f_pgm': '52923.77', 'i_f_iec': '52986.26'},
                           'pandapower': {'i_f': '52986.26'},
                           'vision': {'i_f': 52990}},
    (10500.0, 1.1, 2): {   'pgm': {'i_f': '52923.77'},
                           'hand': {'i_f_pgm': '52923.77', 'i_f_iec': '55635.57'},
                           'pandapower': {'i_f': '55635.57'},
                           'vision': {'i_f': 55640}}}

Note:

  • Pandapower minimum results include the transformer impedance correction as well (I see conflicting information regarding this in the internet, need to verify which is correct). If we compare it with a hypothetical hand calculation including c=1.1 impedance correction factor, we get the exact same result. Maybe this is a bug at their end, I shall raise an issue there after we resolve our discussion here.
  • Vision results are rounded off.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions