<jupyter_start><jupyter_text>SimBench Grid Usage Examples including Study Cases and Time Series Analysis This tutorial shows how SimBench grids can be received and used.First, this tutorial introduces the SimBench code which is required to receive SimBench grids.Second, the received SimBench grids are exemplary used to:* run a simple power flow* run and analyze predefined study cases* run a timeseries calculation using the predefined profiles<jupyter_code># Let's do some necessary imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

import pandapower as pp
import pandapower.plotting as plot
import simbench as sb<jupyter_output><empty_output><jupyter_text>SimBench provides a benchmark dataset which contains* 1 extra high voltage (EHV) grid* 2 high voltage (HV) grids* 4 medium voltage (MV) grids* 6 low voltage (LV) grids* 27 end-consumer load profiles in p and q (LV, MV)* 28 generation profiles of renewables (LV-EHV)* 320 generation profiles of power plants (EHV)* 43 storage profiles (LV-EHV)* grid equivalent profiles for all LV-HV grids for loads, renewables and storages* 3 scenarios of each gridSimBench grids are appropriate to be used for various use cases also including use cases considering multiple voltage levels. As a result, all SimBench grids, comprising all voltage levels, can be interconnected. To reduce the number of possible combinations, the most relevant combinations are selected and available as simbench code. **The SimBench code names distinctively a grid which is a useful excerpt of the full SimBench grid dataset**. Thus, the SimBench grid usage achieves improved comprehensibility and simplified reproducibility.  SimBench CodeThe SimBench code is a string which looks like "1-HVMV-urban-2.202-0-no_sw". It uses "-" as separator. The SimBench Code makes use of the subnet parameter which you can find in the data set.![20190227_SimBench_Subnet_60.png](attachment:20190227_SimBench_Subnet_60.png) While the subnet describes to which grid an element belongs to, the SimBench code describes the full grid data excerpt of the SimBench dataset. The grids distinctively named by a SimBench code can include several subnets and the code gives information about the chosen scenario and switch consideration level.![20190227_SimBench_Code_60.png](attachment:20190227_SimBench_Code_60.png) To quickly start with SimBench grids, here you can find the SimBench codes of the basic grids (without any second voltage level) of scenario 0 and with full switch consideration level:| (Exemplary) Subnet | SimBench code       | Urbanzation character | Rated voltage [kV] | No. supply points | Transformer types          | Generation types          | Geo. Information with relation to reality ||------------|---------------------|-----------------------|--------------------|-------------------|----------------------------|---------------------------|-------------------------------------------||    EHV1    | 1-EHV-mixed--0-sw   |    mixed              |    380,   220      |      390      |    209x600MVA              |    Nuclear, coal, gas     | yes                                       ||    HV1     | 1-HV-mixed--0-sw    |    mixed              |    110             |       58       |    2x300MVA,   4x350MVA    | Wind                      | yes                                       ||    HV2     | 1-HV-urban--0-sw    |    urban              |    110             |    79       |    3x300MVA                | Wind                     | yes                                       ||    MV1.101     | 1-MV-rural--0-sw    |    rural              |    20              |    92      |    2x25MVA                 | Wind, PV, Biogas, Hydro   |  no                                       ||    MV2.101     | 1-MV-semiurb--0-sw  |    semi-urban         |    20              |    112      |    2x40MVA                 | Wind, PV, Biogas, Hydro   | no                                        ||    MV3.101     | 1-MV-urban--0-sw    |    urban              |    10              |    134      |    2x63MVA                 | Wind, PV, Hydro           | no                                        ||    MV4.101     | 1-MV-comm--0-sw     |    commercial         |    20              |    98      |    2x40MVA                 | Wind, PV, Biogas, Hydro   | no                                        ||    LV1.101     | 1-LV-rural1--0-sw   |    rural              |    0.4             |    13       |    1x160kVA                | PV                        | no                                        ||    LV2.101     | 1-LV-rural2--0-sw   |    rural              |    0.4             |    93      |    1x250kVA                | PV                        | no                                        ||    LV3.101     | 1-LV-rural3--0-sw   |    rural              |    0.4             |    118      |    1x400kVA                | PV                        | no                                        ||    LV4.101     | 1-LV-semiurb4--0-sw |    semi-urban         |    0.4             |    39       |    1x400kVA                |  PV                       | no                                        ||    LV5.201     | 1-LV-semiurb5--0-sw |    semi-urban         |    0.4             |    104      |    1x630kVA                | PV                        | no                                        ||    LV6.201     | 1-LV-urban6--0-sw   |    urban              |    0.4             |    53       |    1x630kVA                | PV                        | no                                        | To get an overall picture of provided SimBench codes, you can have a full look into the list of all SimBench codes via collect_all_simbench_codes(). You also can restrict the list, giving some parameters to the function.<jupyter_code># get lists of simbench codes
all_simbench_codes = sb.collect_all_simbench_codes()
all_simbench_code_with_LV_as_lower_voltage_level = sb.collect_all_simbench_codes(lv_level="LV")<jupyter_output><empty_output><jupyter_text>Please, notice that the SimBench code was designed with the expectation that the user usually wants to use *only one or two voltage levels at the same time*. In the case that you need more than two voltage levels, you may receive the complete data of SimBench or the complete SimBench grid (here all external grid representations are excluded which are included as detailled grid data -> the complete SimBench grid should be executable within power flow analysis tools) via the following codes:<jupyter_code>complete_data_sb_codes = ["1-complete_data-mixed-all-%i-sw" % scenario for scenario in [1, 2, 3]]
complete_grid_sb_codes = ["1-EHVHVMVLV-mixed-all-%i-sw" % scenario for scenario in [1, 2, 3]]<jupyter_output><empty_output><jupyter_text>Receiving a SimBench GridYou can get a SimBench grid by passing a valid SimBench code to the function get_simbench_net().<jupyter_code>sb_code1 = "1-MV-rural--0-sw"  # rural MV grid of scenario 0 with full switchs
net = sb.get_simbench_net(sb_code1)

sb_code2 = "1-HVMV-urban-all-0-no_sw"  # urban hv grid with one connected mv grid which has the subnet 2.202
multi_voltage_grid = sb.get_simbench_net(sb_code2)<jupyter_output><empty_output><jupyter_text>Simple Power FlowLet's have a look to a simple MV grid for simple power flow analysis.<jupyter_code>net<jupyter_output><empty_output><jupyter_text>All SimBench grids are designed to be realistic. For example, the number of supplying nodes is about 100. In the rural MV grid, these consumers are supplied by open rings systems.<jupyter_code># plot the grid to show the open ring systems
plot.simple_plot(net)<jupyter_output>hp.pandapower.plotting.collections - WARNING: None in collections is of unknown type. Skipping
hp.pandapower.plotting.collections - WARNING: None in collections is of unknown type. Skipping<jupyter_text>In initial state, all powers are set to maximum values. It is unrealistic that all maximum values of all loads and various generation types occur at the same time. But usually this is no problem since the superposition of generation and load is less challenging to supply than time steps where almost only generation or almost only load occur.<jupyter_code># let's simply run a power flow calculation with assuming an outage of the first line in feeder 1
outage_line = 1
outage_line_switches = net.switch.index[(net.switch.element == outage_line) & (net.switch.et == "l")]
net.switch.closed.loc[outage_line_switches] = False

# resupply feeder 1 via feeder 5
feeder1_buses = net.bus.index[net.bus.subnet.str.contains("Feeder1")]
feeder5_buses = net.bus.index[net.bus.subnet.str.contains("Feeder5")]
loop_line_1_5 = net.line.index[net.line.from_bus.isin(feeder1_buses) & net.line.to_bus.isin(feeder5_buses)]
loop_switches_1_5 = net.switch.index[(net.switch.element == loop_line_1_5[0]) & (net.switch.et == "l")]
net.switch.closed.loc[loop_switches_1_5] = True

# run a simple power flow
pp.runpp(net)

# analyze maximal loaded lines
feeder_1_5_lines = net.line.subnet.str.contains("Feeder1") | net.line.subnet.str.contains("Feeder5")
net.res_line.loading_percent.loc[feeder_1_5_lines].max()  # maximal loaded line of Feeder 1 and 5
# -> maximal line loading is less than 100%

# print number of unsupplied buses
unsupplied_buses = pp.unsupplied_buses(net)
len(unsupplied_buses)<jupyter_output><empty_output><jupyter_text>Run and Analyze Predefined Study CasesSimBench provides predefined study cases which can be used for grid planning. The study cases are presented and specified in the paper Meinecke, Klettke, Sarajlic, Dickert, Hable et. al. "General Planning and Operational Principles in German Distribution Systems Used for SimBench", CIRED 2019, 25th International Conference on Electricity Distribution. A summary is also given in the documentation available at the SimBench website (https://simbench.de/en/download/).The study cases define scaling factors for load active and reactive power as well as generation active power.<jupyter_code># the predefined study case data are stored in pandapower within net.loadcases
net.loadcases<jupyter_output><empty_output><jupyter_text>Now we want to apply the study cases.Since the scaling factors in net.loadcases are relative and refer to the maximum values given in net[element][max_val_column] with element is "load", "sgen" or "ext_grid" and max_val_column is "p_mw", "q_mvar" or "vm_pu", first, we need to calculate the absolute values. Afterwards we write the absolute values into net[element][parameter] and run a power flow.<jupyter_code># calculate the absolute values:
loadcases = sb.get_absolute_values(net, profiles_instead_of_study_cases=False)

# define a function to apply absolute values
def apply_absolute_values(net, absolute_values_dict, case_or_time_step):
    for elm_param in absolute_values_dict.keys():
        if absolute_values_dict[elm_param].shape[1]:
            elm = elm_param[0]
            param = elm_param[1]
            net[elm].loc[:, param] = absolute_values_dict[elm_param].loc[case_or_time_step]

# let's analyze the (n-1)-case
apply_absolute_values(net, loadcases, "n1")

# adapt trafo tap position
net.trafo["tp_pos"] = -3

# run a power flow and log basic results
pp.runpp(net)
pp.lf_info(net)<jupyter_output>hp.pandapower.toolbox - INFO: Max voltage in vm_pu:
hp.pandapower.toolbox - INFO:   1.035 at busidx 0 (HV1 Bus 17)
hp.pandapower.toolbox - INFO: Min voltage in vm_pu:
hp.pandapower.toolbox - INFO:   0.9683901319196428 at busidx 68 (MV1.101 Bus 68)
hp.pandapower.toolbox - INFO: Max loading trafo in %:
hp.pandapower.toolbox - INFO:   35.92167323134611 loading at trafo 0 (HV1-MV1.101-Trafo1)
hp.pandapower.toolbox - INFO:   35.92167323134611 loading at trafo 1 (HV1-MV1.101-Trafo2)
hp.pandapower.toolbox - INFO: Max loading line in %:
hp.pandapower.toolbox - INFO:   54.830800326859006 loading at line 44 (MV1.101 Line 45)
hp.pandapower.toolbox - INFO:   54.179294836460755 loading at line 45 (MV1.101 Line 46)<jupyter_text>![voltage_limits_50160_03_smaller.png](attachment:voltage_limits_50160_03_smaller.png)This exemplary power flow results show, SimBench grids in scenario 0 always hold above figured limits of the predefined study cases: In (n-1)-case the voltage limits are 0.915 and 1.055 pu.The grids of scenarios 1 and 2 do not hold all limits since these represents future grids without grid expansion.<jupyter_code># let's have a look at the low load high generation (extra high wind) case
apply_absolute_values(net, loadcases, "lW")

# adapt trafo tap position
net.trafo["tp_pos"] = -1

# run a power flow and log basic results
pp.runpp(net)
pp.lf_info(net)
assert net.res_bus.vm_pu.loc[net.bus.index.difference(feeder1_buses|feeder5_buses)].max() < 1.055
assert net.res_bus.vm_pu.loc[net.bus.index.difference(feeder1_buses|feeder5_buses)].min() > 0.915<jupyter_output>hp.pandapower.toolbox - INFO: Max voltage in vm_pu:
hp.pandapower.toolbox - INFO:   1.0536603662133073 at busidx 47 (MV1.101 Bus 47)
hp.pandapower.toolbox - INFO: Min voltage in vm_pu:
hp.pandapower.toolbox - INFO:   1.015 at busidx 0 (HV1 Bus 17)
hp.pandapower.toolbox - INFO: Max loading trafo in %:
hp.pandapower.toolbox - INFO:   45.97660031142415 loading at trafo 0 (HV1-MV1.101-Trafo1)
hp.pandapower.toolbox - INFO:   45.97660031142415 loading at trafo 1 (HV1-MV1.101-Trafo2)
hp.pandapower.toolbox - INFO: Max loading line in %:
hp.pandapower.toolbox - INFO:   63.46984358472696 loading at line 0 (MV1.101 Line 1)
hp.pandapower.toolbox - INFO:   61.51322190197005 loading at line 1 (MV1.101 Line 2)<jupyter_text>The latest power flow results show, that the voltages of the resupplied feeder 1 and resupplying feeder 5 violates the maximum limit of the voltage magnitudes. This is because SimBench grids do not supply generation units with (n-1)-security.However, all feeders in normal operation hold the limits.  Run a Timeseries Calculation using the Predefined ProfilesSimBench profiles are provided within net.profiles. In the element tables, the "profile" column specifies which profile is to be used for each element.<jupyter_code># load 'net' again to have no outage again
net = sb.get_simbench_net(sb_code1)

# As an example, the first sgen uses the profile:
net.sgen.profile.iloc[0]<jupyter_output><empty_output><jupyter_text>Since there are load profiles for "p_mw" and "q_mvar", the load profile names in net.profile["load"] are extended by thesuffixes "_pload" and "_qload".To check whether all applied respectively required are available in net.profiles, use the function profiles_are_missing().<jupyter_code># check that all needed profiles existent
assert not sb.profiles_are_missing(net)<jupyter_output><empty_output><jupyter_text>Just as the loadcase scaling factors, the given profiles are relative and refer to the maximum values given in net[element][max_val_column]. The absolute values again can be easily calculated via get_absolute_profiles_from_relative_profiles() and applied via apply_absolute_values().Plenty parameters of a net can be observed. Here, let's have a look to - the minimum and maximum voltages in the grid and- the sum of all active powers of loads to compare the course with the load profiles.<jupyter_code># calculate absolute profiles
profiles = sb.get_absolute_values(net, profiles_instead_of_study_cases=True)

# let's calculate the first day of the year (take every fourth time step of the first 96 quarter hour time steps).
time_steps = range(96)

# set trafo tap position so that no voltage limits are violated
net.trafo.tap_pos = 1

# run the time series and store results into a DataFrame
results = pd.DataFrame([], index=time_steps, columns=["Load Sum", "min_vm_pu", "max_vm_pu"])
for time_step in time_steps:
    apply_absolute_values(net, profiles, time_step)
    pp.runpp(net)
    results.loc[time_step, "Load Sum"] = net.res_load.p_mw.sum()
    results.loc[time_step, "min_vm_pu"] = net.res_bus.vm_pu.min()
    results.loc[time_step, "max_vm_pu"] = net.res_bus.vm_pu.max()<jupyter_output>c:\users\e2n011\documents\git\simbench\simbench\converter\auxiliary.py:121: FutureWarning:

Sorting because non-concatenation axis is not aligned. A future version
of pandas will change to not sort by default.

To accept the future behavior, pass 'sort=False'.

To retain the current behavior and silence the warning, pass 'sort=True'.<jupyter_text>If everything worked fine, now we can have a look to the course of the minimum and maximum voltages during the first day of the given profiles.<jupyter_code># plot the voltage extrema
#plt.rc('text', usetex=True)
fig1, ax1 = plt.subplots()
results[["min_vm_pu", "max_vm_pu"]].plot(ax=ax1)
ax1.set_ylabel("Voltage Extrema [pu]")
ax1.set_xlabel("Timesteps (quarter hours)")
plt.show()<jupyter_output><empty_output><jupyter_text>Furthermore, let's compare the calulated active power sum of loads with the considered low voltage grid profiles<jupyter_code># get LV grid profiles DataFrame
load_profile_names = pd.Series(net.profiles["load"].columns)
lv_grid_profile_names = list(load_profile_names.loc[load_profile_names.str.contains("lv_") & load_profile_names.str.contains("pload")])
lv_grid_profiles = net.profiles["load"].loc[time_steps, lv_grid_profile_names]
lv_grid_profiles.columns = ["LV rural %i" % i for i in range(1, 4)] + ["LV semiurb 4"]

# plot load sum result and LV grid profiles
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
results.plot(ax=ax1, color='k')
lv_grid_profiles.plot(ax=ax2, legend=False)
h1, l1 = ax1.get_legend_handles_labels()
h2, l2 = ax2.get_legend_handles_labels()
ax1.legend(h1+h2, l1+l2, loc=2)
ax1.set_ylabel("Active power sum [kw]")
ax2.set_ylabel("Active power per peak power")
ax1.set_xlabel("Timesteps (quarter hours)")
plt.show()<jupyter_output><empty_output><jupyter_text>One can imagine, that an appropriate combination of the load profiles could result in the calculated active power sum of loads. Since pandapower already comes with a timeseries module, to be in line with that and for computational time reasons, we recommend you to use it instead of the simple, here defined function apply_absolute_values().<jupyter_code>import tempfile
from pandapower.timeseries import OutputWriter
from pandapower.timeseries.run_time_series import run_timeseries

# apply ConstControllers
sb.apply_const_controllers(net, profiles)
# create output writer
output_dir = os.path.join(tempfile.gettempdir(), "simbench_time_series_example")
if not os.path.exists(output_dir):
    os.mkdir(output_dir)
ow = OutputWriter(net, time_steps, output_path=output_dir, output_file_type=".json")
ow.log_variable('res_load', 'p_mw', eval_function=np.sum)
ow.log_variable('res_bus', 'vm_pu', eval_function=np.min)
ow.log_variable('res_bus', 'vm_pu', eval_function=np.max)

# run timeseries
run_timeseries(net, time_steps, output_writer=ow)

# read result files and create same result DataFrame as above
vm_pu_file = os.path.join(output_dir, "res_bus", "vm_pu.json")
vm_pu = pd.read_json(vm_pu_file)
load_p_file = os.path.join(output_dir, "res_load", "p_mw.json")
load_p = pd.read_json(load_p_file)
results2 = pd.concat([load_p, vm_pu], axis=1)
results2.columns = ["Load Sum", "min_vm_pu", "max_vm_pu"]
results2.sort_index(inplace=True)<jupyter_output>Progress: |██████████████████████████████████████████████████| 100.0% Complete<jupyter_text>For a step-by-step introduction to pandapower timeseries, please have a look to pandapowers tutorials.As the following lines show, we calculated the same results as before.<jupyter_code>results.head()
results2.head()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09333675038441824, 'fraction_numerical': 0.03403382880574064, 'mean_word_length': 3.283424807903403, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 67, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 62, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '328013', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 6428, 'n_tokens_neox': 5825, 'n_words': 1956, 'path': 'tutorials/simbench_grids_basics_and_usage.ipynb', 'repo_name': 'johanneshiry/simbench', 'size': '19510'}