Optimal Power Flow with AMPL and Python - data management#
Description: Optimal Power Flow with AMPL, Python and amplpy
Tags: AMPL, amplpy, Optimal Power Flow, Python
Notebook author: Nicolau Santos <nicolau@ampl.com>
# Install dependencies
%pip install -q amplpy pandas
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["coin"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
Introduction#
Optimal power flow models are tools of vital importance in the management of power grids. This field is in constant evolution and presents many oportunities for the practicioners, both from the theoretical and practical points of view.
This is the first of a series of notebooks in Optimal Power Flow in Python with AMPL and amplpy.
The notebooks are based on the online materials from Robert J. Vanderbei available here. and on the article Frank and Rebennack and aim to provide robust AMPL models for the variant of the problem presented. For detailed information on the theory of Optimal Power Flow please consult Frank and Rebennack .
In this first notebook we will take a look on how to pass data from Python to AMPL.
Power grid representation#
Optimal Power Flow is usually represented as a set \(N\) of buses (nodes) connected by a set \(L\) of branches (links) that represent elements in the power grid. A straightforward to represent this graph in AMPL is:
set N;
param nL;
set L within {1..nL, N, N};
We define a set N
to represent the buses, a parameter nL
to quantify the number of branches and a set L
with triplets representing the number of the branch, the initial bus and the final bus.
The AMPL implementation is slightly different from the mathematical model due to the possibility of multiple branches with the same initial and final bus.
As a concrete example let’s consider the data from the example in Frank and Rebennack. We have the set of buses
and a set of arcs
A Python representation of the data using lists and tuples is:
N = [1, 2, 3, 4, 5]
L = [(1,1,2), (2,1,3), (3,2,4), (4,3,4), (5,3,5), (6,4,5)]
Note the aditional first element in the tuples of the branch elements to express the number of the row.
We now create a file with the AMPL model. The model file will be loaded later. Another alternative would be to strore the model in a string and load it afterwards.
%%writefile example1.mod
set N;
param nL;
set L within {1..nL, N cross N};
Overwriting example1.mod
We now instantiate AMPL, read the model and assign the Python data structures with data to the corresponding AMPL entities. Afterwards we display the information of the corresponding sets and parameters.
ampl = AMPL()
ampl.read("example1.mod")
ampl.set["N"] = N
ampl.param["nL"] = len(L)
ampl.set["L"] = L
ampl.display("N", "nL", "L")
set N := 1 2 3 4 5;
nL = 6
set L := (1,1,2) (2,1,3) (3,2,4) (4,3,4) (5,3,5) (6,4,5);
As expected we have the same information. The formatting is different, since it comes directly from the AMPL interpreter.
Bus and Branch attributes#
We now present some common attributes of buses and branches in the grid.
Such information is modeled in AMPL as a parameter indexed by a set.
For example, for the bus shunt conductance \(g_{i}^{S}\) we create an AMPL parameter g_s{N}
and for the bus shunt susceptance
\(b_{i}^{S}\) we create an AMPL parameter b_s{N}
.
Some common branch attributes are (mathematical notation, parameter description, AMPL notation):
\(R_{i,k}\) resistance of branch \((i,k)\)
R[l,i,k]
\(X_{i,k}\) reactance of branch \((i,k)\)
X[l,i,k]
\(g_{i,k}^{Sh}\) shunt conductance of branch \((i,k)\)
g_sh[l,i,k]
\(b_{i,k}^{Sh}\) shunt susceptance of branch \((i,k)\)
b_sh[l,i,k]
We can think of these multiple parameters indexed by a common set as a relational table.
Rather than passing the data of these new parameters to AMPL one by one, as previously, we will group the information in Pandas Data Frames and pass the data related with one indexing set with one instruction. We first create a Data Frame for the bus information (df_bus) and another for the Branch information (df_branch).
Note that df_bus is created by columns, while df_branch is generated by rows.
The index in the Pandas DataFrame corresponds to the indexing set in the AMPL model.
To load the data into AMPL we use the set_data
method.
The first parameter of the function is the name of the Data Frame followed by the names of the indexing set in the AMPL model.
import pandas as pd
N = [1, 2, 3, 4, 5]
g_s = [0.0, 0.0, 0.05, 0.0, 0.0]
b_s = {1 : 0.0, 2 : 0.3, 3 : 0.0, 4 : 0.0, 5 : 0.0}
df_bus = pd.DataFrame()
df_bus.index = N
df_bus["g_s"] = g_s
df_bus["b_s"] = b_s
display(df_bus)
df_branch = pd.DataFrame(
[
[1, 1, 2, 0.0, 0.3, 0.0, 0.0, 1.0, 0.0],
[2, 1, 3, 0.023, 0.145, 0.0, 0.04, 1.0, 0.0],
[3, 2, 4, 0.006, 0.032, 0.0, 0.01, 1.0, 0.0],
[4, 3, 4, 0.02, 0.26, 0.0, 0.0, 1.0, -3.0],
[5, 3, 5, 0.0, 0.32, 0.0, 0.0, 0.98, 0.0],
[6, 4, 5, 0.0, 0.5, 0.0, 0.0, 1.0, 0.0]
],
columns=[
"row", "from", "to", "R", "X", "g_sh", "b_sh", "T", "phi"
]
).set_index(["row", "from", "to"])
display(df_branch)
g_s | b_s | |
---|---|---|
1 | 0.00 | 0.0 |
2 | 0.00 | 0.3 |
3 | 0.05 | 0.0 |
4 | 0.00 | 0.0 |
5 | 0.00 | 0.0 |
R | X | g_sh | b_sh | T | phi | |||
---|---|---|---|---|---|---|---|---|
row | from | to | ||||||
1 | 1 | 2 | 0.000 | 0.300 | 0.0 | 0.00 | 1.00 | 0.0 |
2 | 1 | 3 | 0.023 | 0.145 | 0.0 | 0.04 | 1.00 | 0.0 |
3 | 2 | 4 | 0.006 | 0.032 | 0.0 | 0.01 | 1.00 | 0.0 |
4 | 3 | 4 | 0.020 | 0.260 | 0.0 | 0.00 | 1.00 | -3.0 |
5 | 3 | 5 | 0.000 | 0.320 | 0.0 | 0.00 | 0.98 | 0.0 |
6 | 4 | 5 | 0.000 | 0.500 | 0.0 | 0.00 | 1.00 | 0.0 |
As previously, the AMPL model is stored in a file.
%%writefile example2.mod
set N;
param nL;
set L within 1..nL cross N cross N;
param g_s{N};
param b_s{N};
param T {L};
param phi {L};
param R {L};
param X {L};
param g_sh {L};
param b_sh {L};
Overwriting example2.mod
We instantiate AMPL, load the model and load the Pandas DataFrames. At the end we display the data indexed bus and by branch.
ampl = AMPL()
ampl.read("example2.mod")
# load the data
ampl.set_data(df_bus, "N")
ampl.param["nL"] = df_branch.shape[0]
ampl.set_data(df_branch, "L")
ampl.display("g_s", "b_s")
ampl.display("T", "phi", "R", "X", "g_sh", "b_sh")
: g_s b_s :=
1 0 0
2 0 0.3
3 0.05 0
4 0 0
5 0 0
;
: T phi R X g_sh b_sh :=
1 1 2 1 0 0 0.3 0 0
2 1 3 1 0 0.023 0.145 0 0.04
3 2 4 1 0 0.006 0.032 0 0.01
4 3 4 1 -3 0.02 0.26 0 0
5 3 5 0.98 0 0 0.32 0 0
6 4 5 1 0 0 0.5 0 0
;
Once again we have the same data, but comming directly from the AMPL interpreter. Note that the names of the indexing sets of the parameters are omitted.
Branch series admittance and bus admittance matrix#
So far we focused in how to pass data to AMPL. Another common task is to generate data based on some already loaded data. Such examples are (mathematical notation, parameter description, AMPL notation):
\(g_{i,k}\) series conductance of branch \((i,k)\)
g[l,i,k]
\(b_{i,k}\) series susceptance of branch \((i,k)\)
b[l,i,k]
\(G_{i,k}\) bus admittance matrix real part
G[l,i,k]
\(B_{i,k}\) bus admittance matrix imaginary part
B[l,i,k]
Branch series admittance real part is defined as
and imaginary part as
The corresponding AMPL implementation closely follows the mathematical notation. As with are dealing with branch related data we need to add the coefficient for multiple branches between nodes
param g {(l,k,m) in L} := R[l,k,m]/(R[l,k,m]^2 + X[l,k,m]^2); # series conductance
param b {(l,k,m) in L} := -X[l,k,m]/(R[l,k,m]^2 + X[l,k,m]^2); # series susceptance
Other parameter for which generation is required is the bus admittance matrix, with real part
and imaginary part
The AMPL implementation for \(G\) is
param G {(i,k) in YN} =
if (i == k) then (
g_s[i] +
sum{(l,i,u) in L} (g[l,i,u] + g_sh[l,i,u]/2)/T[l,i,u]**2 +
sum{(l,u,i) in L} (g[l,u,i] + g_sh[l,u,i]/2)
)
else (
-sum{(l,i,k) in L} ((
g[l,i,k]*cos(phi[l,i,k])-b[l,i,k]*sin(phi[l,i,k])
)/T[l,i,k]) -
sum{(l,k,i) in L} ((
g[l,k,i]*cos(phi[l,k,i])+b[l,k,i]*sin(phi[l,k,i])
)/T[l,k,i])
);
Note the use of an if then else clause for the case where \(i \neq k\). The procedure for \(B_{ik}\) is identical.
Bellow we present an AMPL model with all the components mentioned through the notebook.
%%writefile example3.mod
# data
set N; # set of buses in the network
param nL; # number of branches in the network
set L within 1..nL cross N cross N; # set of branches in the network
set YN := # index of the bus admittance matrix
setof{i in N} (i,i) union
union{(l,i,k) in L}{(i,k), (k,i)};
# bus data
param g_s {N}; # shunt conductance
param b_s {N}; # shunt susceptance
# branch data
param T {L}; # initial voltage ratio
param phi {L}; # initial phase angle
param R {L}; # branch resistance
param X {L}; # branch reactance
param g_sh {L}; # shunt conductance
param b_sh {L}; # shunt susceptance
param g {(l,k,m) in L} := R[l,k,m]/(R[l,k,m]^2 + X[l,k,m]^2); # series conductance
param b {(l,k,m) in L} := -X[l,k,m]/(R[l,k,m]^2 + X[l,k,m]^2); # series susceptance
# bus admittance matrix real part
param G {(i,k) in YN} =
if (i == k) then (
g_s[i] +
sum{(l,i,u) in L} (g[l,i,u] + g_sh[l,i,u]/2)/T[l,i,u]**2 +
sum{(l,u,i) in L} (g[l,u,i] + g_sh[l,u,i]/2)
)
else (
-sum{(l,i,k) in L} ((
g[l,i,k]*cos(phi[l,i,k])-b[l,i,k]*sin(phi[l,i,k])
)/T[l,i,k]) -
sum{(l,k,i) in L} ((
g[l,k,i]*cos(phi[l,k,i])+b[l,k,i]*sin(phi[l,k,i])
)/T[l,k,i])
);
# bus admittance matrix imaginary part
param B {(i,k) in YN} =
if (i == k) then (
b_s[i] +
sum{(l,i,u) in L} (b[l,i,u] + b_sh[l,i,u]/2)/T[l,i,u]**2 +
sum{(l,u,i) in L} (b[l,u,i] + b_sh[l,u,i]/2)
)
else (
-sum{(l,i,k) in L} (
g[l,i,k]*sin(phi[l,i,k])+b[l,i,k]*cos(phi[l,i,k])
)/T[l,i,k] -
sum{(l,k,i) in L} (
-g[l,k,i]*sin(phi[l,k,i])+b[l,k,i]*cos(phi[l,k,i])
)/T[l,k,i]
);
Overwriting example3.mod
As with previous examples we now instantiate an AMPL object and load the data. At the end we display the elements of the series conductance and susceptance \(g\) and \(b\), the elements of the bus bus admittance matrix index \(YN\) and the bus admittance matrix real and imaginary componentes, \(G\) and \(B\).
ampl = AMPL()
ampl.read("example3.mod")
# load the data
ampl.set_data(df_bus, "N")
ampl.param["nL"] = df_branch.shape[0]
ampl.set_data(df_branch, "L")
ampl.display("g", "b")
ampl.display("YN")
ampl.display("G", "B")
: g b :=
1 1 2 0 -3.33333
2 1 3 1.06709 -6.72729
3 2 4 5.66038 -30.1887
4 3 4 0.294118 -3.82353
5 3 5 0 -3.125
6 4 5 0 -2
;
set YN :=
(1,1) (3,3) (5,5) (2,1) (3,1) (4,2) (4,3) (5,3) (5,4)
(2,2) (4,4) (1,2) (1,3) (2,4) (3,4) (3,5) (4,5);
: G B :=
1 1 1.06709 -10.0406
1 2 0 3.33333
1 3 -1.06709 6.72729
2 1 0 3.33333
2 2 5.66038 -33.217
2 4 -5.66038 30.1887
3 1 -1.06709 6.72729
3 3 1.4112 -13.7847
3 4 0.830751 -3.74376
3 5 0 3.18878
4 2 -5.66038 30.1887
4 3 -0.248402 -3.82677
4 4 5.9545 -36.0072
4 5 0 2
5 3 0 3.18878
5 4 0 2
5 5 0 -5.125
;
Note that, at this point, the data for the branch series admittance and bus admittance matrix is not available in the Python session. If needed, it’s possible to retrieve the data using the corresponding AMPL entities. As an example we retrieve the data from \(G\) as a dictionary and the data from \(B\) as a Pandas DataFrame.
G = ampl.param["G"].to_dict()
B = ampl.param["B"].to_pandas()
print(G)
print(B)
{(1, 1): 1.067087315579475, (1, 2): 0, (1, 3): -1.067087315579475, (2, 1): 0, (2, 2): 5.660377358490567, (2, 4): -5.660377358490567, (3, 1): -1.067087315579475, (3, 3): 1.4112049626382985, (3, 4): 0.8307507651113879, (3, 5): 0, (4, 2): -5.660377358490567, (4, 3): -0.24840223769936126, (4, 4): 5.95449500554939, (4, 5): 0, (5, 3): 0, (5, 4): 0, (5, 5): 0}
B
index0 index1
1 1 -10.040623
2 3.333333
3 6.727290
2 1 3.333333
2 -33.217013
4 30.188679
3 1 6.727290
3 -13.784672
4 -3.743760
5 3.188776
4 2 30.188679
3 -3.826771
4 -36.007209
5 2.000000
5 3 3.188776
4 2.000000
5 -5.125000
Conclusion#
This concludes our brief introduction to data management of Optimal Power Flow with AMPL and Python.
In the next notebook we will provide an AMPL model to solve the Conventional Power Flow.
Bibliography#
Stephen Frank & Steffen Rebennack (2016) An introduction to optimal power flow: Theory, formulation, and examples, IIE Transactions, 48:12, 1172-1197.