Skip to content

Grid Helpers#

Grid Helpers Pandapower#

toop_engine_grid_helpers.pandapower #

toop_engine_grid_helpers.pandapower.example_grids #

add_phaseshift_transformer_to_line #

add_phaseshift_transformer_to_line(
    net,
    line_idx,
    at_from_bus=True,
    tap_min=-30,
    tap_max=30,
    tap_step_degree=2.0,
)

Inserts a phase-shifting transformer into the pandapower network on the given line.

PARAMETER DESCRIPTION
net

The pandapower network.

TYPE: pandapowerNet

line_idx

Index of the line in net.line on which to insert the phase-shifting transformer.

TYPE: int

at_from_bus

If True, the phase-shifting transformer is inserted at the from-bus of the line. If False, it is inserted at the to-bus (default is True

TYPE: bool DEFAULT: True

tap_min

Minimum tap position of the phase-shifting transformer (default is -30).

TYPE: int DEFAULT: -30

tap_max

Maximum tap position of the phase-shifting transformer (default is 30).

TYPE: int DEFAULT: 30

tap_step_degree

Step size in degrees for the tap position of the phase-shifting transformer (default is 2.0).

TYPE: float DEFAULT: 2.0

RETURNS DESCRIPTION
trafo_idx

The index of the newly created transformer in net.trafo.

TYPE: int

helper_bus

The index of the newly created helper bus.

TYPE: int

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/example_grids.py
def add_phaseshift_transformer_to_line(
    net: pp.pandapowerNet,
    line_idx: int,
    at_from_bus: bool = True,
    tap_min: int = -30,
    tap_max: int = 30,
    tap_step_degree: float = 2.0,
) -> tuple[np.integer, np.integer]:
    """
    Inserts a phase-shifting transformer into the pandapower network on the given line.

    Parameters
    ----------
    net : pandapowerNet
        The pandapower network.
    line_idx : int
        Index of the line in net.line on which to insert the phase-shifting transformer.
    at_from_bus : bool, optional
        If True, the phase-shifting transformer is inserted at the from-bus of the line.
        If False, it is inserted at the to-bus (default is True
    tap_min : int, optional
        Minimum tap position of the phase-shifting transformer (default is -30).
    tap_max : int, optional
        Maximum tap position of the phase-shifting transformer (default is 30).
    tap_step_degree : float, optional
        Step size in degrees for the tap position of the phase-shifting transformer (default is 2.0).

    Returns
    -------
    trafo_idx : int
        The index of the newly created transformer in net.trafo.
    helper_bus : int
        The index of the newly created helper bus.
    """
    # 1) Get the from-bus (and base voltage) of the given line
    from_bus = net.line.at[line_idx, "from_bus"]
    to_bus = net.line.at[line_idx, "to_bus"]
    if not at_from_bus:
        from_bus, to_bus = to_bus, from_bus

    base_kv = net.bus.at[from_bus, "vn_kv"]

    # 2) Create a new helper bus with the same base voltage as from_bus, and put it in the middle
    # of the buses, slightly towards the from_bus

    from_x = json.loads(net.bus.at[from_bus, "geo"])["coordinates"][0]
    from_y = json.loads(net.bus.at[from_bus, "geo"])["coordinates"][1]
    to_x = json.loads(net.bus.at[to_bus, "geo"])["coordinates"][0]
    to_y = json.loads(net.bus.at[to_bus, "geo"])["coordinates"][1]
    helper_x = (from_x + to_x) / 2 + (from_x - to_x) * 0.1
    helper_y = (from_y + to_y) / 2 + (from_y - to_y) * 0.1

    helper_bus = pp.create_bus(net, vn_kv=base_kv, name=f"Helper Bus for Line {line_idx}", geodata=(helper_x, helper_y))

    # 3) Insert a transformer with the specified shift angle
    #    (For simplicity, we pick some typical parameters. Adjust as needed.)
    trafo_idx = pp.create_transformer_from_parameters(
        net,
        hv_bus=from_bus,
        lv_bus=helper_bus,
        sn_mva=max(net.sn_mva, 10),  # or a relevant S rated MVA
        vn_hv_kv=base_kv,
        vn_lv_kv=base_kv,
        vk_percent=10.0,  # Example short-circuit voltage
        vkr_percent=0.1,  # Example short-circuit real part
        pfe_kw=0,  # Example iron losses
        i0_percent=0,  # Example open-circuit currents
        name=f"Phase Shift Trafo on Line {line_idx}",
        tap_side="hv",
        tap_neutral=0,  # position = 0 is the neutral
        tap_min=tap_min,  # min tap position
        tap_max=tap_max,  # max tap position
        tap_step_degree=tap_step_degree,
        tap_pos=0,  # start tap position
        tap_changer_type=True,
    )

    # 4) “Move” the from-bus connection of the line to the helper bus
    net.line.at[line_idx, ("from_bus" if at_from_bus else "to_bus")] = helper_bus

    return trafo_idx, helper_bus

pandapower_case30_with_psts #

pandapower_case30_with_psts()

Create a pandapower IEEE 30 bus grid with phase-shifting transformers.

RETURNS DESCRIPTION
pandapowerNet

The pandapower IEEE 30 bus network with phase-shifting transformers.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/example_grids.py
def pandapower_case30_with_psts() -> pp.pandapowerNet:
    """Create a pandapower IEEE 30 bus grid with phase-shifting transformers.

    Returns
    -------
    pp.pandapowerNet
        The pandapower IEEE 30 bus network with phase-shifting transformers.
    """
    net = pp.networks.case30()
    # Add two phase shifters in the middle of the grid and one on the edge to almost separate
    # the grid into two parts, only connected by line 34 if you removed all psts
    add_phaseshift_transformer_to_line(net, 13, at_from_bus=False, tap_min=-20, tap_max=20, tap_step_degree=1.0)
    add_phaseshift_transformer_to_line(net, 11, at_from_bus=False)
    add_phaseshift_transformer_to_line(net, 14, tap_max=40, tap_step_degree=10.0)
    return net

pandapower_case30_with_psts_and_weak_branches #

pandapower_case30_with_psts_and_weak_branches()

Create a pandapower IEEE 30 bus grid with phase-shifting transformers and overloaded branches.

RETURNS DESCRIPTION
pandapowerNet

The pandapower IEEE 30 bus network with phase-shifting transformers and weak branches.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/example_grids.py
def pandapower_case30_with_psts_and_weak_branches() -> pp.pandapowerNet:
    """Create a pandapower IEEE 30 bus grid with phase-shifting transformers and overloaded branches.

    Returns
    -------
    pp.pandapowerNet
        The pandapower IEEE 30 bus network with phase-shifting transformers and weak branches.
    """
    net = pandapower_case30_with_psts()
    # Also create a weird trafo to check for bugs
    lv_bus = pp.create_bus(net, vn_kv=20, name="LV Bus", geodata=(5, -5))
    pp.create_transformer_from_parameters(
        net,
        hv_bus=25,
        vn_hv_kv=net.bus.at[25, "vn_kv"],
        lv_bus=lv_bus,
        vn_lv_kv=20,
        sn_mva=100,
        vk_percent=10,
        vkr_percent=0.1,
        pfe_kw=30,
        i0_percent=0.1,
        shift_degree=120,  # This trafo is wired weirdly
    )

    # Weaken the line 34 to make it a bottleneck
    net.line.loc[34, "max_i_ka"] /= 2
    return net

replace_bus_index #

replace_bus_index(net, new_index)

Replaces the bus index in a pandapower network

PARAMETER DESCRIPTION
net

The pandapower network to modify.

TYPE: pandapowerNet

new_index

The new bus index

TYPE: list[Union[int, integer]]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/example_grids.py
def replace_bus_index(net: pp.pandapowerNet, new_index: list[Union[int, np.integer]]) -> None:
    """Replaces the bus index in a pandapower network

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network to modify.
    new_index : list[Union[int, np.integer]]
        The new bus index
    """
    bus_idx_map = {new: old for old, new in zip(net.bus.index, new_index, strict=True)}
    for table, key in pp.element_bus_tuples():
        if table in net and key in net[table] and len(net[table]) > 0:
            net[table][key] = net[table][key].map(bus_idx_map)
    net.bus = net.bus.reindex(new_index).reset_index(drop=True)

pandapower_extended_oberrhein #

pandapower_extended_oberrhein()

Creates a pandapower extended version of the oberrhein grid.

RETURNS DESCRIPTION
pandapowerNet

A pandapower grid with additional elements.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/example_grids.py
def pandapower_extended_oberrhein() -> pp.pandapowerNet:
    """Creates a pandapower extended version of the oberrhein grid.

    Returns
    -------
    pp.pandapowerNet
        A pandapower grid with additional elements.
    """
    net = pp.networks.mv_oberrhein()

    # Add branches to create splittable substations
    std_type = "NA2XS2Y 1x95 RM/25 12/20 kV"
    pp.create_line(net, 4, 290, length_km=1, std_type=std_type, name="imaginary_line_1")
    pp.create_line(net, 3, 290, length_km=1, std_type=std_type, name="imaginary_line_2")
    pp.create_line(net, 273, 304, length_km=1, std_type=std_type, name="imaginary_line_3")
    pp.create_line(net, 273, 305, length_km=1, std_type=std_type, name="imaginary_line_4")
    pp.create_line(
        net,
        from_bus=116,
        to_bus=237,
        length_km=1,
        std_type=std_type,
        name="imaginary_line_5",
    )
    # net.line.geo.loc[line_id, "coords"] = [
    #     (net.bus.geo.loc[116, "x"], net.bus.geo.loc[116, "y"]),
    #     (net.bus.geo.loc[237, "x"], net.bus.geo.loc[237, "y"]),
    # ]

    pp.create_line(
        net,
        from_bus=116,
        to_bus=298,
        length_km=10,
        std_type=std_type,
        name="n_2_safe_line",  # This is a line that should be safe in N-2
    )
    # net.line.geo.loc[line_id, "coords"] = [
    #     (net.bus.geo.loc[116, "x"], net.bus.geo.loc[116, "y"]),
    #     (net.bus.geo.loc[298, "x"], net.bus.geo.loc[298, "y"]),
    # ]
    # Create a "splittable" substation but with a lot of stub lines going into it
    # This should be recognized as a relevant substation first but in the action selection
    # The actions should be filtered out
    stub_bus_1 = pp.create_bus(net, vn_kv=20, name="stub_bus_1")
    stub_bus_2 = pp.create_bus(net, vn_kv=20, name="stub_bus_2")
    stub_bus_3 = pp.create_bus(net, vn_kv=20, name="stub_bus_3")

    pp.create_line(net, 42, stub_bus_1, length_km=1, std_type=std_type, name="stub_line_1")
    pp.create_line(net, 42, stub_bus_2, length_km=1, std_type=std_type, name="stub_line_2")
    pp.create_line(net, 42, stub_bus_3, length_km=1, std_type=std_type, name="stub_line_3")

    # create a 10kv "grid" to add two 3w trafos
    lowv_bus = pp.create_bus(net, vn_kv=10, name="lowv_bus_1")
    midv_bus = pp.create_bus(net, vn_kv=20, name="midv_bus_1")
    pp.create_transformer3w(
        net,
        hv_bus=net.trafo.hv_bus.values[0],
        mv_bus=midv_bus,
        lv_bus=lowv_bus,
        name="3w_trafo_1",
        std_type="63/25/38 MVA 110/20/10 kV",
    )
    pp.create_transformer3w(
        net,
        hv_bus=net.trafo.hv_bus.values[1],
        mv_bus=midv_bus,
        lv_bus=lowv_bus,
        name="3w_trafo_2",
        std_type="63/25/38 MVA 110/20/10 kV",
    )

    # Add a PST
    pp.create_transformer_from_parameters(
        net,
        hv_bus=5,
        lv_bus=100,
        sn_mva=25,
        vn_hv_kv=20,
        vn_lv_kv=20,
        vkr_percent=0.1,
        pfe_kw=1,
        vk_percent=10,
        i0_percent=0.1,
        tap_step_degree=1,
        tap_side="lv",
        tap_pos=5,
        tap_neutral=0,
        tap_max=30,
        tap_min=-30,
        tap_changer_type=True,
    )

    # Add out of service injecions
    pp.create_gen(
        net,
        net.bus.index[1],
        p_mw=1.0,
        q_mvar=1.0,
        in_service=False,
        name="outofservice_gen",
    )
    pp.create_sgen(
        net,
        net.bus.index[2],
        p_mw=1.0,
        q_mvar=1.0,
        in_service=False,
        name="outofservice_sgen",
    )
    pp.create_load(
        net,
        net.bus.index[3],
        p_mw=1.0,
        q_mvar=1.0,
        in_service=False,
        name="outofservice_load",
    )

    # Add shunts, dclines, wards, xwards elements to test their influence
    pp.create_shunts(
        net,
        net.bus.index[1:4],
        q_mvar=[1.0, 2.0, 3.0],
        p_mw=[2.0, 1.0, 4.0],
        in_service=[True, True, False],
        name=["test_shunt_1", "test_shunt_2", "outofservice_shunt"],
        id_characteristic_table=[0, 1, 2],  # due to a pandapower bug, this is not optional
        # references the index of the characteristic from the lookup table net.shunt_characteristic_table
    )
    pp.create_dcline(
        net,
        from_bus=116,
        to_bus=237,
        p_mw=10.0,
        loss_percent=1.0,
        loss_mw=1.0,
        vm_from_pu=1.0,
        vm_to_pu=1.0,
        name="test_dcline",
    )
    pp.create_dcline(
        net,
        from_bus=116,
        to_bus=237,
        p_mw=10.0,
        loss_percent=1.0,
        loss_mw=1.0,
        vm_from_pu=1.0,
        vm_to_pu=1.0,
        in_service=False,
        name="outofservice_test_dcline",
    )
    pp.create_ward(net, net.bus.index[4], 1.0, 2.0, 3.0, 4.0, name="test_ward")
    pp.create_ward(
        net,
        net.bus.index[4],
        1.0,
        2.0,
        3.0,
        4.0,
        in_service=False,
        name="outofservice_test_ward",
    )

    pp.create_xward(
        net,
        bus=net.bus.index[3],
        ps_mw=1.0,
        qs_mvar=2.0,
        pz_mw=3.0,
        qz_mvar=4.0,
        r_ohm=1.0,
        x_ohm=1.0,
        vm_pu=1,
        name="test_xward",
    )

    # Set scaling of sgens so they are recognized
    net.sgen.scaling = 1.0

    np.random.seed(0)
    pp.rundcpp(net)

    # Convert one of the slack busses to a generator
    pp.create_gen(
        net,
        bus=net.ext_grid.bus[1],
        name=net.ext_grid.name[1],
        p_mw=net.res_ext_grid.p_mw[1],
        q_mvar=net.res_ext_grid.q_mvar[1],
        vm_pu=1,
    )
    net.ext_grid.drop(1, inplace=True)
    net.switch.closed = True

    # Reindex
    pp.toolbox.create_continuous_bus_index(net)
    pp.toolbox.create_continuous_elements_index(net)

    # Remove the scaling factor from the loads
    net.load.p_mw *= net.load.scaling
    net.load.q_mvar *= net.load.scaling
    net.load.scaling = 1
    return net

pandapower_non_converging_case57 #

pandapower_non_converging_case57()

Creates a ac-non-converging pandapower case57 grid.

Still converges in DC.

RETURNS DESCRIPTION
pandapowerNet

A pandapower grid that does not converge in AC load flow.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/example_grids.py
def pandapower_non_converging_case57() -> pp.pandapowerNet:
    """Creates a ac-non-converging pandapower case57 grid.

    Still converges in DC.

    Returns
    -------
    pp.pandapowerNet
        A pandapower grid that does not converge in AC load flow.
    """

    net = pp.networks.case57()
    # Change the 115kv to a 50kV bus to prevent convergence in AC but keep it converging in DC
    net.bus.loc[net.bus.vn_kv == 115, "vn_kv"] = 50
    net.trafo.loc[net.trafo.vn_lv_kv == 115, "vn_lv_kv"] = 50
    return net

pandapower_extended_case57 #

pandapower_extended_case57()

Creates a pandapower case57 grid with additional elements.

RETURNS DESCRIPTION
pandapowerNet

A pandapower grid with additional elements

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/example_grids.py
def pandapower_extended_case57() -> pp.pandapowerNet:
    """Creates a pandapower case57 grid with additional elements.

    Returns
    -------
    pp.pandapowerNet
        A pandapower grid with additional elements"""
    net = pp.networks.case57()
    net.line["name"] = net.line.index.astype(str)
    net.trafo["name"] = net.trafo.index.astype(str)
    net.bus["name"] = net.bus.index.astype(str)
    pst_bus = pp.create_bus(net, vn_kv=115, name="PSTBus")

    pp.create_line_from_parameters(
        net,
        from_bus=pst_bus,
        to_bus=16,
        length_km=1,
        r_ohm_per_km=0.1 * (np.square(115) / net.sn_mva),
        x_ohm_per_km=0.1 * (np.square(115) / net.sn_mva),
        c_nf_per_km=0,
        max_i_ka=9999,
        name="PSTLine",
    )
    pp.create_transformer_from_parameters(
        net,
        hv_bus=5,  # hv and lv will be switched later
        lv_bus=pst_bus,
        sn_mva=1,
        vn_hv_kv=115,
        vn_lv_kv=115,
        vk_percent=0.1,
        vkr_percent=0,
        pfe_kw=0,
        i0_percent=0,
        shift_degree=-8,
        name="PST",
    )

    # The bus needs to appear in the sixth place to match powsybl
    new_idx = net.bus.index[:6].tolist() + [pst_bus] + net.bus.index[6:-1].tolist()
    replace_bus_index(net, new_idx)

    # We have to change from- and to buses on all trafos (except for the one that we added manually)
    # So we can just change all of them, the order on the create_transformer call was switched
    trafo = net.trafo
    trafo_copy = deepcopy(net.trafo)
    trafo_copy["lv_bus"] = trafo["hv_bus"]
    trafo_copy["hv_bus"] = trafo["lv_bus"]
    trafo_copy["vn_hv_kv"] = trafo["vn_lv_kv"]
    trafo_copy["vn_lv_kv"] = trafo["vn_hv_kv"]
    trafo_copy["tap_side"] = "lv"
    trafo_copy.sort_values(["hv_bus", "lv_bus"], inplace=True)
    trafo_copy.reset_index(drop=True, inplace=True)
    net.trafo = trafo_copy

    # We have to re-order the busbars again, this time I didn't bother to reverse-engineer the
    # powsybl order, so I just sort them through Kuhn-Munkres
    # Code to reproduce this order:
    # a = pp_backend.net.res_bus.va_degree.values
    # b = powsybl_backend.net.get_buses()["v_angle"].values
    # cost_matrix = (a[None, :] - b[:, None]) ** 2
    # _, indices = scipy.optimize.linear_sum_assignment(cost_matrix)

    indices = [
        0,
        1,
        2,
        3,
        18,
        4,
        5,
        6,
        7,
        29,
        8,
        9,
        55,
        10,
        51,
        11,
        41,
        43,
        12,
        13,
        49,
        14,
        46,
        15,
        45,
        16,
        17,
        19,
        20,
        21,
        22,
        23,
        24,
        25,
        26,
        27,
        28,
        30,
        31,
        32,
        34,
        33,
        35,
        36,
        37,
        38,
        39,
        57,
        40,
        56,
        42,
        44,
        47,
        48,
        50,
        52,
        53,
        54,
    ]
    replace_bus_index(net, indices)
    return net

example_multivoltage_cross_coupler #

example_multivoltage_cross_coupler()

Expands the single busbar coupler to a double busbar with a busbar coupler and a cross coupler See also: https://github.com/e2nIEE/pandapower/blob/develop/tutorials/create_advanced.ipynb

DS- Disconnector (type CB) BB- Busbar (type b) BC- Busbar coupler (two nodes and three switches) CC- Cross coupler (two nodes and three switches) PS- Power switch / Branch switch Branch 1 Branch 2 | | / DS / DS | | | | / CB / CB | | ____| ____| | | | | | / DS | / DS | | CC ⅓ | | BB 1----------------|----------- _______/______ --------|-------------- BB 3 | | | | DS / DS / DS / DS / | | CC 2/4 | | BB 2----|-----------/--------- _______/________ -------------------|--------BB 4 | | | | DS / | | / DS | | | | |/| |/| BC ½ BC ¾

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/example_grids.py
def example_multivoltage_cross_coupler() -> pp.pandapowerNet:
    """
    Expands the single busbar coupler to a double busbar with a busbar coupler and a cross coupler
    See also: https://github.com/e2nIEE/pandapower/blob/develop/tutorials/create_advanced.ipynb

    DS- Disconnector (type CB)
    BB- Busbar (type b)
    BC- Busbar coupler (two nodes and three switches)
    CC- Cross coupler (two nodes and three switches)
    PS- Power switch / Branch switch
                            Branch 1                         Branch 2
                                |                                    |
                                / DS                                 / DS
                                |                                    |
                                |                                    |
                                / CB                                 / CB
                                |                                    |
                            ____|                                ____|
                            |   |                               |   |
                            |   / DS                            |   / DS
                            |   |              CC 1/3           |   |
        BB 1----------------|----------- _______/______ --------|-------------- BB 3
                |           |                                   |          |
            DS /       DS /                                 DS /       DS /
                |           |             CC 2/4                |          |
        BB 2----|-----------/--------- _______/________ -------------------|--------BB 4
            |   |                                                          |   |
        DS /   |                                                          |   / DS
            |   |                                                          |   |
            |_/_|                                                          |_/_|
            BC 1/2                                                         BC 3/4

    """
    net = pp.networks.example_multivoltage()

    # convert the Single Busbar Coupler to Double Busbar Coupler
    net.bus.loc[16, "name"] = "Double Busbar Coupler 1"
    # create the Double Busbar Coupler
    pp.create_bus(net, name="Double Busbar Coupler 2", vn_kv=110, type="b")  # id = 57
    pp.create_bus(net, name="Double Busbar Coupler 3", vn_kv=110, type="b")  # id = 58
    pp.create_bus(net, name="Double Busbar Coupler 4", vn_kv=110, type="b")  # id = 59

    # create the Cross Coupler for bus 1 and 3
    # two buses and three switches
    pp.create_bus(net, name="Cross Coupler 1/3", vn_kv=110, type="n")  # id = 60
    pp.create_bus(net, name="Cross Coupler 3/1", vn_kv=110, type="n")  # id = 61
    pp.create_switch(net, 16, 60, et="b", closed=True, type="DS", name="Cross Coupler 1/3 DS")  # id = 88
    pp.create_switch(net, 60, 61, et="b", closed=True, type="CB", name="Cross Coupler 1-3 CB")  # id = 89
    pp.create_switch(net, 61, 58, et="b", closed=True, type="DS", name="Cross Coupler 1-3 DS")  # id = 90

    # create the Cross Coupler for bus 2 and 4
    # two buses and three switches
    pp.create_bus(net, name="Cross Coupler 2/4", vn_kv=110, type="n")  # id = 62
    pp.create_bus(net, name="Cross Coupler 4/2", vn_kv=110, type="n")  # id = 63
    pp.create_switch(net, 57, 62, et="b", closed=True, type="DS", name="Cross Coupler 2/4 DS")  # id = 91
    pp.create_switch(net, 62, 63, et="b", closed=True, type="CB", name="Cross Coupler 2-4 CB")  # id = 92
    pp.create_switch(net, 63, 59, et="b", closed=True, type="DS", name="Cross Coupler 2-4 DS")  # id = 93

    # create the Busbar Coupler for bus 1 and 2
    # two buses and three switches
    pp.create_bus(net, name="Busbar Coupler 1/2", vn_kv=110, type="n")  # id = 64
    pp.create_bus(net, name="Busbar Coupler 2/1", vn_kv=110, type="n")  # id = 65
    pp.create_switch(net, 16, 64, et="b", closed=True, type="DS", name="Busbar Coupler 1/2 DS")  # id = 94
    pp.create_switch(net, 64, 65, et="b", closed=True, type="CB", name="Busbar Coupler 1-2 CB")  # id = 95
    pp.create_switch(net, 65, 57, et="b", closed=True, type="DS", name="Busbar Coupler 1-2 DS")  # id = 96

    # create the Busbar Coupler for bus 3 and 4
    # two buses and three switches
    pp.create_bus(net, name="Busbar Coupler 3/4", vn_kv=110, type="n")  # id = 66
    pp.create_bus(net, name="Busbar Coupler 4/3", vn_kv=110, type="n")  # id = 67
    pp.create_switch(net, 58, 66, et="b", closed=True, type="DS", name="Busbar Coupler 3/4 DS")  # id = 97
    pp.create_switch(net, 66, 67, et="b", closed=True, type="CB", name="Busbar Coupler 3-4 CB")  # id = 98
    pp.create_switch(net, 67, 59, et="b", closed=True, type="DS", name="Busbar Coupler 3-4 DS")  # id = 99

    # there are 5 assets linked to the original busbar
    # create new switches for the parallel busbars
    # move some of the assets to the new busbars
    # current assets linked to the busbar
    # {'sgen': [0], 'trafo': [0], 'load': [0], 'line': [0, 5]}
    # new assignment of the assets
    # BB1 -> line 0 + load 0
    # BB2 -> trafo 0
    # BB3 -> line 5
    # BB4 -> sgen 0

    # create new switches for the first two busbars
    pp.create_switch(net, 23, 57, et="b", closed=True, type="DS", name="Bus SB T1.2.2")  # id = 100 -> trafo 0
    pp.create_switch(net, 25, 57, et="b", closed=False, type="DS", name="Bus SB T2.2.2")  # id = 101 -> line 0
    pp.create_switch(net, 29, 57, et="b", closed=False, type="DS", name="Bus SB T4.2.2")  # id = 102 -> load 0
    # create new switches for the last two busbars
    pp.create_switch(net, 27, 59, et="b", closed=False, type="DS", name="Bus SB T3.2.2")  # id = 103 -> line 5
    pp.create_switch(net, 31, 59, et="b", closed=True, type="DS", name="Bus SB T5.2.2")  # id = 104 -> sgen 0

    # move bus from existing busbar to new busbar
    net.switch.loc[24, "bus"] = 58
    net.switch.loc[28, "bus"] = 58
    net.switch.loc[28, "closed"] = False  # BB4 -> sgen 0 therefore switch open to bus 58

    return net

toop_engine_grid_helpers.pandapower.loadflow_parameters #

A Collection of Pandapower parameters used in the Loadflow Solver.

PANDAPOWER_LOADFLOW_PARAM_RUNPP module-attribute #

PANDAPOWER_LOADFLOW_PARAM_RUNPP = {
    "distributed_slack": False,
    "voltage_depend_loads": False,
    "trafo_model": "t",
    "numba": True,
    "max_iteration": 20,
}

PANDAPOWER_LOADFLOW_PARAM_PPCI module-attribute #

PANDAPOWER_LOADFLOW_PARAM_PPCI = {
    "calculate_voltage_angles": True,
    "trafo_model": "t",
    "check_connectivity": True,
    "mode": "pf",
    "switch_rx_ratio": 2,
    "enforce_q_lims": False,
    "voltage_depend_loads": False,
    "consider_line_temperature": False,
    "tdpf": False,
    "tdpf_update_r_theta": True,
    "tdpf_delay_s": None,
    "distributed_slack": False,
    "delta": 0,
    "trafo3w_losses": "hv",
    "init_results": True,
    "p_lim_default": 1000000000.0,
    "q_lim_default": 1000000000.0,
    "neglect_open_switch_branches": False,
    "tolerance_mva": 1e-08,
    "trafo_loading": "current",
    "numba": True,
    "algorithm": "nr",
    "max_iteration": 25,
    "v_debug": False,
    "only_v_results": False,
    "use_umfpack": True,
    "permc_spec": None,
    "lightsim2grid": False,
    "recycle": False,
}

PANDAPOWER_LOADFLOW_PARAM_PPCI_AC module-attribute #

PANDAPOWER_LOADFLOW_PARAM_PPCI_AC = {
    None: PANDAPOWER_LOADFLOW_PARAM_PPCI,
    "ac": True,
    "init_vm_pu": "dc",
    "init_va_degree": "dc",
}

PANDAPOWER_LOADFLOW_PARAM_PPCI_DC module-attribute #

PANDAPOWER_LOADFLOW_PARAM_PPCI_DC = {
    None: PANDAPOWER_LOADFLOW_PARAM_PPCI,
    "ac": False,
    "init_vm_pu": "none",
    "init_va_degree": "none",
}

toop_engine_grid_helpers.pandapower.pandapower_helpers #

Holds functions that help with all sorts of pandapower conversions, that are not specific to a single task.

power_key_lookup_table module-attribute #

power_key_lookup_table = {
    "def_active": {
        "gen": "p_mw",
        "sgen": "p_mw",
        "load": "p_mw",
        "shunt": "p_mw",
        "dcline_from": "p_mw",
        "dcline_to": "dcline",
        "ward_load": "ps_mw",
        "ward_shunt": "pz_mw",
        "xward_load": "ps_mw",
        "xward_shunt": "pz_mw",
    },
    "def_reactive": {
        "sgen": "q_mvar",
        "load": "q_mvar",
        "shunt": "q_mvar",
        "ward_load": "qs_mvar",
        "ward_shunt": "qz_mvar",
        "xward_load": "qs_mvar",
        "xward_shunt": "qz_mvar",
    },
    "res_active": {
        "gen": "p_mw",
        "sgen": "p_mw",
        "load": "p_mw",
        "shunt": "p_mw",
        "dcline_from": "p_from_mw",
        "dcline_to": "p_to_mw",
        "ward_load": "p_mw",
        "xward_load": "p_mw",
    },
    "res_reactive": {
        "gen": "q_mvar",
        "sgen": "q_mvar",
        "load": "q_mvar",
        "shunt": "q_mvar",
        "dcline_from": "q_from_mvar",
        "dcline_to": "q_to_mvar",
        "ward_load": "q_mvar",
        "xward_load": "q_mvar",
    },
}

type_mapper module-attribute #

type_mapper = {
    "line": "res_line",
    "trafo": "res_trafo",
    "trafo3w_lv": "res_trafo3w",
    "trafo3w_mv": "res_trafo3w",
    "trafo3w_hv": "res_trafo3w",
    "impedance": "res_impedance",
    "xward": "res_xward",
}

sign_mapper module-attribute #

sign_mapper = {
    "line": -1,
    "trafo": -1,
    "trafo3w_lv": 1,
    "trafo3w_mv": 1,
    "trafo3w_hv": -1,
    "impedance": -1,
    "xward": -1,
}

column_mapper module-attribute #

column_mapper = {
    "active_from": {
        "line": "p_from_mw",
        "trafo": "p_hv_mw",
        "trafo3w_lv": "p_lv_mw",
        "trafo3w_mv": "p_mv_mw",
        "trafo3w_hv": "p_hv_mw",
        "impedance": "p_from_mw",
        "xward": "p_mw",
    },
    "reactive_from": {
        "line": "q_from_mvar",
        "trafo": "q_hv_mvar",
        "trafo3w_lv": "q_lv_mvar",
        "trafo3w_mv": "q_mv_mvar",
        "trafo3w_hv": "q_hv_mvar",
        "impedance": "q_from_mvar",
        "xward": "q_mvar",
    },
    "current_from": {
        "line": "i_from_ka",
        "trafo": "i_hv_ka",
        "trafo3w_lv": "i_lv_ka",
        "trafo3w_mv": "i_mv_ka",
        "trafo3w_hv": "i_hv_ka",
        "impedance": "i_from_ka",
        "xward": "no_column",
    },
    "active_to": {
        "line": "p_to_mw",
        "trafo": "p_lv_mw",
        "trafo3w_lv": "p_lv_mw",
        "trafo3w_mv": "p_mv_mw",
        "trafo3w_hv": "p_hv_mw",
        "impedance": "p_to_mw",
        "xward": "p_mw",
    },
    "reactive_to": {
        "line": "q_to_mvar",
        "trafo": "q_lv_mvar",
        "trafo3w_lv": "q_lv_mvar",
        "trafo3w_mv": "q_mv_mvar",
        "trafo3w_hv": "q_hv_mvar",
        "impedance": "q_to_mvar",
        "xward": "q_mvar",
    },
    "current_to": {
        "line": "i_to_ka",
        "trafo": "i_lv_ka",
        "trafo3w_lv": "i_lv_ka",
        "trafo3w_mv": "i_mv_ka",
        "trafo3w_hv": "i_hv_ka",
        "impedance": "i_to_ka",
        "xward": "no_column",
    },
}

isnan_column_mapper module-attribute #

isnan_column_mapper = {
    "line": "i_from_ka",
    "trafo": "i_hv_ka",
    "trafo3w_lv": "i_lv_ka",
    "trafo3w_mv": "i_mv_ka",
    "trafo3w_hv": "i_hv_ka",
    "impedance": "i_from_ka",
}

get_phaseshift_key #

get_phaseshift_key(trafo_type)

Get the key of the shift_degree in the trafo

PARAMETER DESCRIPTION
trafo_type

The type of trafo

TYPE: str

RETURNS DESCRIPTION
str

The key of the phaseshift attribute in the trafo(3w)dataframe

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_phaseshift_key(trafo_type: str) -> str:
    """Get the key of the shift_degree in the trafo

    Parameters
    ----------
    trafo_type : str
        The type of trafo

    Returns
    -------
    str
        The key of the phaseshift attribute in the trafo(3w)dataframe
    """
    lookup_table = {
        "trafo": "shift_degree",
        "trafo3w_lv": "shift_lv_degree",
        "trafo3w_mv": "shift_mv_degree",
    }
    assert trafo_type in lookup_table
    return lookup_table[trafo_type]

get_phaseshift_mask #

get_phaseshift_mask(net)

Return the mask over phaseshifters in the ppci and the minimum and maximum setpoints for these PSTs

PARAMETER DESCRIPTION
net

The pandapower network to extract the data from

TYPE: pandapowerNet

RETURNS DESCRIPTION
pst_mask

The mask over phaseshifters in the ppci, true if it is a trafo with a phase shift in the ppci

TYPE: Bool[ndarray, ' n_branches']

controllable_mask

The mask over controllable phase shifters in the ppci. True if a trafo has a tap that can change the phase shift Note that this won't catch trafos with a fixed shift_degree, only those which have a tap changer that can change the angle shift.

TYPE: Bool[ndarray, ' n_branches']

shift_taps

A list of arrays with the tap positions for each controllable phase shifter. The outer list has length sum(controllable_mask), the inner arrays have length equal to the number of taps of each PST (varying)

TYPE: list[Float[ndarray, ' n_tap_positions']]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_phaseshift_mask(
    net: pandapower.pandapowerNet,
) -> tuple[Bool[np.ndarray, " n_branches"], Bool[np.ndarray, " n_branches"], list[Float[np.ndarray, " n_tap_positions"]]]:
    """Return the mask over phaseshifters in the ppci and the minimum and maximum setpoints for these PSTs

    Parameters
    ----------
    net : pandapower.pandapowerNet
        The pandapower network to extract the data from

    Returns
    -------
    pst_mask : Bool[np.ndarray, " n_branches"]
        The mask over phaseshifters in the ppci, true if it is a trafo with a phase shift in the ppci
    controllable_mask : Bool[np.ndarray, " n_branches"]
        The mask over controllable phase shifters in the ppci. True if a trafo has a tap that can
        change the phase shift
        Note that this won't catch trafos with a fixed shift_degree, only those which have a tap
        changer that can change the angle shift.
    shift_taps : list[Float[np.ndarray, " n_tap_positions"]]
        A list of arrays with the tap positions for each controllable phase shifter. The outer list has length
        sum(controllable_mask), the inner arrays have length equal to the number of taps of each PST (varying)
    """
    # Everything that has a shift angle in the PPCI must be regarded as a phase shifter
    ppci = pp.converter.to_ppc(net, init="flat", calculate_voltage_angles=True)
    has_shift_angle = np.array(ppci["branch"][:, SHIFT] != 0)

    # Additionally there are trafos that have a tap_changer_type, but might be set to neutral
    # Hence, these need to be regarded separately. Trafos with a tap will also get a max and min
    # setpoint
    if not len(net.trafo):
        return has_shift_angle, np.zeros_like(has_shift_angle), []

    controllable: Bool[np.ndarray, " n_trafos"] = (
        (net.trafo.vn_hv_kv == net.trafo.vn_lv_kv)
        & (net.trafo.tap_changer_type is not None)
        & (net.trafo.tap_step_degree != 0)
        & ~net.trafo.tap_min.isna()
        & ~net.trafo.tap_max.isna()
        & ~net.trafo.tap_step_degree.isna()
    ).values
    tap_min = np.array(net.trafo.tap_min)[controllable]
    tap_max = np.array(net.trafo.tap_max)[controllable]
    tap_step = np.array(net.trafo.tap_step_degree)[controllable]

    shift_taps = [
        np.arange(t_min, t_max + 1) * t_step for (t_min, t_max, t_step) in zip(tap_min, tap_max, tap_step, strict=True)
    ]

    ppci_start, ppci_end = net._pd2ppc_lookups["branch"]["trafo"]
    pst_indices = np.arange(ppci_start, ppci_end)[controllable]

    controllable_global: Bool[np.ndarray, " n_branches"] = np.zeros_like(has_shift_angle)
    controllable_global[pst_indices] = True
    has_shift_angle[pst_indices] = True

    return has_shift_angle, controllable_global, shift_taps

get_bus_key #

get_bus_key(branch_name, branch_from_end)

Get the key of the bus in the branch name

PARAMETER DESCRIPTION
branch_name

The name of the branch

TYPE: str

branch_from_end

Whether to get the from or to bus, True for from, False for to

TYPE: bool

RETURNS DESCRIPTION
str

The key of the bus in the branch name

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_bus_key(branch_name: str, branch_from_end: bool) -> str:
    """Get the key of the bus in the branch name

    Parameters
    ----------
    branch_name : str
        The name of the branch
    branch_from_end : bool
        Whether to get the from or to bus, True for from, False for to

    Returns
    -------
    str
        The key of the bus in the branch name
    """
    lookup_table = {
        "line": {True: "from_bus", False: "to_bus"},
        "trafo": {True: "hv_bus", False: "lv_bus"},
        "trafo3w_lv": {True: "lv_bus", False: "lv_bus"},
        "trafo3w_mv": {True: "mv_bus", False: "mv_bus"},
        "trafo3w_hv": {True: "hv_bus", False: "hv_bus"},
    }

    assert branch_name in lookup_table
    assert branch_from_end in lookup_table[branch_name]

    return lookup_table[branch_name][branch_from_end]

get_bus_key_injection #

get_bus_key_injection(injection_type)

Get the key of the bus in the injection type

PARAMETER DESCRIPTION
injection_type

The type of the injection

TYPE: str

RETURNS DESCRIPTION
str

The key of the bus in the injection type

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_bus_key_injection(injection_type: str) -> str:
    """Get the key of the bus in the injection type

    Parameters
    ----------
    injection_type : str
        The type of the injection

    Returns
    -------
    str
        The key of the bus in the injection type
    """
    lookup_table = {
        "gen": "bus",
        "sgen": "bus",
        "load": "bus",
        "shunt": "bus",
        "dcline_from": "from_bus",
        "dcline_to": "to_bus",
        # "ward_load": "bus",
        # "ward_shunt": "bus",
        # "xward_load": "bus",
        # "xward_gen": "bus",
        # "xward_shunt": "bus",
    }
    assert injection_type in lookup_table
    return lookup_table[injection_type]

get_element_table #

get_element_table(element_type, res_table=False)

Get the element table for the injection or branch type or bus

PARAMETER DESCRIPTION
element_type

The type description of the branch/injection/bus

TYPE: str

res_table

Whether to get the res table. Defaults to False

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
str

The element table for the branch/injection/bus type

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_element_table(element_type: str, res_table: bool = False) -> str:
    """Get the element table for the injection or branch type or bus

    Parameters
    ----------
    element_type : str
        The type description of the branch/injection/bus
    res_table : bool
        Whether to get the res table. Defaults to False

    Returns
    -------
    str
        The element table for the branch/injection/bus type
    """
    lookup_table = {
        "bus": "bus",
        "switch": "switch",
        # branches
        "line": "line",
        "impedance": "impedance",
        "trafo": "trafo",
        "trafo3w": "trafo3w",
        "trafo3w_lv": "trafo3w",
        "trafo3w_mv": "trafo3w",
        "trafo3w_hv": "trafo3w",
        # injections
        "gen": "gen",
        "sgen": "sgen",
        "load": "load",
        "shunt": "shunt",
        "dcline_from": "dcline",
        "dcline_to": "dcline",
        "ward_load": "ward",
        "ward_shunt": "ward",
        "xward_load": "xward",
        "xward_gen": "xward",
        "xward_shunt": "xward",
    }
    assert element_type in lookup_table

    key = lookup_table[element_type]

    if res_table:
        key = "res_" + key

    return key

get_power_key #

get_power_key(
    element_type, res_table=False, reactive=False
)

Get the power key for the injection type in the res/non-res tables

PARAMETER DESCRIPTION
element_type

The type description of the injection

TYPE: str

res_table

Whether to get the key for the res table. Defaults to False

TYPE: bool DEFAULT: False

reactive

Whether to get the reactive power instead of the active power. Defaults to False

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
str

The power key for the injection type in pandapowers

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_power_key(element_type: str, res_table: bool = False, reactive: bool = False) -> str:
    """
    Get the power key for the injection type in the res/non-res tables

    Parameters
    ----------
    element_type : str
        The type description of the injection
    res_table : bool
        Whether to get the key for the res table. Defaults to False
    reactive : bool
        Whether to get the reactive power instead of the active power. Defaults to False

    Returns
    -------
    str
        The power key for the injection type in pandapowers
    """
    table_key = ("res" if res_table else "def") + ("_reactive" if reactive else "_active")

    return power_key_lookup_table[table_key][element_type]

get_pandapower_loadflow_results_in_ppc #

get_pandapower_loadflow_results_in_ppc(net)

Get the loadflow result for all pandapower branches that are included in ppc (= even if not in service)

PARAMETER DESCRIPTION
net

The pandapower network with loadflows already computed

TYPE: pandapowerNet

RETURNS DESCRIPTION
Float[ndarray, ' n_branches_ppc']
An array of all loadflows of branches that are in ppci in the correct order
Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_pandapower_loadflow_results_in_ppc(
    net: pp.pandapowerNet,
) -> Float[np.ndarray, " n_branches_ppc"]:
    """Get the loadflow result for all pandapower branches that are included in ppc (= even if not in service)

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network with loadflows already computed

    Returns
    -------
    Float[np.ndarray, " n_branches_ppc"]
    An array of all loadflows of branches that are in ppci in the correct order
    """
    branch_loads_pandapower = np.concatenate(
        [
            net.res_line.p_from_mw.values,
            net.res_trafo.p_hv_mw.values,
            net.res_trafo3w.p_hv_mw.values,
            net.res_trafo3w.p_mv_mw.values,
            net.res_trafo3w.p_lv_mw.values,
            net.res_impedance.p_from_mw.values,
            np.zeros(
                len(net.xward), dtype=float
            ),  # The p_mw from res_xward is stored on aux bus level not on the aux branch
        ]
    )
    return branch_loads_pandapower

get_pandapower_loadflow_results_injection #

get_pandapower_loadflow_results_injection(
    net, types, ids, reactive=False
)

Use a list of injection types and ids to get the loadflow results

PARAMETER DESCRIPTION
net

The pandapower network with loadflows already computed

TYPE: pandapowerNet

types

The types of the injections, length n_injections

TYPE: Sequence[str]

ids

The ids of the injections, length n_injections

TYPE: Sequence[int]

reactive

Whether to get the reactive power instead of the active power. Defaults to False

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Float[ndarray, ' n_injections']

The loadflow results of the injections in the correct order

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_pandapower_loadflow_results_injection(
    net: pp.pandapowerNet,
    types: Sequence[str],
    ids: Sequence[Integral],
    reactive: bool = False,
) -> Float[np.ndarray, " n_injections"]:
    """Use a list of injection types and ids to get the loadflow results

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network with loadflows already computed
    types : Sequence[str]
        The types of the injections, length n_injections
    ids : Sequence[int]
        The ids of the injections, length n_injections
    reactive : bool
        Whether to get the reactive power instead of the active power. Defaults to False

    Returns
    -------
    Float[np.ndarray, " n_injections"]
        The loadflow results of the injections in the correct order
    """

    def get_from_net(elem_type: str, elem_id: Integral) -> float:
        """Get data for a single injection

        Parameters
        ----------
        elem_type : str
            The type of the injection
        elem_id : int
            The id of the injection

        Returns
        -------
        float
            The power of the injection
        """
        table = get_element_table(elem_type, res_table=False)
        res_table = get_element_table(elem_type, res_table=True)
        try:
            power_key = get_power_key(elem_type, res_table=True, reactive=reactive)
        except KeyError:
            return 0.0

        sign = pandapower.toolbox.signing_system_value(table)
        return net[res_table].loc[elem_id, power_key] * sign

    injection_power = np.array([get_from_net(t, i) for t, i in zip(types, ids, strict=True)])
    return injection_power

get_pandapower_branch_loadflow_results_sequence #

get_pandapower_branch_loadflow_results_sequence(
    net,
    types,
    ids,
    measurement,
    from_end=True,
    fill_na=None,
    adjust_signs=True,
)

Use a list of branch types and ids to get the loadflow results

This can also applied in case the network is slightly different than the net in the backend, e.g. to use a non-preprocessed net

PARAMETER DESCRIPTION
net

The pandapower network with loadflows already computed

TYPE: pandapowerNet

types

The types of the branches, length n_branches

TYPE: Sequence[str]

ids

The ids of the branches, length n_branches

TYPE: Sequence[int]

measurement

The type of the measurement to get. Can be "active", "reactive" or "current"

TYPE: Literal[active, reactive, current]

from_end

Whether to get the results from the end of the branch. Defaults to True

TYPE: bool DEFAULT: True

fill_na

Whether to replace NaN values with a constant

TYPE: Optional[float] DEFAULT: None

adjust_signs

Whether to adjust the signs of the results according to the type of branch. Defaults to True

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
Float[ndarray, ' n_branches']

The loadflow results of the branches in the correct order

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_pandapower_branch_loadflow_results_sequence(
    net: pp.pandapowerNet,
    types: Sequence[str],
    ids: Sequence[Integral],
    measurement: Literal["active", "reactive", "current"],
    from_end: bool = True,
    fill_na: Optional[float] = None,
    adjust_signs: bool = True,
) -> Float[np.ndarray, " n_branches"]:
    """Use a list of branch types and ids to get the loadflow results

    This can also applied in case the network is slightly different than the net in the backend,
    e.g. to use a non-preprocessed net

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network with loadflows already computed
    types : Sequence[str]
        The types of the branches, length n_branches
    ids : Sequence[int]
        The ids of the branches, length n_branches
    measurement : Literal["active", "reactive", "current"]
        The type of the measurement to get. Can be "active", "reactive" or "current"
    from_end : bool
        Whether to get the results from the end of the branch. Defaults to True
    fill_na: Optional[float]
        Whether to replace NaN values with a constant
    adjust_signs: bool
        Whether to adjust the signs of the results according to the type of branch. Defaults to True

    Returns
    -------
    Float[np.ndarray, " n_branches"]
        The loadflow results of the branches in the correct order
    """
    assert len(types) == len(ids)

    mapper_key = measurement + ("_from" if from_end else "_to")

    mapped_types = (type_mapper[t] for t in types)
    mapped_columns = (column_mapper[mapper_key][t] for t in types)
    mapped_signs = (sign_mapper[t] for t in types) if adjust_signs else (1 for _ in types)

    # It might be more performant to group this in the future, i.e. to get all results at once
    branch_loads = np.array(
        [net[t][c].loc[i] * s for t, c, i, s in zip(mapped_types, mapped_columns, ids, mapped_signs, strict=True)]
    )

    # Currents are saved in kA, we want A
    if measurement == "current":
        branch_loads *= 1000

    if fill_na is not None:
        branch_loads = np.nan_to_num(branch_loads, nan=fill_na)

    return branch_loads

get_pandapower_bus_loadflow_results_sequence #

get_pandapower_bus_loadflow_results_sequence(
    net, monitored_bus_ids, voltage_magnitude=False
)

Use a list of bus ids to get the loadflow results

This can also applied in case the network is slightly different than the net in the backend, e.g. to use a non-preprocessed net

PARAMETER DESCRIPTION
net

The pandapower network with loadflows already computed

TYPE: pandapowerNet

monitored_bus_ids

The ids of the buses, length n_buses

TYPE: Sequence[int]

voltage_magnitude

Whether to get the voltage magnitude instead of the angle. Defaults to False

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Float[ndarray, ' n_buses']

The loadflow results of the buses in the correct order

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_pandapower_bus_loadflow_results_sequence(
    net: pp.pandapowerNet,
    monitored_bus_ids: Sequence[Integral],
    voltage_magnitude: bool = False,
) -> Float[np.ndarray, " n_buses"]:
    """Use a list of bus ids to get the loadflow results

    This can also applied in case the network is slightly different than the net in the backend,
    e.g. to use a non-preprocessed net

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network with loadflows already computed
    monitored_bus_ids : Sequence[int]
        The ids of the buses, length n_buses
    voltage_magnitude : bool
        Whether to get the voltage magnitude instead of the angle. Defaults to False

    Returns
    -------
    Float[np.ndarray, " n_buses"]
        The loadflow results of the buses in the correct order
    """
    if voltage_magnitude:
        res = net.res_bus.vm_pu.loc[monitored_bus_ids].values
    else:
        res = net.res_bus.va_degree.loc[monitored_bus_ids].values
    return res

check_for_splits #

check_for_splits(
    net, monitored_branch_types, monitored_branch_ids
)

Check for splits by checking for NaN values

Unfortunately, p_xx values are zero on split in pandapower

PARAMETER DESCRIPTION
net

The pandapower network with loadflows already computed

TYPE: pandapowerNet

monitored_branch_types

The types of the monitored branches, length N_branches

TYPE: Sequence[str]

monitored_branch_ids

The ids of the monitored branches, length N_branches

TYPE: Sequence[int]

RETURNS DESCRIPTION
bool

Whether a split is present, i.e. any monitored branch has a NaN value

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def check_for_splits(
    net: pp.pandapowerNet,
    monitored_branch_types: Sequence[str],
    monitored_branch_ids: Sequence[Integral],
) -> bool:
    """Check for splits by checking for NaN values

    Unfortunately, p_xx values are zero on split in pandapower

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network with loadflows already computed
    monitored_branch_types : Sequence[str]
        The types of the monitored branches, length N_branches
    monitored_branch_ids : Sequence[int]
        The ids of the monitored branches, length N_branches

    Returns
    -------
    bool
        Whether a split is present, i.e. any monitored branch has a NaN value
    """
    assert len(monitored_branch_types) == len(monitored_branch_ids)

    mapped_types = (type_mapper[t] for t in monitored_branch_types)
    mapped_columns = (isnan_column_mapper[t] for t in monitored_branch_types)

    isnan = any(
        np.isnan(net[t][c].loc[i]) for t, c, i in zip(mapped_types, mapped_columns, monitored_branch_ids, strict=True)
    )
    return isnan

get_dc_bus_voltage #

get_dc_bus_voltage(net)

Get the bus voltage of all buses in the network under DC conditions

This is usually bus.vn_kv, except a generator is connected in which case the voltage is the generator voltage

PARAMETER DESCRIPTION
net

The pandapower network

TYPE: pandapowerNet

RETURNS DESCRIPTION
Series

The bus voltage of all buses in the network, indexed by bus id

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_dc_bus_voltage(net: pp.pandapowerNet) -> pd.Series:
    """Get the bus voltage of all buses in the network under DC conditions

    This is usually bus.vn_kv, except a generator is connected in which case the voltage is the
    generator voltage

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network

    Returns
    -------
    pd.Series
        The bus voltage of all buses in the network, indexed by bus id
    """
    buses = pd.merge(
        left=net.bus,
        right=net.gen,
        left_index=True,
        right_on="bus",
        how="left",
        suffixes=("", "_gen"),
    )
    buses.index = net.bus.index
    buses["vn_kv"] = buses["vm_pu"].fillna(1) * buses["vn_kv"]
    return buses["vn_kv"]

get_shunt_real_power #

get_shunt_real_power(
    bus_voltage,
    shunt_power,
    shunt_voltage=None,
    shunt_step=None,
)

Get the real power of all shunts in the network

PARAMETER DESCRIPTION
bus_voltage

The voltage level of the buses the shunts are connected to

TYPE: Float[ndarray, ' n_shunts']

shunt_power

The power of the shunts

TYPE: Float[ndarray, ' n_shunts']

shunt_voltage

The voltage level of the shunts. Defaults to bus_voltage

TYPE: Optional[Float[ndarray, ' n_shunts']] DEFAULT: None

shunt_step

The step of the shunts. Defaults to np.ones(shunt_power.shape[0], dtype=int)

TYPE: Optional[Integer[ndarray, ' n_shunts']] DEFAULT: None

RETURNS DESCRIPTION
Float[ndarray, ' n_shunts']

The real power of all shunts in the network

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_shunt_real_power(
    bus_voltage: Float[np.ndarray, " n_shunts"],
    shunt_power: Float[np.ndarray, " n_shunts"],
    shunt_voltage: Optional[Float[np.ndarray, " n_shunts"]] = None,
    shunt_step: Optional[Integer[np.ndarray, " n_shunts"]] = None,
) -> Float[np.ndarray, " n_shunts"]:
    """Get the real power of all shunts in the network

    Parameters
    ----------
    bus_voltage: Float[np.ndarray, " n_shunts"]
        The voltage level of the buses the shunts are connected to
    shunt_power: Float[np.ndarray, " n_shunts"]
        The power of the shunts
    shunt_voltage: Optional[Float[np.ndarray, " n_shunts"]]
        The voltage level of the shunts. Defaults to bus_voltage
    shunt_step: Optional[Int[np.ndarray, " n_shunts"]]
        The step of the shunts. Defaults to np.ones(shunt_power.shape[0], dtype=int)

    Returns
    -------
    Float[np.ndarray, " n_shunts"]
        The real power of all shunts in the network
    """
    if shunt_voltage is None:
        shunt_voltage = bus_voltage
    if shunt_step is None:
        shunt_step = np.ones(shunt_power.shape[0], dtype=int)
    shunt_vratio = (bus_voltage / shunt_voltage) ** 2
    shunt_p = shunt_power * shunt_step * shunt_vratio
    return shunt_p

get_remotely_connected_buses #

get_remotely_connected_buses(
    net,
    buses,
    consider=("l", "s", "t", "t3", "i"),
    respect_switches=True,
    respect_in_service=False,
    max_num_iterations=100,
)

Get the remotely connected buses, calls pp.toolbox.get_connected_buses until no more buses are found

This is useful for finding grid islands or stations and uses the same function interface as get_connected_buses

PARAMETER DESCRIPTION
net

The pandapower network

TYPE: pandapowerNet

buses

The buses to get the remotely connected buses for

TYPE: Sequence[int]

consider

The types of elements to consider. Defaults to all. Use ("s",) to only consider switches, which will limit the search to a station. According to the pandapower docs: - l: lines - s: switches - t: trafos - t3: trafo3ws - i: impedances

TYPE: Optional[Sequence[str]] DEFAULT: ('l', 's', 't', 't3', 'i')

respect_switches

Whether to respect switches. If true, will not hop over open switches. Defaults to True

TYPE: bool DEFAULT: True

respect_in_service

Whether to respect the in_service flag of the elements. Will not hop over out-of-service elements. Defaults to False

TYPE: bool DEFAULT: False

max_num_iterations

After how many iterations to stop searching for new buses. Defaults to 100

TYPE: int DEFAULT: 100

RETURNS DESCRIPTION
set[int]

The remotely connected buses, indexed by bus id. In contrast to the pandapower function, this will include the set of buses passed in as a parameter except for those that are not in the network.

RAISES DESCRIPTION
RuntimeError

If the maximum number of iterations is reached without finding all connected buses

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def get_remotely_connected_buses(
    net: pp.pandapowerNet,
    buses: Iterable[Integral],
    consider: tuple[str, ...] = ("l", "s", "t", "t3", "i"),
    respect_switches: bool = True,
    respect_in_service: bool = False,
    max_num_iterations: int = 100,
) -> set[int]:
    """Get the remotely connected buses, calls pp.toolbox.get_connected_buses until no more buses are found

    This is useful for finding grid islands or stations and uses the same function interface as get_connected_buses

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network
    buses : Sequence[int]
        The buses to get the remotely connected buses for
    consider : Optional[Sequence[str]]
        The types of elements to consider. Defaults to all. Use ("s",) to only consider switches, which will limit
        the search to a station. According to the pandapower docs:
        - l: lines
        - s: switches
        - t: trafos
        - t3: trafo3ws
        - i: impedances
    respect_switches : bool
        Whether to respect switches. If true, will not hop over open switches. Defaults to True
    respect_in_service : bool
        Whether to respect the in_service flag of the elements. Will not hop over out-of-service elements. Defaults to False
    max_num_iterations : int
        After how many iterations to stop searching for new buses. Defaults to 100

    Returns
    -------
    set[int]
        The remotely connected buses, indexed by bus id. In contrast to the pandapower function, this will include the set
        of buses passed in as a parameter except for those that are not in the network.

    Raises
    ------
    RuntimeError
        If the maximum number of iterations is reached without finding all connected buses
    """
    working_set = set(buses).intersection(net.bus.index)
    for i in range(max_num_iterations):
        new_set: set[Integral] = get_connected_buses(
            net=net,
            buses=working_set,
            consider=consider,
            respect_switches=respect_switches,
            respect_in_service=respect_in_service,
        )
        if new_set in working_set or len(new_set) == 0:
            break
        working_set.update(new_set)

        if i >= max_num_iterations - 1:
            raise RuntimeError(
                f"Max number of iterations ({max_num_iterations}) reached while searching for remotely connected buses."
            )
    return {int(node_id) for node_id in working_set}

load_pandapower_from_fs #

load_pandapower_from_fs(filesystem, file_path)

Load any pandapower network from a filesystem.

Supported formats are pandapower native (.json), Matpower (.mat), CGMES (.zip) and UCTE (.uct).

PARAMETER DESCRIPTION
filesystem

The filesystem to load the pandapower network from.

TYPE: AbstractFileSystem

file_path

The path to the pandapower network file.

TYPE: Path

RETURNS DESCRIPTION
pandapowerNet

The loaded pandapower network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def load_pandapower_from_fs(filesystem: AbstractFileSystem, file_path: Path) -> pandapower.pandapowerNet:
    """Load any pandapower network from a filesystem.

    Supported formats are pandapower native (.json), Matpower (.mat), CGMES (.zip) and UCTE (.uct).

    Parameters
    ----------
    filesystem : AbstractFileSystem
        The filesystem to load the pandapower network from.
    file_path : Path
        The path to the pandapower network file.

    Returns
    -------
    pandapower.pandapowerNet
        The loaded pandapower network.
    """
    with tempfile.TemporaryDirectory() as temp_dir:
        tmp_grid_path = Path(temp_dir) / file_path.name
        tmp_grid_path_str = str(tmp_grid_path)
        filesystem.download(
            str(file_path),
            tmp_grid_path_str,
        )

        if file_path.suffix == ".json":
            net = pandapower.from_json(tmp_grid_path_str)
        elif file_path.suffix == ".mat":
            net = from_mpc(tmp_grid_path_str)
        elif file_path.suffix == ".zip":
            net = from_cim(tmp_grid_path_str)
        elif file_path.suffix == ".uct":
            net = from_ucte(tmp_grid_path_str)
        else:
            raise ValueError(f"Unsupported file format for pandapower network: {file_path}")

    return net

save_pandapower_to_fs #

save_pandapower_to_fs(
    net, filesystem, file_path, format="JSON", make_dir=True
)

Save pandapower network to a filesystem in pandapower native format (.json) or Matpower (.mat).

PARAMETER DESCRIPTION
net

The pandapower network to save.

TYPE: pandapowerNet

filesystem

The filesystem to save the pandapower network to.

TYPE: AbstractFileSystem

file_path

The path to save the pandapower network file to.

TYPE: Path

format

The format to save the pandapower network in. Can be "JSON" or "MATPOWER". Defaults to "JSON".

TYPE: Optional[Literal[JSON, MATPOWER]] DEFAULT: 'JSON'

make_dir

create parent folder if not exists.

TYPE: bool DEFAULT: True

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_helpers.py
def save_pandapower_to_fs(
    net: pandapower.pandapowerNet,
    filesystem: AbstractFileSystem,
    file_path: Path,
    format: Optional[Literal["JSON", "MATPOWER"]] = "JSON",
    make_dir: bool = True,
) -> None:
    """Save pandapower network to a filesystem in pandapower native format (.json) or Matpower (.mat).

    Parameters
    ----------
    net : pandapower.pandapowerNet
        The pandapower network to save.
    filesystem : AbstractFileSystem
        The filesystem to save the pandapower network to.
    file_path : Path
        The path to save the pandapower network file to.
    format : Optional[Literal["JSON", "MATPOWER"]]
        The format to save the pandapower network in. Can be "JSON" or "MATPOWER". Defaults to "JSON".
    make_dir: bool
        create parent folder if not exists.
    """
    if make_dir:
        filesystem.makedirs(Path(file_path).parent.as_posix(), exist_ok=True)
    with tempfile.TemporaryDirectory() as temp_dir:
        tmp_grid_path = Path(temp_dir) / file_path.name
        tmp_grid_path_str = str(tmp_grid_path)

        if format == "JSON":
            pandapower.to_json(net, tmp_grid_path_str)
        elif format == "MATPOWER":
            to_mpc(net, tmp_grid_path_str)
        else:
            raise ValueError(f"Unsupported file format for saving pandapower network: {format}")

        filesystem.upload(
            tmp_grid_path_str,
            str(file_path),
        )

toop_engine_grid_helpers.pandapower.pandapower_id_helpers #

Functions to handle pandapower ids

Pandapower indices are usually not globally unique, so we provide some helper functions to make them globally unique.

SEPARATOR module-attribute #

SEPARATOR = '%%'

get_globally_unique_id #

get_globally_unique_id(elem_id, elem_type)

Get a globally unique id for an element

Unfortunately, pandapowers ids are only unique within their type, so we need to add the type to the id to make it globally unique.

PARAMETER DESCRIPTION
elem_id

The id of the element

TYPE: Union[str, int]

elem_type

The type of the element

TYPE: str

RETURNS DESCRIPTION
str

The globally unique id of the element

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_id_helpers.py
def get_globally_unique_id(elem_id: Union[str, int], elem_type: Optional[str]) -> str:
    """Get a globally unique id for an element

    Unfortunately, pandapowers ids are only unique within their type, so we need to add the type
    to the id to make it globally unique.

    Parameters
    ----------
    elem_id : Union[str, int]
        The id of the element
    elem_type : str
        The type of the element

    Returns
    -------
    str
        The globally unique id of the element
    """
    if elem_type is not None:
        # Sometimes, the elem_type comes with postfixes to separate
        elem_type = str(elem_type).replace(SEPARATOR, "&&")
    else:
        elem_type = ""

    return f"{elem_id}{SEPARATOR}{elem_type}"

parse_globally_unique_id #

parse_globally_unique_id(globally_unique_id)

Parse a globally unique id into its components

PARAMETER DESCRIPTION
globally_unique_id

The globally unique id

TYPE: str

RETURNS DESCRIPTION
int

The id of the element

str

The type of the element

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_id_helpers.py
def parse_globally_unique_id(globally_unique_id: str) -> tuple[int, str]:
    """Parse a globally unique id into its components

    Parameters
    ----------
    globally_unique_id : str
        The globally unique id

    Returns
    -------
    int
        The id of the element
    str
        The type of the element
    """
    elem_id, elem_type = globally_unique_id.split(SEPARATOR)
    return int(elem_id), elem_type

get_globally_unique_id_from_index #

get_globally_unique_id_from_index(
    element_idx, element_type
)

Parse a series of globally unique ids into a dataframe

PARAMETER DESCRIPTION
element_idx

The index of the element table

TYPE: Index | Series

element_type

The type of the element table (e.g. "bus", "line", etc.)

TYPE: str

RETURNS DESCRIPTION
Index

The index with added table name as prefix to make it globally unique

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_id_helpers.py
def get_globally_unique_id_from_index(element_idx: pd.Index | pd.Series, element_type: str) -> pd.Index | pd.Series:
    """Parse a series of globally unique ids into a dataframe

    Parameters
    ----------
    element_idx : pd.Index | pd.Series
        The index of the element table
    element_type : str
        The type of the element table (e.g. "bus", "line", etc.)

    Returns
    -------
    pd.Index
        The index with added table name as prefix to make it globally unique
    """
    globally_unique_ids = element_idx.astype(str) + SEPARATOR + element_type
    return globally_unique_ids

parse_globally_unique_id_series #

parse_globally_unique_id_series(globally_unique_ids)

Parse a series of globally unique ids into a dataframe

PARAMETER DESCRIPTION
globally_unique_ids

The series of globally unique ids

TYPE: Series

RETURNS DESCRIPTION
DataFrame

A dataframe with the id, type and name of the elements

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_id_helpers.py
def parse_globally_unique_id_series(globally_unique_ids: pd.Series) -> pd.DataFrame:
    """Parse a series of globally unique ids into a dataframe

    Parameters
    ----------
    globally_unique_ids : pd.Series
        The series of globally unique ids

    Returns
    -------
    pd.DataFrame
        A dataframe with the id, type and name of the elements
    """
    parsed_ids = globally_unique_ids.str.split(SEPARATOR, expand=True)
    parsed_ids.columns = ["id", "type"]
    parsed_ids["id"] = parsed_ids["id"].astype(int)
    return parsed_ids

table_id #

table_id(globally_unique_id)

Get the id in the pandapower table from a globally unique id

PARAMETER DESCRIPTION
globally_unique_id

The globally unique id

TYPE: str

RETURNS DESCRIPTION
Union[int, str]

The id in the pandapower table

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_id_helpers.py
def table_id(globally_unique_id: str) -> Union[int, str]:
    """Get the id in the pandapower table from a globally unique id

    Parameters
    ----------
    globally_unique_id : str
        The globally unique id

    Returns
    -------
    Union[int, str]
        The id in the pandapower table
    """
    elem_id, _ = parse_globally_unique_id(globally_unique_id)
    return elem_id

table_ids #

table_ids(list_of_globally_unique_ids)

Get the ids in the pandapower table from a list of globally unique ids

PARAMETER DESCRIPTION
list_of_globally_unique_ids

The list of globally unique ids

TYPE: list[str]

RETURNS DESCRIPTION
list[int]

The ids in the pandapower table

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_id_helpers.py
def table_ids(list_of_globally_unique_ids: list[str]) -> list[int]:
    """Get the ids in the pandapower table from a list of globally unique ids

    Parameters
    ----------
    list_of_globally_unique_ids : list[str]
        The list of globally unique ids

    Returns
    -------
    list[int]
        The ids in the pandapower table
    """
    return [table_id(globally_unique_id) for globally_unique_id in list_of_globally_unique_ids]

toop_engine_grid_helpers.pandapower.pandapower_import_helpers #

Module contains additional functions to process pandapower network.

File: pandapower_toolset.py Author: Created:

logger module-attribute #

logger = Logger(__name__)

fuse_closed_switches_fast #

fuse_closed_switches_fast(net, switch_ids=None)

Fuse closed switches in the network by merging busbars.

This routine uses an algorithm to number each busbar and then find the lowest connected busbar iteratively. If a busbar is connected to a lower-numbered busbar, it will be re-labeled to the lower-numbered busbar. This algorithm needs as many iterations as the maximum number of hops between the lowest and highest busbar in any of the substations.

PARAMETER DESCRIPTION
net

The pandapower network to fuse closed switches in, will be modified in-place.

TYPE: pandapowerNet

switch_ids

The switch ids to fuse. If None, all closed switches are fused.

TYPE: Optional[list[int]] DEFAULT: None

RETURNS DESCRIPTION
DataFrame

The closed switches that were fused.

DataFrame

The buses that were dropped because they were relabeled to a lower-numbered busbar.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def fuse_closed_switches_fast(
    net: pp.pandapowerNet,
    switch_ids: Optional[list[int]] = None,
) -> tuple[pd.DataFrame, pd.DataFrame]:
    """Fuse closed switches in the network by merging busbars.

    This routine uses an algorithm to number each busbar and then find the lowest connected busbar
    iteratively. If a busbar is connected to a lower-numbered busbar, it will be re-labeled to the
    lower-numbered busbar. This algorithm needs as many iterations as the maximum number of hops
    between the lowest and highest busbar in any of the substations.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to fuse closed switches in, will be modified in-place.
    switch_ids: list[int]
        The switch ids to fuse. If None, all closed switches are fused.

    Returns
    -------
    pd.DataFrame
        The closed switches that were fused.
    pd.DataFrame
        The buses that were dropped because they were relabeled to a lower-numbered busbar.
    """
    # Label the busbars, find the lowest index that every busbar is coupled to
    labels = np.arange(np.max(net.bus.index) + 1)
    closed_switches = net.switch[net.switch.closed & (net.switch.et == "b") & (net.switch.bus != net.switch.element)]
    if switch_ids is not None:
        closed_switches = closed_switches[closed_switches.index.isin(switch_ids)]
    while not np.array_equal(labels[closed_switches.bus.values], labels[closed_switches.element.values]):
        bus_smaller = labels[closed_switches.bus.values] < labels[closed_switches.element.values]
        element_smaller = labels[closed_switches.bus.values] > labels[closed_switches.element.values]

        # Where the element is smaller, set the bus labels to the element labels
        _, change_idx = np.unique(closed_switches.bus.values[element_smaller], return_index=True)
        labels[closed_switches.bus.values[element_smaller][change_idx]] = labels[
            closed_switches.element.values[element_smaller][change_idx]
        ]

        # Where the bus is smaller (and where the element was not already touched), set the element labels to the bus labels
        was_touched = np.isin(
            closed_switches.element.values,
            closed_switches.bus.values[element_smaller][change_idx],
        )
        cond = bus_smaller & ~was_touched
        _, change_idx = np.unique(closed_switches.element.values[cond], return_index=True)
        labels[closed_switches.element.values[cond][change_idx]] = labels[closed_switches.bus.values[cond][change_idx]]

    # Move all elements over to the lowest index busbar
    move_elements_based_on_labels(net, labels)
    # Drop all busbars that were re-labeled because they were connected to a lower-labeled bus
    buses_to_drop = net.bus[~np.isin(net.bus.index, labels)]
    switch_cond = (net.switch.et == "b") & (net.switch.bus == net.switch.element)
    switch_to_drop = net.switch[switch_cond]
    pp.toolbox.drop_elements(net, "switch", switch_to_drop.index)
    pp.drop_buses(net, buses_to_drop.index)
    return closed_switches, buses_to_drop

move_elements_based_on_labels #

move_elements_based_on_labels(net, labels)

Move all elements in the network to the lowest labeled busbar.

PARAMETER DESCRIPTION
net

The pandapower network to move elements in, will be modified in-place.

TYPE: pandapowerNet

labels

The labels of the busbars to move the elements to.

TYPE: ndarray

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def move_elements_based_on_labels(
    net: pp.pandapowerNet,
    labels: np.ndarray,
) -> None:
    """Move all elements in the network to the lowest labeled busbar.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to move elements in, will be modified in-place.
    labels: np.ndarray
        The labels of the busbars to move the elements to.
    """
    for element, column in pp.element_bus_tuples():
        if element == "switch":
            net[element][column] = labels[net[element][column]]
            switch_cond = net[element].et == "b"
            net[element].loc[switch_cond, "element"] = labels[net[element].loc[switch_cond, "element"]]
            net[element].loc[net[element].index, "bus"] = labels[net[element].loc[net[element].index, "bus"]]
        else:
            net[element][column] = labels[net[element][column]]

select_connected_subnet #

select_connected_subnet(net)

Select the connected subnet of the grid that has a slack and return it.

PARAMETER DESCRIPTION
net

The pandapower network to select the connected subnet from.

TYPE: pandapowerNet

RETURNS DESCRIPTION
pandapowerNet

The connected subnet of the grid that has a slack.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def select_connected_subnet(net: pp.pandapowerNet) -> pp.pandapowerNet:
    """Select the connected subnet of the grid that has a slack and return it.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to select the connected subnet from.

    Returns
    -------
    pp.pandapowerNet
        The connected subnet of the grid that has a slack.
    """
    name = net.name
    mg = pp.topology.create_nxgraph(net, respect_switches=True)

    slack_bus = net.ext_grid[net.ext_grid.in_service].bus
    if len(slack_bus) == 0:
        slack_bus = net.gen[net.gen.slack & net.gen.in_service].bus
        if len(slack_bus) == 0:
            raise ValueError("No slack bus found in the network.")
    slack_bus = slack_bus.iloc[0]

    cc = pp.topology.connected_component(mg, slack_bus)

    next_grid_buses = set(cc)
    net_new = pp.select_subnet(
        net,
        next_grid_buses,
        include_switch_buses=True,
        include_results=False,
        keep_everything_else=True,
    )
    net_new.name = name
    return net_new

replace_zero_branches #

replace_zero_branches(net)

Replace zero-impedance branches with switches in the network.

Some leftover lines and xwards will be bumped to a higher impedance to avoid numerical issues.

PARAMETER DESCRIPTION
net

The pandapower network to replace zero branches in, will be modified in-place.

TYPE: pandapowerNet

RETURNS DESCRIPTION
pandapowerNet

The network with zero branches replaced.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def replace_zero_branches(net: pp.pandapowerNet) -> None:
    """Replace zero-impedance branches with switches in the network.

    Some leftover lines and xwards will be bumped to a higher impedance to avoid numerical issues.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to replace zero branches in, will be modified in-place.

    Returns
    -------
    pp.pandapowerNet
        The network with zero branches replaced.
    """
    pp.toolbox.replace_zero_branches_with_switches(
        net,
        min_length_km=0.0,
        min_r_ohm_per_km=0.002,
        min_x_ohm_per_km=0.002,
        min_c_nf_per_km=0,
        min_rft_pu=0,
        min_xft_pu=0,
    )
    threshold_x_ohm = 0.001
    # net.xward.x_ohm[net.xward.x_ohm == 1e-6] = 1e-2
    net.xward.loc[net.xward.x_ohm < threshold_x_ohm, "x_ohm"] = 0.01
    zero_lines = (net.line.x_ohm_per_km * net.line.length_km) < threshold_x_ohm
    net.line.loc[zero_lines, "x_ohm_per_km"] = 0.01
    net.line.loc[zero_lines, "length_km"] = 1.0

drop_unsupplied_buses #

drop_unsupplied_buses(net)

Drop all unsupplied buses from the network.

PARAMETER DESCRIPTION
net

The pandapower network to drop unsupplied buses from, will be modified in-place.

TYPE: pandapowerNet

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def drop_unsupplied_buses(net: pp.pandapowerNet) -> None:
    """Drop all unsupplied buses from the network.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to drop unsupplied buses from, will be modified in-place.
    """
    pp.drop_buses(net, pp.topology.unsupplied_buses(net))
    assert len(pp.topology.unsupplied_buses(net)) == 0

create_virtual_slack #

create_virtual_slack(net)

Create a virtual slack bus for all ext_grids in the network.

PARAMETER DESCRIPTION
net

The pandapower network to create a virtual slack for, will be modified in-place. Note: network is modified in-place.

TYPE: pandapowerNet

RETURNS DESCRIPTION
pandapowerNet

The network with a virtual slack.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def create_virtual_slack(net: pp.pandapowerNet) -> None:
    """Create a virtual slack bus for all ext_grids in the network.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to create a virtual slack for, will be modified in-place.
        Note: network is modified in-place.

    Returns
    -------
    pp.pandapowerNet
        The network with a virtual slack.
    """
    if net.gen.slack.sum() <= 1:
        return
    # Create a virtual slack where all ext_grids are connected to
    virtual_slack_bus = pp.create_bus(net, vn_kv=380, in_service=True, name="virtual_slack")

    for generator in net.gen[net.gen.slack].index:
        cur_bus = net.gen.loc[generator].bus
        # Connect each gen through a trafo to the virtual slack
        pp.create_transformer_from_parameters(
            net,
            hv_bus=virtual_slack_bus,
            lv_bus=cur_bus,
            name="con_" + str(net.gen.loc[generator].name),
            sn_mva=9999,
            vn_hv_kv=net.bus.vn_kv[cur_bus],
            vn_lv_kv=net.bus.vn_kv[cur_bus],
            # shift_degree=net.ext_grid.loc[generator].va_degree,
            shift_degree=0,
            pfe_kw=1,
            i0_percent=0.1,
            vk_percent=1,
            vkr_percent=0.1,
            xn_ohm=10,
        )

    net.gen.drop(net.gen[net.gen.slack].index, inplace=True)

    pp.create_ext_grid(
        net,
        virtual_slack_bus,
        vm_pu=1,
        va_degree=0,
        in_service=True,
        name="virtual_slack",
    )

remove_out_of_service #

remove_out_of_service(net)

Remove all out-of-service elements from the network.

PARAMETER DESCRIPTION
net

The pandapower network to remove out-of-service elements from, will be modified in-place.

TYPE: pandapowerNet

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def remove_out_of_service(net: pp.pandapowerNet) -> None:
    """Remove all out-of-service elements from the network.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to remove out-of-service elements from, will be modified in-place.
    """
    for element in pp.pp_elements():
        if "bus" == element and "in_service" in net[element]:
            pp.drop_buses(net, net[element][~net[element]["in_service"]].index)
        elif "in_service" in net[element]:
            net[element] = net[element][net[element]["in_service"]]

drop_elements_connected_to_one_bus #

drop_elements_connected_to_one_bus(net, branch_types=None)

Drop elements connected to one bus.

  • impedance -> Capacitor will end up on the same bus
  • trafo3w -> edgecase: trafo3w that goes from one hv to the same level but two different busbars will end up on the same bus
PARAMETER DESCRIPTION
net

pandapower network Note: the network is modified in place

TYPE: pandapowerNet

branch_types

list of branch types to drop elements connected to one bus

TYPE: list[str] DEFAULT: None

RETURNS DESCRIPTION
None
Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def drop_elements_connected_to_one_bus(net: pp.pandapowerNet, branch_types: Optional[list[str]] = None) -> None:
    """Drop elements connected to one bus.

    - impedance -> Capacitor will end up on the same bus
    - trafo3w -> edgecase: trafo3w that goes from one hv to the same level but two
                 different busbars will end up on the same bus

    Parameters
    ----------
    net : pp.pandapowerNet
        pandapower network
        Note: the network is modified in place
    branch_types : list[str]
        list of branch types to drop elements connected to one bus

    Returns
    -------
    None

    """
    if branch_types is None:
        branch_types = ["line", "trafo", "trafo3w", "impedance", "switch"]

    for branch_type in branch_types:
        handle_elements_connected_to_one_bus(net, branch_type)

handle_elements_connected_to_one_bus #

handle_elements_connected_to_one_bus(net, branch_type)

Drop elements of a specific branch type connected to one bus.

PARAMETER DESCRIPTION
net

pandapower network Note: the network is modified in place

TYPE: pandapowerNet

branch_type

branch type to drop elements connected to one bus

TYPE: str

RAISES DESCRIPTION
ValueError

If the branch type is not recognized.

AssertionError

If a two-winding transformer with same hv and lv bus is found. If a three-winding transformer with same hv == lv or hv == mv bus is found

RETURNS DESCRIPTION
None
Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def handle_elements_connected_to_one_bus(net: pp.pandapowerNet, branch_type: str) -> None:
    """Drop elements of a specific branch type connected to one bus.

    Parameters
    ----------
    net : pp.pandapowerNet
        pandapower network
        Note: the network is modified in place
    branch_type : str
        branch type to drop elements connected to one bus

    Raises
    ------
    ValueError
        If the branch type is not recognized.
    AssertionError
        If a two-winding transformer with same hv and lv bus is found.
        If a three-winding transformer with same hv == lv or hv == mv bus is found

    Returns
    -------
    None
    """
    branch_df = getattr(net, branch_type)
    if branch_type == "switch":
        branch_index = branch_df[(branch_df["bus"] == branch_df["element"]) & (branch_df["et"] == "b")].index
        pp.drop_elements(net, element_type=branch_type, element_index=branch_index)
    elif branch_type in ["line", "impedance"]:
        branch_index = branch_df[branch_df["from_bus"] == branch_df["to_bus"]].index
        pp.drop_elements(net, element_type=branch_type, element_index=branch_index)
    elif branch_type == "trafo":
        branch_index = branch_df[branch_df["hv_bus"] == branch_df["lv_bus"]].index
        assert len(branch_index) == 0, (
            "Two winding transformer with same hv and lv bus found in " + f"{branch_df.loc[branch_index].to_dict()}"
        )
    elif branch_type == "trafo3w":
        hv_cond = (branch_df["hv_bus"] == branch_df["lv_bus"]) | (branch_df["hv_bus"] == branch_df["mv_bus"])
        branch_index = branch_df[hv_cond].index
        assert len(branch_index) == 0, (
            "Three winding transformer with same hv == lv or hv == mv bus found in "
            + f"{branch_df.loc[branch_index].to_dict()}"
        )
        lv_cond = branch_df["lv_bus"] == branch_df["mv_bus"]
        branch_index = branch_df[lv_cond].index
        if len(branch_index) > 0:
            logger.warning(
                "Three winding transformer with same mv and lv bus found in " + f"{branch_df.loc[branch_index].to_dict()}"
            )
    else:
        raise ValueError(f"Branch type {branch_type} not recognized for dropping elements connected to one bus.")

toop_engine_grid_helpers.pandapower.pandapower_tasks #

Holds functions helping with some pandapower related tasks

logger module-attribute #

logger = Logger(__name__)

get_max_line_flow #

get_max_line_flow(net)

Get the rated power for each line in the network

PARAMETER DESCRIPTION
net

The pandapower network containing the line

TYPE: pandapowerNet

RETURNS DESCRIPTION
Float[ndarray, ' n_pp_lines']

The rated power of each line branch in the network

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_tasks.py
def get_max_line_flow(net: pp.pandapowerNet) -> Float[np.ndarray, " n_pp_lines"]:
    """Get the rated power for each line in the network

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network containing the line

    Returns
    -------
    Float[np.ndarray, " n_pp_lines"]
        The rated power of each line branch in the network
    """
    voltage_rating = net.bus.loc[net.line["from_bus"].values, "vn_kv"].values * np.sqrt(3.0)
    max_i_ka = net.line.max_i_ka.values
    derating_factor = net.line.df.values
    parallel = net.line.parallel.values
    max_line_flow = max_i_ka * derating_factor * parallel * voltage_rating
    return max_line_flow

get_max_trafo_flow #

get_max_trafo_flow(net)

Get the rated power for each trafo in the network.

PARAMETER DESCRIPTION
net

The pandapower network containing the trafos

TYPE: pandapowerNet

RETURNS DESCRIPTION
Float[ndarray, ' n_pp_trafos']

The rated power of each trafo branch in the network

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_tasks.py
def get_max_trafo_flow(net: pp.pandapowerNet) -> Float[np.ndarray, " n_pp_trafos"]:
    """Get the rated power for each trafo in the network.

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network containing the trafos

    Returns
    -------
    Float[np.ndarray, " n_pp_trafos"]
        The rated power of each trafo branch in the network
    """
    sn_mva = net.trafo.sn_mva.values
    derating_factor = net.trafo.df.values
    parallel = net.trafo.parallel.values
    max_trafo_flow = sn_mva * derating_factor * parallel
    return max_trafo_flow

get_max_trafo3w_flow #

get_max_trafo3w_flow(net)

Get the rated power of the different voltage levels of all trafo3ws in the network

PARAMETER DESCRIPTION
net

The pandapower network containing the trafo3ws

TYPE: pandapowerNet

RETURNS DESCRIPTION
Float[ndarray, ' n_pp_trafo3ws*3']

The rated power of each trafo3w branch in the network

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_tasks.py
def get_max_trafo3w_flow(
    net: pp.pandapowerNet,
) -> Float[np.ndarray, " n_pp_trafo3ws_times_3"]:
    """Get the rated power of the different voltage levels of all trafo3ws in the network

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network containing the trafo3ws

    Returns
    -------
    Float[np.ndarray, " n_pp_trafo3ws*3"]
        The rated power of each trafo3w branch in the network
    """
    max_trafo3w_hv_flow = net.trafo3w.sn_hv_mva.values
    max_trafo3w_mv_flow = net.trafo3w.sn_mv_mva.values
    max_trafo3w_lv_flow = net.trafo3w.sn_lv_mva.values
    return np.concatenate([max_trafo3w_hv_flow, max_trafo3w_mv_flow, max_trafo3w_lv_flow])

get_trafo3w_ppc_branch_idx #

get_trafo3w_ppc_branch_idx(net, trafo3w_pp_idx)

Get the corresponding branch ids of the trafo3w pandapower ids.

PARAMETER DESCRIPTION
net

The pandapower network containing the trafo3ws

TYPE: pandapowerNet

trafo3w_pp_idx

An array of trafo3w indices from pandapower

TYPE: Int[ndarray, ' rel_trafo3ws']

RETURNS DESCRIPTION
Int[ndarray, ' 3 rel_trafo3ws']

Return a 3xnum_trafo3w_pp_idx array containing the hv, mv and lv ppci_branch_idx of the trafo3w

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_tasks.py
def get_trafo3w_ppc_branch_idx(
    net: pp.pandapowerNet, trafo3w_pp_idx: Int[np.ndarray, " rel_trafo3ws"]
) -> Int[np.ndarray, " 3 rel_trafo3ws"]:
    """Get the corresponding branch ids of the trafo3w pandapower ids.

    Parameters
    ----------
    net : pp.pandapowerNet
        The pandapower network containing the trafo3ws
    trafo3w_pp_idx: Int[np.ndarray, " rel_trafo3ws"]
        An array of trafo3w indices from pandapower

    Returns
    -------
    Int[np.ndarray, " 3 rel_trafo3ws"]
        Return a 3xnum_trafo3w_pp_idx array containing the hv, mv and lv ppci_branch_idx of the trafo3w
    """
    # 3 winding trafos are listed after lines and 2w-trafos
    idx_before = net.line.shape[0] + net.trafo.shape[0]
    # Each voltage level of the trafo3w is its own branch.
    # The 3 branches have the ids
    # id += voltage_multiplier * n_trafo3w with voltage_multiplier = (hv->0, mv->1, lv->2)
    num_trafo3w = net.trafo3w.shape[0]
    return np.array(
        [
            idx_before + trafo3w_pp_idx,
            idx_before + trafo3w_pp_idx + num_trafo3w,
            idx_before + trafo3w_pp_idx + 2 * num_trafo3w,
        ]
    )

get_trafo3w_ppc_node_idx #

get_trafo3w_ppc_node_idx(ppci, trafo3w_branch_idx)

Get the corresponding node indices of the trafo3w pandapower ids

PARAMETER DESCRIPTION
ppci

The ppci dict from pandapower

TYPE: dict

trafo3w_branch_idx

An array of trafo3w indices as obtained from get_trafo3w_ppc_branch_idx

TYPE: Int[ndarray, ' 3 rel_trafo3ws']

RETURNS DESCRIPTION
Int[ndarray, ' rel_trafo3ws']

Return a num_trafo3w_pp_idx array containing the ppci node indices of the trafo3w

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_tasks.py
def get_trafo3w_ppc_node_idx(
    ppci: dict, trafo3w_branch_idx: Int[np.ndarray, " 3 rel_trafo3ws"]
) -> Int[np.ndarray, " rel_trafo3ws"]:
    """Get the corresponding node indices of the trafo3w pandapower ids

    Parameters
    ----------
    ppci : dict
        The ppci dict from pandapower
    trafo3w_branch_idx: Int[np.ndarray, " 3 rel_trafo3ws"]
        An array of trafo3w indices as obtained from get_trafo3w_ppc_branch_idx

    Returns
    -------
    Int[np.ndarray, " rel_trafo3ws"]
        Return a num_trafo3w_pp_idx array containing the ppci node indices of the trafo3w
    """
    center_bus = ppci["branch"][trafo3w_branch_idx[0, :], T_BUS].astype(int)
    center_bus_2 = ppci["branch"][trafo3w_branch_idx[1, :], F_BUS].astype(int)
    center_bus_3 = ppci["branch"][trafo3w_branch_idx[2, :], F_BUS].astype(int)
    assert np.array_equal(center_bus, center_bus_2)
    assert np.array_equal(center_bus, center_bus_3)
    return center_bus

Grid Helpers Powsybl#

toop_engine_grid_helpers.powsybl #

toop_engine_grid_helpers.powsybl.example_grids #

add_phaseshift_transformer_to_line_powsybl #

add_phaseshift_transformer_to_line_powsybl(
    net,
    line_idx,
    tap_min=-30,
    tap_max=30,
    tap_step_degree=2.0,
)

Add a phaseshift transformer to the from side of a line

Inserts a helper bus and a transformer with a tap changer to the given line.

PARAMETER DESCRIPTION
net

The powsybl network, will be modified in place.

TYPE: Network

line_idx

The index of the line in net.line on which to insert the phase-shifting transformer.

TYPE: str

tap_min

The minimum tap position, by default -30

TYPE: int DEFAULT: -30

tap_max

The maximum tap position, by default 30

TYPE: int DEFAULT: 30

tap_step_degree

The step size in degrees for each tap position, by default 2.0

TYPE: float DEFAULT: 2.0

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def add_phaseshift_transformer_to_line_powsybl(
    net: pypowsybl.network.Network,
    line_idx: str,
    tap_min: int = -30,
    tap_max: int = 30,
    tap_step_degree: float = 2.0,
) -> None:
    """Add a phaseshift transformer to the from side of a line

    Inserts a helper bus and a transformer with a tap changer to the given line.

    Parameters
    ----------
    net : Network
        The powsybl network, will be modified in place.
    line_idx : str
        The index of the line in net.line on which to insert the phase-shifting transformer.
    tap_min : int, optional
        The minimum tap position, by default -30
    tap_max : int, optional
        The maximum tap position, by default 30
    tap_step_degree : float, optional
        The step size in degrees for each tap position, by default 2.0
    """
    line = net.get_lines(all_attributes=True).loc[line_idx]
    vl = line["voltage_level1_id"]
    nominal_v = net.get_voltage_levels().loc[vl, "nominal_v"]
    helper_bus = f"pst_bus_{line_idx}"
    net.create_buses(
        id=helper_bus,
        voltage_level_id=vl,
    )
    original_bus = line["bus_breaker_bus1_id"]
    net.update_branches(
        id=line_idx,
        bus_breaker_bus1_id=helper_bus,
    )

    pst = f"pst_{line_idx}"
    net.create_2_windings_transformers(
        id=pst,
        bus1_id=original_bus,
        voltage_level1_id=vl,
        bus2_id=helper_bus,
        voltage_level2_id=vl,
        rated_u1=nominal_v,
        rated_u2=nominal_v,
        rated_s=100,
        b=0.0,
        g=0.0,
        r=0.1,
        x=1.0,
    )

    ptc_df = pd.DataFrame.from_records(
        index="id",
        columns=[
            "id",
            "target_deadband",
            "regulation_mode",
            "regulating",
            "low_tap",
            "tap",
        ],
        data=[(pst, 2, "CURRENT_LIMITER", False, -30, 0)],
    )
    steps_df = pd.DataFrame.from_records(
        index="id",
        columns=["id", "b", "g", "r", "x", "rho", "alpha"],
        data=[(pst, 0, 0, 0.1, 1, 1, tap_step_degree * tap) for tap in range(tap_min, tap_max + 1)],
    )
    net.create_phase_tap_changers(ptc_df, steps_df)

powsybl_case30_with_psts #

powsybl_case30_with_psts()

Create a Powsybl IEEE 30 bus grid with phase-shifting transformers.

RETURNS DESCRIPTION
Network

The Powsybl IEEE 30 bus network with phase-shifting transformers.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def powsybl_case30_with_psts() -> pypowsybl.network.Network:
    """Create a Powsybl IEEE 30 bus grid with phase-shifting transformers.

    Returns
    -------
    pypowsybl.network.Network
        The Powsybl IEEE 30 bus network with phase-shifting transformers.
    """
    net = pypowsybl.network.create_ieee30()
    add_phaseshift_transformer_to_line_powsybl(net, "L8-28-1", tap_min=-20, tap_max=10, tap_step_degree=3.0)
    add_phaseshift_transformer_to_line_powsybl(net, "L6-28-1", tap_min=-30, tap_max=35, tap_step_degree=4.0)
    return net

powsybl_extended_case57 #

powsybl_extended_case57()

Create an extended version of the Powsybl IEEE 57 bus grid with additional elements.

RETURNS DESCRIPTION
Network

The extended Powsybl IEEE 57 bus network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def powsybl_extended_case57() -> pypowsybl.network.Network:
    """Create an extended version of the Powsybl IEEE 57 bus grid with additional elements.


    Returns
    -------
    pypowsybl.network.Network
        The extended Powsybl IEEE 57 bus network.
    """
    net = pypowsybl.network.create_ieee57()

    # Update max power of the generators to be able to have a distributed slack (it does not work if the max power is
    # deemed inplausible). This could also be achieved by setting the provider parameter "plausibleActivePowerLimit":"20000",
    # but since we do not want to add this limit to the function updating the values is cleaner.
    gens = net.get_generators()
    gens["max_p"] = 4999
    # Set some gens to voltage regulator False, so that changes in target_q actually do something
    gens.loc[gens.index[2:4], "voltage_regulator_on"] = False
    net.update_generators(gens[["max_p", "voltage_regulator_on"]])

    # Add fake PST
    net.create_buses(id="PSTBus", voltage_level_id="VL6")
    net.create_lines(
        id="PSTLine",
        voltage_level1_id="VL6",
        bus1_id="PSTBus",
        voltage_level2_id="VL17",
        bus2_id="B17",
        r=0.1 / 100,
        x=0.1 / 100,
        g1=0,
        b1=0,
        g2=0,
        b2=0,
    )
    net.create_2_windings_transformers(
        id="PST",
        voltage_level1_id="VL6",
        bus1_id="PSTBus",
        voltage_level2_id="VL6",
        bus2_id="B6",
        rated_u1=1,
        rated_u2=1,
        rated_s=np.nan,
        r=0,
        x=0.1 / 100,
        g=0,
        b=0,
    )
    ptc_df = pd.DataFrame.from_records(
        index="id",
        columns=["id", "regulation_mode", "regulating", "low_tap", "tap"],
        data=[("PST", "CURRENT_LIMITER", False, 0, 1)],
    )
    steps_df = pd.DataFrame.from_records(
        index="id",
        columns=["id", "b", "g", "r", "x", "rho", "alpha"],
        data=[
            ("PST", 0, 0, 0, 0, 1, 0),
            ("PST", 0, 0, 0, 0, 1, 8),
            ("PST", 0, 0, 0, 0, 1, 16),
        ],
    )
    net.create_phase_tap_changers(ptc_df=ptc_df, steps_df=steps_df)
    pypowsybl.loadflow.run_ac(net)

    # Add artificial operational limits
    lines = net.get_lines()
    limits = pd.DataFrame(
        data={
            "element_id": lines.index,
            "side": ["ONE"] * len(lines),
            "name": ["permanent_limit"] * len(lines),
            "type": ["CURRENT"] * len(lines),
            "value": lines["i1"].values,
            "acceptable_duration": [-1] * len(lines),
        }
    )
    limits.index = limits["element_id"]

    limits2 = pd.DataFrame(
        data={
            "element_id": lines.index,
            "side": ["ONE"] * len(lines),
            "name": ["N-1"] * len(lines),
            "type": ["CURRENT"] * len(lines),
            "value": lines["i1"].values * 2,
            "acceptable_duration": [2] * len(lines),
        }
    )
    limits2.index = limits2["element_id"]
    limits = pd.concat([limits, limits2])
    net.create_operational_limits(limits)

    limits = pd.DataFrame(
        data={
            "element_id": net.get_2_windings_transformers().index,
            "side": ["ONE"] * len(net.get_2_windings_transformers()),
            "name": ["permanent_limit"] * len(net.get_2_windings_transformers()),
            "type": ["CURRENT"] * len(net.get_2_windings_transformers()),
            "value": net.get_2_windings_transformers()["i1"].values,
            "acceptable_duration": [-1] * len(net.get_2_windings_transformers()),
        }
    )
    limits.index = limits["element_id"]
    trafo2w = net.get_2_windings_transformers()
    limits2 = pd.DataFrame(
        data={
            "element_id": trafo2w.index,
            "side": ["ONE"] * len(trafo2w),
            "name": ["N-1"] * len(trafo2w),
            "type": ["CURRENT"] * len(trafo2w),
            "value": trafo2w["i1"].values * 2,
            "acceptable_duration": [2] * len(trafo2w),
        }
    )
    limits2.index = limits2["element_id"]
    limits = pd.concat([limits, limits2])
    net.create_operational_limits(limits)
    return net

basic_node_breaker_network_powsybl #

basic_node_breaker_network_powsybl()

Create a basic node breaker network with 5 substations, 5 voltage levels, and 10 buses.

RETURNS DESCRIPTION
Network

The created Powsybl network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def basic_node_breaker_network_powsybl() -> Network:
    """Create a basic node breaker network with 5 substations, 5 voltage levels, and 10 buses.

    Returns
    -------
    pypowsybl.network.Network
        The created Powsybl network.
    """
    net = pypowsybl.network.create_empty()

    n_subs = 5
    n_vls = 5
    # substation_id : number of buses
    n_buses = {1: 3, 2: 3, 3: 2, 4: 2, 5: 1}

    stations = pd.DataFrame.from_records(
        index="id", data=[{"id": f"S{i + 1}", "country": "BE", "name": f"Station{i + 1}"} for i in range(n_subs)]
    )
    voltage_levels = pd.DataFrame.from_records(
        index="id",
        data=[
            {
                "substation_id": f"S{i + 1}",
                "id": f"VL{i + 1}",
                "topology_kind": "NODE_BREAKER",
                "nominal_v": 225,
                "name": f"VLevel{i + 1}",
            }
            for i in range(n_vls)
        ],
    )
    busbars = pd.DataFrame.from_records(
        index="id",
        data=[
            {"voltage_level_id": f"VL{sub_id}", "id": f"BBS{sub_id}_{bus_id}", "node": bus_id - 1, "name": f"bus{bus_id}"}
            for sub_id, num_buses in n_buses.items()
            for bus_id in range(1, num_buses + 1)
        ],
    )
    busbar_section_position = pd.DataFrame.from_records(
        index="id",
        data=[
            {"id": f"BBS{sub_id}_{bus_id}", "section_index": 1, "busbar_index": bus_id}
            for sub_id, num_buses in n_buses.items()
            for bus_id in range(1, num_buses + 1)
        ],
    )

    net.create_substations(stations)
    net.create_voltage_levels(voltage_levels)
    net.create_busbar_sections(busbars)
    net.create_extensions("busbarSectionPosition", busbar_section_position)

    lines = pd.DataFrame.from_records(
        data=[
            {"bus_or_busbar_section_id_1": "BBS1_1", "bus_or_busbar_section_id_2": "BBS2_1"},
            {"bus_or_busbar_section_id_1": "BBS1_2", "bus_or_busbar_section_id_2": "BBS2_2"},
            {"bus_or_busbar_section_id_1": "BBS1_3", "bus_or_busbar_section_id_2": "BBS3_1"},
            {"bus_or_busbar_section_id_1": "BBS1_3", "bus_or_busbar_section_id_2": "BBS4_1"},
            {"bus_or_busbar_section_id_1": "BBS1_2", "bus_or_busbar_section_id_2": "BBS4_2"},
            {"bus_or_busbar_section_id_1": "BBS2_1", "bus_or_busbar_section_id_2": "BBS3_1"},
            {"bus_or_busbar_section_id_1": "BBS2_2", "bus_or_busbar_section_id_2": "BBS3_2"},
            {"bus_or_busbar_section_id_1": "BBS2_1", "bus_or_busbar_section_id_2": "BBS4_1"},
            {"bus_or_busbar_section_id_1": "BBS3_1", "bus_or_busbar_section_id_2": "BBS5_1"},
        ]
    )
    lines["r"] = 0.1
    lines["x"] = 10
    lines["g1"] = 0
    lines["b1"] = 0
    lines["g2"] = 0
    lines["b2"] = 0
    lines["position_order_1"] = 1
    lines["position_order_2"] = 1
    for i, _ in lines.iterrows():
        lines.loc[i, "id"] = f"L{i + 1}"
    lines = lines.set_index("id")
    pypowsybl.network.create_line_bays(net, lines)
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["BBS1_1", "BBS1_2"], bus_or_busbar_section_id_2=["BBS1_2", "BBS1_3"]
    )
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["BBS2_1"], bus_or_busbar_section_id_2=["BBS2_2"]
    )
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["BBS2_2"], bus_or_busbar_section_id_2=["BBS2_3"]
    )
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["BBS3_1"], bus_or_busbar_section_id_2=["BBS3_2"]
    )
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["BBS4_1"], bus_or_busbar_section_id_2=["BBS4_2"]
    )
    pypowsybl.network.create_load_bay(net, id="load1", bus_or_busbar_section_id="BBS2_1", p0=100, q0=10, position_order=1)
    pypowsybl.network.create_load_bay(net, id="load2", bus_or_busbar_section_id="BBS3_2", p0=100, q0=10, position_order=2)
    pypowsybl.network.create_generator_bay(
        net,
        id="generator1",
        max_p=1000,
        min_p=0,
        voltage_regulator_on=True,
        target_p=50,
        target_q=10,
        target_v=225,
        bus_or_busbar_section_id="BBS1_1",
        position_order=1,
    )
    pypowsybl.network.create_generator_bay(
        net,
        id="generator2",
        max_p=1000,
        min_p=0,
        voltage_regulator_on=True,
        target_p=50,
        target_q=10,
        target_v=225,
        bus_or_busbar_section_id="BBS1_2",
        position_order=1,
    )
    pypowsybl.network.create_generator_bay(
        net,
        id="generator3",
        max_p=1000,
        min_p=0,
        voltage_regulator_on=True,
        target_p=100,
        target_q=10,
        target_v=225,
        bus_or_busbar_section_id="BBS5_1",
        position_order=2,
    )
    limits = pd.DataFrame.from_records(
        data=[
            {
                "element_id": "L1",
                "value": 90,
                "side": "ONE",
                "name": "permanent",
                "type": "CURRENT",
                "acceptable_duration": -1,
            },
            {
                "element_id": "L2",
                "value": 90,
                "side": "ONE",
                "name": "permanent",
                "type": "CURRENT",
                "acceptable_duration": -1,
            },
            {
                "element_id": "L3",
                "value": 90,
                "side": "ONE",
                "name": "permanent",
                "type": "CURRENT",
                "acceptable_duration": -1,
            },
        ],
        index="element_id",
    )
    net.create_operational_limits(limits)
    return net

powsybl_case9241 #

powsybl_case9241()

Load the Powsybl case9241 grid.

RETURNS DESCRIPTION
Network

The loaded Powsybl pegase case9241 network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def powsybl_case9241() -> pypowsybl.network.Network:
    """Load the Powsybl case9241 grid.

    Returns
    -------
    pypowsybl.network.Network
        The loaded Powsybl pegase case9241 network.
    """
    pandapower_net = pandapower.networks.case9241pegase()
    net = load_pandapower_net_for_powsybl(pandapower_net, check_trafo_resistance=False)

    return net

create_busbar_b_in_ieee #

create_busbar_b_in_ieee(net)

Create busbar B in the IEEE grid

This is needed to create a busbar B for each bus in the IEEE grid. The busbar A is already there, so we just need to create the busbar B and connect it to the busbar A with a coupler.

The bus B will have an id similar to bus A but with '_b' suffixed.

PARAMETER DESCRIPTION
net

The network to create the busbar Bs in, will be modified in place

TYPE: Network

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def create_busbar_b_in_ieee(net: pypowsybl.network.Network) -> None:
    """Create busbar B in the IEEE grid

    This is needed to create a busbar B for each bus in the IEEE grid. The busbar A is already there, so we just need to
    create the busbar B and connect it to the busbar A with a coupler.

    The bus B will have an id similar to bus A but with '_b' suffixed.

    Parameters
    ----------
    net : pypowsybl.network.Network
        The network to create the busbar Bs in, will be modified in place
    """
    for index, bus in net.get_bus_breaker_view_buses().iterrows():
        net.create_buses(
            id=index + "_b",
            voltage_level_id=bus.voltage_level_id,
            name=bus.name + "_b",
        )
        net.create_switches(
            id="SWITCH-" + index,
            bus1_id=index,
            bus2_id=index + "_b",
            voltage_level_id=bus.voltage_level_id,
            kind="BREAKER",
            open=False,
            retained=True,
        )

