This is an introduction into the usage of the pandapower state estimation module. It shows how to create measurements for a pandapower network and how to perform a state estimation with the weighted least squares (WLS) algorithm.
We will be using the reference network from the book "Power System State Estimation" by Ali Abur and Antonio Gómez Expósito. It contains 3 buses with connecting lines between buses 1-2, 1-3 and 2-3. 8 measurements of different types enable WLS state estimation.
We first create this network in pandapower.
import pandapower as pp
net = pp.create_empty_network()
b1 = pp.create_bus(net, name="bus 1", vn_kv=1., index=1)
b2 = pp.create_bus(net, name="bus 2", vn_kv=1., index=2)
b3 = pp.create_bus(net, name="bus 3", vn_kv=1., index=3)
pp.create_ext_grid(net, 1) # set the slack bus to bus 1
l1 = pp.create_line_from_parameters(net, 1, 2, 1, r_ohm_per_km=.01, x_ohm_per_km=.03, c_nf_per_km=0., max_i_ka=1)
l2 = pp.create_line_from_parameters(net, 1, 3, 1, r_ohm_per_km=.02, x_ohm_per_km=.05, c_nf_per_km=0., max_i_ka=1)
l3 = pp.create_line_from_parameters(net, 2, 3, 1, r_ohm_per_km=.03, x_ohm_per_km=.08, c_nf_per_km=0., max_i_ka=1)
net
This pandapower network includes the following parameter tables: - bus (3 elements) - ext_grid (1 element) - line (3 elements)
Now we can add our measurements, which are valid for one point in time.
We add two voltage magnitude measurements on buses 1 / 2 with voltage magnitude of 1.006 pu / 0.968 pu and a standard deviation of 0.004 pu each:
Measurements are defined via the pandapower create_measurement function.
The physical properties which can be measured are set with the type argument and can be one of the following: "p" for active power, "q" for reactive power, "v" for voltage and "i" for electrical current.
The element is set with the element_type argument, it can be either "bus", "line" or "transformer".
Power is measured in kW / kVar, voltage in per unit and current in A. Bus power injections are positive if power is generated at the bus and negative if it is consumed.
pp.create_measurement(net, "v", "bus", 1.006, .004, element=b1) # V at bus 1
pp.create_measurement(net, "v", "bus", 0.968, .004, element=b2) # V at bus 2
net.measurement
name | measurement_type | element_type | element | value | std_dev | side | |
---|---|---|---|---|---|---|---|
0 | None | v | bus | 1 | 1.006 | 0.004 | None |
1 | None | v | bus | 2 | 0.968 | 0.004 | None |
We add bus injection measurements on bus 2 with P=-501 kW and Q=-286kVar and standard deviations of 10kVA:
pp.create_measurement(net, "p", "bus", 0.501, 0.01, element=b2) # P at bus 2
pp.create_measurement(net, "q", "bus", 0.286, 0.01, element=b2) # Q at bus 2
net.measurement
name | measurement_type | element_type | element | value | std_dev | side | |
---|---|---|---|---|---|---|---|
0 | None | v | bus | 1 | 1.006 | 0.004 | None |
1 | None | v | bus | 2 | 0.968 | 0.004 | None |
2 | None | p | bus | 2 | -0.501 | 0.010 | None |
3 | None | q | bus | 2 | -0.286 | 0.010 | None |
Finally, we add line measurements for lines 0 and 1, both placed at the side of bus 1. The bus parameter defines the bus at which the line measurement is positioned, the line argument is the index of the line.
pp.create_measurement(net, "p", "line", 0.888, 0.008, element=l1, side=b1) # Pline (bus 1 -> bus 2) at bus 1
pp.create_measurement(net, "p", "line", 1.173, 0.008, element=l2, side=b1) # Pline (bus 1 -> bus 3) at bus 1
pp.create_measurement(net, "q", "line", 0.568, 0.008, element=l1, side=b1) # Qline (bus 1 -> bus 2) at bus 1
pp.create_measurement(net, "q", "line", 0.663, 0.008, element=l2, side=b1) # Qline (bus 1 -> bus 3) at bus 1
net.measurement
name | measurement_type | element_type | element | value | std_dev | side | |
---|---|---|---|---|---|---|---|
0 | None | v | bus | 1 | 1.006 | 0.004 | None |
1 | None | v | bus | 2 | 0.968 | 0.004 | None |
2 | None | p | bus | 2 | -0.501 | 0.010 | None |
3 | None | q | bus | 2 | -0.286 | 0.010 | None |
4 | None | p | line | 0 | 0.888 | 0.008 | 1 |
5 | None | p | line | 1 | 1.173 | 0.008 | 1 |
6 | None | q | line | 0 | 0.568 | 0.008 | 1 |
7 | None | q | line | 1 | 0.663 | 0.008 | 1 |
The measurements are now set. We have to initialize the starting voltage magnitude and voltage angles for the state estimator. In continuous operation, this can be the result of the last state estimation. In our case, we set flat start conditions: 1.0 p.u. for voltage magnitude, 0.0 degree for voltage angles. This is easily done with the parameter "init", which we define as "flat".
And now run the state estimation. Afterwards, the result will be stored in the table res_bus_est.
from pandapower.estimation import estimate
success = estimate(net, init='flat')
print(success)
True
The state estimation class allows additionally the removal of bad data, especially single or non-interacting false measurements. For detecting bad data the Chi-squared distribution is used to identify the presence of them. Afterwards follows the largest normalized residual test that identifys the actual measurements which will be removed at the end.
To test this function we will add a single false measurement to the network (active power flow of line 1 at bus 3):
pp.create_measurement(net, "p", "line", 1.0, 0.008, element=l1, side=b3) # Pline (bus 1 -> bus 2) at bus 3
net.measurement
name | measurement_type | element_type | element | value | std_dev | side | |
---|---|---|---|---|---|---|---|
0 | None | v | bus | 1 | 1.006 | 0.004 | None |
1 | None | v | bus | 2 | 0.968 | 0.004 | None |
2 | None | p | bus | 2 | -0.501 | 0.010 | None |
3 | None | q | bus | 2 | -0.286 | 0.010 | None |
4 | None | p | line | 0 | 0.888 | 0.008 | 1 |
5 | None | p | line | 1 | 1.173 | 0.008 | 1 |
6 | None | q | line | 0 | 0.568 | 0.008 | 1 |
7 | None | q | line | 1 | 0.663 | 0.008 | 1 |
8 | None | p | line | 0 | 1.000 | 0.008 | 3 |
The next step is the call of the largest normalized residual test's wrapper function remove_bad_data that handles the removal of the added false measurement, and returns a identication of success of the state estimation. The argument structure of this function is similiar to the estimate function (compare above). It only provides further adjustments according to the maximum allowed normalized residual ("rn_max_threshold"), and the probability of false required by the chi-squared test ("chi2_prob_false").
from pandapower.estimation import remove_bad_data
import numpy as np
success_rn_max = remove_bad_data(net, init='flat', rn_max_threshold=3.0)
print(success_rn_max)
True
The management of results will be the same like for the estimate function (see following section).
We can show the voltage magnitude and angles directly:
net.res_bus_est.vm_pu
0 0.999629 1 0.974156 2 0.943890 Name: vm_pu, dtype: float64
net.res_bus_est.va_degree
0 0.000000 1 -1.247547 2 -2.745717 Name: va_degree, dtype: float64
The results match exactly with the results from the book: Voltages 0.9996, 0.9742, 0.9439; Voltage angles 0.0, -1.2475, -2.7457). Nice!
Let's look at the bus power injections, which are available in res_bus_est as well
net.res_bus_est.p_mw
0 -2.064016 1 0.495975 2 1.514221 Name: p_mw, dtype: float64
net.res_bus_est.q_mvar
0 -1.226440 1 0.297750 2 0.787529 Name: q_mvar, dtype: float64
We can also compare the resulting line power flows with the measurements.
net.res_line_est.p_from_mw
0 0.892992 1 1.171024 2 0.385912 Name: p_from_mw, dtype: float64
net.res_line_est.q_from_mvar
0 0.558822 1 0.667619 2 0.227756 Name: q_from_mvar, dtype: float64
Again, this values do match the estimated values from our reference book. This concludes the small tutorial how to perform state estimation with a pandapower network.