Skip to content

Solvers

This module provides functions for running power flow calculations.

run_opf

Runs Optimal Power Flow (OPF) and adds additional information to network elements.

This function runs the OPF calculation and adds bus index and type information to the results dataframes. It also performs various validation checks on the results.

Parameters:

Name Type Description Default
net pandapowerNet

A pandapower network object containing the power system model.

required
**kwargs Any

Additional keyword arguments to pass to pp.runopp().

{}

Returns:

Name Type Description
bool bool

True if the OPF converged successfully, False otherwise.

Raises:

Type Description
AssertionError

If any of the validation checks fail, including: - Mismatch in active/reactive power - Power bounds violations - Bus power mismatches - Power balance violations

Source code in gridfm_datakit/process/solvers.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def run_opf(net: pandapowerNet, **kwargs: Any) -> bool:
    """Runs Optimal Power Flow (OPF) and adds additional information to network elements.

    This function runs the OPF calculation and adds bus index and type information to the
    results dataframes. It also performs various validation checks on the results.

    Args:
        net: A pandapower network object containing the power system model.
        **kwargs: Additional keyword arguments to pass to pp.runopp().

    Returns:
        bool: True if the OPF converged successfully, False otherwise.

    Raises:
        AssertionError: If any of the validation checks fail, including:
            - Mismatch in active/reactive power
            - Power bounds violations
            - Bus power mismatches
            - Power balance violations
    """
    pp.runopp(net, numba=True, **kwargs)

    # add bus index and type to dataframe of opf results
    net.res_gen["bus"] = net.gen.bus
    net.res_gen["type"] = net.gen.type
    net.res_load["bus"] = net.load.bus
    net.res_load["type"] = net.load.type
    net.res_sgen["bus"] = net.sgen.bus
    net.res_sgen["type"] = net.sgen.type
    net.res_shunt["bus"] = net.shunt.bus
    net.res_ext_grid["bus"] = net.ext_grid.bus
    net.res_bus["type"] = net.bus["type"]

    # The load of course stays the same as before
    assert (net.load.p_mw == net.res_load.p_mw).all(), "Active power load has changed"
    assert (net.load.q_mvar == net.res_load.q_mvar).all(), (
        "Reactive power load has changed"
    )

    # Checking bounds on active and reactive power
    # TODO: see with matteo how we want to handle this
    if len(net.gen) != 0:
        in_service_gen = net.gen[net.gen.in_service]
        in_service_res_gen = net.res_gen[net.gen.in_service]

        if "max_p_mw" in net.gen.columns and "min_p_mw" in net.gen.columns:
            valid_gen = in_service_gen.dropna(
                subset=["max_p_mw", "min_p_mw"],
                how="any",
            )
            valid_res_gen = in_service_res_gen.loc[valid_gen.index]

            if not valid_gen.empty:
                assert (valid_gen.max_p_mw - valid_res_gen.p_mw > -1e-4).all(), (
                    f"Active power exceeds upper bound: "
                    f"{valid_res_gen.p_mw[valid_gen.max_p_mw - valid_res_gen.p_mw <= -1e-4]} exceeds "
                    f"{valid_gen.max_p_mw[valid_gen.max_p_mw - valid_res_gen.p_mw <= -1e-4]}"
                )
                assert (valid_res_gen.p_mw - valid_gen.min_p_mw > -1e-4).all(), (
                    f"Active power falls below lower bound: "
                    f"{valid_res_gen.p_mw[valid_res_gen.p_mw - valid_gen.min_p_mw <= -1e-4]} below "
                    f"{valid_gen.min_p_mw[valid_res_gen.p_mw - valid_gen.min_p_mw <= -1e-4]}"
                )

        if "max_q_mvar" in net.gen.columns and "min_q_mvar" in net.gen.columns:
            valid_q_gen = in_service_gen.dropna(
                subset=["max_q_mvar", "min_q_mvar"],
                how="any",
            )
            valid_q_res_gen = in_service_res_gen.loc[valid_q_gen.index]

            if not valid_q_gen.empty:
                assert (
                    valid_q_gen.max_q_mvar - valid_q_res_gen.q_mvar > -1e-4
                ).all(), (
                    f"Reactive power exceeds upper bound: "
                    f"{valid_q_res_gen.q_mvar[valid_q_gen.max_q_mvar - valid_q_res_gen.q_mvar <= -1e-4]} exceeds "
                    f"{valid_q_gen.max_q_mvar[valid_q_gen.max_q_mvar - valid_q_res_gen.q_mvar <= -1e-4]}"
                )
                assert (
                    valid_q_res_gen.q_mvar - valid_q_gen.min_q_mvar > -1e-4
                ).all(), (
                    f"Reactive power falls below lower bound: "
                    f"{valid_q_res_gen.q_mvar[valid_q_res_gen.q_mvar - valid_q_gen.min_q_mvar <= -1e-4]} below "
                    f"{valid_q_gen.min_q_mvar[valid_q_res_gen.q_mvar - valid_q_gen.min_q_mvar <= -1e-4]}"
                )

    if len(net.sgen) != 0:
        in_service_sgen = net.sgen[net.sgen.in_service]
        in_service_res_sgen = net.res_sgen[net.sgen.in_service]

        if "max_p_mw" in net.sgen.columns and "min_p_mw" in net.sgen.columns:
            valid_sgen = in_service_sgen.dropna(
                subset=["max_p_mw", "min_p_mw"],
                how="any",
            )
            valid_res_sgen = in_service_res_sgen.loc[valid_sgen.index]

            if not valid_sgen.empty:
                assert (valid_sgen.max_p_mw - valid_res_sgen.p_mw > -1e-4).all(), (
                    f"Active power exceeds upper bound for static generators: "
                    f"{valid_res_sgen.p_mw[valid_sgen.max_p_mw - valid_res_sgen.p_mw <= -1e-4]} exceeds "
                    f"{valid_sgen.max_p_mw[valid_sgen.max_p_mw - valid_res_sgen.p_mw <= -1e-4]}"
                )
                assert (valid_res_sgen.p_mw - valid_sgen.min_p_mw > -1e-4).all(), (
                    f"Active power falls below lower bound for static generators: "
                    f"{valid_res_sgen.p_mw[valid_res_sgen.p_mw - valid_sgen.min_p_mw <= -1e-4]} below "
                    f"{valid_sgen.min_p_mw[valid_res_sgen.p_mw - valid_sgen.min_p_mw <= -1e-4]}"
                )

        if "max_q_mvar" in net.sgen.columns and "min_q_mvar" in net.sgen.columns:
            valid_q_sgen = in_service_sgen.dropna(
                subset=["max_q_mvar", "min_q_mvar"],
                how="any",
            )
            valid_q_res_sgen = in_service_res_sgen.loc[valid_q_sgen.index]

            if not valid_q_sgen.empty:
                assert (
                    valid_q_sgen.max_q_mvar - valid_q_res_sgen.q_mvar > -1e-4
                ).all(), (
                    f"Reactive power exceeds upper bound for static generators: "
                    f"{valid_q_res_sgen.q_mvar[valid_q_sgen.max_q_mvar - valid_q_res_sgen.q_mvar <= -1e-4]} exceeds "
                    f"{valid_q_sgen.max_q_mvar[valid_q_sgen.max_q_mvar - valid_q_res_sgen.q_mvar <= -1e-4]}"
                )
                assert (
                    valid_q_res_sgen.q_mvar - valid_q_sgen.min_q_mvar > -1e-4
                ).all(), (
                    f"Reactive power falls below lower bound for static generators: "
                    f"{valid_q_res_sgen.q_mvar[valid_q_res_sgen.q_mvar - valid_q_sgen.min_q_mvar <= -1e-4]} below "
                    f"{valid_q_sgen.min_q_mvar[valid_q_res_sgen.q_mvar - valid_q_sgen.min_q_mvar <= -1e-4]}"
                )

    total_p_diff, total_q_diff = calculate_power_imbalance(net)
    assert np.abs(total_q_diff) < 1e-1, (
        f"Total reactive power imbalance in OPF: {total_q_diff}"
    )
    assert np.abs(total_p_diff) < 1e-1, (
        f"Total active power imbalance in OPF: {total_p_diff}"
    )

    return net.OPF_converged

run_pf

Runs Power Flow (PF) calculation and adds additional information to network elements.

This function runs the power flow calculation and adds bus index and type information to the results dataframes. It also performs validation checks on the results.

Parameters:

Name Type Description Default
net pandapowerNet

A pandapower network object containing the power system model.

required
**kwargs Any

Additional keyword arguments to pass to pp.runpp().

{}

Returns:

Name Type Description
bool bool

True if the power flow converged successfully, False otherwise.

Raises:

Type Description
AssertionError

If any of the validation checks fail, including: - Bus power mismatches - Power balance violations

Source code in gridfm_datakit/process/solvers.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def run_pf(net: pandapowerNet, **kwargs: Any) -> bool:
    """Runs Power Flow (PF) calculation and adds additional information to network elements.

    This function runs the power flow calculation and adds bus index and type information
    to the results dataframes. It also performs validation checks on the results.

    Args:
        net: A pandapower network object containing the power system model.
        **kwargs: Additional keyword arguments to pass to pp.runpp().

    Returns:
        bool: True if the power flow converged successfully, False otherwise.

    Raises:
        AssertionError: If any of the validation checks fail, including:
            - Bus power mismatches
            - Power balance violations
    """
    pp.runpp(net, **kwargs)

    # add bus number to df of opf results
    net.res_gen["bus"] = net.gen.bus
    net.res_gen["type"] = net.gen.type

    net.res_load["bus"] = net.load.bus
    net.res_load["type"] = net.load.type

    net.res_sgen["bus"] = net.sgen.bus
    net.res_sgen["type"] = net.sgen.type

    net.res_shunt["bus"] = net.shunt.bus
    net.res_ext_grid["bus"] = net.ext_grid.bus
    net.res_bus["type"] = net.bus["type"]

    total_p_diff, total_q_diff = calculate_power_imbalance(net)

    assert np.abs(total_q_diff) < 1e-2, (
        f"Total reactive power imbalance in PF: {total_q_diff}"
    )
    assert np.abs(total_p_diff) < 1e-2, (
        f"Total active power imbalance in PF: {total_p_diff}"
    )

    return net.converged

run_dcpf

Runs DC Power Flow (DCPF) calculation and adds additional information to network elements.

This function runs the DC power flow calculation and adds bus index and type information to the results dataframes. It also performs validation checks on the results.

Parameters:

Name Type Description Default
net pandapowerNet

A pandapower network object containing the power system model.

required
**kwargs Any

Additional keyword arguments to pass to pp.rundcpp().

{}

Returns:

Name Type Description
bool bool

True if the DC power flow converged successfully, False otherwise.

Raises:

Type Description
AssertionError

If any of the validation checks fail, including: - Bus power mismatches

Source code in gridfm_datakit/process/solvers.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def run_dcpf(net: pandapowerNet, **kwargs: Any) -> bool:
    """Runs DC Power Flow (DCPF) calculation and adds additional information to network elements.

    This function runs the DC power flow calculation and adds bus index and type information
    to the results dataframes. It also performs validation checks on the results.

    Args:
        net: A pandapower network object containing the power system model.
        **kwargs: Additional keyword arguments to pass to pp.rundcpp().

    Returns:
        bool: True if the DC power flow converged successfully, False otherwise.

    Raises:
        AssertionError: If any of the validation checks fail, including:
            - Bus power mismatches
    """
    pp.rundcpp(net, **kwargs)

    # add bus number to df of opf results
    net.res_gen["bus"] = net.gen.bus
    net.res_gen["type"] = net.gen.type

    net.res_load["bus"] = net.load.bus
    net.res_load["type"] = net.load.type

    net.res_sgen["bus"] = net.sgen.bus
    net.res_sgen["type"] = net.sgen.type

    net.res_shunt["bus"] = net.shunt.bus
    net.res_ext_grid["bus"] = net.ext_grid.bus
    net.res_bus["type"] = net.bus["type"]

    total_p_diff, total_q_diff = calculate_power_imbalance(net)

    print(
        "Total reactive power imbalance in DCPF: ",
        total_q_diff,
        " (It is normal that this is not 0 as we are using a DC model)",
    )
    print(
        "Total active power imbalance in DCPF: ",
        total_p_diff,
        " (Should be close to 0)",
    )

    return net.converged