extract_station_info_powsybl #

extract_station_info_powsybl(net, base_folder)
Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def extract_station_info_powsybl(net: Network, base_folder: Path) -> None:
    stations = get_stations_bus_breaker(net)
    target = base_folder / PREPROCESSING_PATHS["asset_topology_file_path"]
    target.parent.mkdir(parents=True, exist_ok=True)
    save_asset_topology(
        target,
        Topology(
            stations=stations,
            topology_id="extracted_topology",
            timestamp=datetime.datetime.now(),
        ),
    )

case14_matching_asset_topo_powsybl #

case14_matching_asset_topo_powsybl(folder)
Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def case14_matching_asset_topo_powsybl(folder: Path) -> None:
    net = pypowsybl.network.create_ieee14()
    create_busbar_b_in_ieee(net)
    os.makedirs(folder, exist_ok=True)

    grid_path = folder / PREPROCESSING_PATHS["grid_file_path_powsybl"]
    grid_path.parent.mkdir(parents=True, exist_ok=True)
    net.save(grid_path)

    # create asset topology
    extract_station_info_powsybl(net, folder)

    # create masks
    output_path_masks = folder / PREPROCESSING_PATHS["masks_path"]
    output_path_masks.mkdir(parents=True, exist_ok=True)
    rel_sub_mask = np.ones(len(net.get_buses()), dtype=bool)
    np.save(output_path_masks / NETWORK_MASK_NAMES["relevant_subs"], rel_sub_mask)
    line_mask = np.ones(len(net.get_lines()), dtype=bool)
    np.save(output_path_masks / NETWORK_MASK_NAMES["line_for_reward"], line_mask)
    np.save(output_path_masks / NETWORK_MASK_NAMES["line_for_nminus1"], line_mask)
    trafo_mask = np.ones(len(net.get_2_windings_transformers()), dtype=bool)
    np.save(output_path_masks / NETWORK_MASK_NAMES["trafo_for_reward"], trafo_mask)
    np.save(output_path_masks / NETWORK_MASK_NAMES["trafo_for_nminus1"], trafo_mask)
    gen_mask = np.ones(len(net.get_generators()), dtype=bool)
    np.save(output_path_masks / NETWORK_MASK_NAMES["generator_for_nminus1"], gen_mask)

create_complex_grid_battery_hvdc_svc_3w_trafo #

create_complex_grid_battery_hvdc_svc_3w_trafo()

Create a complex grid with batteries, HVDC, SVC, and 3-winding transformers using Powsybl.

This grid includes various components to test different functionalities. It is not aimed to be a realistic representation of an actual power grid but rather a comprehensive test case. The Basecase should converge in about 10 iterations with a tolerance of 1e-6.

TODO: add sensable operational limits, maybe some ratio/phase tap changers, etc. Ideally it should have some overloads that can be solved by ToOp

RETURNS DESCRIPTION
Network

The created complex grid network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
def create_complex_grid_battery_hvdc_svc_3w_trafo() -> Network:
    """Create a complex grid with batteries, HVDC, SVC, and 3-winding transformers using Powsybl.

    This grid includes various components to test different functionalities. It is not aimed to be a realistic
    representation of an actual power grid but rather a comprehensive test case. The Basecase should converge
    in about 10 iterations with a tolerance of 1e-6.

    TODO: add sensable operational limits, maybe some ratio/phase tap changers, etc.
    Ideally it should have some overloads that can be solved by ToOp

    Returns
    -------
    Network
        The created complex grid network.
    """
    n = pypowsybl.network.create_empty("TESTGRID_NODE_BREAKER_HVDC_BAT_SVC_3W_TRAFO")

    # ---------------------------------------------------------------------
    # 1) Substations
    # ---------------------------------------------------------------------
    substations_df = pd.DataFrame(
        [
            {"id": "S_3W", "name": "S_3W", "tso": "TSO", "country": "BE"},
            {"id": "S_2W_MV_LV", "name": "S_2W_MV_LV", "tso": "TSO", "country": "BE"},
            {"id": "S_LV_load", "name": "S_LV_load", "tso": "TSO", "country": "BE"},
            {"id": "S_MV_load", "name": "S_MV_load", "tso": "TSO", "country": "BE"},
            {"id": "S_MV_svc", "name": "S_MV_svc", "tso": "TSO", "country": "BE"},
            {"id": "S_MV", "name": "S_MV", "tso": "TSO", "country": "BE"},
            {"id": "S_2W_MV_HV", "name": "S_2W_MV_HV", "tso": "TSO", "country": "BE"},
            {"id": "S_HV_gen", "name": "S_2W_HV_gen", "tso": "TSO", "country": "BE"},
            {"id": "S_HV_vsc", "name": "S_HV_vsc", "tso": "TSO", "country": "BE"},
            {"id": "S_DE_1", "name": "S_DE_1", "tso": "TSO", "country": "DE"},
            {"id": "S_DE_2", "name": "S_DE_2", "tso": "TSO", "country": "DE"},
        ]
    ).set_index("id")
    n.create_substations(df=substations_df)

    # ---------------------------------------------------------------------
    # 2) Voltage levels
    # ---------------------------------------------------------------------
    vls_df = pd.DataFrame(
        [
            {
                "id": "VL_3W_HV",
                "name": "VL_3W_HV",
                "substation_id": "S_3W",
                "nominal_v": 380.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_3W_MV",
                "name": "VL_3W_MV",
                "substation_id": "S_3W",
                "nominal_v": 110.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_3W_LV",
                "name": "VL_3W_LV",
                "substation_id": "S_3W",
                "nominal_v": 63.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_2W_MV_LV_MV",
                "name": "VL_2W_MV_LV_MV",
                "substation_id": "S_2W_MV_LV",
                "nominal_v": 110.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_2W_MV_LV_LV",
                "name": "VL_2W_MV_LV_LV",
                "substation_id": "S_2W_MV_LV",
                "nominal_v": 63.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_LV_load",
                "name": "VL_LV_load",
                "substation_id": "S_LV_load",
                "nominal_v": 63.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_MV_load",
                "name": "VL_MV_load",
                "substation_id": "S_MV_load",
                "nominal_v": 110.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_MV_svc",
                "name": "VL_MV_svc",
                "substation_id": "S_MV_svc",
                "nominal_v": 110.0,
                "topology_kind": "NODE_BREAKER",
            },
            {"id": "VL_MV", "name": "VL_MV", "substation_id": "S_MV", "nominal_v": 110.0, "topology_kind": "NODE_BREAKER"},
            {
                "id": "VL_2W_MV_HV_MV",
                "name": "VL_2W_MV_HV_MV",
                "substation_id": "S_2W_MV_HV",
                "nominal_v": 110.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_2W_MV_HV_HV",
                "name": "VL_2W_MV_HV_HV",
                "substation_id": "S_2W_MV_HV",
                "nominal_v": 380.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_HV_gen",
                "name": "VL_HV_gen",
                "substation_id": "S_HV_gen",
                "nominal_v": 380.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_HV_vsc",
                "name": "VL_HV_vsc",
                "substation_id": "S_HV_vsc",
                "nominal_v": 380.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_DE_1",
                "name": "VL_DE_1",
                "substation_id": "S_DE_1",
                "nominal_v": 380.0,
                "topology_kind": "NODE_BREAKER",
            },
            {
                "id": "VL_DE_2",
                "name": "VL_DE_2",
                "substation_id": "S_DE_2",
                "nominal_v": 380.0,
                "topology_kind": "NODE_BREAKER",
            },
        ]
    ).set_index("id")
    n.create_voltage_levels(df=vls_df)

    # ---------------------------------------------------------------------
    #  Busbar layouts
    # ---------------------------------------------------------------------
    kwargs_no_layout = {"aligned_buses_or_busbar_count": 1, "section_count": 1, "switch_kinds": ""}
    kwargs_basic_layout = {"aligned_buses_or_busbar_count": 1, "section_count": 2, "switch_kinds": "BREAKER"}
    kwargs_two_busbar_layout = {"aligned_buses_or_busbar_count": 2, "section_count": 1, "switch_kinds": ""}
    kwargs_three_busbar_layout = {"aligned_buses_or_busbar_count": 3, "section_count": 1, "switch_kinds": ""}
    kwargs_four_busbar_layout = {"aligned_buses_or_busbar_count": 2, "section_count": 2, "switch_kinds": "BREAKER"}
    kwargs_four_busbar_disconnector_layout = {
        "aligned_buses_or_busbar_count": 2,
        "section_count": 2,
        "switch_kinds": "DISCONNECTOR",
    }

    no_layout_list = ["VL_LV_load", "VL_DE_1", "VL_DE_2"]
    basic_layout_list = ["VL_2W_MV_LV_LV", "VL_3W_LV"]
    two_busbar_layout_list = ["VL_3W_MV", "VL_2W_MV_LV_MV", "VL_MV_svc", "VL_HV_gen"]
    three_busbar_layout_list = ["VL_2W_MV_HV_MV"]
    four_busbar_layout_list = ["VL_3W_HV", "VL_2W_MV_HV_HV", "VL_HV_vsc"]
    four_busbar_disconnector_layout_list = [
        "VL_MV",
        "VL_MV_load",
    ]

    def _create_busbars(voltage_list: list, kwargs: dict) -> None:
        for vl in voltage_list:
            pypowsybl.network.create_voltage_level_topology(network=n, id=vl, **kwargs)
            if kwargs["aligned_buses_or_busbar_count"] == 2 or kwargs["aligned_buses_or_busbar_count"] == 3:
                pypowsybl.network.create_coupling_device(
                    n,
                    bus_or_busbar_section_id_1=[f"{vl}_1_1"],
                    bus_or_busbar_section_id_2=[f"{vl}_2_1"],
                )

    _create_busbars(no_layout_list, kwargs_no_layout)
    _create_busbars(basic_layout_list, kwargs_basic_layout)
    _create_busbars(two_busbar_layout_list, kwargs_two_busbar_layout)
    _create_busbars(four_busbar_layout_list, kwargs_four_busbar_layout)
    _create_busbars(four_busbar_disconnector_layout_list, kwargs_four_busbar_disconnector_layout)
    _create_busbars(three_busbar_layout_list, kwargs_three_busbar_layout)

    # refine busbar layouts for specific voltage levels
    pypowsybl.network.create_coupling_device(
        n,
        bus_or_busbar_section_id_1=["VL_2W_MV_HV_HV_1_2"],
        bus_or_busbar_section_id_2=["VL_2W_MV_HV_HV_2_2"],
    )

    pypowsybl.network.create_coupling_device(
        n,
        bus_or_busbar_section_id_1=["VL_MV_load_1_2"],
        bus_or_busbar_section_id_2=["VL_MV_load_1_1"],
    )
    n.remove_elements("VL_MV_load_DISCONNECTOR_0_2")
    n.remove_elements("VL_MV_load_DISCONNECTOR_1_3")
    # FIX ME: currently not working due to an importing issue in the simplyfied station function
    # pypowsybl.network.create_coupling_device(
    #     n,
    #     bus_or_busbar_section_id_1=["VL_MV_1_2"],
    #     bus_or_busbar_section_id_2=["VL_MV_2_2"],
    # )
    # pypowsybl.network.create_coupling_device(
    #     n,
    #     bus_or_busbar_section_id_1=["VL_MV_1_1"],
    #     bus_or_busbar_section_id_2=["VL_MV_1_2"],
    # )
    # n.open_switch("VL_MV_DISCONNECTOR_0_2")

    # ---------------------------------------------------------------------
    # 3) AC lines
    # ---------------------------------------------------------------------
    # LV (63 kV)
    lv_short = {"r": 3.5, "x": 9.0, "g1": 0.0, "b1": 7.5586e-06, "g2": 0.0, "b2": 7.5586e-06}
    lv_long = {"r": 5.0, "x": 15.0, "g1": 0.0, "b1": 2.5195e-05, "g2": 0.0, "b2": 2.5195e-05}

    # MV (110 kV)
    mv_short = {"r": 1.8, "x": 5.1, "g1": 0.0, "b1": 3.3058e-06, "g2": 0.0, "b2": 3.3058e-06}
    mv_long = {"r": 4.8, "x": 20.5, "g1": 0.0, "b1": 9.9174e-06, "g2": 0.0, "b2": 9.9174e-06}

    # HV (380 kV)
    hv_short = {"r": 0.8, "x": 8.8, "g1": 0.0, "b1": 3.4626e-07, "g2": 0.0, "b2": 3.4626e-07}
    hv_long = {"r": 1.0, "x": 15.0, "g1": 0.0, "b1": 1.1080e-06, "g2": 0.0, "b2": 1.1080e-06}

    # LV lines
    lv_lines = pd.DataFrame(
        [
            {"bus_or_busbar_section_id_1": "VL_3W_LV_1_1", "bus_or_busbar_section_id_2": "VL_LV_load_1_1", **lv_short},
            {"bus_or_busbar_section_id_1": "VL_2W_MV_LV_LV_1_1", "bus_or_busbar_section_id_2": "VL_LV_load_1_1", **lv_long},
        ]
    )
    lv_lines["position_order_1"] = 1
    lv_lines["position_order_2"] = 1

    # MV lines (first 5 short, rest long based on your list)
    mv_lines = pd.DataFrame(
        [
            {"bus_or_busbar_section_id_1": "VL_MV_svc_1_1", "bus_or_busbar_section_id_2": "VL_3W_MV_1_1", **mv_short},
            {"bus_or_busbar_section_id_1": "VL_MV_svc_1_1", "bus_or_busbar_section_id_2": "VL_2W_MV_HV_MV_1_1", **mv_short},
            {"bus_or_busbar_section_id_1": "VL_2W_MV_LV_MV_1_1", "bus_or_busbar_section_id_2": "VL_3W_MV_1_1", **mv_short},
            {"bus_or_busbar_section_id_1": "VL_MV_load_1_1", "bus_or_busbar_section_id_2": "VL_MV_2_2", **mv_short},
            {"bus_or_busbar_section_id_1": "VL_2W_MV_HV_MV_3_1", "bus_or_busbar_section_id_2": "VL_MV_2_1", **mv_short},
            {"bus_or_busbar_section_id_1": "VL_MV_load_1_1", "bus_or_busbar_section_id_2": "VL_2W_MV_LV_MV_1_1", **mv_long},
            {"bus_or_busbar_section_id_1": "VL_MV_1_1", "bus_or_busbar_section_id_2": "VL_3W_MV_1_1", **mv_long},
            {"bus_or_busbar_section_id_1": "VL_MV_1_2", "bus_or_busbar_section_id_2": "VL_3W_MV_1_1", **mv_long},
            {"bus_or_busbar_section_id_1": "VL_MV_svc_1_1", "bus_or_busbar_section_id_2": "VL_2W_MV_LV_MV_1_1", **mv_long},
            {"bus_or_busbar_section_id_1": "VL_MV_svc_1_1", "bus_or_busbar_section_id_2": "VL_MV_2_1", **mv_long},
            {"bus_or_busbar_section_id_1": "VL_MV_load_1_1", "bus_or_busbar_section_id_2": "VL_2W_MV_HV_MV_1_1", **mv_long},
        ]
    )
    mv_lines["position_order_1"] = 1
    mv_lines["position_order_2"] = 1

    # HV lines (first 4 short, last 4 long)
    hv_lines = pd.DataFrame(
        [
            {"bus_or_busbar_section_id_1": "VL_3W_HV_1_1", "bus_or_busbar_section_id_2": "VL_HV_vsc_1_1", **hv_short},
            {"bus_or_busbar_section_id_1": "VL_3W_HV_2_1", "bus_or_busbar_section_id_2": "VL_HV_vsc_2_1", **hv_short},
            {"bus_or_busbar_section_id_1": "VL_2W_MV_HV_HV_1_2", "bus_or_busbar_section_id_2": "VL_HV_gen_1_1", **hv_short},
            {"bus_or_busbar_section_id_1": "VL_2W_MV_HV_HV_2_2", "bus_or_busbar_section_id_2": "VL_HV_gen_2_1", **hv_short},
            {"bus_or_busbar_section_id_1": "VL_3W_HV_1_1", "bus_or_busbar_section_id_2": "VL_HV_gen_1_1", **hv_long},
            {"bus_or_busbar_section_id_1": "VL_3W_HV_2_1", "bus_or_busbar_section_id_2": "VL_HV_gen_2_1", **hv_long},
            {"bus_or_busbar_section_id_1": "VL_2W_MV_HV_HV_1_1", "bus_or_busbar_section_id_2": "VL_HV_vsc_1_1", **hv_long},
            {"bus_or_busbar_section_id_1": "VL_2W_MV_HV_HV_2_1", "bus_or_busbar_section_id_2": "VL_HV_vsc_2_1", **hv_long},
        ]
    )
    hv_lines["position_order_1"] = 1
    hv_lines["position_order_2"] = 1

    lines = pd.concat([lv_lines, mv_lines, hv_lines], ignore_index=True)
    lines["id"] = [f"L{i + 1}" for i in range(len(lines))]
    lines = lines.set_index("id")
    pypowsybl.network.create_line_bays(n, df=lines)

    n.create_lines(
        id="LINE_out_of_service",
        voltage_level1_id="VL_MV_load",
        node1=50,
        voltage_level2_id="VL_2W_MV_HV_MV",
        node2=50,
        **mv_long,
    )
    n.create_switches(
        id="LINE_out_of_service_BREAKER1",
        name="LINE_out_of_service_BREAKER",
        voltage_level_id="VL_MV_load",
        node1=3,
        node2=50,
        kind="BREAKER",
        open=True,
    )
    n.create_switches(
        id="LINE_out_of_service_BREAKER2",
        name="LINE_out_of_service_BREAKER",
        voltage_level_id="VL_2W_MV_HV_MV",
        node1=1,
        node2=50,
        kind="BREAKER",
        open=True,
    )
    # ---------------------------------------------------------------------
    # 4) Transformers
    # ---------------------------------------------------------------------
    # 2W: 110/63 kV
    pypowsybl.network.create_2_windings_transformer_bays(
        n,
        id="2W_MV_LV",
        b=0.0,
        g=0.0,
        r=0.005,
        x=0.15,
        rated_u1=110.0,
        rated_u2=63.0,
        bus_or_busbar_section_id_1="VL_2W_MV_LV_MV_1_1",
        position_order_1=35,
        direction_1="BOTTOM",
        bus_or_busbar_section_id_2="VL_2W_MV_LV_LV_1_1",
        position_order_2=5,
        direction_2="TOP",
    )

    # 2W: 380/110 kV (two parallel transformers in S_2W_MV_HV)
    pypowsybl.network.create_2_windings_transformer_bays(
        n,
        id="2W_MV_HV_1",
        b=0.0,
        g=0.0,
        r=0.004,
        x=0.12,
        rated_u1=380.0,
        rated_u2=110.0,
        bus_or_busbar_section_id_1="VL_2W_MV_HV_HV_1_2",
        position_order_1=35,
        direction_1="BOTTOM",
        bus_or_busbar_section_id_2="VL_2W_MV_HV_MV_1_1",
        position_order_2=5,
        direction_2="TOP",
    )
    pypowsybl.network.create_2_windings_transformer_bays(
        n,
        id="2W_MV_HV_2",
        b=0.0,
        g=0.0,
        r=0.004,
        x=0.12,
        rated_u1=380.0,
        rated_u2=110.0,
        bus_or_busbar_section_id_1="VL_2W_MV_HV_HV_2_1",
        position_order_1=35,
        direction_1="BOTTOM",
        bus_or_busbar_section_id_2="VL_2W_MV_HV_MV_2_1",
        position_order_2=5,
        direction_2="TOP",
    )

    # 2W inside S_3W to help split flows: 380/110
    pypowsybl.network.create_2_windings_transformer_bays(
        n,
        id="2W_3W_MV_HV",
        b=0.0,
        g=0.0,
        r=0.004,
        x=0.12,
        rated_u1=380.0,
        rated_u2=110.0,
        bus_or_busbar_section_id_1="VL_3W_HV_1_1",
        position_order_1=35,
        direction_1="BOTTOM",
        bus_or_busbar_section_id_2="VL_3W_MV_1_1",
        position_order_2=5,
        direction_2="TOP",
    )

    # 3W: 380/110/63 kV - direct node connections + bay switches around it
    three_w_df = pd.DataFrame(
        [
            {
                "id": "3W",
                "name": "3W 380/110/63",
                "voltage_level1_id": "VL_3W_HV",
                "voltage_level2_id": "VL_3W_MV",
                "voltage_level3_id": "VL_3W_LV",
                "node1": 30,
                "node2": 30,
                "node3": 30,
                "rated_u1": 380.0,
                "rated_u2": 110.0,
                "rated_u3": 63.0,
                "r1": 0.005,
                "x1": 0.15,
                "g1": 0.0,
                "b1": 0.0,
                "r2": 0.005,
                "x2": 0.15,
                "g2": 0.0,
                "b2": 0.0,
                "r3": 0.006,
                "x3": 0.18,
                "g3": 0.0,
                "b3": 0.0,
            }
        ]
    ).set_index("id")
    n.create_3_windings_transformers(three_w_df)

    # Bay switches
    n.create_switches(id="BREAKER_3W_HV", voltage_level_id="VL_3W_HV", node1=30, node2=31, kind="BREAKER", open=False)
    n.create_switches(
        id="DISCONNECTOR_3W_HV_1", voltage_level_id="VL_3W_HV", node1=0, node2=31, kind="DISCONNECTOR", open=False
    )
    n.create_switches(
        id="DISCONNECTOR_3W_HV_2", voltage_level_id="VL_3W_HV", node1=1, node2=31, kind="DISCONNECTOR", open=False
    )
    n.create_switches(id="BREAKER_3W_MV", voltage_level_id="VL_3W_MV", node1=30, node2=31, kind="BREAKER", open=False)
    n.create_switches(
        id="DISCONNECTOR_3W_MV_1", voltage_level_id="VL_3W_MV", node1=0, node2=31, kind="DISCONNECTOR", open=False
    )
    n.create_switches(
        id="DISCONNECTOR_3W_MV_2", voltage_level_id="VL_3W_MV", node1=1, node2=31, kind="DISCONNECTOR", open=False
    )
    n.create_switches(id="BREAKER_3W_LV", voltage_level_id="VL_3W_LV", node1=30, node2=31, kind="BREAKER", open=False)
    n.create_switches(
        id="DISCONNECTOR_3W_LV_1", voltage_level_id="VL_3W_LV", node1=0, node2=31, kind="DISCONNECTOR", open=False
    )

    # ---------------------------------------------------------------------
    # 4) Transformers - PST
    # ---------------------------------------------------------------------
    pypowsybl.network.create_2_windings_transformer_bays(
        n,
        id="2W_MV_HV_PST",
        b=1e-6,
        g=5e-7,
        r=0.1,
        x=12.0,
        rated_u1=110.0,
        rated_u2=110.0,
        bus_or_busbar_section_id_1="VL_2W_MV_HV_MV_1_1",
        position_order_1=50,
        direction_1="BOTTOM",
        bus_or_busbar_section_id_2="VL_2W_MV_HV_MV_3_1",
        position_order_2=50,
        direction_2="BOTTOM",
    )
    ptc_df = pd.DataFrame.from_records(
        index="id",
        columns=["id", "target_deadband", "regulation_mode", "low_tap", "tap"],
        data=[("2W_MV_HV_PST", 2, "CURRENT_LIMITER", -30, -20)],
    )
    taps = np.arange(-30, 31)  # -30 .. 30 inclusive

    # base/min/max values (keep b,g,r,x constant as before, interpolate rho and alpha)
    b_val, g_val, rho_val = 0, 0, 1
    alpha_min, alpha_max = -21.0, 21.0
    x_min, x_max = -30.0, 30.0
    r_min, r_max = -120.0, 120.0

    alphas = np.linspace(alpha_min, alpha_max, len(taps))
    x_vals = np.linspace(x_min, x_max, len(taps))
    r_vals = np.linspace(r_min, r_max, len(taps))

    rows = [
        ("2W_MV_HV_PST", b_val, g_val, r_val, x_val, rho_val, alpha)
        for r_val, x_val, alpha in zip(r_vals, x_vals, alphas, strict=True)
    ]

    steps_df = pd.DataFrame.from_records(data=rows, index="id", columns=["id", "b", "g", "r", "x", "rho", "alpha"])

    n.create_phase_tap_changers(ptc_df, steps_df)
    pypowsybl.network.create_2_windings_transformer_bays(
        n,
        id="MV_load_PST_no_limit",
        b=1e-6,
        g=5e-7,
        r=0.1,
        x=12.0,
        rated_u1=110.0,
        rated_u2=110.0,
        bus_or_busbar_section_id_1="VL_MV_load_1_2",
        position_order_1=50,
        direction_1="BOTTOM",
        bus_or_busbar_section_id_2="VL_MV_load_2_2",
        position_order_2=50,
        direction_2="BOTTOM",
    )
    ptc_df = pd.DataFrame.from_records(
        index="id",
        columns=["id", "target_deadband", "regulation_mode", "low_tap", "tap"],
        data=[("MV_load_PST_no_limit", 2, "CURRENT_LIMITER", -30, -20)],
    )
    taps = np.arange(-30, 31)  # -30 .. 30 inclusive

    # base/min/max values (keep b,g,r,x constant as before, interpolate rho and alpha)
    b_val, g_val, rho_val = 0, 0, 1
    alpha_min, alpha_max = -21.0, 21.0
    x_min, x_max = -30.0, 30.0
    r_min, r_max = -120.0, 120.0

    alphas = np.linspace(alpha_min, alpha_max, len(taps))
    x_vals = np.linspace(x_min, x_max, len(taps))
    r_vals = np.linspace(r_min, r_max, len(taps))

    rows = [
        ("MV_load_PST_no_limit", b_val, g_val, r_val, x_val, rho_val, alpha)
        for r_val, x_val, alpha in zip(r_vals, x_vals, alphas, strict=True)
    ]

    steps_df = pd.DataFrame.from_records(data=rows, index="id", columns=["id", "b", "g", "r", "x", "rho", "alpha"])
    n.create_phase_tap_changers(ptc_df, steps_df)
    # ---------------------------------------------------------------------
    # 5) HVDC
    # ---------------------------------------------------------------------
    # LCC converter stations
    lcc_df = pd.DataFrame(
        [
            {
                "id": "LCC1",
                "name": "LCC station A",
                "power_factor": 0.98,
                "loss_factor": 1.0,
                "bus_or_busbar_section_id": "VL_3W_HV_1_1",
                "position_order": 45,
            },
            {
                "id": "LCC2",
                "name": "LCC station B",
                "power_factor": 0.98,
                "loss_factor": 1.0,
                "bus_or_busbar_section_id": "VL_2W_MV_HV_HV_1_2",
                "position_order": 45,
            },
        ]
    ).set_index("id")
    pypowsybl.network.create_lcc_converter_station_bay(n, df=lcc_df)

    n.create_hvdc_lines(
        id="HVDC_LCC",
        converter_station1_id="LCC1",
        converter_station2_id="LCC2",
        r=1.0,
        nominal_v=380.0,
        converters_mode="SIDE_1_RECTIFIER_SIDE_2_INVERTER",
        max_p=300.0,
        target_p=75.0,
    )

    # VSC converter stations (one on S_3W HV, one on HV_gen)
    vsc_df = pd.DataFrame(
        [
            {
                "id": "VSC_A",
                "name": "VSC A (3W-HV)",
                "loss_factor": 1.0,
                "voltage_regulator_on": True,
                "target_v": 380.0,
                "target_q": 0.0,
                "bus_or_busbar_section_id": "VL_HV_vsc_1_1",
                "position_order": 60,
                "direction": "TOP",
            },
            {
                "id": "VSC_B",
                "name": "VSC B (HV-gen)",
                "loss_factor": 1.0,
                "voltage_regulator_on": True,
                "target_v": 380.0,
                "target_q": 0.0,
                "bus_or_busbar_section_id": "VL_HV_gen_2_1",
                "position_order": 60,
                "direction": "TOP",
            },
        ]
    ).set_index("id")
    pypowsybl.network.create_vsc_converter_station_bay(n, df=vsc_df)

    n.create_hvdc_lines(
        id="HVDC_VSC",
        converter_station1_id="VSC_A",
        converter_station2_id="VSC_B",
        r=1.0,
        nominal_v=380.0,
        converters_mode="SIDE_1_RECTIFIER_SIDE_2_INVERTER",
        max_p=300.0,
        target_p=50.0,  # small transfer to avoid stressing balance
    )

    # ---------------------------------------------------------------------
    # 6) assets: battery, SVC, shunts/reactor, gens, loads, dangling line
    # ---------------------------------------------------------------------
    # Generators (set one big HV gen as main source)
    gens_df = pd.DataFrame(
        [
            {
                "id": "GEN_HV",
                "name": "HV main generator",
                "energy_source": "THERMAL",
                "min_p": 0.0,
                "max_p": 1200.0,
                "target_p": 700.0,
                "voltage_regulator_on": True,
                "target_v": 380.0,
                "bus_or_busbar_section_id": "VL_HV_gen_1_1",
                "position_order": 10,
                "direction": "BOTTOM",
            },
            {
                "id": "GEN_MV",
                "name": "MV local generator",
                "energy_source": "THERMAL",
                "min_p": 0.0,
                "max_p": 80.0,
                "target_p": 30.0,
                "voltage_regulator_on": True,
                "target_v": 110.0,
                "bus_or_busbar_section_id": "VL_MV_svc_1_1",
                "position_order": 10,
                "direction": "BOTTOM",
            },
        ]
    ).set_index("id")
    pypowsybl.network.create_generator_bay(n, df=gens_df)

    # Loads (balanced across VLs; your style)
    loads_df = pd.DataFrame(
        [
            {
                "id": "load_HV_gen",
                "name": "HV interconnection load",
                "p0": 120.0,
                "q0": 40.0,
                "bus_or_busbar_section_id": "VL_HV_gen_2_1",
                "position_order": 20,
                "direction": "BOTTOM",
            },
            {
                "id": "load_HV_vsc",
                "name": "HV local load",
                "p0": 350.0,
                "q0": 120.0,
                "bus_or_busbar_section_id": "VL_HV_vsc_1_1",
                "position_order": 20,
                "direction": "BOTTOM",
            },
            {
                "id": "load_MV",
                "name": "MV interconnection load",
                "p0": 80.0,
                "q0": 20.0,
                "bus_or_busbar_section_id": "VL_MV_1_2",
                "position_order": 20,
                "direction": "BOTTOM",
            },
            {
                "id": "load_MV_load",
                "name": "MV local load",
                "p0": 150.0,
                "q0": 60.0,
                "bus_or_busbar_section_id": "VL_MV_load_1_1",
                "position_order": 20,
                "direction": "BOTTOM",
            },
            {
                "id": "load_VL_MV_svc",
                "name": "LV local load 2",
                "p0": 200.0,
                "q0": 200.0,
                "bus_or_busbar_section_id": "VL_MV_svc_1_1",
                "position_order": 30,
                "direction": "BOTTOM",
            },
            {
                "id": "load_2W_MV_LV_LV",
                "name": "LV interconnection load",
                "p0": 30.0,
                "q0": 10.0,
                "bus_or_busbar_section_id": "VL_2W_MV_LV_LV_1_1",
                "position_order": 20,
                "direction": "BOTTOM",
            },
            {
                "id": "load_LV_load",
                "name": "LV local load 1",
                "p0": 90.0,
                "q0": 30.0,
                "bus_or_busbar_section_id": "VL_LV_load_1_1",
                "position_order": 20,
                "direction": "BOTTOM",
            },
            {
                "id": "load_3W_LV",
                "name": "LV local load 2",
                "p0": 25.0,
                "q0": 8.0,
                "bus_or_busbar_section_id": "VL_3W_LV_1_1",
                "position_order": 30,
                "direction": "BOTTOM",
            },
        ]
    ).set_index("id")
    pypowsybl.network.create_load_bay(n, df=loads_df)

    # Batteries (MV + LV)
    bat_df = pd.DataFrame(
        [
            {
                "id": "BAT_MV",
                "name": "MV battery",
                "min_p": -60.0,
                "max_p": 60.0,
                "bus_or_busbar_section_id": "VL_MV_2_1",
                "position_order": 30,
                "direction": "TOP",
                "target_p": -20.0,
                "target_q": 0.0,
            },
            {
                "id": "BAT_LV",
                "name": "LV battery",
                "min_p": -60.0,
                "max_p": 60.0,
                "bus_or_busbar_section_id": "VL_LV_load_1_1",
                "position_order": 30,
                "direction": "TOP",
                "target_p": 30.0,
                "target_q": 0.0,
            },
        ]
    ).set_index("id")
    pypowsybl.network.create_battery_bay(n, df=bat_df)

    # Shunt capacitor and reactor (inductor) - keep one of each on HV side
    # --- SHUNT BAY DEFINITIONS ---
    shunt_df = pd.DataFrame(
        [
            {
                "id": "SHUNT_HV_CAP",
                "model_type": "LINEAR",
                "section_count": 1,
                "target_v": 380.0,
                "target_deadband": 2.0,
                "bus_or_busbar_section_id": "VL_HV_gen_1_1",
                "position_order": 40,
            },
            {
                "id": "SHUNT_MV_svc",
                "model_type": "LINEAR",
                "section_count": 1,
                "target_v": 110.0,
                "target_deadband": 2.0,
                "bus_or_busbar_section_id": "VL_MV_svc_1_1",
                "position_order": 50,
            },
            {
                "id": "SHUNT_MV",
                "model_type": "LINEAR",
                "section_count": 1,
                "target_v": 110.0,
                "target_deadband": 2.0,
                "bus_or_busbar_section_id": "VL_MV_1_1",
                "position_order": 40,
            },
        ]
    ).set_index("id")

    # --- LINEAR MODEL DEFINITIONS ---
    linear_model_df = pd.DataFrame(
        [
            {"id": "SHUNT_HV_CAP", "g_per_section": 0.0, "b_per_section": 0.0020, "max_section_count": 1},
            {"id": "SHUNT_MV_svc", "g_per_section": 0.0, "b_per_section": 0.0020, "max_section_count": 1},
            {"id": "SHUNT_MV", "g_per_section": 0.0, "b_per_section": 0.0012, "max_section_count": 1},
        ]
    ).set_index("id")

    # --- CREATE SHUNTS ---
    pypowsybl.network.create_shunt_compensator_bay(
        n,
        shunt_df=shunt_df,
        linear_model_df=linear_model_df,
    )

    # STATCOM (SVC) - IMPORTANT: regulating=True
    svc_df = pd.DataFrame(
        [
            {
                "id": "STATCOM_HV",
                "name": "HV STATCOM",
                "b_min": -0.01,
                "b_max": 0.01,
                "regulation_mode": "VOLTAGE",
                "target_v": 110.0,
                "regulating": True,
                "bus_or_busbar_section_id": "VL_MV_svc_1_1",
                "position_order": 55,
                "direction": "TOP",
            },
        ]
    ).set_index("id")
    pypowsybl.network.create_static_var_compensator_bay(n, df=svc_df)

    dangling_df = pd.DataFrame(
        [
            {
                "id": "Dangling_inbound",
                "name": "Dangling inbound",
                "p0": -300,
                "q0": -100,
                "r": hv_long["r"],
                "x": hv_long["x"],
                "g": hv_long["g1"],
                "b": hv_long["b1"],
                "bus_or_busbar_section_id": "VL_2W_MV_HV_HV_1_1",
                "position_order": 60,
                "direction": "BOTTOM",
            },
            {
                "id": "Dangling_outbound",
                "name": "Dangling outbound",
                "p0": 300,
                "q0": 100,
                "r": hv_long["r"],
                "x": hv_long["x"],
                "g": hv_long["g1"],
                "b": hv_long["b1"],
                "bus_or_busbar_section_id": "VL_3W_HV_1_1",
                "position_order": 60,
                "direction": "TOP",
            },
        ]
    ).set_index("id")

    pypowsybl.network.create_dangling_line_bay(network=n, df=dangling_df)

    # line limits
    limits = pd.DataFrame.from_records(
        data=[
            {
                "element_id": "L14",
                "value": 200,
                "side": "ONE",
                "name": "permanent",
                "type": "CURRENT",
                "acceptable_duration": -1,
            },
            {
                "element_id": "L15",
                "value": 200,
                "side": "ONE",
                "name": "permanent",
                "type": "CURRENT",
                "acceptable_duration": -1,
            },
            {
                "element_id": "L16",
                "value": 400,
                "side": "ONE",
                "name": "permanent",
                "type": "CURRENT",
                "acceptable_duration": -1,
            },
            {
                "element_id": "L7",
                "value": 400,
                "side": "ONE",
                "name": "permanent",
                "type": "CURRENT",
                "acceptable_duration": -1,
            },
        ],
        index="element_id",
    )

    # Set Slack bus
    slack_voltage_id = "VL_HV_gen"
    slack_bus_id = "VL_HV_gen_0"
    dict_slack = {"voltage_level_id": slack_voltage_id, "bus_id": slack_bus_id}
    pypowsybl.network.Network.create_extensions(n, extension_name="slackTerminal", **dict_slack)

    pypowsybl.loadflow.run_ac(n)
    i1 = abs(n.get_lines()["i1"])
    i1_arr = np.asarray(i1, dtype=float)
    rounded_i1 = (np.ceil(i1_arr / 100) * 100).astype(int)
    limits = pd.Series(rounded_i1, index=i1.index, name="value").reset_index()
    limits.rename(columns={"id": "element_id"}, inplace=True)
    limits.set_index("element_id", inplace=True)
    limits["side"] = "ONE"
    limits["name"] = "permanent"
    limits["type"] = "CURRENT"
    limits["acceptable_duration"] = -1
    limits.loc[limits["value"] < 0, "value"] = 1000
    n.create_operational_limits(limits)

    # transformer limits
    i1 = abs(n.get_2_windings_transformers()["i1"])
    i1_arr = np.asarray(i1, dtype=float)
    rounded_i1 = (np.ceil(i1_arr / 100) * 100).astype(int)
    limits_tr = pd.Series(rounded_i1, index=n.get_2_windings_transformers().index, name="value").reset_index()
    limits_tr.rename(columns={"id": "element_id"}, inplace=True)
    limits_tr.set_index("element_id", inplace=True)
    limits_tr["side"] = "ONE"
    limits_tr["name"] = "permanent"
    limits_tr["type"] = "CURRENT"
    limits_tr["acceptable_duration"] = -1
    limits_tr.loc[limits_tr["value"] < 0, "value"] = 1000
    # delete no limit trfs
    t_ids = ["MV_load_PST_no_limit"]
    limits_tr = limits_tr.drop(t_ids)
    n.create_operational_limits(limits_tr)

    return n

create_complex_substation_layout_grid #

create_complex_substation_layout_grid()

Create a simplified complex substation-layout network using bay helpers.

This version uses create_voltage_level_topology, create_line_bays, create_2_windings_transformer_bays and create_coupling_device to build the same conceptual layout (two aligned busbars with three sections and three couplers) in a lot fewer lines of code.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def create_complex_substation_layout_grid() -> Network:
    """Create a simplified complex substation-layout network using bay helpers.

    This version uses `create_voltage_level_topology`, `create_line_bays`,
    `create_2_windings_transformer_bays` and `create_coupling_device` to build
    the same conceptual layout (two aligned busbars with three sections and
    three couplers) in a lot fewer lines of code.
    """
    net = pypowsybl.network.create_empty("TEST_COMPLEX_SUBSTATION_LAYOUT_V2")

    # Substations
    net.create_substations(
        id=["S1", "S2", "S3"], name=["Substation 1", "Substation 2", "Substation 3"], country=["DE", "DE", "DE"]
    )

    # Voltage levels
    # VL1: complex node-breaker with 2 aligned busbars and 3 sections each (replicates B1..B6)
    net.create_voltage_levels(
        id=["VL1", "VL2", "VL3", "VL4", "VL5"],
        substation_id=["S1", "S2", "S1", "S3", "S2"],
        nominal_v=[380.0, 380.0, 220.0, 380.0, 220.0],
        topology_kind=["NODE_BREAKER"] * 5,
        name=["VL1", "VL2", "VL3", "VL4", "VL5"],
    )

    # Create topology layouts for the voltage levels. For VL1 we want two aligned
    # busbars with three sections each (3 sections x 2 aligned -> 6 busbar sections)
    pypowsybl.network.create_voltage_level_topology(
        network=net, id="VL1", aligned_buses_or_busbar_count=3, section_count=2, switch_kinds="DISCONNECTOR"
    )
    pypowsybl.network.create_voltage_level_topology(
        network=net, id="VL2", aligned_buses_or_busbar_count=2, section_count=1, switch_kinds=""
    )

    # For the other voltage levels use simple single-busbar layouts
    for vl in ["VL3", "VL4", "VL5"]:
        pypowsybl.network.create_voltage_level_topology(
            network=net, id=vl, aligned_buses_or_busbar_count=1, section_count=1, switch_kinds=""
        )

    # Create three coupling devices (couplers) between corresponding sections of the
    # two aligned busbars in VL1. This models the three couplers in the original function.
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["VL1_1_1"], bus_or_busbar_section_id_2=["VL1_2_1"]
    )
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["VL1_1_2"], bus_or_busbar_section_id_2=["VL1_3_2"]
    )
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["VL1_1_2"], bus_or_busbar_section_id_2=["VL1_2_1"]
    )
    # VL2
    pypowsybl.network.create_coupling_device(
        net, bus_or_busbar_section_id_1=["VL2_1_1"], bus_or_busbar_section_id_2=["VL2_2_1"]
    )

    # Lines: create a small set of line bays connecting VL1 sections to other VLs.
    lines = pd.DataFrame(
        [
            {"bus_or_busbar_section_id_1": "VL1_2_1", "bus_or_busbar_section_id_2": "VL2_1_1", "r": 0.1, "x": 0.2},
            {"bus_or_busbar_section_id_1": "VL1_1_2", "bus_or_busbar_section_id_2": "VL2_1_1", "r": 0.15, "x": 0.25},
            {"bus_or_busbar_section_id_1": "VL1_3_1", "bus_or_busbar_section_id_2": "VL4_1_1", "r": 0.2, "x": 0.3},
            {"bus_or_busbar_section_id_1": "VL1_3_2", "bus_or_busbar_section_id_2": "VL4_1_1", "r": 0.25, "x": 0.35},
        ]
    )
    lines["g1"] = 0.0
    lines["b1"] = 0.0
    lines["g2"] = 0.0
    lines["b2"] = 0.0
    lines["position_order_1"] = 1
    lines["position_order_2"] = 1
    lines["direction_1"] = "TOP"
    lines["direction_2"] = "TOP"
    lines["id"] = [f"L{i + 1}" for i in range(len(lines))]
    lines = lines.set_index("id")
    pypowsybl.network.create_line_bays(net, df=lines)

    # Transformers (use bay helper to create simpler transformer representation)
    pypowsybl.network.create_2_windings_transformer_bays(
        net,
        id="T1",
        b=1e-6,
        g=1e-6,
        r=0.5,
        x=10.0,
        rated_u1=380.0,
        rated_u2=220.0,
        bus_or_busbar_section_id_1="VL1_2_2",
        position_order_1=10,
        direction_1="TOP",
        bus_or_busbar_section_id_2="VL3_1_1",
        position_order_2=5,
        direction_2="TOP",
    )
    pypowsybl.network.create_2_windings_transformer_bays(
        net,
        id="T2",
        b=1e-6,
        g=1e-6,
        r=0.6,
        x=12.0,
        rated_u1=380.0,
        rated_u2=220.0,
        bus_or_busbar_section_id_1="VL1_1_1",
        position_order_1=10,
        direction_1="TOP",
        bus_or_busbar_section_id_2="VL3_1_1",
        position_order_2=5,
        direction_2="TOP",
    )
    pypowsybl.network.create_2_windings_transformer_bays(
        net,
        id="T3",
        b=1e-6,
        g=1e-6,
        r=0.5,
        x=10.0,
        rated_u1=380.0,
        rated_u2=220.0,
        bus_or_busbar_section_id_1="VL2_1_1",
        position_order_1=10,
        direction_1="TOP",
        bus_or_busbar_section_id_2="VL5_1_1",
        position_order_2=5,
        direction_2="TOP",
    )
    pypowsybl.network.create_2_windings_transformer_bays(
        net,
        id="T4",
        b=1e-6,
        g=1e-6,
        r=0.6,
        x=12.0,
        rated_u1=380.0,
        rated_u2=220.0,
        bus_or_busbar_section_id_1="VL2_1_1",
        position_order_1=10,
        direction_1="TOP",
        bus_or_busbar_section_id_2="VL5_1_1",
        position_order_2=5,
        direction_2="TOP",
    )

    # # A couple of generators/loads to make the network usable in tests
    pypowsybl.network.create_generator_bay(
        net,
        id="Gen1",
        max_p=1000.0,
        min_p=0.0,
        target_p=500.0,
        voltage_regulator_on=True,
        target_v=230.0,
        bus_or_busbar_section_id="VL3_1_1",
        position_order=1,
        direction="TOP",
    )
    pypowsybl.network.create_load_bay(
        net, id="Load1", bus_or_busbar_section_id="VL5_1_1", p0=200.0, q0=30.0, position_order=1, direction="TOP"
    )
    pypowsybl.network.create_load_bay(
        net, id="Load2", bus_or_busbar_section_id="VL4_1_1", p0=250.0, q0=20.0, position_order=1, direction="TOP"
    )

    return net

three_node_pst_example #

three_node_pst_example()

Creates a 3 node example grid with 2 PSTs in it

If all N-1 branch cases are computed, there is a limit violation on the BC lines in the default setting (tap 0). However, by changing the tap to -12 (AC) or -10 (DC), the violations can be healed.

This grid can be used to test tap optimization algorithms, which ideally should find a tap that can resolve the problem.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/example_grids.py
def three_node_pst_example() -> Network:
    """Creates a 3 node example grid with 2 PSTs in it

    If all N-1 branch cases are computed, there is a limit violation on the BC lines in the default setting (tap 0).
    However, by changing the tap to -12 (AC) or -10 (DC), the violations can be healed.

    This grid can be used to test tap optimization algorithms, which ideally should find a tap that can resolve the problem.
    """
    # Create an empty network
    net = pypowsybl.network.create_empty(network_id="three_node_pst_example")

    # Create substations
    net.create_substations(id=["SUB_A", "SUB_B", "SUB_C"], country=["BE", "BE", "BE"])

    # Create voltage levels for each substation
    net.create_voltage_levels(
        id=["VL_A", "VL_B", "VL_C"],
        substation_id=["SUB_A", "SUB_B", "SUB_C"],
        topology_kind=["BUS_BREAKER", "BUS_BREAKER", "BUS_BREAKER"],
        nominal_v=[380.0, 380.0, 380.0],
    )

    # Create buses
    net.create_buses(id=["BUS_A", "BUS_B", "BUS_C"], voltage_level_id=["VL_A", "VL_B", "VL_C"])

    # Add generators at stations A and B
    net.create_generators(
        id=["GEN_A", "GEN_B"],
        voltage_level_id=["VL_A", "VL_B"],
        bus_id=["BUS_A", "BUS_B"],
        target_p=[100.0, 100.0],
        target_v=[400.0, 400.0],
        min_p=[0.0, 0.0],
        max_p=[200.0, 200.0],
        voltage_regulator_on=[True, True],
    )

    # Add load at station C
    net.create_loads(id=["LOAD_C"], voltage_level_id=["VL_C"], bus_id=["BUS_C"], p0=[150.0], q0=[50.0])

    # Connect stations with 2 pairs of lines each
    # A to B: 2 lines
    net.create_lines(
        id=["LINE_AB_1", "LINE_AB_2"],
        voltage_level1_id=["VL_A", "VL_A"],
        bus1_id=["BUS_A", "BUS_A"],
        voltage_level2_id=["VL_B", "VL_B"],
        bus2_id=["BUS_B", "BUS_B"],
        r=[0.5, 0.5],
        x=[5.0, 5.0],
        b1=[0.0, 0.0],
        b2=[0.0, 0.0],
    )

    # B to C: 2 lines
    net.create_lines(
        id=["LINE_BC_1", "LINE_BC_2"],
        voltage_level1_id=["VL_B", "VL_B"],
        bus1_id=["BUS_B", "BUS_B"],
        voltage_level2_id=["VL_C", "VL_C"],
        bus2_id=["BUS_C", "BUS_C"],
        r=[0.5, 0.5],
        x=[5.0, 5.0],
        b1=[0.0, 0.0],
        b2=[0.0, 0.0],
    )

    # A to C: 2 lines
    net.create_lines(
        id=["LINE_AC_1", "LINE_AC_2"],
        voltage_level1_id=["VL_A", "VL_A"],
        bus1_id=["BUS_A", "BUS_A"],
        voltage_level2_id=["VL_C", "VL_C"],
        bus2_id=["BUS_C", "BUS_C"],
        r=[0.5, 0.5],
        x=[5.0, 5.0],
        b1=[0.0, 0.0],
        b2=[0.0, 0.0],
    )

    # Create current limits - AB and AC lines have high capacity (1000 A), BC lines are constrained (150 A)
    net.create_operational_limits(
        element_id=["LINE_AB_1", "LINE_AB_2", "LINE_AC_1", "LINE_AC_2", "LINE_BC_1", "LINE_BC_2"],
        element_type=["LINE", "LINE", "LINE", "LINE", "LINE", "LINE"],
        side=["ONE", "ONE", "ONE", "ONE", "ONE", "ONE"],
        name=["permanent_AB1", "permanent_AB2", "permanent_AC1", "permanent_AC2", "permanent_BC1", "permanent_BC2"],
        type=["CURRENT", "CURRENT", "CURRENT", "CURRENT", "CURRENT", "CURRENT"],
        value=[1000.0, 1000.0, 1000.0, 1000.0, 50.0, 50.0],
        acceptable_duration=[-1, -1, -1, -1, -1, -1],
    )

    add_phaseshift_transformer_to_line_powsybl(net, line_idx="LINE_BC_1", tap_min=-30, tap_max=30, tap_step_degree=0.01)

    add_phaseshift_transformer_to_line_powsybl(net, line_idx="LINE_BC_2", tap_min=-30, tap_max=30, tap_step_degree=0.01)

    return net

toop_engine_grid_helpers.powsybl.loadflow_parameters #

A collection of Powsybl parameters used in the Loadflow Solver.

OPENLOADFLOW_PARAM_PF module-attribute #

OPENLOADFLOW_PARAM_PF = {
    "voltageInitModeOverride": "VOLTAGE_MAGNITUDE",
    "reactiveLimitsMaxPpvSwitch": "0",
    "svcVoltageMonitoring": "false",
    "maxActivePowerMismatch": "0.0001",
    "maxReactivePowerMismatch": "0.00001",
    "maxNewtonRaphsonIterations": "25",
    "newtonRaphsonStoppingCriteriaType": "PER_EQUATION_TYPE_CRITERIA",
    "generatorReactivePowerRemoteControl": "true",
    "plausibleActivePowerLimit": "300",
    "useActiveLimits": "false",
    "minPlausibleTargetVoltage": "0.893",
    "maxPlausibleTargetVoltage": "1.105",
}

POWSYBL_LOADFLOW_PARAM_PF module-attribute #

POWSYBL_LOADFLOW_PARAM_PF = Parameters(
    balance_type=PROPORTIONAL_TO_GENERATION_P_MAX,
    connected_component_mode=MAIN,
    countries_to_balance=None,
    dc_power_factor=1.0,
    dc_use_transformer_ratio=True,
    distributed_slack=True,
    phase_shifter_regulation_on=False,
    provider_parameters=OPENLOADFLOW_PARAM_PF,
    read_slack_bus=True,
    shunt_compensator_voltage_control_on=False,
    transformer_voltage_control_on=False,
    use_reactive_limits=True,
    twt_split_shunt_admittance=False,
    voltage_init_mode=PREVIOUS_VALUES,
    write_slack_bus=True,
)

SDL_PARAM module-attribute #

SDL_PARAM = SldParameters(
    use_name=True,
    component_library="Convergence",
    nodes_infos=True,
    display_current_feeder_info=True,
)

NAD_PARAM module-attribute #

NAD_PARAM = NadParameters(
    edge_info_along_edge=True,
    substation_description_displayed=True,
)

DISTRIBUTED_SLACK module-attribute #

DISTRIBUTED_SLACK = Parameters(
    distributed_slack=True,
    balance_type=PROPORTIONAL_TO_GENERATION_P,
    voltage_init_mode=DC_VALUES,
    provider_parameters={
        "slackDistributionFailureBehavior": "LEAVE_ON_SLACK_BUS"
    },
    dc_use_transformer_ratio=True,
)

SINGLE_SLACK module-attribute #

SINGLE_SLACK = Parameters(
    distributed_slack=False, dc_use_transformer_ratio=True
)

toop_engine_grid_helpers.powsybl.powsybl_asset_topo #

Module contains functions to translate the powsybl model to the asset topology model.

File: asset_topology.py Author: Benjamin Petrick Created: 2024-09-18

logger module-attribute #

logger = Logger(__name__)

get_all_element_names #

get_all_element_names(
    network, line_trafo_name_col="elementName"
)

Get the names of all injections and branches in the network.

For trafo and line -> elementName For the rest -> name

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

line_trafo_name_col

Column name for the element names of lines and trafos

TYPE: str DEFAULT: 'elementName'

RETURNS DESCRIPTION
all_names

Series with the names of all injections and branches in the network and their ids as index

TYPE: Series

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_all_element_names(network: Network, line_trafo_name_col: str = "elementName") -> pd.Series:
    """Get the names of all injections and branches in the network.

    For trafo and line -> elementName
    For the rest -> name

    Parameters
    ----------
    network: Network
        pypowsybl network object
    line_trafo_name_col: str
        Column name for the element names of lines and trafos

    Returns
    -------
    all_names: pd.Series
        Series with the names of all injections and branches in the network and their ids as index
    """
    line_names = network.get_lines(attributes=[line_trafo_name_col])[line_trafo_name_col]
    trafo_names = network.get_2_windings_transformers(attributes=[line_trafo_name_col])[line_trafo_name_col]
    trafo_3w_names = network.get_3_windings_transformers(attributes=["name"]).name
    shunt_compensator_names = network.get_shunt_compensators(attributes=["name"]).name
    generator_names = network.get_generators(attributes=["name"]).name
    load_names = network.get_loads(attributes=["name"]).name
    dangling_line_names = network.get_dangling_lines(attributes=["name"]).name
    tie_line_names = network.get_tie_lines(attributes=["name"]).name
    all_names = pd.concat(
        [
            line_names,
            trafo_names,
            trafo_3w_names,
            generator_names,
            load_names,
            dangling_line_names,
            tie_line_names,
            shunt_compensator_names,
        ]
    )
    return all_names

get_asset_switching_table #

get_asset_switching_table(station_buses, station_elements)

Get the asset switching table, which holds the switching of each asset to each busbar.

PARAMETER DESCRIPTION
station_buses

DataFrame with the station busbars Note: The DataFrame is expected be sorted by its "int_id".

TYPE: DataFrame

station_elements

DataFrame with the injections and branches at the station Note: The DataFrame is expected to have a column "bus_int_id" which holds the busbar id for each asset.

TYPE: DataFrame

RETURNS DESCRIPTION
switching_matrix

Switching matrix with the shape (n_bus, n_asset) where n_bus is the number of busbars and n_asset is the number of assets.

TYPE: ndarray

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_asset_switching_table(station_buses: pd.DataFrame, station_elements: pd.DataFrame) -> np.ndarray:
    """Get the asset switching table, which holds the switching of each asset to each busbar.

    Parameters
    ----------
    station_buses: pd.DataFrame
        DataFrame with the station busbars
        Note: The DataFrame is expected be sorted by its "int_id".
    station_elements: pd.DataFrame
        DataFrame with the injections and branches at the station
        Note: The DataFrame is expected to have a column "bus_int_id" which holds the busbar id for each asset.

    Returns
    -------
    switching_matrix: np.ndarray
        Switching matrix with the shape (n_bus, n_asset) where n_bus is the number of busbars
        and n_asset is the number of assets.
    """
    assert station_buses["int_id"].is_monotonic_increasing, "station_buses not sorted"
    station_buses_ranged = station_buses.copy().reset_index(drop=True)

    n_bus = station_buses.shape[0]
    n_asset = station_elements.shape[0]
    switching_matrix = np.zeros((n_bus, n_asset), dtype=bool)

    for asset_idx, bus_id in enumerate(station_elements["bus_int_id"]):
        # if asset is not connected -> -1
        if bus_id != -1:
            bus_idx = station_buses_ranged[station_buses_ranged["int_id"] == bus_id].index[0]
            switching_matrix[bus_idx, asset_idx] = True

    return switching_matrix

get_list_of_coupler_from_df #

get_list_of_coupler_from_df(coupler_elements)

Get the list of coupler elements from the DataFrame.

PARAMETER DESCRIPTION
coupler_elements

DataFrame with the coupler elements Note: datatype of columns is expected to be the same as in the pydantic model.

TYPE: DataFrame

RETURNS DESCRIPTION
coupler_list

List of coupler elements.

TYPE: list[BusbarCoupler]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_list_of_coupler_from_df(coupler_elements: pd.DataFrame) -> list[BusbarCoupler]:
    """Get the list of coupler elements from the DataFrame.

    Parameters
    ----------
    coupler_elements: pd.DataFrame
        DataFrame with the coupler elements
        Note: datatype of columns is expected to be the same as in the pydantic model.

    Returns
    -------
    coupler_list: list[BusbarCoupler]
        List of coupler elements.
    """
    coupler_dict = coupler_elements.to_dict(orient="records")
    coupler_list = [BusbarCoupler(**coupler) for coupler in coupler_dict]
    return coupler_list

get_list_of_switchable_assets_from_df #

get_list_of_switchable_assets_from_df(
    station_branches,
    asset_bay_list=None,
    asset_bay_dict=None,
)

Get the list of switchable assets from the DataFrame.

PARAMETER DESCRIPTION
station_branches

DataFrame with the switchable assets Note: datatype of columns is expected to be the same as in the pydantic model.

TYPE: DataFrame

asset_bay_list

List of asset bays. Note: The list is expected to have the same length as the station_branches.

TYPE: Optional[list[AssetBay]] DEFAULT: None

asset_bay_dict

Dictionary of asset bays with the asset grid_model_id as key.

TYPE: Optional[dict[str, AssetBay]] DEFAULT: None

RETURNS DESCRIPTION
switchable_assets_list

List of switchable assets.

TYPE: list[SwitchableAsset]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_list_of_switchable_assets_from_df(
    station_branches: pd.DataFrame,
    asset_bay_list: Optional[list[AssetBay]] = None,
    asset_bay_dict: Optional[dict[str, AssetBay]] = None,
) -> list[SwitchableAsset]:
    """Get the list of switchable assets from the DataFrame.

    Parameters
    ----------
    station_branches: pd.DataFrame
        DataFrame with the switchable assets
        Note: datatype of columns is expected to be the same as in the pydantic model.
    asset_bay_list: Optional[list[AssetBay]]
        List of asset bays.
        Note: The list is expected to have the same length as the station_branches.
    asset_bay_dict: Optional[dict[str, AssetConnectionPath]]
        Dictionary of asset bays with the asset grid_model_id as key.

    Returns
    -------
    switchable_assets_list: list[SwitchableAsset]
        List of switchable assets.
    """
    switchable_assets_dict = station_branches.to_dict(orient="records")
    if asset_bay_list is not None:
        for index, _ in enumerate(switchable_assets_dict):
            switchable_assets_dict[index]["asset_bay"] = asset_bay_list[index]
    elif asset_bay_dict is not None:
        for index, asset in enumerate(switchable_assets_dict):
            if asset["grid_model_id"] in asset_bay_dict:
                switchable_assets_dict[index]["asset_bay"] = asset_bay_dict[asset["grid_model_id"]]
    switchable_assets_list = [SwitchableAsset(**switchable_asset) for switchable_asset in switchable_assets_dict]

    return switchable_assets_list

get_list_of_busbars_from_df #

get_list_of_busbars_from_df(station_buses)

Get the list of busbars from the DataFrame.

PARAMETER DESCRIPTION
station_buses

DataFrame with the busbars Note: datatype of columns is expected to be the same as in the pydantic model.

TYPE: DataFrame

RETURNS DESCRIPTION
busbar_list

List of busbars.

TYPE: list[Busbar]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_list_of_busbars_from_df(station_buses: pd.DataFrame) -> list[Busbar]:
    """Get the list of busbars from the DataFrame.

    Parameters
    ----------
    station_buses: pd.DataFrame
        DataFrame with the busbars
        Note: datatype of columns is expected to be the same as in the pydantic model.

    Returns
    -------
    busbar_list: list[Busbar]
        List of busbars.
    """
    busbar_dict = station_buses.to_dict(orient="records")
    busbar_list = [Busbar(**busbar) for busbar in busbar_dict]

    return busbar_list

get_bus_info_from_topology #

get_bus_info_from_topology(station_buses, bus_id)

Get the info for all busbars that are part of the bus.

PARAMETER DESCRIPTION
station_buses

Dataframe with all busbars of the station and which bus they are connected to. Comes from BusBreakerTopology.buses

TYPE: DataFrame

bus_id

Bus id for which the buses should be retrieved.

TYPE: str

RETURNS DESCRIPTION
station_buses

DataFrame with the busbars of the specified bus. Note: The DataFrame columns are the same as in the pydantic model.

TYPE: DataFrame

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_bus_info_from_topology(station_buses: pd.DataFrame, bus_id: str) -> pd.DataFrame:
    """Get the info for all busbars that are part of the bus.

    Parameters
    ----------
    station_buses: pd.DataFrame
        Dataframe with all busbars of the station and which bus they are connected to.
        Comes from BusBreakerTopology.buses
    bus_id: str
        Bus id for which the buses should be retrieved.

    Returns
    -------
    station_buses: pd.DataFrame
        DataFrame with the busbars of the specified bus.
        Note: The DataFrame columns are the same as in the pydantic model.
    """
    station_buses = station_buses[station_buses["bus_id"] == bus_id].copy()
    # for UCTE model: if not in service, asset will not appear
    station_buses["in_service"] = True

    # get bus df
    station_buses = (
        station_buses.sort_index().reset_index().reset_index().rename(columns={"index": "int_id", "id": "grid_model_id"})
    )
    station_buses = station_buses[["grid_model_id", "name", "int_id", "in_service"]]

    return station_buses

get_coupler_info_from_topology #

get_coupler_info_from_topology(
    station_switches, switches_df, station_buses
)

Get the coupler elements that are connected to the busbars of the station.

PARAMETER DESCRIPTION
station_switches

Dataframe of all switches at the station and which busbars they connect Comes from BusBreakerTopology.switches

TYPE: DataFrame

switches_df

DataFrame of all switches in the network

TYPE: DataFrame

station_buses

Formatted dataframe of all busbars at the station.

TYPE: DataFrame

RETURNS DESCRIPTION
coupler_elements

DataFrame with the coupler elements of the station. Note: The DataFrame columns are the same as in the pydantic model.

TYPE: DataFrame

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_coupler_info_from_topology(
    station_switches: pd.DataFrame, switches_df: pd.DataFrame, station_buses: pd.DataFrame
) -> pd.DataFrame:
    """Get the coupler elements that are connected to the busbars of the station.

    Parameters
    ----------
    station_switches: pd.DataFrame
        Dataframe of all switches at the station and which busbars they connect
        Comes from BusBreakerTopology.switches
    switches_df: pd.DataFrame
        DataFrame of all switches in the network
    station_buses: pd.DataFrame
        Formatted dataframe of all busbars at the station.

    Returns
    -------
    coupler_elements: pd.DataFrame
        DataFrame with the coupler elements of the station.
        Note: The DataFrame columns are the same as in the pydantic model.
    """
    # get the coupler elements information
    coupler_elements = station_switches.merge(switches_df, how="left", left_index=True, right_index=True)
    # for UCTE model: if not in service, asset will not appear
    coupler_elements["in_service"] = True
    coupler_elements.reset_index(inplace=True)
    # rename the columns to match the pydantic model
    coupler_elements.rename(
        columns={
            "kind": "type",
            "bus1_id": "busbar_from_id",
            "bus2_id": "busbar_to_id",
            "id": "grid_model_id",
        },
        inplace=True,
    )
    # get the busbar ids
    merged_df = pd.merge(
        coupler_elements,
        station_buses,
        left_on="busbar_from_id",
        right_on="grid_model_id",
        how="left",
    )
    coupler_elements["busbar_from_id"] = merged_df["int_id"]
    merged_df = pd.merge(
        coupler_elements,
        station_buses,
        left_on="busbar_to_id",
        right_on="grid_model_id",
        how="left",
    )
    coupler_elements["busbar_to_id"] = merged_df["int_id"]

    return coupler_elements.dropna()

get_name_of_station_elements #

get_name_of_station_elements(
    station_elements, element_names
)

Attach the name of the elements to the station elements.

PARAMETER DESCRIPTION
station_elements

DataFrame with the station elements Comes from BusBreakerTopology.elements

TYPE: DataFrame

element_names

Series with the names of all injections and branches in the network and their ids as index

TYPE: Series

RETURNS DESCRIPTION
station_elements

DataFrame with the names of the elements attached

TYPE: DataFrame

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_name_of_station_elements(station_elements: pd.DataFrame, element_names: pd.Series) -> pd.DataFrame:
    """Attach the name of the elements to the station elements.

    Parameters
    ----------
    station_elements: pd.DataFrame
        DataFrame with the station elements
        Comes from BusBreakerTopology.elements
    element_names: pd.Series
        Series with the names of all injections and branches in the network and their ids as index

    Returns
    -------
    station_elements: pd.DataFrame
        DataFrame with the names of the elements attached
    """
    station_elements["name"] = element_names
    return station_elements

get_asset_info_from_topology #

get_asset_info_from_topology(
    station_elements,
    station_buses,
    dangling_lines,
    element_names,
)

Get the asset information of all elements at the station.

PARAMETER DESCRIPTION
station_elements

DataFrame with the station elements Comes from BusBreakerTopology.elements

TYPE: DataFrame

station_buses

DataFrame with the busbars of the station

TYPE: DataFrame

dangling_lines

DataFrame of all dangling lines in the network with column "tie_line_id"

TYPE: DataFrame

element_names

Series with the names of all injections and branches in the network and their ids as index

TYPE: Series

RETURNS DESCRIPTION
station_elements

DataFrame with the asset information

TYPE: DataFrame

switching_matrix

Switching matrix with the shape (n_bus, n_asset) where n_bus is the number of busbars and n_asset is the number of assets. True, where the asset is connected to the busbar.

TYPE: ndarray

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_asset_info_from_topology(
    station_elements: pd.DataFrame, station_buses: pd.DataFrame, dangling_lines: pd.DataFrame, element_names: pd.Series
) -> tuple[pd.DataFrame, np.ndarray]:
    """Get the asset information of all elements at the station.

    Parameters
    ----------
    station_elements: pd.DataFrame
        DataFrame with the station elements
        Comes from BusBreakerTopology.elements
    station_buses: pd.DataFrame
        DataFrame with the busbars of the station
    dangling_lines: pd.DataFrame^
        DataFrame of all dangling lines in the network with column "tie_line_id"
    element_names: pd.Series
        Series with the names of all injections and branches in the network and their ids as index

    Returns
    -------
    station_elements: pd.DataFrame
        DataFrame with the asset information
    switching_matrix: np.ndarray
        Switching matrix with the shape (n_bus, n_asset) where n_bus is the number of busbars
        and n_asset is the number of assets. True, where the asset is connected to the busbar.
    """
    # check for TIE_LINE
    station_elements = change_dangling_to_tie(dangling_lines, station_elements)
    # get the name for the branches
    station_elements = get_name_of_station_elements(station_elements, element_names)
    # for UCTE model: if not in service, asset will not appear
    station_elements["in_service"] = True
    station_elements.reset_index(inplace=True)
    station_elements.rename(columns={"id": "grid_model_id"}, inplace=True)
    # get the busbar ids for switching matrix
    merged_df = pd.merge(
        station_elements,
        station_buses,
        left_on="bus_id",
        right_on="grid_model_id",
        how="left",
    )
    station_elements["bus_int_id"] = merged_df["int_id"].fillna(-1).astype(int)

    station_elements = station_elements[station_elements["type"] != "BUSBAR_SECTION"]
    # TODO: change selection to bus_id -> keep disconnected assets
    # currently disconnected assets are not shown in the topology
    station_elements = station_elements[station_elements["bus_id"].isin(station_buses["grid_model_id"])]
    switching_matrix = get_asset_switching_table(station_buses=station_buses, station_elements=station_elements)
    # get columns for pydantic model
    station_elements = station_elements[["grid_model_id", "type", "name", "in_service"]].reset_index(drop=True)
    return station_elements, switching_matrix

get_relevant_network_data #

get_relevant_network_data(network, relevant_stations)

Get the relevant data from the network that is required for all stations.

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

relevant_stations

The relevant stations to be included in the resulting topology. Either as a boolean mask over all buses in network.get_buses or as a list of bus ids in network.get_buses()

TYPE: Union[list[str], Bool[ndarray, ' n_buses']]

RETURNS DESCRIPTION
buses_with_substation_and_voltage

DataFrame with the relevant buses, substation id and voltage level

TYPE: DataFrame

switches

DataFrame with all the switches in the network. Includes the column "name"

TYPE: DataFrame

dangling_lines

DataFrame with all the dangling lines in the network. Includes the column "tie_line_id"

TYPE: DataFrame

element_names

Series with the names of all injections and branches in the network and their ids as index

TYPE: Series

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_relevant_network_data(
    network: Network, relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]]
) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.Series]:
    """Get the relevant data from the network that is required for all stations.

    Parameters
    ----------
    network: Network
        pypowsybl network object
    relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]]
        The relevant stations to be included in the resulting topology. Either as a boolean mask over all buses in
        network.get_buses or as a list of bus ids in network.get_buses()

    Returns
    -------
    buses_with_substation_and_voltage: pd.DataFrame
        DataFrame with the relevant buses, substation id and voltage level
    switches: pd.DataFrame
        DataFrame with all the switches in the network. Includes the column "name"
    dangling_lines: pd.DataFrame
        DataFrame with all the dangling lines in the network. Includes the column "tie_line_id"
    element_names: pd.Series
        Series with the names of all injections and branches in the network and their ids as index
    """
    relevant_buses = network.get_buses(attributes=["voltage_level_id"]).loc[relevant_stations]
    voltage_level_df = get_voltage_level_with_region(network, attributes=["substation_id", "nominal_v", "topology_kind"])
    buses_with_substation_and_voltage = relevant_buses.merge(voltage_level_df, left_on="voltage_level_id", right_index=True)
    if buses_with_substation_and_voltage["topology_kind"].unique() == "BUS_BREAKER":
        if "elementName" in network.get_lines(all_attributes=True).columns:
            # For UCTE models, the name is stored in elementName
            element_name_col = "elementName"
        else:
            # All other grid files use normally "name"
            element_name_col = "name"
    elif buses_with_substation_and_voltage["topology_kind"].unique() == "NODE_BREAKER":
        element_name_col = "name"
    else:
        raise ValueError(
            "Relevant stations must be either of kind NODE_BREAKER or BUS_BREAKER, a mix of both is not permitted"
        )
    buses_with_substation_and_voltage.drop(columns=["topology_kind"], inplace=True)

    element_names = get_all_element_names(network, line_trafo_name_col=element_name_col)
    switches = network.get_switches(attributes=["name"])
    dangling_lines = network.get_dangling_lines(attributes=["tie_line_id"])
    return buses_with_substation_and_voltage, switches, dangling_lines, element_names

get_relevant_stations #

get_relevant_stations(network, relevant_stations)

Get all relevant stations from the network.

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

relevant_stations

The relevant stations to be included in the resulting topology. Either as a boolean mask over all buses in network.get_buses or as a list of bus ids in network.get_buses()

TYPE: Union[list[str], Bool[ndarray, ' n_buses']]

RETURNS DESCRIPTION
station

List of all formatted stations of the relevant buses in the network

TYPE: list[Station]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_relevant_stations(
    network: Network, relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]]
) -> list[Station]:
    """Get all relevant stations from the network.

    Parameters
    ----------
    network: Network
        pypowsybl network object
    relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]]
        The relevant stations to be included in the resulting topology. Either as a boolean mask over all buses in
        network.get_buses or as a list of bus ids in network.get_buses()

    Returns
    -------
    station: list[Station]
        List of all formatted stations of the relevant buses in the network
    """
    # Load relevant data once
    buses_with_substation_and_voltage, switches, dangling_lines, element_names = get_relevant_network_data(
        network=network,
        relevant_stations=relevant_stations,
    )

    # Calculate the pydantic station for each relevant bus
    station_list = get_list_of_stations(network, buses_with_substation_and_voltage, switches, dangling_lines, element_names)
    return station_list

get_list_of_stations #

get_list_of_stations(
    network,
    buses_with_substation_and_voltage,
    switches,
    dangling_lines,
    element_names,
)

Get the list of stations from the relevant buses.

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

buses_with_substation_and_voltage

DataFrame with the relevant buses, substation id and voltage level

TYPE: DataFrame

switches

DataFrame with all the switches in the network. Includes the column "name"

TYPE: DataFrame

dangling_lines

DataFrame with all the dangling lines in the network. Includes the column "tie_line_id"

TYPE: DataFrame

element_names

Series with the names of all injections and branches in the network and their ids as index

TYPE: Series

RETURNS DESCRIPTION
station_list

List of all formatted stations of the relevant buses in the network

TYPE: list[Station]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_list_of_stations(
    network: Network,
    buses_with_substation_and_voltage: pd.DataFrame,
    switches: pd.DataFrame,
    dangling_lines: pd.DataFrame,
    element_names: pd.Series,
) -> list[Station]:
    """Get the list of stations from the relevant buses.

    Parameters
    ----------
    network: Network
        pypowsybl network object
    buses_with_substation_and_voltage: pd.DataFrame
        DataFrame with the relevant buses, substation id and voltage level
    switches: pd.DataFrame
        DataFrame with all the switches in the network. Includes the column "name"
    dangling_lines: pd.DataFrame
        DataFrame with all the dangling lines in the network. Includes the column "tie_line_id"
    element_names: pd.Series
        Series with the names of all injections and branches in the network and their ids as index

    Returns
    -------
    station_list: list[Station]
        List of all formatted stations of the relevant buses in the network
    """
    station_list = []
    for bus_id, bus_info in buses_with_substation_and_voltage.iterrows():
        station_topology = network.get_bus_breaker_topology(bus_info.voltage_level_id)
        station_buses = get_bus_info_from_topology(station_topology.buses, bus_id)
        coupler_elements = get_coupler_info_from_topology(station_topology.switches, switches, station_buses)
        station_elements, switching_matrix = get_asset_info_from_topology(
            station_topology.elements, station_buses, dangling_lines, element_names
        )
        asset_connectivity = np.ones_like(switching_matrix, dtype=bool)
        station = Station(
            grid_model_id=bus_id,
            name=bus_info.substation_id,
            region=bus_info.voltage_level_id[0:2],
            voltage_level=bus_info.nominal_v,
            busbars=get_list_of_busbars_from_df(station_buses),
            couplers=get_list_of_coupler_from_df(coupler_elements),
            assets=get_list_of_switchable_assets_from_df(station_elements),
            asset_switching_table=switching_matrix,
            asset_connectivity=asset_connectivity,
        )
        station_list.append(station)
    return station_list

get_topology #

get_topology(
    network,
    relevant_stations,
    topology_id,
    grid_model_file=None,
)

Get the pydantic topology model from the network.

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

relevant_stations

The relevant stations to be included in the resulting topology. Either as a boolean mask over all buses in network.get_buses or as a list of bus ids in network.get_buses()

TYPE: Union[list[str], Bool[ndarray, ' n_buses']]

topology_id

Id of the topology to set in the asset topology

TYPE: str

grid_model_file

Path to the grid model file to set in the asset topology

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
topology

Topology object, including all relevant stations

TYPE: Topology

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_topology(
    network: Network,
    relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]],
    topology_id: str,
    grid_model_file: Optional[str] = None,
) -> Topology:
    """Get the pydantic topology model from the network.

    Parameters
    ----------
    network: Network
        pypowsybl network object
    relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]]
        The relevant stations to be included in the resulting topology. Either as a boolean mask over all buses in
        network.get_buses or as a list of bus ids in network.get_buses()
    topology_id: str
        Id of the topology to set in the asset topology
    grid_model_file: Optional[str]
        Path to the grid model file to set in the asset topology

    Returns
    -------
    topology: Topology
        Topology object, including all relevant stations
    """
    station_list = get_relevant_stations(network=network, relevant_stations=relevant_stations)
    timestamp = datetime.datetime.now()

    return Topology(
        topology_id=topology_id,
        grid_model_file=grid_model_file,
        stations=station_list,
        timestamp=timestamp,
    )

get_stations_bus_breaker #

get_stations_bus_breaker(net)

Convert all stations in a bus-breaker topology grid to the asset topology format.

This is very similar to get_topology but only works for bus-breaker grids. This is mainly used for the test grids. TODO find out why get_topology didn't work and remove either of the two.

PARAMETER DESCRIPTION
net

The bus/breaker powsybl network to convert

TYPE: Network

RETURNS DESCRIPTION
stations

List of all stations in the network

TYPE: list[Station]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_stations_bus_breaker(net: Network) -> list[Station]:
    """Convert all stations in a bus-breaker topology grid to the asset topology format.

    This is very similar to get_topology but only works for bus-breaker grids. This is mainly used for the test grids.
    TODO find out why get_topology didn't work and remove either of the two.

    Parameters
    ----------
    net: Network
        The bus/breaker powsybl network to convert

    Returns
    -------
    stations: list[Station]
        List of all stations in the network
    """
    all_switches = net.get_switches(all_attributes=True)
    all_branches = net.get_branches(all_attributes=True)
    all_injections = net.get_injections(all_attributes=True)
    all_breaker_buses = net.get_bus_breaker_view_buses(all_attributes=True)

    stations = []
    for bus_id, bus_row in net.get_buses().iterrows():
        local_buses = all_breaker_buses[all_breaker_buses["bus_id"] == bus_id]
        local_switches = all_switches[
            (
                all_switches["bus_breaker_bus1_id"].isin(local_buses.index)
                | all_switches["bus_breaker_bus2_id"].isin(local_buses.index)
            )
        ]
        from_branches = all_branches[
            (all_branches["bus_breaker_bus1_id"].isin(local_buses.index) & all_branches["connected1"])
        ]
        to_branches = all_branches[
            (all_branches["bus_breaker_bus2_id"].isin(local_buses.index) & all_branches["connected2"])
        ]
        injections = all_injections[
            (all_injections["bus_breaker_bus_id"].isin(local_buses.index) & all_injections["connected"])
        ]
        busbar_mapper = {grid_model_id: index for index, grid_model_id in enumerate(local_buses.index)}

        busbars = [
            Busbar(grid_model_id=grid_model_id, int_id=busbar_mapper[grid_model_id]) for grid_model_id in local_buses.index
        ]
        couplers = [
            BusbarCoupler(
                grid_model_id=grid_model_id,
                busbar_from_id=busbar_mapper[switch.bus_breaker_bus1_id],
                busbar_to_id=busbar_mapper[switch.bus_breaker_bus2_id],
                open=switch.open,
            )
            for grid_model_id, switch in local_switches.iterrows()
        ]
        from_branch_assets = [
            SwitchableAsset(grid_model_id=grid_model_id, type=branch.type, branch_end="from")
            for grid_model_id, branch in from_branches.iterrows()
        ]
        to_branch_assets = [
            SwitchableAsset(grid_model_id=grid_model_id, type=branch.type, branch_end="to")
            for grid_model_id, branch in to_branches.iterrows()
        ]
        injection_assets = [
            SwitchableAsset(grid_model_id=grid_model_id, type=injection.type)
            for grid_model_id, injection in injections.iterrows()
        ]
        assets = from_branch_assets + to_branch_assets + injection_assets

        from_branch_bus_index = [busbar_mapper[branch.bus_breaker_bus1_id] for branch in from_branches.itertuples()]
        to_branch_bus_index = [busbar_mapper[branch.bus_breaker_bus2_id] for branch in to_branches.itertuples()]
        injection_bus_index = [busbar_mapper[injection.bus_breaker_bus_id] for injection in injections.itertuples()]
        bus_index = from_branch_bus_index + to_branch_bus_index + injection_bus_index

        switching_table = np.zeros((len(busbars), len(assets)), dtype=bool)
        for asset_index, idx in enumerate(bus_index):
            switching_table[idx, asset_index] = True

        station = Station(
            grid_model_id=bus_id,
            name=bus_row.name,
            busbars=busbars,
            couplers=couplers,
            assets=assets,
            asset_switching_table=switching_table,
        )
        stations.append(station)
    return stations

assert_station_in_network #

assert_station_in_network(
    net,
    station,
    couplers_strict=True,
    assets_strict=True,
    busbars_strict=True,
)

Check if an asset topology station and all assets/busbars are actually in the station in the grid

This only checks subsets, i.e. if all asset in the asset topology are also in the grid. If there are more assets in the grid, this will not raise by default. You can enable strict equality checking by setting ..._strict to True.

PARAMETER DESCRIPTION
net

The powsybl network to check the station in

TYPE: Network

station

The asset topology station to check

TYPE: Station

couplers_strict

If you opt out of strict coupler checking, it will only be checked if all couplers in the station are present in the grid, not vice versa.

TYPE: bool DEFAULT: True

assets_strict

If you opt out of strict asset checking, it will only be checked if all assets in the station are present in the grid, not vice versa.

TYPE: bool DEFAULT: True

busbars_strict

If you opt out of strict busbar checking, it will only be checked if all busbars in the station are present in the grid, not vice versa.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

If the station or any of the assets/busbars are not in the network

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def assert_station_in_network(  # noqa: C901
    net: Network,
    station: Station,
    couplers_strict: bool = True,
    assets_strict: bool = True,
    busbars_strict: bool = True,
) -> None:
    """Check if an asset topology station and all assets/busbars are actually in the station in the grid

    This only checks subsets, i.e. if all asset in the asset topology are also in the grid. If there are more assets in the
    grid, this will not raise by default. You can enable strict equality checking by setting ..._strict to True.

    Parameters
    ----------
    net: Network
        The powsybl network to check the station in
    station: Station
        The asset topology station to check
    couplers_strict: bool
        If you opt out of strict coupler checking, it will only be checked if all couplers in the station are present in the
        grid, not vice versa.
    assets_strict: bool
        If you opt out of strict asset checking, it will only be checked if all assets in the station are present in the
        grid, not vice versa.
    busbars_strict: bool
        If you opt out of strict busbar checking, it will only be checked if all busbars in the station are present in the
        grid, not vice versa.

    Raises
    ------
    ValueError
        If the station or any of the assets/busbars are not in the network
    """
    buses_df = net.get_buses(attributes=["voltage_level_id"])
    if station.grid_model_id not in buses_df.index:
        raise ValueError(f"Station {station.grid_model_id} not found in the network")

    bus_breaker_topo = net.get_bus_breaker_topology(buses_df.loc[station.grid_model_id]["voltage_level_id"])

    for asset in station.assets:
        if asset.grid_model_id not in bus_breaker_topo.elements.index:
            raise ValueError(f"Asset {asset.grid_model_id} not found in the station elements: {bus_breaker_topo.elements}")
    if assets_strict and len(bus_breaker_topo.elements) != len(station.assets):
        raise ValueError(f"Asset count mismatch: {len(bus_breaker_topo.elements)} != {len(station.assets)}")

    for busbar in station.busbars:
        if busbar.grid_model_id not in bus_breaker_topo.buses.index:
            raise ValueError(f"Busbar {busbar.grid_model_id} not found in the station buses: {bus_breaker_topo.buses}")
    if busbars_strict and len(bus_breaker_topo.buses) != len(station.busbars):
        raise ValueError(f"Busbar count mismatch: {len(bus_breaker_topo.buses)} != {len(station.busbars)}")

    for coupler in station.couplers:
        if coupler.grid_model_id not in bus_breaker_topo.switches.index:
            raise ValueError(
                f"Coupler {coupler.grid_model_id} not found in the station switches: {bus_breaker_topo.switches}"
            )
    if couplers_strict and len(bus_breaker_topo.switches) != len(station.couplers):
        raise ValueError(f"Coupler count mismatch: {len(bus_breaker_topo.switches)} != {len(station.couplers)}")

toop_engine_grid_helpers.powsybl.powsybl_helpers #

Provides general helper functions for pypowsybl networks.

These functions are used to extract and manipulate data from pypowsybl networks, such as loadflow results, branch limits, and monitored elements.

