Single-Period OPF

This guide walks through a complete AC Optimal Power Flow run using a SimBench low-voltage network.

Choosing a model

Class Formulation Use case
ACOPF Full nonlinear AC General-purpose OPF
DCOPF Linearised DC Fast screening, no reactive power
HC_ACOPF AC + binary wind placement Hosting capacity studies

Step 1 — Load a network

Any pandapower network works. SimBench provides realistic distribution grids with pre-configured load and generation profiles:

import simbench as sb
import pandapower as pp

net = sb.get_simbench_net("1-LV-rural1--0-sw")

You can also build a network manually with pandapower.create_* helpers.

Step 2 — Configure limits

Operational limits are read directly from the pandapower network's element tables. Set them before constructing the model:

# Voltage limits on buses
net.bus["max_vm_pu"] = 1.05
net.bus["min_vm_pu"] = 0.95

# Generator power limits
net.ext_grid["max_p_mw"] = 1000.
net.ext_grid["min_p_mw"] = -1000.
net.ext_grid["max_q_mvar"] = 1000.
net.ext_grid["min_q_mvar"] = -1000.

# Mark static generators as controllable
net.sgen["controllable"] = True
net.sgen["max_p_mw"] = net.sgen["p_mw"]
net.sgen["min_p_mw"] = 0.

# Line thermal limits (percent of rated current)
net.line["max_loading_percent"] = 100.

Step 3 — Build the model

Pass the network to the model constructor. The constructor runs pp.runpp() internally and maps the network data to Pyomo sets and parameters:

from potpourri.models.ACOPF_base import ACOPF

opf = ACOPF(net)

Step 4 — Add OPF constraints and objective

opf.add_OPF()                              # power and thermal limits
opf.add_voltage_deviation_objective()      # minimise (v - 1)²

Alternative objectives:

opf.add_reactive_power_flow_objective()    # minimise Σ qsG²

Step 5 — Solve

opf.solve(solver="ipopt", print_solver_output=False)

Supported solvers:

Solver Type Notes
"ipopt" NLP Default; best for AC OPF
"mindtpy" MINLP For discrete tap changers; requires mip_solver kwarg
"neos" Remote Uses NEOS server; set neos_opt kwarg

Step 6 — Access results

Results are written to the pandapower result tables on opf.net:

print(opf.net.res_bus[["vm_pu", "va_degree"]])
print(opf.net.res_line[["p_from_mw", "loading_percent"]])
print(opf.net.res_sgen[["p_mw", "q_mvar"]])

Tap changer optimisation

To include transformer tap ratios as continuous decision variables:

opf.add_tap_changer_linear()
opf.solve(solver="ipopt")

For discrete tap positions (requires a MIP solver):

opf.add_tap_changer_discrete()
opf.solve(solver="mindtpy", mip_solver="gurobi", print_solver_output=False)

Hosting capacity analysis

HC_ACOPF maximises wind generation subject to Q-curve grid code constraints. If sgen.wind_hc is not set, candidate wind generators are automatically placed at every non-external-grid bus:

from potpourri.models.HC_ACOPF import HC_ACOPF

hc = HC_ACOPF(net)
hc.add_OPF()
hc.solve(solver="ipopt")

# Binary y[w] indicates which wind locations are active
for w in hc.model.WIND_HC:
    print(f"Bus {hc.net.sgen.bus[w]}: y={hc.model.y[w].value:.0f}, "
          f"P={hc.model.psG[w].value * hc.model.baseMVA:.3f} MW")

To run a weighted wind-generation vs. loss objective:

hc.add_loss_obj()
hc.model.eps.set_value(0.7)   # 70% weight on wind, 30% on loss reduction
hc.solve(solver="ipopt")