-
Notifications
You must be signed in to change notification settings - Fork 502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve pp.diagnostic(net) and validation #786
Comments
Thanks a lot for sharing your experience, much appreciated! Could these two issues
Be tackled by just scaling down line length and looking at convergence? This issue:
should not affect convergence, since the real part is capped to the maximum internally. Its still a wrong modeling as you say, so it makes to flag it in the diagnostic. The nan issue:
could maybe be tackled easily by checking for nan in the admittance matrix? This would be an easy approach to flag that there is an nan somewhere - of course it would still be nice to pinpoint the exact elements with nans. This would also be really helpful for short-circuit calculations, where additional parameters are needed (e.g. xdss_pu for net.gen) that are often not available in systems that are made for power flow and lead to nans in the admittance matrix.
This is great initiative, it would be very helpful to have something like that. We had a very similar idea some time ago, but as far as I know never implemented it. I think our idea was more to set all buses out of service except the ext_grid, and then setting buses in service starting at the ext_grid: first all buses that are directly connected to the ext_grid, then buses that are one branch away, two branches away etc. And then checking at which point the power flow fails, to find the culprit. Your approach looks very similar to that, although you are looking at the different voltage levels. But what about if there is only one voltage level? Or if you have narrowed it down to one voltage level, but that still consists of >100 buses? Of course with the approach described above, an open question would be how to tackle grid with multiple ext_grids... Do you have an idea how these approaches could be combined? |
As for c_nf_per_km, and the effect of scaling the voltagedef convergence_test(vn_kv_factor, c_nf_factor):
net = cig.deepcopy()
net.bus.vn_kv *= vn_kv_factor
net.trafo.vn_hv_kv *= vn_kv_factor
net.trafo.vn_lv_kv *= vn_kv_factor
net.shunt.vn_kv *= vn_kv_factor
net.line.c_nf_per_km *= c_nf_factor
try:
pp.runpp(net)
return 1
except Exception as e:
return np.nan These regions of convergence look surprisingly weird/interesting. |
Exploring vk_percent and vkr_percent convergence a bit more:To find the border of the "triangle"-tip of the iceberg with more precision, we can use a binary search: Code
import pandapower as pp
import pandapower.networks as nw
import numpy as np
import matplotlib.pyplot as plt
cig = nw.cigre_networks.create_cigre_network_mv()
def convergence_test(vk_percent, vkr_percent):
net = cig.deepcopy() # Deepcopy is much faster than loading the network again
net.trafo.vk_percent = vk_percent
net.trafo.vkr_percent = vkr_percent
try:
pp.runpp(net)
return 1
except Exception as e:
return np.nan
# Binary search to find the limit between convergence and divergence for vkr_percent
def find_vkr_percent_limit(vk_percent, vkr_conv, vkr_div, tolerance=1e-3):
if abs(vkr_div - vkr_conv) < tolerance: return vkr_conv
vkr_test = 0.5 * (vkr_div + vkr_conv)
if np.isnan(convergence_test(vk_percent, vkr_test)):
return find_vkr_percent_limit(vk_percent, vkr_conv, vkr_test, tolerance)
else:
return find_vkr_percent_limit(vk_percent, vkr_test, vkr_div, tolerance)
# Assuming vkr_percent=0 converges, vk_percent=40 diverges, the limits will be found:
vk_percent = np.linspace(0, 50, 151)
vkr_percent = np.vectorize(find_vkr_percent_limit)(vk_percent, vkr_conv=0.0, vkr_div=40.0)
plt.plot(vk_percent, vkr_percent) Notice that up to some value for vk_percent, 0 <= vkr_percent <= vk_percent will cause converge. But we do actually get divergence here when vkr_percent > vk_percent. The trailing 0s in the end of the plot means that no solution between 0 and 40 was found. |
Very nice analysis! Adding some of this to the diagnostic function would be really cool. I'm curious if anyone has anything insightful to say about negative real line impedance? Pretty sure it's not physically possible. Negative reactance would, I suppose, be possible if the buses are close enough to have capacitance between them. |
@jurasofish Thanks! The 2D-ROC-analysis is quite time consuming - so it might not be feasible to use on large grids. And you'd also need to know "where to look" (which I did using trial and error before finding good bounding-boxes for the ROCs). For large grids, doing this on islands (or some other type of subgrid) could be an interesting approach. What would happen in the real world?Assuming you built a network whose model diverges. After all, it seems like "reality always converges" in some kind of way. ExampleYour lightbulb might be rated for 40W - but that doesn't mean it always consumes 40W in practice. If the grid is not able to deliver enough power to satisfy your neighborhood - the bulb might glow less brightly, consuming less than 40W. "Reality adjusts your I suppose this specific example is already somehow dealt with in pandapower through voltage-dependent loads. I guess once the voltage drops towards 0 in some region, and is not even able to keep the lines/cabled electrified - then this will cause divergence in pandapower. Sort of like trying to model fluid flow in an empty pipe. An intuition-based overviewEssentially, as every individual parameter is scaled up or down - what is the intuition behind why the equations no longer converge, and how would "reality deal with it?"
I think creating some kind of overview like this as part of the documentation could be really useful. |
I think rather than the slack bus or network not being able to supply enough power it might be more productive for you to conceptualise things in terms of voltage collapse. This will also help you understand why voltage dependant/constant impedance loads converge better. The slack bus (and the whole network) can always supply enough power - that's it's purpose. You'd probably find a plots of average/min voltage in the network (pu) and slack bus power generation both versus line x/line r/constant impedance percentage/load size/etc. very informative. |
Also do you mind me asking what your use case is for pandapower? Sounds like you're doing some interesting stuff. |
@jurasofish I'm working for/writing my master thesis for Kongsberg Digital on this project: https://www.kongsberg.com/digital/solutions/kognitwingrid Some of the networks I've been working on has more than 100,000 households/industries connected. (in the future, probably even bigger networks) Data quality from the grid companies is not perfect - and so being able locate/fix problems in the model is important. The more automated the better. One of the scenarios we are looking at is how the increased use of electrical cars in the following years will affect the power grid. |
very nice, my experience is that LV network data is generally very low quality, as they were mostly installed before digital record keeping. If you're looking at EVs you might want to also look at how their inverters can use reactive power to assist local voltage issues |
Yeah, the LV data quality is indeed quite a bit worse than HV. Sometimes power transformers will be flipped the wrong way, lines/transformers are missing values. Voltage levels can sometimes also be wrong - and the grid might be partitioned/disconnected. The data is generally exported in an XML-format (Common Information Model), where you have to follow a ton of references to get what you want. I've gotten pretty good at using pandas merge/concat as a result. I don't know a lot about inverters (other than that they convert DC to AC, and are used in electrical cars/with solar panels). Do you know of any good articles/learning resources for what you are talking about? What are you doing yourself related to pandapower? |
Some more analysis on r_ohm and x_ohm-scalingIt might be interesting to look at the absolute value of the impedance (radius), as well as its angle (theta) - instead of r_ohm and x_ohm directly.
As we noticed earlier, the factor will sometimes need to be very large, or extremely small to make a converging network diverge. Because of this, visualizing this on linear scales is inconvenient. A logarithmic scale might work better. We can use a logarithmic binary search to find the upper and lower bounds for radius given a value of theta. In the plots below, only positive values for r_ohm and x_ohm are considered ( Click here to see the code
import numpy as np
import pandapower as pp
import pandapower.networks as nw
import matplotlib.pyplot as plt
def from_polar(r, th):
return r * np.cos(th), r * np.sin(th)
@np.vectorize
def limit_polar_log_search(test_fn, theta, log_radius_conv, log_radius_div,
tolerance=1e-3, max_iterations=50):
# Check that radius_conv does not cause divergence
if np.isnan(test_fn(*from_polar(np.exp(log_radius_conv), theta))):
return np.nan
# Check that radius_div does not cause convergence
if test_fn(*from_polar(np.exp(log_radius_div), theta)) == 1:
return np.nan
while (np.abs(log_radius_div - log_radius_conv) > tolerance
and max_iterations > 0):
max_iterations -= 1
log_radius_test = (log_radius_div + log_radius_conv) / 2
if np.isnan(test_fn(*from_polar(np.exp(log_radius_test), theta))):
log_radius_div = log_radius_test
else:
log_radius_conv = log_radius_test
return np.exp(log_radius_conv)
cig = nw.cigre_networks.create_cigre_network_hv()
def convergence_test(r_ohm_factor, x_ohm_factor):
net = cig.deepcopy() # Deepcopy is much faster than loading the network again
net.line.r_ohm_per_km *= r_ohm_factor
net.line.x_ohm_per_km *= x_ohm_factor
try:
pp.runpp(net)
return 1
except Exception as e:
return np.nan
theta = np.linspace(0, np.pi/2, 300)
radius_upper = limit_polar_log_search(convergence_test, theta, log_radius_conv=0, log_radius_div=50)
radius_lower = limit_polar_log_search(convergence_test, theta, log_radius_conv=0, log_radius_div=-50)
fig, ax = plt.subplots(1, 1)
ax.plot(theta, radius_lower)
ax.plot(theta, radius_upper)
ax.set_xlabel('impedance scaling factor: theta')
ax.set_ylabel('impedance scaling factor: radius')
ax.set_title('Cigre HV: Upper and lower bounds for convergence')
ax.set_yscale('log') Observations
|
@jurasofish I've been trying to understand voltage collapse a bit more. As I increase c_nf_per_km, and look at the bus-voltages in a 2-bus network, this is what I see: 6000 powerflows/1min 47s Observations
Questions
|
Divergence doesn't neccesarily correspond to voltage collapse!In the figure below, you can see that the system goes from having two possible solutions, to at some point having exactly one solution, before no solutions exist. The same simple system as in the previous post is used here. Notice that the maximum power is achieved at vm_pu=0.5. Divergence is caused by trying to use more power than the maximum power transfer theorem allows. The two solutions simply correspond to the two possible load impedances that yield the same power consumption. Click here to see the bifurcation diagram codeimport pandapower as pp
import matplotlib.pyplot as plt
import numpy as np
def binary_search(fn, x_ok, x_err, tolerance=1e-6, max_iterations=20):
def try_fn(x):
try:
fn(x)
return 1
except:
return 0
assert try_fn(x_ok) == 1
assert try_fn(x_err) == 0
while abs(x_err - x_ok) > tolerance and max_iterations > 0:
x_guess = (x_ok + x_err) / 2
if try_fn(x_guess) == 0:
x_err = x_guess
else:
x_ok = x_guess
max_iterations -= 1
return x_ok
def create_network(p_mw):
net = pp.create_empty_network()
b0, b1 = pp.create_buses(net, nr_buses=2, vn_kv=10)
pp.create_ext_grid(net, bus=b0, vm_pu=1)
pp.create_line_from_parameters(net, from_bus=b0, to_bus=b1, length_km=1,
r_ohm_per_km=1, x_ohm_per_km=0,
c_nf_per_km=0, max_i_ka=10)
pp.create_load(net, bus=b1, p_mw=p_mw)
return net
@np.vectorize
def find_voltage(p_mw, vm_pu_init):
net = create_network(p_mw)
try:
pp.runpp(net, init_vm_pu=[1, vm_pu_init])
return net.res_bus.iloc[1].vm_pu
except:
return np.nan
# Find the maximum value for p_mw where the network converges
p_mw_max = binary_search(lambda p_mw: pp.runpp(create_network(p_mw)), 0, 50)
# Show that in there are two different solutions for every converging p_mw
p_mw = np.linspace(0, p_mw_max, 50)
vm_pu_high = find_voltage(p_mw, vm_pu_init=1)
vm_pu_low = find_voltage(p_mw, vm_pu_init=0.1)
fig = plt.figure()
fig.suptitle('Bifurcation diagram (r_ohm=1, x_ohm=0, c_nf=0)')
plt.plot(p_mw, vm_pu_high)
plt.plot(p_mw, vm_pu_low)
plt.xlabel('p_mw')
plt.ylabel('vm_pu')
plt.legend(['default solution','alternative solution'])
plt.grid(True)
plt.show() Setting c_nf_per_km to a high value (1e7)Some things to notice here:
Setting x_ohm = 3 * r_ohm
Purely reactive line impedance
|
Validation that will catch the most typical problemsThese are problems that will typically always cause divergence (or have no sensible physical interpretation) if not fulfilled. An exception is the max_i_ka-rule - which will not cause divergence if broken, but cause nan-values for loading_percent on lines in the result. from pandapower.auxiliary import pandapowerNet
import numpy as np
def assert_valid_network(net: pandapowerNet):
assert_valid_trafo(net)
assert_valid_trafo3w(net)
assert_valid_line(net)
assert_valid_load(net)
assert_valid_switch(net)
assert_valid_ext_grid(net)
def assert_valid_trafo(net: pandapowerNet):
assert (net.trafo.hv_bus).isin(net.bus.index).all()
assert (net.trafo.lv_bus).isin(net.bus.index).all()
assert (net.trafo.vkr_percent >= 0).all()
assert (net.trafo.vk_percent > 0).all()
assert (net.trafo.vk_percent >= net.trafo.vkr_percent).all()
assert (net.trafo.vn_hv_kv > 0).all()
assert (net.trafo.vn_lv_kv > 0).all()
assert (net.trafo.sn_mva > 0).all()
assert (net.trafo.pfe_kw >= 0).all()
assert (net.trafo.i0_percent >= 0).all()
def assert_valid_trafo3w(net: pandapowerNet):
assert (net.trafo3w.hv_bus).isin(net.bus.index).all()
assert (net.trafo3w.mv_bus).isin(net.bus.index).all()
assert (net.trafo3w.lv_bus).isin(net.bus.index).all()
assert (net.trafo3w.vn_hv_kv > 0).all()
assert (net.trafo3w.vn_mv_kv > 0).all()
assert (net.trafo3w.vn_lv_kv > 0).all()
assert (net.trafo3w.sn_hv_mva > 0).all()
assert (net.trafo3w.sn_mv_mva > 0).all()
assert (net.trafo3w.sn_lv_mva > 0).all()
assert (net.trafo3w.vk_hv_percent > 0).all()
assert (net.trafo3w.vk_mv_percent > 0).all()
assert (net.trafo3w.vk_lv_percent > 0).all()
assert (net.trafo3w.vkr_hv_percent >= 0).all()
assert (net.trafo3w.vkr_mv_percent >= 0).all()
assert (net.trafo3w.vkr_lv_percent >= 0).all()
assert (net.trafo3w.vk_hv_percent >= net.trafo3w.vkr_hv_percent).all()
assert (net.trafo3w.vk_mv_percent >= net.trafo3w.vkr_mv_percent).all()
assert (net.trafo3w.vk_lv_percent >= net.trafo3w.vkr_lv_percent).all()
assert (net.trafo3w.pfe_kw >= 0).all()
assert (net.trafo3w.i0_percent >= 0).all()
def assert_valid_line(net: pandapowerNet):
assert (net.line.from_bus).isin(net.bus.index).all()
assert (net.line.to_bus).isin(net.bus.index).all()
assert (net.line.max_i_ka > 0).all()
assert (net.line.length_km > 0).all()
z_per_km = np.sqrt(net.line.r_ohm_per_km**2 + net.line.x_ohm_per_km**2)
assert (z_per_km < np.inf).all()
assert (z_per_km > 0).all()
assert (net.line.c_nf_per_km < np.inf).all()
assert (net.line.c_nf_per_km >= 0).all()
def assert_valid_load(net: pandapowerNet):
assert (net.load.bus).isin(net.bus.index).all()
assert (net.load.const_z_percent + net.load.const_i_percent <= 100).all()
def assert_valid_switch(net: pandapowerNet):
assert (net.switch.bus).isin(net.bus.index).all()
assert (net.switch.element[net.switch.et == 'b']).isin(net.bus.index).all()
assert (net.switch.element[net.switch.et == 'l']).isin(net.line.index).all()
assert (net.switch.element[net.switch.et == 't']).isin(net.trafo.index).all()
assert (net.switch.element[net.switch.et == 't3']).isin(net.trafo3w.index).all()
def assert_valid_ext_grid(net: pandapowerNet):
assert len(net.ext_grid) > 0
assert (net.ext_grid.bus).isin(net.bus.index).all() ext_grid placement and maximum power transfer theoremLines/impedances constrain the maximum amount of power that can be transferred through based on reference voltage. Depending on where the ext_grid is placed, it might not be possible to deliver all the requested power. This causes the powerflow calculations to diverge. Based on my current understanding - voltage collapse and the maximum power transfer theorem are closely related. To better understand if voltage collapse is the reason for divergence - a type of continuous power flow solution could be relevant. Optimistic powerflow that change properties to increase chances of convergencedef optimistic_network(net: pandapowerNet):
n = net.deepcopy()
n.line.c_nf_per_km = 0
n.trafo.pfe_kw = 0
n.trafo3w.pfe_kw = 0
n.load.p_mw = 0
n.load.q_mvar = 0
n.switch.closed = True
return n
pp.runpp(optimistic_network(net)) |
Hi @bergkvist , thank you for the detailed review of this issue. When it comes to the maximum power transfer theorem, it doesn't seem very practical to implement in diagnostic. But it is interesting on its own. In diagnostic, the overload is covered by reducing the loads and checking the convergence. The search for "islands" to identify unconverging sections of the grid can be very useful, especially in larger systems. The checks whether c_nf_per_km and vkr_percent are too high, as well as np.inf and np.nan, would be useful, too. The functions you are proposing also are looking great. Can you please add those checks in pandapower diagnostic via a pull request? Also, please take a look at the overall structure in the implemented diagnostic module, so that the new checks fit into it. Roman |
Goal
Explore the debugging process of someone with a diverging network - trying to figure out how to make it converge and what they have done wrong.
Motivation
When I first started using pandapower around a year ago, I found it to be very hard to debug why a powerflow didn't converge. Especially after building a large grid. This is something I've gotten a lot better at now, so I want to share some of the tricks I've come across.
Problem
I find that pp.diagnostic often won't be able to figure out why a powerflow diverges.
From experience, there are several things that can cause a powerflow to diverge that the diagnostic tool is not checking for. Some of these things are:
net.line.c_nf_per_km
being too highThis is by far one of the things that seem to affect convergence the most. Trying to multiply this by 0.01/setting a threshold value could be a useful test.
net.trafo.vkr_percent > net.trafo.vk_percent
the real part of a complex number should never be larger than its absolute value.
To fix this problem, I suggest validation logic in pp.create_transformer_from_parameters(...).
Since it is possible to change the value later, it should probably also be checked in the diagnostic tool.
The same is true for
net.trafo3w
, except here we have to check for 3 potential problems:net.trafo3w.vkr_hv_percent > net.trafo.vk_hv_percent
net.trafo3w.vkr_mv_percent > net.trafo.vk_mv_percent
net.trafo3w.vkr_lv_percent > net.trafo.vk_lv_percent
Too large values for
net.line.r_ohm_per_km
ornet.line.x_ohm_per_km
A single inf-value will make everything diverge. This could be handled similarly to
c_nf_per_km
(where an inf-value also will cause divergence)An approach to locating problem elements that pp.diagnostic is not able to pinpoint.
When you build a network with 100,000 loads, and the powerflow doesn't converge, it can be hard to figure out why.
A single bad element can cause the entire powerflow to diverge. Some kind of binary-search (testing for convergence) with pp.select_subnet(net, buses=...) could be an interesting approach to locating bad elements.
My personal approach
Define
net.bus.island
, as illustrated in the image below (partitions in the network could also be very relevant):Within an island, I ensure that every bus has the same reference voltage (vn_kv). Below you can see how I find the islands and iterate through different subnets to narrow down my search:
With the following helper functions:
To further narrow down the search within one of these subnets that I already know has diverged, I use shortest_path-subsubnets (I guess we need 2-subs here).
The text was updated successfully, but these errors were encountered: