Repository: e2nIEE/pandapower

Issue:
Improving help for initialization of runpp function
Hello,

Sorry to bother with this small issue, but i notice a discrepency between the documentation of the `runpp` function (https://pandapower.readthedocs.io/en/v2.2.2/powerflow/ac.html#pandapower.runpp) and what it really does, when "init=flat" is provided.

I understood from the documentation, when using this initialization, that :

- all PQ buses are initalized with Vm = 1.0 and Va = 0.
- all PV buses are initialized with Vm = the appropriate Vm provided in the net.bus["vm_pu"] table (given in appropriate orders) and Va  = 0.
- nothing about the slack bus

In a modified pandapower version to print V0 used in `pandapower.pf.run_newton_raphson_pf.py` to print the V0:
```python
def _run_ac_pf_without_qlims_enforced(ppci, options):
    makeYbus, pfsoln = _get_numba_functions(ppci, options)

    baseMVA, bus, gen, branch, ref, pv, pq, _, _, V0, ref_gens = _get_pf_variables_from_ppci(ppci)
    
    print(V0)  # I added this line of code into pandapower file
    ppci, Ybus, Yf, Yt = _get_Y_bus(ppci, options, makeYbus, baseMVA, bus, branch)

    # compute complex bus power injections [generation - load]
    Sbus = _get_Sbus(ppci, options["recycle"])

    # run the newton power  flow
    V, success, iterations, J, Vm_it, Va_it = newtonpf(Ybus, Sbus, V0, pv, pq, ppci, options)

    # keep "internal" variables in  memory / net["_ppc"]["internal"] -> needed for recycle.
    ppci = _store_internal(ppci, {"J": J, "Vm_it": Vm_it, "Va_it": Va_it, "bus": bus, "gen": gen, "branch": branch,
                                  "baseMVA": baseMVA, "V": V, "pv": pv, "pq": pq, "ref": ref, "Sbus": Sbus,
                                  "ref_gens": ref_gens, "Ybus": Ybus, "Yf": Yf, "Yt": Yt})

    return ppci, success, iterations
```

I got the following results with this code:
```python
import pandapower.networks as pn
import pandapower as pp
net = pn.case118()
pp.runpp(net,  init="flat")
```
And the results is:
```python
np.array([0.955     +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 1.01      +0.j    ,
                       0.971     +0.j    , 0.965     +0.j    , 1.        +0.j    ,
                       0.952     +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       0.973     +0.j    , 0.99      +0.j    , 0.98      +0.j    ,
                       0.975     +0.j    , 0.993     +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.005     +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       0.97      +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       0.973     +0.j    , 0.962     +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 0.992     +0.j    , 1.05      +0.j    ,
                       1.015     +0.j    , 0.968     +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 0.998     +0.j    ,
                       0.967     +0.j    , 0.963     +0.j    , 1.        +0.j    ,
                       0.984     +0.j    , 1.        +0.j    , 0.98      +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       0.97      +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       0.985     +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.005     +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.025     +0.j    , 1.        +0.j    ,
                       0.99      +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 0.955     +0.j    , 0.952     +0.j    ,
                       0.954     +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       0.985     +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       0.995     +0.j    , 0.998     +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.005     +0.j    , 1.05      +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 0.89633629+0.5175j,
                       0.984     +0.j    , 1.015     +0.j    , 1.        +0.j    ,
                       0.98      +0.j    , 0.991     +0.j    , 0.958     +0.j    ,
                       1.        +0.j    , 0.943     +0.j    , 1.006     +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 1.04      +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 0.985     +0.j    ,
                       1.        +0.j    , 1.015     +0.j    , 1.        +0.j    ,
                       1.005     +0.j    , 0.985     +0.j    , 1.05      +0.j    ,
                       0.98      +0.j    , 0.99      +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 1.        +0.j    ,
                       1.        +0.j    , 1.        +0.j    , 1.01      +0.j    ,
                       1.017     +0.j    ])
```

So from that experiments it appears the slack bus (id corresponding to `0.89633629+0.5175j`) is initialized with an angle Va different from 0 (in this case 30 degree) which corresponds to the value given in `net.ext_grid`.

It is probably a choice to initialize powerflow this way, but i think the "flat" word in this case is a bit confusing in this case.

I noticed a similar behavior using the DC approximation. It is not clear how the Vm and Va are computed from the DC approximation. After some experiments:
- for PQ bus it is Vm = `net["_options"]["init_vm_pu"]` and Va resulting from the DC computation
- for PV bus it is Vm specified in net.gen["vm_pu"]
- for slack bus it is Vm and Va speficied in `net.ext_grid`

These are not "bugs"  of course. These specific behavior deserve to be detailed in the documentation in my opinion, especially for larger test case like the case118 which is really sensitive to the initialisation.

Thanks

Benjamin


Discussion:
Hi Benjamin,

thanks for bringing this to our attention! You are certainly right that documentation could be improved...

> So from that experiments it appears the slack bus (id corresponding to 0.89633629+0.5175j) is initialized with an angle Va different from 0 (in this case 30 degree) which corresponds to the value given in net.ext_grid.

Yes that is correct. Slack/PV buses are always initialized with the known voltage values (Vm for PV node, Vm and Va for slack nodes). From my understanding of the power flow calculation, this is necessary because they are not part of the jacobian, which calculates the voltage derivative. So if these voltage values wouldn't be initialized with the known values, there would be no way to ever reach the result, as they are never changed within the Newton-Raphson.

So I would think that this is not an issue specific to pandapower - all tools that use Newton-Raphson have to initialize PV/Slack nodes with the known values, otherwise convergence is impossible. So while it is called flat start, it is always implied that this only applies to unknown voltage values, not to nodes where the voltage is known as an input value.

> I noticed a similar behavior using the DC approximation. It is not clear how the Vm and Va are computed from the DC approximation. After some experiments:

What you derived is correct. Again, if you think about it in a way that only unknown values are initialized, and known values are not initialized, I think it follows logically.

It could however be more clear in the documentation, no doubt about it.... Can you point out where in the documentation you would want to add that?
Hello,

Sorry for taking this long to answer, i thought I did it but apparently not :thinking: 

> What you derived is correct. Again, if you think about it in a way that only unknown values are initialized, and known values are not initialized, I think it follows logically.

Yes you are correct, if we think carefully about it it makes total sense. However this cause a different behaviour between running a dc pf (where everything is computing) and using the dc approximation to start a powerflow (where only the variables are computed and the rest is unchanged). 

I'm not sure that a lot of people will notice this behaviour, as you have to deep dive into the code to notice that. So I totally agree this is a rather minor "issue".

> It could however be more clear in the documentation, no doubt about it.... Can you point out where in the documentation you would want to add that?

I guess something along these lines:

- “flat”- flat start with voltage of 1.0pu and angle of 0° at all PQ-buses and 0° for PV buses as initial solution, the slack bus is initialized with the values provided in `net["ext_grid"]`
- “dc” - initial DC loadflow before the AC loadflow. The results of the DC loadflow are used as initial solution for the AC loadflow. Note that as only the PV bus voltage magnitude values need to be computed, the DC powerflow does not compute the actual value of any of the PQ bus as opposed to running a DC powerflow.

Resolving patch:
diff --git a/pandapower/plotting/collections.py b/pandapower/plotting/collections.py
--- a/pandapower/plotting/collections.py
+++ b/pandapower/plotting/collections.py
@@ -1051,7 +1051,7 @@ def create_line_switch_collection(net, size=1, distance_to_bus=3, use_line_geoda
             if line.name in net.line_geodata.index:
                 line_coords = net.line_geodata.coords.loc[line.name]
                 # check, which end of the line is nearer to the switch bus
-                intersection = position_on_busbar(net, target_bus, busbar_coords=line_coords)
+                intersection = position_on_busbar(net, sb, busbar_coords=line_coords)
                 if intersection is not None:
                     pos_sb = intersection
                 if len(line_coords) >= 2:
diff --git a/pandapower/pypower/opf_costfcn.py b/pandapower/pypower/opf_costfcn.py
--- a/pandapower/pypower/opf_costfcn.py
+++ b/pandapower/pypower/opf_costfcn.py
@@ -58,7 +58,7 @@ def opf_costfcn(x, om, return_hessian=False):
     # formulation, pwl cost is the sum of the y variables.
     ipol = find(gencost[:, MODEL] == POLYNOMIAL)   ## poly MW and MVAr costs
     xx = r_[ Pg, Qg ] * baseMVA
-    if any(ipol):
+    if len(ipol)>0:
         f = sum( totcost(gencost[ipol, :], xx[ipol]) )  ## cost of poly P or Q
     else:
         f = 0
diff --git a/pandapower/run.py b/pandapower/run.py
--- a/pandapower/run.py
+++ b/pandapower/run.py
@@ -98,8 +98,8 @@ def runpp(net, algorithm='nr', calculate_voltage_angles="auto", init="auto",
         pandapower supports four methods for initializing the loadflow:
 
             - "auto" - init defaults to "dc" if calculate_voltage_angles is True or "flat" otherwise
-            - "flat"- flat start with voltage of 1.0pu and angle of 0° at all PQ-buses and 0° for PV buses as initial solution
-            - "dc" - initial DC loadflow before the AC loadflow. The results of the DC loadflow are used as initial solution for the AC loadflow.
+            - "flat"- flat start with voltage of 1.0pu and angle of 0° at all PQ-buses and 0° for PV buses as initial solution, the slack bus is initialized with the values provided in net["ext_grid"]
+            - "dc" - initial DC loadflow before the AC loadflow. The results of the DC loadflow are used as initial solution for the AC loadflow. Note that the DC loadflow only calculates voltage angles at PQ and PV buses, voltage magnitudes are still flat started.
             - "results" - voltage vector of last loadflow from net.res_bus is used as initial solution. This can be useful to accelerate convergence in iterative loadflows like time series calculations.
 
         Considering the voltage angles might lead to non-convergence of the power flow in flat start.


Test patch: