This is an introduction into the usage of the pandapower optimal power flow. It shows how to set the constraints and the cost factors into the pandapower element tables.
We use the following four bus example network for this tutorial:
We first create this network in pandapower:
import pandapower as pp
import numpy as np
net = pp.create_empty_network()
#create buses
bus1 = pp.create_bus(net, vn_kv=220.)
bus2 = pp.create_bus(net, vn_kv=110.)
bus3 = pp.create_bus(net, vn_kv=110.)
bus4 = pp.create_bus(net, vn_kv=110.)
#create 220/110 kV transformer
pp.create_transformer(net, bus1, bus2, std_type="100 MVA 220/110 kV")
#create 110 kV lines
pp.create_line(net, bus2, bus3, length_km=70., std_type='149-AL1/24-ST1A 110.0')
pp.create_line(net, bus3, bus4, length_km=50., std_type='149-AL1/24-ST1A 110.0')
pp.create_line(net, bus4, bus2, length_km=40., std_type='149-AL1/24-ST1A 110.0')
#create loads
pp.create_load(net, bus2, p_mw=60, controllable=False)
pp.create_load(net, bus3, p_mw=70, controllable=False)
pp.create_load(net, bus4, p_mw=10, controllable=False)
#create generators
eg = pp.create_ext_grid(net, bus1, min_p_mw=-1000, max_p_mw=1000)
g0 = pp.create_gen(net, bus3, p_mw=80, min_p_mw=0, max_p_mw=80, vm_pu=1.01, controllable=True)
g1 = pp.create_gen(net, bus4, p_mw=100, min_p_mw=0, max_p_mw=100, vm_pu=1.01, controllable=True)
We specify the same costs for the power at the external grid and all generators to minimize the overall power feed in. This equals an overall loss minimization:
costeg = pp.create_poly_cost(net, 0, 'ext_grid', cp1_eur_per_mw=10)
costgen1 = pp.create_poly_cost(net, 0, 'gen', cp1_eur_per_mw=10)
costgen2 = pp.create_poly_cost(net, 1, 'gen', cp1_eur_per_mw=10)
We run an OPF:
pp.runopp(net, delta=1e-16)
hp.pandapower.run - INFO: These elements have missing power constraint values, which are considered in OPF as +- 1000 TW: ['gen'] hp.pandapower.run - INFO: 'min_vm_pu' is missing in bus table. In OPF these limits are considered as 0.0 pu. hp.pandapower.run - INFO: 'max_vm_pu' is missing in bus table. In OPF these limits are considered as 2.0 pu.
This function runs an Optimal Power Flow using the PYPOWER OPF. To make sure that the PYPOWER OPF converges, we decrease the power tolerance delta
(the default value is delta=1e-10
). The power tolerance delta
is a measure of the extent to which exceeding of minimum and maximum power limits is tolerated. That is, in above case, the limits considered by the OPF for the generators are min_p_mw - delta
and max_p_mw + delta
as lower and upper bound respectively on the active power.
Let's check the results:
net.res_ext_grid
p_mw | q_mvar | |
---|---|---|
0 | 56.530584 | 1.974564 |
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 71.309255 | -1.969681 | -3.713031 | 1.000008 |
1 | 12.303443 | -1.451180 | -3.712735 | 1.000010 |
Since all costs were specified the same, the OPF minimizes overall power generation, which is equal to a loss minimization in the network. The loads at buses 3 and 4 are supplied by generators at the same bus, the load at Bus 2 is provided by a combination of the other generators so that the power transmission leads to minimal losses.
Let's now assign individual costs to each generator.
We assign a cost of 10 ct/kW for the external grid, 15 ct/kw for the generator g0 and 12 ct/kw for generator g1:
net.poly_cost.cp1_eur_per_mw.at[costeg] = 10
net.poly_cost.cp1_eur_per_mw.at[costgen1] = 15
net.poly_cost.cp1_eur_per_mw.at[costgen2] = 12
And now run an OPF:
pp.runopp(net, delta=1e-16)
hp.pandapower.run - INFO: These elements have missing power constraint values, which are considered in OPF as +- 1000 TW: ['gen'] hp.pandapower.run - INFO: 'min_vm_pu' is missing in bus table. In OPF these limits are considered as 0.0 pu. hp.pandapower.run - INFO: 'max_vm_pu' is missing in bus table. In OPF these limits are considered as 2.0 pu.
We can see that all active power is provided by the external grid:
net.res_ext_grid
p_mw | q_mvar | |
---|---|---|
0 | 144.559166 | 9.193021 |
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 0.000079 | 8.601766 | -16.426835 | 0.967619 |
1 | 0.000225 | 10.594623 | -13.481007 | 0.989756 |
This makes sense, because the external grid has the lowest cost of all generators and we did not define any constraints.
The dispatch costs are given in net.res_cost:
net.res_cost
1445.5955448594823
Since all active power comes from the external grid and subsequently flows through the transformer, the transformer is overloaded with a loading of about 145%:
net.res_trafo.loading_percent
0 144.851179 Name: loading_percent, dtype: float64
We now limit the transformer loading to 50%:
net.trafo["max_loading_percent"] = 50
(the max_loading_percent parameter can also be specified directly when creating the transformer) and run the OPF:
pp.runopp(net, delta=1e-16)
hp.pandapower.run - INFO: These elements have missing power constraint values, which are considered in OPF as +- 1000 TW: ['gen'] hp.pandapower.run - INFO: 'min_vm_pu' is missing in bus table. In OPF these limits are considered as 0.0 pu. hp.pandapower.run - INFO: 'max_vm_pu' is missing in bus table. In OPF these limits are considered as 2.0 pu.
We can see that the transformer complies with the maximum loading:
net.res_trafo.loading_percent
0 49.999136 Name: loading_percent, dtype: float64
And power generation is now split between the external grid and generator 1 (which is the second cheapest generation unit):
net.res_ext_grid
p_mw | q_mvar | |
---|---|---|
0 | 49.953012 | -2.147126 |
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 0.000598 | 2.992989 | -6.232710 | 0.985230 |
1 | 93.304317 | 3.453173 | -1.237784 | 1.025709 |
This comes of course with an increase in dispatch costs:
net.res_cost
1619.1908981410575
Wen now look at the line loadings:
net.res_line.loading_percent
0 22.192863 1 57.476322 2 33.473473 Name: loading_percent, dtype: float64
and run the OPF with a 50% loading constraint:
net.line["max_loading_percent"] = 50
pp.runopp(net, delta=1e-16)
hp.pandapower.run - INFO: These elements have missing power constraint values, which are considered in OPF as +- 1000 TW: ['gen'] hp.pandapower.run - INFO: 'min_vm_pu' is missing in bus table. In OPF these limits are considered as 0.0 pu. hp.pandapower.run - INFO: 'max_vm_pu' is missing in bus table. In OPF these limits are considered as 2.0 pu.
Now the line loading constraint is complied with:
net.res_line.loading_percent
0 18.905589 1 49.999986 2 30.435895 Name: loading_percent, dtype: float64
And all generators are involved in supplying the loads:
net.res_ext_grid
p_mw | q_mvar | |
---|---|---|
0 | 49.787584 | -4.603451 |
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 9.136446 | 2.430957 | -5.815440 | 0.993015 |
1 | 83.592614 | 4.853496 | -1.511326 | 1.028887 |
This of course comes with a once again rising dispatch cost:
net.res_cost
1638.0339026783781
Finally, we have a look at the bus voltage:
net.res_bus
vm_pu | va_degree | p_mw | q_mvar | lam_p | lam_q | |
---|---|---|---|---|---|---|
0 | 1.000000 | 0.000000 | -49.787584 | 4.603451 | 10.000000 | -1.410210e-21 |
1 | 1.006024 | -3.408832 | 60.000000 | 0.000000 | 13.095255 | -5.409162e-02 |
2 | 0.993015 | -5.815440 | 60.863554 | -2.430957 | 14.999985 | 4.490949e-22 |
3 | 1.028887 | -1.511326 | -73.592614 | -4.853496 | 12.000007 | 1.781411e-21 |
and constrain it:
net.bus["min_vm_pu"] = 1.0
net.bus["max_vm_pu"] = 1.02
pp.runopp(net, delta=1e-16)
hp.pandapower.run - INFO: These elements have missing power constraint values, which are considered in OPF as +- 1000 TW: ['gen']
We can see that all voltages are within the voltage band:
net.res_bus
vm_pu | va_degree | p_mw | q_mvar | lam_p | lam_q | |
---|---|---|---|---|---|---|
0 | 1.000000 | 0.000000 | -49.906839 | 3.050610 | 10.000000 | -2.360898e-22 |
1 | 1.004168 | -3.421014 | 60.000000 | 0.000000 | 13.126862 | -2.133418e-02 |
2 | 1.000000 | -5.976093 | 59.278168 | -14.859011 | 14.999997 | 1.148963e-21 |
3 | 1.020000 | -1.366891 | -71.863462 | 9.172781 | 12.000002 | -7.089262e-22 |
And all generators are once again involved in supplying the loads:
net.res_ext_grid
p_mw | q_mvar | |
---|---|---|
0 | 49.906839 | -3.05061 |
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 10.721832 | 14.859011 | -5.976093 | 1.00 |
1 | 81.863462 | -9.172781 | -1.366891 | 1.02 |
This of course comes once again with rising dispatch costs:
net.res_cost
1642.2574101559053
pandapower also provides the possibility of running a DC Optimal Power Flow:
pp.rundcopp(net, delta=1e-16)
Since voltage magnitudes are not included in the DC power flow formulation, voltage constraints cannot be considered in the DC OPF:
net.res_bus
vm_pu | va_degree | p_mw | q_mvar | lam_p | lam_q | |
---|---|---|---|---|---|---|
0 | 1.000000 | 8.508637e-24 | -50.000000 | 3.050610 | 10.000000 | 0.0 |
1 | 1.004168 | -3.436967e+00 | 60.000000 | 0.000000 | 13.090909 | 0.0 |
2 | 1.000000 | -5.708566e+00 | 61.488746 | -14.859011 | 15.000000 | 0.0 |
3 | 1.020000 | -1.362340e+00 | -71.488747 | 9.172781 | 12.000000 | 0.0 |
Line and transformer loading limits are however complied with:
net.res_line
p_from_mw | q_from_mvar | p_to_mw | q_to_mvar | pl_mw | ql_mvar | i_from_ka | i_to_ka | i_ka | vm_from_pu | va_from_degree | vm_to_pu | va_to_degree | loading_percent | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 16.715233 | 0.0 | -16.715233 | 0.0 | 0.0 | 0.0 | 0.087732 | 0.087732 | 0.087732 | 1.0 | -3.436967 | 1.0 | -5.708566 | 18.666430 |
1 | -44.773513 | 0.0 | 44.773513 | 0.0 | 0.0 | 0.0 | 0.235000 | 0.235000 | 0.235000 | 1.0 | -5.708566 | 1.0 | -1.362340 | 50.000000 |
2 | 26.715233 | 0.0 | -26.715233 | 0.0 | 0.0 | 0.0 | 0.140219 | 0.140219 | 0.140219 | 1.0 | -1.362340 | 1.0 | -3.436967 | 29.833747 |
net.res_trafo
p_hv_mw | q_hv_mvar | p_lv_mw | q_lv_mvar | pl_mw | ql_mvar | i_hv_ka | i_lv_ka | vm_hv_pu | va_hv_degree | vm_lv_pu | va_lv_degree | loading_percent | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 50.0 | 0.0 | -50.0 | 0.0 | 0.0 | 0.0 | 0.131216 | 0.262432 | 1.0 | 8.508637e-24 | 1.0 | -3.436967 | 50.0 |
As are generator limits:
net.gen
name | bus | p_mw | vm_pu | sn_mva | min_q_mvar | max_q_mvar | scaling | slack | in_service | slack_weight | type | controllable | min_p_mw | max_p_mw | power_station_trafo | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | None | 2 | 80.0 | 1.01 | NaN | NaN | NaN | 1.0 | False | True | 0.0 | None | True | 0.0 | 80.0 | NaN |
1 | None | 3 | 100.0 | 1.01 | NaN | NaN | NaN | 1.0 | False | True | 0.0 | None | True | 0.0 | 100.0 | NaN |
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 8.511254 | 14.859011 | -5.708566 | 1.0 |
1 | 81.488747 | -9.172781 | -1.362340 | 1.0 |
The cost function is the same for the linearized OPF as for the non-linear one:
net.res_cost
1605.5337611101422
The OPF also offers piecewise linear cost functions. Let us first check the actual cost function setup:
net.poly_cost
element | et | cp0_eur | cp1_eur_per_mw | cp2_eur_per_mw2 | cq0_eur | cq1_eur_per_mvar | cq2_eur_per_mvar2 | |
---|---|---|---|---|---|---|---|---|
0 | 0 | ext_grid | 0.0 | 10.0 | 0.0 | 0.0 | 0.0 | 0.0 |
1 | 0 | gen | 0.0 | 15.0 | 0.0 | 0.0 | 0.0 | 0.0 |
2 | 1 | gen | 0.0 | 12.0 | 0.0 | 0.0 | 0.0 | 0.0 |
An element can either have polynomial costs or piecewise linear costs at the same time. So let us first delete the polynomial costs in order to avoid confusion and errors:
net.poly_cost.drop(net.poly_cost.index.values, inplace=True)
The results above have been produced with linear polynomial cost functions. Let's try to reproduce the results using piecewise linear cost functions. Costs have to be defined for the whole range of generators and external grids:
net.gen[["min_p_mw", "max_p_mw"]]
min_p_mw | max_p_mw | |
---|---|---|
0 | 0.0 | 80.0 |
1 | 0.0 | 100.0 |
net.ext_grid[["min_p_mw", "max_p_mw"]]
min_p_mw | max_p_mw | |
---|---|---|
0 | -1000.0 | 1000.0 |
We define the piecewise linear cost as constant over the whole range to reproduce the polyomial costs defined above:
pp.create_pwl_cost(net, 0, "gen", [[0, 80, 15]])
pp.create_pwl_cost(net, 1, "gen", [[0, 100, 12]])
pp.create_pwl_cost(net, 0, "ext_grid", [[-1000, 1000, 10]])
2
Let us check the results from the previous OPF again!
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 8.511254 | 14.859011 | -5.708566 | 1.0 |
1 | 81.488747 | -9.172781 | -1.362340 | 1.0 |
net.res_cost
1605.5337611101422
We run the same OPF now with different cost function setup. We should get the exact same results:
pp.rundcopp(net, delta=1e-16)
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 8.511254 | 14.859011 | -5.708566 | 1.0 |
1 | 81.488747 | -9.172781 | -1.362340 | 1.0 |
net.res_cost
1605.5337611101422
Now lets define real piecewise linear costs for generator 1. We define the costs as 12€/MW up to 70MW and 20€/MW from 70MW to 100MW:
net.pwl_cost.points.loc[1] = [[0, 70, 12], [70, 100, 20]]
And run another OPF:
pp.rundcopp(net, delta=1e-16)
Now we can see that generator 1 only dispatches 70MW, above which generator 0 becomes less expensive and is therefore dispatched:
net.res_gen
p_mw | q_mvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | 20.000007 | 14.859011 | -5.220652 | 1.0 |
1 | 69.999997 | -9.172781 | -1.641146 | 1.0 |
For more information on the status of the OPF solver, set verbose=True:
pp.runopp(net, verbose=True, delta=1e-16)
hp.pandapower.run - INFO: These elements have missing power constraint values, which are considered in OPF as +- 1000 TW: ['gen']
PYPOWER Version 5.1.4, 27-June-2018 -- AC Optimal Power Flow Python Interior Point Solver - PIPS, Version 1.0, 07-Feb-2011 Converged! Converged in 1.85 seconds Objective Function Value = 1666.07 $/hr ================================================================================ | PyPower (ppci) System Summary - these are not valid for pandapower DataFrames| ================================================================================ How many? How much? P (MW) Q (MVAr) --------------------- ------------------- ------------- ----------------- Buses 4 Total Gen Capacity 1180.0 -3000000000.0 to 3000000000.0 Generators 3 On-line Capacity 1180.0 -3000000000.0 to 3000000000.0 Committed Gens 3 Generation (actual) 141.7 1.0 Loads 3 Load 140.0 0.0 Fixed 3 Fixed 140.0 0.0 Dispatchable 0 Dispatchable 0.0 of 0.0 0.0 Shunts 0 Shunt (inj) 0.0 0.0 Branches 4 Losses (I^2 * Z) 1.65 6.34 Transformers 4 Branch Charging (inj) - 5.4 Inter-ties 0 Total Inter-tie Flow 0.0 0.0 Areas 1 Minimum Maximum ------------------------- -------------------------------- Voltage Magnitude 1.000 p.u. @ bus 0 1.020 p.u. @ bus 3 Voltage Angle -5.39 deg @ bus 2 0.00 deg @ bus 0 P Losses (I^2*R) - 1.05 MW @ line 2-3 Q Losses (I^2*X) - 3.00 MVAr @ line 0-1 Lambda P 10.00 $/MWh @ bus 0 15.00 $/MWh @ bus 2 Lambda Q -0.08 $/MWh @ bus 1 0.00 $/MWh @ bus 2 ================================================================================ | Area Summary | ================================================================================ Area # of # of Gens # of Loads # of # of # of # of Num Buses Total Online Total Fixed Disp Shunt Brchs Xfmrs Ties ---- ----- ----- ------ ----- ----- ----- ----- ----- ----- ----- 1 4 3 3 3 3 0 0 4 4 0 ---- ----- ----- ------ ----- ----- ----- ----- ----- ----- ----- Tot: 4 3 3 3 3 0 0 4 4 0 Area Total Gen Capacity On-line Gen Capacity Generation Num MW MVAr MW MVAr MW MVAr ---- ------ ------------------ ------ ------------------ ------ ------ 1 1180.0 -3000000000.0 to 3000000000.0 1180.0 -3000000000.0 to 3000000000.0 141.7 1.0 ---- ------ ------------------ ------ ------------------ ------ ------ Area Disp Load Cap Disp Load Fixed Load Total Load Num MW MVAr MW MVAr MW MVAr MW MVAr ---- ------ ------ ------ ------ ------ ------ ------ ------ 1 0.0 0.0 0.0 0.0 140.0 0.0 140.0 0.0 ---- ------ ------ ------ ------ ------ ------ ------ ------ Tot: 0.0 0.0 0.0 0.0 140.0 0.0 140.0 0.0 Area Shunt Inj Branch Series Losses Net Export Num MW MVAr Charging MW MVAr MW MVAr ---- ------ ------ -------- ------ ------ ------ ------ 1 0.0 0.0 5.4 1.65 6.34 0.0 0.0 ---- ------ ------ -------- ------ ------ ------ ------ Tot: 0.0 0.0 5.4 1.65 6.34 - - ================================================================================ | Generator Data | ================================================================================ Gen Bus Status Pg Qg Lambda ($/MVA-hr) # # (MW) (MVAr) P Q ---- ----- ------ -------- -------- -------- -------- 0 0 1 49.90 -3.17 10.00 -0.00 1 2 1 21.81 8.70 15.00 0.00 2 3 1 70.00 -4.56 14.11 -0.00 -------- -------- Total: 141.70 0.97 ================================================================================ | Bus Data | ================================================================================ Bus Voltage Generation Load Lambda($/MVA-hr) # Mag(pu) Ang(deg) P (MW) Q (MVAr) P (MW) Q (MVAr) P Q ----- ------- -------- -------- -------- -------- -------- ------- ------- 0 1.000 0.000* 49.90 -3.17 - - 10.000 - 1 1.004 -3.420 - - 60.00 0.00 14.482 -0.082 2 1.000 -5.387 21.81 8.70 70.00 0.00 15.000 - 3 1.020 -1.699 70.00 -4.56 10.00 0.00 14.114 - -------- -------- -------- -------- Total: 141.70 0.97 140.00 0.00 ================================================================================ | Branch Data | ================================================================================ Brnch From To From Bus Injection To Bus Injection Loss (I^2 * Z) # Bus Bus P (MW) Q (MVAr) P (MW) Q (MVAr) P (MW) Q (MVAr) ----- ----- ----- -------- -------- -------- -------- -------- -------- 0 1 2 12.68 -5.10 -12.48 3.17 0.196 0.41 1 2 3 -35.71 5.53 36.77 -5.00 1.055 2.23 2 3 1 23.23 0.44 -22.90 -1.09 0.333 0.70 3 0 1 49.90 -3.17 -49.78 6.19 0.065 3.00 -------- -------- Total: 1.649 6.34 ================================================================================ | Voltage Constraints | ================================================================================ Bus # Vmin mu Vmin |V| Vmax Vmax mu ----- -------- ----- ----- ----- -------- 0 0.000 1.000 1.000 1.000 392.325 1 - 1.000 1.004 1.020 - 2 144.754 1.000 1.000 1.020 - 3 - 1.000 1.020 1.020 20.418 ================================================================================ | Generation Constraints | ================================================================================ Gen Bus Active Power Limits # # Pmin mu Pmin Pg Pmax Pmax mu ---- ----- ------- -------- -------- -------- ------- 0 0 - -1000.00 49.90 1000.00 - 1 2 - -0.00 21.81 80.00 - 2 3 - -0.00 70.00 100.00 - Gen Bus Reactive Power Limits # # Qmin mu Qmin Qg Qmax Qmax mu --- --- ------- -------- -------- -------- ------- 0 0 - -1000000000.00 -3.171000000000.00 - 1 2 - -1000000000.00 8.701000000000.00 - 2 3 - -1000000000.00 -4.561000000000.00 - ================================================================================ | Dispatchable Load Constraints | ================================================================================ Gen Bus Active Power Limits # # Pmin mu Pmin Pg Pmax Pmax mu --- --- ------- -------- -------- -------- ------- Gen Bus Reactive Power Limits # # Qmin mu Qmin Qg Qmax Qmax mu --- --- ------- -------- -------- -------- ------- ================================================================================ | Branch Flow Constraints | ================================================================================ Brnch From "From" End Limit "To" End To # Bus |If| mu |If| |Imax| |It| |It| mu Bus ----- ----- ------- -------- -------- -------- ------- ----- 0 1 - 13.60 44.77 12.88 - 2 1 2 - 36.14 44.77 36.38 - 3 2 3 - 22.78 44.77 22.83 - 1 3 0 4.470 50.00 50.00 49.95 - 1