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_kw=60e3, controllable = False)
pp.create_load(net, bus3, p_kw=70e3, controllable = False)
pp.create_load(net, bus4, p_kw=10e3, controllable = False)
#create generators
eg = pp.create_ext_grid(net, bus1)
g0 = pp.create_gen(net, bus3, p_kw=-80*1e3, min_p_kw=-80e3, max_p_kw=0,vm_pu=1.01, controllable=True)
g1 = pp.create_gen(net, bus4, p_kw=-100*1e3, min_p_kw=-100e3, max_p_kw=0, 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_polynomial_cost(net, 0, 'ext_grid', np.array([1, 0]))
costgen1 = pp.create_polynomial_cost(net, 0, 'gen', np.array([1, 0]))
costgen2 = pp.create_polynomial_cost(net, 1, 'gen', np.array([1, 0]))
We run an OPF:
pp.runopp(net, verbose=True)
PYPOWER Version 5.0.0, 29-May-2015 -- AC Optimal Power Flow Python Interior Point Solver - PIPS, Version 1.0, 07-Feb-2011 Numerically failed. Did not converge in 2 iterations.
--------------------------------------------------------------------------- OPFNotConverged Traceback (most recent call last) <ipython-input-3-89e51745b9f9> in <module>() ----> 1 pp.runopp(net, verbose=True) C:\Users\fmeier\pandapower\pandapower\run.py in runopp(net, verbose, calculate_voltage_angles, check_connectivity, suppress_warnings, r_switch, delta, **kwargs) 290 voltage_depend_loads=False, delta=delta) 291 _add_opf_options(net, trafo_loading=trafo_loading, ac=ac) --> 292 _optimal_powerflow(net, verbose, suppress_warnings, **kwargs) 293 294 C:\Users\fmeier\pandapower\pandapower\optimal_powerflow.py in _optimal_powerflow(net, verbose, suppress_warnings, **kwargs) 52 53 if not result["success"]: ---> 54 raise OPFNotConverged("Optimal Power Flow did not converge!") 55 56 # ppci doesn't contain out of service elements, but ppc does -> copy results accordingly OPFNotConverged: Optimal Power Flow did not converge!
Apparently the OPF did not converge. Sometimes the constraints are too broad or too narrow for the OPF to converge. So let's try setting the voltage constraints first:
net.bus['max_vm_pu'] = 2
pp.runopp(net, verbose=True)
let's check the results:
net.res_ext_grid
net.res_gen
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.polynomial_cost.c.at[costeg] = np.array([[0.1, 0]])
net.polynomial_cost.c.at[costgen1] = np.array([[0.15, 0]])
net.polynomial_cost.c.at[costgen2] = np.array([[0.12, 0]])
And now run an OPF:
pp.runopp(net, verbose=True)
PYPOWER Version 5.0.0, 29-May-2015 -- AC Optimal Power Flow Python Interior Point Solver - PIPS, Version 1.0, 07-Feb-2011 Numerically failed. Did not converge in 32 iterations.
--------------------------------------------------------------------------- OPFNotConverged Traceback (most recent call last) <ipython-input-5-89e51745b9f9> in <module>() ----> 1 pp.runopp(net, verbose=True) C:\Users\fmeier\pandapower\pandapower\run.py in runopp(net, verbose, calculate_voltage_angles, check_connectivity, suppress_warnings, r_switch, delta, **kwargs) 290 voltage_depend_loads=False, delta=delta) 291 _add_opf_options(net, trafo_loading=trafo_loading, ac=ac) --> 292 _optimal_powerflow(net, verbose, suppress_warnings, **kwargs) 293 294 C:\Users\fmeier\pandapower\pandapower\optimal_powerflow.py in _optimal_powerflow(net, verbose, suppress_warnings, **kwargs) 52 53 if not result["success"]: ---> 54 raise OPFNotConverged("Optimal Power Flow did not converge!") 55 56 # ppci doesn't contain out of service elements, but ppc does -> copy results accordingly OPFNotConverged: Optimal Power Flow did not converge!
We can see that all active power is provided by the external grid. 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
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
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)
We can see that the transformer complies with the maximum loading:
net.res_trafo
p_hv_kw | q_hv_kvar | p_lv_kw | q_lv_kvar | pl_kw | ql_kvar | i_hv_ka | i_lv_ka | loading_percent | |
---|---|---|---|---|---|---|---|---|---|
0 | 49953.861634 | -2147.313334 | -49833.810965 | 5167.452594 | 120.050669 | 3020.13926 | 0.131216 | 0.262153 | 49.999992 |
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_kw | q_kvar | |
---|---|---|
0 | -49953.861634 | 2147.313334 |
net.res_gen
p_kw | q_kvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | -0.009347 | -2993.145553 | -6.232828 | 0.985230 |
1 | -93304.076626 | -3453.342439 | -1.237884 | 1.025709 |
This comes of course with an increase in dispatch costs:
net.res_cost
16191.876760535753
Wen now look at the line loadings:
net.res_line
p_from_kw | q_from_kvar | p_to_kw | q_to_kvar | pl_kw | ql_kvar | i_from_ka | i_to_ka | i_ka | loading_percent | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 19780.007620 | -2479.435263 | -19341.693479 | 1104.392907 | 438.314142 | -1375.042356 | 0.104309 | 0.103207 | 0.104309 | 22.193318 |
1 | -50658.297175 | 1888.752646 | 52783.695174 | 921.063740 | 2125.398000 | 2809.816386 | 0.270061 | 0.270140 | 0.270140 | 57.476546 |
2 | 30520.381452 | 2532.278698 | -29946.196656 | -2688.017331 | 574.184796 | -155.738633 | 0.156712 | 0.157323 | 0.157323 | 33.472995 |
and run the OPF with a 50% loading constraint:
net.line["max_loading_percent"] = 50
pp.runopp(net, verbose=True)
PYPOWER Version 5.0.0, 29-May-2015 -- AC Optimal Power Flow Python Interior Point Solver - PIPS, Version 1.0, 07-Feb-2011 Converged!
Now the line loading constraint is complied with:
net.res_line
p_from_kw | q_from_kvar | p_to_kw | q_to_kvar | pl_kw | ql_kvar | i_from_ka | i_to_ka | i_ka | loading_percent | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 16727.532318 | -3194.198556 | -16412.740583 | 1533.356250 | 314.791735 | -1660.842305 | 0.088848 | 0.087130 | 0.088848 | 18.903796 |
1 | -44451.211099 | 867.137901 | 46059.955230 | 832.560223 | 1608.744131 | 1699.698124 | 0.235000 | 0.235000 | 0.235000 | 49.999997 |
2 | 27533.096563 | 4051.891931 | -27060.152701 | -4429.882352 | 472.943862 | -377.990422 | 0.141965 | 0.143058 | 0.143058 | 30.437792 |
And all generators are involved in supplying the loads:
net.res_ext_grid
p_kw | q_kvar | |
---|---|---|
0 | -49787.594269 | 4603.789001 |
net.res_gen
p_kw | q_kvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | -9136.048164 | -2400.493544 | -5.814910 | 0.992992 |
1 | -83593.051621 | -4884.451800 | -1.511686 | 1.028900 |
This of course comes with a once again rising dispatch cost:
net.res_cost
16380.332845968122
Finally, we have a look at the bus voltage:
net.res_bus
vm_pu | va_degree | p_kw | q_kvar | lam_p | lam_q | |
---|---|---|---|---|---|---|
0 | 1.000000 | 0.000000 | -49787.594269 | 4603.789001 | 100.000000 | -1.673841e-21 |
1 | 1.006025 | -3.408832 | 60000.000000 | 0.000000 | 130.952237 | -5.410368e-01 |
2 | 0.992992 | -5.814910 | 60863.951836 | -2400.493544 | 149.999983 | 7.893715e-22 |
3 | 1.028900 | -1.511686 | -73593.051621 | -4884.451800 | 120.000009 | 1.859168e-21 |
and constrain it:
net.bus["min_vm_pu"] = 1.0
net.bus["max_vm_pu"] = 1.02
pp.runopp(net)
We can see that all voltages are within the voltage band:
net.res_bus
vm_pu | va_degree | p_kw | q_kvar | lam_p | lam_q | |
---|---|---|---|---|---|---|
0 | 1.000000 | 0.000000 | -49906.847832 | 3050.617583 | 100.000000 | -5.175965e-22 |
1 | 1.004168 | -3.421015 | 60000.000000 | 0.000000 | 131.268594 | -2.133680e-01 |
2 | 1.000000 | -5.976094 | 59278.207273 | -14858.927798 | 149.999993 | 2.520634e-21 |
3 | 1.020000 | -1.366892 | -71863.493411 | 9172.685486 | 120.000004 | -1.555865e-21 |
And all generators are once again involved in supplying the loads:
net.res_ext_grid
p_kw | q_kvar | |
---|---|---|
0 | -49906.847832 | 3050.617583 |
net.res_gen
p_kw | q_kvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | -10721.792727 | -14858.927798 | -5.976094 | 1.00 |
1 | -81863.493411 | 9172.685486 | -1.366892 | 1.02 |
This of course comes once again with rising dispatch costs:
net.res_cost
16422.572901622079
pandapower also provides the possibility of running a DC Optimal Power Flow:
pp.rundcopp(net)
Since voltage magnitudes are not included in the DC power flow formulation, voltage constraints canot be considered in the DC OPF:
net.res_bus
vm_pu | va_degree | p_kw | q_kvar | lam_p | lam_q | |
---|---|---|---|---|---|---|
0 | NaN | 0.000000 | -49999.999962 | NaN | 100.000000 | 0.0 |
1 | NaN | -3.436967 | 60000.000000 | NaN | 130.909091 | 0.0 |
2 | NaN | -5.708566 | 61488.746675 | NaN | 150.000000 | 0.0 |
3 | NaN | -1.362340 | -71488.746713 | NaN | 120.000000 | 0.0 |
Line and transformer loading limits are however complied with:
net.res_line
p_from_kw | q_from_kvar | p_to_kw | q_to_kvar | pl_kw | ql_kvar | i_from_ka | i_to_ka | i_ka | loading_percent | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 16715.233326 | NaN | -16715.233326 | NaN | NaN | NaN | 0.087732 | 0.087732 | 0.087732 | 18.666430 |
1 | -44773.513348 | NaN | 44773.513348 | NaN | NaN | NaN | 0.235000 | 0.235000 | 0.235000 | 50.000000 |
2 | 26715.233365 | NaN | -26715.233365 | NaN | NaN | NaN | 0.140219 | 0.140219 | 0.140219 | 29.833747 |
net.res_trafo
p_hv_kw | q_hv_kvar | p_lv_kw | q_lv_kvar | pl_kw | ql_kvar | i_hv_ka | i_lv_ka | loading_percent | |
---|---|---|---|---|---|---|---|---|---|
0 | 49999.999962 | NaN | -49999.999962 | NaN | NaN | NaN | 0.131216 | 0.262432 | 50.0 |
As are generator limits:
net.gen
name | bus | p_kw | vm_pu | sn_kva | min_q_kvar | max_q_kvar | scaling | in_service | type | min_p_kw | max_p_kw | controllable | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | None | 2 | -80000.0 | 1.01 | NaN | NaN | NaN | 1.0 | True | None | -80000.0 | 0.0 | True |
1 | None | 3 | -100000.0 | 1.01 | NaN | NaN | NaN | 1.0 | True | None | -100000.0 | 0.0 | True |
net.res_gen
p_kw | q_kvar | va_degree | vm_pu | |
---|---|---|---|---|
0 | -8511.253325 | NaN | -5.708566 | 1.0 |
1 | -81488.746713 | NaN | -1.362340 | 1.0 |
The cost function is the same for the linearized OPF as for the non-linear one:
net.res_cost
16055.337600528508
Piecewise linear cost functions The OPF also offers us piecewise linear cost functions. Let us first check the actual cost function setup:
net.polynomial_cost
type | element | element_type | c | |
---|---|---|---|---|
0 | p | 0 | ext_grid | [[0.1, 0.0]] |
1 | p | 0 | gen | [[0.15, 0.0]] |
2 | p | 1 | gen | [[0.12, 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.polynomial_cost= net.polynomial_cost.drop(net.polynomial_cost.index.values)
The results above have been produced with polynomial cost functions, that were linear. Let's try to reproduce the results using piecewise linear cost functions. Note: The cost functions need to have the same gradient!
pp.create_piecewise_linear_cost(net, 0, "gen", np.array([[0, 0], [1 , 0.15]]))
pp.create_piecewise_linear_cost(net, 1, "gen", np.array([[0, 0], [1, 0.12]]))
pp.create_piecewise_linear_cost(net, 0, "ext_grid", np.array([[0, 0], [1, 0.1]]))
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-33-90f53fab73a0> in <module>() ----> 1 pp.create_piecewise_linear_cost(net, 0, "gen", np.array([[0, 0], [1 , 0.15]])) 2 pp.create_piecewise_linear_cost(net, 1, "gen", np.array([[0, 0], [1, 0.12]])) 3 pp.create_piecewise_linear_cost(net, 0, "ext_grid", np.array([[0, 0], [1, 0.1]])) C:\Users\fmeier\pandapower\pandapower\create.py in create_piecewise_linear_cost(net, element, element_type, data_points, type, index) 2127 if not (net[element_type].max_p_kw.at[element] <= max(p) and 2128 net[element_type].min_p_kw.at[element] >= min(p)): -> 2129 raise ValueError("Cost function must be defined for whole power range of the " 2130 "generator") 2131 if type == "q": ValueError: Cost function must be defined for whole power range of the generator
What we forgot is that the piecewise linear function should be defined for the whole range of the generator. The range is determined by p_max and p_min. Let's check:
net.gen.max_p_kw
0 0.0 1 0.0 Name: max_p_kw, dtype: float64
net.gen.min_p_kw
0 -80000.0 1 -100000.0 Name: min_p_kw, dtype: float64
We try again:
pp.create_piecewise_linear_cost(net, 0, "gen", np.array([[-80000* 1 , -80000*0.15], [0, 0]]))
pp.create_piecewise_linear_cost(net, 1, "gen", np.array([[-100000*1, -100000*0.12], [0, 0]]))
1
An external grid usually has no operational limits, but this is a problem for the OPF:
pp.create_piecewise_linear_cost(net, 0, "ext_grid", np.array([[0, 0], [1, 0.1]]))
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-37-42b079913cbe> in <module>() ----> 1 pp.create_piecewise_linear_cost(net, 0, "ext_grid", np.array([[0, 0], [1, 0.1]])) C:\Users\fmeier\pandapower\pandapower\create.py in create_piecewise_linear_cost(net, element, element_type, data_points, type, index) 2124 if not (hasattr(net[element_type], "max_p_kw") and hasattr(net[element_type], 2125 "min_p_kw")): -> 2126 raise AttributeError("No operational constraints defined for controllable element!") 2127 if not (net[element_type].max_p_kw.at[element] <= max(p) and 2128 net[element_type].min_p_kw.at[element] >= min(p)): AttributeError: No operational constraints defined for controllable element!
So we set imaginary constraints, that we can choose very broad:
net.ext_grid["max_p_kw"] = 1e9
net.ext_grid["min_p_kw"] = -1e9
net.ext_grid
name | bus | vm_pu | va_degree | in_service | max_p_kw | min_p_kw | |
---|---|---|---|---|---|---|---|
0 | None | 0 | 1.0 | 0.0 | True | 1.000000e+09 | -1.000000e+09 |
pp.create_piecewise_linear_cost(net, 0, "ext_grid", np.array([[-1e9, -1e9*.1], [1e9, 1e9*0.1]]))
2
Let us check the results from the previous OPF again!
net.res_bus
vm_pu | va_degree | p_kw | q_kvar | lam_p | lam_q | |
---|---|---|---|---|---|---|
0 | NaN | 0.000000 | -49999.999962 | NaN | 100.000000 | 0.0 |
1 | NaN | -3.436967 | 60000.000000 | NaN | 130.909091 | 0.0 |
2 | NaN | -5.708566 | 61488.746675 | NaN | 150.000000 | 0.0 |
3 | NaN | -1.362340 | -71488.746713 | NaN | 120.000000 | 0.0 |
net.res_cost
16055.337600528508
We run the same OPF now with different cost function setup. We should get the exact same results:
pp.rundcopp(net)
net.res_cost
16055.337600298368
net.res_bus
vm_pu | va_degree | p_kw | q_kvar | lam_p | lam_q | |
---|---|---|---|---|---|---|
0 | NaN | -6.163691e-25 | -49999.999965 | NaN | 100.000000 | 0.0 |
1 | NaN | -3.436967e+00 | 60000.000000 | NaN | 130.909091 | 0.0 |
2 | NaN | -5.708566e+00 | 61488.746680 | NaN | 150.000000 | 0.0 |
3 | NaN | -1.362340e+00 | -71488.746715 | NaN | 120.000000 | 0.0 |