extract_single_injection_loadflow_result #

extract_single_injection_loadflow_result(
    injections, injection_id
)

Extract the loadflow results for a single injection.

PARAMETER DESCRIPTION
injections

The injections dataframe with the loadflow results

TYPE: DataFrame

injection_id

The id of the injection to extract

TYPE: str

RETURNS DESCRIPTION
tuple[float, float]

The active and reactive power of the injection

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def extract_single_injection_loadflow_result(injections: pd.DataFrame, injection_id: str) -> tuple[float, float]:
    """Extract the loadflow results for a single injection.

    Parameters
    ----------
    injections : pd.DataFrame
        The injections dataframe with the loadflow results
    injection_id : str
        The id of the injection to extract

    Returns
    -------
    tuple[float, float]
        The active and reactive power of the injection
    """
    p = injections.loc[injection_id, "p"]
    q = injections.loc[injection_id, "q"]
    return p, q

extract_single_branch_loadflow_result #

extract_single_branch_loadflow_result(
    branches, branch_id, from_side=True
)

Extract the loadflow results for a single branch

PARAMETER DESCRIPTION
branches

The branches dataframe with the loadflow results

TYPE: DataFrame

branch_id

The id of the branch to extract

TYPE: str

from_side

If True, the from side is used, by default True If False, the to side is used

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
tuple[float, float]

The active and reactive power of the branch

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def extract_single_branch_loadflow_result(
    branches: pd.DataFrame, branch_id: str, from_side: bool = True
) -> tuple[float, float]:
    """Extract the loadflow results for a single branch

    Parameters
    ----------
    branches : pd.DataFrame
        The branches dataframe with the loadflow results
    branch_id : str
        The id of the branch to extract
    from_side : bool, optional
        If True, the from side is used, by default True
        If False, the to side is used

    Returns
    -------
    tuple[float, float]
        The active and reactive power of the branch
    """
    p_mapper = "p1" if from_side else "p2"
    q_mapper = "q1" if from_side else "q2"

    p = branches.loc[branch_id, p_mapper]
    q = branches.loc[branch_id, q_mapper]
    return p, q

get_branches_with_i #

get_branches_with_i(branches, net)

Get the get_branches results with the i values filled

In DC, they are Nan and will be computed through p = i * v, in AC they are already computed

PARAMETER DESCRIPTION
branches

The result of net.get_branches() with or without i values

TYPE: DataFrame

net

The powsybl network to load voltage information from

TYPE: Network

RETURNS DESCRIPTION
DataFrame

The result of net.get_branches(), but with the i values filled

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def get_branches_with_i(branches: pd.DataFrame, net: Network) -> pd.DataFrame:
    """Get the get_branches results with the i values filled

    In DC, they are Nan and will be computed through p = i * v, in AC they are already computed

    Parameters
    ----------
    branches : pd.DataFrame
        The result of net.get_branches() with or without i values
    net : Network
        The powsybl network to load voltage information from

    Returns
    -------
    pd.DataFrame
        The result of net.get_branches(), but with the i values filled
    """
    if np.any(branches["i1"].notna()):
        return branches
    # DC loadflow, compute i = p / v
    # # P[kW] = sqrt(3) * U[kV] * I[A]
    # P[MW] = P[kW] / 1000
    # I[A] = P[MW] / (1000 * sqrt(3) * U[kV])
    branches = pd.merge(
        left=branches,
        right=net.get_voltage_levels()["nominal_v"].rename("nominal_v_1"),
        left_on="voltage_level1_id",
        right_index=True,
    )
    branches = pd.merge(
        left=branches,
        right=net.get_voltage_levels()["nominal_v"].rename("nominal_v_2"),
        left_on="voltage_level2_id",
        right_index=True,
    )
    branches["i1"] = (branches["p1"] / (branches["nominal_v_1"] * math.sqrt(3) * 1e-3)).abs()
    branches["i2"] = (branches["p2"] / (branches["nominal_v_2"] * math.sqrt(3) * 1e-3)).abs()
    del branches["nominal_v_1"]
    del branches["nominal_v_2"]
    return branches

get_injections_with_i #

get_injections_with_i(injections, net)

Get the get_injections results with the i values filled

In DC, they are Nan and will be computed through p = i * v, in AC they are already computed

PARAMETER DESCRIPTION
injections

The result of net.get_injections() with or without i values

TYPE: DataFrame

net

The powsybl network to load voltage information from

TYPE: Network

RETURNS DESCRIPTION
DataFrame

The result of net.get_injections(), but with the i values filled

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def get_injections_with_i(injections: pd.DataFrame, net: Network) -> pd.DataFrame:
    """Get the get_injections results with the i values filled

    In DC, they are Nan and will be computed through p = i * v, in AC they are already computed

    Parameters
    ----------
    injections : pd.DataFrame
        The result of net.get_injections() with or without i values
    net : Network
        The powsybl network to load voltage information from

    Returns
    -------
    pd.DataFrame
        The result of net.get_injections(), but with the i values filled
    """
    if np.any(injections["i"].notna()):
        return injections
    # DC loadflow, compute i = p / v
    # # P[kW] = sqrt(3) * U[kV] * I[A]
    # P[MW] = P[kW] / 1000
    # I[A] = P[MW] / (1000 * sqrt(3) * U[kV])
    injections = pd.merge(
        left=injections,
        right=net.get_voltage_levels()["nominal_v"],
        left_on="voltage_level_id",
        right_index=True,
    )
    injections["i"] = (injections["p"] / (injections["nominal_v"] * math.sqrt(3) * 1e-3)).abs()
    del injections["nominal_v"]
    return injections

get_branches_with_i_max #

get_branches_with_i_max(
    branches, net, limit_name="permanent_limit"
)

Get the get_branches results with the i_max values

These will be pulled from the current limits table. If one side is missing, the other side will be taken. If both sides are missing, the value will be NaN

PARAMETER DESCRIPTION
branches

The result of net.get_branches() without i_max values

TYPE: DataFrame

net

The powsybl network with current limits

TYPE: Network

limit_name

The name of the limit to pull, by default "permanent_limit"

TYPE: str DEFAULT: 'permanent_limit'

RETURNS DESCRIPTION
DataFrame

The result of net.get_branches(), but with i1_max and i2_max columns

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def get_branches_with_i_max(branches: pd.DataFrame, net: Network, limit_name: str = "permanent_limit") -> pd.DataFrame:
    """Get the get_branches results with the i_max values

    These will be pulled from the current limits table. If one side is missing, the other side will
    be taken. If both sides are missing, the value will be NaN

    Parameters
    ----------
    branches : pd.DataFrame
        The result of net.get_branches() without i_max values
    net : Network
        The powsybl network with current limits
    limit_name : str, optional
        The name of the limit to pull, by default "permanent_limit"

    Returns
    -------
    pd.DataFrame
        The result of net.get_branches(), but with i1_max and i2_max columns
    """
    current_limits = net.get_operational_limits()
    current_limits = current_limits[current_limits["name"] == limit_name]
    current_limits = current_limits.groupby(level=["element_id", "side"]).min().reset_index("side")
    current_limits1 = current_limits[current_limits["side"] == "ONE"]["value"].reindex(current_limits.index)
    current_limits2 = current_limits[current_limits["side"] == "TWO"]["value"].reindex(current_limits.index)

    # Take the other side if one side is missing
    current_limits1 = current_limits1.fillna(current_limits2)
    current_limits2 = current_limits2.fillna(current_limits1)

    branches = pd.merge(
        left=branches,
        right=current_limits1.rename("i1_max"),
        left_index=True,
        right_index=True,
        how="left",
    )
    branches = pd.merge(
        left=branches,
        right=current_limits2.rename("i2_max"),
        left_index=True,
        right_index=True,
        how="left",
    )

    return branches

get_voltage_level_with_region #

get_voltage_level_with_region(
    network, attributes=None, all_attributes=None
)

Get the region for each voltage level in the network.

This function is an extension to the network.get_voltage_levels() function. It retrieves the region for each voltage level using the substation region.

PARAMETER DESCRIPTION
network

The network for which the regions should be retrieved.

TYPE: Network

attributes

The attributes that should be retrieved for the voltage levels. Behaves like the attributes parameter in network.get_voltage_levels().

TYPE: Optional[list[str]] DEFAULT: None

all_attributes

If True, all attributes are retrieved for the voltage levels. Behaves like the all_attributes parameter in network.get_voltage_levels().

TYPE: Optional[Literal[True, False]] DEFAULT: None

RETURNS DESCRIPTION
DataFrame

A DataFrame with the voltage levels and their regions.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def get_voltage_level_with_region(
    network: Network, attributes: Optional[list[str]] = None, all_attributes: Optional[Literal[True, False]] = None
) -> pd.DataFrame:
    """Get the region for each voltage level in the network.

    This function is an extension to the network.get_voltage_levels() function.
    It retrieves the region for each voltage level using the substation region.

    Parameters
    ----------
    network: Network
        The network for which the regions should be retrieved.
    attributes: Optional[list[str]]
        The attributes that should be retrieved for the voltage levels.
        Behaves like the attributes parameter in network.get_voltage_levels().
    all_attributes: Optional[Union[True,False]]
        If True, all attributes are retrieved for the voltage levels.
        Behaves like the all_attributes parameter in network.get_voltage_levels().

    Returns
    -------
    pd.DataFrame
        A DataFrame with the voltage levels and their regions.
    """
    substation_region = network.get_substations(attributes=["country"])
    substation_region.rename(columns={"country": "region"}, inplace=True)
    if attributes is not None and all_attributes is not None:
        raise ValueError("Only one of 'attributes' and 'all_attributes' can be specified")
    if ((attributes is None) and (not all_attributes)) or attributes == ["region"]:
        voltage_level = network.get_voltage_levels()
    elif all_attributes:
        voltage_level = network.get_voltage_levels(all_attributes=True)
    elif attributes is not None:
        if "region" in attributes:
            attributes = [attr for attr in attributes if attr != "region"]
        voltage_level = network.get_voltage_levels(attributes=attributes)
    voltage_level = voltage_level.merge(
        substation_region, left_on="substation_id", right_on="id", how="left", suffixes=("", "_substation")
    ).set_index(voltage_level.index)
    if ["region"] == attributes:
        voltage_level = voltage_level[["region"]]
    return voltage_level

change_dangling_to_tie #

change_dangling_to_tie(dangling_lines, station_elements)

Change the type of the dangling lines to TIE_LINE.

Changes dangling lines if a tie line is present. Keep Dangling lines if no tie line is present.

PARAMETER DESCRIPTION
dangling_lines

Dataframe of all dangling lines in the network with column "tie_line_id"

TYPE: DataFrame

station_elements

DataFrame with the injections and branches of the bus

TYPE: DataFrame

RETURNS DESCRIPTION
DataFrame

A copy of the station_elements dataframe with all dangling lines that are actually part of a tie line set to TIE_LINE.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def change_dangling_to_tie(dangling_lines: pd.DataFrame, station_elements: pd.DataFrame) -> pd.DataFrame:
    """Change the type of the dangling lines to TIE_LINE.

    Changes dangling lines if a tie line is present.
    Keep Dangling lines if no tie line is present.

    Parameters
    ----------
    dangling_lines: pd.DataFrame
        Dataframe of all dangling lines in the network with column "tie_line_id"
    station_elements: pd.DataFrame
        DataFrame with the injections and branches of the bus

    Returns
    -------
    pd.DataFrame
        A copy of the station_elements dataframe with all dangling lines that are actually part of a tie line set to
        TIE_LINE.
    """
    dangling = station_elements[station_elements["type"] == "DANGLING_LINE"].copy()
    dangling_index = dangling.index
    # if dangling lines are present -> check for tie lines
    if not dangling.empty:
        dangling = dangling.merge(
            dangling_lines,
            left_index=True,
            right_index=True,
        )
        condition_empty = dangling["tie_line_id"] == ""
        dangling.loc[~condition_empty, "type"] = "TIE_LINE"
        dangling.loc[condition_empty, "tie_line_id"] = dangling[condition_empty].index
        dangling.rename(columns={"tie_line_id": "id"}, inplace=True)
        dangling.set_index("id", inplace=True)

        station_elements = station_elements.drop(dangling_index)
        station_elements = pd.concat([station_elements, dangling])

    return station_elements

load_powsybl_from_fs #

load_powsybl_from_fs(
    filesystem,
    file_path,
    parameters=None,
    post_processors=None,
    report_node=None,
    allow_variant_multi_thread_access=False,
)

Load any supported Powsybl network format from a filesystem.

Supported standard Powsybl formats like CGMES (.zip), powsybl nativa (.xiddm), UCTE (.uct), Matpower (.mat). For all supported formats, see pypowsybl documentation for pypowsybl.network.load: https://powsybl.readthedocs.io/projects/powsybl-core/en/stable/grid_exchange_formats/index.html

PARAMETER DESCRIPTION
filesystem

The filesystem to load the Powsybl network from.

TYPE: AbstractFileSystem

file_path

The path to the Powsybl network in the filesystem.

TYPE: Path

parameters

Additional parameters to pass to the pypowsybl.network.load function, by default None

TYPE: Optional[Dict[str, str]] DEFAULT: None

post_processors

a list of import post processors (will be added to the ones defined by the platform config), by default None

TYPE: Optional[List[str]] DEFAULT: None

report_node

the reporter to be used to create an execution report

TYPE: Optional[ReportNode] DEFAULT: None

allow_variant_multi_thread_access

allow multi-thread access to variant, by default False

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Network

The loaded Powsybl network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def load_powsybl_from_fs(
    filesystem: AbstractFileSystem,
    file_path: Path,
    parameters: Optional[Dict[str, str]] = None,
    post_processors: Optional[List[str]] = None,
    report_node: Optional[ReportNode] = None,
    allow_variant_multi_thread_access: bool = False,
) -> pypowsybl.network.Network:
    """Load any supported Powsybl network format from a filesystem.

    Supported standard Powsybl formats like CGMES (.zip), powsybl nativa (.xiddm), UCTE (.uct), Matpower (.mat).
    For all supported formats, see pypowsybl documentation for `pypowsybl.network.load`:
    https://powsybl.readthedocs.io/projects/powsybl-core/en/stable/grid_exchange_formats/index.html

    Parameters
    ----------
    filesystem : AbstractFileSystem
        The filesystem to load the Powsybl network from.
    file_path : Path
        The path to the Powsybl network in the filesystem.
    parameters : Optional[Dict[str, str]], optional
        Additional parameters to pass to the pypowsybl.network.load function, by default None
    post_processors : Optional[List[str]], optional
        a list of import post processors (will be added to the ones defined by the platform config), by default None
    report_node : Optional[ReportNode], optional
        the reporter to be used to create an execution report
    allow_variant_multi_thread_access : bool, optional
        allow multi-thread access to variant, by default False

    Returns
    -------
    pypowsybl.network.Network
        The loaded Powsybl network.
    """
    with tempfile.TemporaryDirectory() as temp_dir:
        tmp_grid_path = Path(temp_dir) / file_path.name
        filesystem.download(
            str(file_path),
            str(tmp_grid_path),
        )
        return pypowsybl.network.load(
            file=str(tmp_grid_path),
            parameters=parameters,
            post_processors=post_processors,
            report_node=report_node,
            allow_variant_multi_thread_access=allow_variant_multi_thread_access,
        )

save_powsybl_to_fs #

save_powsybl_to_fs(
    net,
    filesystem,
    file_path,
    format="XIIDM",
    make_dir=True,
)

Save any supported Powsybl network format to a filesystem.

Supported standard Powsybl formats like CGMES (.zip), powsybl nativa (.xiddm), UCTE (.uct), Matpower (.mat). For all supported formats, see pypowsybl documentation for pypowsybl.network.save: https://powsybl.readthedocs.io/projects/powsybl-core/en/stable/grid_exchange_formats/index.html

PARAMETER DESCRIPTION
net

The Powsybl network to save.

TYPE: Network

filesystem

The filesystem to save the Powsybl network to.

TYPE: AbstractFileSystem

file_path

The path to save the Powsybl network in the filesystem.

TYPE: Path

format

The format to save the Powsybl network in, by default "CGMES".

TYPE: Optional[Literal[CGMES, XIIDM, UCTE, MATPOWER]] DEFAULT: 'XIIDM'

make_dir

create parent folder if not exists.

TYPE: bool DEFAULT: True

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def save_powsybl_to_fs(
    net: pypowsybl.network.Network,
    filesystem: AbstractFileSystem,
    file_path: Path,
    format: Optional[Literal["CGMES", "XIIDM", "UCTE", "MATPOWER"]] = "XIIDM",
    make_dir: bool = True,
) -> None:
    """Save any supported Powsybl network format to a filesystem.

    Supported standard Powsybl formats like CGMES (.zip), powsybl nativa (.xiddm), UCTE (.uct), Matpower (.mat).
    For all supported formats, see pypowsybl documentation for `pypowsybl.network.save`:
    https://powsybl.readthedocs.io/projects/powsybl-core/en/stable/grid_exchange_formats/index.html

    Parameters
    ----------
    net : pypowsybl.network.Network
        The Powsybl network to save.
    filesystem : AbstractFileSystem
        The filesystem to save the Powsybl network to.
    file_path : Path
        The path to save the Powsybl network in the filesystem.
    format : Optional[Literal["CGMES", "XIIDM", "UCTE", "MATPOWER"]], optional
        The format to save the Powsybl network in, by default "CGMES".
    make_dir: bool
        create parent folder if not exists.
    """
    if make_dir:
        filesystem.makedirs(Path(file_path).parent.as_posix(), exist_ok=True)
    with tempfile.TemporaryDirectory() as temp_dir:
        tmp_grid_path = Path(temp_dir) / file_path.name
        net.save(str(tmp_grid_path), format=format)
        filesystem.upload(
            str(tmp_grid_path),
            str(file_path),
        )

select_a_generator_as_slack_and_run_loadflow #

select_a_generator_as_slack_and_run_loadflow(network)

Select a generator as slack and run loadflow.

Powsybl tends to set the reference bus and slack as two different buses. Additionally, in some cases the slack is not a generator bus. This function selects a generator as slack bus and runs the loadflow again.

PARAMETER DESCRIPTION
network

The Powsybl network to modify and run loadflow on.

TYPE: Network

RAISES DESCRIPTION
ValueError

If the loadflow does not converge after setting the slack. If the slack bus is not a generator.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def select_a_generator_as_slack_and_run_loadflow(network: Network) -> None:
    """Select a generator as slack and run loadflow.

    Powsybl tends to set the reference bus and slack as two different buses.
    Additionally, in some cases the slack is not a generator bus.
    This function selects a generator as slack bus and runs the loadflow again.

    Parameters
    ----------
    network : Network
        The Powsybl network to modify and run loadflow on.

    Raises
    ------
    ValueError
        If the loadflow does not converge after setting the slack.
        If the slack bus is not a generator.
    """
    try:
        # try to get slack from CGMES data
        b = network.get_buses()
        ref_bus = b[(b["v_angle"] == 0) & (b["connected_component"] == 0)]

        slack_voltage_id = ref_bus["voltage_level_id"].values[0]
        slack_bus_id = ref_bus.index.values[0]
    except Exception:
        # if not found, set first largest generator as slack
        generators = network.get_generators(attributes=["bus_id", "voltage_level_id", "max_p"])
        generators = generators.sort_values(by="max_p", ascending=False)
        first = 1
        slack_bus_id = generators["bus_id"].values[first]
        slack_voltage_id = generators["voltage_level_id"].values[first]

    dict_slack = {"voltage_level_id": slack_voltage_id, "bus_id": slack_bus_id}
    pypowsybl.network.Network.create_extensions(network, extension_name="slackTerminal", **dict_slack)
    network.get_extensions("slackTerminal")

    powsybl_loadflow_parameter = pypowsybl.loadflow.Parameters(
        voltage_init_mode=pypowsybl.loadflow.VoltageInitMode.DC_VALUES,
        read_slack_bus=True,
        distributed_slack=True,
        use_reactive_limits=True,
        connected_component_mode=pypowsybl.loadflow.ConnectedComponentMode.MAIN,  # ConnectedComponentMode
    )

    loadflow_res = pypowsybl.loadflow.run_ac(network, parameters=powsybl_loadflow_parameter)[0]
    if loadflow_res.status != pypowsybl._pypowsybl.LoadFlowComponentStatus.CONVERGED:
        raise ValueError(
            f"Load flow did not converge. Status: {loadflow_res.status}, "
            f"Status text: {loadflow_res.status_text}, "
            f"Reference bus ID: {loadflow_res.reference_bus_id}"
        )

    slack_bus_id = loadflow_res.slack_bus_results[0].id
    generators = network.get_generators(attributes=["bus_id"])
    if slack_bus_id not in generators["bus_id"].values:
        raise ValueError("The slack bus must be a generator.")

load_pandapower_net_for_powsybl #

load_pandapower_net_for_powsybl(
    net,
    check_trafo_resistance=True,
    set_slack_generator=True,
)

Load a pandapower network and convert it to a pypowsybl network.

Known pandapower test grids that fail to convert: (This list is a logical AND of convert_from_pandapower and grid2opt conversion methods + the logical OR of check_powsybl_import errors) 'example_multivoltage' -> Generator minimum reactive power is not set 'simple_four_bus_system' -> Generator minimum reactive power is not set 'simple_mv_open_ring_net' -> 2 windings transformer '0_1_6': b is invalid 'create_cigre_network_hv' -> Generator minimum reactive power is not set 'create_cigre_network_lv' -> No Slack Generator found 'case145' -> Transformer with negative resistance 'case_illinois200' -> No Slack Generator found 'case300' -> No Slack Generator found

PARAMETER DESCRIPTION
net

The pandapower network to convert.

TYPE: pandapowerNet

check_trafo_resistance

If True, check for negative transformer resistance after conversion, by default True

TYPE: bool DEFAULT: True

set_slack_generator

If True, select a generator as slack and run loadflow after conversion, by default True

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
Network

The converted pypowsybl network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def load_pandapower_net_for_powsybl(
    net: pandapower.pandapowerNet, check_trafo_resistance: bool = True, set_slack_generator: bool = True
) -> pypowsybl.network.Network:
    """Load a pandapower network and convert it to a pypowsybl network.

    Known pandapower test grids that fail to convert:
    (This list is a logical AND of convert_from_pandapower and grid2opt conversion methods
    + the logical OR of check_powsybl_import errors)
    'example_multivoltage'      -> Generator minimum reactive power is not set
    'simple_four_bus_system'    -> Generator minimum reactive power is not set
    'simple_mv_open_ring_net'   -> 2 windings transformer '0_1_6': b is invalid
    'create_cigre_network_hv'   -> Generator minimum reactive power is not set
    'create_cigre_network_lv'   -> No Slack Generator found
    'case145'                   -> Transformer with negative resistance
    'case_illinois200'          -> No Slack Generator found
    'case300'                   -> No Slack Generator found

    Parameters
    ----------
    net : pandapower.pandapowerNet
        The pandapower network to convert.
    check_trafo_resistance : bool, optional
        If True, check for negative transformer resistance after conversion, by default True
    set_slack_generator : bool, optional
        If True, select a generator as slack and run loadflow after conversion, by default True

    Returns
    -------
    pypowsybl.network.Network
        The converted pypowsybl network.

    """
    try:
        pypowsybl_network = load_pandapower_net_for_powsybl_with_convert_from_pandapower(net)
        check_powsybl_import(pypowsybl_network, check_trafo_resistance=check_trafo_resistance)
    except (pypowsybl.PyPowsyblError, ValueError) as e:
        try:
            pypowsybl_network = load_pandapower_net_via_grid2opt_for_powsybl(net)
            check_powsybl_import(pypowsybl_network, check_trafo_resistance=check_trafo_resistance)
        except Exception as e2:
            raise ValueError(
                f"Failed to convert pandapower net to pypowsybl network. "
                f"pypowsybl.network.convert_from_pandapower: {e}. Conversion via grid2opt failed with error: {e2}"
            ) from e2
    if set_slack_generator:
        try:
            select_a_generator_as_slack_and_run_loadflow(pypowsybl_network)
        except Exception as e:
            raise ValueError(f"Slack selection failed after conversion from pandapower to powsybl: {e}") from e

    return pypowsybl_network

load_pandapower_net_for_powsybl_with_convert_from_pandapower #

load_pandapower_net_for_powsybl_with_convert_from_pandapower(
    net,
)

Load a pandapower network and convert it to a pypowsybl network using convert_from_pandapower.

Known pandapower test grids that fail to convert: 'example_simple' -> Generator minimum reactive power is not set 'example_multivoltage' -> Generator minimum reactive power is not set 'simple_four_bus_system' -> Generator minimum reactive power is not set 'simple_mv_open_ring_net' -> 2 windings transformer '0_1_6': b is invalid 'create_cigre_network_hv' -> Generator minimum reactive power is not set

PARAMETER DESCRIPTION
net

The pandapower network to convert.

TYPE: pandapowerNet

RETURNS DESCRIPTION
Network

The converted pypowsybl network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def load_pandapower_net_for_powsybl_with_convert_from_pandapower(net: pandapower.pandapowerNet) -> pypowsybl.network.Network:
    """Load a pandapower network and convert it to a pypowsybl network using convert_from_pandapower.

    Known pandapower test grids that fail to convert:
    'example_simple'            -> Generator minimum reactive power is not set
    'example_multivoltage'      -> Generator minimum reactive power is not set
    'simple_four_bus_system'    -> Generator minimum reactive power is not set
    'simple_mv_open_ring_net'   -> 2 windings transformer '0_1_6': b is invalid
    'create_cigre_network_hv'   -> Generator minimum reactive power is not set

    Parameters
    ----------
    net : pandapower.pandapowerNet
        The pandapower network to convert.

    Returns
    -------
    pypowsybl.network.Network
        The converted pypowsybl network.
    """
    pypowsybl_network = pypowsybl.network.convert_from_pandapower(net)
    return pypowsybl_network

load_pandapower_net_via_grid2opt_for_powsybl #

load_pandapower_net_via_grid2opt_for_powsybl(net)

Load a pandapower network and convert it to a pypowsybl network using grid2opt.

Known pandapower test grids that fail to convert: 'example_multivoltage' -> Transformer with negative resistance 'create_cigre_network_hv' -> Line with different voltage levels -> failed transformer conversion 'case14' -> Line with different voltage levels -> failed transformer conversion 'case_ieee30' -> Line with different voltage levels -> failed transformer conversion 'case57' -> Line with different voltage levels -> failed transformer conversion 'case89pegase' -> Line with different voltage levels -> failed transformer conversion 'case118' -> Line with different voltage levels -> failed transformer conversion 'case145' -> Transformer with negative resistance 'case_illinois200' -> Line with different voltage levels -> failed transformer conversion 'case300' -> Line with different voltage levels -> failed transformer conversion

PARAMETER DESCRIPTION
net

The pandapower network to convert.

TYPE: pandapowerNet

RETURNS DESCRIPTION
Network

The converted pypowsybl network.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def load_pandapower_net_via_grid2opt_for_powsybl(
    net: pandapower.pandapowerNet,
) -> pypowsybl.network.Network:
    """Load a pandapower network and convert it to a pypowsybl network using grid2opt.

    Known pandapower test grids that fail to convert:
    'example_multivoltage'      -> Transformer with negative resistance
    'create_cigre_network_hv'   -> Line with different voltage levels -> failed transformer conversion
    'case14'                    -> Line with different voltage levels -> failed transformer conversion
    'case_ieee30'               -> Line with different voltage levels -> failed transformer conversion
    'case57'                    -> Line with different voltage levels -> failed transformer conversion
    'case89pegase'              -> Line with different voltage levels -> failed transformer conversion
    'case118'                   -> Line with different voltage levels -> failed transformer conversion
    'case145'                   -> Transformer with negative resistance
    'case_illinois200'          -> Line with different voltage levels -> failed transformer conversion
    'case300'                   -> Line with different voltage levels -> failed transformer conversion

    Parameters
    ----------
    net : pandapower.pandapowerNet
        The pandapower network to convert.

    Returns
    -------
    pypowsybl.network.Network
        The converted pypowsybl network.
    """
    pandapower.runpp(net)
    with tempfile.NamedTemporaryFile(suffix=".mat", delete=True) as tmpfile:
        _ = pandapower.converter.to_mpc(net, tmpfile.name)
        loading_params = {
            "matpower.import.ignore-base-voltage": "false",  # change the voltage from per unit to Kv
        }
        pypowsybl_network = pypowsybl.network.load(tmpfile.name, loading_params)
    return pypowsybl_network

check_powsybl_import #

check_powsybl_import(
    pypowsybl_network, check_trafo_resistance=True
)

Check the import of a pypowsybl network.

PARAMETER DESCRIPTION
pypowsybl_network

The pypowsybl network to test.

TYPE: Network

check_trafo_resistance

If True, check for negative transformer resistance, by default True

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

If a transformer with negative resistance is found in the converted network. If a line with different voltage levels is found in the converted network. If the load flow does not converge.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_helpers.py
def check_powsybl_import(pypowsybl_network: pypowsybl.network.Network, check_trafo_resistance: bool = True) -> None:
    """Check the import of a pypowsybl network.

    Parameters
    ----------
    pypowsybl_network : pypowsybl.network.Network
        The pypowsybl network to test.
    check_trafo_resistance : bool, optional
        If True, check for negative transformer resistance, by default True

    Raises
    ------
    ValueError
        If a transformer with negative resistance is found in the converted network.
        If a line with different voltage levels is found in the converted network.
        If the load flow does not converge.
    """
    # importing pn.example_multivoltage -> one transformer has negative resistance
    if check_trafo_resistance:
        transformers = pypowsybl_network.get_2_windings_transformers()
        if len(transformers[transformers["r"] < 0]) > 0:
            raise ValueError("A Transformer in the converted pandapower net has a negative resistance")

    # test if lines have the same voltage level
    line_voltage = pypowsybl_network.get_lines(attributes=["voltage_level1_id", "voltage_level2_id"])
    line_voltage = line_voltage.merge(
        pypowsybl_network.get_voltage_levels(attributes=["nominal_v"]), left_on="voltage_level1_id", right_index=True
    )
    line_voltage = line_voltage.merge(
        pypowsybl_network.get_voltage_levels(attributes=["nominal_v"]),
        left_on="voltage_level2_id",
        right_index=True,
        suffixes=("_vl1", "_vl2"),
    )
    if not all(line_voltage["nominal_v_vl1"] == line_voltage["nominal_v_vl2"]):
        raise ValueError("A Line in the converted pandapower net has two different voltage levels")

    loadflow_res = pypowsybl.loadflow.run_ac(pypowsybl_network, DISTRIBUTED_SLACK)[0]
    if loadflow_res.status != pypowsybl._pypowsybl.LoadFlowComponentStatus.CONVERGED:
        raise ValueError(f"Load flow failed: {loadflow_res.status_text}")

Grid Helpers Single Line Diagram#

toop_engine_grid_helpers.powsybl.single_line_diagram.constants #

Defines constants used in the SLD (Single Line Diagram) importer module.

Style guide: Colors for Voltage Levels Voltage level >=400kV (220, 255, 220) | #DCFFDC | NEW Voltage level 380kV (160, 255, 166) | #A0FFA6 Voltage level 220kV (60, 163, 40) | #3CA328 Voltage level 150.sld-bus-170kV (44, 122, 30) | #2C7A1E | NEW Voltage level 110kV (15, 90, 0) | #0F5A00 Voltage level 66kV (80, 255, 90) | #50FF5A | NEW Voltage level <=33kV (12, 244, 30) | #0CF41E Grounded (182, 132, 18) | #B68412 No voltage (255, 0, 0) | #FF0000 Data invalid (200, 10, 200) | #C80AC8 Blocked (0, 120, 230) | #0078E6 Bypass busbar (255, 255, 25) | #E1E119 Manual entry (255, 255, 25) | #E1E119

Line thickness: 3px Switches: Open: Square 18x18, Outline 2px inside in the color of the voltage level, Background color: color/base/levels/04 Closed: Square 18x18, Color of the voltage level Disconnector: Open: Square 16.97x16.97 rotated by 45 degrees, outline 2px inside in the color of the voltage level, background color: color/base/levels/04 Closed: Square 16.97x16.97 rotated by 45 degrees, color of the voltage level

SLD_CSS module-attribute #

SLD_CSS = "<![CDATA[\n/* ----------------------------------------------------------------------- */\n/* File: tautologies.css ------------------------------------------------- */\n.sld-out .sld-arrow-in {visibility: hidden}\n.sld-in .sld-arrow-out {visibility: hidden}\n.sld-closed .sld-sw-open {visibility: hidden}\n.sld-open .sld-sw-closed {visibility: hidden}\n.sld-hidden-node {visibility: hidden}\n.sld-top-feeder .sld-label {dominant-baseline: auto}\n.sld-bottom-feeder .sld-label {dominant-baseline: hanging}\n.sld-active-power .sld-label {dominant-baseline: mathematical}\n.sld-reactive-power .sld-label {dominant-baseline: mathematical}\n.sld-current .sld-label {dominant-baseline: mathematical}\n/* ----------------------------------------------------------------------- */\n/* File: topologicalBaseVoltages.css ------------------------------------- */\n.sld-disconnected {--sld-vl-color: #FF0000}\n.sld-vl300to500.sld-bus-0 {--sld-vl-color: #A0FFA6}\n.sld-vl300to500.sld-bus-1 {--sld-vl-color: #D56701}\n.sld-vl300to500.sld-bus-2 {--sld-vl-color: #3E8AD0}\n.sld-vl300to500.sld-bus-3 {--sld-vl-color: #A0FFA6}\n.sld-vl300to500.sld-bus-4 {--sld-vl-color: #A0FFA6}\n.sld-vl300to500.sld-bus-5 {--sld-vl-color: #A0FFA6}\n.sld-vl300to500.sld-bus-6 {--sld-vl-color: #A0FFA6}\n.sld-vl300to500.sld-bus-7 {--sld-vl-color: #A0FFA6}\n.sld-vl300to500.sld-bus-8 {--sld-vl-color: #A0FFA6}\n.sld-vl300to500.sld-bus-9 {--sld-vl-color: #A0FFA6}\n.sld-vl300to500.highlight {--sld-vl-color: #0050fcff}\n.sld-vl180to300.sld-bus-0 {--sld-vl-color: #3CA328}\n.sld-vl180to300.sld-bus-1 {--sld-vl-color: #7C5CC3}\n.sld-vl180to300.sld-bus-2 {--sld-vl-color: #A67D00}\n.sld-vl180to300.sld-bus-3 {--sld-vl-color: #3CA328}\n.sld-vl180to300.sld-bus-4 {--sld-vl-color: #3CA328}\n.sld-vl180to300.sld-bus-5 {--sld-vl-color: #3CA328}\n.sld-vl180to300.sld-bus-6 {--sld-vl-color: #3CA328}\n.sld-vl180to300.sld-bus-7 {--sld-vl-color: #3CA328}\n.sld-vl180to300.sld-bus-8 {--sld-vl-color: #3CA328}\n.sld-vl180to300.sld-bus-9 {--sld-vl-color: #3CA328}\n.sld-vl180to300.highlight {--sld-vl-color: #0050fcff}\n.sld-vl120to180.sld-bus-0 {--sld-vl-color: #2C7A1E}\n.sld-vl120to180.sld-bus-1 {--sld-vl-color: #72858C}\n.sld-vl120to180.sld-bus-2 {--sld-vl-color: #D94814}\n.sld-vl120to180.sld-bus-3 {--sld-vl-color: #2C7A1E}\n.sld-vl120to180.sld-bus-4 {--sld-vl-color: #2C7A1E}\n.sld-vl120to180.sld-bus-5 {--sld-vl-color: #2C7A1E}\n.sld-vl120to180.sld-bus-6 {--sld-vl-color: #2C7A1E}\n.sld-vl120to180.sld-bus-7 {--sld-vl-color: #2C7A1E}\n.sld-vl120to180.sld-bus-8 {--sld-vl-color: #2C7A1E}\n.sld-vl120to180.sld-bus-9 {--sld-vl-color: #2C7A1E}\n.sld-vl120to180.highlight {--sld-vl-color: #0050fcff}\n.sld-vl70to120.sld-bus-0 {--sld-vl-color: #0F5A00}\n.sld-vl70to120.sld-bus-1 {--sld-vl-color: #699720}\n.sld-vl70to120.sld-bus-2 {--sld-vl-color: #CB2D64}\n.sld-vl70to120.sld-bus-3 {--sld-vl-color: #0F5A00}\n.sld-vl70to120.sld-bus-4 {--sld-vl-color: #0F5A00}\n.sld-vl70to120.sld-bus-5 {--sld-vl-color: #0F5A00}\n.sld-vl70to120.sld-bus-6 {--sld-vl-color: #0F5A00}\n.sld-vl70to120.sld-bus-7 {--sld-vl-color: #0F5A00}\n.sld-vl70to120.sld-bus-8 {--sld-vl-color: #0F5A00}\n.sld-vl70to120.sld-bus-9 {--sld-vl-color: #0F5A00}\n.sld-vl70to120.highlight {--sld-vl-color: #0050fcff}\n.sld-vl50to70.sld-bus-0 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-1 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-2 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-3 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-4 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-5 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-6 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-7 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-8 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.sld-bus-9 {--sld-vl-color: #50FF5A}\n.sld-vl50to70.highlight {--sld-vl-color: #0050fcff}\n.sld-vl30to50.sld-bus-0 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-1 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-2 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-3 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-4 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-5 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-6 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-7 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-8 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.sld-bus-9 {--sld-vl-color: #0CF41E}\n.sld-vl30to50.highlight {--sld-vl-color: #0050fcff}\n.sld-vl0to30.sld-bus-0 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-1 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-2 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-3 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-4 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-5 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-6 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-7 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-8 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.sld-bus-9 {--sld-vl-color: #0CF41E}\n.sld-vl0to30.highlight {--sld-vl-color: #0050fcff}\n\n\n/* ----------------------------------------------------------------------- */\n/* File : highlightLineStates.css ---------------------------------------- */\n.sld-wire.sld-feeder-disconnected {stroke: #FF0000; stroke-width: 1.5px}\n.sld-wire.sld-feeder-disconnected-connected {stroke: #FF0000; stroke-width: 1.5px}\n/* File : components.css ------------------------------------------------- */\n.sld-disconnector.sld-open {stroke-width: 1.5; stroke: var(--sld-vl-color, black); fill: black}\n.sld-disconnector.sld-closed {fill: var(--sld-vl-color, black)}\n.sld-breaker, .sld-load-break-switch {fill: var(--sld-vl-color, blue)}\n.sld-breaker {stroke: var(--sld-vl-color, blue); stroke-width: 1.5}\n.sld-bus-connection {fill: var(--sld-vl-color, black)}\n.sld-cell-shape-flat .sld-bus-connection {visibility: hidden}\n.sld-busbar-section {stroke: var(--sld-vl-color, black); stroke-width: 3; fill: none}\n.sld-wire {stroke: var(--sld-vl-color, #c80000); fill: none; stroke-width: 1.5px}\n.sld-wire.sld-dangling-line {stroke-width: 2px}\n.sld-wire.sld-tie-line {stroke-width: 2px}\n\n/* Stroke --sld-vl-color with fallback #C80AC8 */\n.sld-load {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-battery {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-generator {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-two-wt {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-three-wt {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-winding {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-capacitor {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-inductor {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-pst {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-pst-arrow {stroke: #C80AC8; fill: none; stroke-width: 1.5px}\n.sld-svc {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n.sld-vsc {stroke: var(--sld-vl-color, #C80AC8); font-size: 7.43px; fill: none; stroke-width: 1.5px}\n.sld-lcc {stroke: var(--sld-vl-color, #C80AC8); font-size: 7.43px; fill: none; stroke-width: 1.5px}\n.sld-ground {stroke: var(--sld-vl-color, #C80AC8); fill: none; stroke-width: 1.5px}\n\n.sld-node-infos {stroke: none; fill: var(--sld-vl-color, black)}\n.sld-node {stroke: none; fill: black}\n.sld-flash {stroke: none; fill: black}\n.sld-lock {stroke: none; fill: black}\n.sld-unknown {stroke: none; fill: #C80AC8}\n/* Fonts */\n.sld-label {stroke: none; fill: white; font: 8px serif}\n.sld-bus-legend-info {font: 10px serif; fill: white}\n.sld-graph-label {font: 12px serif}\n/* Specific */\n.sld-grid {stroke: #003700; stroke-dasharray: 1,10}\n.sld-feeder-info.sld-active-power {fill: #fff}\n.sld-feeder-info.sld-reactive-power {fill: #fff}\n.sld-feeder-info.sld-current {fill:#fff}\n.sld-frame {fill: var(--sld-background-color, #000)}\n/* fictitious switches and busbar */\n.sld-breaker.sld-fictitious {stroke: var(--sld-vl-color, #C80AC8); stroke-width: 1.5}\n.sld-disconnector.sld-fictitious {stroke: maroon}\n.sld-load-break-switch.sld-fictitious {stroke: maroon}\n.sld-busbar-section.sld-fictitious {stroke: var(--sld-vl-color, #c80000); stroke-width: 1}\n/* ground disconnector specific */\n.sld-ground-disconnection-attach {stroke: var(--sld-vl-color, #c80000); fill: none}\n.sld-open.sld-ground-disconnection-ground {stroke: black; fill: none}\n.sld-closed.sld-ground-disconnection-ground {stroke: var(--sld-vl-color, #c80000); fill: none}\n.sld-open.sld-ground-disconnection {stroke: var(--sld-vl-color, black); fill: none}\n.sld-closed.sld-ground-disconnection {fill: var(--sld-vl-color, black)}\n.sld-breaker.sld-open {fill: transparent}\n]]>\n"

DARK_MODE_STYLE module-attribute #

DARK_MODE_STYLE = {
    "background": ".sld-frame {fill: var(--sld-background-color, #000)}",
    "active_power": ".sld-feeder-info.sld-active-power {fill: #fff}",
    "reactive_power": ".sld-feeder-info.sld-reactive-power {fill: #fff}",
    "current": ".sld-feeder-info.sld-current {fill:#fff}",
    "label": ".sld-label {stroke: none; fill: white; font: 8px serif}",
    "bus-legend-info": ".sld-bus-legend-info {font: 10px serif; fill: white}",
    "sld-open": ".sld-disconnector.sld-open {stroke-width: 1.5; stroke: var(--sld-vl-color, black); fill: black}",
}

BRIGHT_MODE_STYLE module-attribute #

BRIGHT_MODE_STYLE = {
    "background": ".sld-frame {fill: var(--sld-background-color, #fff)}",
    "active_power": ".sld-feeder-info.sld-active-power {fill: #000}",
    "reactive_power": ".sld-feeder-info.sld-reactive-power {fill: #000}",
    "current": ".sld-feeder-info.sld-current {fill:#000}",
    "label": ".sld-label {stroke: none; fill: black; font: 8px serif}",
    "bus-legend-info": ".sld-bus-legend-info {font: 10px serif; fill: black}",
    "sld-open": ".sld-disconnector.sld-open {stroke-width: 1.5; stroke: var(--sld-vl-color, black); fill: white}",
}

DISCONNECTOR_STYLE module-attribute #

DISCONNECTOR_STYLE = {
    "height": "8",
    "width": "8",
    "x": "0",
    "y": "0",
    "transform": "rotate(45.0,4.0,4.0)",
}

BREAKER_STYLE module-attribute #

BREAKER_STYLE = {'d': 'M1,1 V19 H19 V1z'}

BUSBAR_SECTION_LABEL_POSITION module-attribute #

BUSBAR_SECTION_LABEL_POSITION = {'x': '-5', 'y': '-7'}

toop_engine_grid_helpers.powsybl.single_line_diagram.get_single_line_diagram_custom #

A replacement for the powsybl get_single_line_diagram function.

custom_style_options module-attribute #

custom_style_options = Literal['dark_mode', 'bright_mode']

get_single_line_diagram_custom #

get_single_line_diagram_custom(
    net,
    container_id,
    parameters=None,
    custom_style="dark_mode",
    highlight_grid_model_ids=None,
    highlight_color="#0050fcff",
)

Get a single line diagram with custom styles.

A replacement for the powsybl get_single_line_diagram function that allows for custom styles to be applied, specifically dark mode or bright mode.

PARAMETER DESCRIPTION
net

The network for which to get the single line diagram. Hint: If you want the SLD for including loadflow results, run the loadflow first.

TYPE: Network

container_id

The ID of Voltage level for which to get the single line diagram.

TYPE: str

parameters

Parameters for the single line diagram, by default None.

TYPE: Optional[SldParameters] DEFAULT: None

custom_style

The custom style to apply, either "dark_mode" or "bright_mode". If None, the original style will be used, by default "dark_mode".

TYPE: Optional[Literal[dark_mode, bright_mode]] DEFAULT: 'dark_mode'

highlight_grid_model_ids

The IDs of the grid model elements to highlight, by default None. E.g. get all switches that changed their state from closed to open -> show the difference between the original and new states.

TYPE: Optional[list[str]] DEFAULT: None

highlight_color

The color to use for highlighting, by default "#0050fcff".

TYPE: str DEFAULT: '#0050fcff'

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/get_single_line_diagram_custom.py
def get_single_line_diagram_custom(
    net: pn.Network,
    container_id: str,
    parameters: Optional[pn.SldParameters] = None,
    custom_style: Optional[custom_style_options] = "dark_mode",
    highlight_grid_model_ids: Optional[list[str]] = None,
    highlight_color: str = "#0050fcff",
) -> Svg:
    """Get a single line diagram with custom styles.

    A replacement for the powsybl get_single_line_diagram function that allows
    for custom styles to be applied, specifically dark mode or bright mode.

    Parameters
    ----------
    net : pn.Network
        The network for which to get the single line diagram.
        Hint: If you want the SLD for including loadflow results,
        run the loadflow first.
    container_id : str
        The ID of Voltage level for which to get the single line diagram.
    parameters : Optional[pn.SldParameters]
        Parameters for the single line diagram, by default None.
    custom_style : Optional[Literal["dark_mode", "bright_mode"]], optional
        The custom style to apply, either "dark_mode" or "bright_mode". If None,
        the original style will be used, by default "dark_mode".
    highlight_grid_model_ids : Optional[list[str]], optional
        The IDs of the grid model elements to highlight, by default None.
        E.g. get all switches that changed their state from closed to open -> show the difference between the
        original and new states.
    highlight_color : str, optional
        The color to use for highlighting, by default "#0050fcff".

    """
    if parameters is None:
        parameters = pn.SldParameters(
            use_name=True,
            component_library="Convergence",
            nodes_infos=True,
            display_current_feeder_info=True,
        )
    svg = net.get_single_line_diagram(container_id=container_id, parameters=parameters)
    if custom_style in custom_style_options.__args__:
        svg._content = replace_svg_styles(
            xmlstring=svg._content,
            style=custom_style,
            highlight_color=highlight_color,
            highlight_grid_model_ids=highlight_grid_model_ids,
        )
    # else: keep the original style e.g. Convergence
    return svg

toop_engine_grid_helpers.powsybl.single_line_diagram.replace_convergence_style #

Functions to replace powsybl Convergence styles in SVG files.

Author: Benjamin Petrick Date: 2025-07-15

replace_sld_disconnected #

replace_sld_disconnected(element)

Replace sld-disconnected with the most common vl tag in the SVG tree.

PARAMETER DESCRIPTION
element

The element to search for sld-intern-cell and sld-extern-cell

TYPE: Element

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def replace_sld_disconnected(element: StdETree.Element) -> None:
    """Replace sld-disconnected with the most common vl tag in the SVG tree.

    Parameters
    ----------
    element: StdETree.Element
        The element to search for sld-intern-cell and sld-extern-cell
    """
    if "sld-intern-cell" in element.get("class") or "sld-extern-cell" in element.get("class"):
        # iterate over all children of the element
        # get the most common sld-vl color from the children
        class_tags = [child.get("class") for child in element if child.get("class") is not None]
        vl_tags = extract_sld_bus_numbers(class_tags)
        if len(vl_tags) == 0:
            return
        most_common_vl = get_most_common_bus(vl_tags)
        # replace sld-disconnected with the most common vl tag
        for child in element:
            if child.get("class") is not None and "sld-disconnected" in child.get("class"):
                class_content = child.get("class")
                new_class_content = class_content.replace("sld-disconnected", most_common_vl)
                child.set("class", new_class_content)

move_transform #

move_transform(child, move_by_item)

Move the transform attribute of a child element by a given value.

PARAMETER DESCRIPTION
child

The child element to modify.

TYPE: Element

move_by_item

The value to move the transform by.

TYPE: float

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def move_transform(child: StdETree.Element, move_by_item: float) -> None:
    """Move the transform attribute of a child element by a given value.

    Parameters
    ----------
    child : xml.etree.ElementTree.Element
        The child element to modify.
    move_by_item : float
        The value to move the transform by.
    """
    transform = child.get("transform")
    # content is "translate(X,Y)"
    # set y
    if transform is not None:
        match = re.search(r"translate\(([^,]+),([^,]+)\)", transform)
        if match:
            x = float(match.group(1))
            y = float(match.group(2))
            # move y by move_by_item
            y += move_by_item
            # update the transform attribute
            child.set("transform", f"translate({x},{y})")

move_labels_p_q_i #

move_labels_p_q_i(element, move_by=0.0)

Move labels for active power, reactive power, and current.

Avoids overlapping labels of the three-wt, moves all labels for consistent positioning.

PARAMETER DESCRIPTION
element

The element to search for sld-intern-cell and sld-extern-cell.

TYPE: Element

move_by

The distance to move the labels, by default 0.0.

TYPE: float DEFAULT: 0.0

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def move_labels_p_q_i(element: StdETree.Element, move_by: float = 0.0) -> None:
    """Move labels for active power, reactive power, and current.

    Avoids overlapping labels of the three-wt, moves all labels for consistent positioning.

    Parameters
    ----------
    element : StdETree.Element
        The element to search for sld-intern-cell and sld-extern-cell.
    move_by : float, optional
        The distance to move the labels, by default 0.0.
    """
    if "sld-intern-cell" in element.get("class") or "sld-extern-cell" in element.get("class"):
        if "sld-cell-direction-bottom" in element.get("class"):
            move_by_item = move_by
        else:
            move_by_item = -move_by
        # iterate over all children of the element
        for child in element:
            if child.get("class") is None:
                continue
            if (
                "sld-active-power" in child.get("class")
                or "sld-current" in child.get("class")
                or "sld-reactive-power" in child.get("class")
            ):
                move_transform(child, move_by_item)

replace_disconnector #

replace_disconnector(element, disconnector_style=None)

Replace the powsybl Convergence disconnector with the given style.

Parameter

element: StdETree.Element The element to search for sld-disconnector disconnector_style: dict[str, str] The disconnector style used as the replacement, expects a dict for a rect like:

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def replace_disconnector(element: StdETree.Element, disconnector_style: Optional[dict[str, str]] = None) -> None:
    """Replace the powsybl Convergence disconnector with the given style.

    Parameter
    ---------
    element: StdETree.Element
        The element to search for sld-disconnector
    disconnector_style: dict[str, str]
        The disconnector style used as the replacement,
        expects a dict for a rect like:
        {"height": "8", "width": "8", "x": "0", "y": "0", "transform": "rotate(45.0,4.0,4.0)"}
    """
    if disconnector_style is None:
        disconnector_style = DISCONNECTOR_STYLE

    if "sld-disconnector" in element.get("class"):
        # Find the <rect> child inside this <g> element

        for child in element:
            if child.tag.endswith("path"):
                # replace path with path
                child.tag = "{http://www.w3.org/2000/svg}rect"
                child.attrib.clear()
                child.attrib.update(disconnector_style)

replace_breaker #

replace_breaker(element, breaker_style=None)

Replace the powsybl Convergence breaker with the given style.

Parameter

element: StdETree.Element The element to search for sld-breaker breaker_style: dict[str, str] The breaker style, expects a dict for a path like: "d": "M1,1 V19 H19 V1z"

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def replace_breaker(element: StdETree.Element, breaker_style: Optional[dict[str, str]] = None) -> None:
    """Replace the powsybl Convergence breaker with the given style.

    Parameter
    ---------
    element: StdETree.Element
        The element to search for sld-breaker
    breaker_style: dict[str, str]
        The breaker style, expects a dict for a path like: "d": "M1,1 V19 H19 V1z"
    """
    if breaker_style is None:
        breaker_style = BREAKER_STYLE

    if "sld-breaker" in element.get("class"):
        for child in element:
            if child.tag.endswith("path"):
                child.attrib.update(breaker_style)

move_busbar_section_label #

move_busbar_section_label(
    element, busbar_section_label_position=None
)

Move the busbar section label up, to avoid overlapping.

Parameter

element: StdETree.Element The element to search for sld-busbar-section busbar_section_label_position: dict[str, str] expects values for x and y like: {"x":3, "y":4}

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def move_busbar_section_label(element: StdETree.Element, busbar_section_label_position: dict[str, str] = None) -> None:  # noqa: RUF013
    """Move the busbar section label up, to avoid overlapping.

    Parameter
    ---------
    element: StdETree.Element
        The element to search for sld-busbar-section
    busbar_section_label_position: dict[str, str]
        expects values for x and y like: {"x":3, "y":4}
    """
    if busbar_section_label_position is None:
        busbar_section_label_position = BUSBAR_SECTION_LABEL_POSITION

    if "sld-busbar-section" in element.get("class"):
        for child in element:
            if child.get("class") is not None and "sld-label" in child.get("class"):
                # lift sld label of busbar section
                child.attrib.update(busbar_section_label_position)

switch_dark_to_bright_mode #

switch_dark_to_bright_mode(
    xmlstring,
    dark_mode=DARK_MODE_STYLE,
    bright_mode=BRIGHT_MODE_STYLE,
)

Switches the dark mode to bright mode in the SVG XML string.

PARAMETER DESCRIPTION
xmlstring

The SVG XML string to modify.

TYPE: str

dark_mode

The dark mode styles, a dict with keys that must match the bright_mode and values of default dark mode style to be replaced with the bright_mode style.

TYPE: dict[str, str] DEFAULT: DARK_MODE_STYLE

bright_mode

The bright mode styles, a dict with keys that mus match the dark_mode and values to replace the dark mode values.

TYPE: dict[str, str] DEFAULT: BRIGHT_MODE_STYLE

RETURNS DESCRIPTION
str

The modified SVG XML string with dark mode styles replaced by bright mode styles.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def switch_dark_to_bright_mode(
    xmlstring: str,
    dark_mode: dict[str, str] = DARK_MODE_STYLE,
    bright_mode: dict[str, str] = BRIGHT_MODE_STYLE,
) -> str:
    """Switches the dark mode to bright mode in the SVG XML string.

    Parameters
    ----------
    xmlstring : str
        The SVG XML string to modify.
    dark_mode : dict[str, str]
        The dark mode styles, a dict with keys that must match the bright_mode and values of
        default dark mode style to be replaced with the bright_mode style.
    bright_mode : dict[str, str]
        The bright mode styles, a dict with keys that mus match the dark_mode and values
        to replace the dark mode values.

    Returns
    -------
    str
        The modified SVG XML string with dark mode styles replaced by bright mode styles.
    """
    for key, bright_mode_value in bright_mode.items():
        # Replace dark_mode styles with bright_mode styles
        xmlstring = xmlstring.replace(dark_mode[key], bright_mode_value)
    return xmlstring

replace_svg_styles #

replace_svg_styles(
    xmlstring,
    style="dark_mode",
    sld_css=SLD_CSS,
    move_p_q_i_labels_by=10.0,
    highlight_color="#0050fcff",
    highlight_grid_model_ids=None,
)

Replace powsybl Convergence with dark or bright mode.

Parameter

xmlstring: str The svg xmls string to load and replace the style style: Literal["dark_mode", "bright_mode"] Choose the style dark_mode: dict[str, str] The dark mode styles, a dict with keys that must match the bright_mode and values of default dark mode style to be replaced with the bright_mode style. bright_mode: dict[str, str] The bright mode styles, a dict with keys that mus match the dark_mode and values to replace the dark mode values. sld_css: str The SLD CSS to be added to the SVG. move_p_q_i_labels_by: float The distance to move the labels for active power, reactive power, and current. Default is 10.0. highlight_color: Optional[str] The color to use for highlighting elements in the SVG. highlight_grid_model_ids: Optional[list[str]] The list of grid model IDs to highlight in the SVG.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def replace_svg_styles(
    xmlstring: str,
    style: Literal["dark_mode", "bright_mode"] = "dark_mode",
    sld_css: str = SLD_CSS,
    move_p_q_i_labels_by: float = 10.0,
    highlight_color: Optional[str] = "#0050fcff",
    highlight_grid_model_ids: Optional[list[str]] = None,
) -> str:
    """Replace powsybl Convergence with dark or bright mode.

    Parameter
    ---------
    xmlstring: str
        The svg xmls string to load and replace the style
    style: Literal["dark_mode", "bright_mode"]
        Choose the style
    dark_mode: dict[str, str]
        The dark mode styles, a dict with keys that must match the bright_mode and values of
        default dark mode style to be replaced with the bright_mode style.
    bright_mode: dict[str, str]
        The bright mode styles, a dict with keys that mus match the dark_mode and values
        to replace the dark mode values.
    sld_css: str
        The SLD CSS to be added to the SVG.
    move_p_q_i_labels_by: float
        The distance to move the labels for active power, reactive power, and current.
        Default is 10.0.
    highlight_color: Optional[str]
        The color to use for highlighting elements in the SVG.
    highlight_grid_model_ids: Optional[list[str]]
        The list of grid model IDs to highlight in the SVG.
    """
    tree = ElementTree.fromstring(xmlstring)

    # Find all elements with class="sld-disconnector sld-open sld-disconnected"
    for elem in tree.iter():
        if elem.tag == "{http://www.w3.org/2000/svg}style":
            sld_css = set_highlight_color(xmlstring=sld_css, highlight_color=highlight_color)
            elem.text = sld_css
        if elem.get("class") is None:
            continue

        replace_disconnector(elem)
        replace_breaker(elem)
        move_busbar_section_label(elem)
        move_labels_p_q_i(elem, move_by=move_p_q_i_labels_by)
        replace_sld_disconnected(elem)
        if highlight_grid_model_ids is not None:
            set_highlight_color_for_ids(elem, grid_model_ids=highlight_grid_model_ids)

    # write tree to string
    xmlstring = ElementTree.tostring(tree, encoding="unicode", xml_declaration=True)
    # replace some defuse_et artifacts
    if style == "bright_mode":
        xmlstring = switch_dark_to_bright_mode(xmlstring)

    xmlstring = remove_defuse_artifacts(xmlstring=xmlstring)
    return xmlstring

set_highlight_color_for_ids #

set_highlight_color_for_ids(element, grid_model_ids)

Replace the SVG style given an id.

Parameter

xmlstring: str The svg xmls string to load and replace the style grid_model_ids: list[str] The list of grid model IDs to replace in the SVG. sld_css: str The SLD CSS to be added to the SVG.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def set_highlight_color_for_ids(
    element: StdETree.Element,
    grid_model_ids: list[str],
) -> None:
    """Replace the SVG style given an id.

    Parameter
    ---------
    xmlstring: str
        The svg xmls string to load and replace the style
    grid_model_ids: list[str]
        The list of grid model IDs to replace in the SVG.
    sld_css: str
        The SLD CSS to be added to the SVG.

    """
    if element.get("class") is None or element.get("id") is None:
        return

    for id in grid_model_ids:
        # not sure why "_95", an artifact from/for html?
        if f"id{id}" == element.get("id").replace("_95", "").replace("_45_", "-"):
            # set the highlight class
            element.set("class", f"{element.get('class')} highlight")

set_highlight_color #

set_highlight_color(xmlstring, highlight_color=None)

Set the color for the highlight color

PARAMETER DESCRIPTION
xmlstring

The SVG XML string to modify.

TYPE: str

highlight_color

The color to set for the highlight elements, by default "#0050fcff".

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
str
Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def set_highlight_color(xmlstring: str, highlight_color: Optional[str] = None) -> str:
    """Set the color for the highlight color

    Parameters
    ----------
    xmlstring : str
        The SVG XML string to modify.
    highlight_color : str, optional
        The color to set for the highlight elements, by default "#0050fcff".

    Returns
    -------
    str

    """
    # set the highlight color
    default_color = "#0050fcff"
    if highlight_color is None or default_color != highlight_color:
        content_str = ".highlight {--sld-vl-color: " + default_color + "}"
        replace_str = ".highlight {--sld-vl-color: " + highlight_color + "}"
        xmlstring = xmlstring.replace(content_str, replace_str)

    return xmlstring

remove_defuse_artifacts #

remove_defuse_artifacts(xmlstring)

Remove defuse_et artifacts from the SVG XML string.

xmlstring: str The svg xmls string to load and replace the style

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/replace_convergence_style.py
def remove_defuse_artifacts(xmlstring: str) -> str:
    """Remove defuse_et artifacts from the SVG XML string.

    xmlstring: str
        The svg xmls string to load and replace the style
    """
    xmlstring = xmlstring.replace(":ns0", "").replace("ns0:", "")
    xmlstring = xmlstring.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&")
    xmlstring = xmlstring.replace('encoding="utf-8"', 'encoding="UTF-8" standalone="no"')
    return xmlstring

toop_engine_grid_helpers.powsybl.single_line_diagram.sld_helper_functions #

Helper functions for SLD (Single Line Diagram) processing.

T module-attribute #

T = TypeVar('T')

extract_sld_bus_numbers #

extract_sld_bus_numbers(tags)

Extract all 'sld-bus' tags with their numbers from a list of strings.

Extracts all css classes e.g. sld-bus-0, sld-bus-1, etc. from a list of strings.

PARAMETER DESCRIPTION
tags

List of strings containing 'sld-bus' tags.

TYPE: list[str]

RETURNS DESCRIPTION
list[str]

List of extracted 'sld-bus' tags with their numbers.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/sld_helper_functions.py
def extract_sld_bus_numbers(tags: list[str]) -> list[str]:
    """Extract all 'sld-bus' tags with their numbers from a list of strings.

    Extracts all css classes e.g. sld-bus-0, sld-bus-1, etc. from a list of strings.

    Parameters
    ----------
    tags : list[str]
        List of strings containing 'sld-bus' tags.

    Returns
    -------
    list[str]
        List of extracted 'sld-bus' tags with their numbers.
    """
    # Extract all 'sld-bus' tags with their numbers from each string in the list
    results = []
    for tag_str in tags:
        matches = re.findall(r"sld-bus-\d+", tag_str)
        results.extend(matches)
    return results

get_most_common_bus #

get_most_common_bus(tags)

Get the most common Value of a list.

PARAMETER DESCRIPTION
tags

List of tags to analyze

TYPE: list[Any]

RETURNS DESCRIPTION
most_common_tag

The most common Value from the list

TYPE: T

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/single_line_diagram/sld_helper_functions.py
def get_most_common_bus(tags: list[T]) -> T:
    """Get the most common Value of a list.

    Parameters
    ----------
    tags : list[Any]
        List of tags to analyze

    Returns
    -------
    most_common_tag: T
        The most common Value from the list
    """
    tag_counter = Counter(tags)
    most_common_tag, _ = tag_counter.most_common(1)[0]
    return most_common_tag