From b8843ab3ccab4573132fe8db8461ea1e7daced2e Mon Sep 17 00:00:00 2001 From: Roman Bolgaryn Date: Tue, 6 Jun 2023 13:54:57 +0200 Subject: [PATCH 001/135] gen bug --- pandapower/test/loadflow/test_runpp.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandapower/test/loadflow/test_runpp.py b/pandapower/test/loadflow/test_runpp.py index 8a126fe3b..c5a8deb6e 100644 --- a/pandapower/test/loadflow/test_runpp.py +++ b/pandapower/test/loadflow/test_runpp.py @@ -1449,6 +1449,19 @@ def test_lightsim2grid_option(): assert net._options["lightsim2grid"] +def test_at_isolated_bus(): + net = pp.create_empty_network() + pp.create_buses(net, 4, 110) + pp.create_ext_grid(net, 0) + + pp.create_line_from_parameters(net, 0, 1, 30, 0.0487, 0.13823, 160, 0.664) + + pp.create_gen(net, 3, 0, vm_pu=0, in_service=False) + + pp.runpp(net) + assert net._options["init_vm_pu"] == 1. + + if __name__ == "__main__": pytest.main([__file__]) # test_minimal_net() From 8d3cd2faf8476316fbdfb6e2409d8e20234bee96 Mon Sep 17 00:00:00 2001 From: Marie Fischer Date: Thu, 18 Jul 2024 15:56:53 +0200 Subject: [PATCH 002/135] create voltage controlled svc same as voltage controlled synchron machine (gen), implemented some logger debug meassages --- .../powerfactory/pf_export_functions.py | 2 + .../powerfactory/pp_import_functions.py | 61 +++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/pandapower/converter/powerfactory/pf_export_functions.py b/pandapower/converter/powerfactory/pf_export_functions.py index 93fcde79e..eb42b1331 100644 --- a/pandapower/converter/powerfactory/pf_export_functions.py +++ b/pandapower/converter/powerfactory/pf_export_functions.py @@ -21,6 +21,7 @@ def create_network_dict(app, flag_graphics='GPS'): 'ElmAsm', 'ElmShnt', 'ElmVac', + 'ElmSvs', # branch elements: 'ElmLne', @@ -51,6 +52,7 @@ def create_network_dict(app, flag_graphics='GPS'): 'ElmPvsys': ['W', 'var', 'VA'], 'ElmXnet': ['W', 'var', 'VA'], 'ElmSym': ['W', 'var', 'VA'], + 'ElmSvs': ['W', 'var', 'VA'], 'ElmAsm': ['W', 'var', 'VA'], 'ElmShnt': ['W', 'var', 'VA'], 'ElmZpu': ['W', 'var', 'VA'], diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index bc10a6956..9f9e488f5 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -166,30 +166,43 @@ def from_pf(dict_net, pv_as_slack=True, pf_variable_p_loads='plini', pf_variable for n, fuse in enumerate(dict_net['RelFuse'], 1): create_coup(net=net, item=fuse, is_fuse=True) if n > 0: logger.info('imported %d fuses' % n) - + + logger.debug('creating shunts') # create shunts (ElmShnt): n = 0 for n, shunt in enumerate(dict_net['ElmShnt'], 1): create_shunt(net=net, item=shunt) if n > 0: logger.info('imported %d shunts' % n) - + + logger.debug('creating impedances') # create zpu (ElmZpu): n = 0 for n, zpu in enumerate(dict_net['ElmZpu'], 1): create_zpu(net=net, item=zpu) if n > 0: logger.info('imported %d impedances' % n) - + + logger.debug('creating series inductivity as impedance') # create series inductivity as impedance (ElmSind): n = 0 for n, sind in enumerate(dict_net['ElmSind'], 1): create_sind(net=net, item=sind) if n > 0: logger.info('imported %d SIND' % n) + + logger.debug('creating series capacity as impedance') # create series capacity as impedance (ElmScap): n = 0 for n, scap in enumerate(dict_net['ElmScap'], 1): create_scap(net=net, item=scap) if n > 0: logger.info('imported %d SCAP' % n) - + + logger.debug('creating static var compensator') + # create static var compensator (SVC) with control same as voltage controlled synchron machine (ElmSvs): + n = 0 + for n, svc in enumerate(dict_net['ElmSvs'], 1): + create_svc(net=net, item=svc, pv_as_slack=pv_as_slack, + pf_variable_p_gen=pf_variable_p_gen, dict_net=dict_net) + if n > 0: logger.info('imported %d SVC' % n) + # create vac (ElmVac): n = 0 for n, vac in enumerate(dict_net['ElmVac'], 1): @@ -2798,6 +2811,46 @@ def create_scap(net, item): logger.debug('created series reactor %s as per unit impedance at index %d' % (net.impedance.at[scap, 'name'], scap)) + +def create_svc(net, item, pv_as_slack, pf_variable_p_gen, dict_net): + # SVC is controlled and therefore modelled the same way as a voltage controlled synchron machine (gen) + # TODO: at least implement a uncontrolled svc as synchron machine with const. Q + # TODO: transfer item entries for usage of pp.create_svc, x_l_ohm, x_cvar_ohm, + # thyristor_firing_angle must be computed + name = item.loc_name + sid = None + element = None + logger.debug('>> creating synchronous machine <%s>' % name) + + try: + bus1 = get_connection_nodes(net, item, 1) + except IndexError: + logger.error("Cannot add Sgen '%s': not connected" % name) + return + + logger.debug('creating sym %s as gen' % name) + vm_pu = item.usetp + sid = pp.create_gen(net, bus=bus1, p_mw=0, vm_pu=vm_pu, + name=name, type="SVC", in_service=not bool(item.outserv)) + element = 'gen' + + if sid is None or element is None: + logger.error('Error! SVC not created') + logger.debug('created svc at index <%s>' % sid) + + net[element].loc[sid, 'description'] = ' \n '.join(item.desc) if len(item.desc) > 0 else '' + add_additional_attributes(item, net, element, sid, attr_dict={"for_name": "equipment"}, + attr_list=["sernum", "chr_name", "cpSite.loc_name"]) + + if item.HasResults(0): # 'm' results... + logger.debug('<%s> has results' % name) + net['res_' + element].at[sid, "pf_p"] = ga(item, 'm:P:bus1') #* multiplier + net['res_' + element].at[sid, "pf_q"] = ga(item, 'm:Q:bus1') #* multiplier + else: + net['res_' + element].at[sid, "pf_p"] = np.nan + net['res_' + element].at[sid, "pf_q"] = np.nan + + logger.info('not creating svc for %s' % item.loc_name) def split_line_at_length(net, line, length_pos): From 5297d850f31558d3260456983e650794737aa698 Mon Sep 17 00:00:00 2001 From: Marie Fischer Date: Thu, 18 Jul 2024 16:25:33 +0200 Subject: [PATCH 003/135] changelog and doc ;) --- CHANGELOG.rst | 1 + doc/converter/powerfactory.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 610afd629..dbfb3a35b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,7 @@ Change Log [upcoming release] - 2024-..-.. ------------------------------- +- [ADDED] Static Var Compensator with Voltage Control - [ADDED] low voltage grid Schutterwald - [FIXED] trafo3w with tap changer at star point corrected - [FIXED] namespace changes from numpy 2.0 release diff --git a/doc/converter/powerfactory.rst b/doc/converter/powerfactory.rst index a77f5eb25..25752ac7d 100644 --- a/doc/converter/powerfactory.rst +++ b/doc/converter/powerfactory.rst @@ -27,6 +27,7 @@ The documentation describes how to use the exporter as a function in "Engine mod - RelFuse (Fuses) - ElmZpu (pu Impedance) - ElmSind (Series Reactor) + - ElmSvs (Static Var Compensator with Voltage Control) Setup PowerFactory and Python From 581746be75872220a238ec67237ac75badfaf99f Mon Sep 17 00:00:00 2001 From: Marie Fischer Date: Fri, 19 Jul 2024 14:21:27 +0200 Subject: [PATCH 004/135] update in_service in create_svc and create_vac, in service is set via out of service AND switch position --- pandapower/converter/powerfactory/pp_import_functions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index 9f9e488f5..40b2f402a 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -2712,7 +2712,8 @@ def create_vac(net, item): except IndexError: logger.error("Cannot add VAC '%s': not connected" % item.loc_name) return - + + in_service = monopolar_in_service(item) params = { 'name': item.loc_name, 'bus': bus, @@ -2720,7 +2721,7 @@ def create_vac(net, item): 'qs_mvar': item.Qload - item.Qgen, 'pz_mw': item.Pzload, 'qz_mvar': item.Qzload, - 'in_service': not bool(item.outserv) + 'in_service': in_service } if item.itype == 3: @@ -2830,8 +2831,9 @@ def create_svc(net, item, pv_as_slack, pf_variable_p_gen, dict_net): logger.debug('creating sym %s as gen' % name) vm_pu = item.usetp + in_service = monopolar_in_service(item) sid = pp.create_gen(net, bus=bus1, p_mw=0, vm_pu=vm_pu, - name=name, type="SVC", in_service=not bool(item.outserv)) + name=name, type="SVC", in_service=in_service) element = 'gen' if sid is None or element is None: From 8eea154129c377ca098ff154a21cee18016e92f0 Mon Sep 17 00:00:00 2001 From: Jannis Kupka Date: Sun, 21 Jul 2024 22:26:44 +0200 Subject: [PATCH 005/135] fix index error during unbalanced powerflow with multiple ext grids --- pandapower/results_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/results_gen.py b/pandapower/results_gen.py index ac7b64e6a..777de077f 100644 --- a/pandapower/results_gen.py +++ b/pandapower/results_gen.py @@ -115,7 +115,7 @@ def _get_ext_grid_results_3ph(net, ppc0, ppc1, ppc2): eg_bus_idx_ppc = np.real(ppc1["gen"][eg_idx_ppc, GEN_BUS]).astype(np.int64) # read results from ppc for these buses V012 = np.array(np.zeros((3, n_res_eg)),dtype = np.complex128) - V012[:, eg_is_idx] = np.array([ppc["bus"][eg_bus_idx_ppc, VM] * ppc["bus"][eg_bus_idx_ppc, BASE_KV] + V012[:, eg_idx_ppc] = np.array([ppc["bus"][eg_bus_idx_ppc, VM] * ppc["bus"][eg_bus_idx_ppc, BASE_KV] * np.exp(1j * np.deg2rad(ppc["bus"][eg_bus_idx_ppc, VA])) for ppc in [ppc0, ppc1, ppc2]]) From 117daa08215c3033a62552829161ed09634d28c9 Mon Sep 17 00:00:00 2001 From: Marie Fischer Date: Wed, 24 Jul 2024 13:27:47 +0200 Subject: [PATCH 006/135] add condition, svc only created if its voltage controlled --- .../powerfactory/pp_import_functions.py | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index 40b2f402a..166941b3c 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -2814,7 +2814,7 @@ def create_scap(net, item): (net.impedance.at[scap, 'name'], scap)) def create_svc(net, item, pv_as_slack, pf_variable_p_gen, dict_net): - # SVC is controlled and therefore modelled the same way as a voltage controlled synchron machine (gen) + # SVC is voltage controlled and therefore modelled the same way as a voltage controlled synchron machine (gen) # TODO: at least implement a uncontrolled svc as synchron machine with const. Q # TODO: transfer item entries for usage of pp.create_svc, x_l_ohm, x_cvar_ohm, # thyristor_firing_angle must be computed @@ -2826,33 +2826,34 @@ def create_svc(net, item, pv_as_slack, pf_variable_p_gen, dict_net): try: bus1 = get_connection_nodes(net, item, 1) except IndexError: - logger.error("Cannot add Sgen '%s': not connected" % name) + logger.error("Cannot add SVC '%s': not connected" % name) return - - logger.debug('creating sym %s as gen' % name) - vm_pu = item.usetp - in_service = monopolar_in_service(item) - sid = pp.create_gen(net, bus=bus1, p_mw=0, vm_pu=vm_pu, - name=name, type="SVC", in_service=in_service) - element = 'gen' - - if sid is None or element is None: - logger.error('Error! SVC not created') - logger.debug('created svc at index <%s>' % sid) - - net[element].loc[sid, 'description'] = ' \n '.join(item.desc) if len(item.desc) > 0 else '' - add_additional_attributes(item, net, element, sid, attr_dict={"for_name": "equipment"}, - attr_list=["sernum", "chr_name", "cpSite.loc_name"]) - - if item.HasResults(0): # 'm' results... - logger.debug('<%s> has results' % name) - net['res_' + element].at[sid, "pf_p"] = ga(item, 'm:P:bus1') #* multiplier - net['res_' + element].at[sid, "pf_q"] = ga(item, 'm:Q:bus1') #* multiplier - else: - net['res_' + element].at[sid, "pf_p"] = np.nan - net['res_' + element].at[sid, "pf_q"] = np.nan - logger.info('not creating svc for %s' % item.loc_name) + if item.i_ctrl==1: # 0: no control, 1: voltage control, 2: reactive power control + logger.debug('creating SVC %s as gen' % name) + vm_pu = item.usetp + in_service = monopolar_in_service(item) + svc = pp.create_gen(net, bus=bus1, p_mw=0, vm_pu=vm_pu, + name=name, type="SVC", in_service=in_service) + element = 'gen' + + if svc is None or element is None: + logger.error('Error! SVC not created') + logger.debug('created svc at index <%s>' % svc) + + net[element].loc[svc, 'description'] = ' \n '.join(item.desc) if len(item.desc) > 0 else '' + add_additional_attributes(item, net, element, svc, attr_dict={"for_name": "equipment"}, + attr_list=["sernum", "chr_name", "cpSite.loc_name"]) + + if item.HasResults(0): # 'm' results... + logger.debug('<%s> has results' % name) + net['res_' + element].at[svc, "pf_p"] = ga(item, 'm:P:bus1') #* multiplier + net['res_' + element].at[svc, "pf_q"] = ga(item, 'm:Q:bus1') #* multiplier + else: + net['res_' + element].at[svc, "pf_p"] = np.nan + net['res_' + element].at[svc, "pf_q"] = np.nan + else: + logger.info('not creating SVC for %s' % item.loc_name) def split_line_at_length(net, line, length_pos): From 8c5ab2ff699f46473492bcfeff3bc1c51b982fd9 Mon Sep 17 00:00:00 2001 From: thomaswhaley Date: Tue, 30 Jul 2024 11:30:07 +1200 Subject: [PATCH 007/135] Vectorize switch bus and element checks in create_switches() #2303 --- CHANGELOG.rst | 1 + pandapower/create.py | 50 +++++++++++++----------------- pandapower/test/api/test_create.py | 34 +++++++++++++++----- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 29d01d8bc..39989e8ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -42,6 +42,7 @@ Change Log - [FIXED] deprecation of matplotlib.cm.get_cmap(name) -> matplotlib.colormaps[name] - [FIXED] merge_nets failing if net2 has custom DataFrame that is not present in net1 - [FIXED] fixed some small bugs in the CGMES converter and improved its speed +- [CHANGED] vectorization of switch bus and element checks in create_switches() [2.14.7] - 2024-06-14 ------------------------------- diff --git a/pandapower/create.py b/pandapower/create.py index cd7ab1855..2234e66f5 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -3795,34 +3795,28 @@ def create_switches(net, buses, elements, et, closed=True, type=None, name=None, """ index = _get_multiple_index_with_check(net, "switch", index, len(buses), name="Switches") _check_multiple_node_elements(net, buses) - - for element, elm_type, bus in zip(elements, et, buses): - if elm_type == "l": - elm_tab = 'line' - if element not in net[elm_tab].index: - raise UserWarning("Line %s does not exist" % element) - if (not net[elm_tab]["from_bus"].loc[element] == bus and - not net[elm_tab]["to_bus"].loc[element] == bus): - raise UserWarning("Line %s not connected to bus %s" % (element, bus)) - elif elm_type == "t": - elm_tab = 'trafo' - if element not in net[elm_tab].index: - raise UserWarning("Trafo %s does not exist" % element) - if (not net[elm_tab]["hv_bus"].loc[element] == bus and - not net[elm_tab]["lv_bus"].loc[element] == bus): - raise UserWarning("Trafo %s not connected to bus %s" % (element, bus)) - elif elm_type == "t3": - elm_tab = 'trafo3w' - if element not in net[elm_tab].index: - raise UserWarning("Trafo3w %s does not exist" % element) - if (not net[elm_tab]["hv_bus"].loc[element] == bus and - not net[elm_tab]["mv_bus"].loc[element] == bus and - not net[elm_tab]["lv_bus"].loc[element] == bus): - raise UserWarning("Trafo3w %s not connected to bus %s" % (element, bus)) - elif elm_type == "b": - _check_node_element(net, element) - else: - raise UserWarning("Unknown element type") + _check_multiple_node_elements(net, elements, name="elements") + + buses_s = pd.Series(buses, name="bus") + elements_s = pd.Series(elements, name="element") + et_s = pd.Series([et] * len(buses) if isinstance(et, str) else et, name="et") + # Ensure switches are connected correctly. + for typ, table, joining_busses in [("l", "line", ["from_bus", "to_bus"]), + ("t", "trafo", ["hv_bus", "lv_bus"]), + ("t3", "trafo3w", ["hv_bus", "mv_bus", "lv_bus"])]: + bus_not_connected_mask = ~elements_s[et_s == typ].isin(net[table].index) + if np_any(bus_not_connected_mask): + raise UserWarning("%s busses do not exist: %s" % + (table.capitalize(), elements_s[et_s == typ][bus_not_connected_mask].to_list())) + merged = pd.merge(buses_s[et_s == typ].set_axis(elements_s[et_s == typ]), + net[table][joining_busses], left_index=True, right_index=True) + not_connected_mask = True + for joining_bus in joining_busses: + not_connected_mask &= merged.bus != merged[joining_bus] + if np_any(not_connected_mask): + bus_element_pairs = list(zip(merged.bus[not_connected_mask], not_connected_mask.index)) + raise UserWarning("%s not connected (%s element, bus): %s" % + (table.capitalize(), table, bus_element_pairs)) entries = {"bus": buses, "element": elements, "et": et, "closed": closed, "type": type, "name": name, "z_ohm": z_ohm, "in_ka": in_ka} diff --git a/pandapower/test/api/test_create.py b/pandapower/test/api/test_create.py index 1e1ed8dd7..922322817 100644 --- a/pandapower/test/api/test_create.py +++ b/pandapower/test/api/test_create.py @@ -1292,11 +1292,11 @@ def test_create_switches_raise_errorexcept(): pp.create_switches( net, buses=[6, b2, b3], elements=[l1, t1, b4], et=["l", "t", "b"], z_ohm=0.0 ) - with pytest.raises(UserWarning, match="Line 1 does not exist"): + with pytest.raises(UserWarning, match=r"Line busses do not exist: \[1\]"): pp.create_switches( net, buses=[b1, b2, b3], elements=[1, t1, b4], et=["l", "t", "b"], z_ohm=0.0 ) - with pytest.raises(UserWarning, match="Line %s not connected to bus %s" % (l1, b3)): + with pytest.raises(UserWarning, match=r"Line not connected \(line element, bus\): \[\(%s, %s\)\]" % (b3, l1)): pp.create_switches( net, buses=[b3, b2, b3], @@ -1304,12 +1304,12 @@ def test_create_switches_raise_errorexcept(): et=["l", "t", "b"], z_ohm=0.0, ) - with pytest.raises(UserWarning, match="Trafo 1 does not exist"): + with pytest.raises(UserWarning, match=r"Trafo busses do not exist: \[1\]"): pp.create_switches( net, buses=[b1, b2, b3], elements=[l1, 1, b4], et=["l", "t", "b"], z_ohm=0.0 ) with pytest.raises( - UserWarning, match="Trafo %s not connected to bus %s" % (t1, b1) + UserWarning, match=r"Trafo not connected \(trafo element, bus\): \[\(%s, %s\)\]" % (b1, t1) ): pp.create_switches( net, @@ -1319,12 +1319,12 @@ def test_create_switches_raise_errorexcept(): z_ohm=0.0, ) with pytest.raises( - UserWarning, match=r"Cannot attach to bus 6, bus does not exist" + UserWarning, match=r"Cannot attach to elements \{6\}, they do not exist" ): pp.create_switches( net, buses=[b1, b2, b3], elements=[l1, t1, 6], et=["l", "t", "b"], z_ohm=0.0 ) - with pytest.raises(UserWarning, match="Trafo3w 1 does not exist"): + with pytest.raises(UserWarning, match=r"Trafo3w busses do not exist: \[1\]"): pp.create_switches( net, buses=[b1, b2, b3], @@ -1333,7 +1333,7 @@ def test_create_switches_raise_errorexcept(): z_ohm=0.0, ) with pytest.raises( - UserWarning, match="Trafo3w %s not connected to bus %s" % (t3w1, b3) + UserWarning, match=r"Trafo3w not connected \(trafo3w element, bus\): \[\(%s, %s\)\]" % (b3, t3w1) ): pp.create_switches( net, @@ -1342,6 +1342,26 @@ def test_create_switches_raise_errorexcept(): et=["l", "t", "t3"], z_ohm=0.0, ) + with pytest.raises( + UserWarning, match=r"Cannot attach to elements \{12398\}, they do not exist" + ): + pp.create_switches( + net, + buses=[b1, b2], + elements=[b3, 12398], + et=["b", "b"], + z_ohm=0.0, + ) + with pytest.raises( + UserWarning, match=r"Cannot attach to buses \{13098\}, they do not exist" + ): + pp.create_switches( + net, + buses=[b1, 13098], + elements=[b2, b3], + et=["b", "b"], + z_ohm=0.0, + ) def test_create_loads(): From 97d5c00dd71bd2d77dd788f4c059f9dbea261b36 Mon Sep 17 00:00:00 2001 From: thomaswhaley Date: Tue, 6 Aug 2024 11:27:30 +1200 Subject: [PATCH 008/135] fix buses typo --- pandapower/create.py | 2 +- pandapower/test/api/test_create.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandapower/create.py b/pandapower/create.py index 2234e66f5..e7e5052bb 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -3806,7 +3806,7 @@ def create_switches(net, buses, elements, et, closed=True, type=None, name=None, ("t3", "trafo3w", ["hv_bus", "mv_bus", "lv_bus"])]: bus_not_connected_mask = ~elements_s[et_s == typ].isin(net[table].index) if np_any(bus_not_connected_mask): - raise UserWarning("%s busses do not exist: %s" % + raise UserWarning("%s buses do not exist: %s" % (table.capitalize(), elements_s[et_s == typ][bus_not_connected_mask].to_list())) merged = pd.merge(buses_s[et_s == typ].set_axis(elements_s[et_s == typ]), net[table][joining_busses], left_index=True, right_index=True) diff --git a/pandapower/test/api/test_create.py b/pandapower/test/api/test_create.py index 922322817..a40b65d48 100644 --- a/pandapower/test/api/test_create.py +++ b/pandapower/test/api/test_create.py @@ -1292,7 +1292,7 @@ def test_create_switches_raise_errorexcept(): pp.create_switches( net, buses=[6, b2, b3], elements=[l1, t1, b4], et=["l", "t", "b"], z_ohm=0.0 ) - with pytest.raises(UserWarning, match=r"Line busses do not exist: \[1\]"): + with pytest.raises(UserWarning, match=r"Line buses do not exist: \[1\]"): pp.create_switches( net, buses=[b1, b2, b3], elements=[1, t1, b4], et=["l", "t", "b"], z_ohm=0.0 ) @@ -1304,7 +1304,7 @@ def test_create_switches_raise_errorexcept(): et=["l", "t", "b"], z_ohm=0.0, ) - with pytest.raises(UserWarning, match=r"Trafo busses do not exist: \[1\]"): + with pytest.raises(UserWarning, match=r"Trafo buses do not exist: \[1\]"): pp.create_switches( net, buses=[b1, b2, b3], elements=[l1, 1, b4], et=["l", "t", "b"], z_ohm=0.0 ) @@ -1324,7 +1324,7 @@ def test_create_switches_raise_errorexcept(): pp.create_switches( net, buses=[b1, b2, b3], elements=[l1, t1, 6], et=["l", "t", "b"], z_ohm=0.0 ) - with pytest.raises(UserWarning, match=r"Trafo3w busses do not exist: \[1\]"): + with pytest.raises(UserWarning, match=r"Trafo3w buses do not exist: \[1\]"): pp.create_switches( net, buses=[b1, b2, b3], From 4a2bac0616ae3a3a463f3fc689d9d15ced32b140 Mon Sep 17 00:00:00 2001 From: thomaswhaley Date: Wed, 7 Aug 2024 13:27:45 +1200 Subject: [PATCH 009/135] fix buses error message --- pandapower/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/create.py b/pandapower/create.py index 1214f20b2..dfd791c50 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -4505,7 +4505,7 @@ def create_switches(net, buses, elements, et, closed=True, type=None, name=None, for joining_bus in joining_busses: not_connected_mask &= merged.bus != merged[joining_bus] if np_any(not_connected_mask): - bus_element_pairs = list(zip(merged.bus[not_connected_mask], not_connected_mask.index)) + bus_element_pairs = list(zip(merged.bus[not_connected_mask], merged.index[not_connected_mask])) raise UserWarning("%s not connected (%s element, bus): %s" % (table.capitalize(), table, bus_element_pairs)) From 69a878411650ac5adcaba82799a404731f98b8b9 Mon Sep 17 00:00:00 2001 From: Jannis Kupka Date: Wed, 7 Aug 2024 17:38:46 +0200 Subject: [PATCH 010/135] updated changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 54610f400..05ed65c40 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -67,6 +67,7 @@ Change Log - [ADDED] support for unequal leakage resistance and reactance for HV and LV sides of a 2W-transformer - [ADDED] Add VSC element, dc buses, dc lines, and hybrid AC/DC power flow calculation - [CHANGED] accelerate _integrate_power_elements_connected_with_switch_buses() in get_equivalent() +- [FIXED] index error during unbalanced powerflow if multiple external grids are present [2.14.7] - 2024-06-14 ------------------------------- From 9ff2c8104445a4f67c0f06d5bf92d6e550b22604 Mon Sep 17 00:00:00 2001 From: harrahx3 <46087437+harrahx3@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:00:33 +0200 Subject: [PATCH 011/135] Update line_par.csv Change x_ohm_per_km description from inductance to reactance --- doc/elements/line_par.csv | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/elements/line_par.csv b/doc/elements/line_par.csv index 9054a0f5a..764b86873 100644 --- a/doc/elements/line_par.csv +++ b/doc/elements/line_par.csv @@ -5,18 +5,18 @@ from_bus*;integer;;Index of bus where the line starts to_bus*;integer;;Index of bus where the line ends length_km*;float;:math:`>` 0;length of the line [km] r_ohm_per_km*;float;:math:`\geq` 0 ;resistance of the line [Ohm per km] -x_ohm_per_km*;float;:math:`\geq` 0 ;inductance of the line [Ohm per km] +x_ohm_per_km*;float;:math:`\geq` 0 ;reactance of the line [Ohm per km] c_nf_per_km*;float;:math:`\geq` 0 ;capacitance of the line (line-to-earth) [nano Farad per km] r0_ohm_per_km****;float;:math:`\geq` 0 ;zero sequence resistance of the line [Ohm per km] -x0_ohm_per_km****;float;:math:`\geq` 0 ;zero sequence inductance of the line [Ohm per km] +x0_ohm_per_km****;float;:math:`\geq` 0 ;zero sequence reactance of the line [Ohm per km] c0_nf_per_km****;float;:math:`\geq` 0 ;zero sequence capacitance of the line [nano Farad per km] g_us_per_km*;float;:math:`\geq` 0 ;dielectric conductance of the line [micro Siemens per km] max_i_ka*;float;:math:`>` 0 ;maximal thermal current [kilo Ampere] parallel*;integer;:math:`\geq` 1;number of parallel line systems df*;float;0...1 ;derating factor (scaling) for max_i_ka -type;string;"| Naming conventions: - -| *""ol""* - overhead line +type;string;"| Naming conventions: + +| *""ol""* - overhead line | *""cs""* - underground cable system";type of line max_loading_percent**;float;:math:`>` 0 ;Maximum loading of the line endtemp_degree***;float;:math:`>` 0 ;Short-Circuit end temperature of the line From 3f7dcd4e3b607cd7d19a2860526486f64c9fadfc Mon Sep 17 00:00:00 2001 From: harrahx3 <46087437+harrahx3@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:33:53 +0200 Subject: [PATCH 012/135] Update create.py p_mw is actually a required argument of create_load and create_loads functions --- pandapower/create.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pandapower/create.py b/pandapower/create.py index 7e98c7954..c91918ce5 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -958,12 +958,13 @@ def create_load(net, bus, p_mw, q_mvar=0, const_z_percent=0, const_i_percent=0, **bus** (int) - The bus id to which the load is connected - OPTIONAL: - **p_mw** (float, default 0) - The active power of the load + **p_mw** (float) - The active power of the load - positive value -> load - negative value -> generation + OPTIONAL: + **q_mvar** (float, default 0) - The reactive power of the load **const_z_percent** (float, default 0) - percentage of p_mw and q_mvar that will be \ @@ -1044,12 +1045,13 @@ def create_loads(net, buses, p_mw, q_mvar=0, const_z_percent=0, const_i_percent= **buses** (list of int) - A list of bus ids to which the loads are connected - OPTIONAL: **p_mw** (list of floats) - The active power of the loads - postive value -> load - negative value -> generation + OPTIONAL: + **q_mvar** (list of floats, default 0) - The reactive power of the loads **const_z_percent** (list of floats, default 0) - percentage of p_mw and q_mvar that will \ From 5cf8adbe8e555cfa22cf9d093b4e0473d056b508 Mon Sep 17 00:00:00 2001 From: harrahx3 <46087437+harrahx3@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:35:23 +0200 Subject: [PATCH 013/135] Update create.py Fix style --- pandapower/create.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandapower/create.py b/pandapower/create.py index c91918ce5..b80914f24 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -964,7 +964,6 @@ def create_load(net, bus, p_mw, q_mvar=0, const_z_percent=0, const_i_percent=0, - negative value -> generation OPTIONAL: - **q_mvar** (float, default 0) - The reactive power of the load **const_z_percent** (float, default 0) - percentage of p_mw and q_mvar that will be \ @@ -1051,7 +1050,6 @@ def create_loads(net, buses, p_mw, q_mvar=0, const_z_percent=0, const_i_percent= - negative value -> generation OPTIONAL: - **q_mvar** (list of floats, default 0) - The reactive power of the loads **const_z_percent** (list of floats, default 0) - percentage of p_mw and q_mvar that will \ From f49e4289f38f7273442741334956ecac17537dfe Mon Sep 17 00:00:00 2001 From: srdm Date: Mon, 2 Sep 2024 15:15:26 +0200 Subject: [PATCH 014/135] consider oos elements --- pandapower/topology/create_graph.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pandapower/topology/create_graph.py b/pandapower/topology/create_graph.py index 0b1d07cab..4b07aeb05 100644 --- a/pandapower/topology/create_graph.py +++ b/pandapower/topology/create_graph.py @@ -140,7 +140,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped line = get_edge_table(net, "line", include_lines) if line is not None: - indices, parameter, in_service = init_par(line, calc_branch_impedances) + indices, parameter, in_service = init_par(line, calc_branch_impedances, include_out_of_service) indices[:, F_BUS] = line.from_bus.values indices[:, T_BUS] = line.to_bus.values @@ -166,7 +166,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped impedance = get_edge_table(net, "impedance", include_impedances) if impedance is not None: - indices, parameter, in_service = init_par(impedance, calc_branch_impedances) + indices, parameter, in_service = init_par(impedance, calc_branch_impedances, include_out_of_service) indices[:, F_BUS] = impedance.from_bus.values indices[:, T_BUS] = impedance.to_bus.values @@ -182,7 +182,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped tcsc = get_edge_table(net, "tcsc", include_tcsc) if tcsc is not None: - indices, parameter, in_service = init_par(tcsc, calc_branch_impedances) + indices, parameter, in_service = init_par(tcsc, calc_branch_impedances, include_out_of_service) indices[:, F_BUS] = tcsc.from_bus.values indices[:, T_BUS] = tcsc.to_bus.values @@ -197,7 +197,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped dclines = get_edge_table(net, "dcline", include_dclines) if dclines is not None: - indices, parameter, in_service = init_par(dclines, calc_branch_impedances) + indices, parameter, in_service = init_par(dclines, calc_branch_impedances, include_out_of_service) indices[:, F_BUS] = dclines.from_bus.values indices[:, T_BUS] = dclines.to_bus.values @@ -210,7 +210,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped trafo = get_edge_table(net, "trafo", include_trafos) if trafo is not None: - indices, parameter, in_service = init_par(trafo, calc_branch_impedances) + indices, parameter, in_service = init_par(trafo, calc_branch_impedances, include_out_of_service) indices[:, F_BUS] = trafo.hv_bus.values indices[:, T_BUS] = trafo.lv_bus.values @@ -255,7 +255,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped open_trafo3w_buses = net.switch.bus.values[mask] open_trafo3w = (open_trafo3w_index + open_trafo3w_buses * 1j).flatten() for f, t in combinations(sides, 2): - indices, parameter, in_service = init_par(trafo3w, calc_branch_impedances) + indices, parameter, in_service = init_par(trafo3w, calc_branch_impedances, include_out_of_service) indices[:, F_BUS] = trafo3w["%s_bus" % f].values indices[:, T_BUS] = trafo3w["%s_bus" % t].values @@ -372,7 +372,7 @@ def get_baseR(net, ppc, buses): return np.square(base_kv) / net.sn_mva -def init_par(tab, calc_branch_impedances=False): +def init_par(tab, calc_branch_impedances=False, include_out_of_service=False): n = tab.shape[0] indices = np.zeros((n, 3), dtype=np.int64) indices[:, INDEX] = tab.index @@ -382,7 +382,7 @@ def init_par(tab, calc_branch_impedances=False): parameters = np.zeros((n, 1), dtype=float) if "in_service" in tab: - return indices, parameters, tab.in_service.values.copy() + return indices, parameters, np.ones(n, dtype=bool) if include_out_of_service else tab.in_service.values.copy() else: return indices, parameters From c8ba001119465a9d5b4e5fc5e6ce62af161d9060 Mon Sep 17 00:00:00 2001 From: harrahx3 Date: Mon, 2 Sep 2024 21:10:21 +0200 Subject: [PATCH 015/135] Correcting examples in docstrings --- pandapower/create.py | 77 ++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/pandapower/create.py b/pandapower/create.py index b80914f24..f7bb54816 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -698,7 +698,7 @@ def create_bus(net, vn_kv, name=None, index=None, geodata=None, type="b", zone=N **index** (int) - The unique ID of the created element EXAMPLE: - create_bus(net, name = "bus1") + create_bus(net, 20., name = "bus1") """ index = _get_index_with_check(net, "bus", index) @@ -765,7 +765,7 @@ def create_bus_dc(net, vn_kv, name=None, index=None, geodata=None, type="b", zon **index** (int) - The unique ID of the created element EXAMPLE: - create_bus_dc(net, name = "bus1") + create_bus_dc(net, 20., name = "bus1") """ index = _get_index_with_check(net, "bus_dc", index) @@ -887,20 +887,20 @@ def create_buses_dc(net, nr_buses_dc, vn_kv, index=None, name=None, type="b", ge **nr_buses_dc** (int) - The number of dc buses that is created - OPTIONAL: - **name** (list of string, default None) - the name for this dc bus + **vn_kv** (float) - The grid voltage level. + OPTIONAL: **index** (list of int, default None) - Force specified IDs if available. If None, the indices \ higher than the highest already existing index are selected. + + **name** (list of string, default None) - the name for this dc bus - **vn_kv** (float) - The grid voltage level. + **type** (string, default "b") - Type of the dc bus. "n" - auxilary node, + "b" - busbar, "m" - muff **geodata** ((x,y)-tuple or list of tuples with length == nr_buses_dc, default None) - coordinates used for plotting - **type** (string, default "b") - Type of the dc bus. "n" - auxilary node, - "b" - busbar, "m" - muff - **zone** (string, None) - grid region **in_service** (list of boolean) - True for in_service or False for out of service @@ -919,7 +919,7 @@ def create_buses_dc(net, nr_buses_dc, vn_kv, index=None, name=None, type="b", ge **index** (int) - The unique indices ID of the created elements EXAMPLE: - create_buses_dc(net, name = ["bus1","bus2"]) + create_buses_dc(net, 2, [20., 20.], name = ["bus1","bus2"]) """ index = _get_multiple_index_with_check(net, "bus_dc", index, nr_buses_dc) @@ -1355,7 +1355,7 @@ def create_sgen(net, bus, p_mw, q_mvar=0, sn_mva=nan, name=None, index=None, **index** (int) - The unique ID of the created sgen EXAMPLE: - create_sgen(net, 1, p_mw = -120) + create_sgen(net, 1, p_mw = 120) """ _check_node_element(net, bus) @@ -2016,7 +2016,7 @@ def create_gens(net, buses, p_mw, vm_pu=1., sn_mva=nan, name=None, index=None, m **index** (int) - The unique ID of the created generator EXAMPLE: - create_gen(net, 1, p_mw = 120, vm_pu = 1.02) + create_gens(net, [1, 2], p_mw = [120, 100], vm_pu = [1.02, 0.99]) """ _check_multiple_node_elements(net, buses) @@ -2294,7 +2294,7 @@ def create_line(net, from_bus, to_bus, length_km, std_type, name=None, index=Non **index** (int) - The unique ID of the created line EXAMPLE: - create_line(net, "line1", from_bus = 0, to_bus = 1, length_km=0.1, std_type="NAYY 4x50 SE") + create_line(net, from_bus = 0, to_bus = 1, length_km=0.1, std_type="NAYY 4x50 SE", name = "line1") """ @@ -2430,7 +2430,7 @@ def create_line_dc(net, from_bus_dc, to_bus_dc, length_km, std_type, name=None, **index** (int) - The unique ID of the created dc line EXAMPLE: - create_line_dc(net, "line_dc1", from_bus_dc = 0, to_bus_dc = 1, length_km=0.1, std_type="Not defined yet") + create_line_dc(net, from_bus_dc = 0, to_bus_dc = 1, length_km=0.1, std_type="NAYY 4x50 SE", name = "line_dc1") """ @@ -2560,7 +2560,7 @@ def create_lines(net, from_buses, to_buses, length_km, std_type, name=None, inde **index** (list of int) - The unique ID of the created lines EXAMPLE: - create_lines(net, ["line1", "line2"], from_buses=[0,1], to_buses=[2,3], length_km=0.1, std_type="NAYY 4x50 SE") + create_lines(net, from_buses=[0,1], to_buses=[2,3], length_km=0.1, std_type="NAYY 4x50 SE", name = ["line1", "line2"]) """ _check_multiple_branch_elements(net, from_buses, to_buses, "Lines") @@ -2690,8 +2690,8 @@ def create_lines_dc(net, from_buses_dc, to_buses_dc, length_km, std_type, name=N **index** (list of int) - The unique ID of the created dc lines EXAMPLE: - create_lines_dc(net, ["line_dc1","line_dc2"], from_buses_dc=[0,1], to_buses_dc=[2,3], length_km=0.1, - std_type="Not specified yet") + create_lines_dc(net, from_buses_dc=[0,1], to_buses_dc=[2,3], length_km=0.1, + std_type="Not specified yet", name = ["line_dc1","line_dc2"]) """ _check_multiple_branch_elements(net, from_buses_dc, to_buses_dc, "Lines_dc", node_name='bus_dc', plural='(all dc buses)') @@ -2831,9 +2831,9 @@ def create_line_from_parameters(net, from_bus, to_bus, length_km, r_ohm_per_km, **index** (int) - The unique ID of the created line EXAMPLE: - create_line_from_parameters(net, "line1", from_bus = 0, to_bus = 1, lenght_km=0.1, + create_line_from_parameters(net, from_bus = 0, to_bus = 1, lenght_km=0.1, r_ohm_per_km = .01, x_ohm_per_km = 0.05, c_nf_per_km = 10, - max_i_ka = 0.4) + max_i_ka = 0.4, name = "line1") """ @@ -2967,8 +2967,8 @@ def create_line_dc_from_parameters(net, from_bus_dc, to_bus_dc, length_km, r_ohm **index** (int) - The unique ID of the created line EXAMPLE: - create_line_dc_from_parameters(net, "line_dc1", from_bus_dc = 0, to_bus_dc = 1, lenght_km=0.1, - r_ohm_per_km = .01, max_i_ka = 0.4) + create_line_dc_from_parameters(net, from_bus_dc = 0, to_bus_dc = 1, lenght_km=0.1, + r_ohm_per_km = .01, max_i_ka = 0.4, name = "line_dc1") """ @@ -3106,8 +3106,8 @@ def create_lines_from_parameters(net, from_buses, to_buses, length_km, r_ohm_per **index** (list of int) - The unique ID of the created lines EXAMPLE: - create_lines_from_parameters(net, ["line1","line2"], from_buses = [0,1], to_buses = [2,3], length_km= 0.1, - r_ohm_per_km = .01, x_ohm_per_km = 0.05, c_nf_per_km = 10, max_i_ka = 0.4) + create_lines_from_parameters(net, from_buses = [0,1], to_buses = [2,3], length_km= 0.1, + r_ohm_per_km = .01, x_ohm_per_km = 0.05, c_nf_per_km = 10, max_i_ka = 0.4, name = ["line1","line2"]) """ _check_multiple_branch_elements(net, from_buses, to_buses, "Lines") @@ -3229,11 +3229,11 @@ def create_lines_dc_from_parameters(net, from_buses_dc, to_buses_dc, length_km, tdpf_delay_s parameter) OUTPUT: - **index** (list of int) - The unique ID of the created dc lines + **index** (list of int) - The list of IDs of the created dc lines EXAMPLE: - create_lines_dc_from_parameters(net, name= ["line_dc1","line_dc2"], from_buses_dc = [0,1], to_buses_dc = [2,3], lenght_km=0.1, - r_ohm_per_km = .01, max_i_ka = 0.4) + create_lines_dc_from_parameters(net, from_buses_dc = [0,1], to_buses_dc = [2,3], lenght_km=0.1, + r_ohm_per_km = .01, max_i_ka = 0.4, name= ["line_dc1","line_dc2"]) """ _check_multiple_branch_elements(net, from_buses_dc, to_buses_dc, "Lines_dc",node_name='bus_dc', plural= '(all dc buses)') @@ -3345,8 +3345,8 @@ def create_transformer(net, hv_bus, lv_bus, std_type, name=None, tap_pos=nan, in **index** (int) - The unique ID of the created transformer EXAMPLE: - create_transformer(net, hv_bus = 0, lv_bus = 1, name = "trafo1", std_type = \ - "0.4 MVA 10/0.4 kV") + create_transformer(net, hv_bus = 0, lv_bus = 1, std_type = "0.4 MVA 10/0.4 kV",\ + name = "trafo1") """ # Check if bus exist to attach the trafo to @@ -3769,10 +3769,10 @@ def create_transformers_from_parameters(net, hv_buses, lv_buses, sn_mva, vn_hv_k ** only considered in loadflow if calculate_voltage_angles = True OUTPUT: - **index** (int) - The unique ID of the created transformer + **index** (int) - The list of IDs of the created transformers EXAMPLE: - create_transformer_from_parameters(net, hv_bus=0, lv_bus=1, name="trafo1", sn_mva=40, \ + create_transformers_from_parameters(net, hv_bus=[0, 1], lv_bus=[2, 3], name="trafo1", sn_mva=40, \ vn_hv_kv=110, vn_lv_kv=10, vk_percent=10, vkr_percent=0.3, pfe_kw=30, \ i0_percent=0.1, shift_degree=30) """ @@ -4293,7 +4293,7 @@ def create_transformers3w_from_parameters( **trafo_id** - List of trafo_ids of the created 3W transformers Example: - create_transformer3w_from_parameters(net, hv_bus=0, mv_bus=1, lv_bus=2, name="trafo1", + create_transformers3w_from_parameters(net, hv_bus=[0, 3], mv_bus=[1, 4], lv_bus=[2, 5], name="trafo1", sn_hv_mva=40, sn_mv_mva=20, sn_lv_mva=20, vn_hv_kv=110, vn_mv_kv=20, vn_lv_kv=10, vk_hv_percent=10,vk_mv_percent=11, vk_lv_percent=12, vkr_hv_percent=0.3, vkr_mv_percent=0.31, vkr_lv_percent=0.32, pfe_kw=30, i0_percent=0.1, shift_mv_degree=30, @@ -4401,9 +4401,9 @@ def create_switch(net, bus, element, et, closed=True, type=None, name=None, inde **sid** - The unique switch_id of the created switch EXAMPLE: - create_switch(net, bus = 0, element = 1, et = 'b', type ="LS", z_ohm = 0.1) + create_switch(net, bus=0, element=1, et='b', type="LS", z_ohm=0.1) - create_switch(net, bus = 0, element = 1, et = 'l') + create_switch(net, bus=0, element=1, et='l') """ _check_node_element(net, bus) @@ -4485,12 +4485,12 @@ def create_switches(net, buses, elements, et, closed=True, type=None, name=None, normal operating conditions without tripping OUTPUT: - **sid** - The unique switch_id of the created switch + **sid** - List of switch_id of the created switches EXAMPLE: - create_switch(net, bus = 0, element = 1, et = 'b', type ="LS", z_ohm = 0.1) + create_switches(net, buses=[0, 1], element=1, et='b', type="LS", z_ohm=0.1) - create_switch(net, bus = 0, element = 1, et = 'l') + create_switches(net, buses=[0, 1], element=1, et='l') """ index = _get_multiple_index_with_check(net, "switch", index, len(buses), name="Switches") @@ -4611,10 +4611,10 @@ def create_shunts(net, buses, q_mvar, p_mw=0., vn_kv=None, step=1, max_step=1, n index one higher than the highest already existing index is selected. OUTPUT: - **index** (int) - The unique ID of the created shunt + **index** (int) - The list of IDs of the created shunts EXAMPLE: - create_shunt(net, 0, 20) + create_shunts(net, [0, 2], [20, 30]) """ _check_multiple_node_elements(net, buses) @@ -5597,7 +5597,8 @@ def create_pwl_costs(net, elements, et, points, power_type="p", index=None, chec To create a gen with costs of 1€/MW between 0 and 20 MW and 2€/MW between 20 and 30: - create_pwl_cost(net, 0, "gen", [[0, 20, 1], [20, 30, 2]]) + create_pwl_costs(net, [0, 1], ["gen", "sgen"], [[[0, 20, 1], [20, 30, 2]], \ + [[0, 20, 1], [20, 30, 2]]]) """ if not hasattr(elements, "__iter__") and not isinstance(elements, str): raise ValueError(f"An iterable is expected for elements, not {elements}.") From 832d3b8c1b0a5a23a6fc3ad4ec526c0e5e9a07a0 Mon Sep 17 00:00:00 2001 From: Marie Fischer Date: Mon, 16 Sep 2024 15:42:33 +0200 Subject: [PATCH 016/135] set in_service concerning staswitch open/closed status --- pandapower/converter/powerfactory/pp_import_functions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index 80d9e73de..ede88919e 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -2911,11 +2911,11 @@ def create_sind(net, item): except IndexError: logger.error("Cannot add Sind '%s': not connected" % item.loc_name) return - + in_service = monopolar_in_service(item) sind = pp.create_series_reactor_as_impedance(net, from_bus=bus1, to_bus=bus2, r_ohm=item.rrea, x_ohm=item.xrea, sn_mva=item.Sn, name=item.loc_name, - in_service=not bool(item.outserv)) + in_service=in_service) logger.debug('created series reactor %s as per unit impedance at index %d' % (net.impedance.at[sind, 'name'], sind)) @@ -2934,10 +2934,11 @@ def create_scap(net, item): else: r_ohm = item.gcap/(item.gcap**2 + item.bcap**2) x_ohm = -item.bcap/(item.gcap**2 + item.bcap**2) + in_service = monopolar_in_service(item) scap = pp.create_series_reactor_as_impedance(net, from_bus=bus1, to_bus=bus2, r_ohm=r_ohm, x_ohm=x_ohm, sn_mva=item.Sn, name=item.loc_name, - in_service=not bool(item.outserv)) + in_service=in_service) logger.debug('created series capacitor %s as per unit impedance at index %d' % (net.impedance.at[scap, 'name'], scap)) From d6be6243de35799fffb2593c2b57adaa8e7b8aee Mon Sep 17 00:00:00 2001 From: Marie Fischer Date: Thu, 19 Sep 2024 10:03:20 +0200 Subject: [PATCH 017/135] consider staswitch of imepdances, serial reactors and serial capacitors if they exist # Conflicts: # pandapower/converter/powerfactory/pp_import_functions.py --- .../powerfactory/pp_import_functions.py | 178 ++++++++++++++++-- 1 file changed, 161 insertions(+), 17 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index ede88919e..9b12ff7e3 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -764,8 +764,14 @@ def create_line(net, item, flag_graphics, create_sections, is_unbalanced): net[line_table].loc[sid_list, "equipment"] = item.for_name if ac: - create_connection_switches(net, item, 2, 'l', (params['bus1'], params['bus2']), - (sid_list[0], sid_list[-1])) + new_elements = (sid_list[0], sid_list[-1]) + new_switch_idx, new_switch_closed = create_connection_switches(net, item, 2, 'l', (params['bus1'], params['bus2']), + new_elements) + # correct in_service of lines if station switch is open + # update_in_service_depending_station_switch(net, element_type="line", + # new_elements=new_elements, + # new_switch_idx=new_switch_idx, + # new_switch_closed=new_switch_closed) logger.debug('line <%s> created' % params['name']) @@ -1713,7 +1719,6 @@ def create_load(net, item, pf_variable_p_loads, dict_net, is_unbalanced): params["const_i_percent"] = i params["const_z_percent"] = z - ### for now - don't import ElmLodlvp elif load_class == 'ElmLodlvp': parent = item.fold_id @@ -2398,8 +2403,15 @@ def create_trafo(net, item, export_controller=True, tap_opt="nntap", is_unbalanc get_pf_trafo_results(net, item, tid, is_unbalanced) # adding switches - # False if open, True if closed, None if no switch - create_connection_switches(net, item, 2, 't', (bus1, bus2), (tid, tid)) + # False if open, True if closed, None if no switch + new_elements = (tid, tid) + new_switch_idx, new_switch_closed = create_connection_switches(net, item, 2, 't', (bus1, bus2), + new_elements) + # correct in_service of trafo if station switch is open + # update_in_service_depending_station_switch(net, element_type="trafo", + # new_elements=new_elements, + # new_switch_idx=new_switch_idx, + # new_switch_closed=new_switch_closed) # adding tap changer if (export_controller and pf_type.itapch and item.HasAttribute('ntrcn') and @@ -2571,7 +2583,15 @@ def create_trafo3w(net, item, tap_opt='nntap'): # adding switches # False if open, True if closed, None if no switch - create_connection_switches(net, item, 3, 't3', (bus1, bus2, bus3), (tid, tid, tid)) + new_elements = (tid, tid, tid) + new_switch_idx, new_switch_closed = create_connection_switches(net, item, 3, 't3', + (bus1, bus2, bus3), new_elements) + + # correct in_service of trafo3w if station switch is open + # update_in_service_depending_station_switch(net, element_type="trafo3w", + # new_elements=new_elements, + # new_switch_idx=new_switch_idx, + # new_switch_closed=new_switch_closed) logger.debug('successfully created trafo3w from parameters: %d' % tid) # testen @@ -2805,8 +2825,8 @@ def create_zpu(net, item): # net, from_bus, to_bus, r_pu, x_pu, sn_Mva, name=None, in_service=True, index=None params = { 'name': item.loc_name, - 'from_bus': bus1, - 'to_bus': bus2, + # 'from_bus': bus1, + # 'to_bus': bus2, 'rft_pu': item.r_pu, 'xft_pu': item.x_pu, 'rtf_pu': item.r_pu_ji, @@ -2828,9 +2848,49 @@ def create_zpu(net, item): } logger.debug('params = %s' % params) + + # create auxilary buses + aux_bus1 = pp.create_bus(net, vn_kv=net.bus.vn_kv.at[bus1], name=net.bus.name.at[bus1]+'_aux', + geodata=net.bus.geo.at[bus1], type="b", zone=net.bus.zone.at[bus1], + in_service=True) + params['from_bus'] = aux_bus1 + aux_bus2 = pp.create_bus(net, vn_kv=net.bus.vn_kv.at[bus2], name=net.bus.name.at[bus2]+'_aux', + geodata=net.bus.geo.at[bus2], type="b", zone=net.bus.zone.at[bus2], + in_service=True) + params['to_bus'] = aux_bus2 + xid = pp.create_impedance(net, **params) add_additional_attributes(item, net, element='impedance', element_id=xid, attr_list=["cpSite.loc_name"], attr_dict={"cimRdfId": "origin_id"}) + + # consider and create station switches + new_elements = (aux_bus1, aux_bus2) + new_switch_idx, new_switch_closed = create_connection_switches(net, item, 2, 'b', (bus1, bus2), + new_elements) + + if len(new_switch_idx)==0: + net.impedance.loc[xid, 'from_bus'] = bus1 + net.impedance.loc[xid, 'to_bus'] = bus2 + # drop auxilary buses, not needed + pp.drop_buses(net, buses=[aux_bus1, aux_bus2]) + elif len(new_switch_idx)==1: + sw_bus = net.switch.loc[new_switch_idx[0], 'bus'] + if sw_bus==bus1: + net.impedance.loc[xid, 'to_bus'] = bus2 + # drop one auxilary bus, where no switch exists, not needed + pp.drop_buses(net, buses=[aux_bus2]) + elif sw_bus==bus2: + net.impedance.loc[xid, 'from_bus'] = bus1 + # drop one auxilary bus, where no switch exists, not needed + pp.drop_buses(net, buses=[aux_bus1]) + + # correct in_service of series reactor if station switch is open + # update_in_service_depending_station_switch(net, element_type="impedance", + # new_elements=new_elements, + # new_switch_idx=new_switch_idx, + # new_switch_closed=new_switch_closed) + + logger.debug('created ZPU %s as impedance at index %d' % (net.impedance.at[xid, 'name'], xid)) def create_vac(net, item): @@ -2902,6 +2962,20 @@ def create_vac(net, item): logger.debug('added pf_p and pf_q to {} {}: {}'.format(elm, xid, net['res_' + elm].loc[ xid, ["pf_p", 'pf_q']].values)) +def update_in_service_depending_station_switch(net, element_type, new_elements, new_switch_idx, new_switch_closed): + ### fcn is not used! + if len(new_switch_idx)!= 0: + for i in range(len(new_switch_idx)): + if new_switch_closed[i] == 0: + if net[element_type].loc[new_elements[i], 'in_service']==False: + continue + else: + net[element_type].loc[new_elements[i], 'in_service'] = False + logger.debug('element of element_type %s with index %d is set\ + out of service because station switch is open ' % + (net[element_type].at[new_elements[i], 'name'], new_elements[i])) + else: + pass def create_sind(net, item): # series reactor is modelled as per-unit impedance, values in Ohm are calculated into values in @@ -2911,12 +2985,47 @@ def create_sind(net, item): except IndexError: logger.error("Cannot add Sind '%s': not connected" % item.loc_name) return - in_service = monopolar_in_service(item) - sind = pp.create_series_reactor_as_impedance(net, from_bus=bus1, to_bus=bus2, r_ohm=item.rrea, - x_ohm=item.xrea, sn_mva=item.Sn, + + # create auxilary buses + aux_bus1 = pp.create_bus(net, vn_kv=net.bus.vn_kv.at[bus1], name=net.bus.name.at[bus1]+'_aux', + geodata=net.bus.geo.at[bus1], type="b", zone=net.bus.zone.at[bus1], + in_service=True) + aux_bus2 = pp.create_bus(net, vn_kv=net.bus.vn_kv.at[bus2], name=net.bus.name.at[bus2]+'_aux', + geodata=net.bus.geo.at[bus2], type="b", zone=net.bus.zone.at[bus2], + in_service=True) + + sind = pp.create_series_reactor_as_impedance(net, from_bus=aux_bus1, to_bus=aux_bus2, + r_ohm=item.rrea, x_ohm=item.xrea, sn_mva=item.Sn, name=item.loc_name, - in_service=in_service) - + in_service=not bool(item.outserv)) + + # consider and create station switches + new_elements = (aux_bus1, aux_bus2) + new_switch_idx, new_switch_closed = create_connection_switches(net, item, 2, 'b', (bus1, bus2), + new_elements) + + if len(new_switch_idx)==0: + net.impedance.loc[sind, 'from_bus'] = bus1 + net.impedance.loc[sind, 'to_bus'] = bus2 + # drop auxilary buses, not needed + pp.drop_buses(net, buses=[aux_bus1, aux_bus2]) + elif len(new_switch_idx)==1: + sw_bus = net.switch.loc[new_switch_idx[0], 'bus'] + if sw_bus==bus1: + net.impedance.loc[sind, 'to_bus'] = bus2 + # drop one auxilary bus, where no switch exists, not needed + pp.drop_buses(net, buses=[aux_bus2]) + elif sw_bus==bus2: + net.impedance.loc[sind, 'from_bus'] = bus1 + # drop one auxilary bus, where no switch exists, not needed + pp.drop_buses(net, buses=[aux_bus1]) + + # correct in_service of series reactor if station switch is open + # update_in_service_depending_station_switch(net, element_type="impedance", + # new_elements=new_elements, + # new_switch_idx=new_switch_idx, + # new_switch_closed=new_switch_closed) + logger.debug('created series reactor %s as per unit impedance at index %d' % (net.impedance.at[sind, 'name'], sind)) @@ -2924,7 +3033,7 @@ def create_scap(net, item): # series capacitor is modelled as per-unit impedance, values in Ohm are calculated into values in # per unit at creation try: - (bus1, bus2) = get_connection_nodes(net, item, 2) + (bus1, bus2), _ = get_connection_nodes(net, item, 2) except IndexError: logger.error("Cannot add Scap '%s': not connected" % item.loc_name) return @@ -2934,11 +3043,46 @@ def create_scap(net, item): else: r_ohm = item.gcap/(item.gcap**2 + item.bcap**2) x_ohm = -item.bcap/(item.gcap**2 + item.bcap**2) - in_service = monopolar_in_service(item) - scap = pp.create_series_reactor_as_impedance(net, from_bus=bus1, to_bus=bus2, r_ohm=r_ohm, + + # create auxilary buses + aux_bus1 = pp.create_bus(net, vn_kv=net.bus.vn_kv.at[bus1], name=net.bus.name.at[bus1]+'_aux', + geodata=net.bus.geo.at[bus1], type="b", zone=net.bus.zone.at[bus1], + in_service=True) + aux_bus2 = pp.create_bus(net, vn_kv=net.bus.vn_kv.at[bus2], name=net.bus.name.at[bus2]+'_aux', + geodata=net.bus.geo.at[bus2], type="b", zone=net.bus.zone.at[bus2], + in_service=True) + + scap = pp.create_series_reactor_as_impedance(net, from_bus=aux_bus1, to_bus=aux_bus2, r_ohm=r_ohm, x_ohm=x_ohm, sn_mva=item.Sn, name=item.loc_name, - in_service=in_service) + in_service=not bool(item.outserv)) + + # consider and create station switches + new_elements = (aux_bus1, aux_bus2) + new_switch_idx, new_switch_closed = create_connection_switches(net, item, 2, 'b', (bus1, bus2), + new_elements) + + if len(new_switch_idx)==0: + net.impedance.loc[scap, 'from_bus'] = bus1 + net.impedance.loc[scap, 'to_bus'] = bus2 + # drop auxilary buses, not needed + pp.drop_buses(net, buses=[aux_bus1, aux_bus2]) + elif len(new_switch_idx)==1: + sw_bus = net.switch.loc[new_switch_idx[0], 'bus'] + if sw_bus==bus1: + net.impedance.loc[scap, 'to_bus'] = bus2 + # drop one auxilary bus, where no switch exists, not needed + pp.drop_buses(net, buses=[aux_bus2]) + elif sw_bus==bus2: + net.impedance.loc[scap, 'from_bus'] = bus1 + # drop one auxilary bus, where no switch exists, not needed + pp.drop_buses(net, buses=[aux_bus1]) + + # correct in_service of series capacitor if station switch is open + # update_in_service_depending_station_switch(net, element_type="impedance", + # new_elements=new_elements, + # new_switch_idx=new_switch_idx, + # new_switch_closed=new_switch_closed) logger.debug('created series capacitor %s as per unit impedance at index %d' % (net.impedance.at[scap, 'name'], scap)) From 3f499a7281ed15ca359d6f2c8a80151a4f2b9552 Mon Sep 17 00:00:00 2001 From: Marie Fischer Date: Thu, 19 Sep 2024 10:34:39 +0200 Subject: [PATCH 018/135] fix connection switch fcn output --- pandapower/converter/powerfactory/pp_import_functions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index 9b12ff7e3..5f04d4657 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -628,6 +628,8 @@ def import_switch(item, idx_cubicle): def create_connection_switches(net, item, number_switches, et, buses, elements): # False if open, True if closed, None if no switch logger.debug('creating connection switches') + new_switch_idx = [] + new_switch_closed = [] for i in range(number_switches): switch_is_closed, switch_usage, switch_name = import_switch(item, i) logger.debug('switch closed: %s, switch_usage: %s' % (switch_is_closed, switch_usage)) @@ -635,6 +637,9 @@ def create_connection_switches(net, item, number_switches, et, buses, elements): cd = pp.create_switch(net, bus=buses[i], element=elements[i], et=et, closed=switch_is_closed, type=switch_usage, name=switch_name) net.res_switch.loc[cd, ['pf_closed', 'pf_in_service']] = switch_is_closed, True + new_switch_idx.append(cd) + new_switch_closed.append(switch_is_closed) + return new_switch_idx, new_switch_closed def get_coords_from_buses(net, from_bus, to_bus, **kwargs): From db8810640b643e491d89d2697839f7822691ce6a Mon Sep 17 00:00:00 2001 From: Marie Fischer Date: Wed, 25 Sep 2024 10:40:31 +0200 Subject: [PATCH 019/135] small fix in series capacitors --- pandapower/converter/powerfactory/pp_import_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index e04ce3e87..ac74847a5 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -3043,7 +3043,7 @@ def create_scap(net, item): logger.error("Cannot add Scap '%s': not connected" % item.loc_name) return - if (item.gcap==0) or (item.bcap==0): + if (item.gcap==0) and (item.bcap==0): logger.info('not creating series capacitor for %s' % item.loc_name) else: r_ohm = item.gcap/(item.gcap**2 + item.bcap**2) From b0d9e9576dc35ee7947a6ef63c0b91c8e1d818ee Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Tue, 1 Oct 2024 17:11:54 +0200 Subject: [PATCH 020/135] fix trafo3w tap at star point, tap re-calculation now considers tap_neutral != 0 --- pandapower/build_branch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandapower/build_branch.py b/pandapower/build_branch.py index f48cb2371..8d5930bc9 100644 --- a/pandapower/build_branch.py +++ b/pandapower/build_branch.py @@ -1304,7 +1304,8 @@ def _calculate_3w_tap_changers(t3, t2, sides): if any_at_star_point & np.any(mask_star_point := (tap_mask & at_star_point)): t = tap_arrays["tap_step_percent"][side][mask_star_point] * np.exp(1j * np.deg2rad(tap_arrays["tap_step_degree"][side][mask_star_point])) tap_pos = tap_arrays["tap_pos"][side][mask_star_point] - t_corrected = 100 * t / (100 + (t * tap_pos)) + tap_neutral = tap_arrays["tap_neutral"][side][mask_star_point] + t_corrected = 100 * t / (100 + (t * (tap_pos-tap_neutral))) tap_arrays["tap_step_percent"][side][mask_star_point] = np.abs(t_corrected) tap_arrays["tap_side"][side][mask_star_point] = "lv" if side == "hv" else "hv" tap_arrays["tap_step_degree"][side][mask_star_point] = np.rad2deg(np.angle(t_corrected)) From 116489d0929d1db277061698b2d78ca8a99339bb Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Tue, 1 Oct 2024 17:35:36 +0200 Subject: [PATCH 021/135] update test_contingency for new ls2g version --- pandapower/test/contingency/test_contingency.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandapower/test/contingency/test_contingency.py b/pandapower/test/contingency/test_contingency.py index a2a07fe2e..30decf79f 100644 --- a/pandapower/test/contingency/test_contingency.py +++ b/pandapower/test/contingency/test_contingency.py @@ -132,7 +132,7 @@ def test_with_lightsim2grid(get_net, get_case): net.res_trafo[f"{s}_loading_percent"].values, atol=1e-6, rtol=0), s -@pytest.mark.xfail(reason="remove this xfail when new version of lightsim2grid available") +# @pytest.mark.xfail(reason="remove this xfail when new version of lightsim2grid available") @pytest.mark.skipif(not lightsim2grid_installed, reason="lightsim2grid package is not installed") def test_case118(): net = pp.networks.case118() @@ -404,10 +404,10 @@ def _randomize_indices(net): pp.reindex_elements(net, element, new_index) -def test_reminder_bring_back_case118(): - from packaging.version import Version - if lightsim2grid_installed and Version(lightsim2grid.__version__) > Version("0.9.0"): - raise UserWarning("bring back case 118 and remove xfail for test_unequal_trafo_impedances and test_case118") +# def test_reminder_bring_back_case118(): +# from packaging.version import Version +# if lightsim2grid_installed and Version(lightsim2grid.__version__) > Version("0.9.0"): +# raise UserWarning("bring back case 118 and remove xfail for test_unequal_trafo_impedances and test_case118") # todo: bring back case 118 when lightsim2grid new version is released From 8b19466f2880d47b8570b1e57eff740858579db3 Mon Sep 17 00:00:00 2001 From: marcopau Date: Mon, 7 Oct 2024 11:35:03 +0200 Subject: [PATCH 022/135] Fix pwr injection results for merged buses in SE --- pandapower/build_bus.py | 111 +++++++++++++++++++++++-------- pandapower/estimation/results.py | 6 ++ 2 files changed, 89 insertions(+), 28 deletions(-) diff --git a/pandapower/build_bus.py b/pandapower/build_bus.py index c7a3703e3..7057091be 100644 --- a/pandapower/build_bus.py +++ b/pandapower/build_bus.py @@ -42,27 +42,29 @@ def ds_find(ar, bus): # pragma: no cover @jit(nopython=True, cache=False) -def ds_union(ar, bus1, bus2, bus_is_pv): # pragma: no cover +def ds_union(ar, bus1, bus2, bus_is_pv, bus_is_active, merged_bus): # pragma: no cover root1 = ds_find(ar, bus1) root2 = ds_find(ar, bus2) if root1 == root2: return - if bus_is_pv[root2]: + if bus_is_active[root2] and ~bus_is_pv[root1]: # if root2 is an active bus (load or gen) and root1 is not a PV bus, it will merge root1 into root2 ar[root1] = root2 + merged_bus[root1] = True else: ar[root2] = root1 + merged_bus[root2] = True @jit(nopython=True, cache=False) def ds_create(ar, switch_bus, switch_elm, switch_et_bus, switch_closed, switch_z_ohm, - bus_is_pv, bus_in_service): # pragma: no cover + bus_is_pv, bus_is_active, bus_in_service, merged_bus): # pragma: no cover for i in range(len(switch_bus)): if not switch_closed[i] or not switch_et_bus[i] or switch_z_ohm[i] > 0: continue bus1 = switch_bus[i] bus2 = switch_elm[i] if bus_in_service[bus1] and bus_in_service[bus2]: - ds_union(ar, bus1, bus2, bus_is_pv) + ds_union(ar, bus1, bus2, bus_is_pv, bus_is_active, merged_bus) @jit(nopython=True, cache=False) @@ -74,7 +76,7 @@ def fill_bus_lookup(ar, bus_lookup, bus_index): bus_lookup[b] = bus_lookup[ar[ds]] -def create_bus_lookup_numba(net, bus_index, bus_is_idx, gen_is_mask, eg_is_mask): +def create_bus_lookup_numba(net, bus_index, bus_is_idx): max_bus_idx = np.max(bus_index) # extract numpy arrays of switch table data switch = net["switch"] @@ -86,18 +88,39 @@ def create_bus_lookup_numba(net, bus_index, bus_is_idx, gen_is_mask, eg_is_mask) # create array for fast checking if a bus is in_service bus_in_service = np.zeros(max_bus_idx + 1, dtype=bool) bus_in_service[bus_is_idx] = True + # create mask arrays to distinguish different grid elements + _is_elements = net["_is_elements"] + eg_is_mask = _is_elements['ext_grid'] + gen_is_mask = _is_elements['gen'] + load_is_mask = _is_elements['load'] + motor_is_mask = _is_elements['motor'] + sgen_is_mask = _is_elements['sgen'] + shunt_is_mask = _is_elements['shunt'] + ward_is_mask = _is_elements['ward'] + xward_is_mask = _is_elements['xward'] # create array for fast checking if a bus is pv bus bus_is_pv = np.zeros(max_bus_idx + 1, dtype=bool) bus_is_pv[net["ext_grid"]["bus"].values[eg_is_mask]] = True bus_is_pv[net["gen"]["bus"].values[gen_is_mask]] = True + # create array for checking if a bus is active (i.e., it has some element connected to it) + bus_is_active = np.zeros(max_bus_idx + 1, dtype=bool) + bus_is_active[net["ext_grid"]["bus"].values[eg_is_mask]] = True + bus_is_active[net["gen"]["bus"].values[gen_is_mask]] = True + bus_is_active[net["load"]["bus"].values[load_is_mask]] = True + bus_is_active[net["motor"]["bus"].values[motor_is_mask]] = True + bus_is_active[net["sgen"]["bus"].values[sgen_is_mask]] = True + bus_is_active[net["shunt"]["bus"].values[shunt_is_mask]] = True + bus_is_active[net["ward"]["bus"].values[ward_is_mask]] = True + bus_is_active[net["xward"]["bus"].values[xward_is_mask]] = True # create array that represents the disjoint set ar = np.arange(max_bus_idx + 1) + merged_bus = np.zeros(len(ar), dtype=bool) ds_create(ar, switch_bus, switch_elm, switch_et_bus, switch_closed, switch_z_ohm, bus_is_pv, - bus_in_service) + bus_is_active, bus_in_service, merged_bus) # finally create and fill bus lookup bus_lookup = -np.ones(max_bus_idx + 1, dtype=np.int64) fill_bus_lookup(ar, bus_lookup, bus_index) - return bus_lookup + return bus_lookup, merged_bus class DisjointSet(dict): @@ -130,10 +153,10 @@ def create_consecutive_bus_lookup(net, bus_index): return bus_lookup -def create_bus_lookup_numpy(net, bus_index, bus_is_idx, gen_is_mask, eg_is_mask, - closed_bb_switch_mask): +def create_bus_lookup_numpy(net, bus_index, closed_bb_switch_mask): bus_lookup = create_consecutive_bus_lookup(net, bus_index) net._fused_bb_switches = closed_bb_switch_mask & (net["switch"]["z_ohm"].values <= 0) + merged_bus = np.zeros(len(bus_lookup), dtype=bool) if net._fused_bb_switches.any(): # Note: this might seem a little odd - first constructing a pp to ppc mapping without # fused busses and then update the entries. The alternative (to construct the final @@ -141,9 +164,24 @@ def create_bus_lookup_numpy(net, bus_index, bus_is_idx, gen_is_mask, eg_is_mask, # are not part of any opened bus-bus switches first. It turns out, that the latter takes # quite some time in the average usecase, where #busses >> #bus-bus switches. + # create mask arrays to distinguish different grid elements + _is_elements = net["_is_elements"] + eg_is_mask = _is_elements['ext_grid'] + gen_is_mask = _is_elements['gen'] + load_is_mask = _is_elements['load'] + motor_is_mask = _is_elements['motor'] + sgen_is_mask = _is_elements['sgen'] + shunt_is_mask = _is_elements['shunt'] + ward_is_mask = _is_elements['ward'] + xward_is_mask = _is_elements['xward'] # Find PV / Slack nodes -> their bus must be kept when fused with a PQ node pv_list = [net["ext_grid"]["bus"].values[eg_is_mask], net["gen"]["bus"].values[gen_is_mask]] pv_ref = np.unique(np.hstack(pv_list)) + # Find active nodes -> their bus must be possibly kept when fused with a zero injection node + active_bus = [net["load"]["bus"].values[load_is_mask], net["motor"]["bus"].values[motor_is_mask], \ + net["sgen"]["bus"].values[sgen_is_mask], net["shunt"]["bus"].values[shunt_is_mask], \ + net["ward"]["bus"].values[ward_is_mask], net["xward"]["bus"].values[xward_is_mask]] + active_ref = np.unique(np.hstack(active_bus)) # get the pp-indices of the buses which are connected to a switch to be fused fbus = net["switch"]["bus"].values[net._fused_bb_switches] tbus = net["switch"]["element"].values[net._fused_bb_switches] @@ -170,30 +208,48 @@ def create_bus_lookup_numpy(net, bus_index, bus_is_idx, gen_is_mask, eg_is_mask, if any(i in fbus or i in tbus for i in pv_ref): # for every disjoint set for dj in disjoint_sets: - # check if pv buses are in the disjoint set dj + # check if pv and active buses are in the disjoint set dj pv_buses_in_set = set(pv_ref) & dj nr_pv_bus = len(pv_buses_in_set) - if nr_pv_bus == 0: - # no pv buses. Use any bus in dj - map_to = bus_lookup[dj.pop()] - else: + active_buses_in_set = set(active_ref) & dj + nr_active_bus = len(active_buses_in_set) + if nr_pv_bus > 0: # one pv bus. Get bus from pv_buses_in_set - map_to = bus_lookup[pv_buses_in_set.pop()] + ref_bus = pv_buses_in_set.pop() + else: + if nr_active_bus > 0: + # no pv bus but another active bus. Get bus from active_buses_in_set + ref_bus = active_buses_in_set.pop() + else: + # neither pv buses nor active buses. Use any bus in dj + ref_bus = dj.pop() + map_to = bus_lookup[ref_bus] for bus in dj: # update lookup bus_lookup[bus] = map_to + if bus != ref_bus: + merged_bus[bus] = 1 else: # no PV buses in set for dj in disjoint_sets: - # use any bus in set - map_to = bus_lookup[dj.pop()] + active_buses_in_set = set(active_ref) & dj + nr_active_bus = len(active_buses_in_set) + if nr_active_bus > 0: + # no ov bus but another active bus. Get bus from active_buses_in_set + ref_bus = active_buses_in_set.pop() + else: + # neither pv buses nor active busese. Use any bus in dj + ref_bus = dj.pop() + map_to = bus_lookup[ref_bus] for bus in dj: # update bus lookup bus_lookup[bus] = map_to - return bus_lookup + if bus != ref_bus: + merged_bus[bus] = 1 + return bus_lookup, merged_bus -def create_bus_lookup(net, bus_index, bus_is_idx, gen_is_mask, eg_is_mask, numba): +def create_bus_lookup(net, bus_index, bus_is_idx, numba): switches_with_pos_z_ohm = net["switch"]["z_ohm"].values > 0 if switches_with_pos_z_ohm.any() or not numba: # if there are any closed bus-bus switches find them @@ -208,12 +264,10 @@ def create_bus_lookup(net, bus_index, bus_is_idx, gen_is_mask, eg_is_mask, numba net._impedance_bb_switches = np.zeros(switches_with_pos_z_ohm.shape) if numba: - bus_lookup = create_bus_lookup_numba(net, bus_index, bus_is_idx, - gen_is_mask, eg_is_mask) + bus_lookup, merged_bus = create_bus_lookup_numba(net, bus_index, bus_is_idx) else: - bus_lookup = create_bus_lookup_numpy(net, bus_index, bus_is_idx, - gen_is_mask, eg_is_mask, closed_bb_switch_mask) - return bus_lookup + bus_lookup, merged_bus = create_bus_lookup_numpy(net, bus_index, closed_bb_switch_mask) + return bus_lookup, merged_bus def get_voltage_init_vector(net, init_v, mode, sequence=None): @@ -301,11 +355,10 @@ def _build_bus_ppc(net, ppc, sequence=None): bus_lookup = create_consecutive_bus_lookup(net, bus_index) else: _is_elements = net["_is_elements"] - eg_is_mask = _is_elements['ext_grid'] - gen_is_mask = _is_elements['gen'] + # eg_is_mask = _is_elements['ext_grid'] + # gen_is_mask = _is_elements['gen'] bus_is_idx = _is_elements['bus_is_idx'] - bus_lookup = create_bus_lookup(net, bus_index, bus_is_idx, - gen_is_mask, eg_is_mask, numba=numba) + bus_lookup, merged_bus = create_bus_lookup(net, bus_index, bus_is_idx, numba=numba) n_bus_ppc = len(bus_index) # init ppc with empty values @@ -374,6 +427,8 @@ def _build_bus_ppc(net, ppc, sequence=None): net["_pd2ppc_lookups"]["bus"] = bus_lookup net["_pd2ppc_lookups"]["aux"] = aux + if mode != "nx": + net["_pd2ppc_lookups"]["merged_bus"] = merged_bus def _build_bus_dc_ppc(net, ppc): diff --git a/pandapower/estimation/results.py b/pandapower/estimation/results.py index f6449072c..e182a0947 100644 --- a/pandapower/estimation/results.py +++ b/pandapower/estimation/results.py @@ -44,6 +44,12 @@ def _extract_result_ppci_to_pp(net, ppc, ppci): mapping_table) net.res_bus_est.q_mvar = get_values(ppc["bus"][:, 3], net.bus.index.values, mapping_table) + # overwrite power values for buses that were merged because they would not have the same power inj + # as the bus they were merged to + merged_bus = net["_pd2ppc_lookups"]["merged_bus"] + merged_bus_idx = np.where(merged_bus == True)[0] + net.res_bus_est.loc[merged_bus_idx, 'p_mw'] = 0 + net.res_bus_est.loc[merged_bus_idx, "q_mvar"] = 0 return net From 0bbd43d8e4b16364f433bdf360b1159d30d6e670 Mon Sep 17 00:00:00 2001 From: marcopau Date: Mon, 7 Oct 2024 11:49:22 +0200 Subject: [PATCH 023/135] Added shunt results in SE --- pandapower/estimation/results.py | 13 +++++++++++++ pandapower/results.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pandapower/estimation/results.py b/pandapower/estimation/results.py index e182a0947..d48bcc199 100644 --- a/pandapower/estimation/results.py +++ b/pandapower/estimation/results.py @@ -50,6 +50,19 @@ def _extract_result_ppci_to_pp(net, ppc, ppci): merged_bus_idx = np.where(merged_bus == True)[0] net.res_bus_est.loc[merged_bus_idx, 'p_mw'] = 0 net.res_bus_est.loc[merged_bus_idx, "q_mvar"] = 0 + # add shunt power because the injection at the node computed via Ybus is only the extra injection on top of the shunt + if ~net["shunt"].empty: + for i in range(net["shunt"].shape[0]): + bus = net.shunt.bus.iloc[i] + Sn = complex(net.shunt.p_mw.iloc[i],net.shunt.q_mvar.iloc[i])*net.shunt.step.iloc[i] + Ysh = Sn / (net.shunt.vn_kv.iloc[i]**2) + V = net["res_bus_est"].loc[bus,"vm_pu"]*net["bus"].loc[bus,"vn_kv"] + Sinj = Ysh*(V**2) + net["res_bus_est"].loc[bus,"p_mw"] += Sinj.real + net["res_bus_est"].loc[bus,"q_mvar"] += Sinj.imag + net["res_shunt_est"].loc[net["shunt"].loc[:,"bus"]==bus,"p_mw"] = Sinj.real + net["res_shunt_est"].loc[net["shunt"].loc[:,"bus"]==bus,"q_mvar"] = Sinj.imag + net["res_shunt_est"].loc[net["shunt"].loc[:,"bus"]==bus,"vm_pu"] = net["res_bus_est"].loc[bus,"vm_pu"] return net diff --git a/pandapower/results.py b/pandapower/results.py index 121e04966..4c84da0cc 100644 --- a/pandapower/results.py +++ b/pandapower/results.py @@ -153,7 +153,7 @@ def get_relevant_elements(mode="pf"): elif mode == "sc": return ["bus", "line", "trafo", "trafo3w", "ext_grid", "gen", "sgen", "switch"] elif mode == "se": - return ["bus", "line", "trafo", "trafo3w", "impedance", "switch"] + return ["bus", "line", "trafo", "trafo3w", "impedance", "switch", "shunt"] elif mode == "pf_3ph": return ["bus", "line", "trafo", "ext_grid", "shunt", "load", "sgen", "storage", "asymmetric_load", "asymmetric_sgen"] From 26ac3c77edc4898111d4e73d29f5fbe1eae056dd Mon Sep 17 00:00:00 2001 From: marcopau Date: Mon, 7 Oct 2024 12:02:29 +0200 Subject: [PATCH 024/135] Fix initialization bugs in SE --- pandapower/auxiliary.py | 2 +- pandapower/build_gen.py | 9 ++++----- pandapower/estimation/ppc_conversion.py | 1 + pandapower/pd2ppc.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pandapower/auxiliary.py b/pandapower/auxiliary.py index b421cd5f7..0bf36d884 100644 --- a/pandapower/auxiliary.py +++ b/pandapower/auxiliary.py @@ -1746,7 +1746,7 @@ def _init_runse_options(net, v_start, delta_start, calculate_voltage_angles, net._options = {} _add_ppc_options(net, calculate_voltage_angles=calculate_voltage_angles, trafo_model=trafo_model, check_connectivity=check_connectivity, - mode="pf", switch_rx_ratio=switch_rx_ratio, init_vm_pu=v_start, + mode="se", switch_rx_ratio=switch_rx_ratio, init_vm_pu=v_start, init_va_degree=delta_start, enforce_q_lims=False, recycle=None, voltage_depend_loads=False, trafo3w_losses=trafo3w_losses) _add_pf_options(net, tolerance_mva="1e-8", trafo_loading="power", diff --git a/pandapower/build_gen.py b/pandapower/build_gen.py index 1dec5144b..780588d5b 100644 --- a/pandapower/build_gen.py +++ b/pandapower/build_gen.py @@ -39,9 +39,6 @@ def _build_gen_ppc(net, ppc): mode = net["_options"]["mode"] distributed_slack = net["_options"]["distributed_slack"] - if mode == "estimate": - return - _is_elements = net["_is_elements"] gen_order = dict() f = 0 @@ -211,6 +208,7 @@ def _build_pp_gen(net, ppc, f, t): delta = net["_options"]["delta"] gen_is = net._is_elements["gen"] bus_lookup = net["_pd2ppc_lookups"]["bus"] + mode = net["_options"]["mode"] gen_buses = bus_lookup[net["gen"]["bus"].values[gen_is]] gen_is_vm = net["gen"]["vm_pu"].values[gen_is] @@ -222,11 +220,12 @@ def _build_pp_gen(net, ppc, f, t): # set bus values for generator buses ppc["bus"][gen_buses[ppc["bus"][gen_buses, BUS_TYPE] != REF], BUS_TYPE] = PV - ppc["bus"][gen_buses, VM] = gen_is_vm + if mode != "se": + ppc["bus"][gen_buses, VM] = gen_is_vm add_q_constraints(net, "gen", gen_is, ppc, f, t, delta) add_p_constraints(net, "gen", gen_is, ppc, f, t, delta) - if net._options["mode"] == "opf": + if mode == "opf": # this considers the vm limits for gens ppc = _check_gen_vm_limits(net, ppc, gen_buses, gen_is) if "controllable" in net.gen.columns: diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index 6aa94785d..ceb462e50 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -288,6 +288,7 @@ def _add_zero_injection(net, ppci, bus_append, zero_injection): if net._pd2ppc_lookups['aux']: aux_bus_lookup = np.concatenate([v for k, v in net._pd2ppc_lookups['aux'].items() if k != 'xward']) aux_bus = net._pd2ppc_lookups['bus'][aux_bus_lookup] + aux_bus = aux_bus[aux_bus < ppci["bus"].shape[0]] bus_append[aux_bus, ZERO_INJ_FLAG] = True if isinstance(zero_injection, str): diff --git a/pandapower/pd2ppc.py b/pandapower/pd2ppc.py index c72577a99..dc0045944 100644 --- a/pandapower/pd2ppc.py +++ b/pandapower/pd2ppc.py @@ -202,7 +202,7 @@ def _pd2ppc(net, sequence=None, **kwargs): aux._set_isolated_buses_out_of_service(net, ppc) # we need to check this after checking connectivity (isolated vsc as DC slack cause change of DC_REF to DC_P) - if "pf" in mode: + if "pf" in mode or "se" in mode: _check_for_reference_bus(ppc) _build_gen_ppc(net, ppc) From abc46191070f6145235c7d86ec4198574b9100ab Mon Sep 17 00:00:00 2001 From: marcopau Date: Mon, 7 Oct 2024 12:25:50 +0200 Subject: [PATCH 025/135] Enhancements and logs to WLS estimator --- pandapower/estimation/algorithm/base.py | 14 ++++++++++++-- pandapower/estimation/state_estimation.py | 23 +++++++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index 7824a0ccf..c5efe2db9 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -75,6 +75,8 @@ def __init__(self, tolerance, maximum_iterations, logger=std_logger): self.r = None self.H = None self.hx = None + self.iterations = None + self.obj_func = None def estimate(self, eppci: ExtendedPPCI, **kwargs): self.initialize(eppci) @@ -83,6 +85,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): current_error, cur_it = 100., 0 # invert covariance matrix + eppci.r_cov[eppci.r_cov<(10**(-6))] = 10**(-6) r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) E = eppci.E while current_error > self.tolerance and cur_it < self.max_iterations: @@ -106,10 +109,15 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): E += d_E.ravel() eppci.update_E(E) + # log data + current_error = np.max(np.abs(d_E)) + obj_func = (r.T*r_inv*r)[0,0] + self.logger.debug("Current delta_x: {:.7f}".format(current_error)) + self.logger.debug("Current objective function value: {:.1f}".format(obj_func)) + # prepare next iteration cur_it += 1 - current_error = np.max(np.abs(d_E)) - self.logger.debug("Current error: {:.7f}".format(current_error)) + except np.linalg.linalg.LinAlgError: self.logger.error("A problem appeared while using the linear algebra methods." "Check and change the measurement set.") @@ -117,6 +125,8 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # check if the estimation is successfull self.check_result(current_error, cur_it) + self.iterations = cur_it + self.obj_func = obj_func if self.successful: # store variables required for chi^2 and r_N_max test: self.R_inv = r_inv.toarray() diff --git a/pandapower/estimation/state_estimation.py b/pandapower/estimation/state_estimation.py index d5a7d6fce..28c3cd1f0 100644 --- a/pandapower/estimation/state_estimation.py +++ b/pandapower/estimation/state_estimation.py @@ -3,6 +3,7 @@ # Copyright (c) 2016-2023 by University of Kassel and Fraunhofer Institute for Energy Economics # and Energy System Technology (IEE), Kassel. All rights reserved. +from datetime import datetime import numpy as np from scipy.stats import chi2 @@ -87,7 +88,8 @@ def estimate(net, algorithm='wls', return se.estimate(v_start=v_start, delta_start=delta_start, calculate_voltage_angles=calculate_voltage_angles, zero_injection=zero_injection, - fuse_buses_with_bb_switch=fuse_buses_with_bb_switch, **opt_vars) + fuse_buses_with_bb_switch=fuse_buses_with_bb_switch, + algorithm=algorithm, **opt_vars) def remove_bad_data(net, init='flat', tolerance=1e-6, maximum_iterations=10, @@ -176,13 +178,14 @@ def __init__(self, net, tolerance=1e-6, maximum_iterations=10, algorithm='wls', self.ppc = None self.eppci = None self.recycle = recycle + self.algorithm = algorithm # variables for chi^2 / rn_max tests self.delta = None self.bad_data_present = None def estimate(self, v_start='flat', delta_start='flat', calculate_voltage_angles=True, - zero_injection=None, fuse_buses_with_bb_switch='all', **opt_vars): + zero_injection=None, fuse_buses_with_bb_switch='all', algorithm='wls', **opt_vars): """ The function estimate is the main function of the module. It takes up to three input arguments: v_start, delta_start and calculate_voltage_angles. The first two are the initial @@ -261,7 +264,8 @@ def estimate(self, v_start='flat', delta_start='flat', calculate_voltage_angles= self.net, self.ppc, self.eppci = pp2eppci(self.net, v_start=v_start, delta_start=delta_start, calculate_voltage_angles=calculate_voltage_angles, - zero_injection=zero_injection, ppc=self.ppc, eppci=self.eppci) + zero_injection=zero_injection, algorithm=algorithm, + ppc=self.ppc, eppci=self.eppci) # Estimate voltage magnitude and angle with the given estimator self.eppci = self.solver.estimate(self.eppci, **opt_vars) @@ -278,7 +282,18 @@ def estimate(self, v_start='flat', delta_start='flat', calculate_voltage_angles= # if recycle is not wished, reset ppc, ppci if not self.recycle: self.ppc, self.eppci = None, None - return self.solver.successful + + if algorithm == "wls": + now = datetime.now() + se_results = { + "success": self.solver.successful, + "num_iterations": self.solver.iterations, + "objective_function_value": self.solver.obj_func, + "time": now.strftime("%Y-%m-%d %H:%M:%S")} + else: + se_results = self.solver.successful + + return se_results def perform_chi2_test(self, v_in_out=None, delta_in_out=None, calculate_voltage_angles=True, chi2_prob_false=0.05): From e4b6fbf87bd170a75c25995189c2ac2346525650 Mon Sep 17 00:00:00 2001 From: marcopau Date: Mon, 7 Oct 2024 13:18:00 +0200 Subject: [PATCH 026/135] FIx bugs with current mag meas in SE --- pandapower/estimation/algorithm/base.py | 13 ++++++++ .../estimation/algorithm/matrix_base.py | 25 ++++++++------- pandapower/estimation/ppc_conversion.py | 27 ++++++++++++---- pandapower/pypower/dIbr_dV.py | 32 ++++++++++++++++++- 4 files changed, 79 insertions(+), 18 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index c5efe2db9..cd7f00076 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -77,6 +77,7 @@ def __init__(self, tolerance, maximum_iterations, logger=std_logger): self.hx = None self.iterations = None self.obj_func = None + logging.basicConfig(level=logging.DEBUG) def estimate(self, eppci: ExtendedPPCI, **kwargs): self.initialize(eppci) @@ -97,6 +98,14 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # jacobian matrix H H = csr_matrix(sem.create_hx_jacobian(E)) + # remove current magnitude measurements at the first iteration + # because with flat start they have null derivative + if cur_it == 0 and eppci.any_i_meas: + idx = eppci.idx_non_imeas + r_inv = r_inv[idx,:][:,idx] + r = r[idx,:] + H = H[idx,:] + # gain matrix G_m # G_m = H^t * R^-1 * H G_m = H.T * (r_inv * H) @@ -115,6 +124,10 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): self.logger.debug("Current delta_x: {:.7f}".format(current_error)) self.logger.debug("Current objective function value: {:.1f}".format(obj_func)) + # Restore full weighting matrix with current measurements + if cur_it == 0 and eppci.any_i_meas: + r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) + # prepare next iteration cur_it += 1 diff --git a/pandapower/estimation/algorithm/matrix_base.py b/pandapower/estimation/algorithm/matrix_base.py index 32afa19df..ac1bfdd74 100644 --- a/pandapower/estimation/algorithm/matrix_base.py +++ b/pandapower/estimation/algorithm/matrix_base.py @@ -9,7 +9,7 @@ from pandapower.pypower.idx_brch import F_BUS, T_BUS from pandapower.pypower.dSbus_dV import dSbus_dV from pandapower.pypower.dSbr_dV import dSbr_dV -from pandapower.pypower.dIbr_dV import dIbr_dV +from pandapower.pypower.dIbr_dV import dIbr_dV_new from pandapower.estimation.ppc_conversion import ExtendedPPCI @@ -150,16 +150,19 @@ def _dvabus_dV(V): def _dimiabr_dV(self, V): # for current we only interest in the magnitude at the moment - dif_dth, dif_dv, dit_dth, dit_dv, If, It = dIbr_dV(self.eppci['branch'], self.Yf, self.Yt, V) - dif_dth, dif_dv, dit_dth, dit_dv = map(lambda m: m.toarray(), (dif_dth, dif_dv, dit_dth, dit_dv)) - difm_dth = (np.abs(1e-5 * dif_dth + If.reshape((-1, 1))) - np.abs(If.reshape((-1, 1))))/1e-5 - difm_dv = (np.abs(1e-5 * dif_dv + If.reshape((-1, 1))) - np.abs(If.reshape((-1, 1))))/1e-5 - ditm_dth = (np.abs(1e-5 * dit_dth + It.reshape((-1, 1))) - np.abs(It.reshape((-1, 1))))/1e-5 - ditm_dv = (np.abs(1e-5 * dit_dv + It.reshape((-1, 1))) - np.abs(It.reshape((-1, 1))))/1e-5 - difa_dth = (np.angle(1e-5 * dif_dth + If.reshape((-1, 1))) - np.angle(If.reshape((-1, 1))))/1e-5 - difa_dv = (np.angle(1e-5 * dif_dv + If.reshape((-1, 1))) - np.angle(If.reshape((-1, 1))))/1e-5 - dita_dth = (np.angle(1e-5 * dit_dth + It.reshape((-1, 1))) - np.angle(It.reshape((-1, 1))))/1e-5 - dita_dv = (np.angle(1e-5 * dit_dv + It.reshape((-1, 1))) - np.angle(It.reshape((-1, 1))))/1e-5 + difm_dth, difm_dv, ditm_dth, ditm_dv = dIbr_dV_new(self.eppci['branch'], self.Yf, self.Yt, V) + difm_dth, difm_dv, ditm_dth, ditm_dv = map(lambda m: m.toarray(), (difm_dth, difm_dv, ditm_dth, ditm_dv)) + difa_dth, difa_dv, dita_dth, dita_dv = 0*difm_dth, 0*difm_dv, 0*ditm_dth, 0*ditm_dv + # dif_dth, dif_dv, dit_dth, dit_dv, If, It = dIbr_dV(self.eppci['branch'], self.Yf, self.Yt, V) + # dif_dth, dif_dv, dit_dth, dit_dv = map(lambda m: m.toarray(), (dif_dth, dif_dv, dit_dth, dit_dv)) + # difm_dth = (np.abs(1e-5 * dif_dth + If.reshape((-1, 1))) - np.abs(If.reshape((-1, 1))))/1e-5 + # difm_dv = (np.abs(1e-5 * dif_dv + If.reshape((-1, 1))) - np.abs(If.reshape((-1, 1))))/1e-5 + # ditm_dth = (np.abs(1e-5 * dit_dth + It.reshape((-1, 1))) - np.abs(It.reshape((-1, 1))))/1e-5 + # ditm_dv = (np.abs(1e-5 * dit_dv + It.reshape((-1, 1))) - np.abs(It.reshape((-1, 1))))/1e-5 + # difa_dth = (np.angle(1e-5 * dif_dth + If.reshape((-1, 1))) - np.angle(If.reshape((-1, 1))))/1e-5 + # difa_dv = (np.angle(1e-5 * dif_dv + If.reshape((-1, 1))) - np.angle(If.reshape((-1, 1))))/1e-5 + # dita_dth = (np.angle(1e-5 * dit_dth + It.reshape((-1, 1))) - np.angle(It.reshape((-1, 1))))/1e-5 + # dita_dv = (np.angle(1e-5 * dit_dv + It.reshape((-1, 1))) - np.angle(It.reshape((-1, 1))))/1e-5 return difm_dth, difm_dv, ditm_dth, ditm_dv, difa_dth, difa_dv, dita_dth, dita_dv diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index ceb462e50..5d8c7b1a1 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -89,7 +89,7 @@ def _init_ppc(net, v_start, delta_start, calculate_voltage_angles): return ppc, ppci -def _add_measurements_to_ppci(net, ppci, zero_injection): +def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): """ Add pandapower measurements to the ppci structure by adding new columns @@ -345,6 +345,21 @@ def _build_measurement_vectors(ppci, update_meas_only=False): ppci["branch"][i_degree_line_f_not_nan, branch_cols + IA_FROM], ppci["branch"][i_degree_line_t_not_nan, branch_cols + IA_TO] )).real.astype(np.float64) + imag_meas = np.concatenate((np.zeros(sum(p_bus_not_nan)), + np.zeros(sum(p_line_f_not_nan)), + np.zeros(sum(p_line_t_not_nan)), + np.zeros(sum(q_bus_not_nan)), + np.zeros(sum(q_line_f_not_nan)), + np.zeros(sum(q_line_t_not_nan)), + np.zeros(sum(v_bus_not_nan)), + np.zeros(sum(v_degree_bus_not_nan)), + np.ones(sum(i_line_f_not_nan)), + np.ones(sum(i_line_t_not_nan)), + np.zeros(sum(i_degree_line_f_not_nan)), + np.zeros(sum(i_degree_line_t_not_nan)) + )).astype(bool) + idx_non_imeas = np.flatnonzero(~imag_meas) + if not update_meas_only: # conserve the pandapower indices of measurements in the ppci order pp_meas_indices = np.concatenate((ppci["bus"][p_bus_not_nan, bus_cols + P_IDX], @@ -390,16 +405,16 @@ def _build_measurement_vectors(ppci, update_meas_only=False): any_degree_meas = np.any(np.r_[v_degree_bus_not_nan, i_degree_line_f_not_nan, i_degree_line_t_not_nan]) - return z, pp_meas_indices, r_cov, meas_mask, any_i_meas, any_degree_meas + return z, pp_meas_indices, r_cov, meas_mask, any_i_meas, any_degree_meas, idx_non_imeas else: return z def pp2eppci(net, v_start=None, delta_start=None, calculate_voltage_angles=True, zero_injection="aux_bus", - ppc=None, eppci=None): + algorithm='wls', ppc=None, eppci=None): if isinstance(eppci, ExtendedPPCI): - eppci.data = _add_measurements_to_ppci(net, eppci.data, zero_injection) + eppci.data = _add_measurements_to_ppci(net, eppci.data, zero_injection, algorithm) eppci.update_meas() return net, ppc, eppci else: @@ -408,7 +423,7 @@ def pp2eppci(net, v_start=None, delta_start=None, # add measurements to ppci structure # Finished converting pandapower network to ppci - ppci = _add_measurements_to_ppci(net, ppci, zero_injection) + ppci = _add_measurements_to_ppci(net, ppci, zero_injection, algorithm) return net, ppc, ExtendedPPCI(ppci) @@ -446,7 +461,7 @@ def __init__(self, ppci): def _initialize_meas(self): # calculate relevant vectors from ppci measurements self.z, self.pp_meas_indices, self.r_cov, self.non_nan_meas_mask,\ - self.any_i_meas, self.any_degree_meas =\ + self.any_i_meas, self.any_degree_meas, self.idx_non_imeas =\ _build_measurement_vectors(self, update_meas_only=False) self.non_nan_meas_selector = np.flatnonzero(self.non_nan_meas_mask) diff --git a/pandapower/pypower/dIbr_dV.py b/pandapower/pypower/dIbr_dV.py index d0831e309..e703f0d22 100644 --- a/pandapower/pypower/dIbr_dV.py +++ b/pandapower/pypower/dIbr_dV.py @@ -5,7 +5,7 @@ """Computes partial derivatives of branch currents w.r.t. voltage. """ -from numpy import diag, asmatrix, asarray +from numpy import diag, asmatrix, asarray, conj from scipy.sparse import issparse, csr_matrix as sparse @@ -60,3 +60,33 @@ def dIbr_dV(branch, Yf, Yt, V): It = asarray( Yt * asmatrix(V).T ).flatten() return dIf_dVa, dIf_dVm, dIt_dVa, dIt_dVm, If, It + +def dIbr_dV_new(branch, Yf, Yt, V): + # Compute currents. + if issparse(Yf): + If = Yf * V + It = Yt * V + else: + If = asarray( Yf * asmatrix(V).T ).flatten() + It = asarray( Yt * asmatrix(V).T ).flatten() + + vb = range(len(V)) + diagV = sparse((V, (vb, vb))) + diagVnorm = sparse((V / abs(V), (vb, vb))) + ib = range(len(If)) + idxf = abs(If) == 0 + idxt = abs(It) == 0 + diagIfnorm = sparse((conj(If) / abs(If), (ib, ib))) + diagItnorm = sparse((conj(It) / abs(It), (ib, ib))) + diagIfnorm[idxf,idxf] = 0 + diagItnorm[idxt,idxt] = 0 + a = diagIfnorm * Yf * diagV + dIf_dVa = - a.imag + b = diagIfnorm * Yf * diagVnorm + dIf_dVm = b.real + c = diagItnorm * Yt * diagV + dIt_dVa = - c.imag + d = diagItnorm * Yt * diagVnorm + dIt_dVm = d.real + + return dIf_dVa, dIf_dVm, dIt_dVa, dIt_dVm \ No newline at end of file From 884140fade4fa5a4e33a0ed8a2db6a4f3696fb45 Mon Sep 17 00:00:00 2001 From: marcopau Date: Mon, 7 Oct 2024 16:37:39 +0200 Subject: [PATCH 027/135] Set DC power flow as in normal power flow --- pandapower/estimation/ppc_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index 5d8c7b1a1..45d37160b 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -81,7 +81,7 @@ def _init_ppc(net, v_start, delta_start, calculate_voltage_angles): if np.any(net.trafo.shift_degree): vm_backup = ppci["bus"][:, 7].copy() pq_backup = ppci["bus"][:, [2, 3]].copy() - ppci["bus"][:, [2, 3]] = 0. + # ppci["bus"][:, [2, 3]] = 0. ppci = _run_dc_pf(ppci) ppci["bus"][:, 7] = vm_backup ppci["bus"][:, [2, 3]] = pq_backup From 169305367cf6da0476711ae2155741044afd215d Mon Sep 17 00:00:00 2001 From: marcopau Date: Mon, 7 Oct 2024 16:50:33 +0200 Subject: [PATCH 028/135] Initial steps design AF-WLS --- pandapower/estimation/algorithm/base.py | 83 +++++++++++++++++++++++ pandapower/estimation/ppc_conversion.py | 26 +++++++ pandapower/estimation/state_estimation.py | 6 +- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index cd7f00076..2ab649edb 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -262,3 +262,86 @@ def estimate(self, eppci: ExtendedPPCI, estimator="wls", **kwargs): self.check_result(current_error, cur_it) # update V/delta return eppci + + +class AFWLSAlgorithm(BaseAlgorithm): + def __init__(self, tolerance, maximum_iterations, logger=std_logger): + super(AFWLSAlgorithm, self).__init__(tolerance, maximum_iterations, logger) + + # Parameters for Bad data detection + self.R_inv = None + self.Gm = None + self.r = None + self.H = None + self.hx = None + self.iterations = None + self.obj_func = None + logging.basicConfig(level=logging.DEBUG) + + def estimate(self, eppci: ExtendedPPCI, **kwargs): + self.initialize(eppci) + # matrix calculation object + sem = BaseAlgebra(eppci) + + current_error, cur_it = 100., 0 + # invert covariance matrix + eppci.r_cov[eppci.r_cov<(10**(-6))] = 10**(-6) + r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) + E = eppci.E + while current_error > self.tolerance and cur_it < self.max_iterations: + self.logger.debug("Starting iteration {:d}".format(1 + cur_it)) + try: + # residual r + r = csr_matrix(sem.create_rx(E)).T + + # jacobian matrix H + H = csr_matrix(sem.create_hx_jacobian(E)) + + if cur_it == 0 and eppci.any_i_meas: + idx = eppci.idx_non_imeas + r_inv = r_inv[idx,:][:,idx] + r = r[idx,:] + H = H[idx,:] + + # gain matrix G_m + G_m = H.T * (r_inv * H) + + # state vector difference d_E + # d_E = G_m^-1 * (H' * R^-1 * r) + d_E = spsolve(G_m, H.T * (r_inv * r)) + + # Update E with d_E + E += d_E.ravel() + eppci.update_E(E) + + # log data + current_error = np.max(np.abs(d_E)) + obj_func = (r.T*r_inv*r)[0,0] + self.logger.debug("Current delta_x: {:.7f}".format(current_error)) + self.logger.debug("Current objective function value: {:.1f}".format(obj_func)) + + # Restore full weighting matrix + if cur_it == 0 and eppci.any_i_meas: + r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) + + # prepare next iteration + cur_it += 1 + + except np.linalg.linalg.LinAlgError: + self.logger.error("A problem appeared while using the linear algebra methods." + "Check and change the measurement set.") + return False + + # check if the estimation is successfull + self.check_result(current_error, cur_it) + self.iterations = cur_it + self.obj_func = obj_func + if self.successful: + # store variables required for chi^2 and r_N_max test: + self.R_inv = r_inv.toarray() + self.Gm = G_m.toarray() + self.r = r.toarray() + self.H = H.toarray() + # create h(x) for the current iteration + self.hx = sem.create_hx(eppci.E) + return eppci \ No newline at end of file diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index 5d8c7b1a1..6b7598086 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -270,6 +270,32 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): ppci["branch"] = np.hstack((ppci["branch"], branch_append)) else: ppci["branch"][:, branch_cols: branch_cols + branch_cols_se] = branch_append + + # Add rated power information for AF-WLS estimator + if algorithm == 'af-wls': + cluster_list_loads = net.load["type"].unique() + cluster_list_gen = net.sgen["type"].unique() + cluster_list_tot = np.concatenate((cluster_list_loads, cluster_list_gen), axis=0) + ppci["clusters"] = cluster_list_tot + num_clusters = len(cluster_list_tot) + num_buses = ppci["bus"].shape[0] + ppci["rated_powers_clusters"] = np.zeros([num_buses, 4*num_clusters]) + for var in ["load", "sgen"]: + for item in net[var].index: + if net[var]["in_service"][item]: + bus = net._pd2ppc_lookups["bus"][net[var].bus[item]] + cluster = net[var].type[item] + cluster_idx = np.where(cluster_list_tot == cluster)[0] + P = net[var].p_mw[item] + Q = net[var].q_mvar[item] + if var == 'load': + P *= -1 + Q *= -1 + ppci["rated_powers_clusters"][bus, cluster_idx] += P + ppci["rated_powers_clusters"][bus, cluster_idx + num_clusters] += Q + ppci["rated_powers_clusters"][bus, cluster_idx + 2*num_clusters] += abs(0.3*P) # std dev cluster variability hardcoded, think how to change it + ppci["rated_powers_clusters"][bus, cluster_idx + 2*num_clusters] += abs(0.3*Q) # std dev cluster variability hardcoded, think how to change it + return ppci diff --git a/pandapower/estimation/state_estimation.py b/pandapower/estimation/state_estimation.py index 28c3cd1f0..579614e70 100644 --- a/pandapower/estimation/state_estimation.py +++ b/pandapower/estimation/state_estimation.py @@ -9,7 +9,8 @@ from pandapower.estimation.algorithm.base import (WLSAlgorithm, WLSZeroInjectionConstraintsAlgorithm, - IRWLSAlgorithm) + IRWLSAlgorithm, + AFWLSAlgorithm) from pandapower.estimation.algorithm.lp import LPAlgorithm from pandapower.estimation.algorithm.optimization import OptAlgorithm from pandapower.estimation.ppc_conversion import pp2eppci, _initialize_voltage @@ -26,7 +27,8 @@ 'wls_with_zero_constraint': WLSZeroInjectionConstraintsAlgorithm, 'opt': OptAlgorithm, 'irwls': IRWLSAlgorithm, - 'lp': LPAlgorithm} + 'lp': LPAlgorithm, + 'af-wls': AFWLSAlgorithm} ALLOWED_OPT_VAR = {"a", "opt_method", "estimator"} From e5a21377d6ee33194c813143702a80975d7c346e Mon Sep 17 00:00:00 2001 From: quant Date: Mon, 7 Oct 2024 19:57:24 +0500 Subject: [PATCH 029/135] Copy array element to standard python scalar --- pandapower/estimation/state_estimation.py | 4 +- pandapower/grid_equivalents/rei_generation.py | 2 +- .../test/control/test_discrete_tap_control.py | 48 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/pandapower/estimation/state_estimation.py b/pandapower/estimation/state_estimation.py index d5a7d6fce..36e49fdd6 100644 --- a/pandapower/estimation/state_estimation.py +++ b/pandapower/estimation/state_estimation.py @@ -333,7 +333,7 @@ def perform_chi2_test(self, v_in_out=None, delta_in_out=None, self.logger.debug("Result of Chi^2 test:") self.logger.debug("Number of measurements: %d" % m) self.logger.debug("Number of state variables: %d" % n) - self.logger.debug("Performance index: %.2f" % J) + self.logger.debug("Performance index: %.2f" % J.item()) self.logger.debug("Chi^2 test threshold: %.2f" % test_thresh) if J <= test_thresh: @@ -421,7 +421,7 @@ def perform_rn_max_test(self, v_in_out=None, delta_in_out=None, else: self.logger.debug( "Largest normalized residual test failed (%.1f > %.1f)." - % (max(rN), rn_max_threshold)) + % (max(rN).item(), rn_max_threshold)) # Identify bad data: Determine index corresponding to max(rN): idx_rN = np.argsort(rN, axis=0)[-1] diff --git a/pandapower/grid_equivalents/rei_generation.py b/pandapower/grid_equivalents/rei_generation.py index 5bf5c8c81..c217776e7 100644 --- a/pandapower/grid_equivalents/rei_generation.py +++ b/pandapower/grid_equivalents/rei_generation.py @@ -261,7 +261,7 @@ def _create_net_zpbn(net, boundary_buses, all_internal_buses, all_external_buses sn_mva=Sn, index=max_sgen_idx+len(net_zpbn.sgen)+1) elif elm == "gen": vm_pu = v[key+"_vm_total"][v.ext_bus == int(re.findall(r"\d+", busstr)[0])].values.real - elm_idx = pp.create_gen(net_zpbn, i, float(P), float(vm_pu), name=key+"_rei_"+busstr, + elm_idx = pp.create_gen(net_zpbn, i, float(P), float(vm_pu.item()), name=key+"_rei_"+busstr, sn_mva=Sn, index=max_gen_idx+len(net_zpbn.gen)+1) # ---- match other columns diff --git a/pandapower/test/control/test_discrete_tap_control.py b/pandapower/test/control/test_discrete_tap_control.py index 5a12eaab2..736b03f63 100644 --- a/pandapower/test/control/test_discrete_tap_control.py +++ b/pandapower/test/control/test_discrete_tap_control.py @@ -39,13 +39,13 @@ def test_discrete_tap_control_lv(): logger.info("case1: low voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == -1 # increase voltage from 1.0 pu to 1.03 pu @@ -56,13 +56,13 @@ def test_discrete_tap_control_lv(): logger.info("case2: high voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == -2 # reduce voltage from 1.03 pu to 0.949 pu net.ext_grid.vm_pu = 0.949 @@ -72,13 +72,13 @@ def test_discrete_tap_control_lv(): logger.info("case2: high voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == 1 @@ -100,13 +100,13 @@ def test_discrete_tap_control_hv(): logger.info("case1: low voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == 1 # increase voltage from 1.0 pu to 1.03 pu net.ext_grid.vm_pu = 1.03 @@ -116,13 +116,13 @@ def test_discrete_tap_control_hv(): logger.info("case2: high voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == 2 # increase voltage from 1.0 pu to 1.03 pu net.ext_grid.vm_pu = 0.949 @@ -132,13 +132,13 @@ def test_discrete_tap_control_hv(): logger.info("case2: high voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == -1 @@ -160,13 +160,13 @@ def test_discrete_tap_control_lv_from_tap_step_percent(): logger.info("case1: low voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == -1 # check if it changes the lower and upper limits @@ -187,13 +187,13 @@ def test_discrete_tap_control_lv_from_tap_step_percent(): logger.info("case2: high voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == -2 # reduce voltage from 1.03 pu to 0.969 pu net.ext_grid.vm_pu = 0.969 @@ -203,13 +203,13 @@ def test_discrete_tap_control_lv_from_tap_step_percent(): logger.info("case2: high voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == 1 @@ -231,13 +231,13 @@ def test_discrete_tap_control_hv_from_tap_step_percent(): logger.info("case1: low voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == 1 # check if it changes the lower and upper limits @@ -258,13 +258,13 @@ def test_discrete_tap_control_hv_from_tap_step_percent(): logger.info("case2: high voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == 2 # reduce voltage from 1.03 pu to 0.969 pu net.ext_grid.vm_pu = 0.969 @@ -274,13 +274,13 @@ def test_discrete_tap_control_hv_from_tap_step_percent(): logger.info("case2: high voltage") logger.info("before control: trafo voltage at low voltage bus is %f, tap position is %u" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) # run control pp.runpp(net, run_control=True) logger.info( "after DiscreteTapControl: trafo voltage at low voltage bus is %f, tap position is %f" - % (net.res_bus.vm_pu[net.trafo.lv_bus].values, net.trafo.tap_pos.values)) + % (net.res_bus.vm_pu[net.trafo.lv_bus].values.item(), net.trafo.tap_pos.values.item())) assert net.trafo.tap_pos.at[0] == -1 From 987a6953097685ef9d9c4f9c5a58e54c1ed80ec9 Mon Sep 17 00:00:00 2001 From: quant Date: Mon, 7 Oct 2024 20:00:47 +0500 Subject: [PATCH 030/135] Copy array element to standard python scalar --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 78237ac96..51c0c3e47 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ Change Log [upcoming release] - 2024-..-.. ------------------------------- +- [FIXED] copy array element to standard python scalar - [FIXED] replacing deprecated in1d with isin - [ADDED] A switch to disable updating the vk and vkr values for trafo3w - [FIXED] cast the column to the correct type before assigning values From 4d9fa425ced8710f49b12326e7e097d4ed89f5e1 Mon Sep 17 00:00:00 2001 From: quant Date: Mon, 7 Oct 2024 20:55:11 +0500 Subject: [PATCH 031/135] Passing literal json to 'read_json' is deprecated --- pandapower/io_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/io_utils.py b/pandapower/io_utils.py index 63b49c55a..e226596a5 100644 --- a/pandapower/io_utils.py +++ b/pandapower/io_utils.py @@ -499,7 +499,7 @@ def Series(self): is_multiindex = self.d.pop('is_multiindex', False) index_name = self.d.pop('index_name', None) index_names = self.d.pop('index_names', None) - ser = pd.read_json(self.obj, precise_float=True, **self.d) + ser = pd.read_json(io.StringIO(self.obj), precise_float=True, **self.d) # restore index name and Multiindex if index_name is not None: From ab3dbf6c90dda3e4dfcc0e51dd40fb5b8623ff8a Mon Sep 17 00:00:00 2001 From: quant Date: Mon, 7 Oct 2024 20:56:39 +0500 Subject: [PATCH 032/135] Passing literal json to 'read_json' is deprecated --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 78237ac96..6c95e77f1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ Change Log [upcoming release] - 2024-..-.. ------------------------------- +- [FIXED] passing literal json to 'read_json' is deprecated - [FIXED] replacing deprecated in1d with isin - [ADDED] A switch to disable updating the vk and vkr values for trafo3w - [FIXED] cast the column to the correct type before assigning values From a64c3486c75362cc95eee055c85a9865dec86aa5 Mon Sep 17 00:00:00 2001 From: marcopau Date: Tue, 8 Oct 2024 09:49:18 +0200 Subject: [PATCH 033/135] fix bug --- pandapower/estimation/ppc_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index 6b7598086..4a8adb681 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -294,7 +294,7 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): ppci["rated_powers_clusters"][bus, cluster_idx] += P ppci["rated_powers_clusters"][bus, cluster_idx + num_clusters] += Q ppci["rated_powers_clusters"][bus, cluster_idx + 2*num_clusters] += abs(0.3*P) # std dev cluster variability hardcoded, think how to change it - ppci["rated_powers_clusters"][bus, cluster_idx + 2*num_clusters] += abs(0.3*Q) # std dev cluster variability hardcoded, think how to change it + ppci["rated_powers_clusters"][bus, cluster_idx + 3*num_clusters] += abs(0.3*Q) # std dev cluster variability hardcoded, think how to change it return ppci From 0f7566e2c69b7b57fa6357e9713a85d46f801035 Mon Sep 17 00:00:00 2001 From: marcopau Date: Wed, 9 Oct 2024 18:12:04 +0200 Subject: [PATCH 034/135] First working implementation AF-WLS estimator --- pandapower/estimation/algorithm/base.py | 11 +- .../estimation/algorithm/matrix_base.py | 115 ++++++++++++------ pandapower/estimation/ppc_conversion.py | 59 ++++++--- pandapower/estimation/state_estimation.py | 4 +- 4 files changed, 129 insertions(+), 60 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index 2ab649edb..60401a41d 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -288,6 +288,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): eppci.r_cov[eppci.r_cov<(10**(-6))] = 10**(-6) r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) E = eppci.E + num_clusters = len(self.eppci["clusters"]) while current_error > self.tolerance and cur_it < self.max_iterations: self.logger.debug("Starting iteration {:d}".format(1 + cur_it)) try: @@ -307,12 +308,11 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): G_m = H.T * (r_inv * H) # state vector difference d_E - # d_E = G_m^-1 * (H' * R^-1 * r) d_E = spsolve(G_m, H.T * (r_inv * r)) # Update E with d_E E += d_E.ravel() - eppci.update_E(E) + # eppci.update_E(E1) # log data current_error = np.max(np.abs(d_E)) @@ -342,6 +342,9 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): self.Gm = G_m.toarray() self.r = r.toarray() self.H = H.toarray() - # create h(x) for the current iteration - self.hx = sem.create_hx(eppci.E) + # split voltage and allocation factor variables + E1 = E[:-num_clusters] + E2 = E[-num_clusters:] + eppci.update_E(E1) + eppci.clusters = E2 return eppci \ No newline at end of file diff --git a/pandapower/estimation/algorithm/matrix_base.py b/pandapower/estimation/algorithm/matrix_base.py index ac1bfdd74..f4c871b24 100644 --- a/pandapower/estimation/algorithm/matrix_base.py +++ b/pandapower/estimation/algorithm/matrix_base.py @@ -52,7 +52,13 @@ def create_rx(self, E): def create_hx(self, E): f_bus, t_bus = self.fb, self.tb - V = self.eppci.E2V(E) + if self.eppci.algorithm == "af-wls": + num_clusters = len(self.eppci["clusters"]) + E1 = E[:-num_clusters] + E2 = E[-num_clusters:] + else: + E1 = E + V = self.eppci.E2V(E1) Sfe = V[f_bus] * np.conj(self.Yf * V) Ste = V[t_bus] * np.conj(self.Yt * V) Sbuse = V * np.conj(self.Ybus * V) @@ -64,25 +70,39 @@ def create_hx(self, E): np.imag(Ste), np.abs(V)] - if self.any_i_meas or self.any_degree_meas: - va = np.angle(V) - Ife = self.Yf * V - ifem = np.abs(Ife) - ifea = np.angle(Ife) - Ite = self.Yt * V - item = np.abs(Ite) - itea = np.angle(Ite) + # if self.any_i_meas or self.any_degree_meas: + va = np.angle(V) + Ife = self.Yf * V + ifem = np.abs(Ife) + ifea = np.angle(Ife) + Ite = self.Yt * V + item = np.abs(Ite) + itea = np.angle(Ite) + hx = np.r_[hx, + va, + ifem, + item, + ifea, + itea] + + if self.eppci.algorithm == "af-wls": + Pbuse2 = np.sum(np.multiply(E2,self.eppci["rated_power_clusters"][:,:num_clusters]),axis=1) + Qbuse2 = np.sum(np.multiply(E2,self.eppci["rated_power_clusters"][:,num_clusters:2*num_clusters]),axis=1) hx = np.r_[hx, - va, - ifem, - item, - ifea, - itea] + np.real(Sbuse)-Pbuse2, + np.imag(Sbuse)-Qbuse2] + return hx[self.non_nan_meas_selector] def create_hx_jacobian(self, E): # Using sparse matrix in creation sub-jacobian matrix - V = self.eppci.E2V(E) + if self.eppci.algorithm == "af-wls": + num_clusters = len(self.eppci["clusters"]) + E1 = E[:-num_clusters] + else: + E1 = E + + V = self.eppci.E2V(E1) dSbus_dth, dSbus_dv = self._dSbus_dv(V) dSf_dth, dSf_dv, dSt_dth, dSt_dv = self._dSbr_dv(V) @@ -106,29 +126,54 @@ def create_hx_jacobian(self, E): jac = np.r_[s_jac, vm_jac] - if self.any_i_meas or self.any_degree_meas: - dva_dth, dva_dv = self._dvabus_dV(V) - va_jac = np.c_[dva_dth, dva_dv] - difm_dth, difm_dv, ditm_dth, ditm_dv,\ - difa_dth, difa_dv, dita_dth, dita_dv = self._dimiabr_dV(V) - im_jac_th = np.r_[difm_dth, - ditm_dth] - im_jac_v = np.r_[difm_dv, - ditm_dv] - ia_jac_th = np.r_[difa_dth, - dita_dth] - ia_jac_v = np.r_[difa_dv, - dita_dv] - - im_jac = np.c_[im_jac_th, im_jac_v] - ia_jac = np.c_[ia_jac_th, ia_jac_v] + # if self.any_i_meas or self.any_degree_meas: + dva_dth, dva_dv = self._dvabus_dV(V) + va_jac = np.c_[dva_dth, dva_dv] + difm_dth, difm_dv, ditm_dth, ditm_dv,\ + difa_dth, difa_dv, dita_dth, dita_dv = self._dimiabr_dV(V) + im_jac_th = np.r_[difm_dth, + ditm_dth] + im_jac_v = np.r_[difm_dv, + ditm_dv] + ia_jac_th = np.r_[difa_dth, + dita_dth] + ia_jac_v = np.r_[difa_dv, + dita_dv] + + im_jac = np.c_[im_jac_th, im_jac_v] + ia_jac = np.c_[ia_jac_th, ia_jac_v] + + jac = np.r_[jac, + va_jac, + im_jac, + ia_jac] + + if self.eppci.algorithm == "af-wls": + p_eq_bal_jac_E1 = hstack((dSbus_dth.real, dSbus_dv.real)).toarray() + q_eq_bal_jac_E1 = hstack((dSbus_dth.imag, dSbus_dv.imag)).toarray() + + p_eq_bal_jac_E2 = - self.eppci["rated_power_clusters"][:,:num_clusters] + q_eq_bal_jac_E2 = - self.eppci["rated_power_clusters"][:,num_clusters:2*num_clusters] + + jac_E2 = np.zeros((jac.shape[0],num_clusters)) jac = np.r_[jac, - va_jac, - im_jac, - ia_jac] + p_eq_bal_jac_E1, + q_eq_bal_jac_E1] + + jac_E2 = np.r_[jac_E2, + p_eq_bal_jac_E2, + q_eq_bal_jac_E2] + + jac = jac[self.non_nan_meas_selector, :][:, self.delta_v_bus_selector] + jac_E2 = jac_E2[self.non_nan_meas_selector, :][:] + + jac = np.c_[jac, jac_E2] + + else: + jac = jac[self.non_nan_meas_selector, :][:, self.delta_v_bus_selector] - return jac[self.non_nan_meas_selector, :][:, self.delta_v_bus_selector] + return jac def _dSbus_dv(self, V): dSbus_dv, dSbus_dth = dSbus_dV(self.Ybus, V) diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index 4a8adb681..dda125fa7 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -279,23 +279,25 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): ppci["clusters"] = cluster_list_tot num_clusters = len(cluster_list_tot) num_buses = ppci["bus"].shape[0] - ppci["rated_powers_clusters"] = np.zeros([num_buses, 4*num_clusters]) + ppci["rated_power_clusters"] = np.zeros([num_buses, 4*num_clusters]) for var in ["load", "sgen"]: - for item in net[var].index: - if net[var]["in_service"][item]: - bus = net._pd2ppc_lookups["bus"][net[var].bus[item]] - cluster = net[var].type[item] - cluster_idx = np.where(cluster_list_tot == cluster)[0] - P = net[var].p_mw[item] - Q = net[var].q_mvar[item] - if var == 'load': - P *= -1 - Q *= -1 - ppci["rated_powers_clusters"][bus, cluster_idx] += P - ppci["rated_powers_clusters"][bus, cluster_idx + num_clusters] += Q - ppci["rated_powers_clusters"][bus, cluster_idx + 2*num_clusters] += abs(0.3*P) # std dev cluster variability hardcoded, think how to change it - ppci["rated_powers_clusters"][bus, cluster_idx + 3*num_clusters] += abs(0.3*Q) # std dev cluster variability hardcoded, think how to change it - + in_service = net[var]["in_service"] + active_elements = net[var][in_service] + bus = net._pd2ppc_lookups["bus"][active_elements.bus].astype(int) + P = active_elements.p_mw.values/ppci["baseMVA"] + Q = active_elements.q_mvar.values/ppci["baseMVA"] + if var == 'load': + P *= -1 + Q *= -1 + cluster = active_elements.type.values + for k in range(num_clusters): + cluster[cluster == cluster_list_tot[k]] = k + cluster = cluster.astype(int) + ppci["rated_power_clusters"][bus, cluster] = P + ppci["rated_power_clusters"][bus, cluster + num_clusters] = Q + ppci["rated_power_clusters"][bus, cluster + 2*num_clusters] = abs(0.03*P) # std dev cluster variability hardcoded, think how to change it + ppci["rated_power_clusters"][bus, cluster + 3*num_clusters] = abs(0.03*Q) # std dev cluster variability hardcoded, think how to change it + return ppci @@ -385,6 +387,9 @@ def _build_measurement_vectors(ppci, update_meas_only=False): np.zeros(sum(i_degree_line_t_not_nan)) )).astype(bool) idx_non_imeas = np.flatnonzero(~imag_meas) + if ppci.algorithm == "af-wls": + balance_eq_meas = np.zeros(ppci["rated_power_clusters"].shape[0]).astype(np.float64) + z = np.concatenate((z, balance_eq_meas[ppci.non_slack_bus_mask], balance_eq_meas[ppci.non_slack_bus_mask])) if not update_meas_only: # conserve the pandapower indices of measurements in the ppci order @@ -431,6 +436,15 @@ def _build_measurement_vectors(ppci, update_meas_only=False): any_degree_meas = np.any(np.r_[v_degree_bus_not_nan, i_degree_line_f_not_nan, i_degree_line_t_not_nan]) + if ppci.algorithm == "af-wls": + num_clusters = len(ppci["clusters"]) + # P_balance_dev_std = np.sqrt(np.sum(np.square(ppci["rated_power_clusters"][:,2*num_clusters:3*num_clusters]),axis=1)) + # Q_balance_dev_std = np.sqrt(np.sum(np.square(ppci["rated_power_clusters"][:,3*num_clusters:4*num_clusters]),axis=1)) + P_balance_dev_std = np.full((790,),25) + Q_balance_dev_std = np.full((790,),25) + r_cov = np.concatenate((r_cov, P_balance_dev_std[ppci.non_slack_bus_mask], Q_balance_dev_std[ppci.non_slack_bus_mask])) + meas_mask = np.concatenate((meas_mask, ppci.non_slack_bus_mask, ppci.non_slack_bus_mask)) + return z, pp_meas_indices, r_cov, meas_mask, any_i_meas, any_degree_meas, idx_non_imeas else: return z @@ -441,7 +455,7 @@ def pp2eppci(net, v_start=None, delta_start=None, algorithm='wls', ppc=None, eppci=None): if isinstance(eppci, ExtendedPPCI): eppci.data = _add_measurements_to_ppci(net, eppci.data, zero_injection, algorithm) - eppci.update_meas() + eppci.update_meas(algorithm) return net, ppc, eppci else: # initialize ppc @@ -450,13 +464,14 @@ def pp2eppci(net, v_start=None, delta_start=None, # add measurements to ppci structure # Finished converting pandapower network to ppci ppci = _add_measurements_to_ppci(net, ppci, zero_injection, algorithm) - return net, ppc, ExtendedPPCI(ppci) + return net, ppc, ExtendedPPCI(ppci, algorithm) class ExtendedPPCI(UserDict): - def __init__(self, ppci): + def __init__(self, ppci, algorithm): """Initialize ppci object with measurements.""" self.data = ppci + self.algorithm = algorithm # Measurement relevant parameters self.z = None @@ -466,7 +481,6 @@ def __init__(self, ppci): self.non_nan_meas_selector = None self.any_i_meas = False self.any_degree_meas = False - self._initialize_meas() # check slack bus self.non_slack_buses = np.argwhere(ppci["bus"][:, idx_bus.BUS_TYPE] != 3).ravel() @@ -476,6 +490,9 @@ def __init__(self, ppci): np.ones(self.non_slack_bus_mask.shape[0], dtype=bool)].ravel() self.delta_v_bus_selector = np.flatnonzero(self.delta_v_bus_mask) + # Iniialize measurements + self._initialize_meas() + # Initialize state variable self.v_init = ppci["bus"][:, idx_bus.VM] self.delta_init = np.radians(ppci["bus"][:, idx_bus.VA]) @@ -483,6 +500,8 @@ def __init__(self, ppci): self.v = self.v_init.copy() self.delta = self.delta_init.copy() self.E = self.E_init.copy() + if algorithm == "af-wls": + self.E = np.concatenate((self.E, np.full(ppci["clusters"].shape,0.5))) def _initialize_meas(self): # calculate relevant vectors from ppci measurements diff --git a/pandapower/estimation/state_estimation.py b/pandapower/estimation/state_estimation.py index 579614e70..dfbe0512a 100644 --- a/pandapower/estimation/state_estimation.py +++ b/pandapower/estimation/state_estimation.py @@ -274,6 +274,8 @@ def estimate(self, v_start='flat', delta_start='flat', calculate_voltage_angles= if self.solver.successful: self.net = eppci2pp(self.net, self.ppc, self.eppci) + if self.algorithm == "af-wls": + self.net["res_cluster_est"] = self.eppci.clusters else: self.logger.warning("Estimation failed! Pandapower network failed to update!") @@ -285,7 +287,7 @@ def estimate(self, v_start='flat', delta_start='flat', calculate_voltage_angles= if not self.recycle: self.ppc, self.eppci = None, None - if algorithm == "wls": + if algorithm == "wls" or algorithm == "af-wls": now = datetime.now() se_results = { "success": self.solver.successful, From 7ef2ac51849991ff027d0b0a0e9d97766505b285 Mon Sep 17 00:00:00 2001 From: marcopau Date: Sat, 12 Oct 2024 16:22:29 +0200 Subject: [PATCH 035/135] Added ill-conditioning check --- pandapower/estimation/algorithm/base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index cd7f00076..0a844b86a 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -5,7 +5,7 @@ import numpy as np from scipy.sparse import csr_matrix, vstack, hstack -from scipy.sparse.linalg import spsolve +from scipy.sparse.linalg import spsolve, norm, inv from pandapower.estimation.algorithm.estimator import BaseEstimatorIRWLS, get_estimator from pandapower.estimation.algorithm.matrix_base import BaseAlgebra, \ @@ -86,7 +86,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): current_error, cur_it = 100., 0 # invert covariance matrix - eppci.r_cov[eppci.r_cov<(10**(-6))] = 10**(-6) + eppci.r_cov[eppci.r_cov<(10**(-5))] = 10**(-5) r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) E = eppci.E while current_error > self.tolerance and cur_it < self.max_iterations: @@ -109,6 +109,11 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # gain matrix G_m # G_m = H^t * R^-1 * H G_m = H.T * (r_inv * H) + norm_G = norm(G_m, np.inf) + norm_invG = norm(inv(G_m), np.inf) + cond = norm_G*norm_invG + if cond > 10**18: + self.logger.warning("WARNING: Gain matrix is ill-conditioned: {:.2E}".format(cond)) # state vector difference d_E # d_E = G_m^-1 * (H' * R^-1 * r) From 6db64da35ee3758f23bf2164d5fa756fd3402f8f Mon Sep 17 00:00:00 2001 From: marcopau Date: Sat, 12 Oct 2024 16:24:46 +0200 Subject: [PATCH 036/135] Added ill-conditioning check --- pandapower/estimation/algorithm/base.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index 60401a41d..f07ddf54b 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -5,7 +5,7 @@ import numpy as np from scipy.sparse import csr_matrix, vstack, hstack -from scipy.sparse.linalg import spsolve +from scipy.sparse.linalg import spsolve, norm, inv from pandapower.estimation.algorithm.estimator import BaseEstimatorIRWLS, get_estimator from pandapower.estimation.algorithm.matrix_base import BaseAlgebra, \ @@ -86,7 +86,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): current_error, cur_it = 100., 0 # invert covariance matrix - eppci.r_cov[eppci.r_cov<(10**(-6))] = 10**(-6) + eppci.r_cov[eppci.r_cov<(10**(-5))] = 10**(-5) r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) E = eppci.E while current_error > self.tolerance and cur_it < self.max_iterations: @@ -109,6 +109,11 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # gain matrix G_m # G_m = H^t * R^-1 * H G_m = H.T * (r_inv * H) + norm_G = norm(G_m, np.inf) + norm_invG = norm(inv(G_m), np.inf) + cond = norm_G*norm_invG + if cond > 10**18: + self.logger.warning("WARNING: Gain matrix is ill-conditioned: {:.2E}".format(cond)) # state vector difference d_E # d_E = G_m^-1 * (H' * R^-1 * r) @@ -285,7 +290,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): current_error, cur_it = 100., 0 # invert covariance matrix - eppci.r_cov[eppci.r_cov<(10**(-6))] = 10**(-6) + eppci.r_cov[eppci.r_cov<(10**(-5))] = 10**(-5) r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) E = eppci.E num_clusters = len(self.eppci["clusters"]) @@ -306,6 +311,11 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # gain matrix G_m G_m = H.T * (r_inv * H) + norm_G = norm(G_m, np.inf) + norm_invG = norm(inv(G_m), np.inf) + cond = norm_G*norm_invG + if cond > 10**18: + self.logger.warning("WARNING: Gain matrix is ill-conditioned: {:.2E}".format(cond)) # state vector difference d_E d_E = spsolve(G_m, H.T * (r_inv * r)) From f90e03d7ca055bb66d9402cebab4ab60e684e045 Mon Sep 17 00:00:00 2001 From: dlohmeier Date: Mon, 14 Oct 2024 14:46:09 +0200 Subject: [PATCH 037/135] for json deserialization, it is now possible to ignore objects whose model is unknown (no code found in code base), so that they are just stored as a dict in the network -> this way, a JSON serialized network can be loaded anyway without raising an exception --- pandapower/file_io.py | 19 ++++++++++++++----- pandapower/io_utils.py | 38 ++++++++++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/pandapower/file_io.py b/pandapower/file_io.py index 4a56a7ac5..a864e4fbc 100644 --- a/pandapower/file_io.py +++ b/pandapower/file_io.py @@ -238,7 +238,7 @@ def _from_excel_old(xls): def from_json(filename, convert=True, encryption_key=None, elements_to_deserialize=None, keep_serialized_elements=True, add_basic_std_types=False, replace_elements=None, - empty_dict_like_object=None): + empty_dict_like_object=None, ignore_unknown_objects=False): """ Load a pandapower network from a JSON file. The index of the returned network is not necessarily in the same order as the original network. @@ -270,6 +270,9 @@ def from_json(filename, convert=True, encryption_key=None, elements_to_deseriali the data of the json string. Give another dict-like object to start filling that alternative object with the json data. + **ignore_unknown_objects** (bool, False) - If set to True, ignore any objects that cannot be + deserialized instead of raising an error + OUTPUT: **net** (dict) - The pandapower format network @@ -294,12 +297,13 @@ def from_json(filename, convert=True, encryption_key=None, elements_to_deseriali keep_serialized_elements=keep_serialized_elements, add_basic_std_types=add_basic_std_types, replace_elements=replace_elements, - empty_dict_like_object=empty_dict_like_object) + empty_dict_like_object=empty_dict_like_object, + ignore_unknown_objects=ignore_unknown_objects) def from_json_string(json_string, convert=False, encryption_key=None, elements_to_deserialize=None, keep_serialized_elements=True, add_basic_std_types=False, - replace_elements=None, empty_dict_like_object=None): + replace_elements=None, empty_dict_like_object=None, ignore_unknown_objects=False): """ Load a pandapower network from a JSON string. The index of the returned network is not necessarily in the same order as the original network. @@ -330,6 +334,9 @@ def from_json_string(json_string, convert=False, encryption_key=None, elements_t the data of the json string. Give another dict-like object to start filling that alternative object with the json data. + **ignore_unknown_objects** (bool, False) - If set to True, ignore any objects that cannot be + deserialized instead of raising an error + OUTPUT: **net** (dict) - The pandapower format network @@ -347,10 +354,12 @@ def from_json_string(json_string, convert=False, encryption_key=None, elements_t if elements_to_deserialize is None: net = json.loads(json_string, cls=io_utils.PPJSONDecoder, - empty_dict_like_object=empty_dict_like_object) + empty_dict_like_object=empty_dict_like_object, + ignore_unknown_objects=ignore_unknown_objects) else: net = json.loads(json_string, cls=io_utils.PPJSONDecoder, deserialize_pandas=False, - empty_dict_like_object=empty_dict_like_object) + empty_dict_like_object=empty_dict_like_object, + ignore_unknown_objects=ignore_unknown_objects) net_dummy = create_empty_network() if ('version' not in net.keys()) | (Version(net.version) < Version('2.1.0')): raise UserWarning('table selection is only possible for nets above version 2.0.1. ' diff --git a/pandapower/io_utils.py b/pandapower/io_utils.py index 63b49c55a..919ea3946 100644 --- a/pandapower/io_utils.py +++ b/pandapower/io_utils.py @@ -489,10 +489,11 @@ class FromSerializableRegistry(): class_name = '' module_name = '' - def __init__(self, obj, d, pp_hook_funct): + def __init__(self, obj, d, pp_hook_funct, ignore_unknown_objects=False): self.obj = obj self.d = d self.pp_hook = pp_hook_funct + self.ignore_unknown_objects = ignore_unknown_objects @from_serializable.register(class_name='Series', module_name='pandas.core.series') def Series(self): @@ -577,7 +578,9 @@ def DataFrame(self): # recreate jsoned objects for col in ('object', 'controller'): # "controller" for backwards compatibility if (col in df.columns): - df[col] = df[col].apply(self.pp_hook) + df[col] = df[col].apply(partial( + self.pp_hook, ignore_unknown_objects=self.ignore_unknown_objects + )) if 'geo' in df.columns: df['geo'] = df['geo'].dropna().apply(json.dumps).apply(geojson.loads) return df @@ -627,12 +630,28 @@ def function(self): @from_serializable.register() def rest(self): - module = importlib.import_module(self.module_name) - class_ = getattr(module, self.class_name) + try: + module = importlib.import_module(self.module_name) + except ModuleNotFoundError as e: + if self.ignore_unknown_objects: + warn(f"Module {self.module_name} not found. Returning object as is.") + return json.loads(self.obj) + else: + raise e + try: + class_ = getattr(module, self.class_name) + except AttributeError as e: + if self.ignore_unknown_objects: + warn(f"Class {self.class_name} not found in module {self.module_name}. Returning object as is.") + return json.loads(self.obj) + else: + raise e if isclass(class_) and issubclass(class_, JSONSerializableClass): if isinstance(self.obj, str): self.obj = json.loads(self.obj, cls=PPJSONDecoder, - object_hook=pp_hook) + object_hook=partial( + pp_hook, ignore_unknown_objects=self.ignore_unknown_objects + )) # backwards compatibility if "net" in self.obj: del self.obj["net"] @@ -689,16 +708,18 @@ def __init__(self, **kwargs): deserialize_pandas = kwargs.pop('deserialize_pandas', True) empty_dict_like_object = kwargs.pop('empty_dict_like_object', None) registry_class = kwargs.pop("registry_class", FromSerializableRegistry) + ignore_unknown_objects = kwargs.pop("ignore_unknown_objects", False) super_kwargs = {"object_hook": partial(pp_hook, deserialize_pandas=deserialize_pandas, empty_dict_like_object=empty_dict_like_object, - registry_class=registry_class)} + registry_class=registry_class, + ignore_unknown_objects=ignore_unknown_objects)} super_kwargs.update(kwargs) super().__init__(**super_kwargs) def pp_hook(d, deserialize_pandas=True, empty_dict_like_object=None, - registry_class=FromSerializableRegistry): + registry_class=FromSerializableRegistry, ignore_unknown_objects=False): try: if '_module' in d and '_class' in d: if 'pandas' in d['_module'] and not deserialize_pandas: @@ -713,7 +734,8 @@ def pp_hook(d, deserialize_pandas=True, empty_dict_like_object=None, else: # obj = {"_init": d, "_state": dict()} # backwards compatibility obj = {key: val for key, val in d.items() if key not in ['_module', '_class']} - fs = registry_class(obj, d, pp_hook) + fs = registry_class(obj, d, pp_hook, ignore_unknown_objects) + fs.class_name = d.pop('_class', '') fs.module_name = d.pop('_module', '') fs.empty_dict_like_object = empty_dict_like_object From 8b25160748fb9d8ddadf5f8b0667e5087ae25310 Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Mon, 14 Oct 2024 17:43:04 +0200 Subject: [PATCH 038/135] revert test_contingency.py back to as it was after fixing 0.9.0; todo: fix test_contingency.py for updated ls2g version --- pandapower/test/contingency/test_contingency.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandapower/test/contingency/test_contingency.py b/pandapower/test/contingency/test_contingency.py index 30decf79f..a2a07fe2e 100644 --- a/pandapower/test/contingency/test_contingency.py +++ b/pandapower/test/contingency/test_contingency.py @@ -132,7 +132,7 @@ def test_with_lightsim2grid(get_net, get_case): net.res_trafo[f"{s}_loading_percent"].values, atol=1e-6, rtol=0), s -# @pytest.mark.xfail(reason="remove this xfail when new version of lightsim2grid available") +@pytest.mark.xfail(reason="remove this xfail when new version of lightsim2grid available") @pytest.mark.skipif(not lightsim2grid_installed, reason="lightsim2grid package is not installed") def test_case118(): net = pp.networks.case118() @@ -404,10 +404,10 @@ def _randomize_indices(net): pp.reindex_elements(net, element, new_index) -# def test_reminder_bring_back_case118(): -# from packaging.version import Version -# if lightsim2grid_installed and Version(lightsim2grid.__version__) > Version("0.9.0"): -# raise UserWarning("bring back case 118 and remove xfail for test_unequal_trafo_impedances and test_case118") +def test_reminder_bring_back_case118(): + from packaging.version import Version + if lightsim2grid_installed and Version(lightsim2grid.__version__) > Version("0.9.0"): + raise UserWarning("bring back case 118 and remove xfail for test_unequal_trafo_impedances and test_case118") # todo: bring back case 118 when lightsim2grid new version is released From 2238e4bbead41ef54ddd48053d407b707c9b5ded Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Mon, 14 Oct 2024 19:17:50 +0200 Subject: [PATCH 039/135] add test for fixed trafo tap at star point with tap_neutral not zero --- pandapower/test/loadflow/test_results.py | 37 ++++- .../trafo_3w_tap_results_neutral_not_zero.csv | 129 ++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 pandapower/test/test_files/test_results_files/trafo_3w_tap_results_neutral_not_zero.csv diff --git a/pandapower/test/loadflow/test_results.py b/pandapower/test/loadflow/test_results.py index 58dc709ed..c20882ab7 100644 --- a/pandapower/test/loadflow/test_results.py +++ b/pandapower/test/loadflow/test_results.py @@ -644,7 +644,7 @@ def test_trafo3w(result_test_network, v_tol=1e-6, i_tol=1e-6, s_tol=2e-2, l_tol= @pytest.mark.parametrize("tap_pos", (-1, 2)) @pytest.mark.parametrize("tap_side", ('hv', 'mv', 'lv')) -@pytest.mark.parametrize("tap_step_degree", (15, 30)) +@pytest.mark.parametrize("tap_step_degree", (0, 15, 30)) def test_trafo3w_tap(tap_pos, tap_side, tap_step_degree): results = pd.read_csv(os.path.join(pp.pp_dir, "test", "test_files", "test_results_files", "trafo_3w_tap_results.csv"), sep=";", decimal=",") @@ -670,6 +670,41 @@ def test_trafo3w_tap(tap_pos, tap_side, tap_step_degree): "index==@index & element=='bus' & variable==@variable").value, rtol=0, atol=tol), f"failed for bus {index=}, {variable}, value {net.res_bus.at[index, variable]}" +@pytest.mark.parametrize("tap_pos", (2, 5)) +@pytest.mark.parametrize("tap_side", ('hv', 'mv', 'lv')) +@pytest.mark.parametrize("tap_step_degree", (0, 15, 30)) +def test_trafo3w_tap_neutral_not_zero(tap_pos, tap_side, tap_step_degree): + results = pd.read_csv(os.path.join(pp.pp_dir, "test", "test_files", "test_results_files", "trafo_3w_tap_results_neutral_not_zero.csv"), sep=";", decimal=",") + + if results.query("tap_side == @tap_side & tap_pos == @tap_pos & tap_step_degree == @tap_step_degree").empty: + pytest.skip(f"Skipping combination: tap_side={tap_side}, tap_pos={tap_pos}, tap_step_degree={tap_step_degree}") + + net = pp.from_json(os.path.join(pp.pp_dir, "test", "test_files","test_trafo3w_tap.json")) # + + net.trafo3w.loc[[0, 1], 'tap_min'] += 3 + net.trafo3w.loc[[0, 1], 'tap_max'] += 3 + net.trafo3w.loc[[0, 1], 'tap_neutral'] += 3 + + net.trafo3w.loc[0, 'tap_at_star_point']= False + net.trafo3w.loc[1, 'tap_at_star_point']= True + + net.trafo3w.loc[0, "tap_side"] = tap_side + net.trafo3w.loc[1, "tap_side"] = tap_side + net.trafo3w.loc[0, "tap_pos"] = tap_pos + net.trafo3w.loc[1, "tap_pos"] = tap_pos + net.trafo3w.loc[0, "tap_step_degree"] = tap_step_degree + net.trafo3w.loc[1, "tap_step_degree"] = tap_step_degree + pp.runpp(net) + + for index in range(8): + for variable, tol in zip(("vm_pu", "va_degree"), (1e-6, 1e-3)): + assert np.isclose(net.res_bus.at[index, variable], results.query( + "tap_side==@tap_side & tap_pos==@tap_pos & tap_step_degree==@tap_step_degree &" + "index==@index & element=='bus' & variable==@variable").value, + rtol=0, atol=tol), f"failed for bus {index=}, {variable}, value {net.res_bus.at[index, variable]}" + + + def test_impedance(result_test_network, v_tol=1e-6, i_tol=1e-6, s_tol=5e-3, l_tol=1e-3): net = result_test_network buses = net.bus[net.bus.zone == "test_impedance"] diff --git a/pandapower/test/test_files/test_results_files/trafo_3w_tap_results_neutral_not_zero.csv b/pandapower/test/test_files/test_results_files/trafo_3w_tap_results_neutral_not_zero.csv new file mode 100644 index 000000000..6e07ba49b --- /dev/null +++ b/pandapower/test/test_files/test_results_files/trafo_3w_tap_results_neutral_not_zero.csv @@ -0,0 +1,129 @@ +tap_side;tap_pos;tap_step_degree;element;name;index;variable;value +hv;5;0;bus;0_Slack;0;vm_pu;1 +hv;5;0;bus;0_Slack;0;va_degree;0 +hv;5;0;bus;1_HV;1;vm_pu;0,998129 +hv;5;0;bus;1_HV;1;va_degree;-0,093039 +hv;5;0;bus;2_MV;2;vm_pu;0,898854 +hv;5;0;bus;2_MV;2;va_degree;-0,686136 +hv;5;0;bus;3_LV;3;vm_pu;0,903326 +hv;5;0;bus;3_LV;3;va_degree;-0,394202 +hv;5;0;bus;4_Slack;4;vm_pu;1 +hv;5;0;bus;4_Slack;4;va_degree;0 +hv;5;0;bus;5_HV;5;vm_pu;0,998127 +hv;5;0;bus;5_HV;5;va_degree;-0,092972 +hv;5;0;bus;6_MV;6;vm_pu;0,898367 +hv;5;0;bus;6_MV;6;va_degree;-0,723542 +hv;5;0;bus;7_LV;7;vm_pu;0,902842 +hv;5;0;bus;7_LV;7;va_degree;-0,431292 +hv;2;0;bus;0_Slack;0;vm_pu;1 +hv;2;0;bus;0_Slack;0;va_degree;0 +hv;2;0;bus;1_HV;1;vm_pu;0,998009 +hv;2;0;bus;1_HV;1;va_degree;-0,092674 +hv;2;0;bus;2_MV;2;vm_pu;1,042353 +hv;2;0;bus;2_MV;2;va_degree;-0,583495 +hv;2;0;bus;3_LV;3;vm_pu;1,046203 +hv;2;0;bus;3_LV;3;va_degree;-0,366142 +hv;2;0;bus;4_Slack;4;vm_pu;1 +hv;2;0;bus;4_Slack;4;va_degree;0 +hv;2;0;bus;5_HV;5;vm_pu;0,99801 +hv;2;0;bus;5_HV;5;va_degree;-0,092708 +hv;2;0;bus;6_MV;6;vm_pu;1,042629 +hv;2;0;bus;6_MV;6;va_degree;-0,565781 +hv;2;0;bus;7_LV;7;vm_pu;1,046477 +hv;2;0;bus;7_LV;7;va_degree;-0,348542 +mv;5;0;bus;0_Slack;0;vm_pu;1 +mv;5;0;bus;0_Slack;0;va_degree;0 +mv;5;0;bus;1_HV;1;vm_pu;0,998058 +mv;5;0;bus;1_HV;1;va_degree;-0,092876 +mv;5;0;bus;2_MV;2;vm_pu;1,089856 +mv;5;0;bus;2_MV;2;va_degree;-0,555702 +mv;5;0;bus;3_LV;3;vm_pu;0,993842 +mv;5;0;bus;3_LV;3;va_degree;-0,374824 +mv;5;0;bus;4_Slack;4;vm_pu;1 +mv;5;0;bus;4_Slack;4;va_degree;0 +mv;5;0;bus;5_HV;5;vm_pu;0,998055 +mv;5;0;bus;5_HV;5;va_degree;-0,092822 +mv;5;0;bus;6_MV;6;vm_pu;1,088759 +mv;5;0;bus;6_MV;6;va_degree;-0,61575 +mv;5;0;bus;7_LV;7;vm_pu;0,993837 +mv;5;0;bus;7_LV;7;va_degree;-0,374788 +mv;2;0;bus;0_Slack;0;vm_pu;1 +mv;2;0;bus;0_Slack;0;va_degree;0 +mv;2;0;bus;1_HV;1;vm_pu;0,998054 +mv;2;0;bus;1_HV;1;va_degree;-0,092788 +mv;2;0;bus;2_MV;2;vm_pu;0,9397 +mv;2;0;bus;2_MV;2;va_degree;-0,653191 +mv;2;0;bus;3_LV;3;vm_pu;0,993834 +mv;2;0;bus;3_LV;3;va_degree;-0,374766 +mv;2;0;bus;4_Slack;4;vm_pu;1 +mv;2;0;bus;4_Slack;4;va_degree;0 +mv;2;0;bus;5_HV;5;vm_pu;0,998055 +mv;2;0;bus;5_HV;5;va_degree;-0,092822 +mv;2;0;bus;6_MV;6;vm_pu;0,940291 +mv;2;0;bus;6_MV;6;va_degree;-0,61575 +mv;2;0;bus;7_LV;7;vm_pu;0,993837 +mv;2;0;bus;7_LV;7;va_degree;-0,374788 +lv;5;0;bus;0_Slack;0;vm_pu;1 +lv;5;0;bus;0_Slack;0;va_degree;0 +lv;5;0;bus;1_HV;1;vm_pu;0,998056 +lv;5;0;bus;1_HV;1;va_degree;-0,092829 +lv;5;0;bus;2_MV;2;vm_pu;0,989781 +lv;5;0;bus;2_MV;2;va_degree;-0,615751 +lv;5;0;bus;3_LV;3;vm_pu;1,093532 +lv;5;0;bus;3_LV;3;va_degree;-0,356789 +lv;5;0;bus;4_Slack;4;vm_pu;1 +lv;5;0;bus;4_Slack;4;va_degree;0 +lv;5;0;bus;5_HV;5;vm_pu;0,998055 +lv;5;0;bus;5_HV;5;va_degree;-0,092822 +lv;5;0;bus;6_MV;6;vm_pu;0,98978 +lv;5;0;bus;6_MV;6;va_degree;-0,61575 +lv;5;0;bus;7_LV;7;vm_pu;1,093221 +lv;5;0;bus;7_LV;7;va_degree;-0,374788 +lv;2;0;bus;0_Slack;0;vm_pu;1 +lv;2;0;bus;0_Slack;0;va_degree;0 +lv;2;0;bus;1_HV;1;vm_pu;0,998055 +lv;2;0;bus;1_HV;1;va_degree;-0,092817 +lv;2;0;bus;2_MV;2;vm_pu;0,98978 +lv;2;0;bus;2_MV;2;va_degree;-0,61575 +lv;2;0;bus;3_LV;3;vm_pu;0,943978 +lv;2;0;bus;3_LV;3;va_degree;-0,385997 +lv;2;0;bus;4_Slack;4;vm_pu;1 +lv;2;0;bus;4_Slack;4;va_degree;0 +lv;2;0;bus;5_HV;5;vm_pu;0,998055 +lv;2;0;bus;5_HV;5;va_degree;-0,092822 +lv;2;0;bus;6_MV;6;vm_pu;0,98978 +lv;2;0;bus;6_MV;6;va_degree;-0,61575 +lv;2;0;bus;7_LV;7;vm_pu;0,944145 +lv;2;0;bus;7_LV;7;va_degree;-0,374788 +hv;5;15;bus;0_Slack;0;vm_pu;1 +hv;5;15;bus;0_Slack;0;va_degree;0 +hv;5;15;bus;1_HV;1;vm_pu;0,998127 +hv;5;15;bus;1_HV;1;va_degree;-0,093034 +hv;5;15;bus;2_MV;2;vm_pu;0,901427 +hv;5;15;bus;2_MV;2;va_degree;-2,035887 +hv;5;15;bus;3_LV;3;vm_pu;0,905886 +hv;5;15;bus;3_LV;3;va_degree;-1,74561 +hv;5;15;bus;4_Slack;4;vm_pu;1 +hv;5;15;bus;4_Slack;4;va_degree;0 +hv;5;15;bus;5_HV;5;vm_pu;0,998125 +hv;5;15;bus;5_HV;5;va_degree;-0,092969 +hv;5;15;bus;6_MV;6;vm_pu;0,900955 +hv;5;15;bus;6_MV;6;va_degree;-2,07209 +hv;5;15;bus;7_LV;7;vm_pu;0,905417 +hv;5;15;bus;7_LV;7;va_degree;-1,781509 +hv;2;30;bus;0_Slack;0;vm_pu;1 +hv;2;30;bus;0_Slack;0;va_degree;0 +hv;2;30;bus;1_HV;1;vm_pu;0,998016 +hv;2;30;bus;1_HV;1;va_degree;-0,092697 +hv;2;30;bus;2_MV;2;vm_pu;1,034638 +hv;2;30;bus;2_MV;2;va_degree;0,908979 +hv;2;30;bus;3_LV;3;vm_pu;1,038516 +hv;2;30;bus;3_LV;3;va_degree;1,129574 +hv;2;30;bus;4_Slack;4;vm_pu;1 +hv;2;30;bus;4_Slack;4;va_degree;0 +hv;2;30;bus;5_HV;5;vm_pu;0,998017 +hv;2;30;bus;5_HV;5;va_degree;-0,092725 +hv;2;30;bus;6_MV;6;vm_pu;1,034873 +hv;2;30;bus;6_MV;6;va_degree;0,924241 +hv;2;30;bus;7_LV;7;vm_pu;1,038751 +hv;2;30;bus;7_LV;7;va_degree;1,144736 From 687102a73ae652166d8523c232bfd4e34013c9dc Mon Sep 17 00:00:00 2001 From: dlohmeier Date: Wed, 16 Oct 2024 16:45:54 +0200 Subject: [PATCH 040/135] added changelog entry and test for ignore_unknown_objects option in JSON deserializer --- CHANGELOG.rst | 1 + pandapower/test/api/test_file_io.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fc94fec50..4dcc08944 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -88,6 +88,7 @@ Change Log - [CHANGED] Trafo Controllers can now be added to elements that are out of service, changed self.nothing_to_do() - [ADDED] Discrete shunt controller for local voltage regulation with shunt steps - [ADDED] cim2pp converter: Using lxml to parse XML files (better performance) +- [ADDED] possibility to load JSON files with unknown object models and just store the models as dictionaries in the network [2.14.7] - 2024-06-14 ------------------------------- diff --git a/pandapower/test/api/test_file_io.py b/pandapower/test/api/test_file_io.py index e9393297c..d77cc7d60 100644 --- a/pandapower/test/api/test_file_io.py +++ b/pandapower/test/api/test_file_io.py @@ -601,5 +601,29 @@ def test_multi_index(): assert_frame_equal(df, df2) +def test_ignore_unknown_objects(): + net = pp.networks.create_kerber_dorfnetz() + ctrl = control.ContinuousTapControl(net, 0, 1.02) + json_str = pp.to_json(net) + net2 = pp.from_json_string(json_str, ignore_unknown_objects=False) + + # in general, reloaded net should be equal to original net + assert isinstance(net2.controller.object.at[0], control.ContinuousTapControl) + assert_net_equal(net, net2) + + # slightly change the class name of the controller so that it cannot be identified + # by file_io anymore, but can still be loaded as dict if ignore_unknown_objects=True + json_str2 = json_str.replace("pandapower.control.controller.trafo.ContinuousTapControl", + "pandapower.control.controller.trafo.ContinuousTapControl2") + with pytest.raises(ModuleNotFoundError): + pp.from_json_string(json_str2, ignore_unknown_objects=False) + net3 = pp.from_json_string(json_str2, ignore_unknown_objects=True) + assert isinstance(net3.controller.object.at[0], dict) + + # make sure that the loaded net equals the original net except for the controller + net.controller.object.at[0] = net3.controller.object.at[0] + assert_net_equal(net, net3) + + if __name__ == "__main__": pytest.main([__file__, "-xs"]) From a58da9ee308d5977c19c384fb67ac485eba0940f Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Tue, 22 Oct 2024 09:30:22 +0200 Subject: [PATCH 041/135] Added pandas series accessor to facilitate the usage geojson strings --- pandapower/create.py | 37 ++++++++++++++++++++++++++++++ pandapower/test/api/test_create.py | 27 ++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pandapower/create.py b/pandapower/create.py index 7e98c7954..d186147c9 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -18,6 +18,7 @@ from pandapower.results import reset_results from pandapower.std_types import add_basic_std_types, load_std_type import numpy as np +from geojson import loads, GeoJSON try: import pandaplan.core.pplog as logging @@ -27,6 +28,42 @@ logger = logging.getLogger(__name__) +@pd.api.extensions.register_series_accessor("geojson") +class GeoAccessor: + """ + pandas Series accessor for the geo column. It facilitates the use of geojson strings. + """ + def __init__(self, pandas_obj): + self._validate(pandas_obj) + self._obj = pandas_obj + + @staticmethod + def _validate(obj): + try: + if not obj.apply(loads).apply(isinstance, args=(GeoJSON,)).all(): + raise AttributeError("Can only use .geojson accessor with geojson string values!") + except Exception as e: + raise AttributeError(f"Can only use .geojson accessor with geojson string values!: {e}") + + @staticmethod + def extract_coords(x): + if x["type"] == "Point": + return tuple(x["coordinates"]) + return [tuple(y) for y in x["coordinates"]] + + @property + def coords(self): + return self._obj.apply(loads).apply(self.extract_coords) + + @property + def as_geo_object(self): + return self._obj.apply(loads) + + @property + def type(self): + return self._obj.apply(loads).apply(lambda x: str(x["type"])) + + def create_empty_network(name="", f_hz=50., sn_mva=1, add_stdtypes=True): """ This function initializes the pandapower datastructure. diff --git a/pandapower/test/api/test_create.py b/pandapower/test/api/test_create.py index 1e1ed8dd7..d9bb4f20a 100644 --- a/pandapower/test/api/test_create.py +++ b/pandapower/test/api/test_create.py @@ -17,6 +17,33 @@ pd.set_option("display.width", 1000) +def test_geo_accessor(): + net = pp.create_empty_network() + b1 = pp.create_bus(net, 10, geodata=(1, 1)) + b2 = pp.create_bus(net, 10, geodata=(2, 2)) + l = pp.create_lines( + net, + [b1, b1], + [b2, b2], + [1.5, 3], + std_type="48-AL1/8-ST1A 10.0", + geodata=[[(1, 1), (2, 2), (3, 3)], [(1, 1), (1, 2)]], + ) + + assert net.line.geo.geojson.coords.at[l[0]] == [(1, 1), (2, 2), (3, 3)] + assert net.line.geo.geojson.coords.at[l[1]] == [(1, 1), (1, 2)] + assert net.bus.geo.geojson.coords.at[b1] == (1, 1) + assert net.bus.geo.geojson.coords.at[b2] == (2, 2) + assert net.bus.geo.geojson.type.at[b1] == "Point" + assert net.bus.geo.geojson.type.at[b2] == "Point" + assert net.line.geo.geojson.type.at[l[0]] == "LineString" + assert net.line.geo.geojson.type.at[l[1]] == "LineString" + assert set(net.line.geo.geojson.as_geo_object.at[l[0]].keys()) == {"coordinates", "type"} + assert set(net.line.geo.geojson.as_geo_object.at[l[1]].keys()) == {"coordinates", "type"} + assert set(net.bus.geo.geojson.as_geo_object.at[b1].keys()) == {"coordinates", "type"} + assert set(net.bus.geo.geojson.as_geo_object.at[b2].keys()) == {"coordinates", "type"} + + def test_convenience_create_functions(): net = pp.create_empty_network() b1 = pp.create_bus(net, 110.0) From 3acd74e1d0a64e7d8f9f9c7a03e5f709cfcc548c Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Tue, 22 Oct 2024 09:34:32 +0200 Subject: [PATCH 042/135] changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fc94fec50..2ce4b5b4d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,7 @@ Change Log [upcoming release] - 2024-..-.. ------------------------------- +- [ADDED] pandas series accessor for geo column - [FIXED] Increasing geojson precision as the default precision might cause problems with pandahub [2.14.11] - 2024-07-08 From 5b7d94039ec9b249db7bc149e34cd04ad817f21f Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Wed, 30 Oct 2024 15:01:29 +0100 Subject: [PATCH 043/135] added dropna to accessor --- pandapower/create.py | 8 ++++---- pandapower/test/api/test_create.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pandapower/create.py b/pandapower/create.py index d186147c9..7b3e161f4 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -40,7 +40,7 @@ def __init__(self, pandas_obj): @staticmethod def _validate(obj): try: - if not obj.apply(loads).apply(isinstance, args=(GeoJSON,)).all(): + if not obj.dropna().apply(loads).apply(isinstance, args=(GeoJSON,)).all(): raise AttributeError("Can only use .geojson accessor with geojson string values!") except Exception as e: raise AttributeError(f"Can only use .geojson accessor with geojson string values!: {e}") @@ -53,15 +53,15 @@ def extract_coords(x): @property def coords(self): - return self._obj.apply(loads).apply(self.extract_coords) + return self._obj.dropna().apply(loads).apply(self.extract_coords) @property def as_geo_object(self): - return self._obj.apply(loads) + return self._obj.dropna().apply(loads) @property def type(self): - return self._obj.apply(loads).apply(lambda x: str(x["type"])) + return self._obj.dropna().apply(loads).apply(lambda x: str(x["type"])) def create_empty_network(name="", f_hz=50., sn_mva=1, add_stdtypes=True): diff --git a/pandapower/test/api/test_create.py b/pandapower/test/api/test_create.py index d9bb4f20a..73bfc8f84 100644 --- a/pandapower/test/api/test_create.py +++ b/pandapower/test/api/test_create.py @@ -29,7 +29,9 @@ def test_geo_accessor(): std_type="48-AL1/8-ST1A 10.0", geodata=[[(1, 1), (2, 2), (3, 3)], [(1, 1), (1, 2)]], ) + pp.create_line(net, b1, b2, 1.5, std_type="48-AL1/8-ST1A 10.0") + assert len(net.line.geo.geojson.coords) == 2 assert net.line.geo.geojson.coords.at[l[0]] == [(1, 1), (2, 2), (3, 3)] assert net.line.geo.geojson.coords.at[l[1]] == [(1, 1), (1, 2)] assert net.bus.geo.geojson.coords.at[b1] == (1, 1) From 51257e121fc3409e94556b5b8295e0abf0ea3440 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 30 Oct 2024 16:20:32 +0100 Subject: [PATCH 044/135] consider new geo data format in docstrings --- pandapower/plotting/plotly/simple_plotly.py | 2 +- pandapower/plotting/plotly/vlevel_plotly.py | 4 ++-- pandapower/toolbox/data_modification.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandapower/plotting/plotly/simple_plotly.py b/pandapower/plotting/plotly/simple_plotly.py index 4193739d7..db64ff56d 100644 --- a/pandapower/plotting/plotly/simple_plotly.py +++ b/pandapower/plotting/plotly/simple_plotly.py @@ -116,7 +116,7 @@ def simple_plotly(net, respect_switches=True, use_line_geo=None, on_map=False, **respect_switches** (bool, True) - Respect switches when artificial geodata is created **use_line_geo** (bool, True) - defines if lines patches are based on - net.line_geodata of the lines (True) or on net.bus_geodata of the connected buses (False) + net.line.geo of the lines (True) or on net.bus.geo of the connected buses (False) **on_map** (bool, False) - enables using mapbox plot in plotly. If provided geodata are not real geo-coordinates in lon/lat form, on_map will be set to False. diff --git a/pandapower/plotting/plotly/vlevel_plotly.py b/pandapower/plotting/plotly/vlevel_plotly.py index 409baf916..eda70a6e0 100644 --- a/pandapower/plotting/plotly/vlevel_plotly.py +++ b/pandapower/plotting/plotly/vlevel_plotly.py @@ -35,8 +35,8 @@ def vlevel_plotly(net, respect_switches=True, use_line_geo=None, colors_dict=Non OPTIONAL: **respect_switches** (bool, True) - Respect switches when artificial geodata is created - **use_line_geo** (bool, True) - defines if lines patches are based on net.line_geodata - of the lines (True) or on net.bus_geodata of the connected buses (False) + **use_line_geo** (bool, True) - defines if lines patches are based on net.line.geo + of the lines (True) or on net.bus.geo of the connected buses (False) *colors_dict** (dict, None) - dictionary for customization of colors for each voltage level in the form: voltage : color diff --git a/pandapower/toolbox/data_modification.py b/pandapower/toolbox/data_modification.py index a8164fedb..87e2f6b77 100644 --- a/pandapower/toolbox/data_modification.py +++ b/pandapower/toolbox/data_modification.py @@ -376,7 +376,7 @@ def create_continuous_elements_index(net, start=0, add_df_to_reindex=set()): if et in net and isinstance(net[et], pd.DataFrame): if et in ["bus_geodata", "line_geodata"]: - logger.info(et + " don't need to bo included to 'add_df_to_reindex'. It is " + + logger.info(et + " don't need to be included to 'add_df_to_reindex'. It is " + "already included by et=='" + et.split("_")[0] + "'.") else: reindex_elements(net, et, new_index) From 5f2d6d2c1e583306a5b2958a91fa95f936407e0d Mon Sep 17 00:00:00 2001 From: hkoertge Date: Thu, 31 Oct 2024 12:04:06 +0100 Subject: [PATCH 045/135] fixed create_synthetic_voltage_control_lv_network not using geojson --- .../synthetic_voltage_control_lv_networks.py | 99 +++++++++++++++++-- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/pandapower/networks/synthetic_voltage_control_lv_networks.py b/pandapower/networks/synthetic_voltage_control_lv_networks.py index 6523cab36..c1b7edb71 100644 --- a/pandapower/networks/synthetic_voltage_control_lv_networks.py +++ b/pandapower/networks/synthetic_voltage_control_lv_networks.py @@ -3,10 +3,10 @@ # Copyright (c) 2016-2024 by University of Kassel and Fraunhofer Institute for Energy Economics # and Energy System Technology (IEE), Kassel. All rights reserved. -import io import pandapower as pp import pandas as pd from numpy import nan, append +from geojson import Point, dumps def create_synthetic_voltage_control_lv_network(network_class="rural_1"): @@ -187,13 +187,94 @@ def create_synthetic_voltage_control_lv_network(network_class="rural_1"): # set bus geo data bus_geo = { - "rural_1": '{"x":{"0":0.0,"1":0.0,"2":-1.6666666667,"3":-1.6666666667,"4":-0.1666666667,"5":-0.6666666667,"6":-1.1666666667,"7":-1.6666666667,"8":0.3333333333,"9":-0.1666666667,"10":-0.6666666667,"11":-1.6666666667,"12":1.8333333333,"13":1.3333333333,"14":0.8333333333,"15":0.3333333333,"16":-0.1666666667,"17":-0.6666666667,"18":-1.1666666667,"19":2.3333333333,"20":1.8333333333,"21":1.3333333333,"22":0.8333333333,"23":0.3333333333,"24":-0.1666666667,"25":-1.1666666667},"y":{"0":0.0,"1":1.0,"2":2.0,"3":3.0,"4":2.0,"5":3.0,"6":4.0,"7":5.0,"8":3.0,"9":4.0,"10":5.0,"11":6.0,"12":2.0,"13":3.0,"14":4.0,"15":5.0,"16":6.0,"17":7.0,"18":8.0,"19":3.0,"20":4.0,"21":5.0,"22":6.0,"23":7.0,"24":8.0,"25":9.0}}', - "rural_2": '{"x":{"0":0.0,"1":0.0,"2":-2.5,"3":-2.5,"4":-1.0,"5":-1.5,"6":-2.0,"7":-0.5,"8":-1.0,"9":-2.0,"10":1.0,"11":0.5,"12":0.0,"13":1.5,"14":1.0,"15":0.0,"16":2.5,"17":2.5},"y":{"0":0.0,"1":1.0,"2":2.0,"3":3.0,"4":2.0,"5":3.0,"6":4.0,"7":3.0,"8":4.0,"9":5.0,"10":2.0,"11":3.0,"12":4.0,"13":3.0,"14":4.0,"15":5.0,"16":2.0,"17":3.0}}', - "village_1": '{"x":{"0":0.0,"1":0.0,"2":-3.0,"3":-3.5,"4":-4.0,"5":-4.5,"6":-5.0,"7":-5.5,"8":-6.0,"9":-6.5,"10":-7.0,"11":-2.5,"12":-3.0,"13":-3.5,"14":-4.0,"15":-4.5,"16":-5.0,"17":-5.5,"18":-6.0,"19":-7.0,"20":-1.0,"21":-1.5,"22":-2.0,"23":-2.5,"24":-3.0,"25":-3.5,"26":-4.0,"27":-4.5,"28":-5.0,"29":-5.5,"30":-6.0,"31":-6.5,"32":-7.0,"33":-7.5,"34":-8.0,"35":-8.5,"36":-0.5,"37":-1.0,"38":-1.5,"39":-2.0,"40":-2.5,"41":-3.0,"42":-3.5,"43":-4.0,"44":-4.5,"45":-5.0,"46":-5.5,"47":-6.0,"48":-6.5,"49":-7.0,"50":-7.5,"51":-8.5,"52":1.0,"53":0.5,"54":0.0,"55":-0.5,"56":-1.0,"57":1.5,"58":1.0,"59":0.5,"60":0.0,"61":-1.0,"62":3.0,"63":2.5,"64":2.0,"65":1.5,"66":1.0,"67":0.5,"68":0.0,"69":-0.5,"70":-1.0,"71":3.5,"72":3.0,"73":2.5,"74":2.0,"75":1.5,"76":1.0,"77":0.5,"78":0.0,"79":-1.0},"y":{"0":0.0,"1":1.0,"2":2.0,"3":3.0,"4":4.0,"5":5.0,"6":6.0,"7":7.0,"8":8.0,"9":9.0,"10":10.0,"11":3.0,"12":4.0,"13":5.0,"14":6.0,"15":7.0,"16":8.0,"17":9.0,"18":10.0,"19":11.0,"20":2.0,"21":3.0,"22":4.0,"23":5.0,"24":6.0,"25":7.0,"26":8.0,"27":9.0,"28":10.0,"29":11.0,"30":12.0,"31":13.0,"32":14.0,"33":15.0,"34":16.0,"35":17.0,"36":3.0,"37":4.0,"38":5.0,"39":6.0,"40":7.0,"41":8.0,"42":9.0,"43":10.0,"44":11.0,"45":12.0,"46":13.0,"47":14.0,"48":15.0,"49":16.0,"50":17.0,"51":18.0,"52":2.0,"53":3.0,"54":4.0,"55":5.0,"56":6.0,"57":3.0,"58":4.0,"59":5.0,"60":6.0,"61":7.0,"62":2.0,"63":3.0,"64":4.0,"65":5.0,"66":6.0,"67":7.0,"68":8.0,"69":9.0,"70":10.0,"71":3.0,"72":4.0,"73":5.0,"74":6.0,"75":7.0,"76":8.0,"77":9.0,"78":10.0,"79":11.0}}', - "village_2": '{"x":{"0":0.0,"1":0.0,"2":-3.0,"3":-3.5,"4":-4.0,"5":-4.5,"6":-5.0,"7":-5.5,"8":-6.0,"9":-6.5,"10":-7.0,"11":-2.5,"12":-3.0,"13":-3.5,"14":-4.0,"15":-4.5,"16":-5.0,"17":-5.5,"18":-6.0,"19":-7.0,"20":-1.0,"21":-1.5,"22":-2.0,"23":-2.5,"24":-3.0,"25":-3.5,"26":-4.0,"27":-4.5,"28":-5.0,"29":-5.5,"30":-6.0,"31":-6.5,"32":-0.5,"33":-1.0,"34":-1.5,"35":-2.0,"36":-2.5,"37":-3.0,"38":-3.5,"39":-4.0,"40":-4.5,"41":-5.0,"42":-5.5,"43":-6.5,"44":1.0,"45":0.5,"46":0.0,"47":-0.5,"48":-1.0,"49":1.5,"50":1.0,"51":0.5,"52":0.0,"53":-1.0,"54":3.0,"55":2.5,"56":2.0,"57":1.5,"58":1.0,"59":0.5,"60":0.0,"61":-0.5,"62":-1.0,"63":-1.5,"64":3.5,"65":3.0,"66":2.5,"67":2.0,"68":1.5,"69":1.0,"70":0.5,"71":0.0,"72":-0.5,"73":-1.5},"y":{"0":0.0,"1":1.0,"2":2.0,"3":3.0,"4":4.0,"5":5.0,"6":6.0,"7":7.0,"8":8.0,"9":9.0,"10":10.0,"11":3.0,"12":4.0,"13":5.0,"14":6.0,"15":7.0,"16":8.0,"17":9.0,"18":10.0,"19":11.0,"20":2.0,"21":3.0,"22":4.0,"23":5.0,"24":6.0,"25":7.0,"26":8.0,"27":9.0,"28":10.0,"29":11.0,"30":12.0,"31":13.0,"32":3.0,"33":4.0,"34":5.0,"35":6.0,"36":7.0,"37":8.0,"38":9.0,"39":10.0,"40":11.0,"41":12.0,"42":13.0,"43":14.0,"44":2.0,"45":3.0,"46":4.0,"47":5.0,"48":6.0,"49":3.0,"50":4.0,"51":5.0,"52":6.0,"53":7.0,"54":2.0,"55":3.0,"56":4.0,"57":5.0,"58":6.0,"59":7.0,"60":8.0,"61":9.0,"62":10.0,"63":11.0,"64":3.0,"65":4.0,"66":5.0,"67":6.0,"68":7.0,"69":8.0,"70":9.0,"71":10.0,"72":11.0,"73":12.0}}', - "suburb_1": '{"x":{"0":0.0,"1":0.0,"2":-9.5,"3":-10.0,"4":-10.5,"5":-11.0,"6":-11.5,"7":-12.0,"8":-12.5,"9":-13.0,"10":-13.5,"11":-9.0,"12":-9.5,"13":-10.0,"14":-10.5,"15":-11.0,"16":-11.5,"17":-12.0,"18":-12.5,"19":-13.5,"20":-7.5,"21":-8.0,"22":-8.5,"23":-9.0,"24":-9.5,"25":-10.0,"26":-10.5,"27":-11.0,"28":-11.5,"29":-12.0,"30":-12.5,"31":-13.0,"32":-13.5,"33":-14.0,"34":-14.5,"35":-15.0,"36":-15.5,"37":-7.0,"38":-7.5,"39":-8.0,"40":-8.5,"41":-9.0,"42":-9.5,"43":-10.0,"44":-10.5,"45":-11.0,"46":-11.5,"47":-12.0,"48":-12.5,"49":-13.0,"50":-13.5,"51":-14.0,"52":-14.5,"53":-15.5,"54":-5.5,"55":-6.0,"56":-6.5,"57":-7.0,"58":-7.5,"59":-5.0,"60":-5.5,"61":-6.0,"62":-6.5,"63":-7.5,"64":-3.5,"65":-4.0,"66":-4.5,"67":-5.0,"68":-5.5,"69":-6.0,"70":-6.5,"71":-7.0,"72":-7.5,"73":-8.0,"74":-8.5,"75":-9.0,"76":-9.5,"77":-3.0,"78":-3.5,"79":-4.0,"80":-4.5,"81":-5.0,"82":-5.5,"83":-6.0,"84":-6.5,"85":-7.0,"86":-7.5,"87":-8.0,"88":-8.5,"89":-9.5,"90":-1.5,"91":-2.0,"92":-2.5,"93":-3.0,"94":-3.5,"95":-4.0,"96":-4.5,"97":-5.0,"98":-1.0,"99":-1.5,"100":-2.0,"101":-2.5,"102":-3.0,"103":-3.5,"104":-4.0,"105":-5.0,"106":0.0,"107":0.0,"108":1.5,"109":1.0,"110":0.5,"111":0.0,"112":-0.5,"113":-1.0,"114":-1.5,"115":-2.0,"116":-2.5,"117":2.0,"118":1.5,"119":1.0,"120":0.5,"121":0.0,"122":-0.5,"123":-1.0,"124":-1.5,"125":-2.5,"126":3.5,"127":3.0,"128":2.5,"129":2.0,"130":1.5,"131":1.0,"132":0.5,"133":0.0,"134":-0.5,"135":-1.0,"136":-1.5,"137":-2.0,"138":-2.5,"139":-3.0,"140":-3.5,"141":-4.0,"142":-4.5,"143":4.0,"144":3.5,"145":3.0,"146":2.5,"147":2.0,"148":1.5,"149":1.0,"150":0.5,"151":0.0,"152":-0.5,"153":-1.0,"154":-1.5,"155":-2.0,"156":-2.5,"157":-3.0,"158":-3.5,"159":-4.5,"160":5.5,"161":5.0,"162":4.5,"163":4.0,"164":3.5,"165":6.0,"166":5.5,"167":5.0,"168":4.5,"169":3.5,"170":7.5,"171":7.0,"172":6.5,"173":6.0,"174":5.5,"175":5.0,"176":4.5,"177":4.0,"178":3.5,"179":3.0,"180":2.5,"181":2.0,"182":1.5,"183":8.0,"184":7.5,"185":7.0,"186":6.5,"187":6.0,"188":5.5,"189":5.0,"190":4.5,"191":4.0,"192":3.5,"193":3.0,"194":2.5,"195":1.5,"196":9.5,"197":9.0,"198":8.5,"199":8.0,"200":10.0,"201":9.5,"202":9.0,"203":8.0},"y":{"0":0.0,"1":1.0,"2":2.0,"3":3.0,"4":4.0,"5":5.0,"6":6.0,"7":7.0,"8":8.0,"9":9.0,"10":10.0,"11":3.0,"12":4.0,"13":5.0,"14":6.0,"15":7.0,"16":8.0,"17":9.0,"18":10.0,"19":11.0,"20":2.0,"21":3.0,"22":4.0,"23":5.0,"24":6.0,"25":7.0,"26":8.0,"27":9.0,"28":10.0,"29":11.0,"30":12.0,"31":13.0,"32":14.0,"33":15.0,"34":16.0,"35":17.0,"36":18.0,"37":3.0,"38":4.0,"39":5.0,"40":6.0,"41":7.0,"42":8.0,"43":9.0,"44":10.0,"45":11.0,"46":12.0,"47":13.0,"48":14.0,"49":15.0,"50":16.0,"51":17.0,"52":18.0,"53":19.0,"54":2.0,"55":3.0,"56":4.0,"57":5.0,"58":6.0,"59":3.0,"60":4.0,"61":5.0,"62":6.0,"63":7.0,"64":2.0,"65":3.0,"66":4.0,"67":5.0,"68":6.0,"69":7.0,"70":8.0,"71":9.0,"72":10.0,"73":11.0,"74":12.0,"75":13.0,"76":14.0,"77":3.0,"78":4.0,"79":5.0,"80":6.0,"81":7.0,"82":8.0,"83":9.0,"84":10.0,"85":11.0,"86":12.0,"87":13.0,"88":14.0,"89":15.0,"90":2.0,"91":3.0,"92":4.0,"93":5.0,"94":6.0,"95":7.0,"96":8.0,"97":9.0,"98":3.0,"99":4.0,"100":5.0,"101":6.0,"102":7.0,"103":8.0,"104":9.0,"105":10.0,"106":2.0,"107":3.0,"108":2.0,"109":3.0,"110":4.0,"111":5.0,"112":6.0,"113":7.0,"114":8.0,"115":9.0,"116":10.0,"117":3.0,"118":4.0,"119":5.0,"120":6.0,"121":7.0,"122":8.0,"123":9.0,"124":10.0,"125":11.0,"126":2.0,"127":3.0,"128":4.0,"129":5.0,"130":6.0,"131":7.0,"132":8.0,"133":9.0,"134":10.0,"135":11.0,"136":12.0,"137":13.0,"138":14.0,"139":15.0,"140":16.0,"141":17.0,"142":18.0,"143":3.0,"144":4.0,"145":5.0,"146":6.0,"147":7.0,"148":8.0,"149":9.0,"150":10.0,"151":11.0,"152":12.0,"153":13.0,"154":14.0,"155":15.0,"156":16.0,"157":17.0,"158":18.0,"159":19.0,"160":2.0,"161":3.0,"162":4.0,"163":5.0,"164":6.0,"165":3.0,"166":4.0,"167":5.0,"168":6.0,"169":7.0,"170":2.0,"171":3.0,"172":4.0,"173":5.0,"174":6.0,"175":7.0,"176":8.0,"177":9.0,"178":10.0,"179":11.0,"180":12.0,"181":13.0,"182":14.0,"183":3.0,"184":4.0,"185":5.0,"186":6.0,"187":7.0,"188":8.0,"189":9.0,"190":10.0,"191":11.0,"192":12.0,"193":13.0,"194":14.0,"195":15.0,"196":2.0,"197":3.0,"198":4.0,"199":5.0,"200":3.0,"201":4.0,"202":5.0,"203":6.0}}' + "rural_1": {0: [0.0, 0.0], 1: [0.0, 1.0], 2: [-1.6666666667, 2.0], 3: [-1.6666666667, 3.0], + 4: [-0.1666666667, 2.0], 5: [-0.6666666667, 3.0], 6: [-1.1666666667, 4.0], 7: [-1.6666666667, 5.0], + 8: [0.3333333333, 3.0], 9: [-0.1666666667, 4.0], 10: [-0.6666666667, 5.0], 11: [-1.6666666667, 6.0], + 12: [1.8333333333, 2.0], 13: [1.3333333333, 3.0], 14: [0.8333333333, 4.0], 15: [0.3333333333, 5.0], + 16: [-0.1666666667, 6.0], 17: [-0.6666666667, 7.0], 18: [-1.1666666667, 8.0], + 19: [2.3333333333, 3.0], 20: [1.8333333333, 4.0], 21: [1.3333333333, 5.0], 22: [0.8333333333, 6.0], + 23: [0.3333333333, 7.0], 24: [-0.1666666667, 8.0], 25: [-1.1666666667, 9.0] + }, + "rural_2": {0: [0.0, 0.0], 1: [0.0, 1.0], 2: [-2.5, 2.0], 3: [-2.5, 3.0], 4: [-1.0, 2.0], 5: [-1.5, 3.0], + 6: [-2.0, 4.0], 7: [-0.5, 3.0], 8: [-1.0, 4.0], 9: [-2.0, 5.0], 10: [1.0, 2.0], 11: [0.5, 3.0], + 12: [0.0, 4.0], 13: [1.5, 3.0], 14: [1.0, 4.0], 15: [0.0, 5.0], 16: [2.5, 2.0], 17: [2.5, 3.0] + }, + "village_1": {0: [0.0, 0.0], 1: [0.0, 1.0], 2: [-3.0, 2.0], 3: [-3.5, 3.0], 4: [-4.0, 4.0], 5: [-4.5, 5.0], + 6: [-5.0, 6.0], 7: [-5.5, 7.0], 8: [-6.0, 8.0], 9: [-6.5, 9.0], 10: [-7.0, 10.0], 11: [-2.5, 3.0], + 12: [-3.0, 4.0], 13: [-3.5, 5.0], 14: [-4.0, 6.0], 15: [-4.5, 7.0], 16: [-5.0, 8.0], + 17: [-5.5, 9.0], 18: [-6.0, 10.0], 19: [-7.0, 11.0], 20: [-1.0, 2.0], 21: [-1.5, 3.0], + 22: [-2.0, 4.0], 23: [-2.5, 5.0], 24: [-3.0, 6.0], 25: [-3.5, 7.0], 26: [-4.0, 8.0], + 27: [-4.5, 9.0], 28: [-5.0, 10.0], 29: [-5.5, 11.0], 30: [-6.0, 12.0], 31: [-6.5, 13.0], + 32: [-7.0, 14.0], 33: [-7.5, 15.0], 34: [-8.0, 16.0], 35: [-8.5, 17.0], 36: [-0.5, 3.0], + 37: [-1.0, 4.0], 38: [-1.5, 5.0], 39: [-2.0, 6.0], 40: [-2.5, 7.0], 41: [-3.0, 8.0], + 42: [-3.5, 9.0], 43: [-4.0, 10.0], 44: [-4.5, 11.0], 45: [-5.0, 12.0], 46: [-5.5, 13.0], + 47: [-6.0, 14.0], 48: [-6.5, 15.0], 49: [-7.0, 16.0], 50: [-7.5, 17.0], 51: [-8.5, 18.0], + 52: [1.0, 2.0], 53: [0.5, 3.0], 54: [0.0, 4.0], 55: [-0.5, 5.0], 56: [-1.0, 6.0], 57: [1.5, 3.0], + 58: [1.0, 4.0], 59: [0.5, 5.0], 60: [0.0, 6.0], 61: [-1.0, 7.0], 62: [3.0, 2.0], 63: [2.5, 3.0], + 64: [2.0, 4.0], 65: [1.5, 5.0], 66: [1.0, 6.0], 67: [0.5, 7.0], 68: [0.0, 8.0], 69: [-0.5, 9.0], + 70: [-1.0, 10.0], 71: [3.5, 3.0], 72: [3.0, 4.0], 73: [2.5, 5.0], 74: [2.0, 6.0], 75: [1.5, 7.0], + 76: [1.0, 8.0], 77: [0.5, 9.0], 78: [0.0, 10.0], 79: [-1.0, 11.0] + }, + "village_2": {0: [0.0, 0.0], 1: [0.0, 1.0], 2: [-3.0, 2.0], 3: [-3.5, 3.0], 4: [-4.0, 4.0], 5: [-4.5, 5.0], + 6: [-5.0, 6.0], 7: [-5.5, 7.0], 8: [-6.0, 8.0], 9: [-6.5, 9.0], 10: [-7.0, 10.0], 11: [-2.5, 3.0], + 12: [-3.0, 4.0], 13: [-3.5, 5.0], 14: [-4.0, 6.0], 15: [-4.5, 7.0], 16: [-5.0, 8.0], + 17: [-5.5, 9.0], 18: [-6.0, 10.0], 19: [-7.0, 11.0], 20: [-1.0, 2.0], 21: [-1.5, 3.0], + 22: [-2.0, 4.0], 23: [-2.5, 5.0], 24: [-3.0, 6.0], 25: [-3.5, 7.0], 26: [-4.0, 8.0], + 27: [-4.5, 9.0], 28: [-5.0, 10.0], 29: [-5.5, 11.0], 30: [-6.0, 12.0], 31: [-6.5, 13.0], + 32: [-0.5, 3.0], 33: [-1.0, 4.0], 34: [-1.5, 5.0], 35: [-2.0, 6.0], 36: [-2.5, 7.0], + 37: [-3.0, 8.0], 38: [-3.5, 9.0], 39: [-4.0, 10.0], 40: [-4.5, 11.0], 41: [-5.0, 12.0], + 42: [-5.5, 13.0], 43: [-6.5, 14.0], 44: [1.0, 2.0], 45: [0.5, 3.0], 46: [0.0, 4.0], + 47: [-0.5, 5.0], 48: [-1.0, 6.0], 49: [1.5, 3.0], 50: [1.0, 4.0], 51: [0.5, 5.0], 52: [0.0, 6.0], + 53: [-1.0, 7.0], 54: [3.0, 2.0], 55: [2.5, 3.0], 56: [2.0, 4.0], 57: [1.5, 5.0], 58: [1.0, 6.0], + 59: [0.5, 7.0], 60: [0.0, 8.0], 61: [-0.5, 9.0], 62: [-1.0, 10.0], 63: [-1.5, 11.0], + 64: [3.5, 3.0], 65: [3.0, 4.0], 66: [2.5, 5.0], 67: [2.0, 6.0], 68: [1.5, 7.0], 69: [1.0, 8.0], + 70: [0.5, 9.0], 71: [0.0, 10.0], 72: [-0.5, 11.0], 73: [-1.5, 12.0] + }, + "suburb_1": {0: [0.0, 0.0], 1: [0.0, 1.0], 2: [-9.5, 2.0], 3: [-10.0, 3.0], 4: [-10.5, 4.0], 5: [-11.0, 5.0], + 6: [-11.5, 6.0], 7: [-12.0, 7.0], 8: [-12.5, 8.0], 9: [-13.0, 9.0], 10: [-13.5, 10.0], + 11: [-9.0, 3.0], 12: [-9.5, 4.0], 13: [-10.0, 5.0], 14: [-10.5, 6.0], 15: [-11.0, 7.0], + 16: [-11.5, 8.0], 17: [-12.0, 9.0], 18: [-12.5, 10.0], 19: [-13.5, 11.0], 20: [-7.5, 2.0], + 21: [-8.0, 3.0], 22: [-8.5, 4.0], 23: [-9.0, 5.0], 24: [-9.5, 6.0], 25: [-10.0, 7.0], + 26: [-10.5, 8.0], 27: [-11.0, 9.0], 28: [-11.5, 10.0], 29: [-12.0, 11.0], 30: [-12.5, 12.0], + 31: [-13.0, 13.0], 32: [-13.5, 14.0], 33: [-14.0, 15.0], 34: [-14.5, 16.0], 35: [-15.0, 17.0], + 36: [-15.5, 18.0], 37: [-7.0, 3.0], 38: [-7.5, 4.0], 39: [-8.0, 5.0], 40: [-8.5, 6.0], + 41: [-9.0, 7.0], 42: [-9.5, 8.0], 43: [-10.0, 9.0], 44: [-10.5, 10.0], 45: [-11.0, 11.0], + 46: [-11.5, 12.0], 47: [-12.0, 13.0], 48: [-12.5, 14.0], 49: [-13.0, 15.0], 50: [-13.5, 16.0], + 51: [-14.0, 17.0], 52: [-14.5, 18.0], 53: [-15.5, 19.0], 54: [-5.5, 2.0], 55: [-6.0, 3.0], + 56: [-6.5, 4.0], 57: [-7.0, 5.0], 58: [-7.5, 6.0], 59: [-5.0, 3.0], 60: [-5.5, 4.0], + 61: [-6.0, 5.0], 62: [-6.5, 6.0], 63: [-7.5, 7.0], 64: [-3.5, 2.0], 65: [-4.0, 3.0], + 66: [-4.5, 4.0], 67: [-5.0, 5.0], 68: [-5.5, 6.0], 69: [-6.0, 7.0], 70: [-6.5, 8.0], + 71: [-7.0, 9.0], 72: [-7.5, 10.0], 73: [-8.0, 11.0], 74: [-8.5, 12.0], 75: [-9.0, 13.0], + 76: [-9.5, 14.0], 77: [-3.0, 3.0], 78: [-3.5, 4.0], 79: [-4.0, 5.0], 80: [-4.5, 6.0], + 81: [-5.0, 7.0], 82: [-5.5, 8.0], 83: [-6.0, 9.0], 84: [-6.5, 10.0], 85: [-7.0, 11.0], + 86: [-7.5, 12.0], 87: [-8.0, 13.0], 88: [-8.5, 14.0], 89: [-9.5, 15.0], 90: [-1.5, 2.0], + 91: [-2.0, 3.0], 92: [-2.5, 4.0], 93: [-3.0, 5.0], 94: [-3.5, 6.0], 95: [-4.0, 7.0], + 96: [-4.5, 8.0], 97: [-5.0, 9.0], 98: [-1.0, 3.0], 99: [-1.5, 4.0], 100: [-2.0, 5.0], + 101: [-2.5, 6.0], 102: [-3.0, 7.0], 103: [-3.5, 8.0], 104: [-4.0, 9.0], 105: [-5.0, 10.0], + 106: [0.0, 2.0], 107: [0.0, 3.0], 108: [1.5, 2.0], 109: [1.0, 3.0], 110: [0.5, 4.0], + 111: [0.0, 5.0], 112: [-0.5, 6.0], 113: [-1.0, 7.0], 114: [-1.5, 8.0], 115: [-2.0, 9.0], + 116: [-2.5, 10.0], 117: [2.0, 3.0], 118: [1.5, 4.0], 119: [1.0, 5.0], 120: [0.5, 6.0], + 121: [0.0, 7.0], 122: [-0.5, 8.0], 123: [-1.0, 9.0], 124: [-1.5, 10.0], 125: [-2.5, 11.0], + 126: [3.5, 2.0], 127: [3.0, 3.0], 128: [2.5, 4.0], 129: [2.0, 5.0], 130: [1.5, 6.0], + 131: [1.0, 7.0], 132: [0.5, 8.0], 133: [0.0, 9.0], 134: [-0.5, 10.0], 135: [-1.0, 11.0], + 136: [-1.5, 12.0], 137: [-2.0, 13.0], 138: [-2.5, 14.0], 139: [-3.0, 15.0], 140: [-3.5, 16.0], + 141: [-4.0, 17.0], 142: [-4.5, 18.0], 143: [4.0, 3.0], 144: [3.5, 4.0], 145: [3.0, 5.0], + 146: [2.5, 6.0], 147: [2.0, 7.0], 148: [1.5, 8.0], 149: [1.0, 9.0], 150: [0.5, 10.0], + 151: [0.0, 11.0], 152: [-0.5, 12.0], 153: [-1.0, 13.0], 154: [-1.5, 14.0], 155: [-2.0, 15.0], + 156: [-2.5, 16.0], 157: [-3.0, 17.0], 158: [-3.5, 18.0], 159: [-4.5, 19.0], 160: [5.5, 2.0], + 161: [5.0, 3.0], 162: [4.5, 4.0], 163: [4.0, 5.0], 164: [3.5, 6.0], 165: [6.0, 3.0], + 166: [5.5, 4.0], 167: [5.0, 5.0], 168: [4.5, 6.0], 169: [3.5, 7.0], 170: [7.5, 2.0], + 171: [7.0, 3.0], 172: [6.5, 4.0], 173: [6.0, 5.0], 174: [5.5, 6.0], 175: [5.0, 7.0], + 176: [4.5, 8.0], 177: [4.0, 9.0], 178: [3.5, 10.0], 179: [3.0, 11.0], 180: [2.5, 12.0], + 181: [2.0, 13.0], 182: [1.5, 14.0], 183: [8.0, 3.0], 184: [7.5, 4.0], 185: [7.0, 5.0], + 186: [6.5, 6.0], 187: [6.0, 7.0], 188: [5.5, 8.0], 189: [5.0, 9.0], 190: [4.5, 10.0], + 191: [4.0, 11.0], 192: [3.5, 12.0], 193: [3.0, 13.0], 194: [2.5, 14.0], 195: [1.5, 15.0], + 196: [9.5, 2.0], 197: [9.0, 3.0], 198: [8.5, 4.0], 199: [8.0, 5.0], 200: [10.0, 3.0], + 201: [9.5, 4.0], 202: [9.0, 5.0], 203: [8.0, 6.0] + } } - net.bus_geodata = pd.read_json(io.StringIO(bus_geo[network_class])) - # Match bus.index - net.bus_geodata = net.bus_geodata.loc[net.bus.index] + net.bus.geo = net.bus.apply( + lambda row: dumps(Point(bus_geo[network_class][row.name])), + axis=1 + ) return net From 9b4cc4dc827de599dc90b74e361184460bff2b3f Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Thu, 31 Oct 2024 16:48:26 +0100 Subject: [PATCH 046/135] - coords will return numpy arrays - new methods that transform the geo column to GeoSeries and perform checks --- pandapower/create.py | 76 ++++++++++++++++++++++++++++-- pandapower/test/api/test_create.py | 10 ++-- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/pandapower/create.py b/pandapower/create.py index 7b3e161f4..f1f6d9e1a 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -19,6 +19,8 @@ from pandapower.std_types import add_basic_std_types, load_std_type import numpy as np from geojson import loads, GeoJSON +from shapely import from_geojson, Point +from geopandas import GeoSeries try: import pandaplan.core.pplog as logging @@ -48,21 +50,87 @@ def _validate(obj): @staticmethod def extract_coords(x): if x["type"] == "Point": - return tuple(x["coordinates"]) - return [tuple(y) for y in x["coordinates"]] + return np.array(x["coordinates"]) + return [np.array(y) for y in x["coordinates"]] @property - def coords(self): + def _coords(self): return self._obj.dropna().apply(loads).apply(self.extract_coords) @property - def as_geo_object(self): + def as_geo_obj(self): return self._obj.dropna().apply(loads) @property def type(self): return self._obj.dropna().apply(loads).apply(lambda x: str(x["type"])) + @property + def as_shapely_obj(self): + return self._obj.dropna().apply(from_geojson) + + @property + def as_geoseries(self): + return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326) + + # @staticmethod + # def _create_circle_polygon(center_point, radius, num_points=100): + # # Winkel in Grad für die Punkte auf dem Kreis + # angles = np.linspace(0, 360, num_points) + # circle_points = [] + # + # for angle in angles: + # # Berechne den Punkt am gegebenen Winkel und Radius + # point = geodesic(meters=radius).destination((center_point.y, center_point.x), angle) + # circle_points.append((point.longitude, point.latitude)) + # + # # Erstelle ein Polygon aus den Kreis-Punkten + # return Polygon(circle_points) + # + # @staticmethod + # def _get_in_circle(geom, circle_polygon): + # return geom.within(circle_polygon) or geom.intersects(circle_polygon) + # + # def in_radius(self, reference_point, radius_m): + # # 12,7 ms + # circle_polygon = self._create_circle_polygon(Point(reference_point), radius_m) + # return self._obj.dropna().apply(loads).apply(shape).apply(self._get_in_circle, args=(circle_polygon, )) + + # def within_radius(self, reference_point, radius_m): + # # 2,49 ms +- 625us + # circle_polygon = GeoSeries([Point(reference_point)], crs=4326).to_crs(epsg=31467).buffer(radius_m).iloc[0] + # geoms = GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, + # index=self._obj.dropna().index).to_crs(epsg=31467) + # return geoms.within(circle_polygon) | geoms.intersects(circle_polygon) + + def within_radius(self, reference_point, radius_m): + # 1,73 ms +- 31 us + circle_polygon = GeoSeries([Point(reference_point)], + crs=4326).to_crs(epsg=31467).buffer(radius_m).to_crs(epsg=4326).iloc[0] + geoms = GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj.dropna().index) + return geoms.within(circle_polygon) | geoms.intersects(circle_polygon) + + def within(self, other, **kwargs): + # other is shapely geometry + return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. + dropna().index).within(other=other, **kwargs) + + def intersects(self, other, **kwargs): + return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. + dropna().index).intersects(other=other, **kwargs) + + def contains(self, other, **kwargs): + return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. + dropna().index).contains(other=other, **kwargs) + + def overlaps(self, other, **kwargs): + return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. + dropna().index).overlaps(other=other, **kwargs) + + def buffer(self, distance, **kwargs): + return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. + dropna().index).buffer(distance=distance, **kwargs) + def create_empty_network(name="", f_hz=50., sn_mva=1, add_stdtypes=True): """ diff --git a/pandapower/test/api/test_create.py b/pandapower/test/api/test_create.py index 73bfc8f84..f16402393 100644 --- a/pandapower/test/api/test_create.py +++ b/pandapower/test/api/test_create.py @@ -31,11 +31,11 @@ def test_geo_accessor(): ) pp.create_line(net, b1, b2, 1.5, std_type="48-AL1/8-ST1A 10.0") - assert len(net.line.geo.geojson.coords) == 2 - assert net.line.geo.geojson.coords.at[l[0]] == [(1, 1), (2, 2), (3, 3)] - assert net.line.geo.geojson.coords.at[l[1]] == [(1, 1), (1, 2)] - assert net.bus.geo.geojson.coords.at[b1] == (1, 1) - assert net.bus.geo.geojson.coords.at[b2] == (2, 2) + assert len(net.line.geo.geojson._coords) == 2 + assert np.array_equal(net.line.geo.geojson._coords.at[l[0]], [[1, 1], [2, 2], [3, 3]]) + assert np.array_equal(net.line.geo.geojson._coords.at[l[1]], [[1, 1], [1, 2]]) + assert np.array_equal(net.bus.geo.geojson._coords.at[b1], [1, 1]) + assert np.array_equal(net.bus.geo.geojson._coords.at[b2], [2, 2]) assert net.bus.geo.geojson.type.at[b1] == "Point" assert net.bus.geo.geojson.type.at[b2] == "Point" assert net.line.geo.geojson.type.at[l[0]] == "LineString" From 8ac72b8bc232e5b6a9070133af0d60982ae5b21b Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Fri, 1 Nov 2024 12:05:51 +0100 Subject: [PATCH 047/135] - shifted accessor class from create to auxiliary - added __getattr__ to access GeoSeries methods and attributes directly from .geojson --- pandapower/auxiliary.py | 96 +++++++++++++++++++++++ pandapower/create.py | 105 -------------------------- pandapower/test/api/test_auxiliary.py | 46 +++++++++++ pandapower/test/api/test_create.py | 29 ------- pyproject.toml | 2 +- 5 files changed, 143 insertions(+), 135 deletions(-) diff --git a/pandapower/auxiliary.py b/pandapower/auxiliary.py index b421cd5f7..1068847ad 100644 --- a/pandapower/auxiliary.py +++ b/pandapower/auxiliary.py @@ -33,6 +33,7 @@ from importlib.metadata import PackageNotFoundError from typing_extensions import deprecated +from geojson import loads, GeoJSON import numpy as np import pandas as pd import scipy as sp @@ -58,9 +59,16 @@ import pandaplan.core.pplog as logging except ImportError: import logging +try: + from geopandas import GeoSeries + from shapely import from_geojson, Point + geopandas_available = True +except ImportError: + geopandas_available = False logger = logging.getLogger(__name__) + def log_to_level(msg, passed_logger, level): if level == "error": passed_logger.error(msg) @@ -343,6 +351,94 @@ def __repr__(self): # pragma: no cover return "\n".join(lines) +@pd.api.extensions.register_series_accessor("geojson") +class GeoAccessor: + """ + pandas Series accessor for the geo column. It facilitates the use of geojson strings. + NaN entrys are dropped using the accessor! + """ + def __init__(self, pandas_obj): + self._validate(pandas_obj) + self._obj = pandas_obj + + @staticmethod + def _validate(obj): + try: + if not obj.dropna().apply(loads).apply(isinstance, args=(GeoJSON,)).all(): + raise AttributeError("Can only use .geojson accessor with geojson string values!") + except Exception as e: + raise AttributeError(f"Can only use .geojson accessor with geojson string values!: {e}") + if not geopandas_available: + soft_dependency_error("GeoAccessor", "geopandas") + + @staticmethod + def _extract_coords(x): + if x["type"] == "Point": + return np.array(x["coordinates"]) + return [np.array(y) for y in x["coordinates"]] + + @property + def _coords(self): + """ + Extracts the geometry coordinates from the GeoJSON strings. + It is not recommended to use the standalone coordinates. + Important informations like the crs or latlon/lonlat are lost as a result. + """ + return self._obj.dropna().apply(loads).apply(self._extract_coords) + + @property + def as_geo_obj(self): + """ + Loads the GeoJSON objects. + """ + return self._obj.dropna().apply(loads) + + @property + def type(self): + """ + Extracts the geometry type of the GeoJSON string. + """ + return self._obj.dropna().apply(loads).apply(lambda x: str(x["type"])) + + @property + def as_shapely_obj(self): + """ + Converts the GeoJSON strings to shapely geometrys. + """ + return self._obj.dropna().apply(from_geojson) + + @property + def as_geoseries(self): + """ + Converts the PandasSeries to a GeoSeries with shapely geometrys. + """ + return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj.dropna().index) + + def within_radius(self, reference_point, radius_m): + """ + Returns a Series with booleans, if geometry of geojson is in radius of the reference point. + """ + circle_polygon = GeoSeries([Point(reference_point)], + crs=4326).to_crs(epsg=31467).buffer(radius_m).to_crs(epsg=4326).iloc[0] + geoms = self.as_geoseries + return geoms.within(circle_polygon) | geoms.intersects(circle_polygon) + + def __getattr__(self, item): + """ + Enables access to all methods or attribute calls from a GeoSeries. + """ + geoms = self.as_geoseries + if hasattr(geoms, item): + geoms_item = getattr(geoms, item) + if callable(geoms_item): + def wrapper(*args, **kwargs): + return geoms_item(*args, **kwargs) + return wrapper + else: + return geoms_item + raise AttributeError(f"'GeoAccessor' object has no attribute '{item}'") + + def plural_s(number): return "" if number == 1 else "s" diff --git a/pandapower/create.py b/pandapower/create.py index f1f6d9e1a..7e98c7954 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -18,9 +18,6 @@ from pandapower.results import reset_results from pandapower.std_types import add_basic_std_types, load_std_type import numpy as np -from geojson import loads, GeoJSON -from shapely import from_geojson, Point -from geopandas import GeoSeries try: import pandaplan.core.pplog as logging @@ -30,108 +27,6 @@ logger = logging.getLogger(__name__) -@pd.api.extensions.register_series_accessor("geojson") -class GeoAccessor: - """ - pandas Series accessor for the geo column. It facilitates the use of geojson strings. - """ - def __init__(self, pandas_obj): - self._validate(pandas_obj) - self._obj = pandas_obj - - @staticmethod - def _validate(obj): - try: - if not obj.dropna().apply(loads).apply(isinstance, args=(GeoJSON,)).all(): - raise AttributeError("Can only use .geojson accessor with geojson string values!") - except Exception as e: - raise AttributeError(f"Can only use .geojson accessor with geojson string values!: {e}") - - @staticmethod - def extract_coords(x): - if x["type"] == "Point": - return np.array(x["coordinates"]) - return [np.array(y) for y in x["coordinates"]] - - @property - def _coords(self): - return self._obj.dropna().apply(loads).apply(self.extract_coords) - - @property - def as_geo_obj(self): - return self._obj.dropna().apply(loads) - - @property - def type(self): - return self._obj.dropna().apply(loads).apply(lambda x: str(x["type"])) - - @property - def as_shapely_obj(self): - return self._obj.dropna().apply(from_geojson) - - @property - def as_geoseries(self): - return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326) - - # @staticmethod - # def _create_circle_polygon(center_point, radius, num_points=100): - # # Winkel in Grad für die Punkte auf dem Kreis - # angles = np.linspace(0, 360, num_points) - # circle_points = [] - # - # for angle in angles: - # # Berechne den Punkt am gegebenen Winkel und Radius - # point = geodesic(meters=radius).destination((center_point.y, center_point.x), angle) - # circle_points.append((point.longitude, point.latitude)) - # - # # Erstelle ein Polygon aus den Kreis-Punkten - # return Polygon(circle_points) - # - # @staticmethod - # def _get_in_circle(geom, circle_polygon): - # return geom.within(circle_polygon) or geom.intersects(circle_polygon) - # - # def in_radius(self, reference_point, radius_m): - # # 12,7 ms - # circle_polygon = self._create_circle_polygon(Point(reference_point), radius_m) - # return self._obj.dropna().apply(loads).apply(shape).apply(self._get_in_circle, args=(circle_polygon, )) - - # def within_radius(self, reference_point, radius_m): - # # 2,49 ms +- 625us - # circle_polygon = GeoSeries([Point(reference_point)], crs=4326).to_crs(epsg=31467).buffer(radius_m).iloc[0] - # geoms = GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, - # index=self._obj.dropna().index).to_crs(epsg=31467) - # return geoms.within(circle_polygon) | geoms.intersects(circle_polygon) - - def within_radius(self, reference_point, radius_m): - # 1,73 ms +- 31 us - circle_polygon = GeoSeries([Point(reference_point)], - crs=4326).to_crs(epsg=31467).buffer(radius_m).to_crs(epsg=4326).iloc[0] - geoms = GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj.dropna().index) - return geoms.within(circle_polygon) | geoms.intersects(circle_polygon) - - def within(self, other, **kwargs): - # other is shapely geometry - return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. - dropna().index).within(other=other, **kwargs) - - def intersects(self, other, **kwargs): - return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. - dropna().index).intersects(other=other, **kwargs) - - def contains(self, other, **kwargs): - return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. - dropna().index).contains(other=other, **kwargs) - - def overlaps(self, other, **kwargs): - return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. - dropna().index).overlaps(other=other, **kwargs) - - def buffer(self, distance, **kwargs): - return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj. - dropna().index).buffer(distance=distance, **kwargs) - - def create_empty_network(name="", f_hz=50., sn_mva=1, add_stdtypes=True): """ This function initializes the pandapower datastructure. diff --git a/pandapower/test/api/test_auxiliary.py b/pandapower/test/api/test_auxiliary.py index be6c60a24..be0369a93 100644 --- a/pandapower/test/api/test_auxiliary.py +++ b/pandapower/test/api/test_auxiliary.py @@ -303,6 +303,7 @@ def test_characteristic(file_io): with pytest.raises(NotImplementedError): c3([0]) + def test_log_characteristic_property(): net = pp.create_empty_network() c = LogSplineCharacteristic(net, [10, 1000, 10000], [1000, 0.1, 0.001], interpolator_kind="Pchip", extrapolate=False) @@ -310,6 +311,51 @@ def test_log_characteristic_property(): c([2]) +def test_geo_accessor_geojson(): + net = pp.create_empty_network() + b1 = pp.create_bus(net, 10, geodata=(1, 1)) + b2 = pp.create_bus(net, 10, geodata=(2, 2)) + l = pp.create_lines( + net, + [b1, b1], + [b2, b2], + [1.5, 3], + std_type="48-AL1/8-ST1A 10.0", + geodata=[[(1, 1), (2, 2), (3, 3)], [(1, 1), (1, 2)]], + ) + pp.create_line(net, b1, b2, 1.5, std_type="48-AL1/8-ST1A 10.0") + + assert len(net.line.geo.geojson._coords) == 2 + assert np.array_equal(net.line.geo.geojson._coords.at[l[0]], [[1, 1], [2, 2], [3, 3]]) + assert np.array_equal(net.line.geo.geojson._coords.at[l[1]], [[1, 1], [1, 2]]) + assert np.array_equal(net.bus.geo.geojson._coords.at[b1], [1, 1]) + assert np.array_equal(net.bus.geo.geojson._coords.at[b2], [2, 2]) + assert net.bus.geo.geojson.type.at[b1] == "Point" + assert net.bus.geo.geojson.type.at[b2] == "Point" + assert net.line.geo.geojson.type.at[l[0]] == "LineString" + assert net.line.geo.geojson.type.at[l[1]] == "LineString" + assert set(net.line.geo.geojson.as_geo_obj.at[l[0]].keys()) == {"coordinates", "type"} + assert set(net.line.geo.geojson.as_geo_obj.at[l[1]].keys()) == {"coordinates", "type"} + assert set(net.bus.geo.geojson.as_geo_obj.at[b1].keys()) == {"coordinates", "type"} + assert set(net.bus.geo.geojson.as_geo_obj.at[b2].keys()) == {"coordinates", "type"} + + +@pytest.mark.skipif(not GEOPANDAS_INSTALLED, reason="geopandas is not installed") +def test_geo_accessor_geopandas(): + net = pp.networks.mv_oberrhein() + reference_point = shapely.geometry.Point((7.8947079593416, 48.40549007606241)) + radius_m = 100 + assert net.line.geo.geojson.within_radius(reference_point, radius_m)[:3].all() == True + assert net.line.geo.geojson.within_radius(reference_point, radius_m)[3:].any() == False + + line = shapely.geometry.LineString([[7.8947079593416, 48.40549007606241], + [7.896048283667894, 48.41060722903666], + [7.896173712216692, 48.41100311474432]]) + + assert net.line.geo.geojson.as_shapely_obj.at[0] == line + assert np.allclose(net.line.geo.geojson.total_bounds, [7.74426069, 48.32845845, 7.93829196, 48.47484423]) + assert net.line.geo.geojson.intersects(reference_point)[:3].all() == True + assert net.line.geo.geojson.intersects(reference_point)[3:].any() == False if __name__ == '__main__': diff --git a/pandapower/test/api/test_create.py b/pandapower/test/api/test_create.py index f16402393..1e1ed8dd7 100644 --- a/pandapower/test/api/test_create.py +++ b/pandapower/test/api/test_create.py @@ -17,35 +17,6 @@ pd.set_option("display.width", 1000) -def test_geo_accessor(): - net = pp.create_empty_network() - b1 = pp.create_bus(net, 10, geodata=(1, 1)) - b2 = pp.create_bus(net, 10, geodata=(2, 2)) - l = pp.create_lines( - net, - [b1, b1], - [b2, b2], - [1.5, 3], - std_type="48-AL1/8-ST1A 10.0", - geodata=[[(1, 1), (2, 2), (3, 3)], [(1, 1), (1, 2)]], - ) - pp.create_line(net, b1, b2, 1.5, std_type="48-AL1/8-ST1A 10.0") - - assert len(net.line.geo.geojson._coords) == 2 - assert np.array_equal(net.line.geo.geojson._coords.at[l[0]], [[1, 1], [2, 2], [3, 3]]) - assert np.array_equal(net.line.geo.geojson._coords.at[l[1]], [[1, 1], [1, 2]]) - assert np.array_equal(net.bus.geo.geojson._coords.at[b1], [1, 1]) - assert np.array_equal(net.bus.geo.geojson._coords.at[b2], [2, 2]) - assert net.bus.geo.geojson.type.at[b1] == "Point" - assert net.bus.geo.geojson.type.at[b2] == "Point" - assert net.line.geo.geojson.type.at[l[0]] == "LineString" - assert net.line.geo.geojson.type.at[l[1]] == "LineString" - assert set(net.line.geo.geojson.as_geo_object.at[l[0]].keys()) == {"coordinates", "type"} - assert set(net.line.geo.geojson.as_geo_object.at[l[1]].keys()) == {"coordinates", "type"} - assert set(net.bus.geo.geojson.as_geo_object.at[b1].keys()) == {"coordinates", "type"} - assert set(net.bus.geo.geojson.as_geo_object.at[b2].keys()) == {"coordinates", "type"} - - def test_convenience_create_functions(): net = pp.create_empty_network() b1 = pp.create_bus(net, 110.0) diff --git a/pyproject.toml b/pyproject.toml index 77234e152..98b08cea7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ docs = ["numpydoc", "matplotlib", "sphinx", "sphinx_rtd_theme", "sphinx-pyprojec plotting = ["plotly>=3.1.1", "matplotlib", "igraph", "geopandas>=1.0"] test = ["pytest~=8.1", "pytest-xdist", "nbmake"] performance = ["ortools", "numba>=0.25", "lightsim2grid==0.9.0"] -fileio = ["xlsxwriter", "openpyxl", "cryptography", "geopandas", "psycopg2"] +fileio = ["xlsxwriter", "openpyxl", "cryptography", "geopandas>=1.0", "psycopg2"] converter = ["matpowercaseframes"] pgm = ["power-grid-model-io"] control = ["shapely"] From 8870dd68c37329d5c00a77b7fe9aa14c4692c805 Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Mon, 4 Nov 2024 07:40:49 +0100 Subject: [PATCH 048/135] fix failing tests due to the precision changes --- pandapower/test/plotting/test_geo.py | 58 +++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/pandapower/test/plotting/test_geo.py b/pandapower/test/plotting/test_geo.py index 2559bb0a8..a3aac1c1e 100644 --- a/pandapower/test/plotting/test_geo.py +++ b/pandapower/test/plotting/test_geo.py @@ -10,6 +10,7 @@ import geojson import pandas as pd import pytest +from pandas.testing import assert_frame_equal, assert_index_equal import pandapower.plotting.geo as geo from pandapower.test.helper_functions import create_test_network @@ -54,30 +55,48 @@ def get_network_and_result(net, request): def test__node_geometries_from_geodata(get_network_and_result): pytest.importorskip("geopandas") - from geopandas import testing _net, expected = get_network_and_result _bus_geojson_to_geodata_(_net) result = geo._node_geometries_from_geodata(_net.bus_geodata) - testing.assert_geodataframe_equal(result, expected) + # is mostly the same as assert_geodataframe_equal with check_less_precise=True, but the tolerance in the function + # can't be adapted + assert result.shape == expected.shape + assert isinstance(result, type(expected)) + assert (result.geom_equals_exact(expected.geometry, tolerance=1 * 10 ** (-6)) | + (result.geometry.is_empty & expected.geometry.is_empty) | + (result.geometry.isna() & expected.geometry.isna())).all() + left2 = result.select_dtypes(exclude="geometry") + right2 = expected.select_dtypes(exclude="geometry") + assert_index_equal(result.columns, expected.columns, exact="equiv", obj="GeoDataFrame.columns") + assert_frame_equal(left2, right2, check_dtype=True, check_index_type="equiv", check_column_type="equiv", obj="GeoDataFrame") def test__branch_geometries_from_geodata(get_network_and_result): pytest.importorskip("geopandas") - from geopandas import testing _net, expected = get_network_and_result _line_geojson_to_geodata_(_net) result = geo._branch_geometries_from_geodata(_net.line_geodata) - testing.assert_geodataframe_equal(result, expected) + # is mostly the same as assert_geodataframe_equal with check_less_precise=True, but the tolerance in the function + # can't be adapted + assert result.shape == expected.shape + assert isinstance(result, type(expected)) + assert (result.geom_equals_exact(expected.geometry, tolerance=1 * 10 ** (-6)) | + (result.geometry.is_empty & expected.geometry.is_empty) | + (result.geometry.isna() & expected.geometry.isna())).all() + left2 = result.select_dtypes(exclude="geometry") + right2 = expected.select_dtypes(exclude="geometry") + assert_index_equal(result.columns, expected.columns, exact="equiv", obj="GeoDataFrame.columns") + assert_frame_equal(left2, right2, check_dtype=True, check_index_type="equiv", check_column_type="equiv", + obj="GeoDataFrame") def test__transform_node_geometry_to_geodata(get_network_and_result): pytest.importorskip("geopandas") - from geopandas import testing _net, expected = get_network_and_result _bus_geojson_to_geodata_(_net) @@ -85,19 +104,40 @@ def test__transform_node_geometry_to_geodata(get_network_and_result): # Transforming to geodata to test the inverse... _net.bus_geodata = geo._node_geometries_from_geodata(_net.bus_geodata) result = geo._transform_node_geometry_to_geodata(_net.bus_geodata) - testing.assert_geodataframe_equal(result, expected) + # is mostly the same as assert_geodataframe_equal with check_less_precise=True, but the tolerance in the function + # can't be adapted + assert result.shape == expected.shape + assert isinstance(result, type(expected)) + assert (result.geom_equals_exact(expected.geometry, tolerance=1 * 10 ** (-6)) | + (result.geometry.is_empty & expected.geometry.is_empty) | + (result.geometry.isna() & expected.geometry.isna())).all() + left2 = result.select_dtypes(exclude="geometry") + right2 = expected.select_dtypes(exclude="geometry") + assert_index_equal(result.columns, expected.columns, exact="equiv", obj="GeoDataFrame.columns") + assert_frame_equal(left2, right2, check_dtype=True, check_index_type="equiv", check_column_type="equiv", + obj="GeoDataFrame") def test__transform_branch_geometry_to_coords(get_network_and_result): pytest.importorskip("geopandas") - from geopandas import testing _net, expected = get_network_and_result _line_geojson_to_geodata_(_net) _net.line_geodata = geo._branch_geometries_from_geodata(_net.line_geodata) result = geo._transform_branch_geometry_to_coords(_net.line_geodata) - testing.assert_geodataframe_equal(result, expected) + # is mostly the same as assert_geodataframe_equal with check_less_precise=True, but the tolerance in the function + # can't be adapted + assert result.shape == expected.shape + assert isinstance(result, type(expected)) + assert (result.geom_equals_exact(expected.geometry, tolerance=1 * 10 ** (-6)) | + (result.geometry.is_empty & expected.geometry.is_empty) | + (result.geometry.isna() & expected.geometry.isna())).all() + left2 = result.select_dtypes(exclude="geometry") + right2 = expected.select_dtypes(exclude="geometry") + assert_index_equal(result.columns, expected.columns, exact="equiv", obj="GeoDataFrame.columns") + assert_frame_equal(left2, right2, check_dtype=True, check_index_type="equiv", check_column_type="equiv", + obj="GeoDataFrame") def test__convert_xy_epsg(): @@ -261,7 +301,6 @@ def test_dump_to_geojson(): assert dumps(result, sort_keys=True) == '{"features": [{"geometry": {"coordinates": [[1.0, 2.0], [3.0, 4.0]], "type": "LineString"}, "id": "line-0", "properties": {"c_nf_per_km": 720.0, "df": 1.0, "from_bus": 1, "g_us_per_km": 0.0, "i_from_ka": 7.0, "i_ka": 7.0, "i_to_ka": 7.0, "ices": 0.389985, "in_service": true, "length_km": 1.0, "loading_percent": 7.0, "max_i_ka": 0.328, "name": "line1", "p_from_mw": 7.0, "p_to_mw": 7.0, "parallel": 1, "pl_mw": 7.0, "pp_index": 0, "pp_type": "line", "q_from_mvar": 7.0, "q_to_mvar": 7.0, "ql_mvar": 7.0, "r_ohm_per_km": 0.2067, "std_type": null, "to_bus": 7, "type": null, "va_from_degree": 7.0, "va_to_degree": 7.0, "vm_from_pu": 7.0, "vm_to_pu": 7.0, "x_ohm_per_km": 0.1897522}, "type": "Feature"}], "type": "FeatureCollection"}' - def test_convert_geodata_to_geojson(): pytest.importorskip("geojson") pytest.importorskip("pandapower") @@ -295,6 +334,5 @@ def test_convert_gis_to_geojson(): pytest.skip("Not implemented") - if __name__ == "__main__": pytest.main(["test_geo.py"]) From df7e99f4478f9bf95fd3c5b27b4100f242e12c33 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 6 Nov 2024 14:27:13 +0100 Subject: [PATCH 049/135] fix lengths missmatch of output of coords_from_node_geodata() --- CHANGELOG.rst | 1 + pandapower/plotting/plotting_toolbox.py | 30 ++++++++++++++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fc94fec50..98977dd20 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -87,6 +87,7 @@ Change Log - [CHANGED] accelerate distributed slack power flow calculation by using sparse-aware operations in _subnetworks() - [CHANGED] Trafo Controllers can now be added to elements that are out of service, changed self.nothing_to_do() - [ADDED] Discrete shunt controller for local voltage regulation with shunt steps +- [ADDED] fix lengths missmatch of output if ignore_zero_length is False in plotting utility function coords_from_node_geodata() and rename ignore_zero_length by ignore_no_geo_diff - [ADDED] cim2pp converter: Using lxml to parse XML files (better performance) [2.14.7] - 2024-06-14 diff --git a/pandapower/plotting/plotting_toolbox.py b/pandapower/plotting/plotting_toolbox.py index 11d19a39a..39cfc127d 100644 --- a/pandapower/plotting/plotting_toolbox.py +++ b/pandapower/plotting/plotting_toolbox.py @@ -144,7 +144,7 @@ def get_index_array(indices, net_table_indices): def coords_from_node_geodata(element_indices, from_nodes, to_nodes, node_geodata, table_name, - node_name="Bus", ignore_zero_length=True, node_geodata_to=None): + node_name="Bus", ignore_no_geo_diff=True, node_geodata_to=None): """ Auxiliary function to get the node coordinates for a number of branches with respective from and to nodes. The branch elements for which there is no geodata available are not included in @@ -162,9 +162,9 @@ def coords_from_node_geodata(element_indices, from_nodes, to_nodes, node_geodata :type table_name: str :param node_name: Name of the node type (only for logging) :type node_name: str, default "Bus" - :param ignore_zero_length: States if branches should be left out, if their length is zero, i.e. \ + :param ignore_no_geo_diff: States if branches should be left out, if their length is zero, i.e. \ from_node_coords = to_node_coords - :type ignore_zero_length: bool, default True + :type ignore_no_geo_diff: bool, default True :param node_geodata_to: Dataframe containing x and y coordinates of the "to" nodes (optional, default node_geodata) :type node_geodata_to: pd.DataFrame :return: Return values are:\ @@ -192,15 +192,19 @@ def coords_from_node_geodata(element_indices, from_nodes, to_nodes, node_geodata ) node_geodata = node_geodata.apply(_get_coords_from_geojson) - coords = [f'{{"coordinates": [[{x_from}, {y_from}], [{x_to}, {y_to}]], "type": "LineString"}}' - for [x_from, y_from], [x_to, y_to] - in zip(node_geodata.loc[fb_with_geo[not_nan]], - node_geodata.loc[tb_with_geo[not_nan]]) - if not ignore_zero_length or (ignore_zero_length and not (x_from == x_to and y_from == y_to))] - return coords, np.array(element_indices)[in_geo & not_nan] - - -def set_line_geodata_from_bus_geodata(net, line_index=None, overwrite=False, ignore_zero_length=True): + coords, no_geo_diff = zip(*[ + (f'{{"coordinates": [[{x_from}, {y_from}], [{x_to}, {y_to}]], "type": "LineString"}}', + x_from == x_to and y_from == y_to) for [x_from, y_from], [x_to, y_to] in zip( + node_geodata.loc[fb_with_geo[not_nan]], node_geodata.loc[tb_with_geo[not_nan]])]) + # if not ignore_no_geo_diff or (ignore_no_geo_diff and not ())] + coords, no_geo_diff = np.array(coords), np.array(no_geo_diff) + if ignore_no_geo_diff: + return coords, np.array(element_indices)[in_geo & not_nan] + else: + return coords[~no_geo_diff], np.array(element_indices)[in_geo & not_nan][~no_geo_diff] + + +def set_line_geodata_from_bus_geodata(net, line_index=None, overwrite=False, ignore_no_geo_diff=True): """ Sets coordinates in net.line.geo based on the from_bus and to_bus coordinates in net.bus.geo @@ -227,7 +231,7 @@ def set_line_geodata_from_bus_geodata(net, line_index=None, overwrite=False, ign node_geodata=net.bus.geo, table_name="line", node_name="bus", - ignore_zero_length=ignore_zero_length) + ignore_no_geo_diff=ignore_no_geo_diff) net.line.loc[line_index_successful, 'geo'] = geos From e52a02961a9489a61167e3757995e6f9b8a3f9c8 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Thu, 7 Nov 2024 08:24:47 +0100 Subject: [PATCH 050/135] tiny fix due to renamed parameter --- pandapower/plotting/collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/plotting/collections.py b/pandapower/plotting/collections.py index 04f77cd5e..e060c1d2c 100644 --- a/pandapower/plotting/collections.py +++ b/pandapower/plotting/collections.py @@ -551,7 +551,7 @@ def create_line_collection(net: pandapowerNet, lines=None, node_geodata=net.bus.geo, table_name="line", node_name="bus", - ignore_zero_length=True) + ignore_no_geo_diff=True) line_geodata = line_geodata.combine_first(pd.Series(geos, index=line_index_successful)) From 1058f2e182595d6be7858c0121d0b6e4a6ac4e70 Mon Sep 17 00:00:00 2001 From: Jakob Kirschner Date: Thu, 7 Nov 2024 14:10:40 +0100 Subject: [PATCH 051/135] fix bt_pu in cim series compensators --- .../converter_classes/impedance/seriesCompensatorsCim16.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/converter/cim/cim2pp/converter_classes/impedance/seriesCompensatorsCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/impedance/seriesCompensatorsCim16.py index 030e75701..824e568dd 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/impedance/seriesCompensatorsCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/impedance/seriesCompensatorsCim16.py @@ -107,7 +107,7 @@ def _prepare_series_compensators_cim16(self) -> pd.DataFrame: ser_comp['gf_pu'] = 0. ser_comp['bf_pu'] = 0. ser_comp['gt_pu'] = 0. - ser_comp['bf_pu'] = 0. + ser_comp['bt_pu'] = 0. ser_comp['in_service'] = ser_comp.connected & ser_comp.connected2 ser_comp = ser_comp.rename(columns={'rdfId_Terminal': sc['t_from'], 'rdfId_Terminal2': sc['t_to'], 'rdfId': sc['o_id'], 'index_bus': 'from_bus', 'index_bus2': 'to_bus'}) From f7d9b7879bfc3c1aa03e8313529202f8f3cce9a8 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Thu, 7 Nov 2024 17:23:44 +0100 Subject: [PATCH 052/135] jao converter --- CHANGELOG.rst | 1 + pandapower/converter/__init__.py | 1 + pandapower/converter/jao/__init__.py | 1 + pandapower/converter/jao/from_jao.py | 987 ++++++++++++++++++ .../converter/jao_testfiles/testfile.xlsx | Bin 0 -> 76698 bytes pandapower/test/converter/test_from_jao.py | 60 ++ 6 files changed, 1050 insertions(+) create mode 100644 pandapower/converter/jao/__init__.py create mode 100644 pandapower/converter/jao/from_jao.py create mode 100644 pandapower/test/converter/jao_testfiles/testfile.xlsx create mode 100644 pandapower/test/converter/test_from_jao.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 98977dd20..a4dcba2a9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -88,6 +88,7 @@ Change Log - [CHANGED] Trafo Controllers can now be added to elements that are out of service, changed self.nothing_to_do() - [ADDED] Discrete shunt controller for local voltage regulation with shunt steps - [ADDED] fix lengths missmatch of output if ignore_zero_length is False in plotting utility function coords_from_node_geodata() and rename ignore_zero_length by ignore_no_geo_diff +- [ADDED] converter for European EHV grid data from JAO, the "Single Allocation Platform (SAP) for all European Transmission System Operators (TSOs) that operate in accordance to EU legislation" - [ADDED] cim2pp converter: Using lxml to parse XML files (better performance) [2.14.7] - 2024-06-14 diff --git a/pandapower/converter/__init__.py b/pandapower/converter/__init__.py index acdf19579..5df505f2a 100644 --- a/pandapower/converter/__init__.py +++ b/pandapower/converter/__init__.py @@ -3,3 +3,4 @@ from pandapower.converter.pandamodels import * from pandapower.converter.cim import * from pandapower.converter.powerfactory import * +from pandapower.converter.jao import * diff --git a/pandapower/converter/jao/__init__.py b/pandapower/converter/jao/__init__.py new file mode 100644 index 000000000..2aae42afa --- /dev/null +++ b/pandapower/converter/jao/__init__.py @@ -0,0 +1 @@ +from .from_jao import from_jao \ No newline at end of file diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py new file mode 100644 index 000000000..9ce6b05bf --- /dev/null +++ b/pandapower/converter/jao/from_jao.py @@ -0,0 +1,987 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2024 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +from copy import deepcopy +import os +import json +from functools import reduce +import numpy as np +import pandas as pd +from pandas.api.types import is_integer_dtype, is_object_dtype +from pandapower.io_utils import pandapowerNet +from pandapower.create import create_empty_network, create_buses, create_lines_from_parameters, \ + create_transformers_from_parameters +from pandapower.topology import create_nxgraph, connected_components +from pandapower.plotting import set_line_geodata_from_bus_geodata +from pandapower.toolbox import drop_buses, fuse_buses + +try: + import pandaplan.core.pplog as logging +except ImportError: + import logging + +logger = logging.getLogger(__name__) + + +def from_jao(excel_file_path:str, + html_file_path: str|None, + extend_data_for_grid_group_connections: bool, + drop_grid_groups_islands: bool = False, + apply_data_correction: bool = True, + **kwargs) -> pandapowerNet: + """Converts European (Core) EHV grid data provided by JAO, the "Single Allocation Platform (SAP) + for all European Transmission System Operators (TSOs) that operate in accordance to EU + legislation". At least in November 2024, the data are available at the website + https://www.jao.eu/static-grid-model . There, a map is provided to get an fine overview of the + geographical extent and the scope of the data. These inlcude information about European (Core) + lines, tielines, and transformers. No information is available on load or generation. + The data quality with regard to the interconnection of the equipment, the information provided + and the (incomplete) geodata should be considered with caution. + + Parameters + ---------- + excel_file_path : str + input data including electrical parameters of grids' utilities, stored in multiple sheets + of an excel file + html_file_path : str + input data for geo information. If The converter should be run without geo information, None + can be passed., provided by an html file + extend_data_for_grid_group_connections : bool + if True, connections (additional transformers and merging buses) are created to avoid + islanded grid groups, by default False + drop_grid_groups_islands : bool, optional + if True, islanded grid groups will be dropped if their number of buses is below + min_bus_number (default is 6), by default False + apply_data_correction : bool, optional + _description_, by default True + + Returns + ------- + pandapowerNet + _description_ + + Additional Parameters + --------------------- + minimal_trafo_invention : bool, optional + applies if extend_data_for_grid_group_connections is True. Then, if minimal_trafo_invention + is True, adding transformers stops when no grid groups is islanded anymore (does not apply + for release version 5 or 6, i.e. it does not care what value is passed to + minimal_trafo_invention). If False, all equally named buses that have different voltage + level and lay in different groups will be connected via additional transformers, + by default False + min_bus_number : int|str, optional + Threshold value to decide which small grid groups should be dropped and which large grid + groups should be kept. If all islanded grid groups should be dropped except of the one + largest, set "max". If all grid groups that do not contain a slack element should be + dropped, set "unsupplied". By default 6 + rel_deviation_threshold_for_trafo_bus_creation : float, optional + If the voltage level of transformer locations is far different than the transformer data, + additional buses are created. rel_deviation_threshold_for_trafo_bus_creation defines the + tolerance in which no additional buses are created. By default 0.2 + log_rel_vn_deviation : float, optional + This parameter allows a range below rel_deviation_threshold_for_trafo_bus_creation in which + a warning is logged instead of a creating additional buses. By default 0.12 + + """ + + # --- read data + data = pd.read_excel(excel_file_path, sheet_name=None, header=[0, 1]) + if html_file_path is not None: + with open(html_file_path, mode='r', encoding=kwargs.get("encoding", "utf-8")) as f: + html_str = f.read() + else: + html_str = "" + + # --- manipulate data / data corrections + if apply_data_correction: + html_str = _data_correction(data, html_str) + + # --- parse html_str to line_geo_data + line_geo_data = None + if html_str: + try: + line_geo_data = _parse_html_str(html_str) + except (json.JSONDecodeError, KeyError, AssertionError) as e: + logger.error(f"html data were ignored due to this error:\n{e}") + + # --- create the pandapower net + net = create_empty_network(name=os.path.splitext(os.path.basename(excel_file_path))[0], + **{key: val for key, val in kwargs.items() if key == "sn_mva"}) + _create_buses_from_line_data(net, data) + _create_lines(net, data) + _create_transformers_and_buses(net, data, **kwargs) + + # --- invent connections between grid groups + if extend_data_for_grid_group_connections: + _invent_connections_between_grid_groups(net, **kwargs) + + # --- drop islanded grid groups + if drop_grid_groups_islands: + drop_islanded_grid_groups(net, kwargs.get("min_bus_number", 6)) + + # --- add geo data to buses and lines + if line_geo_data is not None: + _add_bus_geo(net, line_geo_data) + set_line_geodata_from_bus_geodata(net) + + return net + +# --- secondary functions -------------------------------------------------------------------------- + + +def _data_correction(data:dict[str, pd.DataFrame], html_str:str|None) -> str|None: + """Corrects input data in particular with regard to obvious weaknesses in the data provided, + such as inconsistent spellings and missing necessary information + + Parameters + ---------- + data : dict[str, pd.DataFrame] + data provided by the excel file which will be corrected + html_str : str | None + data provided by the html file which will be corrected + + Returns + ------- + str + corrected html_str + """ + + # --- Line and Tieline data --------------------------- + for key in ["Lines", "Tielines"]: + + # --- correct column names + cols = data[key].columns.to_frame().reset_index(drop=True) + cols.loc[cols[1] == "Voltage_level(kV)", 0] = None + cols.loc[cols[1] == "Comment", 0] = None + cols.loc[cols[0].str.startswith("Unnamed:").astype(bool), 0] = None + cols.loc[cols[1] == "Length_(km)", 0] = "Electrical Parameters" # might be wrong in + # Tielines otherwise + data[key].columns = pd.MultiIndex.from_arrays(cols.values.T) + + # --- correct comma separation and cast to floats + data[key][("Maximum Current Imax (A)", "Fixed")] = \ + data[key][("Maximum Current Imax (A)", "Fixed")].replace("\xa0", 999e3).replace( + "-", 999e3).replace(" ", 999e3) + col_names = [("Electrical Parameters", col_level1) for col_level1 in [ + "Length_(km)", "Resistance_R(Ω)", "Reactance_X(Ω)", "Susceptance_B(μS)", + "Length_(km)"]] + [("Maximum Current Imax (A)", "Fixed")] + _float_col_comma_correction(data, key, col_names) + + # --- consolidate to one way of name capitalization + for loc_name in [(None, "NE_name"), ("Substation_1", "Full_name"), + ("Substation_2", "Full_name")]: + data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.replace( + "STANISLAWOW", "Stanislawow").str.replace("VIERRADEN", "Vierraden") + html_str = html_str.replace("STANISLAWOW", "Stanislawow").replace("Chelm", "CHELM") + + # --- Transformer data -------------------------------- + key = "Transformers" + + # --- fix Locations + loc_name = ("Location", "Full Name") + data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.replace( + "PSTMIKULOWA", "PST MIKULOWA").str.replace( + "Chelm", "CHELM").str.replace( + "OLSZTYN-MATK", "OLSZTYN-MATKI").str.replace( + "STANISLAWOW", "Stanislawow").str.replace("VIERRADEN", "Vierraden") + + # --- fix data in nonnull_taps + taps = data[key].loc[:, ("Phase Shifting Properties", "Taps used for RAO")].fillna("").astype( + str).str.replace(" ", "") + nonnull = taps.apply(len).astype(bool) + nonnull_taps = taps.loc[nonnull] + surrounded = nonnull_taps.str.startswith("<") & nonnull_taps.str.endswith(">") + nonnull_taps.loc[surrounded] = nonnull_taps.loc[surrounded].str[1:-1] + slash_sep = (~nonnull_taps.str.contains(";")) & nonnull_taps.str.contains("/") + nonnull_taps.loc[slash_sep] = nonnull_taps.loc[slash_sep].str.replace("/", ";") + nonnull_taps.loc[nonnull_taps == "0"] = "0;0" + data[key].loc[nonnull, ("Phase Shifting Properties", "Taps used for RAO")] = nonnull_taps + data[key].loc[~nonnull, ("Phase Shifting Properties", "Taps used for RAO")] = "0;0" + + # --- phase shifter with double info + cols = ["Phase Regulation δu (%)", "Angle Regulation δu (%)"] + for col in cols: + if is_object_dtype(data[key].loc[:, ("Phase Shifting Properties", col)]): + tr_double = data[key].index[data[key].loc[:, ( + "Phase Shifting Properties", col)].str.contains("/").fillna(0).astype(bool)] + data[key].loc[tr_double, ("Phase Shifting Properties", col)] = data[key].loc[tr_double, + ("Phase Shifting Properties", col)].str.split("/", expand=True)[1].str.replace( + ",", ".").astype(float).values # take second info and correct separation: , -> . + + return html_str + + +def _parse_html_str(html_str:str) -> pd.DataFrame: + """Converts ths geodata from the html file (information hidden in the string), from Lines in + particular, to a DataFrame that can be used later in _add_bus_geo() + + Parameters + ---------- + html_str : str + html file that includes geodata information + + Returns + ------- + pd.DataFrame + extracted geodata for a later and easy use + """ + def _filter_name(st:str) -> str: + name_start = "NE name: " + name_end = "" + pos0 = st.find(name_start) + len(name_start) + pos1 = st.find(name_end, pos0) + assert pos0 >= 0 + assert pos1 >= len(name_start) + return st[pos0:pos1] + + json_start_str = '') + json_str = html_str[json_start_pos:(json_start_pos+json_end_pos)] + geo_data = json.loads(json_str) + geo_data = geo_data["x"]["calls"] + methods_pos = pd.Series({item["method"]: i for i, item in enumerate(geo_data)}) + polylines = geo_data[methods_pos.at["addPolylines"]]["args"] + EIC_start = "EIC Code: " + if len(polylines[6]) != len(polylines[0]): + raise AssertionError("The lists of EIC Code data and geo data are not of the same length.") + line_EIC = [polylines[6][i][polylines[6][i].find(EIC_start)+len(EIC_start):] for i in range( + len(polylines[6]))] + line_name = [_filter_name(polylines[6][i]) for i in range(len(polylines[6]))] + line_geo_data = pd.concat([_lng_lat_to_df(polylines[0][i][0][0], line_EIC[i], line_name[i]) for + i in range(len(polylines[0]))], ignore_index=True) + + # remove trailing whitespaces + for col in ["EIC_Code", "name"]: + line_geo_data[col] = line_geo_data[col].str.strip() + + return line_geo_data + + +def _create_buses_from_line_data(net:pandapowerNet, data:dict[str, pd.DataFrame]) -> None: + """Creates buses to the pandapower net using information from the lines and tielines sheets + (excel file). + + Parameters + ---------- + net : pandapowerNet + net to be filled by buses + data : dict[str, pd.DataFrame] + data provided by the excel file which will be corrected + """ + bus_df_empty = pd.DataFrame({"name": str(), "vn_kv": float(), "TSO": str()}, index=[]) + bus_df = deepcopy(bus_df_empty) + for key in ["Lines", "Tielines"]: + for subst in ['Substation_1', 'Substation_2']: + data_col_tuples = [(subst, "Full_name"), (None, "Voltage_level(kV)"), (None, "TSO")] + to_add = data[key].loc[:, data_col_tuples].set_axis(bus_df.columns, axis="columns") + if len(bus_df): + bus_df = pd.concat([bus_df, to_add]) + else: + bus_df = to_add + bus_df = _drop_duplicates_and_join_TSO(bus_df) + new_bus_idx = create_buses( + net, len(bus_df), vn_kv=bus_df.vn_kv, name=bus_df.name, zone=bus_df.TSO) + assert np.allclose(new_bus_idx, bus_df.index) + + +def _create_lines(net:pandapowerNet, data:dict[str, pd.DataFrame]) -> None: + """Creates lines to the pandapower net using information from the lines and tielines sheets + (excel file). + + Parameters + ---------- + net : pandapowerNet + net to be filled by buses + data : dict[str, pd.DataFrame] + data provided by the excel file which will be corrected + """ + + bus_idx = _get_bus_idx(net) + + for key in ["Lines", "Tielines"]: + length_km = data[key][("Electrical Parameters", "Length_(km)")].values + zero_length = np.isclose(length_km, 0) + no_length = np.isnan(length_km) + if sum(zero_length) or sum(no_length): + logger.warning(f"According to given data, {sum(zero_length)} {key.lower()} have zero " + f"length and {sum(zero_length)} {key.lower()} have no length data. " + "Both types of wrong data are replaced by 1 km.") + length_km[zero_length | no_length] = 1 + vn_kvs = data[key].loc[:, (None, "Voltage_level(kV)")].values + + _ = create_lines_from_parameters( + net, + bus_idx.loc[list(tuple(zip(data[key].loc[:, ("Substation_1", "Full_name")].values, + vn_kvs)))].values, + bus_idx.loc[list(tuple(zip(data[key].loc[:, ("Substation_2", "Full_name")].values, + vn_kvs)))].values, + length_km, + data[key][("Electrical Parameters", "Resistance_R(Ω)")].values / length_km, + data[key][("Electrical Parameters", "Reactance_X(Ω)")].values / length_km, + data[key][("Electrical Parameters", "Susceptance_B(μS)")].values / length_km, + data[key][("Maximum Current Imax (A)", "Fixed")].fillna(999000).values / 1e3, + name=data[key].xs("NE_name", level=1, axis=1).values[:, 0], + EIC_Code=data[key].xs("EIC_Code", level=1, axis=1).values[:, 0], + TSO=data[key].xs("TSO", level=1, axis=1).values[:, 0], + Comment=data[key].xs("Comment", level=1, axis=1).values[:, 0], + Tieline=key=="Tielines", + ) + + +def _create_transformers_and_buses( + net:pandapowerNet, data:dict[str, pd.DataFrame], **kwargs) -> None: + """Creates transformers to the pandapower net using information from the transformers sheet + (excel file). + + Parameters + ---------- + net : pandapowerNet + net to be filled by buses + data : dict[str, pd.DataFrame] + data provided by the excel file which will be corrected + """ + + # --- data preparations + key = "Transformers" + bus_idx = _get_bus_idx(net) + vn_hv_kv, vn_lv_kv = _get_transformer_voltages(data, bus_idx) + trafo_connections = _allocate_trafos_to_buses_and_create_buses( + net, data, bus_idx, vn_hv_kv, vn_lv_kv, **kwargs) + max_i_a = data[key].loc[:, ("Maximum Current Imax (A) primary", "Fixed")] + empty_i_idx = max_i_a.index[max_i_a.isnull()] + max_i_a.loc[empty_i_idx] = data[key].loc[empty_i_idx, ( + "Maximum Current Imax (A) primary", "Max")].values + sn_mva = np.sqrt(3) * max_i_a * vn_hv_kv / 1e3 + z_pu = vn_lv_kv**2 / sn_mva + rk = data[key].xs("Resistance_R(Ω)", level=1, axis=1).values[:, 0] / z_pu + xk = data[key].xs("Reactance_X(Ω)", level=1, axis=1).values[:, 0] / z_pu + b0 = data[key].xs("Susceptance_B (µS)", level=1, axis=1).values[:, 0] * 1e-6 * z_pu + g0 = data[key].xs("Conductance_G (µS)", level=1, axis=1).values[:, 0] * 1e-6 * z_pu + zk = np.sqrt(rk**2 + xk**2) + vk_percent = np.sign(xk) * zk * 100 + vkr_percent = rk * 100 + pfe_kw = g0 * sn_mva * 1e3 + i0_percent = 100 * np.sqrt(b0**2 + g0**2) * net.sn_mva / sn_mva + taps = data[key].loc[:, ("Phase Shifting Properties", "Taps used for RAO")].str.split( + ";", expand=True).astype(int).set_axis(["tap_min", "tap_max"], axis=1) + + du = _get_float_column(data[key], ("Phase Shifting Properties", "Phase Regulation δu (%)")) + dphi = _get_float_column(data[key], ("Phase Shifting Properties", "Angle Regulation δu (%)")) + phase_shifter = np.isclose(du, 0) & (~np.isclose(dphi, 0)) # Symmetrical/Asymmetrical not + # considered + + _ = create_transformers_from_parameters( + net, + trafo_connections.hv_bus.values, + trafo_connections.lv_bus.values, + sn_mva, + vn_hv_kv, + vn_lv_kv, + vkr_percent, + vk_percent, + pfe_kw, + i0_percent, + shift_degree = data[key].xs("Theta θ (°)", level=1, axis=1).values[:, 0], + tap_pos = 0, + tap_neutral = 0, + tap_side = "lv", + tap_min = taps["tap_min"].values, + tap_max = taps["tap_max"].values, + tap_phase_shifter = phase_shifter, + tap_step_percent = du, + tap_step_degree = dphi, + name = data[key].loc[:, ("Location", "Full Name")].str.strip().values, + EIC_Code = data[key].xs("EIC_Code", level=1, axis=1).values[:, 0], + TSO = data[key].xs("TSO", level=1, axis=1).values[:, 0], + Comment = data[key].xs("Comment", level=1, axis=1).replace("\xa0", "").values[:, 0], + ) + + +def _invent_connections_between_grid_groups( + net:pandapowerNet, minimal_trafo_invention:bool=False, **kwargs) -> None: + """Adds connections between islanded grid groups via: + + - adding transformers between equally named buses that have different voltage level and lay in different groups + - merge buses of same voltage level, different grid groups and equal name base + - fuse buses that are close to each other + + Parameters + ---------- + net : pandapowerNet + net to be manipulated + minimal_trafo_invention : bool, optional + if True, adding transformers stops when no grid groups is islanded anymore (does not apply + for release version 5 or 6, i.e. it does not care what value is passed to + minimal_trafo_invention). If False, all equally named buses that have different voltage + level and lay in different groups will be connected via additional transformers, + by default False + """ + grid_groups = get_grid_groups(net) + bus_idx = _get_bus_idx(net) + bus_grid_groups = pd.concat([pd.Series(group, index=buses) for group, buses in zip( + grid_groups.index, grid_groups.buses)]).sort_index() + + # treat for example "Wuergau" equally as "Wuergau (2)": + location_names = pd.Series(bus_idx.index.get_level_values(0)) + location_names = location_names.str.replace(r"(.) \([0-9]+\)", r"\1", regex=True) + bus_idx.index = pd.MultiIndex.from_arrays( + [location_names.values, bus_idx.index.get_level_values(1).to_numpy()], + names=bus_idx.index.names) + + # --- add Transformers between equally named buses that have different voltage level and lay in + # --- different groups + connected_vn_kvs_by_trafos = pd.DataFrame({ + "hv": net.bus.vn_kv.loc[net.trafo.hv_bus.values].values, + "lv": net.bus.vn_kv.loc[net.trafo.lv_bus.values].values, + "index": net.trafo.index}).set_index(["hv", "lv"]).sort_index() + dupl_location_names = location_names[location_names.duplicated()] + + for location_name in dupl_location_names: + if minimal_trafo_invention and not len(bus_grid_groups.unique()) > 1: + break # break with regard to minimal_trafo_invention + grid_groups_at_location = bus_grid_groups.loc[bus_idx.loc[location_name].values] + grid_groups_at_location = grid_groups_at_location.drop_duplicates() + if len(grid_groups_at_location) < 2: + continue + elif len(grid_groups_at_location) > 2: + raise NotImplementedError("Code is not provided to invent Transformer connections " + "between locations with more than two grid groups, i.e. " + "voltage levels.") + TSO = net.bus.zone.at[grid_groups_at_location.index[0]] + vn_kvs = net.bus.vn_kv.loc[grid_groups_at_location.index].sort_values(ascending=False) + try: + trafos_connecting_same_voltage_levels = \ + connected_vn_kvs_by_trafos.loc[tuple(vn_kvs)] + except KeyError: + logger.info(f"For location {location_name}, no transformer data can be reused since " + f"no transformer connects {vn_kvs.sort_values(ascending=False).iat[0]} kV " + f"and {vn_kvs.sort_values(ascending=False).iat[1]} kV.") + continue + trafos_of_same_TSO = trafos_connecting_same_voltage_levels.loc[(net.bus.zone.loc[ + net.trafo.hv_bus.loc[trafos_connecting_same_voltage_levels.values.flatten( + )].values] == TSO).values].values.flatten() + + # from which trafo parameters are copied: + tr_to_be_copied = trafos_of_same_TSO[0] if len(trafos_of_same_TSO) else \ + trafos_connecting_same_voltage_levels.values.flatten()[0] + + # copy transformer data + duplicated_row = net.trafo.loc[[tr_to_be_copied]].copy() + duplicated_row.index = [net.trafo.index.max() + 1] # adjust index + duplicated_row.hv_bus = vn_kvs.index[0] # adjust hv_bus, lv_bus + duplicated_row.lv_bus = vn_kvs.index[1] # adjust hv_bus, lv_bus + duplicated_row.name = "additional transformer to connect the grid" + net.trafo = pd.concat([net.trafo, duplicated_row]) + + bus_grid_groups.loc[bus_grid_groups == grid_groups_at_location.iat[1]] = \ + grid_groups_at_location.iat[0] + + # --- merge buses of same voltage level, different grid groups and equal name base + bus_name_splits = net.bus.name.str.split(r"[ -/]+", expand=True) + buses_with_single_base = net.bus.name.loc[(~bus_name_splits.isnull()).sum(axis=1) == 1] + for idx, name_base in buses_with_single_base.items(): + same_name_base = net.bus.drop(idx).name.str.contains(name_base) + if not any(same_name_base): + continue + other_group = bus_grid_groups.drop(idx) != bus_grid_groups.at[idx] + same_vn = net.bus.drop(idx).vn_kv == net.bus.vn_kv.at[idx] + is_fuse_candidate = same_name_base & other_group & same_vn + if not any(is_fuse_candidate): + continue + to_fuse = bus_grid_groups.drop(idx).loc[is_fuse_candidate].drop_duplicates() + fuse_buses(net, idx, set(to_fuse.index)) + + bus_grid_groups.loc[bus_grid_groups.isin(bus_grid_groups.drop(idx).loc[ + is_fuse_candidate].unique())] = grid_groups_at_location.iat[0] + bus_grid_groups = bus_grid_groups.drop(to_fuse.index) + + # --- fuse buses that are close to each other + for name1, name2 in [("CROISIERE", "BOLLENE (POSTE RESEAU)"), + ("CAEN", "DRONNIERE (LA)"), + ("TRINITE-VICTOR", "MENTON/TRINITE VICTOR")]: + b1 = net.bus.index[net.bus.name == name1] + b2 = net.bus.index[net.bus.name == name2] + if len(b1) == 1 and len(b2) >= 1: + fuse_buses(net, b1[0], set(b2)) + bus_grid_groups = bus_grid_groups.drop(b2) + else: + logger.info("Buses of the following names were intended to be fused but were not found." + f"\n'{name1}' and '{name2}'") + + +def drop_islanded_grid_groups(net:pandapowerNet, min_bus_number:int|str, **kwargs) -> None: + """Drops grid groups that are islanded and include a number of buses below min_bus_number. + + Parameters + ---------- + net : pandapowerNet + net in which islanded grid groups will be dropped + min_bus_number : int | str, optional + Threshold value to decide which small grid groups should be dropped and which large grid + groups should be kept. If all islanded grid groups should be dropped except of the one + largest, set "max". If all grid groups that do not contain a slack element should be + dropped, set "unsupplied". + """ + def _grid_groups_to_drop_by_min_bus_number(): + return grid_groups.loc[grid_groups["n_buses"] < min_bus_number] + + grid_groups = get_grid_groups(net, **kwargs) + + if min_bus_number == "unsupplied": + slack_buses = set(net.ext_grid.loc[net.ext_grid.in_service, "bus"]) | \ + set(net.gen.loc[net.gen.in_service & net.gen.slack, "bus"]) + grid_groups_to_drop = grid_groups.loc[~grid_groups.buses.apply( + lambda x: not x.isdisjoint(slack_buses))] + + elif min_bus_number == "max": + min_bus_number = grid_groups["n_buses"].max() + grid_groups_to_drop = _grid_groups_to_drop_by_min_bus_number() + + elif isinstance(min_bus_number, int): + grid_groups_to_drop = _grid_groups_to_drop_by_min_bus_number() + + else: + raise NotImplementedError( + f"{min_bus_number=} is not implemented. Use an int, 'max', or 'unsupplied' instead.") + + buses_to_drop = reduce(set.union, grid_groups_to_drop.buses) + drop_buses(net, buses_to_drop) + logger.info(f"drop_islanded_grid_groups() drops {len(grid_groups_to_drop)} grid groups with a " + f"total of {grid_groups_to_drop.n_buses.sum()} buses.") + + +def _add_bus_geo(net:pandapowerNet, line_geo_data:pd.DataFrame) -> None: + """Adds geodata to the buses. The function needs to handle cases where line_geo_data does not + include no or multiple geodata per bus. Primarly, the geodata are allocate via EIC Code names, + if ambigous, names are considered. + + Parameters + ---------- + net : pandapowerNet + net in which geodata are added to the buses + line_geo_data : pd.DataFrame + Converted geodata from the html file + """ + iSl = pd.IndexSlice + lgd_EIC_bus = line_geo_data.pivot_table(values="value", index=["EIC_Code", "bus"], + columns="geo_dim") + lgd_name_bus = line_geo_data.pivot_table(values="value", index=["name", "bus"], + columns="geo_dim") + lgd_EIC_bus_idx_extended = pd.MultiIndex.from_frame(lgd_EIC_bus.index.to_frame().assign( + **dict(col_name="EIC_Code")).rename(columns=dict(EIC_Code="identifier")).loc[ + :, ["col_name", "identifier", "bus"]]) + lgd_name_bus_idx_extended = pd.MultiIndex.from_frame(lgd_name_bus.index.to_frame().assign( + **dict(col_name="name")).rename(columns=dict(name="identifier")).loc[ + :, ["col_name", "identifier", "bus"]]) + lgd_bus = pd.concat([lgd_EIC_bus.set_axis(lgd_EIC_bus_idx_extended), + lgd_name_bus.set_axis(lgd_name_bus_idx_extended)]) + dupl_EICs = net.line.EIC_Code.loc[net.line.EIC_Code.duplicated()] + dupl_names = net.line.name.loc[net.line.name.duplicated()] + + def _geo_json_str(this_bus_geo:pd.Series) -> str: + return f'{{"coordinates": [{this_bus_geo.at["lng"]}, {this_bus_geo.at["lat"]}], "type": "Point"}}' + + def _add_bus_geo_inner(bus:int) -> str|None: + from_bus_line_excerpt = net.line.loc[net.line.from_bus == bus, ["EIC_Code", "name", "Tieline"]] + to_bus_line_excerpt = net.line.loc[net.line.to_bus == bus, ["EIC_Code", "name", "Tieline"]] + line_excerpt = pd.concat([from_bus_line_excerpt, to_bus_line_excerpt]) + n_connected_line_ends = len(line_excerpt) + if n_connected_line_ends == 0: + logger.error(f"Bus {bus} (name {net.bus.at[bus, 'name']}) is not found in line_geo_data.") + return None + is_dupl = pd.concat([ + pd.DataFrame({"EIC": from_bus_line_excerpt.EIC_Code.isin(dupl_EICs).values, + "name": from_bus_line_excerpt.name.isin(dupl_names).values}, + index=pd.MultiIndex.from_product([["from"], from_bus_line_excerpt.index], + names=["bus", "line_index"])), + pd.DataFrame({"EIC": to_bus_line_excerpt.EIC_Code.isin(dupl_EICs).values, + "name": to_bus_line_excerpt.name.isin(dupl_names).values}, + index=pd.MultiIndex.from_product([["to"], to_bus_line_excerpt.index], + names=["bus", "line_index"])) + ]) + is_missing = pd.DataFrame({ + "EIC": ~line_excerpt.EIC_Code.isin( + lgd_bus.loc["EIC_Code"].index.get_level_values("identifier")), + "name": ~line_excerpt.name.isin( + lgd_bus.loc["name"].index.get_level_values("identifier")) + }).set_axis(is_dupl.index) + is_tieline = pd.Series(net.line.loc[is_dupl.index.get_level_values("line_index"), + "Tieline"].values, index=is_dupl.index) + + # --- construct access_vals, i.e. values to take line geo data from lgd_bus + # --- if not duplicated, take "EIC_Code". Otherwise and if not dupl, take "name". + # --- Otherwise ignore. Do it for both from and to bus + access_vals = pd.DataFrame({ + "col_name": "EIC_Code", + "identifier": line_excerpt.EIC_Code.values, + "bus": is_dupl.index.get_level_values("bus").values + }) # default is EIC_Code + take_from_name = ((is_dupl.EIC | is_missing.EIC) & (~is_dupl.name & ~is_missing.name)).values + access_vals.loc[take_from_name, "col_name"] = "name" + access_vals.loc[take_from_name, "identifier"] = line_excerpt.name.loc[take_from_name].values + keep = (~(is_dupl | is_missing)).any(axis=1).values + if np.all(is_missing): + log_msg = (f"For bus {bus} (name {net.bus.at[bus, 'name']}), {n_connected_line_ends} " + "were found but no EIC_Codes or names of corresponding lines were found ." + "in the geo data from the html file.") + if is_tieline.all(): + logger.debug(log_msg) + else: + logger.warning(log_msg) + return None + elif sum(keep) == 0: + logger.info(f"For {bus=}, all EIC_Codes and names of connected lines are ambiguous. " + "No geo data is dropped at this point.") + keep[(~is_missing).any(axis=1)] = True + access_vals = access_vals.loc[keep] + + # --- get this_bus_geo from EIC_Code or name with regard to access_vals + this_bus_geo = lgd_bus.loc[iSl[ + access_vals.col_name, access_vals.identifier, access_vals.bus], :] + + if len(this_bus_geo) > 1: + # reduce similar/equal lines + this_bus_geo = this_bus_geo.loc[this_bus_geo.round(2).drop_duplicates().index] + + # --- return geo_json_str + len_this_bus_geo = len(this_bus_geo) + if len_this_bus_geo == 1: + return _geo_json_str(this_bus_geo.iloc[0]) + elif len_this_bus_geo == 2: + how_often = pd.Series( + [sum(np.isclose(lgd_EIC_bus["lat"], this_bus_geo["lat"].iat[i]) & \ + np.isclose(lgd_EIC_bus["lng"], this_bus_geo["lng"].iat[i])) for i in + range(len_this_bus_geo)], index=this_bus_geo.index) + if how_often.at[how_often.idxmax()] >= 1: + logger.warning(f"Bus {bus} (name {net.bus.at[bus, 'name']}) was found multiple times" + " in line_geo_data. No value exists more often than others. " + "The first of most used geo positions is used.") + return _geo_json_str(this_bus_geo.loc[how_often.idxmax()]) + + net.bus.geo = [_add_bus_geo_inner(bus) for bus in net.bus.index] + + +# --- tertiary functions --------------------------------------------------------------------------- + +def _float_col_comma_correction(data:dict[str, pd.DataFrame], key:str, col_names:list): + for col_name in col_names: + data[key][col_name] = pd.to_numeric(data[key][col_name].astype(str).str.replace( + ",", "."), errors="coerce") + + +def _get_transformer_voltages( + data:dict[str, pd.DataFrame], bus_idx:pd.Series) -> tuple[np.ndarray, np.ndarray]: + + key = "Transformers" + vn = data[key].loc[:, [("Voltage_level(kV)", "Primary"), + ("Voltage_level(kV)", "Secondary")]].values + vn_hv_kv = np.max(vn, axis=1) + vn_lv_kv = np.min(vn, axis=1) + if is_integer_dtype(list(bus_idx.index.dtypes)[1]): + vn_hv_kv = vn_hv_kv.astype(int) + vn_lv_kv = vn_lv_kv.astype(int) + + return vn_hv_kv, vn_lv_kv + + +def _allocate_trafos_to_buses_and_create_buses( + net:pandapowerNet, data:dict[str, pd.DataFrame], bus_idx:pd.Series, + vn_hv_kv:np.ndarray, vn_lv_kv:np.ndarray, + rel_deviation_threshold_for_trafo_bus_creation:float=0.2, + log_rel_vn_deviation:float=0.12, **kwargs) -> pd.DataFrame: + """Provides a DataFrame of data to allocate transformers to the buses according to their + location names. If locations of transformers do not exist due to the data of the lines and + tielines sheets, additional buses are created. If locations exist but have a far different + voltage level than the transformer, either a warning is logged or additional buses are created + according to rel_deviation_threshold_for_trafo_bus_creation and log_rel_vn_deviation. + + Parameters + ---------- + net : pandapowerNet + pandapower net + data : dict[str, pd.DataFrame] + _description_ + bus_idx : pd.Series + Series of indices and corresponding location names and voltage levels in the MultiIndex of + the Series + vn_hv_kv : np.ndarray + nominal voltages of the hv side of the transformers + vn_lv_kv : np.ndarray + Nominal voltages of the lv side of the transformers + rel_deviation_threshold_for_trafo_bus_creation : float, optional + If the voltage level of transformer locations is far different than the transformer data, + additional buses are created. rel_deviation_threshold_for_trafo_bus_creation defines the + tolerance in which no additional buses are created. By default 0.2 + log_rel_vn_deviation : float, optional + This parameter allows a range below rel_deviation_threshold_for_trafo_bus_creation in which + a warning is logged instead of a creating additional buses. By default 0.12 + + Returns + ------- + pd.DataFrame + information to which bus the trafos should be connected to. Columns are + ["name", "hv_bus", "lv_bus", "vn_hv_kv", "vn_lv_kv", ...] + """ + + if rel_deviation_threshold_for_trafo_bus_creation < log_rel_vn_deviation: + logger.warning( + f"Given parameters violates the ineqation " + f"{rel_deviation_threshold_for_trafo_bus_creation=} >= {log_rel_vn_deviation=}. " + f"Therefore, rel_deviation_threshold_for_trafo_bus_creation={log_rel_vn_deviation} " + "is assumed.") + rel_deviation_threshold_for_trafo_bus_creation = log_rel_vn_deviation + + key = "Transformers" + bus_location_names = set(net.bus.name) + trafo_bus_names = data[key].loc[:, ("Location", "Full Name")] + trafo_location_names = _find_trafo_locations(trafo_bus_names, bus_location_names) + + # --- construct DataFrame trafo_connections including all information on trafo allocation to + # --- buses + empties = -1*np.ones(len(vn_hv_kv), dtype=int) + trafo_connections = pd.DataFrame({ + "name": trafo_location_names, + "hv_bus": empties, + "lv_bus": empties, + "vn_hv_kv": vn_hv_kv, + "vn_lv_kv": vn_lv_kv, + "vn_hv_kv_next_bus": vn_hv_kv, + "vn_lv_kv_next_bus": vn_lv_kv, + "hv_rel_deviation": np.zeros(len(vn_hv_kv)), + "lv_rel_deviation": np.zeros(len(vn_hv_kv)), + }) + trafo_connections[["hv_bus", "lv_bus"]] = trafo_connections[["hv_bus", "lv_bus"]].astype(np.int64) + + for side in ["hv", "lv"]: + bus_col, trafo_vn_col, next_col, rel_dev_col, has_dev_col = \ + f"{side}_bus", f"vn_{side}_kv", f"vn_{side}_kv_next_bus", f"{side}_rel_deviation", \ + f"trafo_{side}_to_bus_deviation" + name_vn_series = pd.Series(tuple(zip(trafo_location_names, trafo_connections[trafo_vn_col]))) + isin = name_vn_series.isin(bus_idx.index) + trafo_connections[has_dev_col] = ~isin + trafo_connections.loc[isin, bus_col] = bus_idx.loc[name_vn_series.loc[isin]].values + + # --- code to find bus locations with vn deviation + next_vn = np.array([bus_idx.loc[tln.name].index.values[ + (pd.Series(bus_idx.loc[tln.name].index) - getattr(tln, trafo_vn_col)).abs().idxmin( + )] for tln in trafo_connections.loc[~isin, ["name", trafo_vn_col]].itertuples()]) + trafo_connections.loc[~isin, next_col] = next_vn + rel_dev = np.abs(next_vn - trafo_connections.loc[~isin, trafo_vn_col].values) / next_vn + trafo_connections.loc[~isin, rel_dev_col] = rel_dev + trafo_connections.loc[~isin, bus_col] = \ + bus_idx.loc[list(tuple(zip(trafo_connections.loc[~isin, "name"], + trafo_connections.loc[~isin, next_col])))].values + + # --- create buses to avoid too large vn deviations between nodes and transformers + need_bus_creation = trafo_connections[rel_dev_col] > \ + rel_deviation_threshold_for_trafo_bus_creation + new_bus_data = pd.DataFrame({ + "vn_kv": trafo_connections.loc[need_bus_creation, trafo_vn_col].values, + "name": trafo_connections.loc[need_bus_creation, "name"].values, + "TSO": data[key].loc[need_bus_creation, ("Location", "TSO")].values + }) + new_bus_data_dd = _drop_duplicates_and_join_TSO(new_bus_data) + new_bus_idx = create_buses(net, len(new_bus_data_dd), vn_kv=new_bus_data_dd.vn_kv, + name=new_bus_data_dd.name, zone=new_bus_data_dd.TSO) + trafo_connections.loc[need_bus_creation, bus_col] = net.bus.loc[new_bus_idx, [ + "name", "vn_kv"]].reset_index().set_index(["name", "vn_kv"]).loc[list(new_bus_data[[ + "name", "vn_kv"]].itertuples(index=False, name=None))].values + trafo_connections.loc[need_bus_creation, next_col] = \ + trafo_connections.loc[need_bus_creation, trafo_vn_col].values + trafo_connections.loc[need_bus_creation, rel_dev_col] = 0 + trafo_connections.loc[need_bus_creation, has_dev_col] = False + + # --- create buses for trafos that are connected to the same bus at both sides (possible if + # --- vn_hv_kv < vn_lv_kv *(1+rel_deviation_threshold_for_trafo_bus_creation) which usually + # --- occurs for PSTs only) + same_bus_connection = trafo_connections.hv_bus == trafo_connections.lv_bus + duplicated_buses = net.bus.loc[trafo_connections.loc[same_bus_connection, "lv_bus"]].copy() + duplicated_buses["name"] += " (2)" + duplicated_buses.index = list(range(net.bus.index.max()+1, + net.bus.index.max()+1+len(duplicated_buses))) + trafo_connections.loc[same_bus_connection, "lv_bus"] = duplicated_buses.index + net.bus = pd.concat([net.bus, duplicated_buses]) + if n_add_buses := len(duplicated_buses): + tr_names = data[key].loc[trafo_connections.index[same_bus_connection], + ("Location", "Full Name")] + are_PSTs = tr_names.str.contains("PST") + logger.info(f"{n_add_buses} additional buses were created to avoid that transformers are " + f"connected to the same bus at both side, hv and lv. Of the causing " + f"{len(tr_names)} transformers, {sum(are_PSTs)} contain 'PST' in their name. " + f"According to this converter, the power flows over all these transformers will" + f" end at the additional buses. Please consider to connect lines with the " + f"additional buses, so that the power flow is over the (PST) transformers into " + f"the lines.") + + # --- log according to log_rel_vn_deviation + for side in ["hv", "lv"]: + need_logging = trafo_connections.loc[trafo_connections[has_dev_col], + rel_dev_col] > log_rel_vn_deviation + if n_need_logging := sum(need_logging): + max_dev = trafo_connections[rel_dev_col].max() + idx_max_dev = trafo_connections[rel_dev_col].idxmax() + logger.warning( + f"For {n_need_logging} Transformers ({side} side), only locations were found (orig" + f"in are the line and tieline data) that have a higher relative deviation than " + f"{log_rel_vn_deviation}. The maximum relative deviation is {max_dev} which " + f"results from a Transformer rated voltage of " + f"{trafo_connections.at[idx_max_dev, trafo_vn_col]} and a bus " + f"rated voltage (taken from Lines/Tielines data sheet) of " + f"{trafo_connections.at[idx_max_dev, next_col]}. The best locations were " + f"nevertheless applied, due to {rel_deviation_threshold_for_trafo_bus_creation=}") + + assert (trafo_connections.hv_bus > -1).all() + assert (trafo_connections.lv_bus > -1).all() + assert (trafo_connections.hv_bus != trafo_connections.lv_bus).all() + + return trafo_connections + + +def _find_trafo_locations(trafo_bus_names, bus_location_names): + # --- split (original and lower case) strings at " " separators to remove impeding parts for + # identifying the location names + trafo_bus_names_expended = trafo_bus_names.str.split(r"[ ]+|-A[0-9]+|-TD[0-9]+|-PF[0-9]+", + expand=True).fillna("").replace(" ", "") + trafo_bus_names_expended_lower = trafo_bus_names.str.lower().str.split( + r"[ ]+|-A[0-9]+|-TD[0-9]+|-PF[0-9]+", expand=True).fillna("").replace(" ", "") + + # --- identify impeding parts + contains_number = trafo_bus_names_expended.map(lambda x: any(char.isdigit() for char in x)) + to_drop = (trafo_bus_names_expended_lower == "tr") | (trafo_bus_names_expended_lower == "pst") \ + | (trafo_bus_names_expended == "") | (trafo_bus_names_expended == "/") | ( + trafo_bus_names_expended == "LIPST") | (trafo_bus_names_expended == "EHPST") | ( + trafo_bus_names_expended == "TFO") | (trafo_bus_names_expended_lower == "trafo") | ( + trafo_bus_names_expended_lower == "kv") | contains_number + trafo_bus_names_expended[to_drop] = "" + + # --- reconstruct name strings for identification + trafo_bus_names_joined = trafo_bus_names_expended.where(~to_drop).fillna('').agg( + ' '.join, axis=1).str.strip() + trafo_bus_names_longest_part = trafo_bus_names_expended.apply( + lambda row: max(row, key=len), axis=1) + joined_in_buses = trafo_bus_names_joined.isin(bus_location_names) + longest_part_in_buses = trafo_bus_names_longest_part.isin(bus_location_names) + + # --- check whether all name strings point at location names of the buses + if False: # for easy testing + fail = ~(joined_in_buses | longest_part_in_buses) + a = pd.concat([trafo_bus_names_joined.loc[fail], trafo_bus_names_longest_part.loc[fail]], axis=1) + + if n_bus_names_not_found := len(joined_in_buses) - sum(joined_in_buses | longest_part_in_buses): + raise ValueError( + f"For {n_bus_names_not_found} Tranformers, no suitable bus location names were found, " + f"i.e. the algorithm did not find a (part) of Transformers-Location-Full Name that fits" + " to Substation_1 or Substation_2 data in Lines or Tielines sheet.") + + # --- set the trafo location names and trafo bus indices respectively + trafo_location_names = trafo_bus_names_longest_part + trafo_location_names.loc[joined_in_buses] = trafo_bus_names_joined + + return trafo_location_names + + +def _drop_duplicates_and_join_TSO(bus_df:pd.DataFrame) -> pd.DataFrame: + bus_df = bus_df.drop_duplicates(ignore_index=True) + # just keep one bus per name and vn_kv. If there are multiple buses of different TSOs, join the + # TSO strings: + bus_df = bus_df.groupby(["name", "vn_kv"], as_index=False).agg({"TSO": lambda x: '/'.join(x)}) + assert not bus_df.duplicated(["name", "vn_kv"]).any() + return bus_df + + +def _get_float_column(df, col_tuple, fill=0): + series = df.loc[:, col_tuple] + series.loc[series == "\xa0"] = fill + return series.astype(float).fillna(fill) + + +def _get_bus_idx(net:pandapowerNet) -> pd.Series: + return net.bus[["name", "vn_kv"]].rename_axis("index").reset_index().set_index([ + "name", "vn_kv"])["index"] + + +def get_grid_groups(net:pandapowerNet, **kwargs) -> pd.DataFrame: + notravbuses_dict = dict() if "notravbuses" not in kwargs.keys() else { + "notravbuses": kwargs.pop("notravbuses")} + grid_group_buses = [set_ for set_ in connected_components(create_nxgraph(net, **kwargs), + **notravbuses_dict)] + grid_groups = pd.DataFrame({"buses": grid_group_buses}) + grid_groups["n_buses"] = grid_groups["buses"].apply(len) + return grid_groups + + +def _lng_lat_to_df(dict_:dict, line_EIC:str, line_name:str) -> pd.DataFrame: + return pd.DataFrame([ + [line_EIC, line_name, "from", "lng", dict_["lng"][0]], + [line_EIC, line_name, "to", "lng", dict_["lng"][1]], + [line_EIC, line_name, "from", "lat", dict_["lat"][0]], + [line_EIC, line_name, "to", "lat", dict_["lat"][1]], + ], columns=["EIC_Code", "name", "bus", "geo_dim", "value"]) + + +def _fill_geo_at_one_sided_branches_without_geo_extent(net:pandapowerNet): + + def _check_geo_availablitiy(net:pandapowerNet) -> dict[str, pd.Index|int]: + av = dict() # availablitiy of geodata + av["bus_with_geo"] = net.bus.index[~net.bus.geo.isnull()] + av["lines_fbw_tbwo"] = net.line.index[net.line.from_bus.isin(av["bus_with_geo"]) & + (~net.line.to_bus.isin(av["bus_with_geo"]))] + av["lines_fbwo_tbw"] = net.line.index[(~net.line.from_bus.isin(av["bus_with_geo"])) & + net.line.to_bus.isin(av["bus_with_geo"])] + av["trafos_hvbw_lvbwo"] = net.trafo.index[net.trafo.hv_bus.isin(av["bus_with_geo"]) & + (~net.trafo.lv_bus.isin(av["bus_with_geo"]))] + av["trafos_hvbwo_lvbw"] = net.trafo.index[(~net.trafo.hv_bus.isin(av["bus_with_geo"])) & + net.trafo.lv_bus.isin(av["bus_with_geo"])] + av["n_lines_one_side_geo"] = len(av["lines_fbw_tbwo"])+len(av["lines_fbwo_tbw"]) + return av + + geo_avail = _check_geo_availablitiy(net) + while geo_avail["n_lines_one_side_geo"]: + + # copy available geodata to the other end of branches where geodata are missing + for et, bus_w_geo, bus_wo_geo, idx_key in zip( + ["line", "line", "trafo", "trafo"], + ["to_bus", "from_bus", "lv_bus", "hv_bus"], + ["from_bus", "to_bus", "hv_bus", "lv_bus"], + ["lines_fbwo_tbw", "lines_fbw_tbwo", "trafos_hvbwo_lvbw", "trafos_hvbw_lvbwo"]): + net.bus.loc[net[et].loc[geo_avail[idx_key], bus_wo_geo].values, "geo"] = \ + net.bus.loc[net[et].loc[geo_avail[idx_key], bus_w_geo].values, "geo"].values + geo_avail = _check_geo_availablitiy(net) + + set_line_geodata_from_bus_geodata(net) + + +if __name__ == "__main__": + from pathlib import Path + import os + import pandapower as pp + + home = str(Path.home()) + jao_data_folder = os.path.join(home, "Documents", "JAO Static Grid Model") + + release5 = os.path.join(jao_data_folder, "20240329_Core Static Grid Model – 5th release") + excel_file_path = os.path.join(release5, "20240329_Core Static Grid Model_public.xlsx") + html_file_path = os.path.join(release5, "20240329_Core Static Grid Model Map_public", + "2024-03-18_Core_SGM_publication.html") + + release6 = os.path.join(jao_data_folder, "202409_Core Static Grid Mode_6th release") + excel_file_path = os.path.join(release6, "20240916_Core Static Grid Model_for publication.xlsx") + html_file_path = os.path.join(release6, "2024-09-13_Core_SGM_publication_files", + "2024-09-13_Core_SGM_publication.html") + + pp_net_json_file = os.path.join(home, "desktop", "jao_grid.json") + + if 1: # read from original data + net = from_jao(excel_file_path, html_file_path, True, drop_grid_groups_islands=True) + pp.to_json(net, pp_net_json_file) + else: # load net from already converted and stored net + net = pp.from_json(pp_net_json_file) + + print(net) + grid_groups = get_grid_groups(net) + print(grid_groups) + + _fill_geo_at_one_sided_branches_without_geo_extent(net) diff --git a/pandapower/test/converter/jao_testfiles/testfile.xlsx b/pandapower/test/converter/jao_testfiles/testfile.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..01dbecacbb9a1a133f4ed1f9d3b6318a92d9df8d GIT binary patch literal 76698 zcmeFYbzGF)`Y(*4B7%UVNU1Q0bO=KTD5%2+cdL*|{%z??2W*DxtG2?k@Ic#hoaZ|Lxf$!`<@ao8OfR4!#PQ=Iwx^{ieagDTxnaxx#9> zCZYOenKjJ>+4wh5`48?OKW)WE3yPs$L6$mZHe`lB^SII4hNUv15#K+(M$aB;>mj08 z^EH}>SL>5=m5I_zc|Tqq44XWbhe|tbKxLG)?AnIGD?emd2>~-R3 z{CHXk_VRvB*v5~p58F15rgu;c_#SDY9j6klS>7LB&v&MF5ATODe5vEcnhg;b+&7DT ztZxLrlNwi?xG+C*`8dE+?u|s}b?%$bdqoXiu8l%xu1_qQ$2>DxebFWZoA0z0Zdvc; zcRcXf6!Co$k$|hXekAhYaOpAWZ8_^rn_%Lab=n7eIq+WU>7id7+^!&SPfxM2l>Q^E zxo85q?t-wM1W}6z!dl^kPF+5|GlvoyQRmXZW2%`ZK29I0s* z?1P0bsambz#@E!pI+zewz)3bN)S>)wC>Gev_b%pi_IfC{W#`pE`VcWzW3B}5AkF71 z_bZi4-B^b$in6bj`=B3n5kBh9ZVRY|-FNz=Yc>Gy%=D|FRJiulP$u*09{;ujokie_6BE*}*vChO;!;Pa}Q#;_4VZ-3)Fv zzPx&+!Tyloe-~1^HTY3Dm$0z1u3=#j0z0}`b2uaHtqc(eD@>KlSGPf=^Ar8{>Oa9D zm}j|6@&t=lYyR@u-SRx- z{OaFjJv^(vwvV;Adi$a>tUmZ_i3Mf7{B3fBGPw{pAn1}={%!1&sH-R6Hiq>Jor6V- zDBXuXJ-&0rjoE}UHq&1v!pN>!M)^~>-zK$e?AAS%cZt&yAAH&#^4N4N#R+8k>bYM% zfQWoI6*uBG+2p(Do1bJGVSu|KDI?qG(t(y8pN|zq$kLm-;<67tNZ%^A&$V);?o7Vc zvMGo?>ON=bPB2A#{PyzeiNs4fX43Ny7UVvL)Lk4ly?z)hS|aD8Aofu-?6XNSbz~$( zuDy#yvpb!{pwQtGNT+rOK5t!`9cd{WIsfQDU+1H~vUyg@yKXrKtiTfz_YJi~ETU9-3 z{pr_7O{0k(6UqwjogRrj!jYoJaba8gC|AzbbLEgh)9c13+xzxnc*LTSCh4BV$DS3M z(Yf5R)2(Bb#Ea}k>W_4>gXX7V3kD(rV>BxqgL;DIyqPFknQa*_D^!qx8kor zhl+8O8SK%tZRip4)J6S#eC_M*Bj?-eW;&HFQ!BSwzW2{vZ-Fjva1HEE^sabn_^5qz ze{XzmQX)Nb^mjNp?$~|#-G|S2M?1sUmqrUmJw61;eQrcjf6N~KeS7Er<)*gP!;egx zZiS;GkDP_oUW}_h!a2nKWO1GIRamMY$qQ3FgF%}M7RbTCTrv2gr@i;`AY}H7#t)UG zH?&@neyP}e6|s^eEU{;fNEvZ}T^-zgTwcI)`(mqEO_cR5W89G!rxIiw)7D)C<{C%& zZAI!g)GiA&6VbBz=l;BDw!fsC#`I`hIi5iR)wp9Ixl!4jZG@8~+3IBDXj4J$eL(7H zFGCWCe%#$7aJj#4w#2-7+qT~`>uBu*pU~C25{&ie_!?EnJblNWYHz#VAyU7hVLGi3N#)MzZk;RFoLL*hPQ9ffsF9Kmf=a<6Q+xx z3=d#qVco*IdI98&^Gf{p%<)%Mz5tqRu=oG%Uv-f^mR~sWTh?ErUiW$0MZito38I1@ z7wPO{F+Q8^ZQ_gGyN3{s6t8*I&Ynu~n&H?}v^VQyCTqQk;)-RG+FQu|t7RE^+G9)y z{3wx!uWt`k^e2^OKY#Q}+|Ikw`-_6Zu|(!BLs}r@vfs%1hY$VFi|*Ue+9VFJ@5CEX z-uj7j9lHMYp?oFx@!$)?%=@8HuWa-AZY#VF>pr!rt%1vW*v*@TMHgwdAt<_yi39?d zdT+DDYBRL9GX^x>F8VSD^=prLUPAT7SeA2)*Pw0j#h}oeSh1RX%OSm2uG(aNZ`l33 zuHlF5Mx3DI_ox+dIo6Esw>po$Nt`g;#QWbNNGMBpCFTbf)*GEGST_MQ{!|?Ya}yIs z2adBZE=>7JjM7Dn-6r*0zqcx?IX=wyJrvCZA4KNzS>)DuSr0eext?qD@V0!v#i=Kb zHL(@^TmCa6|Cxu>llzad%o1{Ul4Dh20|Jd+{twkD47-=#P)|(hCI^RSJt+85M#|gK z7k%H+>2ULlBimfMHg~4*?Ez+K*@A?pMA?Sz%()(YA8$#Ki>a%AQT#=eEyXQZ`B;te zEhqJ5vUy%hToPA&aaQuW=P8oKxtaw!{>G5Ow+-4`QUcm))UWfhJz%`|e6w#o>U3$} z9TE%IAd4&_EPv~$-dGRi&-ASf-;kcBsy10U`Yc1n@j=Nj)$=xvP4|~ov&ZDUhSImz zPMPpSvD=fYxNO7juFK-y6Cxk^lIGkfB@`PjtDx?YoIYi+FYuB)LAuJjs#tW<(QuEs1%E0iDYQ^=3FuZ17GAyB3)vETG9GvnaX@)U(>e_r6x;>25lb8I+-xn`Z(W7 zq;$AtD)umbqllBshnh;82jMXGNPeWvX85v6xp{rDZ~cqaxy5S(slE$}E4S|2KOoT# zTJjTREf;pbHOcnD#k9PupV5f#`GLSiRcN_MS*POLoI{h!tGcDt-i@P@a>VdWz76V# zpBGb)ET{2gy%e3pdnW?mq;^+!no(sG*at%CfiGi3f5A9H;@c8RFY89t>=PexJ?-aW zY=`eiZJmY-Ct0I zgkEWs!}F=Yj;J4p$8dVwX4#k&dymv=#?F}iV)?~8eu(Ss39`+goJ(_1Y%kehHjBSA zEqN77KpDPr%tI5SHpUcz@;2I|vJzrxctsk#kk$hl`z-7D-<2uWhqlR?D%9`fzu|;_&9v}~tVzzK>CRn~7l#eF#&hZq zy2kT21a3RZCn8K$DkX=Teie3qYx-rtcZckOomYZ2c^!S={@Y8k-s_^D`iv9S=qiU(vrh$}CHh1)L7UA2#I|ITsL zW7D9j20wyyyM+}0<}AZ~;G(-j7cDPZ+uU(@kl|Ryp`YSy2yMVwo^Oq4P5W%btvR4L z-6Oy5onS2?%Jy{AZ1>x~3hzl7q7Xf1OcH6nt#nXmJ=)D|-K#aYTb6_r2~$Yho~bVF zzLzA#(ndRnWq(Pai{WQ}?pGVLX04*sS9nfC(`v{e7p>F6(koVjS~vPC1N;kr{rrLK z0!Py=^{iLqE4Hd|RUIkQRry{fV4qQ#RegPE+tqP6k+4VCLFDH+@8iAOIB)6OEExAZ z5Ms{cYHOo~0i}B4mpcwtRHCpOylni0in@3`4#q!B=y2YQ)))2o`Q`^@0?vZ4CWpu# zUA&+$(G-vxs_o|^%JQZ@C3iJ@wa%s_fTq0qaQ$oMYIfrl8Z})-2bKIE+l!%sbjdz6<+MAw_EA%WC)#Da2oOC|XXk2O;+()eQlqxfq-R;FIQ1|;< z^3Lo#Zvmf#4*B?v)c^ts=`@nQI10Pe!6@~jvYHzc0kjW?6YdNKt_S%yWRls;RKO*5 z^73h&@!nIUMWyxzGF2ITX2V z|1D5c<+$P#@uV|Q9z)1K?0316wTga}gvLKq;$9jL$E;W$OM#vRlD>vS*rgCEo=Y}0 zoaQCZVt9E?$CdI$@Uo;vFW!|l8J#wrxs9wuvhXRZ8JEJAj zq2`K?w*`rl{JZP?@d^sd+%g7378Q1;t0(bu$i4K}546~V2U8GutRFq@x!uvGytdET zUH)H>I_oif;{$@i<`lUVbgqc;A7qxw#1ka(%%8ST!FUF@Kk3efXc_Y=cI!>Gu| z|EsRv7+rTmfvzrpbY%;6u+g;rKVpMHyXxpO@&g^EMaYjpD>=WVaPmI9zQ7;a-z>$; zOg_`}B`-d5WDvZOF__dXs=s+XP+l|7XZ&^`-LxM$o zS6D9ZdSCQqX9@Q-?7r|t3C9IVm^m_Ikfi`F;hcukvupaKe1ou1^h62I!6p|yN(#h% zRWdgylrNtC?_}~AvWZ-D4cT90I{$=B2SX;GhmOY_?o67+eGf?8?&H9iTl z(k|29gjs3Z_*eX&!RqTh8?WBV%O;-cTl`>GEbpHM=)t{03nc6tH>vhbSrv|dEXr2C zW2(&=Rte~CbSVB)Ix=tD2}`(Sp*#|naQ6(<^o;-A{AGrxEW8Too-l82)u|wivy-jl z|GTee6=B{>)xV9I$~bow|M9_Q55FB1879Pqr1mxrNcH{0c>Nu)thnCr+D0dKSbDgYY^|F_skxK+hx05-_-cchnT(<2}+KplUpk0T>m}A6&#QZ z$6hJqqP)h|$;+sXG5mk8k&U8NEpzL-IED0-*9zK@4F8ZUP}Ghl$NFOMkNe&UX6m~u zzUKd|NCD74$>{5Cgh_MfVLpKsS*!kQX~R%q4~OHEoo?^R{o`P^Ee$p98Ho8j6qG!c zP=wExEQIPc7cY~nruowrQvO_E5v)D>0iUP1sT;!Dx*Felg3!Ajl@+(&gee?1mZa|#%H|yRq+uLc~}vvH+tx@dEK zFx;l6>`fPD8sJuz9vUAGPg0^83Gitf9hIQbdSCksFT7xxihfM4;YZu3>Y-~u-n>t8 zOckH9eb+;3d!M4KtO;7vD{ZEZ<>|PCp^U`TlxgXv+}uJX0g|icV&&-pm!$m`nJ7s( zY~)}<@k*=acRz>U;Xizbe?wZ&&p(P@Jc^#aoL<^-AUO7+V+yCLCS@*-ycF0YD3iRF zvJndd&m%@9OGwiPlN71$-ZHdK^a!zG5#AwFQcGh(;vuH?la#j-8h%T+w1^9=jQvBC zb8HW6M*I!n^mFe9v-q;|Lws&&#iyp5nfiyuQi|N+xrrOfNZ%*b(@8|b#p5An%1KOZ z5;yaWhetv>nld-5&0M?Z@{m++^(Q~`AbX)dqY}5Wu;jSsF6qm@!6zrAT&jxYpfe3q znsP}HRxt=>jlmI-&vp}`!VQ7C?#ere@R6+r7Ji2)(v5? zt768Yq7L5vD{bhl7yo+z=k_!%T7C%4Pp#=82DnSD=PK!WqZ^zrypn6ZQLRg$5|Q1x3Ow#9GGSya;CvcfT?@d}J@L0NS!%OiBS_CXd)>t> zL9QM~^y=ow4_ij5Xovk0e=3dAA)vN^v-H2iGl#yTTIlRF0?yDpA}vjD6hq?_bmdLJ zdP;PkatL?$pV35KSU#fAn`%}>CO3~KNl~R0q*|^K{1w6ft7=M9H%E+peve2kB8~5< zhFyPNW>EbmC0S^W;1^=N?+k zaK#{=EPpsb48&Tbz<0Cm&-jDkzI}f46-@uW*FYK*>OC`Y=UKaW!L$ zvOA-H+pijq+#funKpI~kbl?P&82tcLicib-xKSV$Bn!^bkK=z&SfQ*xo}c@ky7OPY zmsi(puq7}(%Q38#f6KDKjRH5%-H5{j+}Qp3?7Wb9j>OlLx%6j7`ZrK+o7FUYT9irG zxH<7??RJ=L+RY*33nzH@p8G9*Y&8F3uk-G&tVhxox5mL))4xb2g_$|&6Pf&W1&*|j zD^`}4i}31g_e`=0Sc3WfQB8;!+x_(l0*8}6vuLS)M;|mdFINRALlQ&PhFkL9r?RX5>n&aW9E1&w>n+l>`Wv@$sVQ@;=C1M{j^eAe zLO%89@$k-=(+&7YS@PZiQ^BRgj~b87<>n6`L>{CNr(2kXaZYg{dV;Q@}a6Y312|D`Wxm3 zJ}k-%;Db=V18CnIwzM(Q9~DuLQ03Bh>@)u-F6cYL zh0ifsf=M-@E8>`#3_sDR*Wgl>j$T*O{2$!c)t)=@65j=k{ucUCzrN-Kr@Hxl&IzWp zV=g_2?KH{6w&Oob zCynHGl8wncHgA0Ciz=PA;SQP%#znL<*>B!J7th)lejf8lgBmsJaF=L%G~LW_NzmNj zOI_@W-G~n6Bd6OOXP;ywuajqT<6*3WC{8y^KkNvs47iLS2|Qz?VzC(>1~D6Scp4k! z^CAs$M{Y%{jKPOI%BMFif=FAHSS(0JELLgPBfcn2d;PuE)2k}&hd1W)^|pxeKU@;Y zFr5ElWVy-ZsvwUu>+?y=pBI9aW*)F>Okp^YM!ZZOrCFKXR3ix)BDq%F5?R48HI&~p znp{4(UY^vfspz!(;?ei0V;vlZ7PoNWoK|f4{E)%o%n{Sa6-f2gTV`Y5mN0X_K(5iM z#H&XwWZyh9)ucP{} zqoyZpdyvf}+jPm~+k7H16fVAL^^w@>qkYfN1q;K8;P+CgMX@@;bg{<=GrF^a@`DsuxojXmCro*Vt3oamjt`FR|&frzp=Z0|GlL z{(RB5_m|wX)iYPu9=Z~fYI2WUhdjTGRntQ2(wxoS9aGvM>q|nk=B3{sJh<*m9@9zA zsy#bJ&bEno7T_HubF8B}JC#O!>SV1u8+OMfz}rfF7V%}3=ro}D;B^${_wWUawS4R2 z`f1|PMooWSioS7H)8ck^Bm>8HX3yOh8|JlxEOK5HMD!s!V?K z3PiO&0LkaB^;}!o-bKiUNdVTV*Cs8>YOI)$>lsf6E`ZT^Kr} zMP*pMK;yJzK#Q-K>(diXHr;w37f{`QP;B+Ht0grRzPjOz`WR-?S~K!iQ)0{WDg(;k zNrm=zjd<}1>nD50P^b#BLrUrQ^% zaxNXoX}>21ms(ZvjP8v2XE6muTYnS5cb0&Rm8nC7u6Pjg@&+CMQdi^D!?%DyO?jOebVJeY$r0^}?K z9fmz;6w7LhoF&s<`-8~P6!nPkCgV&OBSv0QR7RZRac9_$>3!~Tgw0)TjuoEAeF@h$ zn@^(WiBIF@H(FqZrhPi`vMoKC*)_j3G%OJfV7(YP6WmjrRKPR+7Of&0Ds+$_JiSCXRju%pxlG9uKl9b@?fYjG|Z?rs%i<3nB z%+3$4K`z`Pe2}-@BE}%e&c#F4&Kh$Nx|j;@gqV^qM=wO!x!a7$0x_s6@rkDF&Q8^d zRtqs(HLZ{kYB zl|u6`a045oBTS-YTd?KT`P9wW#m~&S%4<>^05f#AyK-i`w#dQB1g;3f?XJttQ;wud z(Q6IWH=PGnkt9O43N6JGovd!9$BiNaYw`C{0kB3-ev0~KQ(?j<&a*I89Xq~o((0y) z$^0?-l@@`d?fr&qrw;po?{6JG(+JQsKgp~%9@kKjQl2A*cT^J{*>}s8=|sy~4|lzl zt%Cj#v+fq)nOUvY9w-A&8-MR`$7wM!ZUgv#&8b@Zd2L5|`ggbmJLSIayH!Se+=>^h z!0C3t{q8rI5#wZb<)142&kU@Qc4lC7z1xQu1s8*WiR>8bOwhzro$M~?s;2#(Jcj_D z8MXxU!4<@3svrFIS+I@hZo3v#mvL=?9d3mdOE~E=q+4R5L?9G#iF~PLmw3vZb)c(- zxe=)A;gCNa;y^tW#^NH|3PIN^IWRPc4Bzwol06-=45^n^+lUU8ZCQ={04>&gWs@LN zt2KFbvLD91M#lgw_NpK_5Fj(Lc%nrZ`3}0aR;vBnJ*7PTt*l26-3;4YMDBzKc6`mm z$9efpMth8!a}lG!`_HLjXXW#OlPAU4b5H?x0jv7TVY2w=Hq8;Q02Zq#klp|!aRJYI ztK5Be7+WCUpQKADciJTeJn@4mODpH23>^YyySAh}8w5xu8>>F-VfNUjx{!d=`c!fP z##-z-Q-&iIS9f(-rwnE+Dzx6(Bn_^2+EoY_oc~lt3)bvws$sAvQ%cn*{QjUOcNCI_ z&$j%2Xx4Z0n+_yEp7j!$DJ1(jB-^Hs<{7S~mD;P(G~xmBsP`4wm&i1py&8>=df%9T zshpSPu?<-FHB`!@;WAY6lnt)Qx|R1@rnKvo^>Y4N4*yuP`uPuYSPN&xO7l3~Ge$xd zX_ZI@RxY&JFsb9M#1uQJuFKGC;x6sH$3J5hB%`FG`j@;>sxY%6rpKTrD;cPti3J*z zi^(gM{-{F`J_}?1ct;X9*QHy?=nCCFlTy0NRL^Y}bmHeqrR)?1U=Hh(&)&Usb#`pu z_KAQ4ci&iYyx^DKr>n+6B=Q`rFTJjG?B@JkuOp@W@W-agc0YVpU|mjvTgQ8YPW(-c zUH5cD0|8R@2R?fJg~>!y;sw#C>_`%aD`si%buE}%DUNvdSkqG9`_pLt0UK_#`Nupj zeQLLM$J>S91oP?ya;0l{#3yQ&_M1bXvJkZiM)b$%pGcL3gOkr) zX_YIeC(z$*YGq@N?|1!Rvmi38_PaPz*ZWi@8(YPUU-6 zg1viB=ZK*}0ME)N7rEWggjY3aydG6Kk=Dh<`Oh7=qka;Y*MW2*x|TO4e|+VmtT*;| zwT27Ub#^BP+@bpkr;|lrzrr(@_F&%T;9cq$j#U|kU!h3T&Pa4vg)vCHpAlVW0fLL@ zf%OlBEECdrovH$svvMx(3lS|;tq>Zo7L~uf7^V|Eew*_X> z3CCmQ`}+ws0f|Q3C%w^ke^kZm53o@+#L9>cU3WIo(334G?tfjT21dK&Hk>*BZ@7oyMPp^xlsEy83>bj&k%_GL!V4-uIlc!*B@TDAG=h9wyjuVOs zxljT(JQiC$RINAX7Xzh%d#N)6B%*BHbyyZ{oyjuuL^e;%WYdA~=x&aOPUVA`;8BO4 z^1=FGgO8J^=`2Cpf*7Mrm?!Zvb5d_%59m+q0|NTfq20bdPACU3Vw}Af!gH={gA|%& zRm#;);;z<|jwbXL*2c9#;+#-C7u@`Tj&Sm|O4Zs)lhNIuk0OD5=h8(sT@A|eA4>#8 zqr2Uj{vTrTs*`ogbV{VgFdU}kgJoeo?3e_es=E&MlhVg6L;$PM-?tCISfvTpw!4xB z*J9_o;IRnlQGEt~COComEt!aLh%g$jF zW$w?q8K*76cZuSqPj9`f@18DWSqi~}_LpMrP&{2hvQg)BCOxFZowAf*w35!%)d^6w zs+p=EOoiZ#*4RBB=>(OwDF$~{NTuVAQ+n?th)G8x-fQzF49&N{s_r6FNP#fQBV)$0 zbvZV=!vIRs#oQ>$c+ zxti)Jor+4=#6UsJH7ZK}G0+`$7ihv`I{!=vp!TxKwN)YUVo;OY&-Ixil_1v+8$ zB^njVTZdB&4#%;an^02u>gqi-Kl=?}SI`8vK*Y?aW`uS@$(~S7^@LuV&#Ga|jS2Kl zBzWf*Job5{zm90C$7%b}wfZ90Q->BW?SS=V7MGvLLFips)xh-$)*)yYN4iV$F56_& zE5iCC-6cjnO#u?5`$9}6+%UoqVL*W}j9379L!ti`!f^BE)EoB6rrBc9V+g2Q)CC-i zZF=5o+UFseOx{jzNzidlc8F1E~+2^f{m{2_L9Sp7C| zy;jC!3M46?*`pkfQkO*V;u#ainEV`Z;Cj|pL8W6p_qBOUKr)@POq(cy-L2R8zWK#B zboL}VRE=59?HlOwVhL(#g>ME<_Qy=kVEYY5%O@SYN(E7N;Fp>ZQ0q>L&UJF|M4UX> zUH_@p0~b$)cYlYc-_&pek}pmvT=g&molv#JRSzMLyEdy#Lu|U@l$UEt0OM&YgJxg@ zCq|H)ABK7=W41u^Tu9KwW{nB7F~`LYTnOQ%?IK~{WFAOpuVS+%gzUsA-CRRTV~)bZ zA!k4%SS>N&(DYYx!!^>SmKYVWow1CxDLCos*P4LiM#XFL^9gcEUaHxIbY!_(9iYxR z^5>0qJ@eG)#1CKjBJKMM8feOh=2W*MMVX{ciFj-=x)^!I1ydo8FT70Pa^8ztgpR-d z_L46`Q^T%Qvy!!8Cu*^Bv*nnZ5{fkPvg1*!oSRqWkZ9-@<1he^po(ey^j#d*0+Yk6 z=~taC`&^1NeY4L1;R*K?v$~*z;82oD=03#Roqe&L z<@~*ci&K`zD`0*(^5hs&bK9IDH2a(|A2Q9>1px|xNeFVhAq|-E7$xV69QV1P>$0*I zho7}U*e{V!M(~bKiq0tQ69;-tu)1{&DIGq0&oUOWo!riE%6{h%2`Y05tPypjAuQPd zI`$G*MbT@HCKLSZalxRLz#;;@j-HjVpzN-jaKkY*PwaUgAehw&c%%1HP)G#t`){Zf|MOHcfYfM1YLje^~!_*q4%SH zwHVdM{c0%;C*uNYz<@+ry({CT$W?USWj9xWd~7(GJZ38tILX~7mlEKbh23?v=Ur{; zL{nAM<@&o3BUKXCUnW#KmzjpTwt_G{w@++kR_ySzo#=wpow!(M!Ct9|Y6)wJRmo*TKnAChMU`a-Bjn<|w;Rbb`XLQK2LK zk_gGR0**F9CF1HT;{yLrsR2*Mg(QNq$&Iz|U4{(bVBCRrY75*^@V3Ynu?Yz9L@$9J3ZL!9egoTy!`t)psv>G4o|PVfruNt*ahMml1~7Arpx;z(Mj1zv|7n zLmjY-1?mY^yA zl(*C~BQ9i>kuNJb*r>Kzq{6E)Gk&DLzZ1;w>@y1kD!&%i6rfpavfCn}@hU~Fp*ZpP z@{6;gkNBcA#;W(CL@Ekf>3}zR9+{h()$T9Q%`<~6teBagvL9ty5%57XEW^}^c}j{q zX!MwmZRwIeGX!7R-kFv7glf<>@!uS0?QAElks);8e!c4gt$X%diMb(<2!kV9@HO zOC?AKUsET+tU5FxLjyNu$E`Lm4Dc9nCj&axSutOh6++yl~QOS{CL*r~LPJSY{0W;u~!rhPe{F}$w)(wYe$)1fYjl+FjGXW}NCYLgrtmb?ZkbE~U*I~7{E z6a%|*CgcWJbHoFdi^b^1sPK^KB12v8Dy@ap7meJPjA(rIBH#)W8i(J(l}L;dolq2F z?S(4cSeUl@2;4AK+;c^>eizJ)vkL<~VLx^a9W2d2h~1LO4O>Mo43uMRj2|S`$|_#q4=*$FNoK z8dNIKCe2uB{zyA#fr|jo;Z{2>yPBzCt16z>bkM9q_kcFYK&!%hg3#^3Rt#D!{?Y-{ z_y07aLSVGE=GZCbAz6(^jomo>o_we&z3CDdCKiX$TcwT6btVbW3D)yg+{Jap?Vty@jSt#tITGtZ()$|BoHRX2CTXY`8jG; zaRJUz_OV-R0(edpZ%jk`+zoiZ4a?D8c7E2_<3AtOVJE5~9aQ%t;!+g}07uYEQF$4y zfMF_ApNg5y+J6pASA6p^N>ED!6w~(HTbmb{1T`aEoX`w%r*Rub=dwvoc016`>bTQL zwdchm#pHp))0(qVak%+E}TY|J#853!1 znx!go$D9plG5BJ5%b*^)Q-gZM&9-tI9KsR--hpD4CP3*!J_JhJE@pZoJ{f&}ci4?~ z8r*wVH|-X8M#hmlaXtoKh27=*w40U-Z>Kgp;|+(oBSE>NjKOEL4?!BS-_X|N$gP8D ze}8OlkmOrqcUCDi-95cfCV)PF>>d9r;2LGN6(tLUj!~;saKRC2*9kMkhJg+aZ{5cV zK^=oXjs>dS*TS%Q8nDY~!iU`HJ^9eHjc7}QH!^r*-~fs3{8Gx+*2@5W>Cs59N=->+ za|0Z>HK-eG-(v8i$SZQFrqEzkov7MdGR=k02rKA9yX~%68qhS?fIBO|1q^}^?DCJH zptXi6TNnTYVmy4CuOca~3LA7vWgjuCpnwW=bAHtsxp&G>lkMIFl~&_EV*eiWoFy5) znqAWy!c=3@_wRpH*sKOKcS>U6oFqt(m>nG1)<}h>xj&i_eol5tmM zs*uje3SBi2`}srVcAP}=#V;y%h+(}{X ze)Ei`P@ULk)`J`F7I5<=XFStgGo2ye#Pn!3 zKW~GrZmXfZ>RphVEril};sSeOM8J2AJ-jig1Iyf(0 zLBH+zzIv*PzBIx2NhU_8a96=zH#8MNF?}fone(G+CYtumN6R4y{!UiV5I=2mGN$#Z z8P}SC7cvy3T*#^M0B@2B%8=+PiIuh$Ni~L2*j=zEoBezgmC>bLho-XAVV+SL)eL|? z=ZH%iOf{xfYX?*kVA(Y|SO*$_$XC(B9|`RQKjw=mb7(G409Vgd7z&~#d$V1d+XM4( z#zSME@^f$iIEF)Z0!w#XR4Gjy4p6sL3A*X`h3rS84j=msA?`CPfIBAsbYj@<(|?K3 zjp_vIHHCI|i!rbv23-5eet>3;ir~)%%tU7lh=Jc&2joH0ME-ia+V+sfd<~x40j4U@ z*a8h-E5!)mAwl~;l@YfZozsDn%8t2mSh9zbR5Pf=oKd@wI3%j-iWo0#{e1KdJ{~^O zC03gOI1h$)Zdx!8%tzWKBK;>~Cgdc!-F3h-_Tc>4wR8kvSA0|gt|sorU{InGqlh!= zln^1Y!8jz_KN0grPFDnE*G-$8@17$;xlSGLC^8uc6;*OEWLvp4}paKGT z4a|Rg$>@Lyl0Kapy}ztO^t5=-@(gVlG|OhQ)A`hRn0CU_pTj0Di0O(b>8>$Jad$z$ zSuyCj6JZF8$n7|{oja~Wz;}x?+krV-SL7ye<@%YYb2_rHZNQ-Jp)r3*`F5?Wt`j4D zu}e!0Dvjibqdc7B#<)Wpq3MnUwM-?3g9rHwbG2Bm0V1#jR&us~h>A27)2c5+I?jD% zusR6tJ*@w=+ycQYR~yG+hyG!x$J0g0z1Y3(xB;iX9{#P)hAEbD^T7x#kb; zYsl)-=@6?TcxD|09g)%Avb^~Lq_nvR$E^AJ>`O8GU~*E8QJhB?O`5iZi&cEvQqJy3 z8bmrkTzRkA^W12+&YA~Q;tbW_N)mh@{tR?@b`G!zfR8>5ew?)RM#;Yx!>MEZlcG`C zfyU9YO$r^dxgF;f4?_>!2X1Tc#%P}1#>84bl!^d1VC?#tLTnt;pQ~!702;VMd*D1l z;c~5zsG5hS?8V=Fv&>5OpIa|c2w)zA%*uBPNG-K>PW$Io;wB_T@oCW zyt%m(K5NLQ6M5-G?W+4Vole@x1P_eZ*UB(W3%D|xSXN%tOY(gZX!o?{+ZOjXlmh0; zE0o@`NKdsKlY?r8Gm4ckA%@3t2+kl^cgsHDY`p4}8w5?x)rsYP%?a0Q25o z5(j*E&&Xsi-m}u5D54kPO5ijD+0Dt5%eW<>U(1zMI?{ zuX4eM&yzIIWj{;)VUfJ%noB6lIKEFdvs#$xQbNmIh4Tw)D_8&FuFV9V;cX51~ zt9tkeFI1R=Yi$>6jaNGk`W?-*@8+STe#m=y4JlA5BgWCE6Ge#Di%AuQYzr&g9G;qj z&+DDNkWO$<9Dff>`&rXeHH=*~Y;H0MObT^j3~?6*N8h8mUNv0x?7@sI0We3Dd9Umr z+wmbH-9vPK)&H@q@aRML%WJ&iIT3&J3hG}h-^T!jFdjffY_ISJFR$;*R}X)w8a7$% zGv&Ad=qx5zgbDuG%=Md|;hCYOShlaAB+fUQM*-uv7R+UiFB^<6bMKftgke`Qn=y4J zOyqh1RIFA;d_uK^q>Ts}{N)wZ`nlI7{RrqEw&tU~rsx zN4Wh3p{;kHXr=!cCn|B7cv_F=9kT`I`w!NT0d_^Lei|LF?R9DbaNhXch&Ogc9ZD>e zt7emTPo$14(X5N-CNk0`uDsP%HH=yBO%Ex>goe}JA?F&B(ygpdc2+%oq-O4XYkEvm zfmA87{oE&4_HO|~w?9uUH4H$EUC}vSs8E=v0@*WchVgY{*l2OMo}ZK-96Tfw<9D3# zTdS>vm_>t(ITog_Fwf@nh^6Wgf{9KqVQu9f4z8 z)F)H+OMxG(4MEP=9KuKdoku$cp%7rurjWFD+8>yZBevOqoT7zxNY)(8ew+sH?KkM3 zRNg;1diNXZ@%=OoekzfMZ^E{NHN>agZo87#jUVGACRUBy?6kFjd|^+bbo$w8>+wC# zz*&fAAlbrW-Key#VY$d0^2L>%6q=p2i6WeKPd|~uu7?73x?|}+?Mt!rE$FesNxR$_ zH&9{i`%=s_Zne)IEXRG?7pYLUf1OOZ;9O`d+Qy!UC;VbPL_O_s&$#$~=-K+hYG1e| z1bMRS;O^+Uy^mE7)o#D;mbrx@M7zhNo`m*{rK8myzU{`G%Z(aes8$*`pPYj&UZ9|s zX6XZgGbb`{_Q9liL$&YxN!|$wH2cFQDy(ZOy71wz z$*RF|M-H>|4G`!e7+Vx8AJV^r=T?_bpTtZJ-EI z!+V{FKtW98yD$qs_Q)scx<7twBc{9x_fH zO+7kV@B12wZAE6{MJMV!QPJ?6H#Ehgr{THz4}H#_unJYH_scPK1XU-O-KVw>;A!1| zR^Cl3j92wJ@29UTMtXFzz%NMh|K;_A%ay&z8>1KW6%C>Bs*^qc%|<*JWl(8xqQO) z-zQsG?8m)U_H?4da~0;j@r>+sEtM5iJ~!=;YCA3Z-t)@C4vI6hpXx3gPdBoSE^+MM zkCPsH{Mwu1OCX-;O8mHqx$MgTyjD<`*jjC8aHbpkzN%d8pli*Vcbk_1*G`lafqi>j>x{ca=J(S=Q)fohT{dE z8K`v?atYY%p7wee4hMOkRSa9ff@p>M!u3%7o%r!LGmRa+%+({)Q|6fcs{MuSo>=VE zfHzy@Y$M?p-h0RU1aLjo~sWx6!vx*M*wAQj3^^FTauU>& z&A4g<+jhnr69T}j_!N46dt;9{-<1B#%A(dKtzKD(JriYCi?Nz`35vPx&jjBK=ufhWGb^f%e&2(0yvM?}64?#W!@N;kev-npHvcRZkjviD1 zi_4HdGL|ENjJ?2Ln8h;l?fCJkDD%huW|))V0#F~V!a#^9aL)31V<*!w-`vaW=ygay z8LKq&LO~j1zM0;&&)*Y56f?d6>esu3#F&Q{C_>fKx`Y)#Y~;%(zrX|ebbW%7MYOx05K#BaPp(coPJPdsiui@^xpF_Oa?$1 z6K0G9r%N3HR^WfQfEEs{0eFC3vWgK3f16-XrufPHtZVy=X>xl&UrT|ieeE;QK4YbO z!ZF>n1lQ=9M27@->QCl8@9ayFxX;Ua0{Sqmt#IfcE&w3^L!bpmv6X!a=3Lc}Ii3aY zS=?OZ-pMjvv-|&ed-r%K(>H#+Rdmq#ER|HUg>0mvj1C*hmMyLGp%g+c{bfy_o$ z!cR?)a`7$J_xNVSM`Msl(1rZA3f}`BAe3yzj90ZxXd(h#blCmY zAbL{|9|ggr6gmLi4pkse?>Z!EBVo_n!P|Ap*wZsJ(2dfdL__f5-v1>*DwKWkjvJ*w ziT3`m)KgI5!`qSivrEBS_RUK|Ef*a%G4!Yed=wJ(G{ z?!`oH)DwMOUewPEIYHP2)d$%**E zD#(XWD>SX$l!^qZKM0NsiDHMjme~T&*PvE}u*RYp^)Q6@q?rG)p&(%Br~eu-90MU} zT~91VZJi+O6Nev|cn{KhZSkvGX(9yvk%IFDhPkaKx&ZWV!HG~CBESIhr#!-du0n%$ zs%9>Yl9sICDTZ4QeWdFZ+5Hx{XFBb_1nc@T+<6sY7Js?bg?RLv6k>6It^#fklDGO8 z`B`SS30@R*62U|#VT$?+h7{gZLmU8tXj^cFX}c&m3w2k^vE+aM(2YWD(Iu6aZTW3v z-sLHmKO@2wO?c}!@G&smC_ZZ7{(|N?54%ON&_|@}@PPc5xWa_IgxxQUWq*R!7fiZB zsQY@!FQSjh;f6sTHxop|z?2ePnJnIU3tDTmPVRfu(&Jt69&Koyw(#%79)K+M_du$= z;n3P*^)0CV#D^YkLzs)iZSo3Z2~>g+9`T`>J8IFvKsWV6n`TBp4vi$th$J}Zu2WpgPorx#v8^5!3{6oG2-ppY@q7nV1S84CK5%hsEwom84y8% z4w&d%Q3BSYX#;_u6@VWo52znCbE0(N!e6O=$`-f^sNg4Jf--R~#Q6ZOr4a&i344xh zH^MGxw2e}*FU`(D!PLd}s8@5*lj3JbDblN#>tr*yqmz`5YP8^?@?3i6h zw@qznEO<3G7P^Hui~!o8^Fp!~F|2C~izWM$18Q)A@aTiC(l$2=6oD;qIDv@J1I(P- z;opRK8DXqWCFjf_mugkuGHhWZ&T6zG(1EECkBH)Z7$vn_acMz|B<{#>CiNkhwh|FHm?6SIa#J8{tDa(5Z= z<2NFfd*9Wy><4Vz!WNVsj~4A?(N6$*}wCRW6#=9~$zwJj=tQe^+rgsTx- zqgeCc$cb%u;$iZ`rqmxu#{2<}Muk=fs1-#?P*p>xBoCS(Ah#a2yMU4hyMVi6NH7fH zT;KW5&|^C4waJI{>u9XOCvhJFz1%1;PXMXkEhUJT189vOFJo?>gXU{^Pa5O_{3lKb z{5WC1!axeA-4?+Ju^6Ul1T4`s4TH{gYP^2<4pBC?m8h>cu!0H^eYgQ}`+wx7Ag&)F zu73sI8CW7$Wp8J8encb-NrkoNHnZ?dsHWWP`2QTP}*ym=*85tXn5rVV>V((Vm!yR&zpL=Ikf9yGa|3< zQY=;8?*6YX9z>H!?b+y^6WU?1$h!LQI3V`(e}&})W=D2VG&=z!~rCwD`0YcXi5VON$LCdGbKUWgS7>SG-eG4B3w zz~j%&6Nx=UXh;_jXV`OxK_3Dkp(-f=CFeSE0_ZH5@TmfP2!TS!z{50Rm0=wPq71c9 zzQ;U*rq?*ETV^8-fH8?E8A_(SLmKr#iyXShId|ov7A3{x${u@Zaeeit zhT3HBREEQFS%j65dPz^vAggYFgJFALaPaPr3j6Oa=aSD!b_>%xi6Deu^4R=m6_XlqPv*EO`<8YX((^jlx>pDA)1+B&8C-IQy8X?dSg5xzKb=4-{IAJ`{SIbU0RM!$;=OoV~yQeL$%Jkot!jz65 zd%u4_?buH*Pczd%TQ2;_eQ>fn1^x%d zTS<+>?^1UK=ha7`CE~sN(&#sR+XLXgBt7poCgy0{uNNho#y^+wl9arJrm9AXs2N$d zIVyoNEmKu(c82!4kGb>@YGxYl?w7h#o;CzYU6OcWPY-COkTm7!Qe^6(PZ3m-zunmF z6Z$6nY2I!gi{bZ?q^Bh{Q|=VPYD$5+`uhB7clc|LI9l&ww=ekq$}IVk#5#xEyDN`p zF4#+BO7G*%zG^fl)|fpcmcebfd|s~WS4W=4hs>6-+usLQhP%yw#!h)-sNYF)RqP(h zZ=hUx-^A`N7PX5RRrz9U!~5xX-pIypqTJ1uH^h=icor^tff@?mn#*JL5@HZ{e1=ZXP$Bs1@CeVyl8I{svcG{oO~=5&UWI+&uPl zWxdpupx)XZ3}$w!9w?}kajs<)3|E$wG;^!1nQ3o^2g_Hqw7k~`XPvu{UIE@*=S~mE z85la5IZ{~FV0Ao)8B^ULy}lwkfpVo!Rc$SU^x2hV?8@8Lz#F;LN#aI)Og!yoKH69u z&{8IG(j(X0#r?|gs7h7qXIvZv_`OIxS)xG4`SC$=3#d)8()zA6ok}IXc`7?h0vsfc@!Kc^EmY zqXNL;lB5Isd9$|}i?VDbT5r*{W76o$ZBJQuFUfiI;sbm7_MRc@q8&{bb}a8nB4syl1b#Sx$<2-lNV z?t^dPwaZzpDdI;uNM84zE^|L=9--AqdZ0=g-_c20v^mO2;*&q!|0a7$13vP~HsZ+3 zgEIV^bKQncuj_r;$5uSnmUx=tzEqtw3Ta#U%8B}R12|3e-tVQq1UoI`3>|{heh9l7 z0BE)jjcjaiIifHGqz9L~;IflsBmmHKOwm2ZE~ zQ}&Wjv7F!=Tg}F%WsX1wJqgaen<_n&R5Um^Ee|l#m337nxL8B%D_8LnGtH-cSSn;C zckEX4_m>+ipYN968v>_K)6Q!TjdqrJ9YTaiB{-Ek=j(6&{!S8CLg~5FGw6)Y;En6O zKZLIcsIUUw{QSuF6!^ah`(KVxP}Ly)LDAy5t;ESNc$@*@@$P4z4&x4We1N}VASH_3 z8prjK?zNa3Y+-Fny_G8&87Q0kMYhXF!di0|*_7S?=0fW7mRmvkuIJyu;D2`zGFGN z3D%c=$}hb4;sj&poLYcXQsu=s0~MD0F@}r5aPXPw@1j5F_vbNBa!92rX1B<9^$#59 z&7L+-Ro8$inq0ZaRSBm+WE$${=Ab$^OcV{k(w|Krg(f37VnZk?f>;rL*>ZgMmD7VNt=aNau;Jgjq zVATFN$HK+;^_jcOHBI;nS?)hElnukRtit6>Rphm(S|`I>>-Klj=m!m$7Gjc2aDrSE zJ8P>G?R)(Z>+b1sj;j%7oL+wyyU!rysT4!9uus>{rAk_Nr@R?1EO_py-nt~VY35dO z()j#uq(X50fzF1T`b^QJ%J+t+DBY29TyK5mt+b>M6rsJZInggFSD1%y2nv8F@6EC0 zEuiL(r*efD%6HR}DpkxLl1+n}=UgG*Jq^VcIRg0zH~}z#Ei@>o^wnqHDS|SwBDr$K z$?(Xu0$1KK;+UxDz#3gnU${Qekh`{;4KB(=D2j{=8l-nF4))75)p7s&%of}o?Do+m z|Bp9($DVlwXzdOIy9C51r+WWU|wO=b+?2*~Q`JeRa)w#RYbu(y(S?!3fC z?qTt&447pzPyE?BVj%`PL6V^yd2wsngsaOyc9>YWFNbPn6z6!Tsn~#t12#t#*`T;e zG#33Ehu*tC&7bfNPKuptUpELmprvUZ-m2;S2XV|*_cQ!cDuW$GzCUZ_zVkX7o{m6; ztD4FZBES^)zM@ve6{=v33oTtj9$vo%kdMAk8vEt#pwtw@GJykk zPaw~e8BM^a9|9T-hhFekxp%a)X`V`=|Jgewde_&+68DvWm<8||%bo#fB5w^BSku-< z`BJxkc(*)pWLa--{zLi-F%Px?nMxa>LS4C0Jb^aNPn0j3xf>_awMyJM+iWO~mUB@W z`EsqOvn&uV+E+NifTT9t$rNE;G^WDdkGPvu}Uh;39&UyDcn+i-Js1T=t<81pzA9Cf{ zVbd+dP8Ii^On-XvG+u_doChw?emvyvo(ee3YUHCLMlsKxu&V+iXoW?1aI_lfvF%n# zw3e&8z>*tL$kphs#hKZ=6HiuHAYazSS4d0m+bxeGnd#r$2(^fvbqn~n+sFIkW2m`* ze|o*KRm4k>`9V3{7g|8*51b%ah&9541wfI8@;DL6@D)jwtPR7JGSjyA=?frfaGZCM zG4wZZUMk0;uqEJqsj;>IIrZ&)1lZ#0p(8eN%ULa;ka<^lR z8<>7QjFSa%-@o}XjoBbb#1UFk=fyE!v z=)|6SG0)?tWoWCv-rt6->I*;zvh0ZC61Sg%i^JYOiq7U{EJkq*Vw3M~xRDxO8wj+g zv@widxadGb1M@KO@T$d5rWXqX1}a&^RV7pv$?isG#-gvi_5)!eZ8^zS?`pc8YtgPnKHn{HvSbB z^rQkE#!yD0ZG!)TM%HR*vuaUM@`0ixs?{VNtg1s)4tE8l^dx1pu0h`9j44(RmPA#(Qe4qd&P^OZlT&jY>4YtAVd-b zNSgMvv!S2{3RTR!$afhp`Ve#A;1cPLz}F+?g$r@-Z03cGj77#%dKlQ!n9CP7@ETatzp0pnxTf+1)On9N(`^P~+c~BPdeA(Gai*PN^ux@wVH3w>|t)>9up6;I@UM~a2icAccGN1{{ zjN!x@uI7OR@@_;qPPpi2**MCMrA>2KNIsxPZG*5|uIIK zPE^1M&WHEl2QA)g#>mI-UuNZQ+=)2JYBmoOH&3Od&lIWM3Fhw3vO`4Y?d?bfm2AXs z#5x^@0e4qcHM=wsS5SCXRympO#LSGEg_Ttd5^+Va_D;6o3bVEjQyk0tI+r*1_Cr!d z0?lC2yXe4J@hb26AMlovpTqT5Z;Q&ln7arP_{Md7g|KN4*o;+m+HJ0hc&7!>5711c zfZcf6Q_uNbP;&5-2u4BCn1s=(5!^v&i+r9g&ka7K6xh4tn+Vo?_zAQPfh82ADm`Ib z%fzOwLTuy*kdP{adQSt5iz8!DfUt1qPkEmgmH2eBSc(ho5mFo`5C`J8^B{S;dwNin z#}7hV2ZWB1Fh>+=qY0^tPKL4$I27%N`U1!@6zc4xiNZ>t;&UV3n+Bj!2b3SLGyZJ6 zDx!tuUXAMenb}Mdt$=o;&k?fF9v7;r$}$7y$C;)@)U!z5w|{?@R$Pn<0%7`d{w|g~ zLQ#=1A=7(&YJjw{>G3cCgS(%)Kg zPK$wutajrG1E?P!^jYGlU_r}Ns?q}KBm`%yx~QJSlge&mcTAZx}IXn?fCeaJaKVI_cw6r+wH(vED%?zIw^ znSnB4(EPNxY@W1XHmD*Lybc2p5ydLOAMe3Pia0<@l#W$yEs_3U!?HNqat2}->?I{F zL!`k`c_xzRQfHR{g5KUc*{I}^NVv5{CaES*4HkW^pFt-=n7vUPbfv8V3mj*o0)na* zrV-F#NT!_tm5IPILD?-Xi&*Z{v5Y}|4uK;qKrv5EGB)0TfG!f`I3}W698@9D`jJU_ zczu0+oUapei>ba4L}$@r+{Yh5x(LHsA7X`vt4%ETYk~^-9-@UIeVq3eblU(lxaiqc z(1Qz>=^4z!sR^BM8q5WDqE49BhZ@NLm40*F^Z@&YCAV@7)MdO!Z*zUJWhMz0ph8^j z;US$IQ|XM9+V!yAiC8DIQHhwPOh>I0NNTjwgrsJY1dpPj2}pCnquOY&V8f#ks~Tgz zcQ&2+Xav6zNvE}oL1vwv*8xDsG9LWX1d;l18ceE2H13b9E3mCI=H3gPt2O3Yr+y9x z_E6dwhb8ofp`%z{Ge{t>Uve{+U^ayz52kaZaM5fR(fO+u;z~g0#FB`>QEftt2xcXx z!#OAbl2G_TgYRTd*&A!G-AGg$IWvY5pr6pPXAw;f@;`nO)F(7C=pNQ=v;tf%r6wi= zE#^cEhTU;ywk@?nEuDxSep?n(6>_p`&;%!f@PU$b3b@u$y}A|Hld_@Q+sI#kjuzm2 z8y+BWCK3QY>+MbGXc)}`k0N^;T3b;Uj1T4=od#MR@4s|8>S3+6FW0R_wFYTiLOtz9 z6$}UgXcOCVYC;8N;sQ^QC}tVtZ!|gqe|M8EC34q5NMR8s%5UWF6x>qc-O8oH_~+pN zaPB~~f_ETj1>;1Z&jh(X{fDBT>QmH$6(D*EjTRi>Ix+mJHjeNVPpRbjTU@`c!|bkm z(WhfYoL1H`L?*0EBp-Asv5h|eT{g5B-Zdz9oF}J92mr(s31jh?KGAh8*Ug0Icrcgm zXoRYUkN*PpI9bu}(~mc~lsF;7w!lDy0LvvbEyG@K9cERxCdp_c3FW4VnIKu|u>sUqPc0o0pK z7!V{2p5JiegAVhEOznY{C&M}Up<4bh%tFid71&*;Ssse11Ui|>I@EM|TSqw?3{0#yQgq4=hgsbG(> z!D;`D8ph`l$LDcebd=~MVt7dV5t3orL^@3-#6;~*P8u$Pw-BriPlcliAQ3xPG@ldT zAL(%f>y}s0e9XiokJY`sA#LnFvSk$%aum!e(M;M`g<$mbG8p|~RB9UX2jL9awZL>i zNdr$&%aek+*7;Sfvf*PbT==AMPl_7_A_hqVXaJa;lmY)11nYl12s0k#$frp+(f<3@ zM=BHyH+SiU#c`ivf$Z45qF~?5&Mp+VEOp znF*auTyZ}r14TPg1&iWY3Oj2)!Sn+9Mpw}{agPy|Qi-fnfMFFn)bnCc8)#J3r;p}| zlINLZbai7!Gm^#((##3L3-gZMiPt9SdE&wAFlGUI9#Ed5uPZ6@|1s;>hj|tALrgDd;S1;lG;VDbT}xSt>~8W`{wu_324GZ0mY;voipkl^Fr zQQ3lEqSFXwv;YH`8XzGeA%1GB^mUno37IqVGuEYd!<1k4J_}KC} z8VxT%>{MKgM27^bNW>HtJvhmNQ=^3V82fEXVZdt;b8=)UQG4)9Ul&(m(vXcqy@Wq7 z9Hx6>j7y|BF>9Dk=zApFQHThV#BLExSKu=b{MN=xu#h5rwm$Ol5t&2^TC^Aj)bIhl zcDTTU9p$+8$Dc%ZDm&JbuvLa?cp+cV}!%PXvfMAwT4j*qPSI$Iy5`?>V zbj(mZ1Hh01!G)L!QKaKUSPC#h;b@*JrqRbU3VX&RGQW-UsbejkL-cWXrE+8KXFC~< zIlI2Ey)($%u>RcFKu^*w&%62x(qqbZA4WI#yDuDv9Ey5ymPuXEPKAN&n>@ARWYY0U zHHlI+&|I_aKl^y15sK5sFVqE}sF7*1*&8;GyT3D`FJi&(OYO~!K<`;BU9PgYhmOp< z`|Akzrx8>pIGV1t+G<-U-#gz1_4|( zUQJNlplrzyHP>&Pmb~xWz_f$o+;pzm&XU56@dB%Hu5+InXSpQf@|Cc0TH?#XqHKSz zny2`<df;n=3}?EtTVq z>pTzh%m%wW$L@|Xo7PjSx+rKlr;a6b*Dd;!r*(u*Nqbwj z0j|qdzR2fk*RNc}7wXU4^K9HDy(A!m*O9mFOrM&>`|&KJGn$meO>H|eJOyb$UdpqG~BY*YOm0k@CE}Qt5KCM(B z`H%$L4x>vpmjOm zo1|ZF=ic3ChSlmmkI?7!23T^cr>;9bleKpz@&bUB?UX-38VU z^Wi$(JyAxKhNb5VO$DYe=+LF^IG8!bIh58l{bS~f)G+we@($GOPYiv=-!HN0z_duoL4H_x2wIN_kNheus31aIbOaT|-# zt6JwynEr6kNTc*H)&GSOS1x1kCdTH(9S0vjuC0F86&FrmAR}Ab5FB_%ft*BL-6Ehk-O7TICty|DB@FTW;+uS6OySiiUpl?WbfJu5xzGiC+7GA>A z{onqI1wP@`uru!vK-g%`*SD$)9*Se4)b({g%vHmofVI(+5=G;Ub%R^Eol68}6#{Pv z5@>&%3mczNY6l5%#Opz-xOm}LCh7gS`>t`vYrn;Pz}JC|)_Tn^uTVQvVI1P?#-4sm7) zDM=NC*HZeP| zxC7A7a$lK<_%mzx*F^5;tjc)`$#(s3iM@Om+Uiak_RuKAw-ELY@Tm}(GV-w9c9S`n zn)s5cutlCpCe3M+Y|>%w*!wv8leOtfEibUXh}BrPZigDr$CUZnC0x8syCZ@>X$s)X zC7f@=H&JhI&#JuHqk_Oqr#!12x?su_3DM^@O1p*oeW3zqy=#((xh$6}L zoF&MFD!B~5Avw!7Q_CquKeYxn|Z#$SVXhdDWCcb8yN|avBkms-yh{-HSn()Yd`e}Qy41yfW#W>kD(;=`>WM&klH`6= zWE%N+sjmSL6|X&K22`lTz62t=8FgS7#B-nFX##*9YU~(rF7P{3K;hLWh5`!j<%c0~ zP4Igu>k`Ba$X_iHm4WR1coU2r#r*8%K*+*GjcINTLia1GZ+uDk&Y zpjTrmiMy-PPXWJIJ5s0aYK)@49RPmWfNsiEL^)y1@Pm9Q4sbzez^|fIbPK1W`1NkC zXcmC1s0O~#ZYA5DTlg5~4`2pT5yJxev@jGTg$l3c?t~+TAHoTtJiQtQ_($-=z;7#o z$XM8`A%&Bua(BHC$j88{dHGd>JJ|nT&&a&Bxi}pDR+^Lul>9Z)5&iBHJN6pTagZe$ zaisIMmgYeIrpy%C@(ri`dL2xeISn4c!1q~|F(@T+%HpyhC6IfEGA{w)6YweLRH1xS zv-lCm$gD8PMyD2wJ{2kojg*yJc*V82814G4+bbFFx8q&nH+$F9ik03dKcWk{uw903 zS+=L_DKXX;cHj(&RaBqt3aL*cxl(0>AHDV6pSq4(d9`sJU6n(6_Hpo%U8@L1WAMu6vrqMB8-Imi4mkoCu)mg^c6NN&3Ww8pHV#2)M1?Z(@MTO}0(ZRQzb%OkNdyPu?^ zm2CQAA}Xm3>-w9=PP}&8Bb zM^r}ELjYU!BG+&)IhCUT#=duPQDItKYlN52zSvW?`>nFhI$nfrzjZ*Wn-DW*&l_zY zdnJxnB)Rtz1-Ay4PE89s-C}GmC5$n{jPs4g{-ICdRFRefQ^#Tw}i7#Kgv0>ErMH{ zBTciP8QG<4a{ko)C>O&IVOe`RviRVXBk@7kDtp)3*0_915DEb=YU@0v^0(&q#D^Nn znT3VBIlMHftY<*rn)MQ6{(y=Y-2Mna;gAW*Ye@D;i`^UHmX&C#Jv|g&E8FI*6We~u z(b0cvY8ma_swi5zXhj>pTGx`vo@=G&g#sOOt(=!rc0XkEYcby1{?7hXx1nZdgS%jf zWpUlIHK#Kz5@vi^Y2=(!(3Kt<_dHnfrV>DtZ`0>xV!>He{s?Mxo^fR%|L+uc6Z0V< z#fuBv4?5S{Eo!cyD2dY&&bBqkRe^y!9K_q!WraEtD?4A1=s{ADnR}=p3ytW^v)<06}If9C^bZEwQ{owU`F)YT-s&Q ze(a0V@zqBGhvknx%Sj?@kw{D-l8D)goKi~HirM=)~prYZR ztxO{uOj*S0n%Pz@Rm&(^nHFE~^vIE)7K3wJHQ`Y<1QP?&wk7Vzr-@r;bWS3ga6(i%$5k{J#{ zQSGxCkTW5i7^#cm5yJ1AWtr0UE_ zc;`;DxO-wv(QiskgiHGYP~ECEo!x+u-o{o+|6PrT-OEET)zPHo3nzb$-0p}-Z#S=R z^`M7JXq0n&&HU8tnDNAB1boFMoLJ{Q%IDHc8q;)lzpGRo3bI2$X0GMsIU^4%J1Z~f zs@0g8LDOidGFdJ)?Em|N0!rQU`vCv`ZlCzpzdB^XwpoZM<$yM0 z(|7+FyD!46ae9lk^L<2DnB}fL2zQJ7!+DTl0xSpFe5;L_Q|5s3+$E%|@pOE~hBP)P z6r2H0kT@9N$Q$K%wj+R-|JqX3U0H}S9>K`YdE^gV(Etl&cMFwIyKAjE8v+H?UCY@E zSM~HuyK$y{h~-sZIurYLD9CBh-t%=fRH2;ckKu=FfCl)N_(IbP6`C`QCOeh?eQSOM6gGADXBfq!;vefp*14Np)m4CKN0 z;60#y;7Y;cRhgX-2jvG5(_2s+m;}zzW^7ce?K}Q)gH^h95^voET>X0tIM1loja&{o!*@k^y?)I7eV>NSX~e@Ed3+0U!t`Pm+;>;HrOXz3qr{{QKK|99~dSSQGU z;59zuvB#0vn*8iVrAd_OtraDX6SvN= zd&g_^o;-Hzm7|QFsWfu++RnS&Ts6lZp?KNR@mh~lPV8h|KYQ!`RMve&7(+q28kw@n zEuwAB)f967BHNm<^b?;Sgj_N+`HUWXUSI_pFQeded?+_)i~9h)5}ux_(qN%{c_+e{ zA*Ikzi7EHTU>IcXrRJ*6vxGo{U2C=9rk{{=l=|6!vBjA_xpN)^cpZ#}s{Iyhb4W8>r>E0AUAhHKC`#`WdJeVA5KdFU`o6{emnC5I=bjwo5Rx06 zSJP%>xQ52J@hbl!V+E{f=vVYt>73N@G<_y^_7pMv%=lnPB0+pS3?LTJK4_Dg)L*a99p#98cMXUU8Yfx@8Hk zU#C~@k23f9#%ra%s166|N^sILB36vodX7=Y`_*zsE5;K*6^(c*#6y(t+xxgdEG#!! zZsK_1JE|AZJMLyuEsJ?v9wofkE<(I+uRm5 z&XHrtljpwOvk&M#=LXQm z-ip3CpgXF@5vI5II>++ok>J<8+<2I$w(Gn9*G@IDF1iTcH-F3~DK|Swf;!Go+vJ$_ z=jehZgY*?IA9}>Jd*dGl!j}4OKosTD}P0#meIutTGZ~lNV ze$IK*qtTfL%UbGNdz&Kp>9ZM5d3r`sux*08Mt zN6Yn^n$Y-A#-pIV`5Oo34JOvM+us{s``d2=)koWVw!6%K{{9nxxMAh_yQWXV$Z3B* zaM}W&W;BW~TwB$3V%TPbh{T!Uw$+Ne1J_hOj)Xb)o!x=O)tq{g4!Vy;o%MOAEo%mK zmuj+T^nHffbFEdkMfDd3FqRxJw7*kr_-n1&Z|kZuey!y{tNNsAo1(Mro+iuL)zUB~ zy!5rQg*aXu1EZ%fu_2ZQe=T3*6e$lc2ZpGd<{qDqF#XEL6{I*v%Txfl9bI2p*8*<)>7wVlx z?Z`^$hjKkn*}s`P&~B5YmMFqsGJW#PQGS3Y@!+#~uwBFOmy4|mk6+&1C}+MmiMsRr zg5EQZ?6Q}lUG(#d>w@RlW4@tXXit(eYN60`IaSwZgTfk)KK_=H@RXcAoFZOi_P}_4yW&5GJW#P(QRI* ztwL&-=DNQUDZ8Y{L^QsL&guogaR4RBEicoJdDjANj;uQ)H#{oD=4mL(zx!+1aNw-O zOLh`wYHL2rH+Hx3cWzR9G0p5%eX-~b`NpnR;nyqd26}p%FSviUlaTisENCL-j=u$C$nUh5yy3HPXZIz~fF2TV>DBJwC11JPcnS$e_57Hs82;i9dMx=y&sJ znZHpPFNRFq#FV91TAMsJ8+$06k$U&B9&ba~&8)uF2iQ$wGG8t~`+mE4(`(Pe^(3W{KcCvss!JV7 zc{X!2?v^6_#pnL<#fEb_)76cG?fQKM3QZwtIHNAUs++x{to9%$Kkdb>9J`Y zpFOU8fKNfkep);)#51t(R_@^jIj&Brb*T(xSntrDw&ul|H-GPmIlO?U6AhoJGxone z00Zzb4|p;6!#ecg=;f7M^L9%A)4q#pHVvO6HHUw(9DC@}?~{AU^_7{55`4%WPwJ`r z+P}F)Ph#w$9cP1@=l-(uYqx8QlsW28@=%QoTa( z5u5cpO7Jlk8r4I!Eyipm9CcoXWMpq=%eD6hVud=yZipn@)SZ)HwbG&A^{iB#>kY}e zDgVyZH=_U27jvHF@^l6~Ni_<0|WE;8m8G$hK{34c~Lxe~#z#911 zMFC^(y8exxp4-Pt(YpVAg5XhU27Z71?UeTwx7GX8*~7f@^0DS12X<}l%3VC*A1*NU zwy9r@WD5m?=#0lV1m^Xt9na4j?OOOD*LfHGR)=5s#mzuda@x@mo4-05AGK2M6=+UV4>w;(FY*%3T|1J}L7ujV8T-(Ox&frU(_(x6O z#6R|J;MAP*F^{Msn%TH-TcF0?|Ef{kmW3swp>vt&eJHQ>EuT)B;d10@WuByc^GLwE z?$?`cjSBft<{3XmsB-9^Qe+4qvd-wTwLY1fO|O;H6M}z|-ejNjb?v-xT;dNqYGM8d z@xWNGC(X|rg*r5rUiET~bP%52uijp2APGM-o;{=gX#4UKpTu<~&Rc>XJ?c7e%^>ij zbl@+19leBuX4CU?bZEi~eD(Ijs~5NYWt<-{sNUZBR7TT1(m}M9X)8Q`AiT6jvu$i< zpq4c4`-=@<$f~Oo9~fV*zqlox?lsiYbgF*a7QJ8Ea*oliO7i?l15+#m_($6huXAVt zi)O;NHXG~4pEz_xiCdao+EPc&SWJ`mDj{&@J64!CRv3Uw`T@3f&zu2Ug-d0Ddy_aP z?7Xf~VjPqfcX|F!yR>*=iu~;bUE+ZsHALRu(Abw`e^qt!O`RUzEvGMRGy>xN0M5!l!KYjJwmrR;QT7VYCw_)W!XYOpJ%lW zq{6w&mg{!xSu&jmwU)D{(rD}UN4f2{j|%te=v{RMX9`^%c2W9^y7C`i!Edh&i_Pkk zTKItug&Knw)$z4L=KQI=XpFe$EV>@c)sR+U>|+tpn|y6XJ!y)e3cg=e}38v@Xq0=DGflBWKPP?j6<5$8rZr7(mylVsLt_ z*GUx1Lsqjz2K-8w&4nMy*v~YfB^LL1jZ(ICI}Pt5S(>G?N=I+Q-=&xbfI`&xcO$9Q zzkcTyy{Qm$zz_qpbzby__%#+>Zr^;N4c=Jg(}+Ma_g7Pwu+n(Ng)Z%YFYVtq2FmE2 zZVx%UjD{j!LWy)}f2fY0UwkXD_@DT?qiG3Ns%*Gl?+kA~Is>K#uFXfV^g^#K)dEl@ zfFENVL|1q14vf8ba{O9pwBq&<_z7$75lb_n;s`rE;YO3<~A&XI+8TFetaFCG@7_)xp(6y^2qYDgoMo5nR)mkuHPHt@n3HJZrbMT!gwa@a6-I43pXDmF8Kug;nS$-sbQ zD4Povb0xCp)5#WV6TcaJiB!8i*T#-%lr0#D$pGlEw#*_P6_1x$Sx0Q82yr8}0-7Tu zntPYgb7xFrO}1JcwR15n7rTyl6dg|BoAQNSbl?1(>I}rn6ZSc;+|x-GCcMJR`Pm|A z$_rDR3A-TN$79?x{5Y2{e_*W^fpgfbt8+F?-`mVx883736>AmPIIu@?eplIy(9nc@t0jgrWgMf+is<3_7q z)#+V`-{a8VaBr=$Dfc}wJxpgw_9z>6NQTYa3eZ=LQ&~y2!p=M9Tai; zt~Q9l;#-a2x4e6>DXwtjR{r(1!=X?5-CMG>OMqzZ1?%$=LI`*g`yOQ-#_rBTQCu~9 zK<#YPAnB<_w#r$AMKJTAsldWXfbEW#0RpyeLdA4qtJuJ`{mY~C|F7OL`g3i6?_Yv< zkUKaTDH>DJ0%yX0x&V<)P``@uCg%d{X<~Un1~>~E)fjSxXnLwflA1=oN$pt^Zh@TN zbg-%P4FW#JkKvk=xNd;I{t9qpJl}oI!_nM?90k!X@px20tkLt`i@NB?P?CT3Hd-|k zhAwj_W+d0N2c8xhvpxGo)0=B_ua)Qgd8}e8~ zFIDL|vwnq>q$eX3xWe=b@}VZf08{8m! z7X9D7WaVFiHvzKEWEUcv0qofK@-LS2!AO_yw3eHe+%Dg}Gx&d#LC*kcU}96hp$@sC zj(QRGL;vBB^}Lpu5?yL7&#dZecg_QWQ*p(X9_htOi2c-Xu~g8@{h$}%8kLZ=)`u{! zIhEB?o4#Od_;%Z9j{4RC>+f$pK4h>}y;=Q7V^0jODS^V1$y?e9Z5VjfkD*NqxZR%{t40=($q*i4X3t?ZDf(xsEZsj zYWA<}-SMpEhPr##D2wty{V@FO&l@u}R3_QHFKXW}m=TCNfUFk6_2w>couCAmouu8} zad9H4n}k9jcvACa=yyQ5prQylO_YWyh0P>_RqS*Z9uySr6iB#2kY!}y*QJVZ%>QN{ z-q7$d&swr!db4gR3(4OYa=T5}E?jXD|KiFPLzenSt>Ijwt7?D9=>NjFSgT}+geR0r z2R4*lKo0;GQ~$qEaQ~8~q*nKrpcEZ8c}RP z3Mnq6p$h-c7we(snT}R;X`wQvmmtE|Nt`TLHv)_QS%#B)-QQ)d#2=wJ|SPz}r zz&n^fHk{##RL=H6W0z-)(Rz>{E=>?|sBGFUKG>aY2%H#lFvlj0e0CD0+#JNq)Gm$q zOx?x$&Vxf9fb42qZW@*Xwm<^|n`3yxk5MBBix-!+3N#1L>gR2_U><)il?C^g4GuJU zjXpnsAwy6G(jMV+9nk3fAEI_t>~MLCybXECCF1h~%Iv}nPHKYjyn&duLZn}a4gt{g z_6oR4R9wct#4v6T7U!VCsECM@*T;x_IjA%3N^ME z1s?he2UY8LFn#J>Ohg@}!VgCM7e(v&AA3QQ%Ls%**ed9Ma8Be-*Fa#phj131&n#V% z#+FB}K*0lDg5n$idK>%10kJSHJYCMu2^yLkhe$X#MogJ03f!`yF7+J)e-U^(W#e@p~Z$r;gu@f(B&RnmoN_V1mt}^ zByRr;{ZL?ugoD`zCw1#QQoX++&uF78N{;Ety(KjQ>LCn7IGJ$2vq=nD#^L{I@4dsh zZ2v!C4J)#eRVWcIa>7NB za}c5gQf(8Un;+0mz))4h0`YJY4I7S6%0ofbK48;7#?w=`{6l`?P_7NqqGVn~8W=X( zNE*rcMOryYUhSxf*`cXg_EGq-uu9CSZI?u{C{lNjm;=|u0m&GmTp@4Czv&*tNEU(D zhyW`f8c)a_M?xC85iUv?2*9O)_L5|Vgf@i7G_bIGpzi|~o!!_EoDv+ZNfcHZOn-x z1ljw`)(MWo!Yu_cHwYF`N1Y$4OKmxaWKV)9S(_cUUHZve% zuT*y`@UCPGr8FoZ0*7pZ4*3k+@us45^(IIh{D*cbfFMM%C~SDHJUXZ0D@0KAGTNJS zfD>+^gO&mT%GcnWAX>=P3Kv$keJTd=skJjip`n+6GgixQCT(bF_yEY?8yokc`z-8x zu9#~x)Z*Pj<%;y+yc5O66~V>1v(Mv6ceO7VE zYOudjKZ!a@?qb_#!M1GdweB}jLaVuWcmMKDB8dTwP@OT~2|7F%Yfb%l_H!P zYZ-$HmgzkWQnU@trPh6mi4$5%q9{DwvB(K7{52PW*n+`=>MSP*_PLMtPd!WW*?i(R zu4{lt3-iI3_=nES2+4G~@ODgDxsGyn439St+@)LwkBFld#kDoa)y9Urx>g1YqFI+Y zIwxIhHNC>uX4nVj0@7D3(tEx>w@j~Vi!bftK~`U%_AGhFmetJLA;p;uUh3;|C2m>+ zucB?=&tI1|#tLRe3aAyAnMRa5G{}=Akk!F~>KtjTHMLG>v#0lTMZlw{H|V7cYL$$k zj)SvjExLL?5cZXd)oBn`BzMuDf93mn>{M!(jLO{8&y%xUQnVi<)*4T*5sXf9zwR=+ z5ouI(uT7+2+B@IEI}jXK$$Q=ux#-Q1#DeCHJ2?OrAM(|49$ zqU`zzadhD(VO>?@n`aJfI0alK(rEENJps-&Ite#GoNKKQ?O&zN@s{bnx?`xL9sS_N zM!Hvw4QIB_-MP}kHsMG^#=X*9_s7)&pV@w|25|Y2@f&Ny&I`aNKRnKcOG=ep`lkMs)ROKeoyiv*-QU)Tr2kPHN6 zUWF`h_UuC_?wd=uO5XQDqqUr_>O9srzaF^b z0AGWBRUPkI;)JU>(S&0Rc!rc4yf$BS*MJ4p&JBl#;%$^|72x$w(CdZm^G>w0 zA7=3qKF9}x9ONxWb|fo-`~n_wvF>YZccg)FKO;0!Ff?qtpdF)_zA~@hg7O$J()K|m zqgoW#r}@^Zw997(oPLvUh~3>_>JpJ%V60g;3tcigvtGyE6}R#3YnTh}##HFgK*UCg zR>^p*#^SK7#v-vIct;z7x$%N}@!0e_xI6f@O?dObM{uDo9=;{-(s?8{CJWe=7(3R0 zGbByOaf0_;!5N*LDWzDp(=?1%>M#gFoMf)l#}tGtZNnQUdi=HQi4vh`jYSaHZT2rH zz%~I^tw|GEp$!u7>ysvG4Y*e9`ebrJWC=%reWvd%^$Rg}xH!3=ST9Le=m>_v`fF~W zPP=|`CeK4yGgNJCM#~Uf%SYf!)2B2dWi(VH!guyDMD+|0e2yjW2No(wZKgEF5M#@<JnyPF^mbWj@ zboSvyV`xSfN*KhFe%p*y{Z>inK$3#V!qcNJ`ckxN4Z`fob5~v~^&ur8iLJs$)9c*I z&@c>=SR_bbOd-{3C!fB9h1DTUgY@Sj^D`cf}kzWsnGVhEVZTS2t40|TOimwhvnfXLYTW@4!}}4fP;j2}3x5&R28sp|CJl1Ji>l4eRI#C#bt% zB|%u;yI|^Zf!DQ}dd3Wp*9Vt7Lfj!0(glpe3%?$CG|WG?<{$)r{wj9zPhY_od#&9i z5EWUa5xT2PqqBsqCaJ_m>N{BVY+Gcwvr$6@O)?$DF}D`bQEs>pABsjNj_gRZXYZ7f z-M|Jp$$k(sEJ$XW6$BaXZhWb8IC8%Mb>Ea#HS)5hz#T_|Ob5zaw~&ZG$h%#0z!cse z6Phv&X_F=d39}*)yTghQ@Oo@F9stH)E&*SLLIVQAt#n6#?66aTc0nS3qgbT62mEig zhsQ@-1zMgKUci0ePrjgN0#F3j)wp6Xa!|11Yml16|vBn_BEq;LiDYLC#k) znl&q~??-ZbA%Y=7x~dKXbOcUMtA4XI%YN&gk@J@UGl2RIJ*;tk-52KfO&0*gEes&1 zp0C{?jUv@;oo~nd@bqhIv1OwnL_X9Mz28(qsSVSlq0T0{J zQjfq#c*F$4w8KcHhOiF86nP2nKw%#MpERQy`*-RBEOJE(hA48RQiAgw1BDSCjVMtf z@G>B4@pgPfz)A?n0|41V7ua+%VS5Nu;b&DH`C2G6%0mBE?3>d>u`O4l1OMDgwU2OC zLQ~zR$;2CTTadsoR>$SLf2id$0JxkmB6%UcMJ{^xrEMYtqA$%d2voCyMQbw&gE~6| zd~D~Tfq#Ib>1s!4F?&|9FLE_pC1qWm&Vce4=aS!n}bP;_dDepQw z_low%U9JzSH93y1%jmdOs|*AxzHLjNjv|m24evm_UqQe{2gm^iNKmpPK?!z8BhE%> zNi9%a&^b)w^rlmCbS_6HMVf*PS5>z-39`#akkt5ruWs5NNnq$_fKg!Gr%ngX2lfES z4klIYdWbxbq?)H?2==3^o7~!^mjh$~2q}b?>_m zz}oSpXb|2pDPcP6GN9pDbgu<<0cQP~H3DoGGGBo6ToB98o&acY%l<_QEi!M$9M2n- zR>;Qcv>4sU76A|doaZw{Xkhc5M1}^qFoPpe3_h|bAw<9X-`RcsU)!Bm(JLOL$qXb* zte>x0HtM>8?=oh9qzE){L^CIV9Se7OfHQLV834T6d)haeq<}A?nZTGOZ$nc0Cf>|y z9w;`WfdK={5pWuGp(xn{YN8hAoy>8hUuWMBf4ps+R8hh4xg@V8RSb_qjbW#cUL7YOVDyr{I-MzAw@1Z2uO zj=9D0$=%u5DI7WhsKgbwCPKXc2ZqVQbhd~Z62WmtzVn2>Ig6BJX!|22Kga0_DE#ME z5c?aJX3LgV;}6JzI7JWQ6u_#x=pjtEXvXtLF^B}7LCrpp4bmGglbNtRLXo1-^%+fFU}@Xp z2ubM>JAqG7rV_RZqJKk$E(rHI>w*#_A%Hr7Mg-%Spi#i~4R2!y9=mPq0OM{q zb^vJQE*fhggss=xL-FsXalGppg(8ib6XSY7K*;KOx@T} zkqPQf!#Y|WaYX746d`seolR8?fi=J<457TUvdf7<$@4Ep%K|&{)*oKt0xYrxvym-8 zOl3KJ05SrohrhU>b)wTCG;7FT1h>1b>TQAFKxm9@&XTa9N;H?0oD|D?n94?6HG=d%+iv87u&fji@`D*cys~4d+(=R5T%&z~^3B zu|ta+pe!NY@=gc*9ylXpN{}%8{FSq~jr^TsKSsECNzo?x3TJf-A)qhd^#TI@cGRFq zZylFWooxhA@vjR2BGT;yyWY&HJz^_(9UuY%bpw%sARXBw10iT`Q*(i*KvSz}7X$$O zjV7R>71TfH9MKXKP{82&f{zfRBT)QrMn~6tz{H@dYyvn$q=CF*9MSVEi|e7Zs*i*@ z{MZy((Rwk0E}&A1eejLz?JENaCBUBVG63X3?v{TANvC|ThRwnGKTId08t ziJ6Vi9ieK0Cay@;-y#(&2z85r;*ls3^d_JJDH{EO5}_WkiT4r36xJ$gcI`PtW$Vfw z_}VGNLWbDfbSuGcS{}gtegyOZWB`a;gK$N+5Sm+{<*>gn{VfcG7SDkD?#)`jmv)lY z&QT!Zh3Oi~lK&p;Zz*8F$XmMaHVPx}*vf8z{B(%7=W*$1fBJ3Mah-QW% zTQvlV2_UAr06C*hZ#a+!a3Cy)>z@EZP69@nh3R+`>LN-KunDZ7o5^>Nt3>R$3NMi9 zw+IO0GJm4|$k9#Nvka6l1L!DFo&;0}36M?SDn-*@;9ek!q8Q)6^bWPy#x5DOz>y6a z1xVW3C2(vC>&E5_h^su`Shln2%V0EJp#AE&e08;g5O?FZ{BM_qrN7I5oht#y2$o6K zd)TW=&uzFA1MNS5dLj^rXOM{v+W}>6Q@Vn`Kqxwt_Q*jr-1b`Uve%A>5CE?udm5Bmwf&J77aO?zyJ)74-YX!3V&f375+PAx`T&A<*u?hbmhj5dU2C zpgRbO|~ip}W*bc+3_J zz{~`-5s`F$k4TDIL`#ZjI04izpm%~Zc zv9VIuG2*nP_nFa48cYAo8e4@0Xzd8}+NpL(+Bbs=%nF(bj4C^>m>@NBv%36Q zgu}OWzH9&^ORPbrA^HMo)b389bvWRepd10}R+tYe24K}qTZ$+s&Y*|44TJ%KKv2sL z5eEc0yz8HeXcX*B6mRjj&4c`7hm+_Q7TDNHU|SDFHgjZ$_2y&!x~g>xWQYj?hK7J4 zPH6Lf-I#vVF;NqP5Pr1T)0H%#ybFLH&|$&noKtv&3#qTGQJ|~0?JNgNhrx}xs=AKJ z4YcHyr`eta)HuRoeBeu)-O_D90>H>ubdmx{F92znf*grf!X{n*o3Gxh@q#qn(*@mt;4$=6KhTN|14`LSU3gBjajbtvy=4*~OVG|3Xb;v#K$l+1YmS~X^dH{eg zAMFKi0#v-!R_+4G0yzqxFu&|T&Os{CpV~I4^ix(-U?a0Sw9dm1b)F`q;S1=RudrfA z5?fY6z-z-Ta6j_Zz(?3esx+H_1$2_ZS8SJjZ6HDlDnStvHZDOE!sXo7J);}aeKM0MO_4>SP%CjdiQ{QGYrumCW5*zdE9+9Cpw z+<<=y47Tpb04Cm)Bevy0B%^Gj&Mo{2xy3Owto)DmG9)mP=fUW}qYg^TYVij7|-I1IDWd!|@og3KH!uJYdzZ)O0Ao&RG zC6K86krxnc>n{y{2*v&>>u$-1|1;8q@C)6{=m%0;n#gM8#=bT4XY)<7nH0->(Yt6} z6EaDG=z%+uzTpC>C3|E?Bn*-00@^NsP=6c)$gL21mq46X3gG)6ak%H2fTIlYKu4Ja zRMQ0NBLR?wk|)jjDR0Lv0woa|F@JNeWB2d@C@5&aj)-(nV@0dV zRgN_1{EjyLmTbPw=?4D7>Cg%-Sb{z9ANk#)ccZskLlV^MI|N8OJU81})n`aRtH4qd-eR&znF|1`r7pc{Ugp5YzygFMCSk@BVi(h{pp>t76AG5T>rF@#XJj18H|Gf}5(3)Ly&JS<*RZ>Z z1E}Bgrc9{uaHFMsNN{#`Len(F_F&*Jvg473mYA2W{Z-(H^$F z$!4zrB%*{3S0JzdiRp^Bb3A&sr%0h;S8=birzBh3Jvg>K(X)Ba0N`%bl=z(aS3q5J zqS#i~b`SM@+7uy?=MY5|>J#VzrGH9Td)v1lRcsBWK{f+&@Y;Wk=i#i}ZtV_?V<9gC z8?k@k^RVH!BUZwRsYW=(wFPMYIZS}`IKAPR4IHq814V;d^IUbCt)iW1gm}Jq>)g9_ zoYD;->OviS0GVg{=QcAC)fGadg5ddG&B4ED25>Y0k%~6eTF{PdsTxOXH|S3^e|y!n zt8?e_or`4|^n4Z&$!{NM6wNaq`%gfAbC8jrmk|`xOMBkLwW~rNy8NNSx>()pFz^Z7j zxSL}AW6}}XU`tAlEr4_ZoIwx{9@HW93ORNe^a)?swWDr?i3rZ4A%gYJ$QvZ2n}aIQ z=y048ZFHn^?y~%!LJ>4NqN)AG=v#dmNZS7}dOll40%+G7B2AqgdF`JgHvrZ-yc?6a zFM%Gy+}mw|<64mOY{mIsX8rz_XP$@V z__IoFdlZ<#wWh!GsM*de9??4cX`xP$c&L<2z?S4}+m+GOyN6VE)H~Q_+6F;c zXb%G%3te#mMuZ*bF8cxW&|bs(sZ~WVwD&3BuEnTx&y*WG3Q8zkSdSUM@-T$J*Eg(wa9wiP+i^yo)Kb5<`emxWj z4Q88xaMdLQ$IU>(8CHazV5ga{uqme@Ivo2l7~hT6T$~#J zR-mnv36!_1u(>Q;zVu4I!+?Ejm=KwYgn=OQu4Gme%wHg{*R);(tos3|fKkBJ_@m*F z9x@YXuz<`Tet@WSTO)-TplMf=ZZqv86FPrEd67wQU;}9P=nU|uRQ8`Tz}CpdJ=n4T zlXGo$zd-})A0RIlW#31OS{fS9L(QsgEJbdtPYkT5E0&>i`fMCJrz2APc23_7`3KPO zLKysOxRJu7Z~#&;6%{zHJ%@6L4`@SD!)DhH;qL!T4>N1pnvQL}9WMP2YNb4Oxno1< zB8>zv6kiZQY_ntU)(+?7A;{kNY|jhmmJyn5g8{3NE+A1Oa@Ka!8qTHeXnR|ajIut2 z!&F`f7vGWige}4lCbPQ{gly7j0;&{RCbUMYb;zJ@7Et<~wvtyF02!ODXx^Rkgu|w8 z*iMHS{VU#W;R3Leu6jq%V@zWY4?rz`NI+fa8UU<+fKTo9j1VIA{3rg`=n7PwisesR z3g2&d9XLP{NL}wJjh=%@UNeedZ7Ox=fO~i_kogEVN;dNBQ%Z|mM@&~cP==_VA{(Qn z>k}x{BJIWsEoydS9|p$81_u1FyV9aq!GXUT%rwJtDeua|Zn-vkBEkb7e!Ddnm`nCZ z`ZJ#<+E6U8i1v6-=x-a+Xr!Hjo#ptFLDcKA!M!7?9e1eZ8wrxJlnqyz1wLNCGaQy6 zFR)nR6m!@2E#oCCUG+u<%^qia<2R4F`-%7&JlLtm-fG*u6rC`a@-25LR|!5NddE9X zegNZghY#PgN21C+FHTd(Vwsz3Q*q_S8d&;gNyYf6T})0Air?@3g5&8LFc#ZH&r_rwQN?O>J!@@dt@}WGGO-x5_bK`;$H|=qw(N=i?TYS2iPjTwSeE7b8R8 z-se3-&VS{Hve|L9LvrnvOijm|?5h+NZ@8T;SI{W`a=VsfIh?UDiY&z)CxRIBXB{iE zfka)PycIvOQlU-IkD`Mgd<+z*50k1naTWHEYCO4)wIclGK7G5``;G^7mc3!4j4?JL zIBZTSW+w!%*C+J^L{*=Zl^5vSkfJ!w=Pu06ddvG)KRF4(&5(|=Rmbv5R)r+_dIvM= z!~u&n%Cd*^4b=HJ2JXg+7<2O4XOa5m)1@AZvDP`0%<_azG(^|a=c(z+71B%PR#b!4 zcwfq^)#{_$Z5rY(+Uc^0wNYpeb zjzk4`SAw6%wSSHG5O=#?EzqufBmK@&@XPOBhH8&PG7X+FEhvQEB2GN2>NC1%mVNzY zJgJa8=7tpB5yA#07BA6rBGUI$LRB97Ea9%!#2PYleD1kppu*oG#?#1LVT$weyvMl6 z;ih-@CRud4PpcX|m$eh|%X`@qTCPqkEB2PwEG<_cvRb_@Xp-x*^TVn4c#O)Eo>M)} zB^%=Q{_jtJnkI>HleEgSdD+BhEN{axR3h$OGUc}V^@j3u0h6DizWR(O`}|fSu5o@z z(=m2q_QG!K{zQAqRO&@R+A#GOQA2#}%7_ON&hCBHv3BwA$NBHymphL`am>SL=>3$X zv<%OE3{|XsdgLE_h`zi(am$RTWrE2|S>P2e$N7Vryunl_Jsfo8PTmYIia!7Dz`iwc z|MMxL7p!}=@;}EHyLDdl)HXhMxZ;-3)OZ@NU>q5#J@pNx4DO@&ZilZ>eY8@_dPJ?L zF~FnD{!=1I`dn|IxlQ+_jYls&hw;LDU185Kv}Yopg(3f@-9=P}%?tar9Py>#jKok=CSHr@HOZ?6Pu=$`B_HJTcq?$4LqB;=y)4z1<4&lSuVlwfC-7FS zkiR-S)X~#pd(TO0d3bFh!xn`*7&rUdc4_VMh6{0VoVFIT{pA|D+6QdR5?5PeRHWRVtb8{;9V%Zs6dWJx0W_^nee$K$#RYn%KUk7JAzsPz| z;;Y~xk;}gymEYHEQ=zsp!}ruvX4X6^9xbWE_@*}YqI=`~%#f3FZ2$BO`O%acjBm@2 z>KL--tZ>lNb3d(@me!D3YkA~p;eX-!A)Iq3vy86o)2}`9DrEf8U58-GlaFrmdQEXN zo<6@;nTaA3#>%aGM#(MBsefl!??jd{I;3E7><80zxS@3Wf0n#%ZNLB zd0f0M$xUg2t1D^9mHAvxiF%)T%pD5c`nPX1`be|xONEUeYGB~8{4ISWE|dPGIeS2G z;%jPy#Nh6WWNv}17n-_fZAU+KWOJ!8{1wvQ@nTK< z&Q~7#@!x2=e(%=m(-@&JDDA_k+{3wjSzUwizXvX5j8Z=w(a8??46ji%IbC_Phx5saWNxP;8|5Im1B6LBYgys&KCMdo@?O^DMcx%@28{&&|c<;A_v zWynn)ZO>s9%YXX*S~}*J66GsIr+v)2RJ<DVFOoDS?;+v8BOD z7cL*bw2MB%pm@NDYvCHBUA)twd0W1=)$$qf;x#-J&iv3mRikAg>PrW1O88Mnjm0fo zEj#++us9u0r9Vbr;tgV9iN!-uK`J6EV^Sj`?=$WNE& zRCA-N=xN@oC)6nJopx0XKkg!`&*IE`S21Kd_2Y(MYmd&?CEmXlQTtx?A8J?_7!xG_ zv50b`i>N6mqNHA>O5Lc3tI_n0o{>1$J8;uluU$9Kb8&D#pc2TJeJMVAVZd>ckDh8c zf$sh31*gWJ6CH*QIm~#Yo`-7g1fQ5(u8zN68TwkCVXUf6mqXhB+H>`1?y5Xu8P^h0 zmqwMcS;?DhF1@n3f4SyhT5w8I3}f~=)<5 zK9fB@l``o59P_oa_o-+6g8ODLgmoe-g+!{aWhq$I*beXSGBpv_SpN`thrTC}paVsW zB@+|Ju3&k~`;f2-iT~p3xCfrJ9)}k>SVO}sI=RG9_g1Jxedj|(ew5+8?C@WcnhpP1 zQ*8DA@tl89Cbkm&3n{Tv6xZmBzhaX6-*|zm&T>x_-{o6_QH)}{QLW9#vz=d;F>~PY zXXN7foY6uh@-_`8H}j$!^Ze*EB$$nyKVf#h}^UEOH5AxtyUdx8vY zs-#g0S=>{(VlwwbzF&{M+Uag~N^JOiDke`nhlzN6PW+w8%2OgdORRLqWAyJ+c|fO2w0>ayc*ymP5mOqg)u=Cr zl}u->N3kD&;#rI6c)_W=0;W+8M~{;+W7(NmVGL@0!^L+s37UL-^n>@=s59|7WYa}Q zJ+RM4xxMW#kIt8VUpRU1OS?)zn_Oq9fq;T-PG_Xf%yS*S=|>xi`nS?WZO^P7*e;Sm z!F3iDCqR*8_*Ifyc7Lk&(WjBmBWu6inNmJoia4v-Gr&%8kg&tE2@Y{2Oy@YY(N0muG)mnaGe)`A)kmAzeJA>T)6W^l06+SsZUyGvp*MD6|ewJtubNEyp)0^66rq@=r)l5=~LpUOhom^9|A6@URR!(icCnr+x zEA{T?>gTiWY;0?4+;tauP5ZBqsa)|_!)hh24Ji8lEqj_N(myLlZlt}Nl0iN?i}%nG zzh`6X1*c~>&gGY_T#0F2{?@Etk?hWq*%B|t$I2~9*%dsOnxnk$Jcoi_ZHOC<(ti90 zb}vKX$E!(;NFJUFZz`gaHl(M;TB^rCHCDp+`Wzw0vlUZTas1) zRt4TVCVxHsK~yR4B-Y&myl?e7+RjyRk@mwsYg=2jc~v9^()mYeF#GPFzbH)|B9Z6i z#&P-mQ@plbA5ZsgzZqip#K4uYwZ^EsEs{QpFIw(+(VR=X+RHY^qmpuQ{J=sOrAGBJ zHZ>MfMm`?H=@vrwny?4hWHjY^uWrcFSXHz(`xGdbxvJWgGT^5dHau+}Ivm}k8`5%5 z?tY$dROsc5XzC=~(PsBzy(pK!g@MIgcxV@&Gv}r7?UqY6=6G}=&Q;8~?|ecUbJfu! z4^fPoRj1>>Vps9YJLa;VKQ+{zi%*Y}A}K5>pe<*8;zBpqQ|+=d7OREfSFL2lcz8<< zs08U7yEvkZL%1(>EGW{dpG}lJU;jX*_UK`|N>+iJL;|nxT)CB$nPRrD_VB@%c84(> z(|p{m(yGZW-R_iR48~t+&HCu7QcD-j`a(|6o~EoLG^h35fs~UEa_tk|zZsQG7y3*Y zeS0C^^)O?E$Zxh!>>WR@++A?>`$cqXC;|QA5*%=#{ z8?YmvoUjs2s$KFu#!FE`zD(z#s#d|3=rs4eWu@fqoc+(% z;xGXjzF`jr^Srz9g93b%*|@TX=8NrCmrV)?$eF@V)ROc%goYJ{I*6=~(RC9O`@aD{ zyrASkw*~!&dr2B2dQ8%*`y+=AmRwS!I<+rj{I!s53umX_?R=G22Zt%1ey}GYeb6=0 zc7{^V(a|7Rsmy`y9ScRvX@LdGpx&6PXt_mwQkX{=D#2fYf>)ZMTKg zTK;gKuff|u)dXF6&9r{ewc+_+hXN}iOt_={+Wqdv%wR_6xM0TV9_g^0d*GUxn}lI3 zzC@d{Eu9bc)R}P#qU-;`H%b&7| zWOv~&?`67KUK6;Gq4VV^QvyAIuc+#398LP&LN5i%VqMR?dzQ_~!xF6-_YaYY9@b3hpUyszqh7CzO^OMU zX(o_XH?P49`{74dQ}uNys9vgPjr4T^)wiW=BSym57@^hqZ!L*|-Xf#s%*xWm%uM0q z^IGdaK5GxSOs_80Ewa<1qR+WdHQBHF#+Kaq*)x;DMRYV~)g zZgF73Qv%v&6t<&O!VVT$6avOy17yY{KT}9Qq&Ro^U@q&+6*{i_9U%|9tu)OFemERD zt9L+4tM}YH8p&~SBKbPi#7>N9Zs|gn9Dy$>X7aN?te)0S`X%8}J06KYb+hNB++&PI za@!}Wy4;k~opDYN5+vg&6*H~A@j5(w^TN>)hjX<+sq)N_+1V(;594&Y_BBr)KbAau zR`Vq12ZP(EaN^J3%SQE#bD2!o6VirXq#%2!UQNh(uJ;Zl>p>s$)80%k3n~_$JT#*x z3>m*sHJ^X~ViSjfBFddV?{j{PI4UPj8`JZRw@WOW-jVP_Hpbcc-V|+5FE~(0GsWEG z52SB>6Z=Er;+1c`x5}{wj7D&t&JbY-nH^p8>d%7lH^2-c>e}e%VL%3Yz>czV=vchz(E2OW7Q# zxYZSvFO0K@{ncS(vAo!vCAL(9{LtY+&DWnl*Iwu^u!<#-BmD4e@Ird~^zo4wH5i9J zzZ9Qh={T0}bTDA)eA)YktIxmlojDRqOiV3lOn|qV(w)_K$Kq~fH2+ZMwCYC_=Eq81 zDuv!sMNOx>TyxZeo!b!YajWX?(lM&ZXaJBgZzeS-~pSSWG&WOCeGm|S{Ib$TIt@6J} z*OD0)ux=h?5M$FKAyj)jU6N7xK^RVyK+p$sRR>h`VV)~g3Gc3(p1|2qw_$w} zj5@}TQ$VRN6g~K+xgtIIqLNWY+>daT8>K1VEeS);b?q;AA-d!rO|1Exm-uLuki;3pArbTESxx|8&ekiFnv5ZFNg4LUQyGR z=1b)MhTxy=8C6@He5HO@@koPiBhNLd#s~*a7gS(TWlEUjVU=*ZRPzJuZ_{&H1ukBj zV6m=zy+U=wT{_LUKJGN7Wo_oC$)Zos9sKe3HykM=D@_~4NoDMJ81cTFl&zvjlxqJ% z?wNe&3+CeJe*xhdj@T zuw0LDE&cYnSnk!h$aTr~~Z0(%P3~XWX`lN9)AmJjZ>wx~ofvLC_ zFTV@;^Dah^@tc0@X}R=HY~Sjj*M~ekK6+->ZX$j5Q+ESrZofV&WfKgEmw~Y*Z5p)XG0!Wy@yHkGdW|qxkOd z;42dvr8BFc?D9{38YiFCIIHxPSSF<{rBDUQcM56K1cGo`E9-8@Mscoy-e~`>V}DCWW*Hb7m5;<3tlMh zSFAt%rBqMSo&Pvf&0^jfmWCuoyjP?^;dK3HX5E>?ZO=+i9=cHUoJmEgp7ZHv-IT^I z+C@v>Z`j9KS_s^l4u;BJP{QJvm%u;1f^Dnep^vLGH+FZNXDPqygMgNlt;rJ6gJ0|ftN2DBRux`+EV4%FI8fGGJG22^P(3>?->28ev63O?`mug^#PxbCm6nF1$uq! z$&Sx%d^grSGyZr&z-lGJ>}mOBTH7N{m#>SnGS9r=vo+`xl6It0)SrrP8W&I67ca25 zLc)L7yNFx68;^VHtjMeH6W;~=$il*c4qx`J+5dFH~9HM7dQ*&0C)t_;u<>A~s>hep>6w z#?#GhL*BIl^uMt=qRt;sJ70HbJbFb8kKT%sBkuv#4>5!L7qYU1JYDw19kmXjh`1;z zrE=UZg8Ou|9rfS|U!ho2B3j%an#VzH!fDbu&xh}KpX{e%8@pcl`H@)_#%r4}VJ_~( zx2>~N3PyGBPV6JS#@ghOiqY^;{=*d~>+3%ho}HyHw|n~=tMLlS{-XvREe8#+G@YP- zJvFA$$bX-ti6QHoHQqZZy7Vb}`fKXlXF`5R8nflC&0Cy%dFt(367~0P?F!OC`ne=e z4Y)c{c{-0vj(i)`Ow@RsoAEtK*ON3}{Vfx5NOkGwfL1O}OrhGGR+9LyS`^PD{V#uM z&9OaL@HC!>B8J)fSus%&8JXwNY@%%SHivjYey_*?ZsSvBmJj&MCdcTB_h}gw61^~# zCHHS4{Y5vy^};qPB{;Cchc_o)i{Me&4PVs|;IvE&ZOQDFoAs2Sxp!1;bT#(g zM|<<)AdC6n1FW(VlBb@Z?|#qeTdoo@#B)ON+=TvRGtaxWV@FqC@yP_&jANM0*a(b& z&wG(9@xZQie5N+E!`SDj?NW(AP(lNb{F;7u?_oXDM?xOd&2{HC`>gzU0&W zctWId@!7GC-gC>>yzKJT5+&$jyV`M3{LW8|r5=*gu{|LSy|Lf!kWy8(xLR{WFFYqxT#2`sCQ=UV@QqpI=d)d zop;Cd(xO4JB%P#2L3ikV`eLek`MHi2tBW?Gru<^B)(j6v)~wwK4V0^1Icat#xq~>5 z^ld6`l=-yhR8hEJWxjc3RdMMdm*);CC0V@KPYeuYzUulGQ(#w36(!sZdcD~DbcNMx zs&-?%W&mUT=A49*h~0yY-+@!6e65(7b(#goA}+kAY2!VZ>C!6m`@}SZ%~TqBDK#o8 zWqeVjvn>?kZ2osaS3*5$-ISLDrL8hT@p2E!iDP&+sLGCn&pn!DEyGwJ{@M0>$-shO zdakOl><5|JgE#ZPg)+UA0-Bm;-&lNC65*aXWBemz|AIPZr=BVP%qhZt1JP$Sr@v^o z*`9blcfZ1D^+2cEq@L|_ksDsZVFTYQZ}DZ#U^ynMa&71_tS7 zRW;VJG0<1Gv$<_ygcR4uDh(Dr(nKZXV}kfNq;Z7!4ANOW*Hli(`%qs~Jm_;NA~5rG z)nNIzXMLIiHjOS%V(w1IB;AL*=1OQZFSm$hZ_ zbh`XGx!Oi;{OXP6PpQqHsH6;SByW=Gd>TLQ_2NnGgSRF@FN&^w6HENoblBgq`pUP% z*wO^p4)2WFlK0DFvK2mPCcyrRWt3Z;6YZPB5=m0&*PhHG#QKqHwZ*M2vX|*fE$N|G zL_7>vnSU^5hY=@XzEr8ZM#@319%Ovm!s4w_8-u3=d5kbMd)4*IANoIgUkKm!NjO*O zo06&PS+_sq&5sPN%>ARu2EqFxTWO4rbPo~s9a^|ELr{dmcC8k8YFgj<&dB=A^&b8w zVy|!Fb5iAZm63+N+2~*|eN&=GI^MXhoa)jNMmj+~^r3Xkwv4X+a++DmT!x$>m8ya4 z#858hS+j+oeV8Vg`&eFAdfjLi(XUul(61|8wE0O`noBQX|42$YUzf$y_iXAM!<>5C zRIZ=R!GGbf=9Oy} zYz1~@<$#*(F8S))f(*ntZk4yN<}$^E zbhDzr8DsXAFfI;sjk8-^lytC^;p2)G={BMmN;#9ucqk9W^95Bg==Ui_^qKp{x4uB; zF&dYF3%ZL#`t@;RCktQqS+x3vwc95??=Wh5KlZ`zcg19{XG{|#8;?#fJ?E3C=@%S6 z0y-IOZd%?K?zDw;v$WF6fA?{^xWQRbicBW##jt|rT=Hcg%EZCnoIy0?K^KIY^}z53EbvaSm+v#FtR!rF(aM^nmViH&##RlSR@s=j`JK?}vx2 znugCut_Zz4W=Qaqeqc;u?a`;bDdoX4!$(6ubW)}8Refs)R7M? zN6P=r4Im>1TQ^`*!9gTBjY!hiQy;dWid1 z==F1GqSVOeeLeN)@X{ahpzoE)! zaWfsV8p}$nFzfl(%F-$N3I_96^}-bvzV;{?J6y`<9nLt=8?8)@g*VLcHck8dF1K<@*jXTGDA}8~I#4>4r;80DH z@YT#8u7#6YqQDFKf^dKreEnq3JH@Q1X!58kOzk@21+;!TB7>7Rk{(~Hc~U$1yplx0 zVSRdkghZQAQ49=5bR1ZLa${#;qikSj2kQ>|p5Jq_>E5=O z3~G=Yx=dQNaeFnnQI`eB!jO|xRhLqJs7g`3O(3|^A6>>RwMK5R6O-xRv$dIXj^``cFpIm$O~Jxa*F&FdolCOOXH+cSsIOhK23bXHx##dI;P%8$c-Vvf=LZF$K=^-jJ}kC zCl7kuCnDM}E{cP*@8pS{-?&-h2x0s!3mhj@pE2K$ff2^^Z|%>8*gvdP{O*H1R!!$hZ$t-Zf{j8v znj=aFey(;OCueOrbrwUrMJ)f3|4d&6MWvPw>i*E0Td{Zq!5G5^cdn*?+r4J|Nx?=x zO4V15qwMYXGm5Uu@pBPLC3fQ{@k^3M^{Jk;y3E5L;CqgZXGK_bI?Mgd4`qeyv#eG< z^|e2_IN!KlNmWXVx_s4tZ3k4tH09byoFT$@0=U;IQcMn=56 zy5K!Wm#>9)7BYtzMsqWMy4r3`D?I<0AJNl!lY(eTk3L&K>=p-KQzG}hMb;FD};-Vc@H2M(3Zf}LIJAK%#^#3J~<^Dg9{XdWW|IlMATa9uSfiFve zI!g)uuWzY$$-quWU&l^|T|r0Jz)aS{&=P8=*P8oGJUECjzo_Eo2eiD7VEQgTbLi$6 z^9Oy6150Z4cDK*CELIjSOmlG1+8P$PVkyN~>4y#~M5eqvm>}E!aYaGIO3&xnV*&Lk zWsh)Iv6meNT(0wp9n8NY>nxQ{6g z1ANK=OJ{dI$Z@f78)U?JlQAbSR$G4ZO ztGC(APCkAhW5mLD?Mt9&YYCBqm==+_*ei1OW-AfgL)1RUhN%2c+TAqC@BGQyd^^M? z@$iYzaI`D4w*O99EHZDo<)^Yq@$lFAaV9Us98Kj`gK)}%LVuU&*6IJq~pe0-}U}CCtHqtt+HD$pEF=-NKd_AXY}#&C=*9+sTsn0@Pc z>pk(1gSkX&Rqi9wPv)I*@$cT0qKI{#xEEr*AWWiF(3_QQSivG_JSO<>)G`7}13>|1*>cq8!f0!eH{`M`m%);C9`^83Z!KO+~iuw|*n*1GoPR8jBG_JUW@ zqk2@Kf>t+K0?1}Or@vnyoKe)&YRYn|%rm(fI5=1Si58dJDLG5dQJtSwM|9zqdly($e&AyY}O*G3^Jtegb=d$(>#QX@Qpj z52H7R8q(Ghe|-tTB+bdtQ49?4n^?%VbnGk?2U@9r`34U}f7*MLz26wJvx@orUspji zoV}~;os8XCC2RGss~|GS-c|MvH0`V+c3>Yo(cbZ; zok!J)(U0y`(D$yeR|ni#p_&v~;eS&F_b#|sR@+&y_UQkx;Eu?)cfq|P)y{$qWdFm0 zJCfDj1@}rmI}0|GBMa^keEuo`?Ok-Qj<9P{%73#cqAL9RMct48n?(`gw|CLKXnf~Q zd1&|jXKcQAnLlCb&LS8Xx+joj{sgUiAO8~q?0#J8)UL-j0l{T?Opq>N!aRZ@1t5jU JX^@IA{vUzW1#bWV literal 0 HcmV?d00001 diff --git a/pandapower/test/converter/test_from_jao.py b/pandapower/test/converter/test_from_jao.py new file mode 100644 index 000000000..100f0fdc4 --- /dev/null +++ b/pandapower/test/converter/test_from_jao.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2024 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +from copy import deepcopy +import os +import pytest +import numpy as np +import pandas as pd + +import pandapower as pp +from pandapower.converter import from_jao + + +def test_from_jao_with_testfile(): + testfile = os.path.join(pp.pp_dir, 'test', 'converter', "jao_testfiles", "testfile.xlsx") + assert os.path.isfile(testfile) + + # --- net1 + net1 = from_jao(testfile, None, False) + + assert len(net1.bus) == 10 + assert len(net1.line) == 7 + assert net1.line.Tieline.sum() == 2 + assert len(net1.trafo) == 1 + + # line data conversion + assert np.all((0.01 < net1.line[['r_ohm_per_km', 'x_ohm_per_km']]) & ( + net1.line[['r_ohm_per_km', 'x_ohm_per_km']] < 0.4)) + assert np.all((0.5 < net1.line['c_nf_per_km']) & (net1.line['c_nf_per_km'] < 25)) + assert np.all(net1.line['g_us_per_km'] < 1) + assert np.all((0.2 < net1.line['max_i_ka']) & (net1.line['max_i_ka'] < 5)) + + # trafo data conversion + assert 100 < net1.trafo.sn_mva.iat[0] < 1000 + assert 6 < net1.trafo.vk_percent.iat[0] < 65 + assert 0.25 < net1.trafo.vkr_percent.iat[0] < 1.2 + assert 10 < net1.trafo.pfe_kw.iat[0] < 1000 + assert net1.trafo.i0_percent.iat[0] < 0.1 + assert np.isclose(net1.trafo.shift_degree.iat[0], 90) + assert np.isclose(net1.trafo.tap_step_degree.iat[0], 1.794) + assert net1.trafo.tap_min.iat[0] == -17 + assert net1.trafo.tap_max.iat[0] == 17 + + # --- net2 + net2 = from_jao(testfile, None, True) + pp.nets_equal(net1, net2) # extend_data_for_grid_group_connections makes no difference here + + # --- net3 + net3 = from_jao(testfile, None, True, drop_grid_groups_islands=True) + assert len(net3.bus) == 6 + assert len(net3.line) == 5 + assert net3.line.Tieline.sum() == 1 + assert len(net3.trafo) == 1 + + +if __name__ == '__main__': + test_from_jao_with_testfile() + # pytest.main([__file__, "-xs"]) \ No newline at end of file From c5b132ef7cd3312d98f7ea4c9681539ffb1b4bf4 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Fri, 8 Nov 2024 16:08:50 +0100 Subject: [PATCH 053/135] solve reviewer comments --- pandapower/converter/jao/from_jao.py | 47 +++++++++++++++++++--------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index 9ce6b05bf..1038ba803 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -30,6 +30,7 @@ def from_jao(excel_file_path:str, extend_data_for_grid_group_connections: bool, drop_grid_groups_islands: bool = False, apply_data_correction: bool = True, + max_i_ka_fillna:float|int=999, **kwargs) -> pandapowerNet: """Converts European (Core) EHV grid data provided by JAO, the "Single Allocation Platform (SAP) for all European Transmission System Operators (TSOs) that operate in accordance to EU @@ -56,6 +57,9 @@ def from_jao(excel_file_path:str, min_bus_number (default is 6), by default False apply_data_correction : bool, optional _description_, by default True + max_i_ka_fillna : float | int, optional + value to fill missing values or data of false type in max_i_ka of lines and transformers. + If no value should be set, you can also pass np.nan. By default 999 Returns ------- @@ -96,7 +100,7 @@ def from_jao(excel_file_path:str, # --- manipulate data / data corrections if apply_data_correction: - html_str = _data_correction(data, html_str) + html_str = _data_correction(data, html_str, max_i_ka_fillna) # --- parse html_str to line_geo_data line_geo_data = None @@ -110,7 +114,7 @@ def from_jao(excel_file_path:str, net = create_empty_network(name=os.path.splitext(os.path.basename(excel_file_path))[0], **{key: val for key, val in kwargs.items() if key == "sn_mva"}) _create_buses_from_line_data(net, data) - _create_lines(net, data) + _create_lines(net, data, max_i_ka_fillna) _create_transformers_and_buses(net, data, **kwargs) # --- invent connections between grid groups @@ -131,7 +135,10 @@ def from_jao(excel_file_path:str, # --- secondary functions -------------------------------------------------------------------------- -def _data_correction(data:dict[str, pd.DataFrame], html_str:str|None) -> str|None: +def _data_correction( + data:dict[str, pd.DataFrame], + html_str:str|None, + max_i_ka_fillna:float|int) -> str|None: """Corrects input data in particular with regard to obvious weaknesses in the data provided, such as inconsistent spellings and missing necessary information @@ -141,12 +148,21 @@ def _data_correction(data:dict[str, pd.DataFrame], html_str:str|None) -> str|Non data provided by the excel file which will be corrected html_str : str | None data provided by the html file which will be corrected + max_i_ka_fillna : float | int + value to fill missing values or data of false type in max_i_ka of lines and transformers. + If no value should be set, you can also pass np.nan. Returns ------- str corrected html_str """ + # old name -> new name + rename_locnames = {"PSTMIKULOWA": "PST MIKULOWA", + "Chelm": "CHELM", + "OLSZTYN-MATK": "OLSZTYN-MATKI", + "STANISLAWOW": "Stanislawow", + "VIERRADEN": "Vierraden"} # --- Line and Tieline data --------------------------- for key in ["Lines", "Tielines"]: @@ -162,8 +178,9 @@ def _data_correction(data:dict[str, pd.DataFrame], html_str:str|None) -> str|Non # --- correct comma separation and cast to floats data[key][("Maximum Current Imax (A)", "Fixed")] = \ - data[key][("Maximum Current Imax (A)", "Fixed")].replace("\xa0", 999e3).replace( - "-", 999e3).replace(" ", 999e3) + data[key][("Maximum Current Imax (A)", "Fixed")].replace( + "\xa0", max_i_ka_fillna*1e3).replace( + "-", max_i_ka_fillna*1e3).replace(" ", max_i_ka_fillna*1e3) col_names = [("Electrical Parameters", col_level1) for col_level1 in [ "Length_(km)", "Resistance_R(Ω)", "Reactance_X(Ω)", "Susceptance_B(μS)", "Length_(km)"]] + [("Maximum Current Imax (A)", "Fixed")] @@ -173,19 +190,15 @@ def _data_correction(data:dict[str, pd.DataFrame], html_str:str|None) -> str|Non for loc_name in [(None, "NE_name"), ("Substation_1", "Full_name"), ("Substation_2", "Full_name")]: data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.replace( - "STANISLAWOW", "Stanislawow").str.replace("VIERRADEN", "Vierraden") - html_str = html_str.replace("STANISLAWOW", "Stanislawow").replace("Chelm", "CHELM") + rename_locnames) + html_str = html_str.replace(rename_locnames) # --- Transformer data -------------------------------- key = "Transformers" # --- fix Locations loc_name = ("Location", "Full Name") - data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.replace( - "PSTMIKULOWA", "PST MIKULOWA").str.replace( - "Chelm", "CHELM").str.replace( - "OLSZTYN-MATK", "OLSZTYN-MATKI").str.replace( - "STANISLAWOW", "Stanislawow").str.replace("VIERRADEN", "Vierraden") + data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.replace(rename_locnames) # --- fix data in nonnull_taps taps = data[key].loc[:, ("Phase Shifting Properties", "Taps used for RAO")].fillna("").astype( @@ -287,7 +300,10 @@ def _create_buses_from_line_data(net:pandapowerNet, data:dict[str, pd.DataFrame] assert np.allclose(new_bus_idx, bus_df.index) -def _create_lines(net:pandapowerNet, data:dict[str, pd.DataFrame]) -> None: +def _create_lines( + net:pandapowerNet, + data:dict[str, pd.DataFrame], + max_i_ka_fillna:float|int) -> None: """Creates lines to the pandapower net using information from the lines and tielines sheets (excel file). @@ -297,6 +313,9 @@ def _create_lines(net:pandapowerNet, data:dict[str, pd.DataFrame]) -> None: net to be filled by buses data : dict[str, pd.DataFrame] data provided by the excel file which will be corrected + max_i_ka_fillna : float | int + value to fill missing values or data of false type in max_i_ka of lines and transformers. + If no value should be set, you can also pass np.nan. """ bus_idx = _get_bus_idx(net) @@ -322,7 +341,7 @@ def _create_lines(net:pandapowerNet, data:dict[str, pd.DataFrame]) -> None: data[key][("Electrical Parameters", "Resistance_R(Ω)")].values / length_km, data[key][("Electrical Parameters", "Reactance_X(Ω)")].values / length_km, data[key][("Electrical Parameters", "Susceptance_B(μS)")].values / length_km, - data[key][("Maximum Current Imax (A)", "Fixed")].fillna(999000).values / 1e3, + data[key][("Maximum Current Imax (A)", "Fixed")].fillna(max_i_ka_fillna*1e3).values / 1e3, name=data[key].xs("NE_name", level=1, axis=1).values[:, 0], EIC_Code=data[key].xs("EIC_Code", level=1, axis=1).values[:, 0], TSO=data[key].xs("TSO", level=1, axis=1).values[:, 0], From 1aca8713e21838d078209720ef245ba36545efce Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Fri, 8 Nov 2024 17:13:18 +0100 Subject: [PATCH 054/135] add jao consideration in docs and improve docstring --- doc/converter.rst | 1 + doc/converter/jao.rst | 9 +++++++ pandapower/converter/jao/from_jao.py | 40 +++++++++++++++++++++++----- 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 doc/converter/jao.rst diff --git a/doc/converter.rst b/doc/converter.rst index 7805ae123..633d7a32b 100644 --- a/doc/converter.rst +++ b/doc/converter.rst @@ -16,4 +16,5 @@ These tools are: converter/matpower converter/powerfactory converter/cgmes + converter/jao diff --git a/doc/converter/jao.rst b/doc/converter/jao.rst new file mode 100644 index 000000000..e91995163 --- /dev/null +++ b/doc/converter/jao.rst @@ -0,0 +1,9 @@ +Documentation for the JAO Static Grid Model Converter Function +============================================================== + +The ``from_jao`` function allows users to convert the Static Grid Model provided by JAO (Joint Allocation Office) into a pandapower network by reading and processing the provided Excel and HTML files. + +Function Overview +----------------- + +.. autofunction:: pandapower.converter.from_jao diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index 1038ba803..1dd11fbcf 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -32,15 +32,30 @@ def from_jao(excel_file_path:str, apply_data_correction: bool = True, max_i_ka_fillna:float|int=999, **kwargs) -> pandapowerNet: - """Converts European (Core) EHV grid data provided by JAO, the "Single Allocation Platform (SAP) - for all European Transmission System Operators (TSOs) that operate in accordance to EU - legislation". At least in November 2024, the data are available at the website - https://www.jao.eu/static-grid-model . There, a map is provided to get an fine overview of the - geographical extent and the scope of the data. These inlcude information about European (Core) - lines, tielines, and transformers. No information is available on load or generation. + """Converts European (Core) EHV grid data provided by JAO (Joint Allocation Office), the + "Single Allocation Platform (SAP) for all European Transmission System Operators (TSOs) that + operate in accordance to EU legislation". + + **Data Sources and Availability:** + + The data are available at the website + `JAO Static Grid Model `_ (November 2024). + There, a map is provided to get an fine overview of the geographical extent and the scope of + the data. These inlcude information about European (Core) lines, tielines, and transformers. + + **Limitations:** + + No information is available on load or generation. The data quality with regard to the interconnection of the equipment, the information provided and the (incomplete) geodata should be considered with caution. + **Features of the converter:** + + - **Data Correction:** corrects known data inconsistencies, such as inconsistent spellings and missing necessary information. + - **Geographical Data Parsing:** Parses geographical data from the HTML file to add geolocation information to buses and lines. + - **Grid Group Connections:** Optionally extends the network by connecting islanded grid groups to avoid disconnected components. + - **Data Customization:** Allows for customization through additional parameters to control transformer creation, grid group dropping, and voltage level deviations. + Parameters ---------- excel_file_path : str @@ -64,7 +79,7 @@ def from_jao(excel_file_path:str, Returns ------- pandapowerNet - _description_ + net created from the jao data Additional Parameters --------------------- @@ -88,6 +103,17 @@ def from_jao(excel_file_path:str, This parameter allows a range below rel_deviation_threshold_for_trafo_bus_creation in which a warning is logged instead of a creating additional buses. By default 0.12 + Examples + -------- + >>> from pathlib import Path + >>> import os + >>> import pandapower as pp + >>> net = pp.converter.from_jao() + >>> home = str(Path.home()) + >>> # assume that the files are located at your desktop: + >>> excel_file_path = os.path.join(home, "desktop", "202409_Core Static Grid Mode_6th release") + >>> html_file_path = os.path.join(home, "desktop", "2024-09-13_Core_SGM_publication.html") + >>> net = from_jao(excel_file_path, html_file_path, True, drop_grid_groups_islands=True) """ # --- read data From 76e5bc642ebc271d9b3631e734bd68639f65a833 Mon Sep 17 00:00:00 2001 From: marcopau Date: Sat, 9 Nov 2024 15:51:43 +0100 Subject: [PATCH 055/135] Fix divergence issue in extreme scenarios --- pandapower/estimation/algorithm/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index 0a844b86a..0a1217a4f 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -119,6 +119,12 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # d_E = G_m^-1 * (H' * R^-1 * r) d_E = spsolve(G_m, H.T * (r_inv * r)) + # Scaling of Delta_X to avoid divergence due o ill-conditioning and + # operating conditions far from starting state variables + current_error = np.max(np.abs(d_E)) + if current_error > 0.35: + d_E = d_E*0.35/current_error + # Update E with d_E E += d_E.ravel() eppci.update_E(E) From f5481ca977dd9634caf2c84607c51aa53385f957 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 13 Nov 2024 08:26:05 +0100 Subject: [PATCH 056/135] fix typing of None in py3.9 --- pandapower/converter/jao/from_jao.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index 1dd11fbcf..8b6e6769c 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -7,6 +7,7 @@ import os import json from functools import reduce +from typing import Optional import numpy as np import pandas as pd from pandas.api.types import is_integer_dtype, is_object_dtype @@ -26,7 +27,7 @@ def from_jao(excel_file_path:str, - html_file_path: str|None, + html_file_path: Optional[str], extend_data_for_grid_group_connections: bool, drop_grid_groups_islands: bool = False, apply_data_correction: bool = True, @@ -163,8 +164,8 @@ def from_jao(excel_file_path:str, def _data_correction( data:dict[str, pd.DataFrame], - html_str:str|None, - max_i_ka_fillna:float|int) -> str|None: + html_str:Optional[str], + max_i_ka_fillna:float|int) -> Optional[str]: """Corrects input data in particular with regard to obvious weaknesses in the data provided, such as inconsistent spellings and missing necessary information @@ -629,7 +630,7 @@ def _add_bus_geo(net:pandapowerNet, line_geo_data:pd.DataFrame) -> None: def _geo_json_str(this_bus_geo:pd.Series) -> str: return f'{{"coordinates": [{this_bus_geo.at["lng"]}, {this_bus_geo.at["lat"]}], "type": "Point"}}' - def _add_bus_geo_inner(bus:int) -> str|None: + def _add_bus_geo_inner(bus:int) -> Optional[str]: from_bus_line_excerpt = net.line.loc[net.line.from_bus == bus, ["EIC_Code", "name", "Tieline"]] to_bus_line_excerpt = net.line.loc[net.line.to_bus == bus, ["EIC_Code", "name", "Tieline"]] line_excerpt = pd.concat([from_bus_line_excerpt, to_bus_line_excerpt]) From 7fe23090b1a086033831c6d888e02cf46a6cb3db Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 13 Nov 2024 14:22:00 +0100 Subject: [PATCH 057/135] jao: further fixing python typing --- pandapower/converter/jao/from_jao.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index 8b6e6769c..92dfe322f 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*-nt # Copyright (c) 2016-2024 by University of Kassel and Fraunhofer Institute for Energy Economics # and Energy System Technology (IEE), Kassel. All rights reserved. @@ -7,7 +7,7 @@ import os import json from functools import reduce -from typing import Optional +from typing import Optional, Any import numpy as np import pandas as pd from pandas.api.types import is_integer_dtype, is_object_dtype @@ -31,7 +31,7 @@ def from_jao(excel_file_path:str, extend_data_for_grid_group_connections: bool, drop_grid_groups_islands: bool = False, apply_data_correction: bool = True, - max_i_ka_fillna:float|int=999, + max_i_ka_fillna:Any[float,int]=999, **kwargs) -> pandapowerNet: """Converts European (Core) EHV grid data provided by JAO (Joint Allocation Office), the "Single Allocation Platform (SAP) for all European Transmission System Operators (TSOs) that @@ -91,7 +91,7 @@ def from_jao(excel_file_path:str, minimal_trafo_invention). If False, all equally named buses that have different voltage level and lay in different groups will be connected via additional transformers, by default False - min_bus_number : int|str, optional + min_bus_number : Any[int,str], optional Threshold value to decide which small grid groups should be dropped and which large grid groups should be kept. If all islanded grid groups should be dropped except of the one largest, set "max". If all grid groups that do not contain a slack element should be @@ -165,7 +165,7 @@ def from_jao(excel_file_path:str, def _data_correction( data:dict[str, pd.DataFrame], html_str:Optional[str], - max_i_ka_fillna:float|int) -> Optional[str]: + max_i_ka_fillna:Any[float,int]) -> Optional[str]: """Corrects input data in particular with regard to obvious weaknesses in the data provided, such as inconsistent spellings and missing necessary information @@ -330,7 +330,7 @@ def _create_buses_from_line_data(net:pandapowerNet, data:dict[str, pd.DataFrame] def _create_lines( net:pandapowerNet, data:dict[str, pd.DataFrame], - max_i_ka_fillna:float|int) -> None: + max_i_ka_fillna:Any[float,int]) -> None: """Creates lines to the pandapower net using information from the lines and tielines sheets (excel file). @@ -558,7 +558,7 @@ def _invent_connections_between_grid_groups( f"\n'{name1}' and '{name2}'") -def drop_islanded_grid_groups(net:pandapowerNet, min_bus_number:int|str, **kwargs) -> None: +def drop_islanded_grid_groups(net:pandapowerNet, min_bus_number:Any[int,str], **kwargs) -> None: """Drops grid groups that are islanded and include a number of buses below min_bus_number. Parameters @@ -970,7 +970,7 @@ def _lng_lat_to_df(dict_:dict, line_EIC:str, line_name:str) -> pd.DataFrame: def _fill_geo_at_one_sided_branches_without_geo_extent(net:pandapowerNet): - def _check_geo_availablitiy(net:pandapowerNet) -> dict[str, pd.Index|int]: + def _check_geo_availablitiy(net:pandapowerNet) -> dict[str, Any[pd.Index,int]]: av = dict() # availablitiy of geodata av["bus_with_geo"] = net.bus.index[~net.bus.geo.isnull()] av["lines_fbw_tbwo"] = net.line.index[net.line.from_bus.isin(av["bus_with_geo"]) & From 48739fa3e9563943a37929713d80813f286a1d49 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 13 Nov 2024 14:28:33 +0100 Subject: [PATCH 058/135] replace typing.Any by typing.Union (wrong usage) --- pandapower/converter/jao/from_jao.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index 92dfe322f..d4bedf908 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -7,7 +7,7 @@ import os import json from functools import reduce -from typing import Optional, Any +from typing import Optional, Union import numpy as np import pandas as pd from pandas.api.types import is_integer_dtype, is_object_dtype @@ -31,7 +31,7 @@ def from_jao(excel_file_path:str, extend_data_for_grid_group_connections: bool, drop_grid_groups_islands: bool = False, apply_data_correction: bool = True, - max_i_ka_fillna:Any[float,int]=999, + max_i_ka_fillna:Union[float,int]=999, **kwargs) -> pandapowerNet: """Converts European (Core) EHV grid data provided by JAO (Joint Allocation Office), the "Single Allocation Platform (SAP) for all European Transmission System Operators (TSOs) that @@ -91,7 +91,7 @@ def from_jao(excel_file_path:str, minimal_trafo_invention). If False, all equally named buses that have different voltage level and lay in different groups will be connected via additional transformers, by default False - min_bus_number : Any[int,str], optional + min_bus_number : Union[int,str], optional Threshold value to decide which small grid groups should be dropped and which large grid groups should be kept. If all islanded grid groups should be dropped except of the one largest, set "max". If all grid groups that do not contain a slack element should be @@ -165,7 +165,7 @@ def from_jao(excel_file_path:str, def _data_correction( data:dict[str, pd.DataFrame], html_str:Optional[str], - max_i_ka_fillna:Any[float,int]) -> Optional[str]: + max_i_ka_fillna:Union[float,int]) -> Optional[str]: """Corrects input data in particular with regard to obvious weaknesses in the data provided, such as inconsistent spellings and missing necessary information @@ -330,7 +330,7 @@ def _create_buses_from_line_data(net:pandapowerNet, data:dict[str, pd.DataFrame] def _create_lines( net:pandapowerNet, data:dict[str, pd.DataFrame], - max_i_ka_fillna:Any[float,int]) -> None: + max_i_ka_fillna:Union[float,int]) -> None: """Creates lines to the pandapower net using information from the lines and tielines sheets (excel file). @@ -558,7 +558,7 @@ def _invent_connections_between_grid_groups( f"\n'{name1}' and '{name2}'") -def drop_islanded_grid_groups(net:pandapowerNet, min_bus_number:Any[int,str], **kwargs) -> None: +def drop_islanded_grid_groups(net:pandapowerNet, min_bus_number:Union[int,str], **kwargs) -> None: """Drops grid groups that are islanded and include a number of buses below min_bus_number. Parameters @@ -970,7 +970,7 @@ def _lng_lat_to_df(dict_:dict, line_EIC:str, line_name:str) -> pd.DataFrame: def _fill_geo_at_one_sided_branches_without_geo_extent(net:pandapowerNet): - def _check_geo_availablitiy(net:pandapowerNet) -> dict[str, Any[pd.Index,int]]: + def _check_geo_availablitiy(net:pandapowerNet) -> dict[str, Union[pd.Index,int]]: av = dict() # availablitiy of geodata av["bus_with_geo"] = net.bus.index[~net.bus.geo.isnull()] av["lines_fbw_tbwo"] = net.line.index[net.line.from_bus.isin(av["bus_with_geo"]) & From e4775729a506bec5e610577b8292bce266bbe138 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 13 Nov 2024 15:46:34 +0100 Subject: [PATCH 059/135] trial to fix the tests --- pandapower/converter/jao/from_jao.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index d4bedf908..7c7a1277c 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -185,11 +185,11 @@ def _data_correction( corrected html_str """ # old name -> new name - rename_locnames = {"PSTMIKULOWA": "PST MIKULOWA", - "Chelm": "CHELM", - "OLSZTYN-MATK": "OLSZTYN-MATKI", - "STANISLAWOW": "Stanislawow", - "VIERRADEN": "Vierraden"} + rename_locnames = [("PSTMIKULOWA", "PST MIKULOWA"), + ("Chelm", "CHELM"), + ("OLSZTYN-MATK", "OLSZTYN-MATKI"), + ("STANISLAWOW", "Stanislawow"), + ("VIERRADEN", "Vierraden")] # --- Line and Tieline data --------------------------- for key in ["Lines", "Tielines"]: @@ -216,8 +216,8 @@ def _data_correction( # --- consolidate to one way of name capitalization for loc_name in [(None, "NE_name"), ("Substation_1", "Full_name"), ("Substation_2", "Full_name")]: - data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.replace( - rename_locnames) + data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().apply( + _multi_str_repl, repl=rename_locnames) html_str = html_str.replace(rename_locnames) # --- Transformer data -------------------------------- @@ -225,7 +225,8 @@ def _data_correction( # --- fix Locations loc_name = ("Location", "Full Name") - data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.replace(rename_locnames) + data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.apply( + _multi_str_repl, repl=rename_locnames) # --- fix data in nonnull_taps taps = data[key].loc[:, ("Phase Shifting Properties", "Taps used for RAO")].fillna("").astype( @@ -1000,6 +1001,11 @@ def _check_geo_availablitiy(net:pandapowerNet) -> dict[str, Union[pd.Index,int]] set_line_geodata_from_bus_geodata(net) +def _multi_str_repl(st:str, repl:list[tuple]) -> str: + for (old, new) in repl: + return st.replace(old, new) + + if __name__ == "__main__": from pathlib import Path import os From 10e763ae4f52706c32a7c3f33b63e96a4e22860e Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 13 Nov 2024 16:51:35 +0100 Subject: [PATCH 060/135] fix test --- pandapower/converter/jao/from_jao.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index 7c7a1277c..349951347 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -218,14 +218,14 @@ def _data_correction( ("Substation_2", "Full_name")]: data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().apply( _multi_str_repl, repl=rename_locnames) - html_str = html_str.replace(rename_locnames) + html_str = _multi_str_repl(html_str, rename_locnames) # --- Transformer data -------------------------------- key = "Transformers" # --- fix Locations loc_name = ("Location", "Full Name") - data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().str.apply( + data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().apply( _multi_str_repl, repl=rename_locnames) # --- fix data in nonnull_taps From 7c6c7f6cfe7654de6dbc0f20b3904acec684153c Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Fri, 15 Nov 2024 15:03:43 +0100 Subject: [PATCH 061/135] just style refactoring --- pandapower/converter/jao/from_jao.py | 183 ++++++++++++++------------- 1 file changed, 97 insertions(+), 86 deletions(-) diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index 349951347..e527eb745 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -26,12 +26,12 @@ logger = logging.getLogger(__name__) -def from_jao(excel_file_path:str, +def from_jao(excel_file_path: str, html_file_path: Optional[str], extend_data_for_grid_group_connections: bool, drop_grid_groups_islands: bool = False, apply_data_correction: bool = True, - max_i_ka_fillna:Union[float,int]=999, + max_i_ka_fillna: Union[float, int] = 999, **kwargs) -> pandapowerNet: """Converts European (Core) EHV grid data provided by JAO (Joint Allocation Office), the "Single Allocation Platform (SAP) for all European Transmission System Operators (TSOs) that @@ -163,9 +163,9 @@ def from_jao(excel_file_path:str, def _data_correction( - data:dict[str, pd.DataFrame], - html_str:Optional[str], - max_i_ka_fillna:Union[float,int]) -> Optional[str]: + data: dict[str, pd.DataFrame], + html_str: Optional[str], + max_i_ka_fillna: Union[float, int]) -> Optional[str]: """Corrects input data in particular with regard to obvious weaknesses in the data provided, such as inconsistent spellings and missing necessary information @@ -247,14 +247,15 @@ def _data_correction( if is_object_dtype(data[key].loc[:, ("Phase Shifting Properties", col)]): tr_double = data[key].index[data[key].loc[:, ( "Phase Shifting Properties", col)].str.contains("/").fillna(0).astype(bool)] - data[key].loc[tr_double, ("Phase Shifting Properties", col)] = data[key].loc[tr_double, - ("Phase Shifting Properties", col)].str.split("/", expand=True)[1].str.replace( - ",", ".").astype(float).values # take second info and correct separation: , -> . + data[key].loc[tr_double, ("Phase Shifting Properties", col)] = data[key].loc[ + tr_double, ("Phase Shifting Properties", col)].str.split("/", expand=True)[ + 1].str.replace(",", ".").astype(float).values # take second info and correct + # separation: , -> . return html_str -def _parse_html_str(html_str:str) -> pd.DataFrame: +def _parse_html_str(html_str: str) -> pd.DataFrame: """Converts ths geodata from the html file (information hidden in the string), from Lines in particular, to a DataFrame that can be used later in _add_bus_geo() @@ -268,7 +269,7 @@ def _parse_html_str(html_str:str) -> pd.DataFrame: pd.DataFrame extracted geodata for a later and easy use """ - def _filter_name(st:str) -> str: + def _filter_name(st: str) -> str: name_start = "NE name: " name_end = "" pos0 = st.find(name_start) + len(name_start) @@ -292,7 +293,7 @@ def _filter_name(st:str) -> str: len(polylines[6]))] line_name = [_filter_name(polylines[6][i]) for i in range(len(polylines[6]))] line_geo_data = pd.concat([_lng_lat_to_df(polylines[0][i][0][0], line_EIC[i], line_name[i]) for - i in range(len(polylines[0]))], ignore_index=True) + i in range(len(polylines[0]))], ignore_index=True) # remove trailing whitespaces for col in ["EIC_Code", "name"]: @@ -301,7 +302,7 @@ def _filter_name(st:str) -> str: return line_geo_data -def _create_buses_from_line_data(net:pandapowerNet, data:dict[str, pd.DataFrame]) -> None: +def _create_buses_from_line_data(net: pandapowerNet, data: dict[str, pd.DataFrame]) -> None: """Creates buses to the pandapower net using information from the lines and tielines sheets (excel file). @@ -329,9 +330,9 @@ def _create_buses_from_line_data(net:pandapowerNet, data:dict[str, pd.DataFrame] def _create_lines( - net:pandapowerNet, - data:dict[str, pd.DataFrame], - max_i_ka_fillna:Union[float,int]) -> None: + net: pandapowerNet, + data: dict[str, pd.DataFrame], + max_i_ka_fillna: Union[float, int]) -> None: """Creates lines to the pandapower net using information from the lines and tielines sheets (excel file). @@ -369,17 +370,18 @@ def _create_lines( data[key][("Electrical Parameters", "Resistance_R(Ω)")].values / length_km, data[key][("Electrical Parameters", "Reactance_X(Ω)")].values / length_km, data[key][("Electrical Parameters", "Susceptance_B(μS)")].values / length_km, - data[key][("Maximum Current Imax (A)", "Fixed")].fillna(max_i_ka_fillna*1e3).values / 1e3, + data[key][("Maximum Current Imax (A)", "Fixed")].fillna( + max_i_ka_fillna*1e3).values / 1e3, name=data[key].xs("NE_name", level=1, axis=1).values[:, 0], EIC_Code=data[key].xs("EIC_Code", level=1, axis=1).values[:, 0], TSO=data[key].xs("TSO", level=1, axis=1).values[:, 0], Comment=data[key].xs("Comment", level=1, axis=1).values[:, 0], - Tieline=key=="Tielines", - ) + Tieline=key == "Tielines", + ) def _create_transformers_and_buses( - net:pandapowerNet, data:dict[str, pd.DataFrame], **kwargs) -> None: + net: pandapowerNet, data: dict[str, pd.DataFrame], **kwargs) -> None: """Creates transformers to the pandapower net using information from the transformers sheet (excel file). @@ -417,7 +419,7 @@ def _create_transformers_and_buses( du = _get_float_column(data[key], ("Phase Shifting Properties", "Phase Regulation δu (%)")) dphi = _get_float_column(data[key], ("Phase Shifting Properties", "Angle Regulation δu (%)")) - phase_shifter = np.isclose(du, 0) & (~np.isclose(dphi, 0)) # Symmetrical/Asymmetrical not + phase_shifter = np.isclose(du, 0) & (~np.isclose(dphi, 0)) # Symmetrical/Asymmetrical not # considered _ = create_transformers_from_parameters( @@ -431,24 +433,24 @@ def _create_transformers_and_buses( vk_percent, pfe_kw, i0_percent, - shift_degree = data[key].xs("Theta θ (°)", level=1, axis=1).values[:, 0], - tap_pos = 0, - tap_neutral = 0, - tap_side = "lv", - tap_min = taps["tap_min"].values, - tap_max = taps["tap_max"].values, - tap_phase_shifter = phase_shifter, - tap_step_percent = du, - tap_step_degree = dphi, - name = data[key].loc[:, ("Location", "Full Name")].str.strip().values, - EIC_Code = data[key].xs("EIC_Code", level=1, axis=1).values[:, 0], - TSO = data[key].xs("TSO", level=1, axis=1).values[:, 0], - Comment = data[key].xs("Comment", level=1, axis=1).replace("\xa0", "").values[:, 0], - ) + shift_degree=data[key].xs("Theta θ (°)", level=1, axis=1).values[:, 0], + tap_pos=0, + tap_neutral=0, + tap_side="lv", + tap_min=taps["tap_min"].values, + tap_max=taps["tap_max"].values, + tap_phase_shifter=phase_shifter, + tap_step_percent=du, + tap_step_degree=dphi, + name=data[key].loc[:, ("Location", "Full Name")].str.strip().values, + EIC_Code=data[key].xs("EIC_Code", level=1, axis=1).values[:, 0], + TSO=data[key].xs("TSO", level=1, axis=1).values[:, 0], + Comment=data[key].xs("Comment", level=1, axis=1).replace("\xa0", "").values[:, 0], + ) def _invent_connections_between_grid_groups( - net:pandapowerNet, minimal_trafo_invention:bool=False, **kwargs) -> None: + net: pandapowerNet, minimal_trafo_invention: bool = False, **kwargs) -> None: """Adds connections between islanded grid groups via: - adding transformers between equally named buses that have different voltage level and lay in different groups @@ -487,7 +489,7 @@ def _invent_connections_between_grid_groups( dupl_location_names = location_names[location_names.duplicated()] for location_name in dupl_location_names: - if minimal_trafo_invention and not len(bus_grid_groups.unique()) > 1: + if minimal_trafo_invention and len(bus_grid_groups.unique()) <= 1: break # break with regard to minimal_trafo_invention grid_groups_at_location = bus_grid_groups.loc[bus_idx.loc[location_name].values] grid_groups_at_location = grid_groups_at_location.drop_duplicates() @@ -559,7 +561,10 @@ def _invent_connections_between_grid_groups( f"\n'{name1}' and '{name2}'") -def drop_islanded_grid_groups(net:pandapowerNet, min_bus_number:Union[int,str], **kwargs) -> None: +def drop_islanded_grid_groups( + net: pandapowerNet, + min_bus_number: Union[int, str], + **kwargs) -> None: """Drops grid groups that are islanded and include a number of buses below min_bus_number. Parameters @@ -600,7 +605,7 @@ def _grid_groups_to_drop_by_min_bus_number(): f"total of {grid_groups_to_drop.n_buses.sum()} buses.") -def _add_bus_geo(net:pandapowerNet, line_geo_data:pd.DataFrame) -> None: +def _add_bus_geo(net: pandapowerNet, line_geo_data: pd.DataFrame) -> None: """Adds geodata to the buses. The function needs to handle cases where line_geo_data does not include no or multiple geodata per bus. Primarly, the geodata are allocate via EIC Code names, if ambigous, names are considered. @@ -613,9 +618,9 @@ def _add_bus_geo(net:pandapowerNet, line_geo_data:pd.DataFrame) -> None: Converted geodata from the html file """ iSl = pd.IndexSlice - lgd_EIC_bus = line_geo_data.pivot_table(values="value", index=["EIC_Code", "bus"], - columns="geo_dim") - lgd_name_bus = line_geo_data.pivot_table(values="value", index=["name", "bus"], + lgd_EIC_bus = line_geo_data.pivot_table(values="value", index=["EIC_Code", "bus"], + columns="geo_dim") + lgd_name_bus = line_geo_data.pivot_table(values="value", index=["name", "bus"], columns="geo_dim") lgd_EIC_bus_idx_extended = pd.MultiIndex.from_frame(lgd_EIC_bus.index.to_frame().assign( **dict(col_name="EIC_Code")).rename(columns=dict(EIC_Code="identifier")).loc[ @@ -628,26 +633,28 @@ def _add_bus_geo(net:pandapowerNet, line_geo_data:pd.DataFrame) -> None: dupl_EICs = net.line.EIC_Code.loc[net.line.EIC_Code.duplicated()] dupl_names = net.line.name.loc[net.line.name.duplicated()] - def _geo_json_str(this_bus_geo:pd.Series) -> str: + def _geo_json_str(this_bus_geo: pd.Series) -> str: return f'{{"coordinates": [{this_bus_geo.at["lng"]}, {this_bus_geo.at["lat"]}], "type": "Point"}}' - def _add_bus_geo_inner(bus:int) -> Optional[str]: - from_bus_line_excerpt = net.line.loc[net.line.from_bus == bus, ["EIC_Code", "name", "Tieline"]] + def _add_bus_geo_inner(bus: int) -> Optional[str]: + from_bus_line_excerpt = net.line.loc[net.line.from_bus == + bus, ["EIC_Code", "name", "Tieline"]] to_bus_line_excerpt = net.line.loc[net.line.to_bus == bus, ["EIC_Code", "name", "Tieline"]] line_excerpt = pd.concat([from_bus_line_excerpt, to_bus_line_excerpt]) n_connected_line_ends = len(line_excerpt) if n_connected_line_ends == 0: - logger.error(f"Bus {bus} (name {net.bus.at[bus, 'name']}) is not found in line_geo_data.") + logger.error( + f"Bus {bus} (name {net.bus.at[bus, 'name']}) is not found in line_geo_data.") return None is_dupl = pd.concat([ pd.DataFrame({"EIC": from_bus_line_excerpt.EIC_Code.isin(dupl_EICs).values, "name": from_bus_line_excerpt.name.isin(dupl_names).values}, - index=pd.MultiIndex.from_product([["from"], from_bus_line_excerpt.index], - names=["bus", "line_index"])), + index=pd.MultiIndex.from_product([["from"], from_bus_line_excerpt.index], + names=["bus", "line_index"])), pd.DataFrame({"EIC": to_bus_line_excerpt.EIC_Code.isin(dupl_EICs).values, "name": to_bus_line_excerpt.name.isin(dupl_names).values}, - index=pd.MultiIndex.from_product([["to"], to_bus_line_excerpt.index], - names=["bus", "line_index"])) + index=pd.MultiIndex.from_product([["to"], to_bus_line_excerpt.index], + names=["bus", "line_index"])) ]) is_missing = pd.DataFrame({ "EIC": ~line_excerpt.EIC_Code.isin( @@ -665,8 +672,9 @@ def _add_bus_geo_inner(bus:int) -> Optional[str]: "col_name": "EIC_Code", "identifier": line_excerpt.EIC_Code.values, "bus": is_dupl.index.get_level_values("bus").values - }) # default is EIC_Code - take_from_name = ((is_dupl.EIC | is_missing.EIC) & (~is_dupl.name & ~is_missing.name)).values + }) # default is EIC_Code + take_from_name = ((is_dupl.EIC | is_missing.EIC) & ( + ~is_dupl.name & ~is_missing.name)).values access_vals.loc[take_from_name, "col_name"] = "name" access_vals.loc[take_from_name, "identifier"] = line_excerpt.name.loc[take_from_name].values keep = (~(is_dupl | is_missing)).any(axis=1).values @@ -681,7 +689,7 @@ def _add_bus_geo_inner(bus:int) -> Optional[str]: return None elif sum(keep) == 0: logger.info(f"For {bus=}, all EIC_Codes and names of connected lines are ambiguous. " - "No geo data is dropped at this point.") + "No geo data is dropped at this point.") keep[(~is_missing).any(axis=1)] = True access_vals = access_vals.loc[keep] @@ -699,13 +707,13 @@ def _add_bus_geo_inner(bus:int) -> Optional[str]: return _geo_json_str(this_bus_geo.iloc[0]) elif len_this_bus_geo == 2: how_often = pd.Series( - [sum(np.isclose(lgd_EIC_bus["lat"], this_bus_geo["lat"].iat[i]) & \ + [sum(np.isclose(lgd_EIC_bus["lat"], this_bus_geo["lat"].iat[i]) & np.isclose(lgd_EIC_bus["lng"], this_bus_geo["lng"].iat[i])) for i in range(len_this_bus_geo)], index=this_bus_geo.index) if how_often.at[how_often.idxmax()] >= 1: logger.warning(f"Bus {bus} (name {net.bus.at[bus, 'name']}) was found multiple times" - " in line_geo_data. No value exists more often than others. " - "The first of most used geo positions is used.") + " in line_geo_data. No value exists more often than others. " + "The first of most used geo positions is used.") return _geo_json_str(this_bus_geo.loc[how_often.idxmax()]) net.bus.geo = [_add_bus_geo_inner(bus) for bus in net.bus.index] @@ -713,14 +721,14 @@ def _add_bus_geo_inner(bus:int) -> Optional[str]: # --- tertiary functions --------------------------------------------------------------------------- -def _float_col_comma_correction(data:dict[str, pd.DataFrame], key:str, col_names:list): +def _float_col_comma_correction(data: dict[str, pd.DataFrame], key: str, col_names: list): for col_name in col_names: data[key][col_name] = pd.to_numeric(data[key][col_name].astype(str).str.replace( ",", "."), errors="coerce") def _get_transformer_voltages( - data:dict[str, pd.DataFrame], bus_idx:pd.Series) -> tuple[np.ndarray, np.ndarray]: + data: dict[str, pd.DataFrame], bus_idx: pd.Series) -> tuple[np.ndarray, np.ndarray]: key = "Transformers" vn = data[key].loc[:, [("Voltage_level(kV)", "Primary"), @@ -735,10 +743,10 @@ def _get_transformer_voltages( def _allocate_trafos_to_buses_and_create_buses( - net:pandapowerNet, data:dict[str, pd.DataFrame], bus_idx:pd.Series, - vn_hv_kv:np.ndarray, vn_lv_kv:np.ndarray, - rel_deviation_threshold_for_trafo_bus_creation:float=0.2, - log_rel_vn_deviation:float=0.12, **kwargs) -> pd.DataFrame: + net: pandapowerNet, data: dict[str, pd.DataFrame], bus_idx: pd.Series, + vn_hv_kv: np.ndarray, vn_lv_kv: np.ndarray, + rel_deviation_threshold_for_trafo_bus_creation: float = 0.2, + log_rel_vn_deviation: float = 0.12, **kwargs) -> pd.DataFrame: """Provides a DataFrame of data to allocate transformers to the buses according to their location names. If locations of transformers do not exist due to the data of the lines and tielines sheets, additional buses are created. If locations exist but have a far different @@ -790,23 +798,25 @@ def _allocate_trafos_to_buses_and_create_buses( # --- buses empties = -1*np.ones(len(vn_hv_kv), dtype=int) trafo_connections = pd.DataFrame({ - "name": trafo_location_names, - "hv_bus": empties, - "lv_bus": empties, - "vn_hv_kv": vn_hv_kv, - "vn_lv_kv": vn_lv_kv, - "vn_hv_kv_next_bus": vn_hv_kv, - "vn_lv_kv_next_bus": vn_lv_kv, - "hv_rel_deviation": np.zeros(len(vn_hv_kv)), - "lv_rel_deviation": np.zeros(len(vn_hv_kv)), - }) - trafo_connections[["hv_bus", "lv_bus"]] = trafo_connections[["hv_bus", "lv_bus"]].astype(np.int64) + "name": trafo_location_names, + "hv_bus": empties, + "lv_bus": empties, + "vn_hv_kv": vn_hv_kv, + "vn_lv_kv": vn_lv_kv, + "vn_hv_kv_next_bus": vn_hv_kv, + "vn_lv_kv_next_bus": vn_lv_kv, + "hv_rel_deviation": np.zeros(len(vn_hv_kv)), + "lv_rel_deviation": np.zeros(len(vn_hv_kv)), + }) + trafo_connections[["hv_bus", "lv_bus"]] = trafo_connections[[ + "hv_bus", "lv_bus"]].astype(np.int64) for side in ["hv", "lv"]: bus_col, trafo_vn_col, next_col, rel_dev_col, has_dev_col = \ f"{side}_bus", f"vn_{side}_kv", f"vn_{side}_kv_next_bus", f"{side}_rel_deviation", \ f"trafo_{side}_to_bus_deviation" - name_vn_series = pd.Series(tuple(zip(trafo_location_names, trafo_connections[trafo_vn_col]))) + name_vn_series = pd.Series( + tuple(zip(trafo_location_names, trafo_connections[trafo_vn_col]))) isin = name_vn_series.isin(bus_idx.index) trafo_connections[has_dev_col] = ~isin trafo_connections.loc[isin, bus_col] = bus_idx.loc[name_vn_series.loc[isin]].values @@ -835,7 +845,7 @@ def _allocate_trafos_to_buses_and_create_buses( name=new_bus_data_dd.name, zone=new_bus_data_dd.TSO) trafo_connections.loc[need_bus_creation, bus_col] = net.bus.loc[new_bus_idx, [ "name", "vn_kv"]].reset_index().set_index(["name", "vn_kv"]).loc[list(new_bus_data[[ - "name", "vn_kv"]].itertuples(index=False, name=None))].values + "name", "vn_kv"]].itertuples(index=False, name=None))].values trafo_connections.loc[need_bus_creation, next_col] = \ trafo_connections.loc[need_bus_creation, trafo_vn_col].values trafo_connections.loc[need_bus_creation, rel_dev_col] = 0 @@ -913,9 +923,10 @@ def _find_trafo_locations(trafo_bus_names, bus_location_names): longest_part_in_buses = trafo_bus_names_longest_part.isin(bus_location_names) # --- check whether all name strings point at location names of the buses - if False: # for easy testing + if False: # for easy testing fail = ~(joined_in_buses | longest_part_in_buses) - a = pd.concat([trafo_bus_names_joined.loc[fail], trafo_bus_names_longest_part.loc[fail]], axis=1) + a = pd.concat([trafo_bus_names_joined.loc[fail], + trafo_bus_names_longest_part.loc[fail]], axis=1) if n_bus_names_not_found := len(joined_in_buses) - sum(joined_in_buses | longest_part_in_buses): raise ValueError( @@ -930,7 +941,7 @@ def _find_trafo_locations(trafo_bus_names, bus_location_names): return trafo_location_names -def _drop_duplicates_and_join_TSO(bus_df:pd.DataFrame) -> pd.DataFrame: +def _drop_duplicates_and_join_TSO(bus_df: pd.DataFrame) -> pd.DataFrame: bus_df = bus_df.drop_duplicates(ignore_index=True) # just keep one bus per name and vn_kv. If there are multiple buses of different TSOs, join the # TSO strings: @@ -945,12 +956,12 @@ def _get_float_column(df, col_tuple, fill=0): return series.astype(float).fillna(fill) -def _get_bus_idx(net:pandapowerNet) -> pd.Series: +def _get_bus_idx(net: pandapowerNet) -> pd.Series: return net.bus[["name", "vn_kv"]].rename_axis("index").reset_index().set_index([ "name", "vn_kv"])["index"] -def get_grid_groups(net:pandapowerNet, **kwargs) -> pd.DataFrame: +def get_grid_groups(net: pandapowerNet, **kwargs) -> pd.DataFrame: notravbuses_dict = dict() if "notravbuses" not in kwargs.keys() else { "notravbuses": kwargs.pop("notravbuses")} grid_group_buses = [set_ for set_ in connected_components(create_nxgraph(net, **kwargs), @@ -960,7 +971,7 @@ def get_grid_groups(net:pandapowerNet, **kwargs) -> pd.DataFrame: return grid_groups -def _lng_lat_to_df(dict_:dict, line_EIC:str, line_name:str) -> pd.DataFrame: +def _lng_lat_to_df(dict_: dict, line_EIC: str, line_name: str) -> pd.DataFrame: return pd.DataFrame([ [line_EIC, line_name, "from", "lng", dict_["lng"][0]], [line_EIC, line_name, "to", "lng", dict_["lng"][1]], @@ -969,10 +980,10 @@ def _lng_lat_to_df(dict_:dict, line_EIC:str, line_name:str) -> pd.DataFrame: ], columns=["EIC_Code", "name", "bus", "geo_dim", "value"]) -def _fill_geo_at_one_sided_branches_without_geo_extent(net:pandapowerNet): +def _fill_geo_at_one_sided_branches_without_geo_extent(net: pandapowerNet): - def _check_geo_availablitiy(net:pandapowerNet) -> dict[str, Union[pd.Index,int]]: - av = dict() # availablitiy of geodata + def _check_geo_availablitiy(net: pandapowerNet) -> dict[str, Union[pd.Index, int]]: + av = dict() # availablitiy of geodata av["bus_with_geo"] = net.bus.index[~net.bus.geo.isnull()] av["lines_fbw_tbwo"] = net.line.index[net.line.from_bus.isin(av["bus_with_geo"]) & (~net.line.to_bus.isin(av["bus_with_geo"]))] @@ -1001,7 +1012,7 @@ def _check_geo_availablitiy(net:pandapowerNet) -> dict[str, Union[pd.Index,int]] set_line_geodata_from_bus_geodata(net) -def _multi_str_repl(st:str, repl:list[tuple]) -> str: +def _multi_str_repl(st: str, repl: list[tuple]) -> str: for (old, new) in repl: return st.replace(old, new) @@ -1026,10 +1037,10 @@ def _multi_str_repl(st:str, repl:list[tuple]) -> str: pp_net_json_file = os.path.join(home, "desktop", "jao_grid.json") - if 1: # read from original data + if 1: # read from original data net = from_jao(excel_file_path, html_file_path, True, drop_grid_groups_islands=True) pp.to_json(net, pp_net_json_file) - else: # load net from already converted and stored net + else: # load net from already converted and stored net net = pp.from_json(pp_net_json_file) print(net) From 4d57136b91f5f734f14b45df6f353a357852d406 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Mon, 18 Nov 2024 10:11:49 +0100 Subject: [PATCH 062/135] fixed missing gcc compiler in docs_check pipeline. --- .github/workflows/github_test_action.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index 504384211..ed0414a99 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -314,7 +314,12 @@ jobs: - name: Check docs for Python ${{ matrix.python-version }} uses: e2nIEE/sphinx-action@master with: - pre-build-command: "python -m pip install --upgrade pip; - python -m pip install .[docs];" + pre-build-command: " + apt update; + apt upgrade -y; + apt install build-essential -y; + python -m pip install --upgrade pip; + python -m pip install .[docs]; + " build-command: "sphinx-build -b html . _build -W" docs-folder: "doc/" From eb5caf15912c027c4d8bde964e2fe60f1829e081 Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Mon, 18 Nov 2024 11:22:05 +0100 Subject: [PATCH 063/135] - added tuutorial - removed redundant function - adjusted test --- pandapower/auxiliary.py | 9 - pandapower/test/api/test_auxiliary.py | 13 +- tutorials/Working with GeoJSON.ipynb | 7915 +++++++++++++++++++++++++ 3 files changed, 7922 insertions(+), 15 deletions(-) create mode 100644 tutorials/Working with GeoJSON.ipynb diff --git a/pandapower/auxiliary.py b/pandapower/auxiliary.py index 1068847ad..7cb495733 100644 --- a/pandapower/auxiliary.py +++ b/pandapower/auxiliary.py @@ -414,15 +414,6 @@ def as_geoseries(self): """ return GeoSeries(self._obj.dropna().pipe(from_geojson), crs=4326, index=self._obj.dropna().index) - def within_radius(self, reference_point, radius_m): - """ - Returns a Series with booleans, if geometry of geojson is in radius of the reference point. - """ - circle_polygon = GeoSeries([Point(reference_point)], - crs=4326).to_crs(epsg=31467).buffer(radius_m).to_crs(epsg=4326).iloc[0] - geoms = self.as_geoseries - return geoms.within(circle_polygon) | geoms.intersects(circle_polygon) - def __getattr__(self, item): """ Enables access to all methods or attribute calls from a GeoSeries. diff --git a/pandapower/test/api/test_auxiliary.py b/pandapower/test/api/test_auxiliary.py index be0369a93..33e532bf3 100644 --- a/pandapower/test/api/test_auxiliary.py +++ b/pandapower/test/api/test_auxiliary.py @@ -343,10 +343,13 @@ def test_geo_accessor_geojson(): @pytest.mark.skipif(not GEOPANDAS_INSTALLED, reason="geopandas is not installed") def test_geo_accessor_geopandas(): net = pp.networks.mv_oberrhein() - reference_point = shapely.geometry.Point((7.8947079593416, 48.40549007606241)) - radius_m = 100 - assert net.line.geo.geojson.within_radius(reference_point, radius_m)[:3].all() == True - assert net.line.geo.geojson.within_radius(reference_point, radius_m)[3:].any() == False + reference_point = (7.781067, 48.389774) + radius_m = 2200 + circle_polygon = gpd.GeoSeries([shapely.geometry.Point(reference_point)], + crs=4326).to_crs(epsg=31467).buffer(radius_m).to_crs(epsg=4326).iloc[0] + assert net.line.geo.geojson.within(circle_polygon).sum() == 11 + assert all(net.line[net.line.geo.geojson.within(circle_polygon)].index == [14, 17, 46, 47, 55, 116, + 117, 118, 120, 121, 134]) line = shapely.geometry.LineString([[7.8947079593416, 48.40549007606241], [7.896048283667894, 48.41060722903666], @@ -354,8 +357,6 @@ def test_geo_accessor_geopandas(): assert net.line.geo.geojson.as_shapely_obj.at[0] == line assert np.allclose(net.line.geo.geojson.total_bounds, [7.74426069, 48.32845845, 7.93829196, 48.47484423]) - assert net.line.geo.geojson.intersects(reference_point)[:3].all() == True - assert net.line.geo.geojson.intersects(reference_point)[3:].any() == False if __name__ == '__main__': diff --git a/tutorials/Working with GeoJSON.ipynb b/tutorials/Working with GeoJSON.ipynb new file mode 100644 index 000000000..5ea820028 --- /dev/null +++ b/tutorials/Working with GeoJSON.ipynb @@ -0,0 +1,7915 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "61a0f50a-318c-4a8b-98e9-17c3bc14b839", + "metadata": {}, + "source": [ + "## Working with GeoJSON" + ] + }, + { + "cell_type": "markdown", + "id": "dc6d7b2d-deb2-4da2-aa31-420c66b63edf", + "metadata": {}, + "source": [ + "In the 3.0 release of pandapower, significant changes have been made to how geospatial data is stored in the net object. Previously, geospatial data was stored in the *line_geodata* and *bus_geodata* tables. However, this structure caused several problems:\n", + "\n", + "- In what projection are the data points stored?\n", + "- What do x and y represent? Does x represent latitude, longitude or something else?\n", + "- Copmatibility issues with frontend services and QGIS\n", + "\n", + "To resolve this ambiguity, all geospatial data has now been moved to the line and bus tables under the *geo* column. The geometries are stored as GeoJSON objects. [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) is a widely used standard for geospatial data, supporting points, lines, and polygons in a unified format with explicit projection definitions." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fe634b5e-44ca-43f3-befb-1aecbe995a56", + "metadata": {}, + "outputs": [], + "source": [ + "import pandapower as pp\n", + "import pandapower.networks as pn\n", + "import pandas as pd\n", + "pd.set_option(\"display.max_colwidth\", 120)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "08f8a836-ea70-4f20-af4b-3f25166fa9cd", + "metadata": {}, + "outputs": [], + "source": [ + "net = pn.mv_oberrhein()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2bed640f-1894-4b71-9b93-8899dd047f6c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 {\"coordinates\": [[7.8947079593416, 48.40549007606241], [7.896048283667894, 48.41060722903666], [7.896173712216692, 4...\n", + "1 {\"coordinates\": [[7.8947079593416, 48.40549007606241], [7.892690190852129, 48.40551636934287], [7.887502065342014, 4...\n", + "2 {\"coordinates\": [[7.8947079593416, 48.40549007606241], [7.895064345442788, 48.40360449047714]], \"type\": \"LineString\"}\n", + "3 {\"coordinates\": [[7.895064345442788, 48.40360449047714], [7.89613191775149, 48.39796660294922]], \"type\": \"LineString\"}\n", + "4 {\"coordinates\": [[7.89613191775149, 48.39796660294922], [7.897077173332039, 48.39788292247765], [7.9009358501319475,...\n", + "Name: geo, dtype: object \n", + "\n", + "0 {\"coordinates\": [7.765225672614365, 48.41091584192147], \"type\": \"Point\"}\n", + "1 {\"coordinates\": [7.778809539550178, 48.40987064550492], \"type\": \"Point\"}\n", + "2 {\"coordinates\": [7.779195765893586, 48.4120381144602], \"type\": \"Point\"}\n", + "3 {\"coordinates\": [7.775204689771646, 48.40610336882051], \"type\": \"Point\"}\n", + "4 {\"coordinates\": [7.76606484746611, 48.4124244421834], \"type\": \"Point\"}\n", + "Name: geo, dtype: object\n" + ] + } + ], + "source": [ + "print(net.line.geo.head(), \"\\n\")\n", + "print(net.bus.geo.head())" + ] + }, + { + "cell_type": "markdown", + "id": "74af88da-10fe-43dd-a8ce-6b3ec57ab9ad", + "metadata": {}, + "source": [ + "To make working with these new geospatial data easier and more efficient, a pandas series accessor has been introduced. This accessor allows direct interaction with GeoJSON data and integrates functions provided by GeoSeries in geopandas." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ee923be1-9fa4-4141-8099-9c2d96b4a169", + "metadata": {}, + "outputs": [], + "source": [ + "from shapely.geometry import shape, Point, Polygon\n", + "from geopandas import GeoSeries\n", + "from pandapower.plotting import simple_plotly, create_line_trace, create_bus_trace\n", + "import plotly.graph_objects as go" + ] + }, + { + "cell_type": "markdown", + "id": "9b18429f-ffae-481b-bbcf-278aeed2fd1e", + "metadata": {}, + "source": [ + "Create a reference point and a polygon to check what elements are inside a certain radius. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8de3ac01-8008-42cc-b669-23cf029424e5", + "metadata": {}, + "outputs": [], + "source": [ + "reference_point = (7.781067, 48.389774)\n", + "radius_m = 2200\n", + "circle_polygon = GeoSeries([Point(reference_point)], crs=4326).to_crs(epsg=31467).buffer(radius_m).to_crs(epsg=4326).iloc[0]" + ] + }, + { + "cell_type": "markdown", + "id": "4cea9b32-fe45-4c95-8842-a7e688c82e2f", + "metadata": {}, + "source": [ + "Access the GeoJSON accessor and the needed GeoSeries methods." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f1052155-c72c-4ec2-86ec-9b871c2de3f6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 7.74426069, 48.32845845, 7.93829196, 48.47484423])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lines_intersect = net.line[net.line.geo.geojson.intersects(circle_polygon)].index\n", + "buses_within = net.bus[net.bus.geo.geojson.within(circle_polygon)].index\n", + "net.line.geo.geojson.total_bounds" + ] + }, + { + "cell_type": "markdown", + "id": "e7966a23-87e9-4dae-a256-c0170234e4fb", + "metadata": {}, + "source": [ + "And plot the results" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "32231088-02e8-497d-9cae-279aba140370", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": true, + "text": "Index: 0
Name: Line 0
Length: 0.586 km
R: 0.094 Ohm
X: 0.069 Ohm
", + "type": "scatter", + "x": [ + 7.89470796, + 7.89604828, + 7.89617371 + ], + "y": [ + 48.40549008, + 48.41060723, + 48.41100311 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 1
Name: Line 1
Length: 1.374 km
R: 0.221 Ohm
X: 0.161 Ohm
", + "type": "scatter", + "x": [ + 7.89470796, + 7.89269019, + 7.88750207, + 7.87852914 + ], + "y": [ + 48.40549008, + 48.40551637, + 48.40575964, + 48.40552685 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 2
Name: Line 2
Length: 0.206 km
R: 0.033 Ohm
X: 0.024 Ohm
", + "type": "scatter", + "x": [ + 7.89470796, + 7.89506435 + ], + "y": [ + 48.40549008, + 48.40360449 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 3
Name: Line 3
Length: 0.617 km
R: 0.099 Ohm
X: 0.072 Ohm
", + "type": "scatter", + "x": [ + 7.89506435, + 7.89613192 + ], + "y": [ + 48.40360449, + 48.3979666 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 4
Name: Line 4
Length: 0.382 km
R: 0.061 Ohm
X: 0.045 Ohm
", + "type": "scatter", + "x": [ + 7.89613192, + 7.89707717, + 7.90093585, + 7.90130481 + ], + "y": [ + 48.3979666, + 48.39788292, + 48.39783123, + 48.39757192 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 5
Name: Line 5
Length: 3.303 km
R: 0.532 Ohm
X: 0.386 Ohm
", + "type": "scatter", + "x": [ + 7.78157825, + 7.78241594, + 7.78757125, + 7.80563692, + 7.81137059 + ], + "y": [ + 48.36717009, + 48.367589, + 48.37274714, + 48.38813105, + 48.38535297 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 6
Name: Line 6
Length: 0.35 km
R: 0.056 Ohm
X: 0.041 Ohm
", + "type": "scatter", + "x": [ + 7.78157825, + 7.78412315, + 7.78580944 + ], + "y": [ + 48.36717009, + 48.3667497, + 48.36588431 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 7
Name: Line 7
Length: 0.736 km
R: 0.118 Ohm
X: 0.086 Ohm
", + "type": "scatter", + "x": [ + 7.78157825, + 7.78187454, + 7.78091314, + 7.77915169, + 7.77724287, + 7.77577656, + 7.77569591 + ], + "y": [ + 48.36717009, + 48.36700871, + 48.36559507, + 48.36620372, + 48.36639702, + 48.36481251, + 48.36446775 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 10
Name: Line 10
Length: 0.313 km
R: 0.05 Ohm
X: 0.037 Ohm
", + "type": "scatter", + "x": [ + 7.76147904, + 7.75838011 + ], + "y": [ + 48.35023997, + 48.34834127 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 11
Name: Line 11
Length: 0.258 km
R: 0.042 Ohm
X: 0.03 Ohm
", + "type": "scatter", + "x": [ + 7.75569132, + 7.75642284, + 7.75645615, + 7.75838011 + ], + "y": [ + 48.34750562, + 48.34759206, + 48.34812068, + 48.34834127 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 12
Name: Line 12
Length: 0.435 km
R: 0.07 Ohm
X: 0.051 Ohm
", + "type": "scatter", + "x": [ + 7.75598728, + 7.75493654, + 7.75226417, + 7.7490983 + ], + "y": [ + 48.38553948, + 48.38515046, + 48.38528923, + 48.38651875 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 14
Name: Line 14
Length: 0.357 km
R: 0.058 Ohm
X: 0.042 Ohm
", + "type": "scatter", + "x": [ + 7.75598728, + 7.75586722, + 7.7565325, + 7.75620547, + 7.75669407, + 7.75527749 + ], + "y": [ + 48.38553948, + 48.38577499, + 48.38647184, + 48.38674957, + 48.38741536, + 48.38787178 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 15
Name: Line 15
Length: 0.254 km
R: 0.041 Ohm
X: 0.03 Ohm
", + "type": "scatter", + "x": [ + 7.81672668, + 7.81622594, + 7.81440186 + ], + "y": [ + 48.38789817, + 48.38787587, + 48.38940745 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 16
Name: Line 16
Length: 0.635 km
R: 0.102 Ohm
X: 0.074 Ohm
", + "type": "scatter", + "x": [ + 7.81192866, + 7.81348597, + 7.81397942, + 7.81428296, + 7.81686386, + 7.81672668 + ], + "y": [ + 48.38755402, + 48.38824938, + 48.38770218, + 48.38637651, + 48.38743514, + 48.38789817 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 17
Name: Line 17
Length: 1.3 km
R: 0.209 Ohm
X: 0.152 Ohm
", + "type": "scatter", + "x": [ + 7.76396796, + 7.77111891 + ], + "y": [ + 48.39721686, + 48.40810119 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 18
Name: Line 18
Length: 0.139 km
R: 0.022 Ohm
X: 0.016 Ohm
", + "type": "scatter", + "x": [ + 7.77111891, + 7.76965603, + 7.76946387 + ], + "y": [ + 48.40810119, + 48.40897041, + 48.40913221 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 20
Name: Line 20
Length: 0.632 km
R: 0.102 Ohm
X: 0.074 Ohm
", + "type": "scatter", + "x": [ + 7.83613832, + 7.83612548, + 7.83739965, + 7.84120448, + 7.84164512 + ], + "y": [ + 48.40115573, + 48.40114719, + 48.40180507, + 48.40398808, + 48.40414471 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 21
Name: Line 21
Length: 0.112 km
R: 0.018 Ohm
X: 0.013 Ohm
", + "type": "scatter", + "x": [ + 7.83613832, + 7.83578331, + 7.83571694 + ], + "y": [ + 48.40115573, + 48.40022903, + 48.40021513 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 22
Name: Line 22
Length: 0.312 km
R: 0.05 Ohm
X: 0.036 Ohm
", + "type": "scatter", + "x": [ + 7.8893131, + 7.88800433, + 7.88846812 + ], + "y": [ + 48.41271759, + 48.41442695, + 48.41556286 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 24
Name: Line 24
Length: 0.695 km
R: 0.112 Ohm
X: 0.081 Ohm
", + "type": "scatter", + "x": [ + 7.88846812, + 7.8790417 + ], + "y": [ + 48.41556286, + 48.41572609 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 25
Name: Line 25
Length: 0.235 km
R: 0.038 Ohm
X: 0.028 Ohm
", + "type": "scatter", + "x": [ + 7.90130341, + 7.90201885, + 7.90263858 + ], + "y": [ + 48.41377255, + 48.41396058, + 48.4152001 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 26
Name: Line 26
Length: 0.561 km
R: 0.09 Ohm
X: 0.066 Ohm
", + "type": "scatter", + "x": [ + 7.89638358, + 7.89759947, + 7.89789524, + 7.90183054, + 7.90263858 + ], + "y": [ + 48.41424248, + 48.41442463, + 48.41538087, + 48.41552371, + 48.4152001 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 27
Name: Line 27
Length: 0.522 km
R: 0.084 Ohm
X: 0.061 Ohm
", + "type": "scatter", + "x": [ + 7.81091684, + 7.80981003, + 7.81328782 + ], + "y": [ + 48.43490897, + 48.43301239, + 48.43169169 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 28
Name: Line 28
Length: 0.283 km
R: 0.034 Ohm
X: 0.032 Ohm
", + "type": "scatter", + "x": [ + 7.81328782, + 7.81279149, + 7.81092206, + 7.80946722 + ], + "y": [ + 48.43169169, + 48.43138573, + 48.43111791, + 48.43003727 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 29
Name: Line 29
Length: 1.688 km
R: 0.272 Ohm
X: 0.197 Ohm
", + "type": "scatter", + "x": [ + 7.81593058, + 7.81744827, + 7.8154392, + 7.81828689, + 7.82563195, + 7.82449029 + ], + "y": [ + 48.46357677, + 48.46330569, + 48.4608867, + 48.45821173, + 48.45715225, + 48.45412175 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 30
Name: Line 30
Length: 1.073 km
R: 0.173 Ohm
X: 0.125 Ohm
", + "type": "scatter", + "x": [ + 7.80501543, + 7.8082015, + 7.81153875, + 7.81182781, + 7.81550992, + 7.81538774, + 7.81593058 + ], + "y": [ + 48.46296614, + 48.46152072, + 48.46377225, + 48.46392051, + 48.46287434, + 48.46354503, + 48.46357677 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 32
Name: Line 32
Length: 0.324 km
R: 0.052 Ohm
X: 0.038 Ohm
", + "type": "scatter", + "x": [ + 7.81599919, + 7.81749857, + 7.81777506, + 7.8195498 + ], + "y": [ + 48.45200045, + 48.45179274, + 48.45227496, + 48.45155157 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 34
Name: Line 34
Length: 0.309 km
R: 0.05 Ohm
X: 0.036 Ohm
", + "type": "scatter", + "x": [ + 7.80022205, + 7.80011359, + 7.80061038, + 7.79950378 + ], + "y": [ + 48.46640752, + 48.46713886, + 48.46829712, + 48.46870231 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 35
Name: Line 35
Length: 0.541 km
R: 0.087 Ohm
X: 0.063 Ohm
", + "type": "scatter", + "x": [ + 7.80752534, + 7.80022205 + ], + "y": [ + 48.46584322, + 48.46640752 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 36
Name: Line 36
Length: 0.296 km
R: 0.036 Ohm
X: 0.033 Ohm
", + "type": "scatter", + "x": [ + 7.8060588, + 7.80642131, + 7.80559996, + 7.80673406 + ], + "y": [ + 48.4359944, + 48.43620557, + 48.43694878, + 48.43815272 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 37
Name: Line 37
Length: 0.48 km
R: 0.059 Ohm
X: 0.054 Ohm
", + "type": "scatter", + "x": [ + 7.8060588, + 7.80533577, + 7.80519949, + 7.80460343, + 7.80466609 + ], + "y": [ + 48.4359944, + 48.4362301, + 48.43479354, + 48.43346182, + 48.43251067 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 38
Name: Line 38
Length: 0.711 km
R: 0.084 Ohm
X: 0.228 Ohm
", + "type": "scatter", + "x": [ + 7.76944157, + 7.76608392, + 7.76574338 + ], + "y": [ + 48.41414363, + 48.41861893, + 48.41987793 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 39
Name: Line 39
Length: 0.242 km
R: 0.039 Ohm
X: 0.028 Ohm
", + "type": "scatter", + "x": [ + 7.76944157, + 7.76973028, + 7.77054216, + 7.77150376 + ], + "y": [ + 48.41414363, + 48.41394093, + 48.41413473, + 48.41297284 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 40
Name: Line 40
Length: 0.261 km
R: 0.042 Ohm
X: 0.031 Ohm
", + "type": "scatter", + "x": [ + 7.76944157, + 7.76840613, + 7.76779918, + 7.76713218, + 7.76659341 + ], + "y": [ + 48.41414363, + 48.41437144, + 48.41460782, + 48.41456685, + 48.41394964 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 41
Name: Line 41
Length: 0.345 km
R: 0.055 Ohm
X: 0.04 Ohm
", + "type": "scatter", + "x": [ + 7.81091684, + 7.8117633, + 7.81235889, + 7.81213406 + ], + "y": [ + 48.43490897, + 48.434804, + 48.43586959, + 48.43725275 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 42
Name: Line 42
Length: 1.621 km
R: 0.261 Ohm
X: 0.19 Ohm
", + "type": "scatter", + "x": [ + 7.75962984, + 7.75828376, + 7.75716085, + 7.75346128, + 7.75312516 + ], + "y": [ + 48.34441059, + 48.34240156, + 48.34188624, + 48.33596248, + 48.33198343 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 43
Name: Line 43
Length: 0.782 km
R: 0.126 Ohm
X: 0.091 Ohm
", + "type": "scatter", + "x": [ + 7.75312516, + 7.76187712, + 7.76124005 + ], + "y": [ + 48.33198343, + 48.3303465, + 48.32945775 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 44
Name: Line 44
Length: 0.248 km
R: 0.04 Ohm
X: 0.029 Ohm
", + "type": "scatter", + "x": [ + 7.75312516, + 7.75357352, + 7.75339537 + ], + "y": [ + 48.33198343, + 48.3302089, + 48.32979038 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 45
Name: Line 45
Length: 2.611 km
R: 0.42 Ohm
X: 0.305 Ohm
", + "type": "scatter", + "x": [ + 7.8790417, + 7.87741614, + 7.85466459, + 7.84538508 + ], + "y": [ + 48.41572609, + 48.41575402, + 48.42084885, + 48.42296455 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 46
Name: Line 46
Length: 1.158 km
R: 0.186 Ohm
X: 0.136 Ohm
", + "type": "scatter", + "x": [ + 7.77526664, + 7.77749826, + 7.78873191 + ], + "y": [ + 48.40850593, + 48.40904312, + 48.4038838 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 47
Name: Line 47
Length: 1.526 km
R: 0.246 Ohm
X: 0.179 Ohm
", + "type": "scatter", + "x": [ + 7.78873191, + 7.80573238, + 7.80577582 + ], + "y": [ + 48.4038838, + 48.39607413, + 48.39606378 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 48
Name: Line 48
Length: 2.033 km
R: 0.327 Ohm
X: 0.238 Ohm
", + "type": "scatter", + "x": [ + 7.84263834, + 7.86375666 + ], + "y": [ + 48.39811545, + 48.40965934 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 49
Name: Line 49
Length: 0.997 km
R: 0.161 Ohm
X: 0.117 Ohm
", + "type": "scatter", + "x": [ + 7.86375666, + 7.86622592, + 7.87554357, + 7.87852914 + ], + "y": [ + 48.40965934, + 48.40835437, + 48.40633627, + 48.40552685 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 50
Name: Line 50
Length: 0.804 km
R: 0.129 Ohm
X: 0.094 Ohm
", + "type": "scatter", + "x": [ + 7.81265378, + 7.81224648, + 7.81164404, + 7.80993094, + 7.80608736, + 7.80577582 + ], + "y": [ + 48.39170089, + 48.39208848, + 48.3932605, + 48.3941038, + 48.39455474, + 48.39606378 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 51
Name: Line 51
Length: 0.947 km
R: 0.153 Ohm
X: 0.111 Ohm
", + "type": "scatter", + "x": [ + 7.84263834, + 7.84203085, + 7.83909688, + 7.83989267, + 7.84139083, + 7.842892, + 7.84208706, + 7.84164512 + ], + "y": [ + 48.39811545, + 48.39828929, + 48.40038691, + 48.40115019, + 48.4022093, + 48.40261122, + 48.40386263, + 48.40414471 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 52
Name: Line 52
Length: 0.929 km
R: 0.11 Ohm
X: 0.297 Ohm
", + "type": "scatter", + "x": [ + 7.78665902, + 7.78697406, + 7.79905951 + ], + "y": [ + 48.43625936, + 48.43621079, + 48.43759153 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 53
Name: Line 53
Length: 0.778 km
R: 0.092 Ohm
X: 0.249 Ohm
", + "type": "scatter", + "x": [ + 7.77637635, + 7.78665902 + ], + "y": [ + 48.43784809, + 48.43625936 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 54
Name: Line 54
Length: 0.713 km
R: 0.087 Ohm
X: 0.08 Ohm
", + "type": "scatter", + "x": [ + 7.79905951, + 7.79926973, + 7.80294669, + 7.80443842, + 7.80526684, + 7.80673406 + ], + "y": [ + 48.43759153, + 48.43836508, + 48.43811625, + 48.43774645, + 48.43721361, + 48.43815272 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 55
Name: Line 55
Length: 1.136 km
R: 0.183 Ohm
X: 0.133 Ohm
", + "type": "scatter", + "x": [ + 7.76396796, + 7.76495858, + 7.76629087, + 7.7659259, + 7.76376563, + 7.76270002, + 7.76157105, + 7.76147033 + ], + "y": [ + 48.39721686, + 48.39694908, + 48.39667324, + 48.39581646, + 48.39356873, + 48.39201827, + 48.38930585, + 48.38881634 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 56
Name: Line 56
Length: 2.149 km
R: 0.346 Ohm
X: 0.251 Ohm
", + "type": "scatter", + "x": [ + 7.84538508, + 7.82134675, + 7.81812248 + ], + "y": [ + 48.42296455, + 48.42844094, + 48.42974203 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 57
Name: Line 57
Length: 0.811 km
R: 0.131 Ohm
X: 0.095 Ohm
", + "type": "scatter", + "x": [ + 7.81434875, + 7.81661465 + ], + "y": [ + 48.43503248, + 48.44215898 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 58
Name: Line 58
Length: 0.47 km
R: 0.076 Ohm
X: 0.055 Ohm
", + "type": "scatter", + "x": [ + 7.83405306, + 7.83429351, + 7.83604853, + 7.83517172, + 7.83584018 + ], + "y": [ + 48.46664643, + 48.46690297, + 48.46785146, + 48.4694022, + 48.47002488 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 59
Name: Line 59
Length: 3.155 km
R: 0.508 Ohm
X: 0.369 Ohm
", + "type": "scatter", + "x": [ + 7.87260819, + 7.87120517, + 7.86568507, + 7.85567453, + 7.83429197, + 7.83405306 + ], + "y": [ + 48.45794147, + 48.45816151, + 48.45958978, + 48.45952178, + 48.46491575, + 48.46664643 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 60
Name: Line 60
Length: 0.58 km
R: 0.093 Ohm
X: 0.068 Ohm
", + "type": "scatter", + "x": [ + 7.82727201, + 7.82773651, + 7.82914139, + 7.83186634, + 7.8335644, + 7.83405306 + ], + "y": [ + 48.46648941, + 48.46658586, + 48.46719882, + 48.46600061, + 48.46663092, + 48.46664643 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 61
Name: Line 61
Length: 0.618 km
R: 0.1 Ohm
X: 0.072 Ohm
", + "type": "scatter", + "x": [ + 7.81894107, + 7.82727201 + ], + "y": [ + 48.46584214, + 48.46648941 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 62
Name: Line 62
Length: 1.006 km
R: 0.12 Ohm
X: 0.322 Ohm
", + "type": "scatter", + "x": [ + 7.76153339, + 7.77637635 + ], + "y": [ + 48.43987488, + 48.43784809 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 63
Name: Line 63
Length: 0.222 km
R: 0.036 Ohm
X: 0.026 Ohm
", + "type": "scatter", + "x": [ + 7.78701848, + 7.78662366, + 7.78600346 + ], + "y": [ + 48.46045784, + 48.45994229, + 48.4585943 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 64
Name: Line 64
Length: 0.936 km
R: 0.151 Ohm
X: 0.11 Ohm
", + "type": "scatter", + "x": [ + 7.79891555, + 7.79884316, + 7.78719486, + 7.78701848 + ], + "y": [ + 48.46284776, + 48.46255155, + 48.46047176, + 48.46045784 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 65
Name: Line 65
Length: 1.341 km
R: 0.216 Ohm
X: 0.157 Ohm
", + "type": "scatter", + "x": [ + 7.79305306, + 7.79508502, + 7.7949182, + 7.79616509, + 7.78883056, + 7.78670975, + 7.7864642, + 7.78701848 + ], + "y": [ + 48.46585031, + 48.46578218, + 48.46500727, + 48.46263647, + 48.461078, + 48.46104579, + 48.46053878, + 48.46045784 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 67
Name: Line 67
Length: 0.26 km
R: 0.042 Ohm
X: 0.03 Ohm
", + "type": "scatter", + "x": [ + 7.81137059, + 7.81159537, + 7.81106554, + 7.81136875, + 7.81192866 + ], + "y": [ + 48.38535297, + 48.38592827, + 48.38643007, + 48.3871739, + 48.38755402 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 68
Name: Line 68
Length: 1.804 km
R: 0.29 Ohm
X: 0.211 Ohm
", + "type": "scatter", + "x": [ + 7.90849453, + 7.90939134, + 7.92408252, + 7.92629214, + 7.93125749 + ], + "y": [ + 48.39773842, + 48.39944778, + 48.39840209, + 48.39898093, + 48.3980943 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 69
Name: Line 69
Length: 0.59 km
R: 0.095 Ohm
X: 0.069 Ohm
", + "type": "scatter", + "x": [ + 7.93125749, + 7.93285196, + 7.93773871 + ], + "y": [ + 48.3980943, + 48.39632695, + 48.39605777 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 70
Name: Line 70
Length: 0.368 km
R: 0.059 Ohm
X: 0.043 Ohm
", + "type": "scatter", + "x": [ + 7.90485626, + 7.90565763, + 7.90302727 + ], + "y": [ + 48.42317236, + 48.423879, + 48.42556885 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 71
Name: Line 71
Length: 0.162 km
R: 0.026 Ohm
X: 0.019 Ohm
", + "type": "scatter", + "x": [ + 7.90302727, + 7.90431342, + 7.90432357 + ], + "y": [ + 48.42556885, + 48.42674696, + 48.42675459 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 72
Name: Line 72
Length: 1.455 km
R: 0.234 Ohm
X: 0.17 Ohm
", + "type": "scatter", + "x": [ + 7.90302727, + 7.90042277, + 7.89592512, + 7.89306465, + 7.8883681, + 7.88777356 + ], + "y": [ + 48.42556885, + 48.42678215, + 48.43071547, + 48.43187132, + 48.43274836, + 48.43320652 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 73
Name: Line 73
Length: 0.466 km
R: 0.075 Ohm
X: 0.054 Ohm
", + "type": "scatter", + "x": [ + 7.90115002, + 7.90298335, + 7.9045586, + 7.90759827 + ], + "y": [ + 48.42189392, + 48.42021485, + 48.41985264, + 48.42108182 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 74
Name: Line 74
Length: 0.53 km
R: 0.085 Ohm
X: 0.062 Ohm
", + "type": "scatter", + "x": [ + 7.90759827, + 7.90753143, + 7.90529898, + 7.90828612 + ], + "y": [ + 48.42108182, + 48.42140869, + 48.4225983, + 48.42418937 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 75
Name: Line 75
Length: 0.304 km
R: 0.049 Ohm
X: 0.036 Ohm
", + "type": "scatter", + "x": [ + 7.90115002, + 7.90485626 + ], + "y": [ + 48.42189392, + 48.42317236 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 76
Name: Line 76
Length: 0.347 km
R: 0.056 Ohm
X: 0.041 Ohm
", + "type": "scatter", + "x": [ + 7.75752837, + 7.75743197, + 7.75456734 + ], + "y": [ + 48.3495233, + 48.35033194, + 48.35261081 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 77
Name: Line 77
Length: 2.122 km
R: 0.342 Ohm
X: 0.248 Ohm
", + "type": "scatter", + "x": [ + 7.75706106, + 7.76129469, + 7.75752837 + ], + "y": [ + 48.36812437, + 48.35405755, + 48.3495233 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 78
Name: Line 78
Length: 0.296 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "type": "scatter", + "x": [ + 7.91778262, + 7.91291408 + ], + "y": [ + 48.42400659, + 48.42255372 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 79
Name: Line 79
Length: 0.275 km
R: 0.044 Ohm
X: 0.032 Ohm
", + "type": "scatter", + "x": [ + 7.91291408, + 7.91460816, + 7.91393385 + ], + "y": [ + 48.42255372, + 48.42455518, + 48.42590629 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 80
Name: Line 80
Length: 0.428 km
R: 0.069 Ohm
X: 0.05 Ohm
", + "type": "scatter", + "x": [ + 7.90933438, + 7.90960971, + 7.91052635, + 7.91170676, + 7.91260163, + 7.91393385 + ], + "y": [ + 48.42618849, + 48.42620031, + 48.42567694, + 48.42624241, + 48.42551677, + 48.42590629 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 81
Name: Line 81
Length: 0.338 km
R: 0.054 Ohm
X: 0.039 Ohm
", + "type": "scatter", + "x": [ + 7.91291408, + 7.9149878, + 7.91378673 + ], + "y": [ + 48.42255372, + 48.42109714, + 48.42043868 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 82
Name: Line 82
Length: 0.349 km
R: 0.056 Ohm
X: 0.041 Ohm
", + "type": "scatter", + "x": [ + 7.90933438, + 7.91062827, + 7.90828612 + ], + "y": [ + 48.42618849, + 48.42540398, + 48.42418937 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 85
Name: Line 85
Length: 0.329 km
R: 0.053 Ohm
X: 0.038 Ohm
", + "type": "scatter", + "x": [ + 7.8884535, + 7.88614206, + 7.88603646 + ], + "y": [ + 48.41021512, + 48.41132488, + 48.41239165 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 86
Name: Line 86
Length: 0.261 km
R: 0.042 Ohm
X: 0.031 Ohm
", + "type": "scatter", + "x": [ + 7.8893131, + 7.88675468, + 7.88626878, + 7.88603646 + ], + "y": [ + 48.41271759, + 48.41253887, + 48.41264631, + 48.41239165 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 87
Name: Line 87
Length: 0.298 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "type": "scatter", + "x": [ + 7.90172272, + 7.90147838, + 7.90130481 + ], + "y": [ + 48.40051529, + 48.39898139, + 48.39757192 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 90
Name: Line 90
Length: 0.315 km
R: 0.051 Ohm
X: 0.037 Ohm
", + "type": "scatter", + "x": [ + 7.89617371, + 7.89836453, + 7.89989694 + ], + "y": [ + 48.41100311, + 48.41119729, + 48.41219988 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 91
Name: Line 91
Length: 2.528 km
R: 0.407 Ohm
X: 0.296 Ohm
", + "type": "scatter", + "x": [ + 7.93283542, + 7.93111133, + 7.92478234, + 7.91378673 + ], + "y": [ + 48.40163972, + 48.40314742, + 48.41145849, + 48.42043868 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 92
Name: Line 92
Length: 0.093 km
R: 0.015 Ohm
X: 0.011 Ohm
", + "type": "scatter", + "x": [ + 7.89437546, + 7.89460962, + 7.89638358 + ], + "y": [ + 48.41295983, + 48.41328729, + 48.41424248 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 93
Name: Line 93
Length: 0.387 km
R: 0.062 Ohm
X: 0.045 Ohm
", + "type": "scatter", + "x": [ + 7.8893131, + 7.89437546 + ], + "y": [ + 48.41271759, + 48.41295983 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 94
Name: Line 94
Length: 0.272 km
R: 0.044 Ohm
X: 0.032 Ohm
", + "type": "scatter", + "x": [ + 7.89989694, + 7.90151744, + 7.90130341 + ], + "y": [ + 48.41219988, + 48.4124301, + 48.41377255 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 95
Name: Line 95
Length: 0.247 km
R: 0.04 Ohm
X: 0.029 Ohm
", + "type": "scatter", + "x": [ + 7.93283542, + 7.93445481, + 7.93628735 + ], + "y": [ + 48.40163972, + 48.40146312, + 48.40091852 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 96
Name: Line 96
Length: 0.25 km
R: 0.04 Ohm
X: 0.029 Ohm
", + "type": "scatter", + "x": [ + 7.93628735, + 7.93787155, + 7.93829196 + ], + "y": [ + 48.40091852, + 48.39967434, + 48.39911502 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 97
Name: Line 97
Length: 0.459 km
R: 0.074 Ohm
X: 0.054 Ohm
", + "type": "scatter", + "x": [ + 7.93773871, + 7.93790841, + 7.93663385, + 7.93681572, + 7.93773403, + 7.93754769, + 7.93829196 + ], + "y": [ + 48.39605777, + 48.3962261, + 48.39706036, + 48.39826119, + 48.3986056, + 48.39883465, + 48.39911502 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 98
Name: Line 98
Length: 0.366 km
R: 0.059 Ohm
X: 0.043 Ohm
", + "type": "scatter", + "x": [ + 7.75339537, + 7.7558693, + 7.75792025 + ], + "y": [ + 48.32979038, + 48.32897412, + 48.32845845 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 99
Name: Line 99
Length: 0.291 km
R: 0.047 Ohm
X: 0.034 Ohm
", + "type": "scatter", + "x": [ + 7.75962984, + 7.75782277, + 7.75578023 + ], + "y": [ + 48.34441059, + 48.34403542, + 48.34396313 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 100
Name: Line 100
Length: 0.299 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "type": "scatter", + "x": [ + 7.90591947, + 7.90587019, + 7.90775174, + 7.90849453 + ], + "y": [ + 48.39621703, + 48.39677807, + 48.39705376, + 48.39773842 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 101
Name: Line 101
Length: 0.236 km
R: 0.038 Ohm
X: 0.028 Ohm
", + "type": "scatter", + "x": [ + 7.90205872, + 7.90261989, + 7.90443237 + ], + "y": [ + 48.39529931, + 48.39512339, + 48.39390775 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 102
Name: Line 102
Length: 0.489 km
R: 0.079 Ohm
X: 0.057 Ohm
", + "type": "scatter", + "x": [ + 7.90443237, + 7.90720374, + 7.90819035, + 7.90794776, + 7.9083386 + ], + "y": [ + 48.39390775, + 48.39294498, + 48.39430049, + 48.3944891, + 48.3949975 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 103
Name: Line 103
Length: 0.298 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "type": "scatter", + "x": [ + 7.9083386, + 7.90787741, + 7.90591947 + ], + "y": [ + 48.3949975, + 48.3963407, + 48.39621703 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 104
Name: Line 104
Length: 0.238 km
R: 0.038 Ohm
X: 0.028 Ohm
", + "type": "scatter", + "x": [ + 7.77541442, + 7.77589723, + 7.77627383, + 7.77670102 + ], + "y": [ + 48.3593875, + 48.36023212, + 48.36082142, + 48.36133205 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 105
Name: Line 105
Length: 0.177 km
R: 0.029 Ohm
X: 0.021 Ohm
", + "type": "scatter", + "x": [ + 7.77670102, + 7.77713313, + 7.77757114, + 7.77756681 + ], + "y": [ + 48.36133205, + 48.36134284, + 48.36244816, + 48.36261088 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 106
Name: Line 106
Length: 0.283 km
R: 0.046 Ohm
X: 0.033 Ohm
", + "type": "scatter", + "x": [ + 7.77603927, + 7.77632396, + 7.77541442 + ], + "y": [ + 48.35663792, + 48.35789023, + 48.3593875 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 107
Name: Line 107
Length: 0.615 km
R: 0.099 Ohm
X: 0.072 Ohm
", + "type": "scatter", + "x": [ + 7.77032597, + 7.77212983, + 7.77603927 + ], + "y": [ + 48.35975183, + 48.35778043, + 48.35663792 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 108
Name: Line 108
Length: 1.278 km
R: 0.206 Ohm
X: 0.149 Ohm
", + "type": "scatter", + "x": [ + 7.77032597, + 7.76147904 + ], + "y": [ + 48.35975183, + 48.35023997 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 109
Name: Line 109
Length: 0.147 km
R: 0.024 Ohm
X: 0.017 Ohm
", + "type": "scatter", + "x": [ + 7.77569591, + 7.77641956, + 7.77756681 + ], + "y": [ + 48.36446775, + 48.36350223, + 48.36261088 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 110
Name: Line 110
Length: 0.253 km
R: 0.041 Ohm
X: 0.03 Ohm
", + "type": "scatter", + "x": [ + 7.75578023, + 7.75504431, + 7.75395235, + 7.75385941 + ], + "y": [ + 48.34396313, + 48.34426775, + 48.34552928, + 48.34577289 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 111
Name: Line 111
Length: 0.214 km
R: 0.034 Ohm
X: 0.025 Ohm
", + "type": "scatter", + "x": [ + 7.75618441, + 7.7562193, + 7.75569132 + ], + "y": [ + 48.34562366, + 48.34600529, + 48.34750562 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 112
Name: Line 112
Length: 0.415 km
R: 0.067 Ohm
X: 0.049 Ohm
", + "type": "scatter", + "x": [ + 7.75569132, + 7.75419626, + 7.75453023, + 7.75269597 + ], + "y": [ + 48.34750562, + 48.34758219, + 48.34920225, + 48.34979926 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 113
Name: Line 113
Length: 0.18 km
R: 0.029 Ohm
X: 0.021 Ohm
", + "type": "scatter", + "x": [ + 7.75618441, + 7.75608475, + 7.75403123, + 7.75385941 + ], + "y": [ + 48.34562366, + 48.34553329, + 48.34569381, + 48.34577289 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 114
Name: Line 114
Length: 0.562 km
R: 0.09 Ohm
X: 0.066 Ohm
", + "type": "scatter", + "x": [ + 7.75269597, + 7.75181083, + 7.74517856 + ], + "y": [ + 48.34979926, + 48.34979996, + 48.34907864 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 116
Name: Line 116
Length: 0.399 km
R: 0.064 Ohm
X: 0.047 Ohm
", + "type": "scatter", + "x": [ + 7.75780411, + 7.75917051, + 7.76226955 + ], + "y": [ + 48.38819904, + 48.38730208, + 48.38622196 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 117
Name: Line 117
Length: 0.261 km
R: 0.042 Ohm
X: 0.031 Ohm
", + "type": "scatter", + "x": [ + 7.76226955, + 7.76344386, + 7.7644815 + ], + "y": [ + 48.38622196, + 48.38591421, + 48.38725384 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 118
Name: Line 118
Length: 0.37 km
R: 0.06 Ohm
X: 0.043 Ohm
", + "type": "scatter", + "x": [ + 7.7644815, + 7.76213031, + 7.76276391, + 7.76147033 + ], + "y": [ + 48.38725384, + 48.38794756, + 48.38852075, + 48.38881634 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 119
Name: Line 119
Length: 0.251 km
R: 0.04 Ohm
X: 0.029 Ohm
", + "type": "scatter", + "x": [ + 7.7490983, + 7.74791722, + 7.74990855 + ], + "y": [ + 48.38651875, + 48.38670319, + 48.38849988 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 120
Name: Line 120
Length: 0.304 km
R: 0.049 Ohm
X: 0.036 Ohm
", + "type": "scatter", + "x": [ + 7.75780411, + 7.75698634, + 7.75516628, + 7.75527749 + ], + "y": [ + 48.38819904, + 48.38848522, + 48.38875367, + 48.38787178 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 121
Name: Line 121
Length: 0.233 km
R: 0.037 Ohm
X: 0.027 Ohm
", + "type": "scatter", + "x": [ + 7.75387463, + 7.75423985, + 7.75489924, + 7.75490995, + 7.75612888 + ], + "y": [ + 48.38198803, + 48.38177685, + 48.38177159, + 48.38124748, + 48.38127334 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 122
Name: Line 122
Length: 0.118 km
R: 0.019 Ohm
X: 0.014 Ohm
", + "type": "scatter", + "x": [ + 7.74776625, + 7.7490983 + ], + "y": [ + 48.38593046, + 48.38651875 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 123
Name: Line 123
Length: 0.397 km
R: 0.064 Ohm
X: 0.046 Ohm
", + "type": "scatter", + "x": [ + 7.75387463, + 7.75041699, + 7.7512397 + ], + "y": [ + 48.38198803, + 48.38256216, + 48.38363665 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 124
Name: Line 124
Length: 0.52 km
R: 0.084 Ohm
X: 0.061 Ohm
", + "type": "scatter", + "x": [ + 7.7512397, + 7.75163773, + 7.750171, + 7.74884833, + 7.74776625 + ], + "y": [ + 48.38363665, + 48.38424806, + 48.38471159, + 48.38478902, + 48.38593046 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 125
Name: Line 125
Length: 1.526 km
R: 0.246 Ohm
X: 0.179 Ohm
", + "type": "scatter", + "x": [ + 7.75387463, + 7.75471952, + 7.75706106 + ], + "y": [ + 48.38198803, + 48.37590247, + 48.36812437 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 126
Name: Line 126
Length: 0.254 km
R: 0.041 Ohm
X: 0.03 Ohm
", + "type": "scatter", + "x": [ + 7.74776625, + 7.7473456, + 7.74426069 + ], + "y": [ + 48.38593046, + 48.38664415, + 48.38747799 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 127
Name: Line 127
Length: 1.601 km
R: 0.19 Ohm
X: 0.512 Ohm
", + "type": "scatter", + "x": [ + 7.76574338, + 7.75852885 + ], + "y": [ + 48.41987793, + 48.43345788 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 128
Name: Line 128
Length: 0.473 km
R: 0.076 Ohm
X: 0.055 Ohm
", + "type": "scatter", + "x": [ + 7.76574338, + 7.76644776, + 7.76739671 + ], + "y": [ + 48.41987793, + 48.42002993, + 48.42373656 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 129
Name: Line 129
Length: 0.298 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "type": "scatter", + "x": [ + 7.77518896, + 7.77674239, + 7.77880954 + ], + "y": [ + 48.4106662, + 48.41077816, + 48.40987065 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 130
Name: Line 130
Length: 0.295 km
R: 0.047 Ohm
X: 0.034 Ohm
", + "type": "scatter", + "x": [ + 7.76522567, + 7.76483782, + 7.76606485 + ], + "y": [ + 48.41091584, + 48.41125561, + 48.41242444 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 131
Name: Line 131
Length: 0.417 km
R: 0.067 Ohm
X: 0.049 Ohm
", + "type": "scatter", + "x": [ + 7.7617981, + 7.76399226, + 7.76572294, + 7.76606485 + ], + "y": [ + 48.41348695, + 48.41182097, + 48.41245092, + 48.41242444 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 132
Name: Line 132
Length: 0.323 km
R: 0.052 Ohm
X: 0.038 Ohm
", + "type": "scatter", + "x": [ + 7.76946387, + 7.76943184, + 7.7665952, + 7.76522567 + ], + "y": [ + 48.40913221, + 48.40915918, + 48.40969202, + 48.41091584 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 133
Name: Line 133
Length: 0.296 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "type": "scatter", + "x": [ + 7.77599056, + 7.77674873, + 7.77861783, + 7.77919577 + ], + "y": [ + 48.41297438, + 48.41331062, + 48.41231411, + 48.41203811 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 134
Name: Line 134
Length: 0.282 km
R: 0.045 Ohm
X: 0.033 Ohm
", + "type": "scatter", + "x": [ + 7.77526664, + 7.77576803, + 7.77520469 + ], + "y": [ + 48.40850593, + 48.40798048, + 48.40610337 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 135
Name: Line 135
Length: 0.396 km
R: 0.064 Ohm
X: 0.046 Ohm
", + "type": "scatter", + "x": [ + 7.76659341, + 7.76544426, + 7.76267774, + 7.7617981 + ], + "y": [ + 48.41394964, + 48.41402817, + 48.41423953, + 48.41348695 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 136
Name: Line 136
Length: 0.371 km
R: 0.06 Ohm
X: 0.043 Ohm
", + "type": "scatter", + "x": [ + 7.77150376, + 7.77342658, + 7.77532817, + 7.77599056 + ], + "y": [ + 48.41297284, + 48.41358426, + 48.41284311, + 48.41297438 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 137
Name: Line 137
Length: 0.274 km
R: 0.044 Ohm
X: 0.032 Ohm
", + "type": "scatter", + "x": [ + 7.77526664, + 7.77583421, + 7.77591886, + 7.77518896 + ], + "y": [ + 48.40850593, + 48.40895948, + 48.41002749, + 48.4106662 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 138
Name: Line 138
Length: 0.329 km
R: 0.053 Ohm
X: 0.038 Ohm
", + "type": "scatter", + "x": [ + 7.77518896, + 7.77615683, + 7.77606931, + 7.77543634, + 7.77599056 + ], + "y": [ + 48.4106662, + 48.41108201, + 48.41202505, + 48.41266512, + 48.41297438 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 139
Name: Line 139
Length: 0.499 km
R: 0.08 Ohm
X: 0.058 Ohm
", + "type": "scatter", + "x": [ + 7.83571694, + 7.83401712, + 7.8326561, + 7.83058701 + ], + "y": [ + 48.40021513, + 48.39984603, + 48.39850087, + 48.39753675 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 140
Name: Line 140
Length: 0.316 km
R: 0.051 Ohm
X: 0.037 Ohm
", + "type": "scatter", + "x": [ + 7.83571694, + 7.8358586, + 7.8368015, + 7.83782535 + ], + "y": [ + 48.40021513, + 48.39919649, + 48.39847232, + 48.3990085 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 141
Name: Line 141
Length: 0.388 km
R: 0.062 Ohm
X: 0.045 Ohm
", + "type": "scatter", + "x": [ + 7.81434875, + 7.81325053, + 7.81253272, + 7.8128154, + 7.81257537, + 7.81213406 + ], + "y": [ + 48.43503248, + 48.43528857, + 48.43543937, + 48.43601104, + 48.437316, + 48.43725275 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 142
Name: Line 142
Length: 0.07 km
R: 0.011 Ohm
X: 0.008 Ohm
", + "type": "scatter", + "x": [ + 7.81440186, + 7.8142915, + 7.81375095 + ], + "y": [ + 48.38940745, + 48.38938411, + 48.38919055 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 143
Name: Line 143
Length: 0.286 km
R: 0.046 Ohm
X: 0.033 Ohm
", + "type": "scatter", + "x": [ + 7.81440186, + 7.81428393, + 7.8127534, + 7.81265378 + ], + "y": [ + 48.38940745, + 48.38967584, + 48.39156961, + 48.39170089 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 144
Name: Line 144
Length: 1.517 km
R: 0.244 Ohm
X: 0.178 Ohm
", + "type": "scatter", + "x": [ + 7.82229551, + 7.8238694, + 7.82106089, + 7.82234704, + 7.82409718, + 7.82817169, + 7.82922235, + 7.83034891, + 7.83058701 + ], + "y": [ + 48.38807328, + 48.38845699, + 48.3902318, + 48.39111773, + 48.3930508, + 48.3948561, + 48.39670488, + 48.39743032, + 48.39753675 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 145
Name: Line 145
Length: 0.719 km
R: 0.116 Ohm
X: 0.084 Ohm
", + "type": "scatter", + "x": [ + 7.81702049, + 7.81878871, + 7.81943944, + 7.82266554, + 7.82198478, + 7.82281578, + 7.82229551 + ], + "y": [ + 48.38429982, + 48.38482288, + 48.38520652, + 48.38641133, + 48.387031, + 48.38770494, + 48.38807328 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 146
Name: Line 146
Length: 0.22 km
R: 0.035 Ohm
X: 0.026 Ohm
", + "type": "scatter", + "x": [ + 7.81550141, + 7.81654608, + 7.81760243, + 7.81702049 + ], + "y": [ + 48.38288709, + 48.38240623, + 48.38311519, + 48.38429982 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 147
Name: Line 147
Length: 0.21 km
R: 0.034 Ohm
X: 0.025 Ohm
", + "type": "scatter", + "x": [ + 7.81800216, + 7.81735047, + 7.81627775, + 7.81599919 + ], + "y": [ + 48.45286283, + 48.4523668, + 48.45247233, + 48.45200045 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 148
Name: Line 148
Length: 0.239 km
R: 0.039 Ohm
X: 0.028 Ohm
", + "type": "scatter", + "x": [ + 7.81599919, + 7.81594703, + 7.81544555, + 7.81487777, + 7.81386611 + ], + "y": [ + 48.45200045, + 48.45184899, + 48.45198309, + 48.45113912, + 48.45138395 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 149
Name: Line 149
Length: 0.345 km
R: 0.055 Ohm
X: 0.04 Ohm
", + "type": "scatter", + "x": [ + 7.82260854, + 7.81800216 + ], + "y": [ + 48.45231101, + 48.45286283 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 150
Name: Line 150
Length: 0.327 km
R: 0.053 Ohm
X: 0.038 Ohm
", + "type": "scatter", + "x": [ + 7.82260854, + 7.82256678, + 7.82319722, + 7.82449029 + ], + "y": [ + 48.45231101, + 48.45266208, + 48.45431913, + 48.45412175 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 151
Name: Line 151
Length: 0.553 km
R: 0.068 Ohm
X: 0.062 Ohm
", + "type": "scatter", + "x": [ + 7.80946722, + 7.80854958, + 7.80464847, + 7.80466609 + ], + "y": [ + 48.43003727, + 48.43027934, + 48.43083045, + 48.43251067 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 152
Name: Line 152
Length: 0.146 km
R: 0.024 Ohm
X: 0.017 Ohm
", + "type": "scatter", + "x": [ + 7.80946722, + 7.80939155, + 7.81078043 + ], + "y": [ + 48.43003727, + 48.4299802, + 48.42871494 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 153
Name: Line 153
Length: 0.482 km
R: 0.078 Ohm
X: 0.056 Ohm
", + "type": "scatter", + "x": [ + 7.81434875, + 7.81587268, + 7.81541879, + 7.81753115 + ], + "y": [ + 48.43503248, + 48.43497042, + 48.43310111, + 48.43283117 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 154
Name: Line 154
Length: 0.443 km
R: 0.071 Ohm
X: 0.052 Ohm
", + "type": "scatter", + "x": [ + 7.81812248, + 7.81808382, + 7.81713512, + 7.81746181, + 7.81664213, + 7.81753115 + ], + "y": [ + 48.42974203, + 48.42987869, + 48.43023546, + 48.43069249, + 48.43088424, + 48.43283117 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 156
Name: Line 156
Length: 0.284 km
R: 0.046 Ohm
X: 0.033 Ohm
", + "type": "scatter", + "x": [ + 7.81812248, + 7.81640506, + 7.81702793, + 7.81719863 + ], + "y": [ + 48.42974203, + 48.42919601, + 48.428887, + 48.4281192 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 157
Name: Line 157
Length: 1.082 km
R: 0.128 Ohm
X: 0.346 Ohm
", + "type": "scatter", + "x": [ + 7.90923486, + 7.90348365, + 7.90031707, + 7.8959583 + ], + "y": [ + 48.4561543, + 48.45358185, + 48.45301795, + 48.45361129 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 158
Name: Line 158
Length: 0.689 km
R: 0.082 Ohm
X: 0.221 Ohm
", + "type": "scatter", + "x": [ + 7.8959583, + 7.89568627, + 7.88830762 + ], + "y": [ + 48.45361129, + 48.4547515, + 48.45601148 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 161
Name: Line 161
Length: 0.468 km
R: 0.057 Ohm
X: 0.052 Ohm
", + "type": "scatter", + "x": [ + 7.88830762, + 7.88776916, + 7.88367481, + 7.88212331 + ], + "y": [ + 48.45601148, + 48.45627719, + 48.45609713, + 48.45630215 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 162
Name: Line 162
Length: 2.595 km
R: 0.308 Ohm
X: 0.83 Ohm
", + "type": "scatter", + "x": [ + 7.88546159, + 7.91396063 + ], + "y": [ + 48.4585246, + 48.4569382 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 163
Name: Line 163
Length: 0.306 km
R: 0.049 Ohm
X: 0.036 Ohm
", + "type": "scatter", + "x": [ + 7.80752534, + 7.80754102, + 7.81003126, + 7.81053946, + 7.8112944 + ], + "y": [ + 48.46584322, + 48.46596098, + 48.46600036, + 48.46630498, + 48.466147 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 164
Name: Line 164
Length: 0.566 km
R: 0.091 Ohm
X: 0.066 Ohm
", + "type": "scatter", + "x": [ + 7.81894107, + 7.81480813, + 7.81294305, + 7.8112944 + ], + "y": [ + 48.46584214, + 48.46581066, + 48.46590381, + 48.466147 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 165
Name: Line 165
Length: 0.365 km
R: 0.043 Ohm
X: 0.117 Ohm
", + "type": "scatter", + "x": [ + 7.91309968, + 7.91279093, + 7.9120236, + 7.90923486 + ], + "y": [ + 48.4566322, + 48.45683348, + 48.45740074, + 48.4561543 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 167
Name: Line 167
Length: 0.464 km
R: 0.075 Ohm
X: 0.054 Ohm
", + "type": "scatter", + "x": [ + 7.79891555, + 7.80501543 + ], + "y": [ + 48.46284776, + 48.46296614 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 168
Name: Line 168
Length: 0.42 km
R: 0.068 Ohm
X: 0.049 Ohm
", + "type": "scatter", + "x": [ + 7.79950378, + 7.79789121, + 7.79674274 + ], + "y": [ + 48.46870231, + 48.46588425, + 48.46588038 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 169
Name: Line 169
Length: 0.272 km
R: 0.044 Ohm
X: 0.032 Ohm
", + "type": "scatter", + "x": [ + 7.79674274, + 7.79411097, + 7.79305306 + ], + "y": [ + 48.46588038, + 48.46590636, + 48.46585031 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 170
Name: Line 170
Length: 0.285 km
R: 0.046 Ohm
X: 0.033 Ohm
", + "type": "scatter", + "x": [ + 7.87725311, + 7.87671923, + 7.87675502, + 7.87697289, + 7.87715626 + ], + "y": [ + 48.46499297, + 48.46561508, + 48.46636841, + 48.46654892, + 48.46740551 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 171
Name: Line 171
Length: 0.77 km
R: 0.124 Ohm
X: 0.09 Ohm
", + "type": "scatter", + "x": [ + 7.8758161, + 7.87553451, + 7.87598516, + 7.87548683, + 7.87663709, + 7.87744195, + 7.87845944, + 7.87715626 + ], + "y": [ + 48.47236011, + 48.47232304, + 48.47165336, + 48.4711409, + 48.46913517, + 48.46937138, + 48.46779457, + 48.46740551 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 172
Name: Line 172
Length: 0.264 km
R: 0.032 Ohm
X: 0.03 Ohm
", + "type": "scatter", + "x": [ + 7.88344689, + 7.88378722, + 7.88260716 + ], + "y": [ + 48.46135559, + 48.46292134, + 48.46302758 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 173
Name: Line 173
Length: 0.197 km
R: 0.024 Ohm
X: 0.022 Ohm
", + "type": "scatter", + "x": [ + 7.88260716, + 7.88401855, + 7.88432076 + ], + "y": [ + 48.46302758, + 48.46350368, + 48.46419159 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 174
Name: Line 174
Length: 0.28 km
R: 0.045 Ohm
X: 0.033 Ohm
", + "type": "scatter", + "x": [ + 7.88023004, + 7.88045814, + 7.88260716 + ], + "y": [ + 48.46216934, + 48.46323446, + 48.46302758 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 175
Name: Line 175
Length: 0.398 km
R: 0.049 Ohm
X: 0.045 Ohm
", + "type": "scatter", + "x": [ + 7.88546159, + 7.88510802, + 7.88463276, + 7.8840797, + 7.88379148, + 7.88340999, + 7.88344689 + ], + "y": [ + 48.4585246, + 48.45925446, + 48.46021773, + 48.46019306, + 48.46086262, + 48.46087555, + 48.46135559 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 177
Name: Line 177
Length: 0.293 km
R: 0.047 Ohm
X: 0.034 Ohm
", + "type": "scatter", + "x": [ + 7.8758161, + 7.87666867 + ], + "y": [ + 48.47236011, + 48.47484423 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 178
Name: Line 178
Length: 0.449 km
R: 0.072 Ohm
X: 0.052 Ohm
", + "type": "scatter", + "x": [ + 7.87990442, + 7.87881309, + 7.8762772, + 7.8758161 + ], + "y": [ + 48.46999586, + 48.4713525, + 48.4716203, + 48.47236011 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 179
Name: Line 179
Length: 0.551 km
R: 0.089 Ohm
X: 0.064 Ohm
", + "type": "scatter", + "x": [ + 7.88088449, + 7.88165, + 7.88259545, + 7.88105677, + 7.87990442 + ], + "y": [ + 48.46626641, + 48.46667163, + 48.4670659, + 48.46954176, + 48.46999586 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 180
Name: Line 180
Length: 1.42 km
R: 0.229 Ohm
X: 0.166 Ohm
", + "type": "scatter", + "x": [ + 7.88777356, + 7.88466467, + 7.87836276, + 7.87625272 + ], + "y": [ + 48.43320652, + 48.43560077, + 48.44264826, + 48.44299303 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 181
Name: Line 181
Length: 0.54 km
R: 0.087 Ohm
X: 0.063 Ohm
", + "type": "scatter", + "x": [ + 7.88517175, + 7.88450517, + 7.88091238, + 7.87808299 + ], + "y": [ + 48.45375027, + 48.45379718, + 48.45401796, + 48.45477828 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 182
Name: Line 182
Length: 0.355 km
R: 0.043 Ohm
X: 0.04 Ohm
", + "type": "scatter", + "x": [ + 7.87808299, + 7.87790893, + 7.87858024, + 7.87701423, + 7.87771765 + ], + "y": [ + 48.45477828, + 48.45486293, + 48.45553885, + 48.45634075, + 48.45713891 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 183
Name: Line 183
Length: 0.338 km
R: 0.041 Ohm
X: 0.038 Ohm
", + "type": "scatter", + "x": [ + 7.88212331, + 7.87980154, + 7.87771765 + ], + "y": [ + 48.45630215, + 48.45668177, + 48.45713891 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 184
Name: Line 184
Length: 0.44 km
R: 0.071 Ohm
X: 0.051 Ohm
", + "type": "scatter", + "x": [ + 7.88546159, + 7.88352252, + 7.88060094, + 7.88076055 + ], + "y": [ + 48.4585246, + 48.4583596, + 48.45867551, + 48.45936355 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 185
Name: Line 185
Length: 1.095 km
R: 0.176 Ohm
X: 0.128 Ohm
", + "type": "scatter", + "x": [ + 7.88517175, + 7.88530198, + 7.88291376, + 7.87922427, + 7.87688954 + ], + "y": [ + 48.45375027, + 48.45321614, + 48.44925911, + 48.44679416, + 48.44659344 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 186
Name: Line 186
Length: 0.346 km
R: 0.056 Ohm
X: 0.041 Ohm
", + "type": "scatter", + "x": [ + 7.88517175, + 7.88695118, + 7.88692885, + 7.88824433 + ], + "y": [ + 48.45375027, + 48.45372929, + 48.45278221, + 48.45231709 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 187
Name: Line 187
Length: 0.41 km
R: 0.066 Ohm
X: 0.048 Ohm
", + "type": "scatter", + "x": [ + 7.87688954, + 7.87686341, + 7.87640856, + 7.87643942, + 7.87625272 + ], + "y": [ + 48.44659344, + 48.44509708, + 48.44445558, + 48.44349227, + 48.44299303 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 189
Name: Line 189
Length: 0.446 km
R: 0.072 Ohm
X: 0.052 Ohm
", + "type": "scatter", + "x": [ + 7.87260819, + 7.87239645, + 7.87493521, + 7.87520041 + ], + "y": [ + 48.45794147, + 48.45828506, + 48.46130227, + 48.46134835 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 190
Name: Line 190
Length: 0.314 km
R: 0.051 Ohm
X: 0.037 Ohm
", + "type": "scatter", + "x": [ + 7.87520041, + 7.87515322, + 7.87712123, + 7.87750063 + ], + "y": [ + 48.46134835, + 48.46161584, + 48.46328049, + 48.46363931 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 191
Name: Line 191
Length: 0.152 km
R: 0.024 Ohm
X: 0.018 Ohm
", + "type": "scatter", + "x": [ + 7.87750063, + 7.87725311 + ], + "y": [ + 48.46363931, + 48.46499297 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 192
Name: Line 192
Length: 0.397 km
R: 0.064 Ohm
X: 0.046 Ohm
", + "type": "scatter", + "x": [ + 7.88088449, + 7.88213238, + 7.88358692, + 7.88432076 + ], + "y": [ + 48.46626641, + 48.46601268, + 48.46585855, + 48.46419159 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "lines", + "showlegend": false, + "text": "Index: 193
Name: Line 193
Length: 1.006 km
R: 0.12 Ohm
X: 0.322 Ohm
", + "type": "scatter", + "x": [ + 7.75852885, + 7.7589569, + 7.75843138, + 7.76153339 + ], + "y": [ + 48.43345788, + 48.43510804, + 48.43782243, + 48.43987488 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "disconnected lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "dot", + "width": 0.5 + }, + "mode": "lines", + "name": "disconnected branches", + "showlegend": true, + "text": "Index: 66
Name: Line 66
Length: 0.446 km
R: 0.072 Ohm
X: 0.052 Ohm
", + "type": "scatter", + "x": [ + 7.81137059, + 7.81131879, + 7.81550141 + ], + "y": [ + 48.38535297, + 48.38527882, + 48.38288709 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "disconnected lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "dot", + "width": 0.5 + }, + "mode": "lines", + "name": "disconnected branches", + "showlegend": false, + "text": "Index: 8
Name: Line 8
Length: 0.146 km
R: 0.023 Ohm
X: 0.017 Ohm
", + "type": "scatter", + "x": [ + 7.75838011, + 7.75808461, + 7.75752837 + ], + "y": [ + 48.34834127, + 48.34860058, + 48.3495233 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "disconnected lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "dot", + "width": 0.5 + }, + "mode": "lines", + "name": "disconnected branches", + "showlegend": false, + "text": "Index: 23
Name: Line 23
Length: 1.188 km
R: 0.191 Ohm
X: 0.139 Ohm
", + "type": "scatter", + "x": [ + 7.88846812, + 7.89363213, + 7.90115002 + ], + "y": [ + 48.41556286, + 48.41919475, + 48.42189392 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "disconnected lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "dot", + "width": 0.5 + }, + "mode": "lines", + "name": "disconnected branches", + "showlegend": false, + "text": "Index: 88
Name: Line 88
Length: 0.296 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "type": "scatter", + "x": [ + 7.90130481, + 7.90122937, + 7.90205872 + ], + "y": [ + 48.39757192, + 48.3966186, + 48.39529931 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "disconnected lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "dot", + "width": 0.5 + }, + "mode": "lines", + "name": "disconnected branches", + "showlegend": false, + "text": "Index: 188
Name: Line 188
Length: 0.386 km
R: 0.062 Ohm
X: 0.045 Ohm
", + "type": "scatter", + "x": [ + 7.87771765, + 7.87260819 + ], + "y": [ + 48.45713891, + 48.45794147 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "disconnected lines", + "line": { + "color": "rgb(128, 128, 128)", + "dash": "dot", + "width": 0.5 + }, + "mode": "lines", + "name": "disconnected branches", + "showlegend": false, + "text": "Index: 31
Name: Line 31
Length: 0.967 km
R: 0.156 Ohm
X: 0.113 Ohm
", + "type": "scatter", + "x": [ + 7.81661465, + 7.8195498 + ], + "y": [ + 48.44215898, + 48.45155157 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "marker": { + "color": "rgb(128, 128, 128)", + "size": 1, + "symbol": "circle" + }, + "mode": "markers", + "name": "edge_center", + "showlegend": false, + "text": [ + "Index: 0
Name: Line 0
Length: 0.586 km
R: 0.094 Ohm
X: 0.069 Ohm
", + "Index: 1
Name: Line 1
Length: 1.374 km
R: 0.221 Ohm
X: 0.161 Ohm
", + "Index: 2
Name: Line 2
Length: 0.206 km
R: 0.033 Ohm
X: 0.024 Ohm
", + "Index: 3
Name: Line 3
Length: 0.617 km
R: 0.099 Ohm
X: 0.072 Ohm
", + "Index: 4
Name: Line 4
Length: 0.382 km
R: 0.061 Ohm
X: 0.045 Ohm
", + "Index: 5
Name: Line 5
Length: 3.303 km
R: 0.532 Ohm
X: 0.386 Ohm
", + "Index: 6
Name: Line 6
Length: 0.35 km
R: 0.056 Ohm
X: 0.041 Ohm
", + "Index: 7
Name: Line 7
Length: 0.736 km
R: 0.118 Ohm
X: 0.086 Ohm
", + "Index: 10
Name: Line 10
Length: 0.313 km
R: 0.05 Ohm
X: 0.037 Ohm
", + "Index: 11
Name: Line 11
Length: 0.258 km
R: 0.042 Ohm
X: 0.03 Ohm
", + "Index: 12
Name: Line 12
Length: 0.435 km
R: 0.07 Ohm
X: 0.051 Ohm
", + "Index: 14
Name: Line 14
Length: 0.357 km
R: 0.058 Ohm
X: 0.042 Ohm
", + "Index: 15
Name: Line 15
Length: 0.254 km
R: 0.041 Ohm
X: 0.03 Ohm
", + "Index: 16
Name: Line 16
Length: 0.635 km
R: 0.102 Ohm
X: 0.074 Ohm
", + "Index: 17
Name: Line 17
Length: 1.3 km
R: 0.209 Ohm
X: 0.152 Ohm
", + "Index: 18
Name: Line 18
Length: 0.139 km
R: 0.022 Ohm
X: 0.016 Ohm
", + "Index: 20
Name: Line 20
Length: 0.632 km
R: 0.102 Ohm
X: 0.074 Ohm
", + "Index: 21
Name: Line 21
Length: 0.112 km
R: 0.018 Ohm
X: 0.013 Ohm
", + "Index: 22
Name: Line 22
Length: 0.312 km
R: 0.05 Ohm
X: 0.036 Ohm
", + "Index: 24
Name: Line 24
Length: 0.695 km
R: 0.112 Ohm
X: 0.081 Ohm
", + "Index: 25
Name: Line 25
Length: 0.235 km
R: 0.038 Ohm
X: 0.028 Ohm
", + "Index: 26
Name: Line 26
Length: 0.561 km
R: 0.09 Ohm
X: 0.066 Ohm
", + "Index: 27
Name: Line 27
Length: 0.522 km
R: 0.084 Ohm
X: 0.061 Ohm
", + "Index: 28
Name: Line 28
Length: 0.283 km
R: 0.034 Ohm
X: 0.032 Ohm
", + "Index: 29
Name: Line 29
Length: 1.688 km
R: 0.272 Ohm
X: 0.197 Ohm
", + "Index: 30
Name: Line 30
Length: 1.073 km
R: 0.173 Ohm
X: 0.125 Ohm
", + "Index: 32
Name: Line 32
Length: 0.324 km
R: 0.052 Ohm
X: 0.038 Ohm
", + "Index: 34
Name: Line 34
Length: 0.309 km
R: 0.05 Ohm
X: 0.036 Ohm
", + "Index: 35
Name: Line 35
Length: 0.541 km
R: 0.087 Ohm
X: 0.063 Ohm
", + "Index: 36
Name: Line 36
Length: 0.296 km
R: 0.036 Ohm
X: 0.033 Ohm
", + "Index: 37
Name: Line 37
Length: 0.48 km
R: 0.059 Ohm
X: 0.054 Ohm
", + "Index: 38
Name: Line 38
Length: 0.711 km
R: 0.084 Ohm
X: 0.228 Ohm
", + "Index: 39
Name: Line 39
Length: 0.242 km
R: 0.039 Ohm
X: 0.028 Ohm
", + "Index: 40
Name: Line 40
Length: 0.261 km
R: 0.042 Ohm
X: 0.031 Ohm
", + "Index: 41
Name: Line 41
Length: 0.345 km
R: 0.055 Ohm
X: 0.04 Ohm
", + "Index: 42
Name: Line 42
Length: 1.621 km
R: 0.261 Ohm
X: 0.19 Ohm
", + "Index: 43
Name: Line 43
Length: 0.782 km
R: 0.126 Ohm
X: 0.091 Ohm
", + "Index: 44
Name: Line 44
Length: 0.248 km
R: 0.04 Ohm
X: 0.029 Ohm
", + "Index: 45
Name: Line 45
Length: 2.611 km
R: 0.42 Ohm
X: 0.305 Ohm
", + "Index: 46
Name: Line 46
Length: 1.158 km
R: 0.186 Ohm
X: 0.136 Ohm
", + "Index: 47
Name: Line 47
Length: 1.526 km
R: 0.246 Ohm
X: 0.179 Ohm
", + "Index: 48
Name: Line 48
Length: 2.033 km
R: 0.327 Ohm
X: 0.238 Ohm
", + "Index: 49
Name: Line 49
Length: 0.997 km
R: 0.161 Ohm
X: 0.117 Ohm
", + "Index: 50
Name: Line 50
Length: 0.804 km
R: 0.129 Ohm
X: 0.094 Ohm
", + "Index: 51
Name: Line 51
Length: 0.947 km
R: 0.153 Ohm
X: 0.111 Ohm
", + "Index: 52
Name: Line 52
Length: 0.929 km
R: 0.11 Ohm
X: 0.297 Ohm
", + "Index: 53
Name: Line 53
Length: 0.778 km
R: 0.092 Ohm
X: 0.249 Ohm
", + "Index: 54
Name: Line 54
Length: 0.713 km
R: 0.087 Ohm
X: 0.08 Ohm
", + "Index: 55
Name: Line 55
Length: 1.136 km
R: 0.183 Ohm
X: 0.133 Ohm
", + "Index: 56
Name: Line 56
Length: 2.149 km
R: 0.346 Ohm
X: 0.251 Ohm
", + "Index: 57
Name: Line 57
Length: 0.811 km
R: 0.131 Ohm
X: 0.095 Ohm
", + "Index: 58
Name: Line 58
Length: 0.47 km
R: 0.076 Ohm
X: 0.055 Ohm
", + "Index: 59
Name: Line 59
Length: 3.155 km
R: 0.508 Ohm
X: 0.369 Ohm
", + "Index: 60
Name: Line 60
Length: 0.58 km
R: 0.093 Ohm
X: 0.068 Ohm
", + "Index: 61
Name: Line 61
Length: 0.618 km
R: 0.1 Ohm
X: 0.072 Ohm
", + "Index: 62
Name: Line 62
Length: 1.006 km
R: 0.12 Ohm
X: 0.322 Ohm
", + "Index: 63
Name: Line 63
Length: 0.222 km
R: 0.036 Ohm
X: 0.026 Ohm
", + "Index: 64
Name: Line 64
Length: 0.936 km
R: 0.151 Ohm
X: 0.11 Ohm
", + "Index: 65
Name: Line 65
Length: 1.341 km
R: 0.216 Ohm
X: 0.157 Ohm
", + "Index: 67
Name: Line 67
Length: 0.26 km
R: 0.042 Ohm
X: 0.03 Ohm
", + "Index: 68
Name: Line 68
Length: 1.804 km
R: 0.29 Ohm
X: 0.211 Ohm
", + "Index: 69
Name: Line 69
Length: 0.59 km
R: 0.095 Ohm
X: 0.069 Ohm
", + "Index: 70
Name: Line 70
Length: 0.368 km
R: 0.059 Ohm
X: 0.043 Ohm
", + "Index: 71
Name: Line 71
Length: 0.162 km
R: 0.026 Ohm
X: 0.019 Ohm
", + "Index: 72
Name: Line 72
Length: 1.455 km
R: 0.234 Ohm
X: 0.17 Ohm
", + "Index: 73
Name: Line 73
Length: 0.466 km
R: 0.075 Ohm
X: 0.054 Ohm
", + "Index: 74
Name: Line 74
Length: 0.53 km
R: 0.085 Ohm
X: 0.062 Ohm
", + "Index: 75
Name: Line 75
Length: 0.304 km
R: 0.049 Ohm
X: 0.036 Ohm
", + "Index: 76
Name: Line 76
Length: 0.347 km
R: 0.056 Ohm
X: 0.041 Ohm
", + "Index: 77
Name: Line 77
Length: 2.122 km
R: 0.342 Ohm
X: 0.248 Ohm
", + "Index: 78
Name: Line 78
Length: 0.296 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "Index: 79
Name: Line 79
Length: 0.275 km
R: 0.044 Ohm
X: 0.032 Ohm
", + "Index: 80
Name: Line 80
Length: 0.428 km
R: 0.069 Ohm
X: 0.05 Ohm
", + "Index: 81
Name: Line 81
Length: 0.338 km
R: 0.054 Ohm
X: 0.039 Ohm
", + "Index: 82
Name: Line 82
Length: 0.349 km
R: 0.056 Ohm
X: 0.041 Ohm
", + "Index: 85
Name: Line 85
Length: 0.329 km
R: 0.053 Ohm
X: 0.038 Ohm
", + "Index: 86
Name: Line 86
Length: 0.261 km
R: 0.042 Ohm
X: 0.031 Ohm
", + "Index: 87
Name: Line 87
Length: 0.298 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "Index: 90
Name: Line 90
Length: 0.315 km
R: 0.051 Ohm
X: 0.037 Ohm
", + "Index: 91
Name: Line 91
Length: 2.528 km
R: 0.407 Ohm
X: 0.296 Ohm
", + "Index: 92
Name: Line 92
Length: 0.093 km
R: 0.015 Ohm
X: 0.011 Ohm
", + "Index: 93
Name: Line 93
Length: 0.387 km
R: 0.062 Ohm
X: 0.045 Ohm
", + "Index: 94
Name: Line 94
Length: 0.272 km
R: 0.044 Ohm
X: 0.032 Ohm
", + "Index: 95
Name: Line 95
Length: 0.247 km
R: 0.04 Ohm
X: 0.029 Ohm
", + "Index: 96
Name: Line 96
Length: 0.25 km
R: 0.04 Ohm
X: 0.029 Ohm
", + "Index: 97
Name: Line 97
Length: 0.459 km
R: 0.074 Ohm
X: 0.054 Ohm
", + "Index: 98
Name: Line 98
Length: 0.366 km
R: 0.059 Ohm
X: 0.043 Ohm
", + "Index: 99
Name: Line 99
Length: 0.291 km
R: 0.047 Ohm
X: 0.034 Ohm
", + "Index: 100
Name: Line 100
Length: 0.299 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "Index: 101
Name: Line 101
Length: 0.236 km
R: 0.038 Ohm
X: 0.028 Ohm
", + "Index: 102
Name: Line 102
Length: 0.489 km
R: 0.079 Ohm
X: 0.057 Ohm
", + "Index: 103
Name: Line 103
Length: 0.298 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "Index: 104
Name: Line 104
Length: 0.238 km
R: 0.038 Ohm
X: 0.028 Ohm
", + "Index: 105
Name: Line 105
Length: 0.177 km
R: 0.029 Ohm
X: 0.021 Ohm
", + "Index: 106
Name: Line 106
Length: 0.283 km
R: 0.046 Ohm
X: 0.033 Ohm
", + "Index: 107
Name: Line 107
Length: 0.615 km
R: 0.099 Ohm
X: 0.072 Ohm
", + "Index: 108
Name: Line 108
Length: 1.278 km
R: 0.206 Ohm
X: 0.149 Ohm
", + "Index: 109
Name: Line 109
Length: 0.147 km
R: 0.024 Ohm
X: 0.017 Ohm
", + "Index: 110
Name: Line 110
Length: 0.253 km
R: 0.041 Ohm
X: 0.03 Ohm
", + "Index: 111
Name: Line 111
Length: 0.214 km
R: 0.034 Ohm
X: 0.025 Ohm
", + "Index: 112
Name: Line 112
Length: 0.415 km
R: 0.067 Ohm
X: 0.049 Ohm
", + "Index: 113
Name: Line 113
Length: 0.18 km
R: 0.029 Ohm
X: 0.021 Ohm
", + "Index: 114
Name: Line 114
Length: 0.562 km
R: 0.09 Ohm
X: 0.066 Ohm
", + "Index: 116
Name: Line 116
Length: 0.399 km
R: 0.064 Ohm
X: 0.047 Ohm
", + "Index: 117
Name: Line 117
Length: 0.261 km
R: 0.042 Ohm
X: 0.031 Ohm
", + "Index: 118
Name: Line 118
Length: 0.37 km
R: 0.06 Ohm
X: 0.043 Ohm
", + "Index: 119
Name: Line 119
Length: 0.251 km
R: 0.04 Ohm
X: 0.029 Ohm
", + "Index: 120
Name: Line 120
Length: 0.304 km
R: 0.049 Ohm
X: 0.036 Ohm
", + "Index: 121
Name: Line 121
Length: 0.233 km
R: 0.037 Ohm
X: 0.027 Ohm
", + "Index: 122
Name: Line 122
Length: 0.118 km
R: 0.019 Ohm
X: 0.014 Ohm
", + "Index: 123
Name: Line 123
Length: 0.397 km
R: 0.064 Ohm
X: 0.046 Ohm
", + "Index: 124
Name: Line 124
Length: 0.52 km
R: 0.084 Ohm
X: 0.061 Ohm
", + "Index: 125
Name: Line 125
Length: 1.526 km
R: 0.246 Ohm
X: 0.179 Ohm
", + "Index: 126
Name: Line 126
Length: 0.254 km
R: 0.041 Ohm
X: 0.03 Ohm
", + "Index: 127
Name: Line 127
Length: 1.601 km
R: 0.19 Ohm
X: 0.512 Ohm
", + "Index: 128
Name: Line 128
Length: 0.473 km
R: 0.076 Ohm
X: 0.055 Ohm
", + "Index: 129
Name: Line 129
Length: 0.298 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "Index: 130
Name: Line 130
Length: 0.295 km
R: 0.047 Ohm
X: 0.034 Ohm
", + "Index: 131
Name: Line 131
Length: 0.417 km
R: 0.067 Ohm
X: 0.049 Ohm
", + "Index: 132
Name: Line 132
Length: 0.323 km
R: 0.052 Ohm
X: 0.038 Ohm
", + "Index: 133
Name: Line 133
Length: 0.296 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "Index: 134
Name: Line 134
Length: 0.282 km
R: 0.045 Ohm
X: 0.033 Ohm
", + "Index: 135
Name: Line 135
Length: 0.396 km
R: 0.064 Ohm
X: 0.046 Ohm
", + "Index: 136
Name: Line 136
Length: 0.371 km
R: 0.06 Ohm
X: 0.043 Ohm
", + "Index: 137
Name: Line 137
Length: 0.274 km
R: 0.044 Ohm
X: 0.032 Ohm
", + "Index: 138
Name: Line 138
Length: 0.329 km
R: 0.053 Ohm
X: 0.038 Ohm
", + "Index: 139
Name: Line 139
Length: 0.499 km
R: 0.08 Ohm
X: 0.058 Ohm
", + "Index: 140
Name: Line 140
Length: 0.316 km
R: 0.051 Ohm
X: 0.037 Ohm
", + "Index: 141
Name: Line 141
Length: 0.388 km
R: 0.062 Ohm
X: 0.045 Ohm
", + "Index: 142
Name: Line 142
Length: 0.07 km
R: 0.011 Ohm
X: 0.008 Ohm
", + "Index: 143
Name: Line 143
Length: 0.286 km
R: 0.046 Ohm
X: 0.033 Ohm
", + "Index: 144
Name: Line 144
Length: 1.517 km
R: 0.244 Ohm
X: 0.178 Ohm
", + "Index: 145
Name: Line 145
Length: 0.719 km
R: 0.116 Ohm
X: 0.084 Ohm
", + "Index: 146
Name: Line 146
Length: 0.22 km
R: 0.035 Ohm
X: 0.026 Ohm
", + "Index: 147
Name: Line 147
Length: 0.21 km
R: 0.034 Ohm
X: 0.025 Ohm
", + "Index: 148
Name: Line 148
Length: 0.239 km
R: 0.039 Ohm
X: 0.028 Ohm
", + "Index: 149
Name: Line 149
Length: 0.345 km
R: 0.055 Ohm
X: 0.04 Ohm
", + "Index: 150
Name: Line 150
Length: 0.327 km
R: 0.053 Ohm
X: 0.038 Ohm
", + "Index: 151
Name: Line 151
Length: 0.553 km
R: 0.068 Ohm
X: 0.062 Ohm
", + "Index: 152
Name: Line 152
Length: 0.146 km
R: 0.024 Ohm
X: 0.017 Ohm
", + "Index: 153
Name: Line 153
Length: 0.482 km
R: 0.078 Ohm
X: 0.056 Ohm
", + "Index: 154
Name: Line 154
Length: 0.443 km
R: 0.071 Ohm
X: 0.052 Ohm
", + "Index: 156
Name: Line 156
Length: 0.284 km
R: 0.046 Ohm
X: 0.033 Ohm
", + "Index: 157
Name: Line 157
Length: 1.082 km
R: 0.128 Ohm
X: 0.346 Ohm
", + "Index: 158
Name: Line 158
Length: 0.689 km
R: 0.082 Ohm
X: 0.221 Ohm
", + "Index: 161
Name: Line 161
Length: 0.468 km
R: 0.057 Ohm
X: 0.052 Ohm
", + "Index: 162
Name: Line 162
Length: 2.595 km
R: 0.308 Ohm
X: 0.83 Ohm
", + "Index: 163
Name: Line 163
Length: 0.306 km
R: 0.049 Ohm
X: 0.036 Ohm
", + "Index: 164
Name: Line 164
Length: 0.566 km
R: 0.091 Ohm
X: 0.066 Ohm
", + "Index: 165
Name: Line 165
Length: 0.365 km
R: 0.043 Ohm
X: 0.117 Ohm
", + "Index: 167
Name: Line 167
Length: 0.464 km
R: 0.075 Ohm
X: 0.054 Ohm
", + "Index: 168
Name: Line 168
Length: 0.42 km
R: 0.068 Ohm
X: 0.049 Ohm
", + "Index: 169
Name: Line 169
Length: 0.272 km
R: 0.044 Ohm
X: 0.032 Ohm
", + "Index: 170
Name: Line 170
Length: 0.285 km
R: 0.046 Ohm
X: 0.033 Ohm
", + "Index: 171
Name: Line 171
Length: 0.77 km
R: 0.124 Ohm
X: 0.09 Ohm
", + "Index: 172
Name: Line 172
Length: 0.264 km
R: 0.032 Ohm
X: 0.03 Ohm
", + "Index: 173
Name: Line 173
Length: 0.197 km
R: 0.024 Ohm
X: 0.022 Ohm
", + "Index: 174
Name: Line 174
Length: 0.28 km
R: 0.045 Ohm
X: 0.033 Ohm
", + "Index: 175
Name: Line 175
Length: 0.398 km
R: 0.049 Ohm
X: 0.045 Ohm
", + "Index: 177
Name: Line 177
Length: 0.293 km
R: 0.047 Ohm
X: 0.034 Ohm
", + "Index: 178
Name: Line 178
Length: 0.449 km
R: 0.072 Ohm
X: 0.052 Ohm
", + "Index: 179
Name: Line 179
Length: 0.551 km
R: 0.089 Ohm
X: 0.064 Ohm
", + "Index: 180
Name: Line 180
Length: 1.42 km
R: 0.229 Ohm
X: 0.166 Ohm
", + "Index: 181
Name: Line 181
Length: 0.54 km
R: 0.087 Ohm
X: 0.063 Ohm
", + "Index: 182
Name: Line 182
Length: 0.355 km
R: 0.043 Ohm
X: 0.04 Ohm
", + "Index: 183
Name: Line 183
Length: 0.338 km
R: 0.041 Ohm
X: 0.038 Ohm
", + "Index: 184
Name: Line 184
Length: 0.44 km
R: 0.071 Ohm
X: 0.051 Ohm
", + "Index: 185
Name: Line 185
Length: 1.095 km
R: 0.176 Ohm
X: 0.128 Ohm
", + "Index: 186
Name: Line 186
Length: 0.346 km
R: 0.056 Ohm
X: 0.041 Ohm
", + "Index: 187
Name: Line 187
Length: 0.41 km
R: 0.066 Ohm
X: 0.048 Ohm
", + "Index: 189
Name: Line 189
Length: 0.446 km
R: 0.072 Ohm
X: 0.052 Ohm
", + "Index: 190
Name: Line 190
Length: 0.314 km
R: 0.051 Ohm
X: 0.037 Ohm
", + "Index: 191
Name: Line 191
Length: 0.152 km
R: 0.024 Ohm
X: 0.018 Ohm
", + "Index: 192
Name: Line 192
Length: 0.397 km
R: 0.064 Ohm
X: 0.046 Ohm
", + "Index: 193
Name: Line 193
Length: 1.006 km
R: 0.12 Ohm
X: 0.322 Ohm
", + "Index: 66
Name: Line 66
Length: 0.446 km
R: 0.072 Ohm
X: 0.052 Ohm
", + "Index: 8
Name: Line 8
Length: 0.146 km
R: 0.023 Ohm
X: 0.017 Ohm
", + "Index: 23
Name: Line 23
Length: 1.188 km
R: 0.191 Ohm
X: 0.139 Ohm
", + "Index: 88
Name: Line 88
Length: 0.296 km
R: 0.048 Ohm
X: 0.035 Ohm
", + "Index: 188
Name: Line 188
Length: 0.386 km
R: 0.062 Ohm
X: 0.045 Ohm
", + "Index: 31
Name: Line 31
Length: 0.967 km
R: 0.156 Ohm
X: 0.113 Ohm
" + ], + "type": "scatter", + "x": [ + 7.89537812, + 7.883015605000001, + 7.894886155, + 7.895598135, + 7.8990065099999995, + 7.796604085, + 7.7828507, + 7.7781972800000005, + 7.759929575, + 7.75741813, + 7.753600355, + 7.75644977, + 7.8153139, + 7.8141311899999994, + 7.767543435, + 7.77038747, + 7.839302065, + 7.835960815, + 7.888658715, + 7.88375491, + 7.9023287149999994, + 7.89986289, + 7.811548925, + 7.811856775000001, + 7.821959420000001, + 7.809870125, + 7.817636815, + 7.800361985, + 7.803873695, + 7.80616701, + 7.80490146, + 7.767762745, + 7.77102296, + 7.768102655, + 7.812061095, + 7.755311065, + 7.7575011400000005, + 7.75334934, + 7.866040365, + 7.783115085, + 7.797232145000001, + 7.8531975, + 7.870884745, + 7.80800915, + 7.84064175, + 7.793016785000001, + 7.781517685, + 7.80110821, + 7.7648457650000005, + 7.833365915, + 7.815481699999999, + 7.8356101250000005, + 7.84498325, + 7.830503865000001, + 7.8231065399999995, + 7.76895487, + 7.78631356, + 7.79301901, + 7.792497825, + 7.8112171450000005, + 7.91673693, + 7.935295334999999, + 7.90434245, + 7.903670345, + 7.898173945, + 7.9037709750000005, + 7.9067925500000005, + 7.90300314, + 7.755999655, + 7.759177875, + 7.91534835, + 7.91376112, + 7.9111165549999996, + 7.913950939999999, + 7.909457195, + 7.88729778, + 7.88803389, + 7.90160055, + 7.897269120000001, + 7.919284535, + 7.8954965999999995, + 7.89184428, + 7.90070719, + 7.9353710799999995, + 7.93707945, + 7.936724785, + 7.754632335, + 7.7568015, + 7.906810965, + 7.90352613, + 7.905818055, + 7.90689844, + 7.77608553, + 7.777352135, + 7.77586919, + 7.7740845499999995, + 7.765902505, + 7.776993185, + 7.75449833, + 7.75595531, + 7.754363245, + 7.75505799, + 7.748494695, + 7.76072003, + 7.76396268, + 7.763305904999999, + 7.748912884999999, + 7.75607631, + 7.754904595, + 7.748432275, + 7.75214581, + 7.749509665, + 7.75589029, + 7.745803145, + 7.762136115000001, + 7.766922235, + 7.777775965, + 7.765451335, + 7.76289518, + 7.76801352, + 7.77768328, + 7.77548636, + 7.764061, + 7.774377375, + 7.775876535, + 7.776113069999999, + 7.83333661, + 7.83633005, + 7.81267406, + 7.814021225, + 7.813518665, + 7.82322211, + 7.82105249, + 7.817074255, + 7.81681411, + 7.815161659999999, + 7.82030535, + 7.822882, + 7.806599025000001, + 7.81008599, + 7.815645735, + 7.81705197, + 7.81726377, + 7.90190036, + 7.891996945, + 7.885721985, + 7.89971111, + 7.8087861400000005, + 7.8168746, + 7.91062923, + 7.801965490000001, + 7.798697495000001, + 7.7954268550000005, + 7.876737125, + 7.8760619599999995, + 7.883617055, + 7.883312855, + 7.8815326500000005, + 7.88435623, + 7.876242384999999, + 7.877545145, + 7.88182611, + 7.8815137150000005, + 7.882708774999999, + 7.877797235, + 7.880962425, + 7.88206173, + 7.881069015, + 7.886940015, + 7.876635985, + 7.87366583, + 7.876137225, + 7.87737687, + 7.88285965, + 7.75869414, + 7.8134101000000005, + 7.75780649, + 7.897391075, + 7.901644045, + 7.87516292, + 7.8180822249999995 + ], + "y": [ + 48.408048655, + 48.405643245, + 48.404547285, + 48.400785545, + 48.397857075000005, + 48.380439095, + 48.366959895, + 48.366300370000005, + 48.34929062, + 48.348230975, + 48.385219844999995, + 48.387082465, + 48.388641660000005, + 48.387039345, + 48.402659025, + 48.408535799999996, + 48.402896575, + 48.400692379999995, + 48.41357227, + 48.415644475, + 48.41458034, + 48.415452290000005, + 48.43235204, + 48.43125182, + 48.45768199, + 48.462646485, + 48.45203385, + 48.46771799, + 48.46612537, + 48.43755075, + 48.43412768, + 48.416381279999996, + 48.413553785000005, + 48.41448963, + 48.435336795, + 48.33892436, + 48.331164965, + 48.331096165000005, + 48.418301435000004, + 48.40646346, + 48.399978965, + 48.403887395, + 48.40734532, + 48.39432927, + 48.401679745, + 48.43690116, + 48.437053725, + 48.438240664999995, + 48.394692594999995, + 48.425702745, + 48.43859573, + 48.46862683, + 48.462218765, + 48.466599715, + 48.466165775, + 48.438861485000004, + 48.459268295, + 48.461511654999995, + 48.461857235, + 48.386801985000005, + 48.398924935, + 48.39619236, + 48.424723924999995, + 48.426157905, + 48.42874881, + 48.420033745, + 48.423393835, + 48.42253314, + 48.351471375, + 48.36109096, + 48.423280155, + 48.42355445, + 48.425959675, + 48.42182543, + 48.424796674999996, + 48.41077, + 48.412628229999996, + 48.39974834, + 48.4111002, + 48.415948584999995, + 48.413764885, + 48.41283871, + 48.41231499, + 48.40119082, + 48.40029643, + 48.397660775, + 48.329382249999995, + 48.343999275, + 48.396915915, + 48.394515569999996, + 48.393426364999996, + 48.396278865, + 48.36052677, + 48.3618955, + 48.358638865, + 48.357209174999994, + 48.3549959, + 48.363056555, + 48.344898515, + 48.346755455, + 48.348392219999994, + 48.345613549999996, + 48.3494393, + 48.38676202, + 48.386584025000005, + 48.3876007, + 48.387601535, + 48.388619445, + 48.381509535, + 48.386224604999995, + 48.382275095, + 48.384750305, + 48.37201342, + 48.38706107, + 48.426667905, + 48.421883245000004, + 48.410324405, + 48.411840025000004, + 48.41265396, + 48.409425600000006, + 48.41281236499999, + 48.407041925, + 48.41413385, + 48.413213685, + 48.409493485, + 48.41155353, + 48.39917345, + 48.398834405, + 48.435725205, + 48.38928733, + 48.390622725, + 48.392084264999994, + 48.385808925, + 48.38276071, + 48.452419565, + 48.451561104999996, + 48.45258692, + 48.453490605, + 48.430554895, + 48.429347570000004, + 48.434035765000004, + 48.430788365, + 48.42946902, + 48.453299900000005, + 48.45538149, + 48.45618716, + 48.4577314, + 48.46598067, + 48.4658264, + 48.45677752, + 48.462906950000004, + 48.46729328, + 48.46589337, + 48.465991745, + 48.470138035000005, + 48.462138464999995, + 48.46326563, + 48.463131020000006, + 48.460205395, + 48.47360217, + 48.4714864, + 48.468303829999996, + 48.439124515, + 48.45390757, + 48.4559398, + 48.45649196, + 48.458517555, + 48.448026635000005, + 48.45325575, + 48.444776329999996, + 48.459793665, + 48.462448165, + 48.464316139999994, + 48.465935615, + 48.436465235, + 48.384082955, + 48.34906194, + 48.420544335, + 48.395958955, + 48.45754019, + 48.446855275000004 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "2W transformers", + "line": { + "color": "rgb(0, 128, 0)", + "width": 5 + }, + "mode": "lines", + "name": "2W transformers", + "showlegend": true, + "text": "Index: 114
Name: HV/MV Transformer 0
V_n HV: 110.0 kV
V_n LV: 20.0 kV
Tap pos.: -2.0
", + "type": "scatter", + "x": [ + 7.91396063, + 7.91396063, + 7.91396063 + ], + "y": [ + 48.4569382, + 48.4569382, + 48.4569382 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "2W transformers", + "line": { + "color": "rgb(0, 128, 0)", + "width": 5 + }, + "mode": "lines", + "name": "2W transformers", + "showlegend": false, + "text": "Index: 142
Name: HV/MV Transformer 1
V_n HV: 110.0 kV
V_n LV: 20.0 kV
Tap pos.: -3.0
", + "type": "scatter", + "x": [ + 7.76153339, + 7.76153339, + 7.76153339 + ], + "y": [ + 48.43987488, + 48.43987488, + 48.43987488 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "2W transformers", + "marker": { + "color": "rgb(0, 128, 0)", + "size": 1, + "symbol": "circle" + }, + "mode": "markers", + "name": "edge_center", + "showlegend": false, + "text": [ + "Index: 114
Name: HV/MV Transformer 0
V_n HV: 110.0 kV
V_n LV: 20.0 kV
Tap pos.: -2.0
", + "Index: 142
Name: HV/MV Transformer 1
V_n HV: 110.0 kV
V_n LV: 20.0 kV
Tap pos.: -3.0
" + ], + "type": "scatter", + "x": [ + 7.91396063, + 7.76153339 + ], + "y": [ + 48.4569382, + 48.43987488 + ] + }, + { + "hoverinfo": "text", + "marker": { + "color": "rgb(255, 255, 0)", + "size": 20, + "symbol": "square" + }, + "mode": "markers", + "name": "external grid", + "text": [ + "Index: 0
Name: External Grid 0
V_m: 1.0 p.u.
V_a: 0.0 °
", + "Index: 1
Name: External Grid 1
V_m: 1.0 p.u.
V_a: 0.0 °
" + ], + "type": "scatter", + "x": [ + 7.91396063, + 7.76153339 + ], + "y": [ + 48.4569382, + 48.43987488 + ] + }, + { + "hoverinfo": "text", + "marker": { + "color": "rgb(0, 0, 255)", + "size": 10, + "symbol": "circle" + }, + "mode": "markers", + "name": "buses", + "text": [ + "Index: 0
Name: Bus 0
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.122 MW
", + "Index: 1
Name: Bus 1
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.092 MW
", + "Index: 2
Name: Bus 2
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.076 MW
", + "Index: 3
Name: Bus 3
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.076 MW
", + "Index: 4
Name: Bus 4
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.092 MW
", + "Index: 5
Name: Bus 5
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.076 MW
", + "Index: 6
Name: Bus 6
V_n: 20.0 kV
Load: 0.500 MW
", + "Index: 7
Name: Bus 7
V_n: 20.0 kV
", + "Index: 8
Name: Bus 8
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.137 MW
", + "Index: 29
Name: Bus 9
V_n: 20.0 kV
Static generation: 0.400 MW
", + "Index: 30
Name: Bus 10
V_n: 20.0 kV
", + "Index: 31
Name: Bus 11
V_n: 20.0 kV
Static generation: 0.800 MW
", + "Index: 32
Name: Bus 12
V_n: 20.0 kV
Static generation: 0.200 MW
", + "Index: 33
Name: Bus 13
V_n: 20.0 kV
Static generation: 0.856 MW
", + "Index: 34
Name: Bus 14
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 35
Name: Bus 15
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.155 MW
", + "Index: 36
Name: Bus 16
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 37
Name: Bus 17
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 38
Name: Bus 18
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.049 MW
", + "Index: 39
Name: Bus 19
V_n: 20.0 kV
", + "Index: 40
Name: Bus 20
V_n: 20.0 kV
Static generation: 0.160 MW
", + "Index: 41
Name: Bus 21
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.140 MW
", + "Index: 42
Name: Bus 22
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.191 MW
", + "Index: 43
Name: Bus 23
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 44
Name: Bus 24
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.295 MW
", + "Index: 45
Name: Bus 25
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.093 MW
", + "Index: 46
Name: Bus 26
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.309 MW
", + "Index: 47
Name: Bus 27
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.140 MW
", + "Index: 48
Name: Bus 28
V_n: 20.0 kV
Load: 0.500 MW
", + "Index: 49
Name: Bus 29
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.093 MW
", + "Index: 50
Name: Bus 30
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 51
Name: Bus 31
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.191 MW
", + "Index: 52
Name: Bus 32
V_n: 20.0 kV
Load: 0.500 MW
", + "Index: 53
Name: Bus 33
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 54
Name: Bus 34
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.137 MW
", + "Index: 55
Name: Bus 35
V_n: 20.0 kV
Load: 0.500 MW
", + "Index: 56
Name: Bus 36
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.295 MW
", + "Index: 57
Name: Bus 37
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.191 MW
", + "Index: 58
Name: Bus 38
V_n: 110.0 kV
", + "Index: 64
Name: Bus 39
V_n: 20.0 kV
", + "Index: 65
Name: Bus 40
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.295 MW
", + "Index: 71
Name: Bus 41
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.155 MW
", + "Index: 72
Name: Bus 42
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 73
Name: Bus 43
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 74
Name: Bus 44
V_n: 20.0 kV
", + "Index: 75
Name: Bus 45
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 76
Name: Bus 46
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 77
Name: Bus 47
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.155 MW
", + "Index: 78
Name: Bus 48
V_n: 20.0 kV
", + "Index: 79
Name: Bus 49
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.191 MW
", + "Index: 80
Name: Bus 50
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 81
Name: Bus 51
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 82
Name: Bus 52
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 83
Name: Bus 53
V_n: 20.0 kV
Static generation: 0.315 MW
", + "Index: 84
Name: Bus 54
V_n: 20.0 kV
Static generation: 0.500 MW
", + "Index: 85
Name: Bus 55
V_n: 20.0 kV
Static generation: 0.300 MW
", + "Index: 86
Name: Bus 56
V_n: 20.0 kV
", + "Index: 94
Name: Bus 57
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.108 MW
", + "Index: 95
Name: Bus 58
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.108 MW
", + "Index: 98
Name: Bus 59
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.069 MW
", + "Index: 100
Name: Bus 60
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.108 MW
", + "Index: 101
Name: Bus 61
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.191 MW
", + "Index: 102
Name: Bus 62
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.118 MW
", + "Index: 103
Name: Bus 63
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 104
Name: Bus 64
V_n: 20.0 kV
", + "Index: 106
Name: Bus 65
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.295 MW
", + "Index: 107
Name: Bus 66
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.049 MW
", + "Index: 108
Name: Bus 67
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.191 MW
", + "Index: 109
Name: Bus 68
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.049 MW
", + "Index: 110
Name: Bus 69
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.309 MW
", + "Index: 111
Name: Bus 70
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 116
Name: Bus 71
V_n: 20.0 kV
", + "Index: 117
Name: Bus 72
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.140 MW
", + "Index: 118
Name: Bus 73
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 119
Name: Bus 74
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 120
Name: Bus 75
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.093 MW
", + "Index: 126
Name: Bus 76
V_n: 20.0 kV
", + "Index: 129
Name: Bus 77
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.095 MW
", + "Index: 131
Name: Bus 78
V_n: 20.0 kV
", + "Index: 132
Name: Bus 79
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.069 MW
", + "Index: 133
Name: Bus 80
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.076 MW
", + "Index: 134
Name: Bus 81
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.069 MW
", + "Index: 136
Name: Bus 82
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.191 MW
", + "Index: 137
Name: Bus 83
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.069 MW
", + "Index: 138
Name: Bus 84
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 140
Name: Bus 85
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.122 MW
", + "Index: 141
Name: Bus 86
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.076 MW
", + "Index: 142
Name: Bus 87
V_n: 20.0 kV
", + "Index: 143
Name: Bus 88
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.155 MW
", + "Index: 144
Name: Bus 89
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.076 MW
", + "Index: 145
Name: Bus 90
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 146
Name: Bus 91
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.233 MW
", + "Index: 147
Name: Bus 92
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.137 MW
", + "Index: 148
Name: Bus 93
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.093 MW
", + "Index: 149
Name: Bus 94
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.076 MW
", + "Index: 150
Name: Bus 95
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.093 MW
", + "Index: 153
Name: Bus 96
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 155
Name: Bus 97
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 157
Name: Bus 98
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 159
Name: Bus 99
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 161
Name: Bus 100
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.069 MW
", + "Index: 162
Name: Bus 101
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.108 MW
", + "Index: 167
Name: Bus 102
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.109 MW
", + "Index: 168
Name: Bus 103
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.109 MW
", + "Index: 169
Name: Bus 104
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.109 MW
", + "Index: 170
Name: Bus 105
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.118 MW
", + "Index: 171
Name: Bus 106
V_n: 20.0 kV
", + "Index: 172
Name: Bus 107
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.214 MW
", + "Index: 173
Name: Bus 108
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.069 MW
", + "Index: 174
Name: Bus 109
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.118 MW
", + "Index: 176
Name: Bus 110
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 178
Name: Bus 111
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 181
Name: Bus 112
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 184
Name: Bus 113
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 186
Name: Bus 114
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 188
Name: Bus 115
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 189
Name: Bus 116
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.309 MW
", + "Index: 190
Name: Bus 117
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.309 MW
", + "Index: 192
Name: Bus 118
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.309 MW
", + "Index: 194
Name: Bus 119
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.039 MW
", + "Index: 195
Name: Bus 120
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.039 MW
", + "Index: 196
Name: Bus 121
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.309 MW
", + "Index: 197
Name: Bus 122
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 198
Name: Bus 123
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 199
Name: Bus 124
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 200
Name: Bus 125
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 201
Name: Bus 126
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.108 MW
", + "Index: 205
Name: Bus 127
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.118 MW
", + "Index: 207
Name: Bus 128
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.118 MW
", + "Index: 210
Name: Bus 129
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.069 MW
", + "Index: 213
Name: Bus 130
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.108 MW
", + "Index: 215
Name: Bus 131
V_n: 20.0 kV
Load: 0.500 MW
", + "Index: 216
Name: Bus 132
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.069 MW
", + "Index: 219
Name: Bus 133
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.100 MW
", + "Index: 221
Name: Bus 134
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.060 MW
", + "Index: 223
Name: Bus 135
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.150 MW
", + "Index: 224
Name: Bus 136
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.150 MW
", + "Index: 227
Name: Bus 137
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.100 MW
", + "Index: 229
Name: Bus 138
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.060 MW
", + "Index: 231
Name: Bus 139
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.060 MW
", + "Index: 235
Name: Bus 140
V_n: 20.0 kV
Load: 0.500 MW
", + "Index: 236
Name: Bus 141
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.160 MW
", + "Index: 237
Name: Bus 142
V_n: 20.0 kV
Static generation: 0.150 MW
", + "Index: 238
Name: Bus 143
V_n: 20.0 kV
", + "Index: 239
Name: Bus 144
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.090 MW
", + "Index: 240
Name: Bus 145
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.122 MW
", + "Index: 241
Name: Bus 146
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.076 MW
", + "Index: 242
Name: Bus 147
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.122 MW
", + "Index: 243
Name: Bus 148
V_n: 20.0 kV
", + "Index: 244
Name: Bus 149
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.137 MW
", + "Index: 245
Name: Bus 150
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.198 MW
", + "Index: 246
Name: Bus 151
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 247
Name: Bus 152
V_n: 20.0 kV
", + "Index: 248
Name: Bus 153
V_n: 20.0 kV
Static generation: 0.295 MW
", + "Index: 253
Name: Bus 154
V_n: 20.0 kV
Static generation: 0.355 MW
", + "Index: 269
Name: Bus 155
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.309 MW
", + "Index: 271
Name: Bus 156
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.109 MW
", + "Index: 273
Name: Bus 157
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.095 MW
", + "Index: 275
Name: Bus 158
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 281
Name: Bus 159
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 285
Name: Bus 160
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 286
Name: Bus 161
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.054 MW
", + "Index: 287
Name: Bus 162
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 288
Name: Bus 163
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 289
Name: Bus 164
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.191 MW
", + "Index: 290
Name: Bus 165
V_n: 20.0 kV
Load: 0.630 MW
Static generation: 0.198 MW
", + "Index: 298
Name: Bus 166
V_n: 20.0 kV
Static generation: 0.300 MW
", + "Index: 301
Name: Bus 167
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.054 MW
", + "Index: 303
Name: Bus 168
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 304
Name: Bus 169
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 305
Name: Bus 170
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 312
Name: Bus 171
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 313
Name: Bus 172
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 314
Name: Bus 173
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 315
Name: Bus 174
V_n: 20.0 kV
Load: 0.400 MW
Static generation: 0.068 MW
", + "Index: 316
Name: Bus 175
V_n: 20.0 kV
Load: 0.250 MW
Static generation: 0.041 MW
", + "Index: 317
Name: Bus 176
V_n: 20.0 kV
", + "Index: 318
Name: Bus 177
V_n: 110.0 kV
", + "Index: 319
Name: Bus 178
V_n: 20.0 kV
" + ], + "type": "scatter", + "x": [ + 7.76522567, + 7.77880954, + 7.77919577, + 7.77520469, + 7.76606485, + 7.76739671, + 7.75852885, + 7.76574338, + 7.76946387, + 7.78665902, + 7.79905951, + 7.81661465, + 7.82727201, + 7.84538508, + 7.83405306, + 7.87771765, + 7.81386611, + 7.87688954, + 7.93125749, + 7.91396063, + 7.87852914, + 7.87625272, + 7.79891555, + 7.88824433, + 7.79950378, + 7.87260819, + 7.81894107, + 7.87520041, + 7.78600346, + 7.87750063, + 7.78701848, + 7.80501543, + 7.87725311, + 7.8112944, + 7.81137059, + 7.88088449, + 7.79674274, + 7.80752534, + 7.91396063, + 7.81800216, + 7.81599919, + 7.8959583, + 7.80673406, + 7.88830762, + 7.80946722, + 7.80466609, + 7.83584018, + 7.87808299, + 7.88212331, + 7.82260854, + 7.88546159, + 7.88517175, + 7.82449029, + 7.88777356, + 7.79305306, + 7.75706106, + 7.90923486, + 7.90933438, + 7.90828612, + 7.91778262, + 7.91291408, + 7.81078043, + 7.91393385, + 7.81753115, + 7.81812248, + 7.81719863, + 7.91378673, + 7.81213406, + 7.89617371, + 7.81434875, + 7.83782535, + 7.83571694, + 7.88344689, + 7.88023004, + 7.88076055, + 7.88432076, + 7.77637635, + 7.75752837, + 7.81440186, + 7.90115002, + 7.81265378, + 7.90485626, + 7.84164512, + 7.90759827, + 7.83058701, + 7.81375095, + 7.82229551, + 7.90302727, + 7.87666867, + 7.81192866, + 7.87990442, + 7.8758161, + 7.81550141, + 7.88260716, + 7.81702049, + 7.87715626, + 7.75962984, + 7.75792025, + 7.75339537, + 7.76124005, + 7.93773871, + 7.93829196, + 7.75838011, + 7.75598728, + 7.78157825, + 7.83613832, + 7.77111891, + 7.81672668, + 7.93283542, + 7.93628735, + 7.77603927, + 7.77032597, + 7.75269597, + 7.75385941, + 7.74517856, + 7.75456734, + 7.81593058, + 7.8195498, + 7.80022205, + 7.90263858, + 7.88846812, + 7.81328782, + 7.76147904, + 7.75618441, + 7.75569132, + 7.75578023, + 7.89989694, + 7.89437546, + 7.89638358, + 7.8884535, + 7.90130341, + 7.88603646, + 7.8893131, + 7.90172272, + 7.89506435, + 7.90205872, + 7.90849453, + 7.90443237, + 7.90591947, + 7.9083386, + 7.90432357, + 7.90130481, + 7.84263834, + 7.89470796, + 7.89613192, + 7.76659341, + 7.7617981, + 7.77150376, + 7.77599056, + 7.77518896, + 7.77526664, + 7.75527749, + 7.86375666, + 7.80577582, + 7.76396796, + 7.81091684, + 7.75387463, + 7.7490983, + 7.78580944, + 7.7512397, + 7.77541442, + 7.77756681, + 7.77569591, + 7.77670102, + 7.8060588, + 7.76944157, + 7.78873191, + 7.76226955, + 7.7644815, + 7.76147033, + 7.75612888, + 7.74776625, + 7.74426069, + 7.75780411, + 7.74990855, + 7.75312516, + 7.8790417, + 7.76153339, + 7.76153339 + ], + "y": [ + 48.41091584, + 48.40987065, + 48.41203811, + 48.40610337, + 48.41242444, + 48.42373656, + 48.43345788, + 48.41987793, + 48.40913221, + 48.43625936, + 48.43759153, + 48.44215898, + 48.46648941, + 48.42296455, + 48.46664643, + 48.45713891, + 48.45138395, + 48.44659344, + 48.3980943, + 48.4569382, + 48.40552685, + 48.44299303, + 48.46284776, + 48.45231709, + 48.46870231, + 48.45794147, + 48.46584214, + 48.46134835, + 48.4585943, + 48.46363931, + 48.46045784, + 48.46296614, + 48.46499297, + 48.466147, + 48.38535297, + 48.46626641, + 48.46588038, + 48.46584322, + 48.4569382, + 48.45286283, + 48.45200045, + 48.45361129, + 48.43815272, + 48.45601148, + 48.43003727, + 48.43251067, + 48.47002488, + 48.45477828, + 48.45630215, + 48.45231101, + 48.4585246, + 48.45375027, + 48.45412175, + 48.43320652, + 48.46585031, + 48.36812437, + 48.4561543, + 48.42618849, + 48.42418937, + 48.42400659, + 48.42255372, + 48.42871494, + 48.42590629, + 48.43283117, + 48.42974203, + 48.4281192, + 48.42043868, + 48.43725275, + 48.41100311, + 48.43503248, + 48.3990085, + 48.40021513, + 48.46135559, + 48.46216934, + 48.45936355, + 48.46419159, + 48.43784809, + 48.3495233, + 48.38940745, + 48.42189392, + 48.39170089, + 48.42317236, + 48.40414471, + 48.42108182, + 48.39753675, + 48.38919055, + 48.38807328, + 48.42556885, + 48.47484423, + 48.38755402, + 48.46999586, + 48.47236011, + 48.38288709, + 48.46302758, + 48.38429982, + 48.46740551, + 48.34441059, + 48.32845845, + 48.32979038, + 48.32945775, + 48.39605777, + 48.39911502, + 48.34834127, + 48.38553948, + 48.36717009, + 48.40115573, + 48.40810119, + 48.38789817, + 48.40163972, + 48.40091852, + 48.35663792, + 48.35975183, + 48.34979926, + 48.34577289, + 48.34907864, + 48.35261081, + 48.46357677, + 48.45155157, + 48.46640752, + 48.4152001, + 48.41556286, + 48.43169169, + 48.35023997, + 48.34562366, + 48.34750562, + 48.34396313, + 48.41219988, + 48.41295983, + 48.41424248, + 48.41021512, + 48.41377255, + 48.41239165, + 48.41271759, + 48.40051529, + 48.40360449, + 48.39529931, + 48.39773842, + 48.39390775, + 48.39621703, + 48.3949975, + 48.42675459, + 48.39757192, + 48.39811545, + 48.40549008, + 48.3979666, + 48.41394964, + 48.41348695, + 48.41297284, + 48.41297438, + 48.4106662, + 48.40850593, + 48.38787178, + 48.40965934, + 48.39606378, + 48.39721686, + 48.43490897, + 48.38198803, + 48.38651875, + 48.36588431, + 48.38363665, + 48.3593875, + 48.36261088, + 48.36446775, + 48.36133205, + 48.4359944, + 48.41414363, + 48.4038838, + 48.38622196, + 48.38725384, + 48.38881634, + 48.38127334, + 48.38593046, + 48.38747799, + 48.38819904, + 48.38849988, + 48.33198343, + 48.41572609, + 48.43987488, + 48.43987488 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": true, + "text": "Line 5", + "type": "scatter", + "x": [ + 7.78157825, + 7.78241594, + 7.78757125, + 7.80563692, + 7.81137059 + ], + "y": [ + 48.36717009, + 48.367589, + 48.37274714, + 48.38813105, + 48.38535297 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 12", + "type": "scatter", + "x": [ + 7.75598728, + 7.75493654, + 7.75226417, + 7.7490983 + ], + "y": [ + 48.38553948, + 48.38515046, + 48.38528923, + 48.38651875 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 14", + "type": "scatter", + "x": [ + 7.75598728, + 7.75586722, + 7.7565325, + 7.75620547, + 7.75669407, + 7.75527749 + ], + "y": [ + 48.38553948, + 48.38577499, + 48.38647184, + 48.38674957, + 48.38741536, + 48.38787178 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 17", + "type": "scatter", + "x": [ + 7.76396796, + 7.77111891 + ], + "y": [ + 48.39721686, + 48.40810119 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 18", + "type": "scatter", + "x": [ + 7.77111891, + 7.76965603, + 7.76946387 + ], + "y": [ + 48.40810119, + 48.40897041, + 48.40913221 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 46", + "type": "scatter", + "x": [ + 7.77526664, + 7.77749826, + 7.78873191 + ], + "y": [ + 48.40850593, + 48.40904312, + 48.4038838 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 47", + "type": "scatter", + "x": [ + 7.78873191, + 7.80573238, + 7.80577582 + ], + "y": [ + 48.4038838, + 48.39607413, + 48.39606378 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 50", + "type": "scatter", + "x": [ + 7.81265378, + 7.81224648, + 7.81164404, + 7.80993094, + 7.80608736, + 7.80577582 + ], + "y": [ + 48.39170089, + 48.39208848, + 48.3932605, + 48.3941038, + 48.39455474, + 48.39606378 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 55", + "type": "scatter", + "x": [ + 7.76396796, + 7.76495858, + 7.76629087, + 7.7659259, + 7.76376563, + 7.76270002, + 7.76157105, + 7.76147033 + ], + "y": [ + 48.39721686, + 48.39694908, + 48.39667324, + 48.39581646, + 48.39356873, + 48.39201827, + 48.38930585, + 48.38881634 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 116", + "type": "scatter", + "x": [ + 7.75780411, + 7.75917051, + 7.76226955 + ], + "y": [ + 48.38819904, + 48.38730208, + 48.38622196 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 117", + "type": "scatter", + "x": [ + 7.76226955, + 7.76344386, + 7.7644815 + ], + "y": [ + 48.38622196, + 48.38591421, + 48.38725384 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 118", + "type": "scatter", + "x": [ + 7.7644815, + 7.76213031, + 7.76276391, + 7.76147033 + ], + "y": [ + 48.38725384, + 48.38794756, + 48.38852075, + 48.38881634 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 120", + "type": "scatter", + "x": [ + 7.75780411, + 7.75698634, + 7.75516628, + 7.75527749 + ], + "y": [ + 48.38819904, + 48.38848522, + 48.38875367, + 48.38787178 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 121", + "type": "scatter", + "x": [ + 7.75387463, + 7.75423985, + 7.75489924, + 7.75490995, + 7.75612888 + ], + "y": [ + 48.38198803, + 48.38177685, + 48.38177159, + 48.38124748, + 48.38127334 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 123", + "type": "scatter", + "x": [ + 7.75387463, + 7.75041699, + 7.7512397 + ], + "y": [ + 48.38198803, + 48.38256216, + 48.38363665 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 125", + "type": "scatter", + "x": [ + 7.75387463, + 7.75471952, + 7.75706106 + ], + "y": [ + 48.38198803, + 48.37590247, + 48.36812437 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 134", + "type": "scatter", + "x": [ + 7.77526664, + 7.77576803, + 7.77520469 + ], + "y": [ + 48.40850593, + 48.40798048, + 48.40610337 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "line": { + "color": "rgb(255, 0, 0)", + "dash": "solid", + "width": 1 + }, + "mode": "lines", + "name": "intersecting lines", + "showlegend": false, + "text": "Line 137", + "type": "scatter", + "x": [ + 7.77526664, + 7.77583421, + 7.77591886, + 7.77518896 + ], + "y": [ + 48.40850593, + 48.40895948, + 48.41002749, + 48.4106662 + ] + }, + { + "hoverinfo": "text", + "legendgroup": "lines", + "marker": { + "color": "rgb(255, 0, 0)", + "size": 1, + "symbol": "circle" + }, + "mode": "markers", + "name": "edge_center", + "showlegend": false, + "type": "scatter", + "x": [ + 7.796604085, + 7.753600355, + 7.75644977, + 7.767543435, + 7.77038747, + 7.783115085, + 7.797232145000001, + 7.80800915, + 7.7648457650000005, + 7.76072003, + 7.76396268, + 7.763305904999999, + 7.75607631, + 7.754904595, + 7.75214581, + 7.75589029, + 7.77548636, + 7.775876535 + ], + "y": [ + 48.380439095, + 48.385219844999995, + 48.387082465, + 48.402659025, + 48.408535799999996, + 48.40646346, + 48.399978965, + 48.39432927, + 48.394692594999995, + 48.38676202, + 48.386584025000005, + 48.3876007, + 48.388619445, + 48.381509535, + 48.382275095, + 48.37201342, + 48.407041925, + 48.409493485 + ] + }, + { + "fill": "toself", + "fillcolor": "rgba(255, 165, 0, 0.2)", + "line": { + "color": "orange", + "width": 0.5 + }, + "mode": "lines", + "name": "radius", + "type": "scatter", + "x": [ + 7.810765587871878, + 7.8106677617774745, + 7.810284889412847, + 7.809620689511773, + 7.80868158796014, + 7.807476654787392, + 7.806017515821131, + 7.804318239893644, + 7.802395202718501, + 7.800266928772189, + 7.797953912719253, + 7.795478422107161, + 7.792864283228295, + 7.790136652199385, + 7.787321773442097, + 7.784446727862211, + 7.78153917311691, + 7.778627078430524, + 7.775738456467715, + 7.772901094799059, + 7.7701422894976515, + 7.767488583386114, + 7.764965511411912, + 7.7625973555647985, + 7.760406911664869, + 7.758415270242596, + 7.756641613605075, + 7.75510303103586, + 7.753814353910397, + 7.752788012326605, + 7.752033914651572, + 7.7515593511730385, + 7.751368922818724, + 7.751464495671189, + 7.751845181761232, + 7.75250734637233, + 7.753444641833605, + 7.754648067522638, + 7.756106055430859, + 7.757804581108197, + 7.759727297693585, + 7.761855693032329, + 7.764169267795486, + 7.766645732964237, + 7.769261224793018, + 7.771990535169408, + 7.774807355131724, + 7.777684529170111, + 7.7805943178263135, + 7.783508666021535, + 7.786399474482693, + 7.7892388716052094, + 7.791999483085885, + 7.79465469668231, + 7.7971789195054155, + 7.799547825328778, + 7.801738589500652, + 7.8037301091720055, + 7.805503206703785, + 7.807040814287756, + 7.8083281385541365, + 7.809352801055552, + 7.8101049578160415, + 7.810577393136094, + 7.810765587871878 + ], + "y": [ + 48.39008484020486, + 48.38814455055748, + 48.386219988990426, + 48.38432968754383, + 48.38249184632931, + 48.38072415842748, + 48.37904363973767, + 48.3774664654094, + 48.376007814419005, + 48.374681723774806, + 48.373500953739835, + 48.37247686535485, + 48.371619311425, + 48.370936542005055, + 48.37043512527882, + 48.370119884582365, + 48.3699938521666, + 48.37005824013639, + 48.370312428841046, + 48.3707539728248, + 48.371378624281405, + 48.37218037379024, + 48.373151507948194, + 48.374282683351645, + 48.375563016227744, + 48.37698018686498, + 48.37852055785215, + 48.38016930500276, + 48.38191055971954, + 48.38372756144352, + 48.385602818733815, + 48.387518277439725, + 48.38945549435633, + 48.39139581469939, + 48.393320551696085, + 48.39521116656465, + 48.397049447148795, + 48.39881768348325, + 48.400498838916576, + 48.402076713547686, + 48.40353610145513, + 48.40486293732134, + 48.406044432253736, + 48.407069197362, + 48.40792735384139, + 48.4086106284868, + 48.40911243370284, + 48.40942793122652, + 48.40955407893841, + 48.409489660302576, + 48.40923529614661, + 48.408793438665676, + 48.40816834770915, + 48.40736604958169, + 48.40639427876253, + 48.405262403113596, + 48.403981333308515, + 48.40256341736819, + 48.40102232133412, + 48.399372897243836, + 48.397631040110234, + 48.395813533363906, + 48.39393788774903, + 48.392022172339374, + 48.39008484020486 + ] + }, + { + "marker": { + "color": "orange", + "size": 10 + }, + "mode": "markers", + "name": "reference point", + "type": "scatter", + "x": [ + 7.7810670720720925 + ], + "y": [ + 48.3897749246129 + ] + }, + { + "hoverinfo": "text", + "marker": { + "color": "rgb(255, 0, 0)", + "size": 5, + "symbol": "circle" + }, + "mode": "markers", + "name": "buses within radius", + "text": [ + "Bus 3", + "Bus 103", + "Bus 106", + "Bus 150", + "Bus 151", + "Bus 153", + "Bus 154", + "Bus 156", + "Bus 166", + "Bus 167", + "Bus 168", + "Bus 169", + "Bus 170", + "Bus 173" + ], + "type": "scatter", + "x": [ + 7.77520469, + 7.75598728, + 7.77111891, + 7.77526664, + 7.75527749, + 7.80577582, + 7.76396796, + 7.75387463, + 7.78873191, + 7.76226955, + 7.7644815, + 7.76147033, + 7.75612888, + 7.75780411 + ], + "y": [ + 48.40610337, + 48.38553948, + 48.40810119, + 48.40850593, + 48.38787178, + 48.39606378, + 48.39721686, + 48.38198803, + 48.4038838, + 48.38622196, + 48.38725384, + 48.38881634, + 48.38127334, + 48.38819904 + ] + } + ], + "layout": { + "autosize": true, + "height": 528.1109895327797, + "hovermode": "closest", + "legend": { + "itemsizing": "constant" + }, + "margin": { + "b": 5, + "l": 5, + "r": 5, + "t": 5 + }, + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "font": { + "size": 16 + } + }, + "width": 700, + "xaxis": { + "autorange": true, + "range": [ + 7.730316686096759, + 7.952235963903241 + ], + "showgrid": false, + "showticklabels": false, + "type": "linear", + "zeroline": false + }, + "yaxis": { + "autorange": true, + "range": [ + 48.318085289702445, + 48.48521739029756 + ], + "showgrid": false, + "showticklabels": false, + "type": "linear", + "zeroline": false + } + } + }, + "image/png": "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", + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x, y = circle_polygon.exterior.xy\n", + "polygon_trace = go.Scatter(\n", + " x=list(x), \n", + " y=list(y), \n", + " fill=\"toself\",\n", + " mode=\"lines\",\n", + " line=dict(color=\"orange\", width=0.5),\n", + " fillcolor=\"rgba(255, 165, 0, 0.2)\",\n", + " name=\"radius\"\n", + ")\n", + "\n", + "point_trace = go.Scatter(\n", + " x=[circle_polygon.centroid.x], \n", + " y=[circle_polygon.centroid.y], \n", + " mode=\"markers\",\n", + " marker=dict(color=\"orange\", size=10),\n", + " name=\"reference point\"\n", + ")\n", + "lt = create_line_trace(net, lines=lines_intersect, color=\"red\", trace_name='intersecting lines')\n", + "bt = create_bus_trace(net, buses=buses_within, color=\"red\", trace_name=\"buses within radius\")\n", + "fig = simple_plotly(net, auto_open=False, additional_traces=lt)\n", + "fig.add_trace(polygon_trace)\n", + "fig.add_trace(point_trace)\n", + "fig.add_trace(bt[0])" + ] + }, + { + "cell_type": "markdown", + "id": "7e7917d4-c696-459c-aa59-df11bb2ffac7", + "metadata": {}, + "source": [ + "It is also possible to load the GeoJSON, or view the geo column as GeoSeries or a Series with shapely objects." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3bfe1268-5b40-44c6-b194-aced9ee9d07d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 {\"coordinates\": [7.765225672614365, 48.41091584192147], \"type\": \"Point\"}\n", + "1 {\"coordinates\": [7.778809539550178, 48.40987064550492], \"type\": \"Point\"}\n", + "2 {\"coordinates\": [7.779195765893586, 48.4120381144602], \"type\": \"Point\"}\n", + "3 {\"coordinates\": [7.775204689771646, 48.40610336882051], \"type\": \"Point\"}\n", + "4 {\"coordinates\": [7.76606484746611, 48.4124244421834], \"type\": \"Point\"}\n", + "Name: geo, dtype: object" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "net.bus.geo.head() # entries are strings" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f4f49408-5d9b-4083-893b-e857c999befd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 {'type': 'Point', 'coordinates': [7.76522567, 48.41091584]}\n", + "1 {'type': 'Point', 'coordinates': [7.77880954, 48.40987065]}\n", + "2 {'type': 'Point', 'coordinates': [7.77919577, 48.41203811]}\n", + "3 {'type': 'Point', 'coordinates': [7.77520469, 48.40610337]}\n", + "4 {'type': 'Point', 'coordinates': [7.76606485, 48.41242444]}\n", + "Name: geo, dtype: object" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "net.bus.geo.geojson.as_geo_obj.head() # entries are dicts" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "dfbcd7ff-aa5d-4d8f-aaaf-3909dd4fd654", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 POINT (7.76523 48.41092)\n", + "1 POINT (7.77881 48.40987)\n", + "2 POINT (7.7792 48.41204)\n", + "3 POINT (7.7752 48.4061)\n", + "4 POINT (7.76606 48.41242)\n", + "dtype: geometry" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "net.bus.geo.geojson.as_geoseries.head() # GeoSeries " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fc2c3aba-9897-47a3-b076-0fca36134ad2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 POINT (7.765225672614365 48.41091584192147)\n", + "1 POINT (7.778809539550178 48.40987064550492)\n", + "2 POINT (7.779195765893586 48.4120381144602)\n", + "3 POINT (7.775204689771646 48.40610336882051)\n", + "4 POINT (7.76606484746611 48.4124244421834)\n", + "Name: geo, dtype: object" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "net.bus.geo.geojson.as_shapely_obj.head() # pandas Series with shapely objects" + ] + }, + { + "cell_type": "markdown", + "id": "9240803c-ce51-4086-af42-a79192c8fcd7", + "metadata": {}, + "source": [ + "It is also possible to get the coordinates or the geometry type from the GeoJSON. \n", + "**Note that it is not recommended to use ._coords in applications, because the projection definition is lost!**" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "be589243-c803-48e5-a6ee-ab1140ca350e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 [7.76522567, 48.41091584]\n", + "1 [7.77880954, 48.40987065]\n", + "2 [7.77919577, 48.41203811]\n", + "3 [7.77520469, 48.40610337]\n", + "4 [7.76606485, 48.41242444]\n", + "Name: geo, dtype: object" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "net.bus.geo.geojson._coords.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "749ec31d-cde0-4d21-b2d9-fe5dd424e5a5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 Point\n", + "1 Point\n", + "2 Point\n", + "3 Point\n", + "4 Point\n", + "Name: geo, dtype: object" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "net.bus.geo.geojson.type.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00ac6187-dc91-4360-b1f5-edbbb8f2710f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From ca5c2184b1c37e8c96d92ef84af66968301ab911 Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Mon, 18 Nov 2024 11:39:34 +0100 Subject: [PATCH 064/135] removed unused import --- pandapower/auxiliary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/auxiliary.py b/pandapower/auxiliary.py index 7cb495733..393b7c044 100644 --- a/pandapower/auxiliary.py +++ b/pandapower/auxiliary.py @@ -61,7 +61,7 @@ import logging try: from geopandas import GeoSeries - from shapely import from_geojson, Point + from shapely import from_geojson geopandas_available = True except ImportError: geopandas_available = False From 15d89ce1970e7222e1f2ddf04bfdd80fca8e43ca Mon Sep 17 00:00:00 2001 From: hkoertge Date: Mon, 18 Nov 2024 11:48:15 +0100 Subject: [PATCH 065/135] added gfortran --- .github/workflows/github_test_action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index ed0414a99..39d8da471 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -318,6 +318,7 @@ jobs: apt update; apt upgrade -y; apt install build-essential -y; + apt install gfortran -y; python -m pip install --upgrade pip; python -m pip install .[docs]; " From 76315e53cf5d672f6fa623032043a1e2c0f9cb95 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Mon, 18 Nov 2024 13:11:49 +0100 Subject: [PATCH 066/135] added cmake and pkg-config --- .github/workflows/github_test_action.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index 39d8da471..682cf4653 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -314,13 +314,8 @@ jobs: - name: Check docs for Python ${{ matrix.python-version }} uses: e2nIEE/sphinx-action@master with: - pre-build-command: " - apt update; - apt upgrade -y; - apt install build-essential -y; - apt install gfortran -y; + pre-build-command: "apt update && apt upgrade -y && apt install -y build-essential gfortran cmake pkg-config; python -m pip install --upgrade pip; - python -m pip install .[docs]; - " + python -m pip install .[docs];" build-command: "sphinx-build -b html . _build -W" docs-folder: "doc/" From 1c8ed55995275f4897f78cb96fcd72a5523aee4f Mon Sep 17 00:00:00 2001 From: hkoertge Date: Mon, 18 Nov 2024 13:17:23 +0100 Subject: [PATCH 067/135] added scypi dependency openblas --- .github/workflows/github_test_action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index 682cf4653..a816bec26 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -314,7 +314,7 @@ jobs: - name: Check docs for Python ${{ matrix.python-version }} uses: e2nIEE/sphinx-action@master with: - pre-build-command: "apt update && apt upgrade -y && apt install -y build-essential gfortran cmake pkg-config; + pre-build-command: "apt update && apt upgrade -y && apt install -y build-essential gfortran cmake pkg-config libopenblas-dev; python -m pip install --upgrade pip; python -m pip install .[docs];" build-command: "sphinx-build -b html . _build -W" From 0205705dbe1205357596563c0472b0b344e27976 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Tue, 19 Nov 2024 09:39:52 +0100 Subject: [PATCH 068/135] fixes #2335 again. --- pandapower/convert_format.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandapower/convert_format.py b/pandapower/convert_format.py index 94e2f0962..1b252d357 100644 --- a/pandapower/convert_format.py +++ b/pandapower/convert_format.py @@ -46,6 +46,7 @@ def convert_format(net, elements_to_deserialize=None): _add_missing_columns(net, elements_to_deserialize) _create_seperate_cost_tables(net, elements_to_deserialize) if Version(str(net.format_version)) < Version("3.0.0"): + _convert_geo_data(net, elements_to_deserialize) _convert_group_element_index(net) _convert_trafo_controller_parameter_names(net) if Version(str(net.format_version)) < Version("2.4.0"): From acb0a4233d28d320b92f791b49961d99a91cad1b Mon Sep 17 00:00:00 2001 From: hkoertge Date: Tue, 19 Nov 2024 09:44:12 +0100 Subject: [PATCH 069/135] fixed missing import --- pandapower/convert_format.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandapower/convert_format.py b/pandapower/convert_format.py index 1b252d357..817c81e77 100644 --- a/pandapower/convert_format.py +++ b/pandapower/convert_format.py @@ -11,6 +11,7 @@ from pandapower._version import __version__, __format_version__ from pandapower.create import create_empty_network, create_poly_cost +from pandapower.plotting import geo from pandapower.results import reset_results from pandapower.control import TrafoController From 02984641ef5fc338b7241c2965524f59d2697ae5 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Tue, 19 Nov 2024 10:13:47 +0100 Subject: [PATCH 070/135] fixes #2335 --- pandapower/convert_format.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pandapower/convert_format.py b/pandapower/convert_format.py index 94e2f0962..e448a2b04 100644 --- a/pandapower/convert_format.py +++ b/pandapower/convert_format.py @@ -13,6 +13,7 @@ from pandapower.create import create_empty_network, create_poly_cost from pandapower.results import reset_results from pandapower.control import TrafoController +import pandapower.plotting.geo as geo try: import pandaplan.core.pplog as logging @@ -22,14 +23,6 @@ logger = logging.getLogger(__name__) -def _compare_version( - net_version, - compare_version: Union[str, int], - compare: Callable[[Version, Version], bool] = lambda x, y: x < y -) -> bool: - return compare(Version(str(net_version)), Version(str(compare_version))) - - def convert_format(net, elements_to_deserialize=None): """ Converts old nets to new format to ensure consistency. The converted net is returned. @@ -46,6 +39,7 @@ def convert_format(net, elements_to_deserialize=None): _add_missing_columns(net, elements_to_deserialize) _create_seperate_cost_tables(net, elements_to_deserialize) if Version(str(net.format_version)) < Version("3.0.0"): + _convert_geo_data(net, elements_to_deserialize) _convert_group_element_index(net) _convert_trafo_controller_parameter_names(net) if Version(str(net.format_version)) < Version("2.4.0"): @@ -56,7 +50,7 @@ def convert_format(net, elements_to_deserialize=None): _convert_to_mw(net) _update_trafo_parameter_names(net, elements_to_deserialize) reset_results(net) - if isinstance(net.format_version, float) and net.format_version < 1.6: + if Version(str(net.format_version)) < Version("1.6"): set_data_type_of_columns_to_default(net) _convert_objects(net, elements_to_deserialize) _update_characteristics(net, elements_to_deserialize) @@ -74,7 +68,7 @@ def _convert_geo_data(net, elements_to_deserialize=None): or (_check_elements_to_deserialize('line_geodata', elements_to_deserialize) and _check_elements_to_deserialize('line', elements_to_deserialize))): if hasattr(net, 'bus_geodata') or hasattr(net, 'line_geodata'): - if _compare_version(net.format_version, "1.6"): + if Version(str(net.format_version)) < Version("1.6"): net.bus_geodata = pd.DataFrame.from_dict(net.bus_geodata) net.line_geodata = pd.DataFrame.from_dict(net.line_geodata) geo.convert_geodata_to_geojson(net) From f01cd049b63c810a5af7476818f9a978b681bc4e Mon Sep 17 00:00:00 2001 From: Daniel Lohmeier <31214121+dlohmeier@users.noreply.github.com> Date: Wed, 20 Nov 2024 08:05:59 +0100 Subject: [PATCH 071/135] Update io_utils.py --- pandapower/io_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/io_utils.py b/pandapower/io_utils.py index 884af89cc..a24771a93 100644 --- a/pandapower/io_utils.py +++ b/pandapower/io_utils.py @@ -735,7 +735,7 @@ def pp_hook(d, deserialize_pandas=True, empty_dict_like_object=None, # obj = {"_init": d, "_state": dict()} # backwards compatibility obj = {key: val for key, val in d.items() if key not in ['_module', '_class']} fs = registry_class(obj, d, pp_hook, ignore_unknown_objects) - + fs.class_name = d.pop('_class', '') fs.module_name = d.pop('_module', '') fs.empty_dict_like_object = empty_dict_like_object From c285660131ce88a945abf5646f6cff9db0b1db05 Mon Sep 17 00:00:00 2001 From: dlohmeier Date: Wed, 20 Nov 2024 08:56:09 +0100 Subject: [PATCH 072/135] added test for special exception in io_utils --- pandapower/test/api/test_file_io.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pandapower/test/api/test_file_io.py b/pandapower/test/api/test_file_io.py index d77cc7d60..a71a3aab0 100644 --- a/pandapower/test/api/test_file_io.py +++ b/pandapower/test/api/test_file_io.py @@ -229,7 +229,7 @@ def test_json_encoding_decoding(): net = networks.mv_oberrhein() net.tuple = (1, "4") net.mg = topology.create_nxgraph(net) - s = set(['1', 4]) + s = {'1', 4} t = tuple(['2', 3]) f = frozenset(['12', 3]) a = np.array([1., 2.]) @@ -603,7 +603,7 @@ def test_multi_index(): def test_ignore_unknown_objects(): net = pp.networks.create_kerber_dorfnetz() - ctrl = control.ContinuousTapControl(net, 0, 1.02) + control.ContinuousTapControl(net, 0, 1.02) json_str = pp.to_json(net) net2 = pp.from_json_string(json_str, ignore_unknown_objects=False) @@ -617,12 +617,19 @@ def test_ignore_unknown_objects(): "pandapower.control.controller.trafo.ContinuousTapControl2") with pytest.raises(ModuleNotFoundError): pp.from_json_string(json_str2, ignore_unknown_objects=False) + json_str3 = json_str.replace("\"ContinuousTapControl", "\"ContinuousTapControl2") + with pytest.raises(AttributeError): + pp.from_json_string(json_str3, ignore_unknown_objects=False) net3 = pp.from_json_string(json_str2, ignore_unknown_objects=True) assert isinstance(net3.controller.object.at[0], dict) + net4 = pp.from_json_string(json_str3, ignore_unknown_objects=True) + assert isinstance(net4.controller.object.at[0], dict) # make sure that the loaded net equals the original net except for the controller - net.controller.object.at[0] = net3.controller.object.at[0] + net3.controller.object.at[0] = net.controller.object.at[0] + net4.controller.object.at[0] = net.controller.object.at[0] assert_net_equal(net, net3) + assert_net_equal(net, net4) if __name__ == "__main__": From a3fc9800adf619de72d1dbd506d99ca960bfa539 Mon Sep 17 00:00:00 2001 From: Stefan Lanz Date: Wed, 20 Nov 2024 14:38:10 +0100 Subject: [PATCH 073/135] don't change pandas options when importing pandapower --- pandapower/converter/cim/cim2pp/build_pp_net.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandapower/converter/cim/cim2pp/build_pp_net.py b/pandapower/converter/cim/cim2pp/build_pp_net.py index 1872fc5fd..4686fc9b2 100644 --- a/pandapower/converter/cim/cim2pp/build_pp_net.py +++ b/pandapower/converter/cim/cim2pp/build_pp_net.py @@ -18,8 +18,6 @@ logger = logging.getLogger('cim.cim2pp.build_pp_net') -pd.set_option('display.max_columns', 900) -pd.set_option('display.max_rows', 90000) sc = cim_tools.get_pp_net_special_columns_dict() From 4d8e97bea7a9c31c289313ab31f23e6e92dee672 Mon Sep 17 00:00:00 2001 From: JakobKirschner <123633166+JakobKirschner@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:29:40 +0100 Subject: [PATCH 074/135] Feature/add regions to bus (#2425) * add regions to bus df * add regions ids to bus df * Comment to test how to commit into your branch * fix converter tests --------- Co-authored-by: Dominik Hilbrich Co-authored-by: mrifraunhofer <94368111+mrifraunhofer@users.noreply.github.com> Co-authored-by: V3 Co-authored-by: Moritz Franz <102804850+mfranz13@users.noreply.github.com> Co-authored-by: KS_HTK --- CHANGELOG.rst | 2 ++ .../connectivitynodes/connectivityNodesCim16.py | 12 ++++++++++++ pandapower/converter/cim/cim2pp/from_cim.py | 1 + pandapower/converter/cim/cim_tools.py | 7 ++++--- pandapower/test/converter/test_from_cim.py | 2 +- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 693a6ec34..70f34f543 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ Change Log ------------------------------- - [ADDED] pandas series accessor for geo column - [FIXED] Increasing geojson precision as the default precision might cause problems with pandahub +- [ADDED] Add GeographicalRegion and SubGeographicalRegion names and ids to bus df in cim converter +- [CHANGED] Capitalize first letter of columns busbar_id, busbar_name and substation_id in bus df for cim converter [2.14.11] - 2024-07-08 ------------------------------- diff --git a/pandapower/converter/cim/cim2pp/converter_classes/connectivitynodes/connectivityNodesCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/connectivitynodes/connectivityNodesCim16.py index f1de75cf8..34bfd4484 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/connectivitynodes/connectivityNodesCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/connectivitynodes/connectivityNodesCim16.py @@ -248,6 +248,18 @@ def _prepare_connectivity_nodes_cim16(self) -> Tuple[pd.DataFrame, pd.DataFrame] bb = bb.drop_duplicates(subset=['rdfId'], keep='first') connectivity_nodes = pd.merge(connectivity_nodes, bb, how='left', on='rdfId') + if "Substation" in connectivity_nodes.columns: + # add (sub) geographical regions + sgr = self.cimConverter.cim['eq']['SubGeographicalRegion'][['rdfId', 'name', 'Region']] + regions = pd.merge(self.cimConverter.cim['eq']['Substation'], sgr, left_on="Region", right_on="rdfId", + suffixes=["_substation", "_SubGeographicalRegion"]) + regions = pd.merge(self.cimConverter.cim['eq']['GeographicalRegion'], regions, left_on="rdfId", right_on="Region_SubGeographicalRegion") + regions = regions.rename(columns={'name': 'GeographicalRegion_name', 'name_SubGeographicalRegion': 'SubGeographicalRegion_name', + 'rdfId': 'GeographicalRegion_id', 'rdfId_SubGeographicalRegion': 'SubGeographicalRegion_id'}) + regions = regions.drop(columns=['name_substation', 'Region_substation', 'Region_SubGeographicalRegion']) + connectivity_nodes = pd.merge(connectivity_nodes, regions, how='left', left_on='Substation', right_on='rdfId_substation') + connectivity_nodes = connectivity_nodes.drop(columns=["rdfId_substation"]) + connectivity_nodes = connectivity_nodes.rename(columns={'rdfId': sc['o_id'], 'TopologicalNode': sc['ct'], 'nominalVoltage': 'vn_kv', 'name_substation': 'zone'}) connectivity_nodes['in_service'] = True diff --git a/pandapower/converter/cim/cim2pp/from_cim.py b/pandapower/converter/cim/cim2pp/from_cim.py index dfcc0f1d4..c45d00fd7 100644 --- a/pandapower/converter/cim/cim2pp/from_cim.py +++ b/pandapower/converter/cim/cim2pp/from_cim.py @@ -103,6 +103,7 @@ def from_cim(file_list: List[str] = None, encoding: str = 'utf-8', convert_line_ custom_converter_classes: Dict = None, cgmes_version: str = '2.4.15', **kwargs) -> \ pandapower.auxiliary.pandapowerNet: + # Nur zum Testen, kann wieder gelöscht werden """ Convert a CIM net to a pandapower net from XML files. Additional parameters for kwargs: diff --git a/pandapower/converter/cim/cim_tools.py b/pandapower/converter/cim/cim_tools.py index 9668b2387..e04b92988 100644 --- a/pandapower/converter/cim/cim_tools.py +++ b/pandapower/converter/cim/cim_tools.py @@ -24,7 +24,7 @@ def get_pp_net_special_columns_dict() -> Dict[str, str]: 'o_prf': 'origin_profile', 'ct': 'cim_topnode', 'tc': 'tapchanger_class', 'tc_id': 'tapchanger_id', 'pte_id': 'PowerTransformerEnd_id', 'pte_id_hv': 'PowerTransformerEnd_id_hv', 'pte_id_mv': 'PowerTransformerEnd_id_mv', 'pte_id_lv': 'PowerTransformerEnd_id_lv', - 'cnc_id': 'ConnectivityNodeContainer_id', 'sub_id': 'substation_id', 'src': 'source', 'name': 'name', + 'cnc_id': 'ConnectivityNodeContainer_id', 'sub_id': 'Substation_id', 'src': 'source', 'name': 'name', 'desc': 'description', 'a_id': 'analog_id'}) @@ -50,8 +50,9 @@ def extend_pp_net_cim(net: pandapowerNet, override: bool = True) -> pandapowerNe fill_dict: Dict[str, Dict[str, List[str]]] = dict() fill_dict['bus'] = dict() - fill_dict['bus'][np_str_type] = [sc['o_prf'], sc['ct'], sc['cnc_id'], sc['sub_id'], 'description', 'busbar_id', - 'busbar_name'] + fill_dict['bus'][np_str_type] = [sc['o_prf'], sc['ct'], sc['cnc_id'], sc['sub_id'], 'description', 'Busbar_id', + 'Busbar_name', 'GeographicalRegion_id', 'GeographicalRegion_name', + 'SubGeographicalRegion_id', 'SubGeographicalRegion_name'] fill_dict['ext_grid'] = dict() fill_dict['ext_grid'][np_str_type] = [sc['t'], sc['sub'], 'description'] diff --git a/pandapower/test/converter/test_from_cim.py b/pandapower/test/converter/test_from_cim.py index 6930f1d15..48880f8b2 100644 --- a/pandapower/test/converter/test_from_cim.py +++ b/pandapower/test/converter/test_from_cim.py @@ -1104,7 +1104,7 @@ def test_fullgrid_bus(fullgrid_v2): assert 'tp' == element_0['origin_profile'].item() assert '_4c66b132-0977-1e4c-b9bb-d8ce2e912e35' == element_0['cim_topnode'].item() assert math.isnan(element_0['ConnectivityNodeContainer_id'].item()) - assert math.isnan(element_0['substation_id'].item()) + assert math.isnan(element_0['Substation_id'].item()) assert 'BBRUS151; BGENT_51' == element_0['description'].item() element_1 = fullgrid_v2.bus[fullgrid_v2.bus['origin_id'] == '_1098b1c9-dc85-40ce-b65c-39ae02a3afaa'] From 2ce76b5a831b6b7f01361d6ada8905403c55c066 Mon Sep 17 00:00:00 2001 From: bott-j Date: Thu, 21 Nov 2024 10:34:00 +1100 Subject: [PATCH 075/135] Change default value of encoding keyword argument to None for parse_files() and from_cim() #2419 --- pandapower/converter/cim/cim2pp/from_cim.py | 4 ++-- pandapower/converter/cim/cim_classes.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandapower/converter/cim/cim2pp/from_cim.py b/pandapower/converter/cim/cim2pp/from_cim.py index c45d00fd7..d93dd8d21 100644 --- a/pandapower/converter/cim/cim2pp/from_cim.py +++ b/pandapower/converter/cim/cim2pp/from_cim.py @@ -94,7 +94,7 @@ def get_converter_classes(): return converter_classes -def from_cim(file_list: List[str] = None, encoding: str = 'utf-8', convert_line_to_switch: bool = False, +def from_cim(file_list: List[str] = None, encoding: str = None, convert_line_to_switch: bool = False, line_r_limit: float = 0.1, line_x_limit: float = 0.1, repair_cim: Union[str, interfaces.CIMRepair] = None, repair_cim_class: Type[interfaces.CIMRepair] = None, @@ -123,7 +123,7 @@ def from_cim(file_list: List[str] = None, encoding: str = 'utf-8', convert_line_ if there are errors in the conversion. Default: True. :param file_list: The path to the CGMES files as a list. - :param encoding: The encoding from the files. Optional, default: utf-8 + :param encoding: The encoding from the files. Optional, default: None :param convert_line_to_switch: Set this parameter to True to enable line -> switch conversion. All lines with a resistance lower or equal than line_r_limit or a reactance lower or equal than line_x_limit will become a switch. Optional, default: False diff --git a/pandapower/converter/cim/cim_classes.py b/pandapower/converter/cim/cim_classes.py index 80cd72eab..5c6c6ea92 100644 --- a/pandapower/converter/cim/cim_classes.py +++ b/pandapower/converter/cim/cim_classes.py @@ -34,14 +34,14 @@ def __init__(self, cim: Dict[str, Dict[str, pd.DataFrame]] = None, cgmes_version self.file_names: Dict[str, str] = dict() self.report_container = ReportContainer() - def parse_files(self, file_list: List[str] or str = None, encoding: str = 'utf-8', prepare_cim_net: bool = False, + def parse_files(self, file_list: List[str] or str = None, encoding: str = None, prepare_cim_net: bool = False, set_data_types: bool = False) -> CimParser: """ Parse CIM XML files from a storage. :param file_list: The path to the CGMES files as a list. Note: The files need a FullModel to parse the CGMES profile. Optional, default: None. - :param encoding: The encoding from the files. Optional, default: utf-8 + :param encoding: The encoding from the files. Optional, default: None :param prepare_cim_net: Set this parameter to True to prepare the parsed cim data according to the CimConverter. Optional, default: False :param set_data_types: Set this parameter to True to set the cim data types at the parsed data. Optional, From 8e33b3571120f336fb22ed4bf7c693248b11df49 Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Thu, 21 Nov 2024 08:13:52 +0100 Subject: [PATCH 076/135] fix powermodels dependency version --- .install_julia.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.install_julia.sh b/.install_julia.sh index a260cecfc..81ebcd830 100755 --- a/.install_julia.sh +++ b/.install_julia.sh @@ -86,7 +86,7 @@ case $(uname) in curl -L "$BASEURL/linux/$ARCH/$JULIANAME-$SUFFIX.tar.gz" | tar -xz sudo ln -s $PWD/julia-*/bin/julia /usr/local/bin/julia julia -e 'import Pkg; Pkg.add("PyCall");' - julia -e 'import Pkg; Pkg.add("PowerModels"); Pkg.add("Ipopt");' + julia -e 'import Pkg; Pkg.add(name="PowerModels", version="0.19.2"); Pkg.add("Ipopt");' julia -e 'import Pkg; Pkg.add("JSON"); Pkg.add("JuMP"); Pkg.add("Cbc"); Pkg.add("Juniper");' ;; Darwin) From b59303538b7ab29cdc54277638bdccbbe58b9403 Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Thu, 21 Nov 2024 09:01:21 +0100 Subject: [PATCH 077/135] more version fixtures --- .install_julia.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.install_julia.sh b/.install_julia.sh index 81ebcd830..dd5930f24 100755 --- a/.install_julia.sh +++ b/.install_julia.sh @@ -86,8 +86,8 @@ case $(uname) in curl -L "$BASEURL/linux/$ARCH/$JULIANAME-$SUFFIX.tar.gz" | tar -xz sudo ln -s $PWD/julia-*/bin/julia /usr/local/bin/julia julia -e 'import Pkg; Pkg.add("PyCall");' - julia -e 'import Pkg; Pkg.add(name="PowerModels", version="0.19.2"); Pkg.add("Ipopt");' - julia -e 'import Pkg; Pkg.add("JSON"); Pkg.add("JuMP"); Pkg.add("Cbc"); Pkg.add("Juniper");' + julia -e 'import Pkg; Pkg.add(name="PowerModels", version="0.19.2"); Pkg.add(name="Ipopt", version="0.9");' + julia -e 'import Pkg; Pkg.add(name="JSON", version="0.21"); Pkg.add(name="JuMP", version="0.22"); Pkg.add(name="Cbc", version="0.9"); Pkg.add(name="Juniper", version="0.8");' ;; Darwin) if [ -e /usr/local/bin/julia ]; then From 43278a2eed6b52623830193c25a4d62e76bfcf9c Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Thu, 21 Nov 2024 09:49:56 +0100 Subject: [PATCH 078/135] next try --- .install_julia.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.install_julia.sh b/.install_julia.sh index dd5930f24..2f3b351fd 100755 --- a/.install_julia.sh +++ b/.install_julia.sh @@ -86,8 +86,7 @@ case $(uname) in curl -L "$BASEURL/linux/$ARCH/$JULIANAME-$SUFFIX.tar.gz" | tar -xz sudo ln -s $PWD/julia-*/bin/julia /usr/local/bin/julia julia -e 'import Pkg; Pkg.add("PyCall");' - julia -e 'import Pkg; Pkg.add(name="PowerModels", version="0.19.2"); Pkg.add(name="Ipopt", version="0.9");' - julia -e 'import Pkg; Pkg.add(name="JSON", version="0.21"); Pkg.add(name="JuMP", version="0.22"); Pkg.add(name="Cbc", version="0.9"); Pkg.add(name="Juniper", version="0.8");' + julia -e 'import Pkg; Pkg.Registry.update(); Pkg.add("PandaModels"); Pkg.build(); Pkg.resolve();' ;; Darwin) if [ -e /usr/local/bin/julia ]; then From e927c2035b5d1f5c7dff975db1ad347fd6022e96 Mon Sep 17 00:00:00 2001 From: Stefan Lanz Date: Thu, 21 Nov 2024 13:11:02 +0100 Subject: [PATCH 079/135] update changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 70f34f543..23fa8c8b0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,7 @@ Change Log - [FIXED] Increasing geojson precision as the default precision might cause problems with pandahub - [ADDED] Add GeographicalRegion and SubGeographicalRegion names and ids to bus df in cim converter - [CHANGED] Capitalize first letter of columns busbar_id, busbar_name and substation_id in bus df for cim converter +- [FIXED] Do not modify pandas options when importing pandapower [2.14.11] - 2024-07-08 ------------------------------- From e05bdb473cfe4673974bfed4a93a8c4002f5e4ba Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Thu, 21 Nov 2024 16:22:27 +0100 Subject: [PATCH 080/135] - fixed unnecessary key dropping - fixed test_compare_pwl_and_poly - marked test_without_ext_grid as expected to fail --- pandapower/converter/pandamodels/from_pm.py | 5 ----- pandapower/test/opf/test_pandamodels_runpm.py | 6 ++++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pandapower/converter/pandamodels/from_pm.py b/pandapower/converter/pandamodels/from_pm.py index acb22a768..a66e9d25d 100644 --- a/pandapower/converter/pandamodels/from_pm.py +++ b/pandapower/converter/pandamodels/from_pm.py @@ -38,11 +38,6 @@ def read_pm_results_to_net(net, ppc, ppci, result_pm): _extract_results(net, result) else: neti = deepcopy(net) - removed_keys = set(net.keys()) - pp_elements(res_elements=True) - \ - {"_options", "_is_elements", "_pd2ppc_lookups", "res_bus", "res_switch"} | \ - {"measurement"} - for rk in removed_keys: - neti.pop(rk) for tp, ri in result.items(): add_time_series_data_to_net(neti, net.controller, tp) _extract_results(neti, ri) diff --git a/pandapower/test/opf/test_pandamodels_runpm.py b/pandapower/test/opf/test_pandamodels_runpm.py index 4c5e4b8f3..364a288c7 100644 --- a/pandapower/test/opf/test_pandamodels_runpm.py +++ b/pandapower/test/opf/test_pandamodels_runpm.py @@ -208,7 +208,7 @@ def test_compare_pwl_and_poly(net_3w_trafo_opf): pp.create_poly_cost(net, 1, 'gen', cp1_eur_per_mw=2) # pp.runopp(net) - pp.runpm_ac_opf(net, correct_pm_network_data=False) + pp.runpm_ac_opf(net) consistency_checks(net) np.allclose(p_gen, net.res_gen.p_mw.values) @@ -217,7 +217,7 @@ def test_compare_pwl_and_poly(net_3w_trafo_opf): np.allclose(va_bus, net.res_bus.va_degree.values) # pp.rundcopp(net) - pp.runpm_dc_opf(net, correct_pm_network_data=False) + pp.runpm_dc_opf(net) consistency_checks(net, test_q=False) np.allclose(p_gen, net.res_gen.p_mw.values) @@ -286,6 +286,8 @@ def test_pwl(): @pytest.mark.slow @pytest.mark.skipif(not julia_installed, reason="requires julia installation") +@pytest.mark.xfail +# todo: this test will not work like this def test_without_ext_grid(): net = pp.create_empty_network() From 52665ec4d5ea347b0357345a79195cc4692f9028 Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Thu, 21 Nov 2024 16:40:12 +0100 Subject: [PATCH 081/135] added fixes to changelog --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 70f34f543..5dc6c4e5a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,8 @@ Change Log [upcoming release] - 2024-..-.. ------------------------------- +- [FIXED] PandaModels OPF with 'bus_dc' key errors +- [FIXED] julia tests - [FIXED] passing literal json to 'read_json' is deprecated - [FIXED] replacing deprecated in1d with isin - [ADDED] A switch to disable updating the vk and vkr values for trafo3w From 06b616bc4ca3646f95e53313896086745ed20c29 Mon Sep 17 00:00:00 2001 From: David Heck Date: Fri, 22 Nov 2024 09:49:15 +0100 Subject: [PATCH 082/135] fix cim2pp encoding problem (#2457) --- CHANGELOG.rst | 1 + pandapower/converter/cim/cim2pp/from_cim.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 56fa3b80c..049f52637 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,7 @@ Change Log - [ADDED] Add GeographicalRegion and SubGeographicalRegion names and ids to bus df in cim converter - [CHANGED] Capitalize first letter of columns busbar_id, busbar_name and substation_id in bus df for cim converter - [FIXED] Do not modify pandas options when importing pandapower +- [FIXED] cim2pp: set default xml encoding to None to avoid error after changing to lxml [2.14.11] - 2024-07-08 ------------------------------- diff --git a/pandapower/converter/cim/cim2pp/from_cim.py b/pandapower/converter/cim/cim2pp/from_cim.py index c45d00fd7..8b32f9414 100644 --- a/pandapower/converter/cim/cim2pp/from_cim.py +++ b/pandapower/converter/cim/cim2pp/from_cim.py @@ -94,7 +94,7 @@ def get_converter_classes(): return converter_classes -def from_cim(file_list: List[str] = None, encoding: str = 'utf-8', convert_line_to_switch: bool = False, +def from_cim(file_list: List[str] = None, encoding: str = None, convert_line_to_switch: bool = False, line_r_limit: float = 0.1, line_x_limit: float = 0.1, repair_cim: Union[str, interfaces.CIMRepair] = None, repair_cim_class: Type[interfaces.CIMRepair] = None, From a9337fb482d73e322e98f0213b17c59f21307492 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Fri, 22 Nov 2024 13:02:24 +0100 Subject: [PATCH 083/135] added test for issue #2321 closes #2321 --- .../test/toolbox/test_grid_modification.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pandapower/test/toolbox/test_grid_modification.py b/pandapower/test/toolbox/test_grid_modification.py index 717759016..bb19022db 100644 --- a/pandapower/test/toolbox/test_grid_modification.py +++ b/pandapower/test/toolbox/test_grid_modification.py @@ -213,6 +213,22 @@ def test_merge_with_characteristics(): assert merged.trafo.loc[1, "vk_percent_characteristic"] == 1 +def test_merge_nets_with_custom_elements(): + from pandapower.networks.simple_pandapower_test_networks import simple_four_bus_system + + df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + # create two networks + net1 = simple_four_bus_system() + net2 = simple_four_bus_system() + + net2["test"] = df.copy() + res_net1 = pp.merge_nets(net1, net2, validate=False) + res_net2 = pp.merge_nets(net2, net1, validate=False) + assert df.equals(res_net1["test"]) + assert df.equals(res_net2["test"]) + assert pp.nets_equal(res_net1, res_net2) + + def test_select_subnet(): # This network has switches of type 'l' and 't' net = nw.create_cigre_network_mv() From 27447aaf8885a57e6e0cfe86ad8bba993bc712a2 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Mon, 25 Nov 2024 11:42:38 +0100 Subject: [PATCH 084/135] updated for geojson support --- .../powerfactory/pp_import_functions.py | 126 +++++++++--------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index 065cd3364..4dd9c7692 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -3,14 +3,17 @@ import numbers import re from itertools import combinations +from typing import Literal, Optional, Union +import geojson import networkx as nx - import numpy as np +from pandas import DataFrame + import pandapower as pp +from pandapower.results import reset_results from pandapower.auxiliary import ADict import pandapower.control as control -from pandas import DataFrame, Series try: import pandaplan.core.pplog as logging @@ -23,6 +26,12 @@ # make wrapper for GetAttribute def ga(element, attr): return element.GetAttribute(attr) +# Define global variables +line_dict = {} +trafo_dict = {} +switch_dict = {} +bus_dict = {} +grf_map = {} # import network to pandapower: @@ -30,10 +39,9 @@ def from_pf(dict_net, pv_as_slack=True, pf_variable_p_loads='plini', pf_variable flag_graphics='GPS', tap_opt="nntap", export_controller=True, handle_us="Deactivate", max_iter=None, is_unbalanced=False, create_sections=True): global line_dict + global line_dict, trafo_dict, switch_dict, bus_dict, grf_map line_dict = {} - global trafo_dict trafo_dict = {} - global switch_dict switch_dict = {} logger.debug("__name__: %s" % __name__) logger.debug('started from_pf') @@ -45,10 +53,8 @@ def from_pf(dict_net, pv_as_slack=True, pf_variable_p_loads='plini', pf_variable grid_name = dict_net['ElmNet'].loc_name base_sn_mva = dict_net['global_parameters']['base_sn_mva'] net = pp.create_empty_network(grid_name, sn_mva=base_sn_mva) - net['bus_geodata'] = DataFrame(columns=['x', 'y']) - net['line_geodata'] = DataFrame(columns=['coords']) - pp.results.reset_results(net, mode="pf_3ph") + reset_results(net, mode="pf_3ph") if max_iter is not None: pp.set_user_pf_options(net, max_iteration=max_iter) logger.info('creating grid %s' % grid_name) @@ -57,9 +63,7 @@ def from_pf(dict_net, pv_as_slack=True, pf_variable_p_loads='plini', pf_variable logger.debug('creating buses') # create buses: - global bus_dict bus_dict = {} - global grf_map grf_map = dict_net.get('graphics', {}) logger.debug('the graphic mapping is: %s' % grf_map) @@ -375,11 +379,12 @@ def create_bus(net, item, flag_graphics, is_unbalanced): else: x, y = 0, 0 - # only values > 0+-1e-3 are entered into the bus_geodata - if x > 1e-3 or y > 1e-3: - geodata = (x, y) - else: - geodata = None + # Commented out because geojson is set up to do the precision handling + # # only values > 0+-1e-3 are entered into the bus.geo + # if x > 1e-3 or y > 1e-3: + # geodata = (x, y) + # else: + # geodata = None usage = ["b", "m", "n"] params = { @@ -387,7 +392,7 @@ def create_bus(net, item, flag_graphics, is_unbalanced): 'vn_kv': item.uknom, 'in_service': not bool(item.outserv), 'type': usage[item.iUsage], - 'geodata': geodata + 'geodata': geojson.dumps(geojson.Point((x, y))), } system_type = {0: "ac", 1: "dc", 2: "ac/bi"}[item.systype] @@ -625,23 +630,17 @@ def create_connection_switches(net, item, number_switches, et, buses, elements): def get_coords_from_buses(net, from_bus, to_bus, **kwargs): - coords = [] - if from_bus in net.bus_geodata.index: - x1, y1 = net.bus_geodata.loc[from_bus, ['x', 'y']] - has_coords = True - else: - x1, y1 = np.nan, np.nan - has_coords = False + coords: list[tuple[float, float]] = [] + from_geo: Optional[str] = None + to_geo: Optional[str] = None + if from_bus in net.bus.index: + from_geo: str = net.bus.loc[from_bus, ['geo']] - if to_bus in net.bus_geodata.index: - x2, y2 = net.bus_geodata.loc[to_bus, ['x', 'y']] - has_coords = True - else: - x2, y2 = np.nan, np.nan - has_coords = False + if to_bus in net.bus.index: + to_geo: str = net.bus.loc[to_bus, ['geo']] - if has_coords: - coords = [[x1, y1], [x2, y2]] + if from_geo and to_geo: + coords = [geojson.utils.coords(geojson.loads(from_geo)), geojson.utils.coords(geojson.loads(to_geo))] logger.debug('got coords from buses: %s' % coords) else: logger.debug('no coords for line between buses %d and %d' % (from_bus, to_bus)) @@ -656,7 +655,7 @@ def get_coords_from_item(item): c = tuple((x, y) for [y, x] in coords) except ValueError: try: - c = tuple((x, y) for [y, x, z] in coords) + c = tuple((x, y, z) for [y, x, z] in coords) except ValueError: c = [] return c @@ -684,7 +683,6 @@ def get_coords_from_grf_object(item): if len(coords) == 0: coords = [[graphic_object.rCenterX, graphic_object.rCenterY]] * 2 logger.debug('extracted line coords from graphic object: %s' % coords) - # net.line_geodata.loc[lid, 'coords'] = coords else: coords = [] @@ -859,6 +857,11 @@ def get_section_coords(coords, sec_len, start_len, scale_factor): def segment_buses(net, bus1, bus2, num_sections, line_name): # , sec_len, start_len, coords): + """ + splits bus1, bus2 line so that it creates num_sections amount of lines. + Yields start, end for each line segment. + e.g. Yields bus1, a, a, bus2 for num_sections = 2. + """ yield bus1 m = 1 # if coords: @@ -877,8 +880,10 @@ def segment_buses(net, bus1, bus2, num_sections, line_name): # , sec_len, start bus_name = "%s (Muff %u)" % (line_name, m) vn_kv = net.bus.at[bus1, "vn_kv"] zone = net.bus.at[bus1, "zone"] - k = pp.create_bus(net, name=bus_name, type='ls', vn_kv=vn_kv, zone=zone) + bus = pp.create_bus(net, name=bus_name, type='ls', vn_kv=vn_kv, zone=zone) + # TODO: implement coords for segmentation buses. + # Handle coords if line has multiple coords. # if coords: # split_len += sec_len[m - 1] * scale_factor # @@ -888,9 +893,9 @@ def segment_buses(net, bus1, bus2, num_sections, line_name): # , sec_len, start # logger.warning('bus %d has 0 coords, bus1: %d, bus2: %d' % k, bus1, bus2) if "description" in net.bus: - net.bus.at[k, "description"] = u"" - yield k - yield k + net.bus.at[bus, "description"] = "" + yield bus + yield bus m += 1 else: yield bus2 @@ -928,10 +933,10 @@ def create_line_sections(net, item_list, line, bus1, bus2, coords, parallel, is_ scaling_factor = sum(sec_len) / calc_len_coords(coords) sec_coords = get_section_coords(coords, sec_len=item.dline, start_len=item.rellen, scale_factor=scaling_factor) - net.line_geodata.loc[sid, 'coords'] = sec_coords + net.line.loc[sid, 'geo'] = geojson.dumps(geojson.LineString(sec_coords)) # p1 = sec_coords[0] # p2 = sec_coords[-1] - net.bus_geodata.loc[bus2, ['x', 'y']] = sec_coords[-1] + net.bus.loc[bus2, ['geo']] = geojson.dumps(geojson.Point(sec_coords[-1])) except ZeroDivisionError: logger.warning("Could not generate geodata for line !!") @@ -1589,9 +1594,9 @@ def split_line_add_bus_old(net, item, parent): raise RuntimeError('incorrect length for section %s: %.3f' % (sec, sec_len_b)) # get coords - if sid in net.line_geodata.index.values: + if net.line.at[sid, 'geo'].notna(): logger.debug('line has coords') - coords = net.line_geodata.at[sid, 'coords'] + coords = geojson.utils.coords(geojson.loads(net.line.at[sid, 'geo'])) logger.debug('old geodata of line %d: %s' % (sid, coords)) # get coords for 2 split lines @@ -1619,12 +1624,17 @@ def split_line_add_bus_old(net, item, parent): logger.debug('created new bus in net: %s' % net.bus.loc[bus]) # create new line - lid = pp.create_line(net, from_bus=bus, to_bus=net.line.at[sid, 'to_bus'], - length_km=sec_len_b, - std_type=net.line.at[sid, 'std_type'], - name=net.line.at[sid, 'name'], df=net.line.at[sid, 'df']) + lid = pp.create_line( + net, + from_bus=bus, + to_bus=net.line.at[sid, 'to_bus'], + length_km=sec_len_b, + std_type=net.line.at[sid, 'std_type'], + name=net.line.at[sid, 'name'], + df=net.line.at[sid, 'df'], + geodata=coords_b + ) net.line.at[lid, 'section'] = net.line.at[sid, 'section'] - net.line_geodata.loc[lid, 'coords'] = coords_b if not net.line.loc[sid, 'section_idx']: net.line.loc[sid, 'section_idx'] = 0 @@ -1635,7 +1645,7 @@ def split_line_add_bus_old(net, item, parent): net.line.at[sid, 'to_bus'] = bus net.line.at[sid, 'length_km'] = sec_len_a - net.line_geodata.loc[sid, 'coords'] = coords_a + net.line.at[sid, 'geo'] = geojson.dumps(geojson.LineString(coords_a)) logger.debug('changed: %s' % net.line.loc[sid]) else: # no new bus/line are created: take the to_bus @@ -3324,20 +3334,17 @@ def split_line_at_length(net, line, length_pos): if 'max_loading_percent' in net.line.columns: net.line.loc[new_line, 'max_loading_percent'] = net.line.at[line, 'max_loading_percent'] - if 'line_geodata' in net.keys() and line in net.line_geodata.index.values: - coords = net.line_geodata.at[line, 'coords'] + if net.line.loc[line, 'geo'].notna(): + coords = geojson.utils.coords(geojson.loads(net.line.loc[line, 'geo'])) scaling_factor = old_length / calc_len_coords(coords) sec_coords_a = get_section_coords(coords, sec_len=length_pos, start_len=0., scale_factor=scaling_factor) - sec_coords_b = get_section_coords(coords, sec_len=new_length, start_len=length_pos, - scale_factor=scaling_factor) - - net.line_geodata.loc[line, 'coords'] = sec_coords_a - net.line_geodata.loc[new_line, 'coords'] = sec_coords_b + net.line.loc[line, 'geo'] = geojson.dumps(geojson.LineString(sec_coords_a)) + net.line.loc[new_line, 'geo'] = geojson.dumps(geojson.LineString(sec_coords_b)) - net.bus_geodata.loc[bus, ['x', 'y']] = sec_coords_b[0] + net.bus.loc[bus, ['geo']] = geojson.Point(sec_coords_b[0]) return bus @@ -3438,7 +3445,7 @@ def split_line(net, line_idx, pos_at_line, line_item): net.line.at[new_line, 'order'] = net.line.at[line_idx, 'order'] + 1 net.res_line.at[new_line, 'pf_loading'] = net.res_line.at[line_idx, 'pf_loading'] - if line_idx in net.line_geodata.index.values: + if line_idx in net.line.index: logger.debug('setting new coords') set_new_coords(net, new_bus, line_idx, new_line, line_length, pos_at_line) @@ -3503,7 +3510,7 @@ def break_coords_sections(coords, section_length, scale_factor_length): # set up new coordinates for line sections that are split by the new bus of the ElmLodlvp def set_new_coords(net, bus_id, line_idx, new_line_idx, line_length, pos_at_line): - line_coords = net.line_geodata.at[line_idx, 'coords'] + line_coords = net.line.at[line_idx, 'geo'] logger.debug('got coords for line %s' % line_idx) scale_factor_length = get_scale_factor(line_length, line_coords) @@ -3512,11 +3519,10 @@ def set_new_coords(net, bus_id, line_idx, new_line_idx, line_length, pos_at_line logger.debug('calculated new coords: %s, %s ' % (section_coords, new_coords)) - net.line_geodata.at[line_idx, 'coords'] = section_coords - net.line_geodata.at[new_line_idx, 'coords'] = new_coords + net.line.at[line_idx, 'geo'] = geojson.dumps(geojson.LineString(section_coords)) + net.line.at[new_line_idx, 'geo'] = geojson.dumps(geojson.LineString(new_coords)) - net.bus_geodata.at[bus_id, 'x'] = new_coords[0][0] - net.bus_geodata.at[bus_id, 'y'] = new_coords[0][1] + net.bus.at[bus_id, 'geo'] = geojson.dumps(geojson.Point(new_coords[0])) # gather info about ElmLodlvp in a dict From d8486f6ad5381fcb469d06d3ff0e8a0599755068 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Mon, 25 Nov 2024 11:47:52 +0100 Subject: [PATCH 085/135] removed ga wrapper function for better code quality --- .../powerfactory/pp_import_functions.py | 202 +++++++++--------- 1 file changed, 99 insertions(+), 103 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index 4dd9c7692..472f6445a 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -22,10 +22,6 @@ logger = logging.getLogger(__name__) - -# make wrapper for GetAttribute -def ga(element, attr): - return element.GetAttribute(attr) # Define global variables line_dict = {} trafo_dict = {} @@ -346,12 +342,12 @@ def add_additional_attributes(item, net, element, element_id, attr_list=None, at obj = item for a in attr.split('.'): if hasattr(obj, 'HasAttribute') and obj.HasAttribute(a): - obj = ga(obj, a) + obj = obj.GetAttributes(a) if obj is not None and isinstance(obj, str): net[element].loc[element_id, attr_dict[attr]] = obj elif item.HasAttribute(attr): - chr_name = ga(item, attr) + chr_name = item.GetAttributes(attr) if chr_name is not None: if isinstance(chr_name, (str, numbers.Number)): net[element].loc[element_id, attr_dict[attr]] = chr_name @@ -366,13 +362,13 @@ def add_additional_attributes(item, net, element, element_id, attr_list=None, at def create_bus(net, item, flag_graphics, is_unbalanced): # add geo data if flag_graphics == 'GPS': - x = ga(item, 'e:GPSlon') - y = ga(item, 'e:GPSlat') + x = item.GetAttributes('e:GPSlon') + y = item.GetAttributes('e:GPSlat') elif flag_graphics == 'graphic objects': graphic_object = get_graphic_object(item) if graphic_object: - x = ga(graphic_object, 'rCenterX') - y = ga(graphic_object, 'rCenterY') + x = graphic_object.GetAttributes('rCenterX') + y = graphic_object.GetAttributes('rCenterY') # add gr coord data else: x, y = 0, 0 @@ -472,7 +468,7 @@ def get_pf_bus_results(net, item, bid, is_unbalanced, system_type): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(0): - res = ga(item, res_var_pf) + res = item.GetAttributes(res_var_pf) # dc bus voltage can be negative: net[bus_type].at[bid, res_var_pp] = np.abs(res) if "vm_pu" in res_var_pp else res @@ -480,7 +476,7 @@ def get_pf_bus_results(net, item, bid, is_unbalanced, system_type): # # This one deletes all the results :( # # Don't use it # def find_bus_index_in_net(item, net=None): -# foreign_key = int(ga(item, 'for_name')) +# foreign_key = int(item.GetAttributes('for_name')) # return foreign_key @@ -489,32 +485,32 @@ def get_pf_bus_results(net, item, bid, is_unbalanced, system_type): # def find_bus_index_in_net(item, net): # usage = ["b", "m", "n"] # # to be sure that the bus is the correct one -# name = ga(item, 'loc_name') -# bus_type = usage[ga(item, 'iUsage')] +# name = item.GetAttributes('loc_name') +# bus_type = usage[item.GetAttributes('iUsage')] # logger.debug('looking for bus <%s> in net' % name) # # if item.HasAttribute('cpSubstat'): -# substat = ga(item, 'cpSubstat') +# substat = item.GetAttributes('cpSubstat') # if substat is not None: -# descr = ga(substat, 'loc_name') +# descr = substat.GetAttributes('loc_name') # logger.debug('bus <%s> has substat, descr is <%s>' % (name, descr)) # else: # # omg so ugly :( -# descr = ga(item, 'desc') +# descr = item.GetAttributes('desc') # descr = descr[0] if len(descr) > 0 else "" # logger.debug('substat is none, descr of bus <%s> is <%s>' % (name, descr)) # else: -# descr = ga(item, 'desc') +# descr = item.GetAttributes('desc') # descr = descr[0] if len(descr) > 0 else "" # logger.debug('no attribute "substat", descr of bus <%s> is <%s>' % (name, descr)) # # try: -# zone = ga(item, 'Grid') -# zone_name = ga(zone, 'loc_name').split('.ElmNet')[0] +# zone = item.GetAttributes('Grid') +# zone_name = zone.GetAttributes('loc_name').split('.ElmNet')[0] # logger.debug('zone "Grid" found: <%s>' % zone_name) # except: -# zone = ga(item, 'cpGrid') -# zone_name = ga(zone, 'loc_name').split('.ElmNet')[0] +# zone = item.GetAttributes('cpGrid') +# zone_name = zone.GetAttributes('loc_name').split('.ElmNet')[0] # logger.debug('zone "cpGrid" found: <%s>' % zone_name) # # temp_df_a = net.bus[net.bus.zone == zone_name] @@ -568,12 +564,12 @@ def get_connection_nodes(net, item, num_nodes): item, pf_class)) if pf_class == "ElmTr2": - v.append(ga(item, 't:utrn_h')) - v.append(ga(item, 't:utrn_l')) + v.append(item.GetAttributes('t:utrn_h')) + v.append(item.GetAttributes('t:utrn_l')) elif pf_class == "ElmTr3": - v.append(ga(item, 't:utrn3_h')) - v.append(ga(item, 't:utrn3_m')) - v.append(ga(item, 't:utrn3_l')) + v.append(item.GetAttributes('t:utrn3_h')) + v.append(item.GetAttributes('t:utrn3_m')) + v.append(item.GetAttributes('t:utrn3_l')) else: v = [net[table].vn_kv.at[existing_bus] for _ in buses] @@ -907,7 +903,7 @@ def create_line_sections(net, item_list, line, bus1, bus2, coords, parallel, is_ item_list.sort(key=lambda x: x.index) # to ensure they are in correct order if line.HasResults(-1): # -1 for 'c' results (whatever that is...) - line_loading = ga(line, 'c:loading') + line_loading = line.GetAttributes('c:loading') else: line_loading = np.nan @@ -1109,7 +1105,7 @@ def get_pf_line_results(net, item, lid, is_unbalanced, ac): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(-1): # -1 for 'c' results (whatever that is...) - res = ga(item, res_var_pf) + res = item.GetAttributes(res_var_pf) net[line_type].at[lid, res_var_pp] = res @@ -1241,8 +1237,8 @@ def create_ext_net(net, item, pv_as_slack, is_unbalanced): # if item.HasResults(0): # 'm' results... # # sm:r, sm:i don't work... # logger.debug('<%s> has results' % name) - # net['res_' + elm].at[xid, "pf_p"] = ga(item, 'm:P:bus1') - # net['res_' + elm].at[xid, "pf_q"] = ga(item, 'm:Q:bus1') + # net['res_' + elm].at[xid, "pf_p"] = item.GetAttributes('m:P:bus1') + # net['res_' + elm].at[xid, "pf_q"] = item.GetAttributes('m:Q:bus1') # else: # net['res_' + elm].at[xid, "pf_p"] = np.nan # net['res_' + elm].at[xid, "pf_q"] = np.nan @@ -1279,7 +1275,7 @@ def get_pf_ext_grid_results(net, item, xid, is_unbalanced): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(0): - res = ga(item, res_var_pf) + res = item.GetAttributes(res_var_pf) net[ext_grid_type].at[xid, res_var_pp] = res @@ -1369,11 +1365,11 @@ def ask_load_params(item, pf_variable_p_loads, dict_net, variables): if pf_variable_p_loads == 'm:P:bus1' and not item.HasResults(0): raise RuntimeError('load %s does not have results and is ignored' % item.loc_name) if 'p_mw' in variables: - params.p_mw = ga(item, pf_variable_p_loads) * multiplier + params.p_mw = item.GetAttributes(pf_variable_p_loads) * multiplier if 'q_mvar' in variables: - params.q_mvar = ga(item, map_power_var(pf_variable_p_loads, 'q')) * multiplier + params.q_mvar = item.GetAttributes(map_power_var(pf_variable_p_loads, 'q')) * multiplier if 'sn_mva' in variables: - params.sn_mva = ga(item, map_power_var(pf_variable_p_loads, 's')) * multiplier + params.sn_mva = item.GetAttributes(map_power_var(pf_variable_p_loads, 's')) * multiplier kap = -1 if item.pf_recap == 1 else 1 try: @@ -1401,17 +1397,17 @@ def ask_unbalanced_load_params(item, pf_variable_p_loads, dict_net, variables): if pf_variable_p_loads == 'm:P:bus1' and not item.HasResults(0): raise RuntimeError('load %s does not have results and is ignored' % item.loc_name) if 'p_mw' in variables: - params.p_a_mw = ga(item, pf_variable_p_loads + "r") - params.p_b_mw = ga(item, pf_variable_p_loads + "s") - params.p_c_mw = ga(item, pf_variable_p_loads + "t") + params.p_a_mw = item.GetAttributes(pf_variable_p_loads + "r") + params.p_b_mw = item.GetAttributes(pf_variable_p_loads + "s") + params.p_c_mw = item.GetAttributes(pf_variable_p_loads + "t") if 'q_mvar' in variables: - params.q_a_mvar = ga(item, map_power_var(pf_variable_p_loads, 'q') + "r") - params.q_b_mvar = ga(item, map_power_var(pf_variable_p_loads, 'q') + "s") - params.q_c_mvar = ga(item, map_power_var(pf_variable_p_loads, 'q') + "t") + params.q_a_mvar = item.GetAttributes(map_power_var(pf_variable_p_loads, 'q') + "r") + params.q_b_mvar = item.GetAttributes(map_power_var(pf_variable_p_loads, 'q') + "s") + params.q_c_mvar = item.GetAttributes(map_power_var(pf_variable_p_loads, 'q') + "t") if 'sn_mva' in variables: - params.sn_a_mva = ga(item, map_power_var(pf_variable_p_loads, 's') + "r") - params.sn_b_mva = ga(item, map_power_var(pf_variable_p_loads, 's') + "s") - params.sn_c_mva = ga(item, map_power_var(pf_variable_p_loads, 's') + "t") + params.sn_a_mva = item.GetAttributes(map_power_var(pf_variable_p_loads, 's') + "r") + params.sn_b_mva = item.GetAttributes(map_power_var(pf_variable_p_loads, 's') + "s") + params.sn_c_mva = item.GetAttributes(map_power_var(pf_variable_p_loads, 's') + "t") kap = -1 if item.pf_recap == 1 else 1 try: @@ -1666,7 +1662,7 @@ def create_load(net, item, pf_variable_p_loads, dict_net, is_unbalanced): ask = ask_unbalanced_load_params if is_unbalanced else ask_load_params if load_class == 'ElmLodlv': - # if bool(ga(item, 'e:cHasPartLod')): + # if bool(item.GetAttributes('e:cHasPartLod')): # logger.info('ElmLodlv %s has partial loads - skip' % item.loc_name) # part_lods = item.GetContents('*.ElmLodlvp') # logger.debug('%s' % part_lods) @@ -1701,8 +1697,8 @@ def create_load(net, item, pf_variable_p_loads, dict_net, is_unbalanced): i = 0 z = 0 for cc, ee in zip(("aP", "bP", "cP"), ("kpu0", "kpu1", "kpu")): - c = ga(load_type, cc) - e = ga(load_type, ee) + c = load_type.GetAttributes(cc) + e = load_type.GetAttributes(ee) if e == 1: i += 100 * c elif e == 2: @@ -1788,8 +1784,8 @@ def create_load(net, item, pf_variable_p_loads, dict_net, is_unbalanced): # if not is_unbalanced: # if item.HasResults(0): # 'm' results... # logger.debug('<%s> has results' % params.name) - # net["res_load"].at[ld, "pf_p"] = ga(item, 'm:P:bus1') - # net["res_load"].at[ld, "pf_q"] = ga(item, 'm:Q:bus1') + # net["res_load"].at[ld, "pf_p"] = item.GetAttributes('m:P:bus1') + # net["res_load"].at[ld, "pf_q"] = item.GetAttributes('m:Q:bus1') # else: # net["res_load"].at[ld, "pf_p"] = np.nan # net["res_load"].at[ld, "pf_q"] = np.nan @@ -1820,7 +1816,7 @@ def get_pf_load_results(net, item, ld, is_unbalanced): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(0): - res = ga(item, res_var_pf) * get_power_multiplier(item, res_var_pf) + res = item.GetAttributes(res_var_pf) * get_power_multiplier(item, res_var_pf) net[load_type].at[ld, res_var_pp] = res @@ -1830,11 +1826,11 @@ def ask_gen_params(item, pf_variable_p_gen, *vars): if pf_variable_p_gen == 'm:P:bus1' and not item.HasResults(0): raise RuntimeError('generator %s does not have results and is ignored' % item.loc_name) if 'p_mw' in vars: - params.p_mw = ga(item, pf_variable_p_gen) * multiplier + params.p_mw = item.GetAttributes(pf_variable_p_gen) * multiplier if 'q_mvar' in vars: - params.q_mvar = ga(item, map_power_var(pf_variable_p_gen, 'q')) * multiplier + params.q_mvar = item.GetAttributes(map_power_var(pf_variable_p_gen, 'q')) * multiplier if 'sn_mva' in vars: - params.sn_mva = ga(item, map_power_var(pf_variable_p_gen, 'sn')) * multiplier + params.sn_mva = item.GetAttributes(map_power_var(pf_variable_p_gen, 'sn')) * multiplier params.scaling = item.scale0 if pf_variable_p_gen == 'pgini' else 1 # p_mw = p_mw, q_mvar = q_mvar, scaling = scaling @@ -1850,25 +1846,25 @@ def ask_unbalanced_sgen_params(item, pf_variable_p_sgen, *vars): technology = item.phtech if technology in [0, 1]: # (0-1: 3PH) if 'p_mw' in vars: - params.p_a_mw = ga(item, pf_variable_p_sgen) / 3 - params.p_b_mw = ga(item, pf_variable_p_sgen) / 3 - params.p_c_mw = ga(item, pf_variable_p_sgen) / 3 + params.p_a_mw = item.GetAttributes(pf_variable_p_sgen) / 3 + params.p_b_mw = item.GetAttributes(pf_variable_p_sgen) / 3 + params.p_c_mw = item.GetAttributes(pf_variable_p_sgen) / 3 if 'q_mvar' in vars: - params.q_a_mvar = ga(item, map_power_var(pf_variable_p_sgen, 'q')) / 3 - params.q_b_mvar = ga(item, map_power_var(pf_variable_p_sgen, 'q')) / 3 - params.q_c_mvar = ga(item, map_power_var(pf_variable_p_sgen, 'q')) / 3 + params.q_a_mvar = item.GetAttributes(map_power_var(pf_variable_p_sgen, 'q')) / 3 + params.q_b_mvar = item.GetAttributes(map_power_var(pf_variable_p_sgen, 'q')) / 3 + params.q_c_mvar = item.GetAttributes(map_power_var(pf_variable_p_sgen, 'q')) / 3 elif technology in [2, 3, 4]: # (2-4: 1PH) if 'p_mw' in vars: - params.p_a_mw = ga(item, pf_variable_p_sgen) + params.p_a_mw = item.GetAttributes(pf_variable_p_sgen) params.p_b_mw = 0 params.p_c_mw = 0 if 'q_mvar' in vars: - params.q_a_mvar = ga(item, map_power_var(pf_variable_p_sgen, 'q')) + params.q_a_mvar = item.GetAttributes(map_power_var(pf_variable_p_sgen, 'q')) params.q_b_mvar = 0 params.q_c_mvar = 0 if 'sn_mva' in vars: - params.sn_mva = ga(item, map_power_var(pf_variable_p_sgen, 's')) + params.sn_mva = item.GetAttributes(map_power_var(pf_variable_p_sgen, 's')) params.scaling = item.scale0 if pf_variable_p_sgen == 'pgini' else 1 return params @@ -2046,7 +2042,7 @@ def get_pf_sgen_results(net, item, sg, is_unbalanced, element='sgen'): res = np.nan if item.HasResults(0): if res_var_pf is not None: - res = ga(item, res_var_pf) * get_power_multiplier(item, res_var_pf) + res = item.GetAttributes(res_var_pf) * get_power_multiplier(item, res_var_pf) else: res = np.nan net[sgen_type].at[sg, res_var_pp] = res @@ -2084,8 +2080,8 @@ def create_sgen_neg_load(net, item, pf_variable_p_loads, dict_net): if item.HasResults(0): # 'm' results... logger.debug('<%s> has results' % params.name) - net.res_sgen.at[sg, "pf_p"] = -ga(item, 'm:P:bus1') - net.res_sgen.at[sg, "pf_q"] = -ga(item, 'm:Q:bus1') + net.res_sgen.at[sg, "pf_p"] = -item.GetAttributes('m:P:bus1') + net.res_sgen.at[sg, "pf_q"] = -item.GetAttributes('m:Q:bus1') else: net.res_sgen.at[sg, "pf_p"] = np.nan net.res_sgen.at[sg, "pf_q"] = np.nan @@ -2184,8 +2180,8 @@ def create_sgen_sym(net, item, pv_as_slack, pf_variable_p_gen, dict_net, export_ if item.HasResults(0): # 'm' results... logger.debug('<%s> has results' % name) - net['res_' + element].at[sid, "pf_p"] = ga(item, 'm:P:bus1') * multiplier - net['res_' + element].at[sid, "pf_q"] = ga(item, 'm:Q:bus1') * multiplier + net['res_' + element].at[sid, "pf_p"] = item.GetAttributes('m:P:bus1') * multiplier + net['res_' + element].at[sid, "pf_q"] = item.GetAttributes('m:Q:bus1') * multiplier else: net['res_' + element].at[sid, "pf_p"] = np.nan net['res_' + element].at[sid, "pf_q"] = np.nan @@ -2199,10 +2195,10 @@ def create_sgen_asm(net, item, pf_variable_p_gen, dict_net): dict_net['global_parameters']['global_generation_scaling'] multiplier = get_power_multiplier(item, pf_variable_p_gen) - p_res = ga(item, 'pgini') * multiplier - q_res = ga(item, 'qgini') * multiplier + p_res = item.GetAttributes('pgini') * multiplier + q_res = item.GetAttributes('qgini') * multiplier if item.HasResults(0): - q_res = ga(item, 'm:Q:bus1') / global_scaling * multiplier + q_res = item.GetAttributes('m:Q:bus1') / global_scaling * multiplier else: logger.warning('reactive power for asynchronous generator is not exported properly ' '(advanced modelling of asynchronous generators not implemented)') @@ -2237,8 +2233,8 @@ def create_sgen_asm(net, item, pf_variable_p_gen, dict_net): attr_list=["sernum", "chr_name", "cpSite.loc_name"]) if item.HasResults(0): - net.res_sgen.at[sid, 'pf_p'] = ga(item, 'm:P:bus1') * multiplier - net.res_sgen.at[sid, 'pf_q'] = ga(item, 'm:Q:bus1') * multiplier + net.res_sgen.at[sid, 'pf_p'] = item.GetAttributes('m:P:bus1') * multiplier + net.res_sgen.at[sid, 'pf_q'] = item.GetAttributes('m:Q:bus1') * multiplier else: net.res_sgen.at[sid, 'pf_p'] = np.nan net.res_sgen.at[sid, 'pf_q'] = np.nan @@ -2349,11 +2345,11 @@ def create_trafo(net, item, export_controller=True, tap_opt="nntap", is_unbalanc tap_pos = np.nan if pf_type.itapch: if tap_opt == "nntap": - tap_pos = ga(item, "nntap") + tap_pos = item.GetAttributes("nntap") logger.debug("got tap %f from nntap" % tap_pos) elif tap_opt == "c:nntap": - tap_pos = ga(item, "c:nntap") + tap_pos = item.GetAttributes("c:nntap") logger.debug("got tap %f from c:nntap" % tap_pos) else: raise ValueError('could not read current tap position: tap_opt = %s' % tap_opt) @@ -2362,9 +2358,9 @@ def create_trafo(net, item, export_controller=True, tap_opt="nntap", is_unbalanc # In PowerFactory, if the first tap changer is absent, the second is also, even if the check was there if pf_type.itapch and pf_type.itapch2: if tap_opt == "nntap": - tap_pos2 = ga(item, "nntap2") + tap_pos2 = item.GetAttributes("nntap2") elif tap_opt == "c:nntap": - tap_pos2 = ga(item, "c:nntap2") + tap_pos2 = item.GetAttributes("c:nntap2") if std_type is not None: tid = pp.create_transformer(net, hv_bus=bus1, lv_bus=bus2, name=name, @@ -2475,7 +2471,7 @@ def get_pf_trafo_results(net, item, tid, is_unbalanced): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(-1): # -1 for 'c' results (whatever that is...) - res = ga(item, res_var_pf) + res = item.GetAttributes(res_var_pf) net[trafo_type].at[tid, res_var_pp] = res @@ -2544,21 +2540,21 @@ def create_trafo3w(net, item, tap_opt='nntap'): ts = ["h", "m", "l"][side[0]] # figure out current tap position if tap_opt == "nntap": - tap_pos = ga(item, 'n3tap_' + ts) + tap_pos = item.GetAttributes('n3tap_' + ts) logger.debug("got tap %f from n3tap" % tap_pos) elif tap_opt == "c:nntap": - tap_pos = ga(item, "c:n3tap_" + ts) + tap_pos = item.GetAttributes("c:n3tap_" + ts) logger.debug("got tap %f from c:n3tap" % tap_pos) else: raise ValueError('could not read current tap position: tap_opt = %s' % tap_opt) params.update({ 'tap_side': ts + 'v', # hv, mv, lv - 'tap_step_percent': ga(item, 't:du3tp_' + ts), - 'tap_step_degree': ga(item, 't:ph3tr_' + ts), - 'tap_min': ga(item, 't:n3tmn_' + ts), - 'tap_max': ga(item, 't:n3tmx_' + ts), - 'tap_neutral': ga(item, 't:n3tp0_' + ts), + 'tap_step_percent': item.GetAttributes('t:du3tp_' + ts), + 'tap_step_degree': item.GetAttributes('t:ph3tr_' + ts), + 'tap_min': item.GetAttributes('t:n3tmn_' + ts), + 'tap_max': item.GetAttributes('t:n3tmx_' + ts), + 'tap_neutral': item.GetAttributes('t:n3tp0_' + ts), 'tap_pos': tap_pos }) @@ -2572,7 +2568,7 @@ def create_trafo3w(net, item, tap_opt='nntap'): logger.debug('successfully created trafo3w from parameters: %d' % tid) # testen - # net.trafo3w.loc[tid, 'tap_step_degree'] = ga(item, 't:ph3tr_h') + # net.trafo3w.loc[tid, 'tap_step_degree'] = item.GetAttributes('t:ph3tr_h') # adding switches # False if open, True if closed, None if no switch @@ -2587,7 +2583,7 @@ def create_trafo3w(net, item, tap_opt='nntap'): # assign loading from power factory results if item.HasResults(-1): # -1 for 'c' results (whatever that is...) logger.debug('trafo3w <%s> has results' % item.loc_name) - loading = ga(item, 'c:loading') + loading = item.GetAttributes('c:loading') net.res_trafo3w.at[tid, "pf_loading"] = loading else: net.res_trafo3w.at[tid, "pf_loading"] = np.nan @@ -2597,12 +2593,12 @@ def create_trafo3w(net, item, tap_opt='nntap'): if pf_type.itapzdep: x_points = (net.trafo3w.at[tid, "tap_min"], net.trafo3w.at[tid, "tap_neutral"], net.trafo3w.at[tid, "tap_max"]) for side in ("hv", "mv", "lv"): - vk_min = ga(pf_type, f"uktr3mn_{side[0]}") + vk_min = pf_type.GetAttributes(f"uktr3mn_{side[0]}") vk_neutral = net.trafo3w.at[tid, f"vk_{side}_percent"] - vk_max = ga(pf_type, f"uktr3mx_{side[0]}") - vkr_min = ga(pf_type, f"uktrr3mn_{side[0]}") + vk_max = pf_type.GetAttributes(f"uktr3mx_{side[0]}") + vkr_min = pf_type.GetAttributes(f"uktrr3mn_{side[0]}") vkr_neutral = net.trafo3w.at[tid, f"vkr_{side}_percent"] - vkr_max = ga(pf_type, f"uktrr3mx_{side[0]}") + vkr_max = pf_type.GetAttributes(f"uktrr3mx_{side[0]}") # todo zero-sequence parameters (must be implemented in build_branch first) pp.control.create_trafo_characteristics(net, trafotable="trafo3w", trafo_index=tid, variable=f"vk_{side}_percent", x_points=x_points, @@ -2658,7 +2654,7 @@ def create_coup(net, item, is_fuse=False): # # false approach, completely irrelevant # def create_switch(net, item): # switch_types = {"cbk": "CB", "sdc": "LBS", "swt": "LS", "dct": "DS"} -# name = ga(item, 'loc_name') +# name = item.GetAttributes('loc_name') # logger.debug('>> creating switch <%s>' % name) # # pf_bus1 = item.GetNode(0) @@ -2673,8 +2669,8 @@ def create_coup(net, item, is_fuse=False): # bus2 = find_bus_index_in_net(pf_bus2, net) # logger.debug('switch %s connects buses <%d> and <%d>' % (name, bus1, bus2)) # -# switch_is_closed = bool(ga(item, 'on_off')) -# switch_usage = switch_types[ga(item, 'aUsage')] +# switch_is_closed = bool(item.GetAttributes('on_off')) +# switch_usage = switch_types[item.GetAttributes('aUsage')] # # cd = pp.create_switch(net, name=name, bus=bus1, element=bus2, et='b', # closed=switch_is_closed, type=switch_usage) @@ -2780,8 +2776,8 @@ def create_shunt(net, item): attr_list=['cpSite.loc_name'], attr_dict={"cimRdfId": "origin_id"}) if item.HasResults(0): - net.res_shunt.loc[sid, 'pf_p'] = ga(item, 'm:P:bus1') * multiplier - net.res_shunt.loc[sid, 'pf_q'] = ga(item, 'm:Q:bus1') * multiplier + net.res_shunt.loc[sid, 'pf_p'] = item.GetAttributes('m:P:bus1') * multiplier + net.res_shunt.loc[sid, 'pf_q'] = item.GetAttributes('m:Q:bus1') * multiplier else: net.res_shunt.loc[sid, 'pf_p'] = np.nan net.res_shunt.loc[sid, 'pf_q'] = np.nan @@ -2886,8 +2882,8 @@ def create_vac(net, item): params['name'], item.itype)) if item.HasResults(0): # -1 for 'c' results (whatever that is...) - net['res_%s' % elm].at[xid, "pf_p"] = -ga(item, 'm:P:bus1') - net['res_%s' % elm].at[xid, "pf_q"] = -ga(item, 'm:Q:bus1') + net['res_%s' % elm].at[xid, "pf_p"] = -item.GetAttributes('m:P:bus1') + net['res_%s' % elm].at[xid, "pf_q"] = -item.GetAttributes('m:Q:bus1') else: net['res_%s' % elm].at[xid, "pf_p"] = np.nan net['res_%s' % elm].at[xid, "pf_q"] = np.nan @@ -2954,8 +2950,8 @@ def _get_vsc_control_modes(item, mono=True): f" {item.loc_name} not implemented: {c_m}") if item.HasResults(0): - p_set_dc = -ga(item, f"m:P:{dc_bus_str}") - q_set_ac = -ga(item, "m:Q:busac") * scaling + p_set_dc = -item.GetAttributes(f"m:P:{dc_bus_str}") + q_set_ac = -item.GetAttributes("m:Q:busac") * scaling else: p_set_dc = -item.psetp * scaling # does not work - in PowerFactory, the P set-point relates to AC side q_set_ac = -item.qsetp * scaling @@ -3022,7 +3018,7 @@ def create_vscmono(net, item): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(0): - res = ga(item, res_var_pf) + res = item.GetAttributes(res_var_pf) net.res_vsc.at[vid, res_var_pp] = -res @@ -3070,11 +3066,11 @@ def create_vsc(net, item): if item.HasResults(0): for res_var_pp, res_var_pf in result_variables.items(): - res = ga(item, res_var_pf) + res = item.GetAttributes(res_var_pf) net.res_vsc.at[vid_1, res_var_pp] = -res / 2 net.res_vsc.at[vid_2, res_var_pp] = -res / 2 - net.res_vsc.at[vid_1, "pf_p_dc_mw"] = -ga(item, "m:P:busdm") - net.res_vsc.at[vid_2, "pf_p_dc_mw"] = -ga(item, "m:P:busdp") + net.res_vsc.at[vid_1, "pf_p_dc_mw"] = -item.GetAttributes("m:P:busdm") + net.res_vsc.at[vid_2, "pf_p_dc_mw"] = -item.GetAttributes("m:P:busdp") else: net.res_vsc.loc[vid_1, ["pf_p_mw", "pf_q_mvar", "pf_p_dc_mw"]] = np.nan net.res_vsc.loc[vid_2, ["pf_p_mw", "pf_q_mvar", "pf_p_dc_mw"]] = np.nan From 26dbbb02a38a27213780dd6a097f0b1d2ed17b67 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Mon, 25 Nov 2024 11:51:05 +0100 Subject: [PATCH 086/135] fixed formatting issues switched to f-strings instead of % notation added some type hints and docstrings --- .../powerfactory/pp_import_functions.py | 243 +++++++++++------- 1 file changed, 152 insertions(+), 91 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index 472f6445a..6fb6747d2 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -31,10 +31,19 @@ # import network to pandapower: -def from_pf(dict_net, pv_as_slack=True, pf_variable_p_loads='plini', pf_variable_p_gen='pgini', - flag_graphics='GPS', tap_opt="nntap", export_controller=True, handle_us="Deactivate", - max_iter=None, is_unbalanced=False, create_sections=True): - global line_dict +def from_pf( + dict_net, + pv_as_slack=True, + pf_variable_p_loads='plini', + pf_variable_p_gen='pgini', + flag_graphics: Literal["GPS", "no geodata"] = 'GPS', + tap_opt="nntap", + export_controller=True, + handle_us: Literal["Deactivate", "Drop", "Nothing"] = "Deactivate", + max_iter=None, + is_unbalanced=False, + create_sections=True +): global line_dict, trafo_dict, switch_dict, bus_dict, grf_map line_dict = {} trafo_dict = {} @@ -448,12 +457,12 @@ def get_pf_bus_results(net, item, bid, is_unbalanced, system_type): if is_unbalanced: bus_type = "res_bus_3ph" result_variables = { - "pf_vm_a_pu": "m:u:A", - "pf_va_a_degree": "m:phiu:A", - "pf_vm_b_pu": "m:u:B", - "pf_va_b_degree": "m:phiu:B", - "pf_vm_c_pu": "m:u:C", - "pf_va_c_degree": "m:phiu:C", + "pf_vm_a_pu": "m:u:A", + "pf_va_a_degree": "m:phiu:A", + "pf_vm_b_pu": "m:u:B", + "pf_va_b_degree": "m:phiu:B", + "pf_vm_c_pu": "m:u:C", + "pf_va_c_degree": "m:phiu:C", } elif system_type == "ac": bus_type = "res_bus" @@ -751,13 +760,21 @@ def create_line(net, item, flag_graphics, create_sections, is_unbalanced): logger.debug('line <%s> created' % params['name']) -def point_len(p1, p2): +def point_len( + p1: tuple[Union[float, int], Union[float, int]], + p2: tuple[Union[float, int], Union[float, int]]) -> float: + """ + Calculate distance between p1 and p2 + """ x1, y1 = p1 x2, y2 = p2 return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5 -def calc_len_coords(coords): +def calc_len_coords(coords: list[tuple[Union[float, int], Union[float, int]]]) -> float: + """ + Calculate the sum of point distances in list of coords + """ tot_len = 0 for i in range(len(coords) - 1): tot_len += point_len(coords[i], coords[i + 1]) @@ -873,7 +890,7 @@ def segment_buses(net, bus1, bus2, num_sections, line_name): # , sec_len, start # split_len = 0 while m < num_sections: - bus_name = "%s (Muff %u)" % (line_name, m) + bus_name = f"{line_name} (Muff {m})" vn_kv = net.bus.at[bus1, "vn_kv"] zone = net.bus.at[bus1, "zone"] bus = pp.create_bus(net, name=bus_name, type='ls', vn_kv=vn_kv, zone=zone) @@ -1085,15 +1102,15 @@ def get_pf_line_results(net, item, lid, is_unbalanced, ac): if is_unbalanced: line_type = "res_line_3ph" result_variables = { - "pf_i_a_from_ka": "m:I:bus1:A", - "pf_i_a_to_ka": "m:I:bus2:A", - "pf_i_b_from_ka": "m:I:bus1:B", - "pf_i_b_to_ka": "m:I:bus2:B", - "pf_i_c_from_ka": "m:I:bus1:C", - "pf_i_c_to_ka": "m:I:bus2:C", - "pf_i_n_from_ka": "m:I0x3:bus1", - "pf_i_n_to_ka": "m:I0x3:bus2", - "pf_loading_percent": "c:loading", + "pf_i_a_from_ka": "m:I:bus1:A", + "pf_i_a_to_ka": "m:I:bus2:A", + "pf_i_b_from_ka": "m:I:bus1:B", + "pf_i_b_to_ka": "m:I:bus2:B", + "pf_i_c_from_ka": "m:I:bus1:C", + "pf_i_c_to_ka": "m:I:bus2:C", + "pf_i_n_from_ka": "m:I0x3:bus1", + "pf_i_n_to_ka": "m:I0x3:bus2", + "pf_loading_percent": "c:loading", } elif ac: line_type = "res_line" @@ -1133,14 +1150,14 @@ def create_line_type(net, item, cable_in_air=False): type_data = { "r_ohm_per_km": item.rline, "x_ohm_per_km": item.xline, - "c_nf_per_km": item.cline*item.frnom/50 * 1e3, # internal unit for C in PF is uF + "c_nf_per_km": item.cline * item.frnom / 50 * 1e3, # internal unit for C in PF is uF "q_mm2": item.qurs, "max_i_ka": max_i_ka if max_i_ka != 0 else 1e-3, "endtemp_degree": item.rtemp, "type": line_or_cable, "r0_ohm_per_km": item.rline0, "x0_ohm_per_km": item.xline0, - "c0_nf_per_km": item.cline0*item.frnom/50 * 1e3, # internal unit for C in PF is uF + "c0_nf_per_km": item.cline0 * item.frnom / 50 * 1e3, # internal unit for C in PF is uF "alpha": item.alpha } pp.create_std_type(net, type_data, name, "line") @@ -1258,12 +1275,12 @@ def get_pf_ext_grid_results(net, item, xid, is_unbalanced): if is_unbalanced: ext_grid_type = "res_ext_grid_3ph" result_variables = { - "pf_p_a": "m:P:bus1:A", - "pf_q_a": "m:Q:bus1:A", - "pf_p_b": "m:P:bus1:B", - "pf_q_b": "m:Q:bus1:B", - "pf_p_c": "m:P:bus1:C", - "pf_q_c": "m:Q:bus1:C", + "pf_p_a": "m:P:bus1:A", + "pf_q_a": "m:Q:bus1:A", + "pf_p_b": "m:P:bus1:B", + "pf_q_b": "m:Q:bus1:B", + "pf_p_c": "m:P:bus1:C", + "pf_q_c": "m:Q:bus1:C", } else: ext_grid_type = "res_ext_grid" @@ -1894,7 +1911,7 @@ def create_sgen_genstat(net, item, pv_as_slack, pf_variable_p_gen, dict_net, is_ return params.update(ask(item, pf_variable_p_gen, 'p_mw', 'q_mvar', 'sn_mva')) - logger.debug('genstat parameters: ' % params) + logger.debug(f'genstat parameters: {params}') params.in_service = monopolar_in_service(item) @@ -2371,16 +2388,31 @@ def create_trafo(net, item, export_controller=True, tap_opt="nntap", is_unbalanc logger.debug('created trafo at index <%d>' % tid) else: logger.info("Create Trafo 3ph") - tid = pp.create_transformer_from_parameters(net, hv_bus=bus1, lv_bus=bus2, name=name, - tap_pos=tap_pos, - in_service=in_service, parallel=item.ntnum, df=item.ratfac, - sn_mva=pf_type.strn, vn_hv_kv=pf_type.utrn_h, vn_lv_kv=pf_type.utrn_l, - vk_percent=pf_type.uktr, vkr_percent=pf_type.uktrr, - pfe_kw=pf_type.pfe, i0_percent=pf_type.curmg, - vector_group=pf_type.vecgrp[:-1], vk0_percent=pf_type.uk0tr, - vkr0_percent=pf_type.ur0tr, mag0_percent=pf_type.zx0hl_n, - mag0_rx=pf_type.rtox0_n, si0_hv_partial=pf_type.zx0hl_h, - shift_degree=pf_type.nt2ag * 30, tap2_pos=tap_pos2) + tid = pp.create_transformer_from_parameters( + net, + hv_bus=bus1, + lv_bus=bus2, + name=name, + tap_pos=tap_pos, + in_service=in_service, + parallel=item.ntnum, + df=item.ratfac, + sn_mva=pf_type.strn, + vn_hv_kv=pf_type.utrn_h, + vn_lv_kv=pf_type.utrn_l, + vk_percent=pf_type.uktr, + vkr_percent=pf_type.uktrr, + pfe_kw=pf_type.pfe, + i0_percent=pf_type.curmg, + vector_group=pf_type.vecgrp[:-1], + vk0_percent=pf_type.uk0tr, + vkr0_percent=pf_type.ur0tr, + mag0_percent=pf_type.zx0hl_n, + mag0_rx=pf_type.rtox0_n, + si0_hv_partial=pf_type.zx0hl_h, + shift_degree=pf_type.nt2ag * 30, + tap2_pos=tap_pos2 + ) trafo_dict[item] = tid # add value for voltage setpoint @@ -2452,15 +2484,15 @@ def get_pf_trafo_results(net, item, tid, is_unbalanced): if is_unbalanced: trafo_type = "res_trafo_3ph" result_variables = { - "pf_i_a_hv_ka": "m:I:bushv:A", - "pf_i_a_lv_ka": "m:I:buslv:A", - "pf_i_b_hv_ka": "m:I:bushv:B", - "pf_i_b_lv_ka": "m:I:buslv:B", - "pf_i_c_hv_ka": "m:I:bushv:C", - "pf_i_c_lv_ka": "m:I:buslv:C", - "pf_i_n_hv_ka": "m:I0x3:bushv", - "pf_i_n_lv_ka": "m:I0x3:buslv", - "pf_loading_percent": "c:loading", + "pf_i_a_hv_ka": "m:I:bushv:A", + "pf_i_a_lv_ka": "m:I:buslv:A", + "pf_i_b_hv_ka": "m:I:bushv:B", + "pf_i_b_lv_ka": "m:I:buslv:B", + "pf_i_c_hv_ka": "m:I:bushv:C", + "pf_i_c_lv_ka": "m:I:buslv:C", + "pf_i_n_hv_ka": "m:I0x3:bushv", + "pf_i_n_lv_ka": "m:I0x3:buslv", + "pf_loading_percent": "c:loading", } else: trafo_type = "res_trafo" @@ -2912,6 +2944,7 @@ def create_sind(net, item): logger.debug('created series reactor %s as per unit impedance at index %d' % (net.impedance.at[sind, 'name'], sind)) + def create_scap(net, item): # series capacitor is modelled as per-unit impedance, values in Ohm are calculated into values in # per unit at creation @@ -2921,11 +2954,11 @@ def create_scap(net, item): logger.error("Cannot add Scap '%s': not connected" % item.loc_name) return - if (item.gcap==0) or (item.bcap==0): + if (item.gcap == 0) or (item.bcap == 0): logger.info('not creating series capacitor for %s' % item.loc_name) else: - r_ohm = item.gcap/(item.gcap**2 + item.bcap**2) - x_ohm = -item.bcap/(item.gcap**2 + item.bcap**2) + r_ohm = item.gcap / (item.gcap ** 2 + item.bcap ** 2) + x_ohm = -item.bcap / (item.gcap ** 2 + item.bcap ** 2) scap = pp.create_series_reactor_as_impedance(net, from_bus=bus1, to_bus=bus2, r_ohm=r_ohm, x_ohm=x_ohm, sn_mva=item.Sn, name=item.loc_name, @@ -2971,7 +3004,6 @@ def _get_vsc_control_modes(item, mono=True): def create_vscmono(net, item): - (bus, bus_dc), _ = get_connection_nodes(net, item, 2) sn_mva = item.Snom @@ -3006,7 +3038,9 @@ def create_vscmono(net, item): } if params["r_dc_ohm"] == 0: - logger.warning(f"VSCmono element {params['name']} has no DC resistive loss factor - power flow will not converge!") + logger.warning( + f"VSCmono element {params['name']} has no DC resistive loss factor - power flow will not converge!" + ) vid = pp.create_vsc(net, **params) logger.debug(f'created VSC {vid} for vscmono {item.loc_name}') @@ -3076,12 +3110,10 @@ def create_vsc(net, item): net.res_vsc.loc[vid_2, ["pf_p_mw", "pf_q_mvar", "pf_p_dc_mw"]] = np.nan - def create_stactrl(net, item): stactrl_in_service = True if item.outserv: logger.info(f"Station controller {item.loc_name} is out of service") - stactrl_in_service = False return machines = [m for m in item.psym if m is not None] @@ -3237,15 +3269,15 @@ def create_stactrl(net, item): if not has_path and not control_mode == 0 and not item.i_droop: return - if control_mode == 0: #### VOLTAGE CONTROL + if control_mode == 0: # VOLTAGE CONTROL # controlled_node = item.rembar controlled_node = item.cpCtrlNode bus = bus_dict[controlled_node] # controlled node - if item.uset_mode == 0: #### Station controller + if item.uset_mode == 0: # Station controller v_setpoint_pu = item.usetp else: - v_setpoint_pu = controlled_node.vtarget #### Bus target voltage + v_setpoint_pu = controlled_node.vtarget # Bus target voltage if item.i_droop: # Enable Droop bsc = pp.control.BinarySearchControl(net, ctrl_in_service=stactrl_in_service, @@ -3274,28 +3306,49 @@ def create_stactrl(net, item): # q_control_mode = item.qu_char # 0: "Const Q", 1: "Q(V) Characteristic", 2: "Q(P) Characteristic" # q_control_terminal = q_control_cubicle.cterm # terminal of the cubicle if item.qu_char == 0: - pp.control.BinarySearchControl(net, ctrl_in_service=stactrl_in_service, - output_element=gen_element, output_variable="q_mvar", - output_element_index=gen_element_index, - output_element_in_service=gen_element_in_service, - input_element=res_element_table, - output_values_distribution=distribution, damping_factor=0.9, - input_variable=variable, input_element_index=res_element_index, - set_point=item.qsetp, voltage_ctrl=False, tol=1e-6) + pp.control.BinarySearchControl( + net, ctrl_in_service=stactrl_in_service, + output_element=gen_element, + output_variable="q_mvar", + output_element_index=gen_element_index, + output_element_in_service=gen_element_in_service, + input_element=res_element_table, + output_values_distribution=distribution, + damping_factor=0.9, + input_variable=variable, + input_element_index=res_element_index, + set_point=item.qsetp, + voltage_ctrl=False, tol=1e-6 + ) elif item.qu_char == 1: controlled_node = item.refbar bus = bus_dict[controlled_node] # controlled node - bsc = pp.control.BinarySearchControl(net, ctrl_in_service=stactrl_in_service, - output_element=gen_element, output_variable="q_mvar", - output_element_index=gen_element_index, - output_element_in_service=gen_element_in_service, - input_element=res_element_table, - output_values_distribution=distribution, damping_factor=0.9, - input_variable=variable, input_element_index=res_element_index, - set_point=item.qsetp, voltage_ctrl=False, bus_idx=bus, tol=1e-6) - pp.control.DroopControl(net, q_droop_mvar=item.Srated * 100 / item.ddroop, bus_idx=bus, - vm_set_pu=item.udeadbup, vm_set_ub=item.udeadbup, vm_set_lb=item.udeadblow, - controller_idx=bsc.index, voltage_ctrl=False) + bsc = pp.control.BinarySearchControl( + net, ctrl_in_service=stactrl_in_service, + output_element=gen_element, + output_variable="q_mvar", + output_element_index=gen_element_index, + output_element_in_service=gen_element_in_service, + input_element=res_element_table, + output_values_distribution=distribution, + damping_factor=0.9, + input_variable=variable, + input_element_index=res_element_index, + set_point=item.qsetp, + voltage_ctrl=False, + bus_idx=bus, + tol=1e-6 + ) + pp.control.DroopControl( + net, + q_droop_mvar=item.Srated * 100 / item.ddroop, + bus_idx=bus, + vm_set_pu=item.udeadbup, + vm_set_ub=item.udeadbup, + vm_set_lb=item.udeadblow, + controller_idx=bsc.index, + voltage_ctrl=False + ) else: raise NotImplementedError else: @@ -3322,10 +3375,17 @@ def split_line_at_length(net, line, length_pos): std_type = net.line.at[line, 'std_type'] name = net.line.at[line, 'name'] - new_line = pp.create_line(net, from_bus=bus, to_bus=bus2, length_km=new_length, - std_type=std_type, name=name, df=net.line.at[line, 'df'], - parallel=net.line.at[line, 'parallel'], - in_service=net.line.at[line, 'in_service']) + new_line = pp.create_line( + net, + from_bus=bus, + to_bus=bus2, + length_km=new_length, + std_type=std_type, + name=name, + df=net.line.at[line, 'df'], + parallel=net.line.at[line, 'parallel'], + in_service=net.line.at[line, 'in_service'] + ) if 'max_loading_percent' in net.line.columns: net.line.loc[new_line, 'max_loading_percent'] = net.line.at[line, 'max_loading_percent'] @@ -3334,8 +3394,10 @@ def split_line_at_length(net, line, length_pos): coords = geojson.utils.coords(geojson.loads(net.line.loc[line, 'geo'])) scaling_factor = old_length / calc_len_coords(coords) - sec_coords_a = get_section_coords(coords, sec_len=length_pos, start_len=0., - scale_factor=scaling_factor) + sec_coords_a = get_section_coords(coords, sec_len=length_pos, start_len=0., scale_factor=scaling_factor) + sec_coords_b = get_section_coords( + coords, sec_len=new_length, start_len=length_pos, scale_factor=scaling_factor + ) net.line.loc[line, 'geo'] = geojson.dumps(geojson.LineString(sec_coords_a)) net.line.loc[new_line, 'geo'] = geojson.dumps(geojson.LineString(sec_coords_b)) @@ -3349,16 +3411,14 @@ def get_lodlvp_length_pos(line_item, lod_item): sections = line_item.GetContents('*.ElmLnesec') if len(sections) > 0: sections.sort(lambda x: x.index) - sections_start = [s.rellen for s in sections] sections_end = [s.rellen + s.dline for s in sections] else: - sections_start = [0] sections_end = [line_item.dline] loads = line_item.GetContents('*.ElmLodlvp') if len(loads) > 0: loads.sort(lambda x: x.rellen) - loads_start = [l.rellen for l in loads] + loads_start = [load.rellen for load in loads] else: loads_start = [0] @@ -3403,7 +3463,8 @@ def split_line(net, line_idx, pos_at_line, line_item): return bus_j elif (pos_at_line - line_length) > tol: raise ValueError( - 'Position at line is higher than the line length itself! Line length: %.7f, position at line: %.7f (line: \n%s)' % ( + 'Position at line is higher than the line length itself!\ + Line length: %.7f, position at line: %.7f (line: \n%s)' % ( # line_length, pos_at_line, line_item.loc_name)) line_length, pos_at_line, net.line.loc[line_dict[line_item]])) else: @@ -3661,7 +3722,7 @@ def split_all_lines(net, lvp_dict): # val = [(92, 1, 0.025, 0.1), (91, 2, 0.031, 0.2), (90, 2, 0.032, 0.3)] for load_item, pos_at_line, (p, q) in val: logger.debug(load_item) - ## calculate at once and then read from dict - not good approach! don't do it + # calculate at once and then read from dict - not good approach! don't do it # section, pos_at_sec = get_pos_at_sec(net, net_dgs, lvp_dict, line, load_idx) # section = pas[load_idx]['section'] # pos_at_sec = pas[load_idx]['pos'] @@ -3683,7 +3744,7 @@ def split_all_lines(net, lvp_dict): net.res_load.at[new_load, 'pf_p'] = p net.res_load.at[new_load, 'pf_q'] = q else: - # const I not implemented for sgen... + # const I is not implemented for sgen new_load = pp.create_sgen(net, new_bus, name=load_item.loc_name, p_mw=p, q_mvar=q) logger.debug('created sgen %s' % new_load) net.res_sgen.at[new_load, 'pf_p'] = p From ef62aa295f616ce51fe4f8654f0c0196e50663e2 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Mon, 25 Nov 2024 11:52:50 +0100 Subject: [PATCH 087/135] rewrote calculation to remove redundant calls renamed vars to small letters as per python naming convention --- .../powerfactory/pp_import_functions.py | 109 ++++++++---------- 1 file changed, 45 insertions(+), 64 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index 6fb6747d2..e6297797c 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -2717,6 +2717,12 @@ def create_shunt(net, item): logger.error("Cannot add Shunt '%s': not connected" % item.loc_name) return + def calc_p_mw_and_q_mvar(r: float, x: float) -> tuple[float, float]: + if r == 0 and x == 0: + return 0, 0 + divisor: float = (r ** 2 + x ** 2) + return (item.ushnm ** 2 * r) / divisor * multiplier, (item.ushnm ** 2 * x) / divisor * multiplier + multiplier = get_power_multiplier(item, 'Qact') bus, _ = get_connection_nodes(net, item, 1) params = { @@ -2729,83 +2735,58 @@ def create_shunt(net, item): 'max_step': item.ncapx } print(item.loc_name) + r_val: float = .0 + x_val: float = .0 if item.shtype == 0: # Shunt is a R-L-C element - - R = item.rrea - X = -1e6 / item.bcap + item.xrea - if R == 0 and X == 0: #TODO put this into one function - p_mw = 0 - params['q_mvar'] = 0 - else: - p_mw = (item.ushnm ** 2 * R) / (R ** 2 + X ** 2) * multiplier - params['q_mvar'] = (item.ushnm ** 2 * X) / (R ** 2 + X ** 2) * multiplier - sid = pp.create_shunt(net, p_mw=p_mw, **params) + r_val = item.rrea + x_val = -1e6 / item.bcap + item.xrea elif item.shtype == 1: # Shunt is an R-L element - - R = item.rrea - X = item.xrea - if R == 0 and X == 0: #TODO put this into one function - p_mw = 0 - params['q_mvar'] = 0 - else: - p_mw = (item.ushnm ** 2 * R) / (R ** 2 + X ** 2) * multiplier - params['q_mvar'] = (item.ushnm ** 2 * X) / (R ** 2 + X ** 2) * multiplier - sid = pp.create_shunt(net, p_mw=p_mw, **params) + r_val = item.rrea + x_val = item.xrea elif item.shtype == 2: # Shunt is a capacitor bank - B = item.bcap*1e-6 - G = item.gparac*1e-6 - - R = G/(G**2 + B**2) - X = -B/(G**2 + B**2) - if R == 0 and X == 0: #TODO put this into one function - p_mw = 0 - params['q_mvar'] = 0 - else: - p_mw = (item.ushnm ** 2 * R) / (R ** 2 + X ** 2) * multiplier - params['q_mvar'] = (item.ushnm ** 2 * X) / (R ** 2 + X ** 2) * multiplier - sid = pp.create_shunt(net, p_mw=p_mw, **params) + b = item.bcap*1e-6 + g = item.gparac*1e-6 + + r_val = g / (g ** 2 + b ** 2) + x_val = -b / (g ** 2 + b ** 2) elif item.shtype == 3: # Shunt is a R-L-C, Rp element + rp = item.rpara + rs = item.rrea + xl = item.xrea + bc = -item.bcap * 1e-6 - Rp = item.rpara - Rs = item.rrea - Xl = item.xrea - Bc = -item.bcap * 1e-6 - - R = Rp * (Rp * Rs + Rs ** 2 + Xl ** 2) / ((Rp + Rs) ** 2 + Xl ** 2) - X = 1 / Bc + (Xl * Rp ** 2) / ((Rp + Rs) ** 2 + Xl ** 2) - if R == 0 and X == 0: #TODO put this into one function - p_mw = 0 - params['q_mvar'] = 0 - else: - p_mw = (item.ushnm ** 2 * R) / (R ** 2 + X ** 2) * multiplier - params['q_mvar'] = (item.ushnm ** 2 * X) / (R ** 2 + X ** 2) * multiplier - sid = pp.create_shunt(net, p_mw=p_mw, **params) + r_val = rp * (rp * rs + rs ** 2 + xl ** 2) / ((rp + rs) ** 2 + xl ** 2) + x_val = 1 / bc + (xl * rp ** 2) / ((rp + rs) ** 2 + xl ** 2) elif item.shtype == 4: # Shunt is a R-L-C1-C2, Rp element - - Rp = item.rpara - Rs = item.rrea - Xl = item.xrea - B1 = 2 * np.pi * 50 * item.c1 * 1e-6 - B2 = 2 * np.pi * 50 * item.c2 * 1e-6 - - Z = Rp * (Rs + 1j * (Xl - 1 / B1)) / (Rp + Rs + 1j * (Xl - 1 / B1)) - 1j / B2 - R = np.real(Z) - X = np.imag(Z) - if R == 0 and X == 0: #TODO put this into one function - p_mw = 0 - params['q_mvar'] = 0 - else: - p_mw = (item.ushnm ** 2 * R) / (R ** 2 + X ** 2) * multiplier - params['q_mvar'] = (item.ushnm ** 2 * X) / (R ** 2 + X ** 2) * multiplier + rp = item.rpara + rs = item.rrea + xl = item.xrea + b1 = 2 * np.pi * 50 * item.c1 * 1e-6 + b2 = 2 * np.pi * 50 * item.c2 * 1e-6 + + z = rp * (rs + 1j * (xl - 1 / b1)) / (rp + rs + 1j * (xl - 1 / b1)) - 1j / b2 + r_val = np.real(z) + x_val = np.imag(z) + + if 0 <= item.shtype <= 4: + p_mw, params['q_mvar'] = calc_p_mw_and_q_mvar(r_val, x_val) sid = pp.create_shunt(net, p_mw=p_mw, **params) - add_additional_attributes(item, net, element='shunt', element_id=sid, - attr_list=['cpSite.loc_name'], attr_dict={"cimRdfId": "origin_id"}) + add_additional_attributes( + item, + net, + element='shunt', + element_id=sid, + attr_list=['cpSite.loc_name'], + attr_dict={"cimRdfId": "origin_id"} + ) + else: + raise AttributeError(f"Shunt type {item.shtype} not valid: {item}") if item.HasResults(0): net.res_shunt.loc[sid, 'pf_p'] = item.GetAttributes('m:P:bus1') * multiplier From ce45bd044736fad21f204e944813c9a34be53aee Mon Sep 17 00:00:00 2001 From: hkoertge Date: Tue, 26 Nov 2024 11:35:36 +0100 Subject: [PATCH 088/135] fixed us of net.bus_geodata and net.line_geodata reformatted file simplified some functions renamed some capitalized variables --- pandapower/protection/utility_functions.py | 328 ++++++++------------- 1 file changed, 120 insertions(+), 208 deletions(-) diff --git a/pandapower/protection/utility_functions.py b/pandapower/protection/utility_functions.py index 7ae26364e..b1f3ad034 100644 --- a/pandapower/protection/utility_functions.py +++ b/pandapower/protection/utility_functions.py @@ -1,8 +1,9 @@ # This function includes various function used for general functionalities such as plotting, grid search import copy -from typing import overload, List, Tuple +from typing import List, Tuple +from matplotlib.collections import PatchCollection from typing_extensions import deprecated import geojson @@ -16,6 +17,7 @@ import pandapower as pp import pandapower.plotting as plot +from pandapower import pandapowerNet from pandapower.topology.create_graph import create_nxgraph import warnings @@ -122,7 +124,8 @@ def create_sc_bus(net_copy, sc_line_id, sc_fraction): x1, y1 = _get_coords_from_bus_idx(net, aux_line.from_bus)[0] x2, y2 = _get_coords_from_bus_idx(net, aux_line.to_bus)[0] - net.bus.geo.at[max_idx_bus + 1] = geojson.dumps(geojson.Point((sc_fraction * (x2 - x1) + x1, sc_fraction * (y2 - y1) + y1)), sort_keys=True) + net.bus.geo.at[max_idx_bus + 1] = geojson.dumps( + geojson.Point((sc_fraction * (x2 - x1) + x1, sc_fraction * (y2 - y1) + y1)), sort_keys=True) return net @@ -145,21 +148,9 @@ def calc_faults_at_full_line(net, line, location_step_size=0.01, start_location= return fault_currents -def get_line_idx(net, switch_id): - # get the line id from swithc id - line_idx = net.switch.element.at[switch_id] - return line_idx - - -def get_bus_idx(net, switch_id): - # get the bus id using switch if - bus_idx = net.switch.bus.at[switch_id] - return bus_idx - - def get_opposite_side_bus_from_switch(net, switch_id): # get the frm and to bus of switch - line_idx = get_line_idx(net, switch_id) + line_idx = net.switch.element.at[switch_id] is_from_bus = get_from_bus_info_switch(net, switch_id) if is_from_bus: @@ -184,33 +175,15 @@ def get_opposite_side_bus_from_bus_line(net, bus_idx, line_idx): def get_from_bus_info_switch(net, switch_id): # get the from bus of given switch id - bus_idx = get_bus_idx(net, switch_id) - line_idx = get_line_idx(net, switch_id) - - for line in net.line.index: # can be written better - if line == line_idx: - if bus_idx == net.line.from_bus.at[line_idx]: # asks if switch is at from_bus - is_from_bus = True - # sc_fraction = 0.95 - else: # else it is at to_bus - is_from_bus = False - # sc_fraction = 0.05 + bus_idx = net.switch.bus.at[switch_id] + line_idx = net.switch.element.at[switch_id] - return is_from_bus + return bus_idx == net.line.from_bus.at[line_idx] def get_from_bus_info_bus_line(net, bus_idx, line_idx): # get bus nfo of given line - for line in net.line.index: # can be written better - if line == line_idx: - if bus_idx == net.line.from_bus.at[line_idx]: # asks if switch is at from_bus - is_from_bus = True - # sc_fraction = 0.95 - else: # else it is at to_bus - is_from_bus = False - # sc_fraction = 0.05 - - return is_from_bus + return bus_idx == net.line.from_bus.at[line_idx] def get_line_impedance(net, line_idx): @@ -218,23 +191,19 @@ def get_line_impedance(net, line_idx): line_length = net.line.length_km.at[line_idx] line_r_per_km = net.line.r_ohm_per_km.at[line_idx] line_x_per_km = net.line.x_ohm_per_km.at[line_idx] - Z_line = complex(line_r_per_km * line_length, line_x_per_km * line_length) # Z = R + jX - return Z_line + z_line = complex(line_r_per_km * line_length, line_x_per_km * line_length) # Z = R + jX + return z_line -def get_lowest_impedance_line(net, lines): +def get_lowest_impedance_line(net: pandapowerNet, lines): # get the low impedenceline - i = 0 + min_imp_line = None + min_impedance = float('inf') for line in lines: impedance = abs(get_line_impedance(net, line)) - if i == 0: - min_imp_line = line + if impedance < min_impedance: min_impedance = impedance - else: - if impedance < min_impedance: - min_impedance = impedance - min_imp_line = line - i += 1 + min_imp_line = line return min_imp_line @@ -260,6 +229,42 @@ def fuse_bus_switches(net, bus_switches): return net +def get_fault_annotation(net: pandapowerNet, fault_current: float = .0, font_size_bus: float = 0.06) -> PatchCollection: + max_bus_idx = max(net.bus.dropna(subset=['geo']).index) + fault_text = f'\tI_sc = {fault_current}kA' + + fault_geo_x_y: Tuple[float, float] = next(geojson.utils.coords(geojson.loads(net.bus.geo.at[max_bus_idx]))) + fault_geo_x_y = (fault_geo_x_y[0], fault_geo_x_y[1] - font_size_bus + 0.02) + + # list of new geo data for line (half position of switch) + fault_annotate: PatchCollection = plot.create_annotation_collection( + texts=[fault_text], + coords=[fault_geo_x_y], + size=font_size_bus, + prop=None + ) + + return fault_annotate + + +def get_sc_location_annotation(net: pandapowerNet, sc_location: float, font_size_bus: float = 0.06) -> PatchCollection: + max_bus_idx = max(net.bus.dropna(subset=['geo']).index) + sc_text = f'\tsc_location: {sc_location * 100}%' + + # list of new geo data for line (middle of position of switch) + sc_geo_x_y = next(geojson.utils.coords(geojson.loads(net.bus.geo.at[max_bus_idx]))) + sc_geo_x_y = (sc_geo_x_y[0], sc_geo_x_y[1] + 0.02) + + sc_annotate: PatchCollection = plot.create_annotation_collection( + texts=[sc_text], + coords=[sc_geo_x_y], + size=font_size_bus, + prop=None + ) + + return sc_annotate + + def plot_tripped_grid(net, trip_decisions, sc_location, bus_size=0.055, plot_annotations=True): # plot the tripped grid of net_sc if MPLCURSORS_INSTALLED: @@ -348,6 +353,8 @@ def plot_tripped_grid(net, trip_decisions, sc_location, bus_size=0.055, plot_ann line_text = [] line_geodata = [] + fault_current: float = .0 + # for Switches in trip_decisions: for line in net.line.index: @@ -362,20 +369,22 @@ def plot_tripped_grid(net, trip_decisions, sc_location, bus_size=0.055, plot_ann respect_in_service=False) bus_list = list(get_bus_index) + bus_coords: List[Tuple[float, float]] = [ + geojson.utils.coords(geojson.loads(net.bus.geo.at[bus])) for bus in bus_list + ] # TODO: # place annotations on middle of the line - line_geo_x = (net.bus_geodata.iloc[bus_list[0]].x + net.bus_geodata.iloc[bus_list[1]].x) / 2 - - line_geo_y = ((net.bus_geodata.iloc[bus_list[0]].y + net.bus_geodata.iloc[bus_list[1]].y) / 2) + 0.05 + line_geo_x = (bus_coords[0][0] + bus_coords[1][0]) / 2 + line_geo_y = ((bus_coords[0][1] + bus_coords[1][1]) / 2) + 0.05 line_geo_x_y = [line_geo_x, line_geo_y] # list of new geo data for line (half position of switch) line_geodata.append(tuple(line_geo_x_y)) - fault_current = round(net.res_bus_sc['ikss_ka'].at[max(net.bus.index)], - 2) # round(Switches['Fault Current'],2) + fault_current = round(net.res_bus_sc['ikss_ka'].at[max(net.bus.index)], 2) + # round(Switches['Fault Current'],2) line_text.append(text_line) @@ -385,67 +394,27 @@ def plot_tripped_grid(net, trip_decisions, sc_location, bus_size=0.055, plot_ann # Bus Annotatations bus_text = [] - for i in net.bus_geodata.index: + for i in net.bus.geo.dropna().index: bus_texts = 'bus_' + str(i) bus_text.append(bus_texts) bus_text = bus_text[:-1] - bus_geodata = net.bus_geodata[['x', 'y']] + bus_geodata = net.bus.geo.dropna().apply(geojson.loads).apply(geojson.utils.coords).apply(next).to_list() # placing bus - bus_geodata['x'] = bus_geodata['x'] - 0.11 - bus_geodata['y'] = bus_geodata['y'] + 0.095 + bus_index = [(x[0] - 0.11, x[1] + 0.095) for x in bus_geodata] # TODO: - bus_index = [tuple(x) for x in bus_geodata.to_numpy()] bus_annotate = plot.create_annotation_collection(texts=bus_text, coords=bus_index, size=0.06, prop=None) collection.append(bus_annotate) # Short circuit annotations - fault_geodata = [] - - fault_text = [] - - fault_texts = ' I_sc = ' + str(fault_current) + 'kA' - - font_size_bus = 0.06 # font size of fault location text - - fault_geo_x = net.bus_geodata.iloc[max(net.bus_geodata.index)][0] - fault_geo_y = net.bus_geodata.iloc[max(net.bus_geodata.index)][1] - font_size_bus + 0.02 - - fault_geo_x_y = [fault_geo_x, fault_geo_y] - - # list of new geo data for line (half position of switch) - fault_geodata.append(tuple(fault_geo_x_y)) - - fault_text.append(fault_texts) - fault_annotate = plot.create_annotation_collection(texts=fault_text, coords=fault_geodata, size=0.06, prop=None) - - collection.append(fault_annotate) + collection.append(get_fault_annotation(net, fault_current)) # sc_location annotation - sc_text = [] - sc_geodata = [] - - sc_texts = ' sc_location: ' + str(sc_location * 100) + '%' - - # font_size_bus=0.06 # font size of sc location - - sc_geo_x = net.bus_geodata.iloc[max(net.bus_geodata.index)][0] - - sc_geo_y = net.bus_geodata.iloc[max(net.bus_geodata.index)][1] + 0.02 - - sc_geo_x_y = [sc_geo_x, sc_geo_y] - - # list of new geo data for line (middle of position of switch) - sc_geodata.append(tuple(sc_geo_x_y)) - - sc_text.append(sc_texts) - sc_annotate = plot.create_annotation_collection(texts=sc_text, coords=sc_geodata, size=0.06, prop=None) - - collection.append(sc_annotate) + collection.append(get_sc_location_annotation(net, sc_location)) # switch annotations # from pandapower.protection.implemeutility_functions import switch_geodata @@ -595,7 +564,9 @@ def plot_tripped_grid_protection_device(net, trip_decisions, sc_location, sc_bus bus_list = list(get_bus_index) # place annotations on middle of the line - bus_coords = list(zip(*net.bus.geo.iloc[bus_list[0:2]].apply(geojson.loads).apply(geojson.utils.coords).apply(next).to_list())) + bus_coords = list( + zip(*net.bus.geo.iloc[bus_list[0:2]].apply(geojson.loads).apply(geojson.utils.coords).apply( + next).to_list())) line_geo_x_y = [sum(x) / 2 for x in bus_coords] line_geo_x_y[1] += 0.05 @@ -610,7 +581,7 @@ def plot_tripped_grid_protection_device(net, trip_decisions, sc_location, sc_bus line_annotate = plot.create_annotation_collection(texts=line_text, coords=line_geodata, size=0.06, prop=None) collection.append(line_annotate) - # Bus Annotatations + # Bus Annotations bus_text = [] for i in net.bus.index: bus_texts = f'bus_{i}' @@ -626,42 +597,13 @@ def plot_tripped_grid_protection_device(net, trip_decisions, sc_location, sc_bus bus_annotate = plot.create_annotation_collection(texts=bus_text, coords=bus_geodata, size=0.06, prop=None) collection.append(bus_annotate) - font_size_bus = 0.06 # font size of fault location text max_bus_idx = max(net.bus.dropna(subset=['geo']).index) # Short circuit annotations - fault_geodata = [] - fault_text = [] - fault_texts = f'\tI_sc = {fault_current}kA' - - fault_geo_x_y = next(geojson.utils.coords(geojson.loads(net.bus.geo.at[max_bus_idx]))) - fault_geo_x_y = (fault_geo_x_y[0], fault_geo_x_y[1] - font_size_bus + 0.02) - - # list of new geo data for line (half position of switch) - fault_geodata.append(fault_geo_x_y) - - fault_text.append(fault_texts) - fault_annotate = plot.create_annotation_collection(texts=fault_text, coords=fault_geodata, size=0.06, prop=None) - - collection.append(fault_annotate) + collection.append(get_fault_annotation(net, fault_current)) # sc_location annotation - sc_text = [] - sc_geodata = [] - sc_texts = f'\tsc_location: {sc_location * 100}%' - - # font_size_bus=0.06 # font size of sc location - - sc_geo_x_y = next(geojson.utils.coords(geojson.loads(net.bus.geo.at[max_bus_idx]))) - sc_geo_x_y = (sc_geo_x_y[0], sc_geo_x_y[1] + 0.02) - - # list of new geo data for line (middle of position of switch) - sc_geodata.append(sc_geo_x_y) - - sc_text.append(sc_texts) - sc_annotate = plot.create_annotation_collection(texts=sc_text, coords=sc_geodata, size=0.06, prop=None) - - collection.append(sc_annotate) + collection.append(get_sc_location_annotation(net, sc_location)) # switch annotations # from pandapower.protection.utility_functions import switch_geodata @@ -699,7 +641,7 @@ def get_connected_lines(net, bus_idx): # first one. E.g. the from_bus given the to_bus of a line. @deprecated("Use pandapower.next_bus(net, bus, element_id instead!") def next_buses(net, bus, element_id): - return pp.next_bus(net,bus,element_id) + return pp.next_bus(net, bus, element_id) # get the connected bus listr from start to end bus @@ -896,89 +838,59 @@ def bus_path_from_to_bus(net, radial_start_bus, loop_start_bus, end_bus): return bus_path -def get_switches_in_path(net, pathes): +def get_switches_in_path(net, paths): """function calculate the switching times from the bus path""" - Lines_in_path = [] + lines_in_path: List[List] = [] - for path in pathes: - Lines_at_path = [] + for path in paths: + lines_at_path: set = set() for bus in path: - Lines_at_paths = [] - lines_at_bus = pp.get_connected_elements(net, "l", bus) - - for line in lines_at_bus: - Lines_at_path.append(line) - - for Line1 in Lines_at_path: - if net.line.from_bus[Line1] in path: - if net.line.to_bus[Line1] in path: - if Line1 not in Lines_at_paths: - Lines_at_paths.append(Line1) - - Lines_in_path.append(Lines_at_paths) + lines_at_path.update(pp.get_connected_elements(net, "l", bus)) - switches_in_net = net.switch.index - switches_in_path = [] + lines_at_paths = [ + line for line in lines_at_path + if net.line.from_bus[line] in path and net.line.to_bus[line] in path + ] - for Linepath in Lines_in_path: - switches_at_path = [] + lines_in_path.append(lines_at_paths) - for Line in Linepath: - - for switch in switches_in_net: - if net.switch.et[switch] == "l": - if net.switch.element[switch] == Line: - switches_at_path.append(switch) - switches_in_path.append(switches_at_path) + switches_in_path = [ + [net.switch[(net.switch['et'] == 'l') & (net.switch['element'] == line)].index for line in line_path] + for line_path in lines_in_path + ] return switches_in_path -def get_vi_angle(net, switch_id, powerflow_results=False): +def get_vi_angle(net: pandapowerNet, switch_id: int, **kwargs) -> float: """calculate the angle between voltage and current with reference to voltage""" - pp.runpp(net) - line_idx = get_line_idx(net, switch_id) - bus_idx = get_bus_idx(net, switch_id) - - if powerflow_results: - - if get_from_bus_info_switch(net, switch_id): - - P = net.res_line.p_from_mw.at[line_idx] - Q = net.res_line.q_from_mvar.at[line_idx] + if "powerflow_results" in kwargs: + logger.warning( + "The powerflow_results argument is deprecated and will be removed in the future." + ) - vm = net.bus.vn_kv.at[bus_idx] * net.res_line.vm_from_pu.at[line_idx] - else: - P = net.res_line.p_to_mw.at[line_idx] - Q = net.res_line.q_to_mvar.at[line_idx] + pp.runpp(net) + line_idx = net.switch.element.at[switch_id] - vm = net.bus.vn_kv.at[bus_idx] * net.res_line.vm_to_pu.at[line_idx] + if get_from_bus_info_switch(net, switch_id): + p = net.res_line_sc.p_from_mw.at[line_idx] + q = net.res_line_sc.q_from_mvar.at[line_idx] else: - - if get_from_bus_info_switch(net, switch_id): - - P = net.res_line_sc.p_from_mw.at[line_idx] - Q = net.res_line_sc.q_from_mvar.at[line_idx] - - vm = net.bus.vn_kv.at[bus_idx] * net.res_line_sc.vm_from_pu.at[line_idx] - - else: - P = net.res_line_sc.p_to_mw.at[line_idx] - Q = net.res_line_sc.q_to_mvar.at[line_idx] - vm = net.bus.vn_kv.at[bus_idx] * net.res_line_sc.vm_to_pu.at[line_idx] - - if P > 0 and Q > 0: - vi_angle = math.degrees(math.atan(Q / P)) - elif P < 0 and Q >= 0: - vi_angle = math.degrees(math.atan(Q / P)) + 180 - elif P < 0 and Q < 0: - vi_angle = math.degrees(math.atan(Q / P)) - 180 - elif P == 0 and Q > 0: + p = net.res_line_sc.p_to_mw.at[line_idx] + q = net.res_line_sc.q_to_mvar.at[line_idx] + + if p > 0 and q > 0: + vi_angle = math.degrees(math.atan(q / p)) + elif p < 0 <= q: + vi_angle = math.degrees(math.atan(q / p)) + 180 + elif p < 0 and q < 0: + vi_angle = math.degrees(math.atan(q / p)) - 180 + elif p == 0 < q: vi_angle = 90 - elif P == 0 and Q < 0: + elif p == 0 > q: vi_angle = -90 else: vi_angle = math.inf @@ -1007,8 +919,8 @@ def bus_path_multiple_ext_bus(net): elif len(from_bus_path) != len(to_bus_path): if len(from_bus_path) > 1 and len(to_bus_path) > 1: - minlen = min(len(from_bus_path), len(to_bus_path)) - if from_bus_path[minlen - 1] != to_bus_path[minlen - 1]: + min_len = min(len(from_bus_path), len(to_bus_path)) + if from_bus_path[min_len - 1] != to_bus_path[min_len - 1]: if len(from_bus_path) < len(to_bus_path): from_bus_path.append(to_bus_path[-1]) max_bus_path.append(from_bus_path) @@ -1025,19 +937,19 @@ def bus_path_multiple_ext_bus(net): return bus_path - # get the line path from the given bus path +# get the line path from the given bus path def get_line_path(net, bus_path): """ Function return the list of line path from the given bus path""" - line_path=[] - for i in range(len(bus_path)-1): - bus1=bus_path[i] - bus2=bus_path[i+1] - line1=net.line[(net.line.from_bus==bus1) & (net.line.to_bus==bus2)].index.to_list() - line2=net.line[(net.line.from_bus==bus2) & (net.line.to_bus==bus1)].index.to_list() - if len(line2)==0: + line_path = [] + for i in range(len(bus_path) - 1): + bus1 = bus_path[i] + bus2 = bus_path[i + 1] + line1 = net.line[(net.line.from_bus == bus1) & (net.line.to_bus == bus2)].index.to_list() + line2 = net.line[(net.line.from_bus == bus2) & (net.line.to_bus == bus1)].index.to_list() + if len(line2) == 0: line_path.append(line1[0]) - if len(line1)==0: + if len(line1) == 0: line_path.append(line2[0]) return line_path From f31b42ecdfa515bdce69266a69b1413d7445a208 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Fri, 29 Nov 2024 15:18:28 +0100 Subject: [PATCH 089/135] fixed typo changed to f-string improved if else where variable possibly not assigned --- pandapower/plotting/plotly/mapbox_plot.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pandapower/plotting/plotly/mapbox_plot.py b/pandapower/plotting/plotly/mapbox_plot.py index d1ee39021..e178cd910 100644 --- a/pandapower/plotting/plotly/mapbox_plot.py +++ b/pandapower/plotting/plotly/mapbox_plot.py @@ -26,18 +26,19 @@ def _on_map_test(x, y): except ImportError: # if geopy is not available there will be no geo-coordinates check # therefore if geo-coordinates are not real and user sets on_map=True, an empty map will be plot! - raise ImportError('Geo-coordinates check cannot be peformed because geopy package not available \n\t--> ' - 'if geo-coordinates are not in lat/lon format an empty plot may appear...') + raise ImportError( + 'Geo-coordinates check cannot be performed because geopy package not available \n\t--> ' + 'if geo-coordinates are not in lat/lon format an empty plot may appear...' + ) try: - location = geolocator.reverse("{0}, {1}".format(x, y), language='en-US') + location = geolocator.reverse(f"{x}, {y}", language='en-US') except GeocoderTimedOut: logger.error("Existing net geodata cannot be geo-located: possible reason: geo-data not in lat/long ->" "try geo_data_to_latlong(net, projection) to transform geodata to lat/long!") - - if location.address is None: - return False else: - return True + if location.address is None: + return False + return True def geo_data_to_latlong(net, projection): From 6808dc81ba9d47d46afad798bda9389f6b83516e Mon Sep 17 00:00:00 2001 From: hkoertge Date: Fri, 29 Nov 2024 15:19:28 +0100 Subject: [PATCH 090/135] deprecated geo_data_to_latlong as it basically is a wrapper, removed commented out code --- pandapower/plotting/plotly/mapbox_plot.py | 38 +++-------------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/pandapower/plotting/plotly/mapbox_plot.py b/pandapower/plotting/plotly/mapbox_plot.py index e178cd910..cdec01449 100644 --- a/pandapower/plotting/plotly/mapbox_plot.py +++ b/pandapower/plotting/plotly/mapbox_plot.py @@ -5,6 +5,9 @@ import os + +from typing_extensions import deprecated + from pandapower.plotting import geo try: @@ -41,6 +44,7 @@ def _on_map_test(x, y): return True +@deprecated('geo_data_to_latlong is deprecated and will be removed shortly, use pandapower.geo.convert_crs instead') def geo_data_to_latlong(net, projection): """ Transforms network's geodata (in `net.bus_geodata` and `net.line_geodata`) from specified projection to lat/long (WGS84). @@ -56,40 +60,6 @@ def geo_data_to_latlong(net, projection): """ geo.convert_crs(net, epsg_in=projection.split(':')[1], epsg_out=4326) - # try: - # from pyproj import Proj, transform - # except ImportError: - # raise ImportError('Geo-coordinates check cannot be peformed because pyproj package not available \n\t--> ' - # 'if geo-coordinates are not in lat/lon format an empty plot may appear...') - # - # if projection == 'epsg:4326': - # return - # - # wgs84 = Proj(init='epsg:4326') # lat/long - # - # try: - # projection = Proj(init=projection) - # except: - # logger.warning("Transformation of geodata to lat/long failed! because of:]\n" - # "Unknown projection provided " - # "(format 'epsg:' required as available at http://spatialreference.org/ref/epsg/ )") - # return - # - # # transform all geodata to long/lat using set or found projection - # try: - # lon, lat = transform(projection, wgs84, net.bus_geodata.loc[:, 'x'].values, net.bus_geodata.loc[:, 'y'].values) - # net.bus_geodata.loc[:, 'x'], net.bus_geodata.loc[:, 'y'] = lon, lat - # - # if net.line_geodata.shape[0] > 0: - # for idx in net.line_geodata.index: - # line_coo = np.array(net.line_geodata.loc[idx, 'coords']) - # lon, lat = transform(projection, wgs84, line_coo[:, 0], line_coo[:, 1]) - # net.line_geodata.loc[idx, 'coords'] = np.array([lon, lat]).T.tolist() - # return - # except: - # logger.warning('Transformation of geodata to lat/long failed!') - # return - def set_mapbox_token(token): from pandapower.__init__ import pp_dir From 35163139b129d8ae156931190bc9aeb8c8630563 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Fri, 29 Nov 2024 15:21:50 +0100 Subject: [PATCH 091/135] deprecated projection keyword for simple_plotly function --- pandapower/plotting/plotly/simple_plotly.py | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pandapower/plotting/plotly/simple_plotly.py b/pandapower/plotting/plotly/simple_plotly.py index db64ff56d..3a04f6091 100644 --- a/pandapower/plotting/plotly/simple_plotly.py +++ b/pandapower/plotting/plotly/simple_plotly.py @@ -4,6 +4,8 @@ # and Energy System Technology (IEE), Kassel. All rights reserved. +import warnings +from typing_extensions import overload import pandas as pd from pandapower.plotting.generic_geodata import create_generic_coordinates @@ -98,12 +100,33 @@ def get_hoverinfo(net, element, precision=3, sub_index=None): return hoverinfo +@overload +def simple_plotly(net, respect_switches=True, use_line_geo=None, on_map=False, + *, map_style='basic', figsize=1.0, aspectratio='auto', + line_width=1.0, bus_size=10.0, ext_grid_size=20.0, + bus_color="blue", line_color='grey', trafo_color='green', + trafo3w_color='green', ext_grid_color="yellow", + filename='temp-plot.html', auto_open=True, showlegend=True, + additional_traces=None, zoomlevel=11, auto_draw_traces=True, hvdc_color='cyan'): ... + + +@overload +@deprecated("projection is deprecated and will be removed in future versions. geojson should always be WGS84.") def simple_plotly(net, respect_switches=True, use_line_geo=None, on_map=False, projection='epsg:4326', map_style='basic', figsize=1.0, aspectratio='auto', line_width=1.0, bus_size=10.0, ext_grid_size=20.0, bus_color="blue", line_color='grey', trafo_color='green', trafo3w_color='green', ext_grid_color="yellow", filename='temp-plot.html', auto_open=True, showlegend=True, + additional_traces=None, zoomlevel=11, auto_draw_traces=True, hvdc_color='cyan'): ... + + +def simple_plotly(net, respect_switches=True, use_line_geo=None, on_map=False, + projection=None, map_style='basic', figsize=1.0, aspectratio='auto', + line_width=1.0, bus_size=10.0, ext_grid_size=20.0, + bus_color="blue", line_color='grey', trafo_color='green', + trafo3w_color='green', ext_grid_color="yellow", + filename='temp-plot.html', auto_open=True, showlegend=True, additional_traces=None, zoomlevel=11, auto_draw_traces=True, hvdc_color='cyan'): """ Plots a pandapower network as simple as possible in plotly. @@ -207,6 +230,14 @@ def simple_plotly(net, respect_switches=True, use_line_geo=None, on_map=False, showlegend=showlegend, zoomlevel=zoomlevel, hvdc_color=hvdc_color) + if projection is not None: + warnings.warn( + FutureWarning( + "projection is deprecated and will be removed in future versions. geojson should always be WGS84." + ), + stacklevel=2 + ) + if additional_traces: if isinstance(additional_traces, dict): additional_traces = [additional_traces] From a412ade18421fc823206ff81df7a0fef44509e7c Mon Sep 17 00:00:00 2001 From: hkoertge Date: Fri, 29 Nov 2024 15:23:14 +0100 Subject: [PATCH 092/135] cleaned up code a bit --- pandapower/plotting/plotly/simple_plotly.py | 179 ++++++++++++-------- 1 file changed, 109 insertions(+), 70 deletions(-) diff --git a/pandapower/plotting/plotly/simple_plotly.py b/pandapower/plotting/plotly/simple_plotly.py index 3a04f6091..f4bf0d325 100644 --- a/pandapower/plotting/plotly/simple_plotly.py +++ b/pandapower/plotting/plotly/simple_plotly.py @@ -196,40 +196,6 @@ def simple_plotly(net, respect_switches=True, use_line_geo=None, on_map=False, OUTPUT: **figure** (graph_objs._figure.Figure) figure object """ - node_element = "bus" - branch_element = "line" - trans_element = "trafo" - trans3w_element = "trafo3w" - separator_element = "switch" - traces, settings = _simple_plotly_generic(net=net, - respect_separators=respect_switches, - use_branch_geodata=use_line_geo, - on_map=on_map, - projection=projection, - map_style=map_style, - figsize=figsize, - aspectratio=aspectratio, - branch_width=line_width, - node_size=bus_size, - ext_grid_size=ext_grid_size, - node_color=bus_color, - branch_color=line_color, - trafo_color=trafo_color, - trafo3w_color=trafo3w_color, - ext_grid_color=ext_grid_color, - node_element=node_element, - branch_element=branch_element, - trans_element=trans_element, - trans3w_element=trans3w_element, - separator_element=separator_element, - branch_trace_func=create_line_trace, - node_trace_func=create_bus_trace, - hoverinfo_func=get_hoverinfo, - filename=filename, - auto_open=auto_open, - showlegend=showlegend, - zoomlevel=zoomlevel, - hvdc_color=hvdc_color) if projection is not None: warnings.warn( FutureWarning( @@ -238,6 +204,39 @@ def simple_plotly(net, respect_switches=True, use_line_geo=None, on_map=False, stacklevel=2 ) + settings = dict( + on_map=on_map, + map_style=map_style, + figsize=figsize, + aspectratio=aspectratio, + filename=filename, + auto_open=auto_open, + showlegend=showlegend, + zoomlevel=zoomlevel + ) + + traces, _ = _simple_plotly_generic( + net=net, + respect_separators=respect_switches, + use_branch_geodata=use_line_geo, + branch_width=line_width, + node_size=bus_size, + ext_grid_size=ext_grid_size, + node_color=bus_color, + branch_color=line_color, + trafo_color=trafo_color, + trafo3w_color=trafo3w_color, + ext_grid_color=ext_grid_color, + node_element="bus", + branch_element="line", + trans_element="trafo", + trans3w_element="trafo3w", + branch_trace_func=create_line_trace, + node_trace_func=create_bus_trace, + hoverinfo_func=get_hoverinfo, + hvdc_color=hvdc_color, + settings=settings + ) if additional_traces: if isinstance(additional_traces, dict): additional_traces = [additional_traces] @@ -256,33 +255,55 @@ def simple_plotly(net, respect_switches=True, use_line_geo=None, on_map=False, else: return traces, settings -def _simple_plotly_generic(net, respect_separators, use_branch_geodata, on_map, projection, map_style, - figsize, aspectratio, branch_width, node_size, ext_grid_size, node_color, - branch_color, trafo_color, trafo3w_color, ext_grid_color, + +def _simple_plotly_generic(net, respect_separators, use_branch_geodata, branch_width, node_size, ext_grid_size, + node_color, branch_color, trafo_color, trafo3w_color, ext_grid_color, node_element, branch_element, trans_element, trans3w_element, - separator_element, branch_trace_func, node_trace_func, - hoverinfo_func, filename='temp-plot.html', auto_open=True, - showlegend=True, zoomlevel=11, hvdc_color="cyan"): - settings = dict(on_map=on_map, projection=projection, map_style=map_style, figsize=figsize, - aspectratio=aspectratio, filename=filename, auto_open=auto_open, - showlegend=showlegend, zoomlevel=zoomlevel) + branch_trace_func, node_trace_func, hoverinfo_func, + hvdc_color="cyan", settings=None, **kwargs): + + if 'projection' in kwargs: + warnings.warn( + FutureWarning( + "projection is deprecated and will not be used. geojson should always be WGS84." + ), + stacklevel=2 + ) + kwargs.pop('projection') + + settings_defaults = { # if no settings are provided, these are used + 'on_map': kwargs.get('on_map', True), + 'map_style': kwargs.get('map_style', 'basic'), + 'figsize': kwargs.get('figsize', 1.), + 'aspectratio': kwargs.get('aspectratio', 'auto'), + 'filename': kwargs.get('filename', 'temp-plot.html'), + 'auto_open': kwargs.get('auto_open', 'auto'), + 'showlegend': kwargs.get('showlegend', True), + 'zoomlevel': kwargs.get('zoomlevel', 11) + } + + settings = settings_defaults | settings if settings else {} # add missing settings to settings dict if len(net[node_element]["geo"].dropna()) == 0: - logger.warning("No or insufficient geodata available --> Creating artificial coordinates." + - " This may take some time...") + logger.warning( + "No or insufficient geodata available --> Creating artificial coordinates. This may take some time..." + ) create_generic_coordinates(net, respect_switches=respect_separators) - if on_map: + if settings['on_map']: logger.warning( "Map plots not available with artificial coordinates and will be disabled!") - on_map = False - # check if geodata are real geographical lat/lon coordinates using geopy - if on_map and projection is not None: - geo_data_to_latlong(net, projection=projection) + settings['on_map'] = False + # ----- Nodes (Buses) ------ # initializing node trace hoverinfo = hoverinfo_func(net, element=node_element) - node_trace = node_trace_func(net, net[node_element].index, size=node_size, color=node_color, - infofunc=hoverinfo) + node_trace = node_trace_func( + net, + net[node_element].index, + size=node_size, + color=node_color, + infofunc=hoverinfo + ) # ----- branches (Lines) ------ # if node geodata is available, but no branch geodata if use_branch_geodata is None: @@ -292,10 +313,15 @@ def _simple_plotly_generic(net, respect_separators, use_branch_geodata, on_map, "No or insufficient line geodata available --> only bus geodata will be used.") use_branch_geodata = False hoverinfo = hoverinfo_func(net, element=branch_element) - branch_traces = branch_trace_func(net, net[branch_element].index, use_branch_geodata, - respect_separators, - color=branch_color, width=branch_width, - infofunc=hoverinfo) + branch_traces = branch_trace_func( + net, + net[branch_element].index, + use_branch_geodata, + respect_separators, + color=branch_color, + width=branch_width, + infofunc=hoverinfo + ) trans_trace = [] trans_trace3w = [] ext_grid_trace = [] @@ -303,31 +329,44 @@ def _simple_plotly_generic(net, respect_separators, use_branch_geodata, on_map, # ----- Trafos ------ if 'trafo' in net and len(net.trafo): hoverinfo = hoverinfo_func(net, element=trans_element) - trans_trace = create_trafo_trace(net, color=trafo_color, width=branch_width * 5, - infofunc=hoverinfo, - use_line_geo=use_branch_geodata) + trans_trace = create_trafo_trace( + net, + color=trafo_color, + width=branch_width * 5, + infofunc=hoverinfo, + use_line_geo=use_branch_geodata + ) # ----- 3W Trafos ------ if 'trafo3w' in net and len(net.trafo3w): hoverinfo = hoverinfo_func(net, element=trans3w_element) - trans_trace3w = create_trafo_trace(net, color=trafo3w_color, trafotype='3W', - width=branch_width * 5, - trace_name='3W transformers', infofunc=hoverinfo, - use_line_geo=use_branch_geodata) + trans_trace3w = create_trafo_trace( + net, color=trafo3w_color, trafotype='3W', + width=branch_width * 5, + trace_name='3W transformers', + infofunc=hoverinfo, + use_line_geo=use_branch_geodata + ) # ----- Ext grid ------ # get external grid from _create_node_trace if 'ext_grid' in net and len(net.ext_grid): - marker_type = 'circle' if on_map else 'square' # workaround because doesn't appear on mapbox if square + marker_type = 'circle' if settings['on_map'] else 'square' # workaround because doesn't appear on mapbox if square hoverinfo = hoverinfo_func(net, element="ext_grid") - ext_grid_trace = _create_node_trace(net, nodes=net.ext_grid[node_element], size=ext_grid_size, - patch_type=marker_type, color=ext_grid_color, - infofunc=hoverinfo, trace_name='external grid', - node_element=node_element, branch_element=branch_element) + ext_grid_trace = _create_node_trace( + net, + nodes=net.ext_grid[node_element], + size=ext_grid_size, + patch_type=marker_type, + color=ext_grid_color, + infofunc=hoverinfo, + trace_name='external grid', + node_element=node_element, + branch_element=branch_element + ) # ----- HVDC lines ------ if 'dcline' in net and len(net.dcline): dc_line_trace = create_dcline_trace(net, color=hvdc_color) - return branch_traces + trans_trace + trans_trace3w + ext_grid_trace + node_trace + dc_line_trace,\ - settings + return branch_traces + trans_trace + trans_trace3w + ext_grid_trace + node_trace + dc_line_trace, settings if __name__ == '__main__': From b2a105283df100e50911b8aa22667f07afff320a Mon Sep 17 00:00:00 2001 From: hkoertge Date: Fri, 29 Nov 2024 15:23:46 +0100 Subject: [PATCH 093/135] improved error catching and fixed grammer error --- pandapower/plotting/plotly/traces.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandapower/plotting/plotly/traces.py b/pandapower/plotting/plotly/traces.py index 65e7c46f6..a5cc95914 100644 --- a/pandapower/plotting/plotly/traces.py +++ b/pandapower/plotting/plotly/traces.py @@ -1077,9 +1077,9 @@ def draw_traces(traces, on_map=False, map_style='basic', showlegend=True, figsiz if on_map: try: on_map = _on_map_test(traces[0]['x'][0], traces[0]['y'][0]) - except: + except ImportError: logger.warning("Test if geo-data are in lat/long cannot be performed using geopy -> " - "eventual plot errors are possible.") + "plot errors are possible.") if on_map is False: logger.warning("Existing geodata are not real lat/lon geographical coordinates. -> " From dc9f0d019450c6ca5136ca862051455626656078 Mon Sep 17 00:00:00 2001 From: hkoertge Date: Fri, 29 Nov 2024 15:33:39 +0100 Subject: [PATCH 094/135] cleaned up main in simple_plotly.py --- pandapower/plotting/plotly/simple_plotly.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pandapower/plotting/plotly/simple_plotly.py b/pandapower/plotting/plotly/simple_plotly.py index f4bf0d325..0d61998ca 100644 --- a/pandapower/plotting/plotly/simple_plotly.py +++ b/pandapower/plotting/plotly/simple_plotly.py @@ -370,12 +370,9 @@ def _simple_plotly_generic(net, respect_separators, use_branch_geodata, branch_w if __name__ == '__main__': - from pandapower import networks as nw from pandapower.networks import mv_oberrhein from pandapower.plotting.plotly.traces import create_weighted_marker_trace - # simple_plotly(net) - # net = nw.example_multivoltage() - # fig = simple_plotly(net, trafo3w_color='k') + net = mv_oberrhein() net.load.scaling, net.sgen.scaling = 1, 1 # different markers and sizemodes as examples @@ -386,5 +383,9 @@ def _simple_plotly_generic(net, respect_separators, use_branch_geodata, branch_w patch_type="circle-open", sizemode="diameter", marker_scaling=100, scale_marker_size=[0.2, 0.4]) - fig = simple_plotly(net, bus_size=1, aspectratio="original", additional_traces=[markers_sgen, - markers_load]) + fig = simple_plotly( + net, + bus_size=1, + aspectratio="original", + additional_traces=[markers_sgen, markers_load] + ) From e2e85281fc0a5af794be78139d76d45f1fec3b29 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Mon, 2 Dec 2024 08:40:26 +0100 Subject: [PATCH 095/135] improve lightsim2grid documentation --- CHANGELOG.rst | 1 + doc/powerflow/ac.rst | 65 ++++++++++++++++++++++++++++++++++++++--- pandapower/auxiliary.py | 8 +++-- pandapower/run.py | 10 +++---- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 049f52637..9fc265b8d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,7 @@ Change Log - [ADDED] Add GeographicalRegion and SubGeographicalRegion names and ids to bus df in cim converter - [CHANGED] Capitalize first letter of columns busbar_id, busbar_name and substation_id in bus df for cim converter - [FIXED] Do not modify pandas options when importing pandapower +- [ADDED] improved lightsim2grid documentation including compatibitliy issues - [FIXED] cim2pp: set default xml encoding to None to avoid error after changing to lxml [2.14.11] - 2024-07-08 diff --git a/doc/powerflow/ac.rst b/doc/powerflow/ac.rst index bc6e46c55..331e4abc0 100644 --- a/doc/powerflow/ac.rst +++ b/doc/powerflow/ac.rst @@ -13,12 +13,69 @@ pandapower uses PYPOWER to solve the power flow problem: If you are interested in the pypower casefile that pandapower is using for power flow, you can find it in net["_ppc"]. However all necessary informations are written into the pandpower format net, so the pandapower user should not usually have to deal with pypower. -If available, the librabry lightsim2grid is used as a backend for power flow simulation instead of the -implementation in pandapower, leading to a boost in performance. The library lightsim2grid is implemented in C++ and -can either be installed with pip install lightsim2grid, or built from source. More about the library and the -installation guide can be found in the `documentation `_ or +Accelerating Packages +------------------------- + +Two external packages are available which let accelerate pandapower's power flow command :code:`runppp`: + +1. numba +2. lightsim2grid + +If available, i.e. installed on the operating computer, the code will check by default all +prerequisites to use the external packages. numba is a python JIT compiler, +cf. `link `_. In constrast, the library lightsim2grid +is used as a backend for power flow simulation instead of the +implementation in pandapower. It leads to a boost in performance. The library lightsim2grid is +implemented in C++ and can either be installed with pip install lightsim2grid, or built from source. +More about the library and the installation guide can be found in the +`documentation `_ or its GitHub `repository `_. +lightsim2grid Compatibility +``````````````````````````````` + +lightsim2grid is supported if all the following conditions are met: + +1. The lightsim2grid library is installed and available. +2. The selected power flow algorithm is Newton-Raphson (algorithm='nr'). +3. Voltage-dependent loads are not enabled (voltage_depend_loads=False). +4. Either: + + * There is only one slack bus in the network, or + * Distributed slack is enabled (distributed_slack=True). + +5. None of the following elements are present in the grid model: + + * Controllable shunts, including SVC, SSC, or VSC elements. + * Controllable impedances, such as TCSC elements. + * DC elements, including DC buses (bus_dc) or DC lines (line_dc). + +6. Temperature-Dependent Power Flow is not requested (tdpf=False). + +When lightsim2grid is Not Supported +``````````````````````````````````````` + +If any of the above conditions are not met, lightsim2grid cannot be used. In such cases: + +* If lightsim2grid='auto' (default), the fallback to the standard pandapower implementation occurs without a detailed message. +* If lightsim2grid=True is explicitly set, an appropriate error or warning is raised or logged, depending on the condition. + +Common Limitations of lightsim2grid +```````````````````````````````````````` + +lightsim2grid does not currently support: + +* Algorithms other than Newton-Raphson +* Voltage-dependent loads +* Multiple slack buses without distributed slack +* Grids containing any of the following advanced elements: + + * Controllable shunts (SVC, SSC, VSC) + * Controllable impedances (TCSC) + * DC buses or DC lines + +* Temperature-Dependent Power Flow (tdpf=True) + Temperature-Dependent Power Flow (TDPF) --------------------------------------- diff --git a/pandapower/auxiliary.py b/pandapower/auxiliary.py index 393b7c044..b8e73d26f 100644 --- a/pandapower/auxiliary.py +++ b/pandapower/auxiliary.py @@ -1273,9 +1273,11 @@ def _check_if_numba_is_installed(level="warning"): def _check_lightsim2grid_compatibility(net, lightsim2grid, voltage_depend_loads, algorithm, distributed_slack, tdpf): """ - Implement some checks to decide whether the package lightsim2grid can be used. The package implements a backend for - power flow calculation in C++ and provides a speed-up. If lightsim2grid is "auto" (default), we don't bombard the - user with messages. Otherwise, if lightsim2grid is True bus cannot be used, we inform the user abot it. + Implement some checks to decide whether the package lightsim2grid can be used. These checks are + documentated in :code:`doc\powerflow\ac.rst` The package implements a backend for power flow + calculation in C++ and provides a speed-up. If lightsim2grid + is "auto" (default), we don't bombard the user with messages. Otherwise, if lightsim2grid is + True bus cannot be used, we inform the user abot it. """ if not lightsim2grid: return False # early return :) diff --git a/pandapower/run.py b/pandapower/run.py index f77e4f6ee..2ba35b72a 100644 --- a/pandapower/run.py +++ b/pandapower/run.py @@ -174,12 +174,12 @@ def runpp(net, algorithm='nr', calculate_voltage_angles=True, init="auto", **KWARGS**: - **lightsim2grid** ((bool,str), "auto") - whether to use the package lightsim2grid for power flow backend + **lightsim2grid** ((bool,str), "auto") - whether to use the package lightsim2grid for power + flow backend. For more details on compatibility, check out pandapower's documentation. - **numba** (bool, True) - Activation of numba JIT compiler in the newton solver - - If set to True, the numba JIT compiler is used to generate matrices for the powerflow, - which leads to significant speed improvements. + **numba** (bool, True) - Activation of numba JIT compiler in the newton solver. + If set to True, the numba JIT compiler is used to generate matrices for the powerflow, + which leads to significant speed improvements. **switch_rx_ratio** (float, 2) - rx_ratio of bus-bus-switches. If the impedance of switches defined in net.switch.z_ohm is zero, buses connected by a closed bus-bus switch are fused to From 5857af2b503ea48085ac1c73d7795f1e0fd12d0c Mon Sep 17 00:00:00 2001 From: marcopau Date: Tue, 3 Dec 2024 10:48:39 +0100 Subject: [PATCH 096/135] AF-WLS changes --- pandapower/estimation/algorithm/base.py | 9 +++++- .../estimation/algorithm/matrix_base.py | 15 ++++++---- pandapower/estimation/ppc_conversion.py | 30 ++++++++++++------- pandapower/pf/run_newton_raphson_pf.py | 1 + pandapower/pypower/newtonpf.py | 12 ++++++++ 5 files changed, 49 insertions(+), 18 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index f07ddf54b..6177314b1 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -37,7 +37,8 @@ def __init__(self, tolerance, maximum_iterations, logger=std_logger): def check_observability(self, eppci: ExtendedPPCI, z): # Check if observability criterion is fulfilled and the state estimation is possible - if len(z) < 2 * eppci["bus"].shape[0] - 1: + num_slacks = sum(~eppci.non_slack_bus_mask) + if len(z) < 2 * eppci["bus"].shape[0] - num_slacks: self.logger.error("System is not observable (cancelling)") self.logger.error("Measurements available: %d. Measurements required: %d" % (len(z), 2 * eppci["bus"].shape[0] - 1)) @@ -320,6 +321,12 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # state vector difference d_E d_E = spsolve(G_m, H.T * (r_inv * r)) + # # Scaling of Delta_X to avoid divergence due o ill-conditioning and + # # operating conditions far from starting state variables + # current_error = np.max(np.abs(d_E)) + # if current_error > 0.25: + # d_E = d_E*0.25/current_error + # Update E with d_E E += d_E.ravel() # eppci.update_E(E1) diff --git a/pandapower/estimation/algorithm/matrix_base.py b/pandapower/estimation/algorithm/matrix_base.py index f4c871b24..de3b778ed 100644 --- a/pandapower/estimation/algorithm/matrix_base.py +++ b/pandapower/estimation/algorithm/matrix_base.py @@ -90,7 +90,8 @@ def create_hx(self, E): Qbuse2 = np.sum(np.multiply(E2,self.eppci["rated_power_clusters"][:,num_clusters:2*num_clusters]),axis=1) hx = np.r_[hx, np.real(Sbuse)-Pbuse2, - np.imag(Sbuse)-Qbuse2] + np.imag(Sbuse)-Qbuse2, + E2] return hx[self.non_nan_meas_selector] @@ -151,23 +152,25 @@ def create_hx_jacobian(self, E): if self.eppci.algorithm == "af-wls": p_eq_bal_jac_E1 = hstack((dSbus_dth.real, dSbus_dv.real)).toarray() q_eq_bal_jac_E1 = hstack((dSbus_dth.imag, dSbus_dv.imag)).toarray() + af_vmeas_E1 = np.zeros((num_clusters,jac.shape[1])) + jac_E2 = np.zeros((jac.shape[0],num_clusters)) p_eq_bal_jac_E2 = - self.eppci["rated_power_clusters"][:,:num_clusters] q_eq_bal_jac_E2 = - self.eppci["rated_power_clusters"][:,num_clusters:2*num_clusters] - - jac_E2 = np.zeros((jac.shape[0],num_clusters)) + af_vmeas_E2 = np.identity(num_clusters) jac = np.r_[jac, p_eq_bal_jac_E1, - q_eq_bal_jac_E1] + q_eq_bal_jac_E1, + af_vmeas_E1] jac_E2 = np.r_[jac_E2, p_eq_bal_jac_E2, - q_eq_bal_jac_E2] + q_eq_bal_jac_E2, + af_vmeas_E2] jac = jac[self.non_nan_meas_selector, :][:, self.delta_v_bus_selector] jac_E2 = jac_E2[self.non_nan_meas_selector, :][:] - jac = np.c_[jac, jac_E2] else: diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index dda125fa7..33e7c0c63 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -290,13 +290,21 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): P *= -1 Q *= -1 cluster = active_elements.type.values + if (bus >= ppci["bus"].shape[0]).any(): + std_logger.warning("Loads or sgen defined in pp-grid do not exist in ppci, will be deleted!") + P = P[bus < ppci["bus"].shape[0]] + Q = Q[bus < ppci["bus"].shape[0]] + cluster = cluster[bus < ppci["bus"].shape[0]] + bus = bus[bus < ppci["bus"].shape[0]] for k in range(num_clusters): cluster[cluster == cluster_list_tot[k]] = k cluster = cluster.astype(int) - ppci["rated_power_clusters"][bus, cluster] = P - ppci["rated_power_clusters"][bus, cluster + num_clusters] = Q - ppci["rated_power_clusters"][bus, cluster + 2*num_clusters] = abs(0.03*P) # std dev cluster variability hardcoded, think how to change it - ppci["rated_power_clusters"][bus, cluster + 3*num_clusters] = abs(0.03*Q) # std dev cluster variability hardcoded, think how to change it + for i in range(len(P)): + bus_i, cluster_i, P_i, Q_i = bus[i], cluster[i], P[i], Q[i] + ppci["rated_power_clusters"][bus_i, cluster_i] += P_i + ppci["rated_power_clusters"][bus_i, cluster_i + num_clusters] += Q_i + ppci["rated_power_clusters"][bus_i, cluster_i + 2*num_clusters] += abs(0.03*P_i) # std dev cluster variability hardcoded, think how to change it + ppci["rated_power_clusters"][bus_i, cluster_i + 3*num_clusters] += abs(0.03*Q_i) # std dev cluster variability hardcoded, think how to change it return ppci @@ -389,7 +397,8 @@ def _build_measurement_vectors(ppci, update_meas_only=False): idx_non_imeas = np.flatnonzero(~imag_meas) if ppci.algorithm == "af-wls": balance_eq_meas = np.zeros(ppci["rated_power_clusters"].shape[0]).astype(np.float64) - z = np.concatenate((z, balance_eq_meas[ppci.non_slack_bus_mask], balance_eq_meas[ppci.non_slack_bus_mask])) + af_vmeas = 0.4*np.ones(len(ppci["clusters"])) + z = np.concatenate((z, balance_eq_meas[ppci.non_slack_bus_mask], balance_eq_meas[ppci.non_slack_bus_mask], af_vmeas)) if not update_meas_only: # conserve the pandapower indices of measurements in the ppci order @@ -438,12 +447,11 @@ def _build_measurement_vectors(ppci, update_meas_only=False): i_degree_line_t_not_nan]) if ppci.algorithm == "af-wls": num_clusters = len(ppci["clusters"]) - # P_balance_dev_std = np.sqrt(np.sum(np.square(ppci["rated_power_clusters"][:,2*num_clusters:3*num_clusters]),axis=1)) - # Q_balance_dev_std = np.sqrt(np.sum(np.square(ppci["rated_power_clusters"][:,3*num_clusters:4*num_clusters]),axis=1)) - P_balance_dev_std = np.full((790,),25) - Q_balance_dev_std = np.full((790,),25) - r_cov = np.concatenate((r_cov, P_balance_dev_std[ppci.non_slack_bus_mask], Q_balance_dev_std[ppci.non_slack_bus_mask])) - meas_mask = np.concatenate((meas_mask, ppci.non_slack_bus_mask, ppci.non_slack_bus_mask)) + P_balance_dev_std = np.sqrt(np.sum(np.square(ppci["rated_power_clusters"][:,2*num_clusters:3*num_clusters]),axis=1)) + Q_balance_dev_std = np.sqrt(np.sum(np.square(ppci["rated_power_clusters"][:,3*num_clusters:4*num_clusters]),axis=1)) + af_vmeas_dev_std = 0.15*np.ones(len(ppci["clusters"])) + r_cov = np.concatenate((r_cov, P_balance_dev_std[ppci.non_slack_bus_mask], Q_balance_dev_std[ppci.non_slack_bus_mask], af_vmeas_dev_std)) + meas_mask = np.concatenate((meas_mask, ppci.non_slack_bus_mask, ppci.non_slack_bus_mask, np.ones(len(ppci["clusters"])))) return z, pp_meas_indices, r_cov, meas_mask, any_i_meas, any_degree_meas, idx_non_imeas else: diff --git a/pandapower/pf/run_newton_raphson_pf.py b/pandapower/pf/run_newton_raphson_pf.py index 48455e457..74b9c2df3 100644 --- a/pandapower/pf/run_newton_raphson_pf.py +++ b/pandapower/pf/run_newton_raphson_pf.py @@ -163,6 +163,7 @@ def _run_ac_pf_without_qlims_enforced(ppci, options): # run the newton power flow + options["lightsim2grid"] = False if options["lightsim2grid"]: V, success, iterations, J, Vm_it, Va_it = newton_ls(Ybus.tocsc(), Sbus, V0, ref, pv, pq, ppci, options) T = None diff --git a/pandapower/pypower/newtonpf.py b/pandapower/pypower/newtonpf.py index a22fee238..3e98d0a9f 100644 --- a/pandapower/pypower/newtonpf.py +++ b/pandapower/pypower/newtonpf.py @@ -439,6 +439,15 @@ def newtonpf(Ybus, Sbus, V0, ref, pv, pq, ppci, options, makeYbus=None): J = J + J_m_hvdc dx = -1 * spsolve(J, F, permc_spec=permc_spec, use_umfpack=use_umfpack) + + # log data + initial_delta = np.max(np.abs(dx)) + print("Initial current delta_x: {:.7f}".format(initial_delta)) + # if initial_delta > 0.35: + # dx = dx*0.35/initial_delta + # new_delta = np.max(np.abs(dx)) + # print("Smoothed current delta_x: {:.7f}".format(new_delta)) + # update voltage if dist_slack: slack = slack + dx[j0:j1] @@ -465,6 +474,9 @@ def newtonpf(Ybus, Sbus, V0, ref, pv, pq, ppci, options, makeYbus=None): Vm = abs(V) # update Vm and Va again in case Va = angle(V) # we wrapped around with a negative Vm + print("Max voltage magnitude: {:.7f}".format(max(Vm))) + print("Min voltage magnitude: {:.7f}".format(min(Vm))) + if v_debug: Vm_it = column_stack((Vm_it, Vm)) Va_it = column_stack((Va_it, Va)) From 6c3acea3ee1dac98be9edcf3b1804fe3a4c09168 Mon Sep 17 00:00:00 2001 From: Moritz Franz Date: Tue, 3 Dec 2024 10:54:37 +0100 Subject: [PATCH 097/135] - removed some unused imports - fixed typo - RATE_A got the default value of 100% loading, resulting in non convergence in the julia test --- pandapower/build_branch.py | 54 ++++++++++++------- pandapower/converter/pandamodels/to_pm.py | 1 + pandapower/pypower/printpf.py | 2 +- pandapower/test/opf/test_pandamodels_runpm.py | 2 - pandapower/timeseries/ts_runpp.py | 11 ++-- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/pandapower/build_branch.py b/pandapower/build_branch.py index 8d5930bc9..45d455aa3 100644 --- a/pandapower/build_branch.py +++ b/pandapower/build_branch.py @@ -162,13 +162,15 @@ def _calc_trafo3w_parameter(net, ppc, update_vk_values: bool=True): branch[f:t, SHIFT] = shift branch[f:t, BR_STATUS] = in_service # always set RATE_A for completeness + # RATE_A is conisdered by the (PowerModels) OPF. If zero -> unlimited if "max_loading_percent" in trafo_df: max_load = get_trafo_values(trafo_df, "max_loading_percent") sn_mva = get_trafo_values(trafo_df, "sn_mva") branch[f:t, RATE_A] = max_load / 100. * sn_mva else: - sn_mva = get_trafo_values(trafo_df, "sn_mva") - branch[f:t, RATE_A] = sn_mva + # PowerModels considers "0" as "no limit" + # todo: inf and convert only when using PowerModels to 0., pypower opf converts the zero to inf + branch[f:t, RATE_A] = 0. if net["_options"]["mode"] == "opf" else 100. def _calc_line_parameter(net, ppc, elm="line", ppc_elm="branch"): @@ -244,13 +246,16 @@ def _calc_line_parameter(net, ppc, elm="line", ppc_elm="branch"): branch[f:t, BR_STATUS] = line["in_service"].values # always set RATE_A for completeness: # RATE_A is conisdered by the (PowerModels) OPF. If zero -> unlimited - # TODO: check why OPF test fails if 100 instead of 0 - max_load = line.max_loading_percent.values if "max_loading_percent" in line else 0. - vr = net.bus.loc[line["from_bus"].values, "vn_kv"].values * np.sqrt(3.) - max_i_ka = line.max_i_ka.values - df = line.df.values - # This calculates the maximum apparent power at 1.0 p.u. - branch[f:t, RATE_A] = max_load / 100. * max_i_ka * df * parallel * vr + if "max_loading_percent" in line: + max_load = line.max_loading_percent.values + vr = net.bus.loc[line["from_bus"].values, "vn_kv"].values * np.sqrt(3.) + max_i_ka = line.max_i_ka.values + df = line.df.values + branch[f:t, RATE_A] = max_load / 100. * max_i_ka * df * parallel * vr + else: + # PowerModels considers "0" as "no limit" + # todo: inf and convert only when using PowerModels to 0., pypower opf converts the zero to inf + branch[f:t, RATE_A] = 0. if mode == "opf" else 100. def _calc_line_dc_parameter(net, ppc, elm="line_dc", ppc_elm="branch_dc"): @@ -321,12 +326,17 @@ def _calc_line_dc_parameter(net, ppc, elm="line_dc", ppc_elm="branch_dc"): branch_dc[f:t, DC_BR_STATUS] = line_dc["in_service"].values # always set RATE_A for completeness: # RATE_A is conisdered by the (PowerModels) OPF. If zero -> unlimited - max_load = line_dc.max_loading_percent.values if "max_loading_percent" in line_dc else 0. - vr = net.bus_dc.loc[line_dc["from_bus_dc"].values, "vn_kv"].values * np.sqrt(3.) - max_i_ka = line_dc.max_i_ka.values - df = line_dc.df.values - # This calculates the maximum apparent power at 1.0 p.u. - branch_dc[f:t, DC_RATE_A] = max_load / 100. * max_i_ka * df * parallel * vr + if "max_loading_percent" in line_dc: + max_load = line_dc.max_loading_percent.values + vr = net.bus_dc.loc[line_dc["from_bus_dc"].values, "vn_kv"].values * np.sqrt(3.) + max_i_ka = line_dc.max_i_ka.values + df = line_dc.df.values + # This calculates the maximum apparent power at 1.0 p.u. + branch_dc[f:t, DC_RATE_A] = max_load / 100. * max_i_ka * df * parallel * vr + else: + # PowerModels considers "0" as "no limit" + # todo: inf and convert only when using PowerModels to 0., pypower opf converts the zero to inf + branch_dc[f:t, DC_RATE_A] = 0. if mode == "opf" else 100. def _calc_trafo_parameter(net, ppc, update_vk_values: bool=True): @@ -364,10 +374,16 @@ def _calc_trafo_parameter(net, ppc, update_vk_values: bool=True): raise UserWarning("Rating factor df must be positive. Transformers with false " "rating factors: %s" % trafo.query('df<=0').index.tolist()) # always set RATE_A for completeness - max_load = trafo.max_loading_percent.values if "max_loading_percent" in trafo else 100 - sn_mva = trafo.sn_mva.values - df = trafo.df.values - branch[f:t, RATE_A] = max_load / 100. * sn_mva * df * parallel + # RATE_A is conisdered by the (PowerModels) OPF. If zero -> unlimited + if "max_loading_percent" in trafo: + max_load = trafo.max_loading_percent.values + sn_mva = trafo.sn_mva.values + df = trafo.df.values + branch[f:t, RATE_A] = max_load / 100. * sn_mva * df * parallel + else: + # PowerModels considers "0" as "no limit" + # todo: inf and convert only when using PowerModels to 0., pypower opf converts the zero to inf + branch[f:t, RATE_A] = 0. if net["_options"]["mode"] == "opf" else 100. def get_trafo_values(trafo_df, par): diff --git a/pandapower/converter/pandamodels/to_pm.py b/pandapower/converter/pandamodels/to_pm.py index 81add6fcc..5787a1c20 100644 --- a/pandapower/converter/pandamodels/to_pm.py +++ b/pandapower/converter/pandamodels/to_pm.py @@ -151,6 +151,7 @@ def convert_to_pm_structure(net, opf_flow_lim="S", from_time_step=None, to_time_ ppci = build_ne_branch(net, ppci) net["_ppc_opf"] = ppci pm = ppc_to_pm(net, ppci) + # todo: somewhere here should RATE_A be converted to 0., because only PowerModels uses 0 as no limits (pypower opf converts the zero to inf) pm = add_pm_options(pm, net) pm = add_params_to_pm(net, pm) if from_time_step is not None and to_time_step is not None: diff --git a/pandapower/pypower/printpf.py b/pandapower/pypower/printpf.py index d6a8e6573..8a0d9cec0 100644 --- a/pandapower/pypower/printpf.py +++ b/pandapower/pypower/printpf.py @@ -659,7 +659,7 @@ def printpf(baseMVA, bus=None, gen=None, branch=None, f=None, success=None, fd.write('\n================================================================================') fd.write('\n| Branch Flow Constraints |') fd.write('\n================================================================================') - fd.write('\nBrnch From "From" End Limit "To" End To') + fd.write('\nBranch From "From" End Limit "To" End To') fd.write(strg) fd.write('\n----- ----- ------- -------- -------- -------- ------- -----') for i in range(nl): diff --git a/pandapower/test/opf/test_pandamodels_runpm.py b/pandapower/test/opf/test_pandamodels_runpm.py index 364a288c7..ff874a2ac 100644 --- a/pandapower/test/opf/test_pandamodels_runpm.py +++ b/pandapower/test/opf/test_pandamodels_runpm.py @@ -286,8 +286,6 @@ def test_pwl(): @pytest.mark.slow @pytest.mark.skipif(not julia_installed, reason="requires julia installation") -@pytest.mark.xfail -# todo: this test will not work like this def test_without_ext_grid(): net = pp.create_empty_network() diff --git a/pandapower/timeseries/ts_runpp.py b/pandapower/timeseries/ts_runpp.py index 431f02920..708eeb3b6 100644 --- a/pandapower/timeseries/ts_runpp.py +++ b/pandapower/timeseries/ts_runpp.py @@ -1,6 +1,4 @@ import inspect -import collections -import functools import numpy as np from numpy import complex128, zeros @@ -12,20 +10,17 @@ from pandapower.control.controller.trafo_control import TrafoController from pandapower.auxiliary import _clean_up from pandapower.build_branch import _calc_trafo_parameter, _calc_trafo3w_parameter -from pandapower.build_bus import _calc_pq_elements_and_add_on_ppc, \ - _calc_shunts_and_add_on_ppc -from pandapower.pypower.idx_brch import F_BUS, T_BUS, BR_R, BR_X, BR_B, TAP, SHIFT, BR_STATUS, RATE_A +from pandapower.build_bus import _calc_pq_elements_and_add_on_ppc from pandapower.pypower.idx_bus import PD, QD from pandapower.pd2ppc import _pd2ppc from pandapower.pypower.idx_bus_dc import DC_PD -from pandapower.pypower.makeSbus import _get_Sbus, _get_Cg, makeSbus +from pandapower.pypower.makeSbus import makeSbus from pandapower.pf.pfsoln_numba import pfsoln as pfsoln_full, pf_solution_single_slack from pandapower.powerflow import LoadflowNotConverged, _add_auxiliary_elements from pandapower.results import _copy_results_ppci_to_ppc, _extract_results, _get_aranged_lookup from pandapower.results_branch import _get_branch_flows, _get_line_results, _get_trafo3w_results, _get_trafo_results -from pandapower.results_bus import write_pq_results_to_element, _get_bus_v_results, _get_bus_results, _get_bus_dc_results +from pandapower.results_bus import _get_bus_results, _get_bus_dc_results from pandapower.results_gen import _get_gen_results -from pandapower.timeseries.output_writer import OutputWriter try: import pandaplan.core.pplog as logging From 42e788b056a1e78d1a4b480e5cceaff1495a1000 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Tue, 3 Dec 2024 14:58:04 +0100 Subject: [PATCH 098/135] some improvements regards pandas 2.2.2 futurewarnings and error raises --- pandapower/auxiliary.py | 15 ++++++++++++++- pandapower/contingency/contingency.py | 23 ++++++++++++++--------- pandapower/create.py | 18 +++++++++++++----- pandapower/sql_io.py | 3 ++- pyproject.toml | 2 +- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/pandapower/auxiliary.py b/pandapower/auxiliary.py index b8e73d26f..623696cb3 100644 --- a/pandapower/auxiliary.py +++ b/pandapower/auxiliary.py @@ -36,6 +36,8 @@ from geojson import loads, GeoJSON import numpy as np import pandas as pd +from pandas.api.types import is_numeric_dtype, is_string_dtype, is_object_dtype +# from pandas.api.types import is_integer_dtype, is_float_dtype import scipy as sp import numbers from packaging.version import Version @@ -451,12 +453,23 @@ def element_types_to_ets(element_types=None): ser2 = pd.Series(ser1.index, index=list(ser1)) if element_types is None: return ser2 - elif isinstance(ets, str): + elif isinstance(element_types, str): return ser2.at[element_types] else: return list(ser2.loc[element_types]) +def empty_defaults_per_dtype(dtype): + if is_numeric_dtype(dtype): + return np.nan + elif is_string_dtype(dtype): + return "" + elif is_object_dtype(dtype): + return None + else: + raise NotImplementedError(f"{dtype=} is not implemented in _empty_defaults()") + + def _preserve_dtypes(df, dtypes): for item, dtype in list(dtypes.items()): if df.dtypes.at[item] != dtype: diff --git a/pandapower/contingency/contingency.py b/pandapower/contingency/contingency.py index 6b6b51d7e..3184c6f2d 100644 --- a/pandapower/contingency/contingency.py +++ b/pandapower/contingency/contingency.py @@ -136,15 +136,19 @@ def run_contingency(net, nminus1_cases, pf_options=None, pf_options_nminus1=None def run_contingency_ls2g(net, nminus1_cases, contingency_evaluation_function=pp.runpp, **kwargs): """ - Execute contingency analysis using the lightsim2grid library. This works much faster than using pandapower. - Limitation: the results for branch flows are valid only for the "from_bus" of lines and "hv_bus" of transformers. - This can lead to a small difference to the results using pandapower. - The results are written in pandapower results tables. - Make sure that the N-1 cases do not lead to isolated grid, otherwise results with pandapower and this function will - be different. Reason: pandapower selects a different gen as slack if the grid becomes isolated, but - lightsim2grid would simply return nan as results for such a contingency situation. - WARNING: continuous bus indices, 0-start, are required! - This function can be passed through to pandapower.timeseries.run_timeseries as the run_control_fct argument. + Execute contingency analysis using the lightsim2grid library. This works much faster than using + pandapower. + This function can be passed through to pandapower.timeseries.run_timeseries as the + run_control_fct argument. + + **Limitation:** the results for branch flows are valid only for the "from_bus" of lines and + "hv_bus" of transformers. This can lead to a small difference to the results using pandapower. + The results are written in pandapower results tables. Make sure that the N-1 cases do not lead + to isolated grid, otherwise results with pandapower and this function will + be different. Reason: pandapower selects a different gen as slack if the grid becomes isolated, + but lightsim2grid would simply return nan as results for such a contingency situation. + + **WARNING:** continuous bus indices, 0-start, are required! The results will written for the following additional variables: table res_bus with columns "max_vm_pu", "min_vm_pu", @@ -159,6 +163,7 @@ def run_contingency_ls2g(net, nminus1_cases, contingency_evaluation_function=pp. INPUT ---------- + **net** - pandapowerNet **nminus1_cases** - dict describes all N-1 cases, e.g. {"line": {"index": [1, 2, 3]}, "trafo": {"index": [0]}} diff --git a/pandapower/create.py b/pandapower/create.py index 7e98c7954..60f65f78d 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -14,7 +14,8 @@ from pandas.api.types import is_object_dtype from pandapower._version import __version__, __format_version__ -from pandapower.auxiliary import pandapowerNet, get_free_id, _preserve_dtypes, ensure_iterability +from pandapower.auxiliary import pandapowerNet, get_free_id, _preserve_dtypes, ensure_iterability, \ + empty_defaults_per_dtype from pandapower.results import reset_results from pandapower.std_types import add_basic_std_types, load_std_type import numpy as np @@ -4929,8 +4930,8 @@ def create_impedance(net, from_bus, to_bus, rft_pu, xft_pu, sn_mva, rtf_pu=None, UserWarning If required impedance parameters are missing. """ - - + + index = _get_index_with_check(net, "impedance", index) _check_branch_element(net, "Impedance", index, from_bus, to_bus) @@ -5111,7 +5112,7 @@ def create_impedances(net, from_buses, to_buses, rft_pu, xft_pu, sn_mva, rtf_pu= entries = dict(zip(columns, values)) _set_multiple_entries(net, "impedance", index, **entries, **kwargs) - + if rft0_pu is not None: _set_value_if_not_nan(net, index, rft0_pu, "rft0_pu", "impedance") _set_value_if_not_nan(net, index, xft0_pu, "xft0_pu", "impedance") @@ -6074,7 +6075,14 @@ def check_entry(val): net[table][col] = val # extend the table by the frame we just created - net[table] = pd.concat([net[table], dd[dd.columns[~dd.isnull().all()]]], sort=False) + if len(net[table]): + net[table] = pd.concat([net[table], dd[dd.columns[~dd.isnull().all()]]], sort=False) + else: + dd_columns = dd.columns[~dd.isnull().all()] + complete_columns = list(net[table].columns)+list(dd_columns.difference(net[table].columns)) + empty_dict = {key: empty_defaults_per_dtype(dtype) for key, dtype in net[table][net[ + table].columns.difference(dd_columns)].dtypes.to_dict().items()} + net[table] = dd[dd_columns].assign(**empty_dict)[complete_columns] # and preserve dtypes if preserve_dtypes: diff --git a/pandapower/sql_io.py b/pandapower/sql_io.py index 1175809f7..198ca8a5d 100644 --- a/pandapower/sql_io.py +++ b/pandapower/sql_io.py @@ -87,7 +87,8 @@ def download_sql_table(cursor, table_name, **id_columns): colnames = [desc[0] for desc in cursor.description] table = cursor.fetchall() df = pd.DataFrame(table, columns=colnames) - df = df.fillna(np.nan) + with pd.option_context('future.no_silent_downcasting', True): + df = df.fillna(np.nan).infer_objects() index_name = f"{table_name.split('.')[-1]}_id" if index_name in df.columns: df = df.set_index(index_name) diff --git a/pyproject.toml b/pyproject.toml index 98b08cea7..ab4cc262b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ all = [ "numpydoc", "sphinx", "sphinx_rtd_theme", "sphinx-pyproject", "plotly>=3.1.1", "matplotlib", "igraph", "geopandas>=1.0", "pytest~=8.1", "pytest-xdist", "nbmake", - "ortools", "numba>=0.25", "lightsim2grid==0.9.0", + "ortools", "numba>=0.25", "lightsim2grid>=0.9", "xlsxwriter", "openpyxl", "cryptography", "psycopg2", "matpowercaseframes", "power-grid-model-io" From 2244f465422a4718187d94ccc28fa3df2f2fe2a6 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 4 Dec 2024 13:18:37 +0100 Subject: [PATCH 099/135] avoid duplicated keys in kwargs and pf_options in run_contingency() --- CHANGELOG.rst | 1 + pandapower/contingency/contingency.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9fc265b8d..1cca5083f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ Change Log - [CHANGED] Capitalize first letter of columns busbar_id, busbar_name and substation_id in bus df for cim converter - [FIXED] Do not modify pandas options when importing pandapower - [ADDED] improved lightsim2grid documentation including compatibitliy issues +- [FIXED] avoid duplicated keys in kwargs and pf_options in run_contingency() - [FIXED] cim2pp: set default xml encoding to None to avoid error after changing to lxml [2.14.11] - 2024-07-08 diff --git a/pandapower/contingency/contingency.py b/pandapower/contingency/contingency.py index 6b6b51d7e..3088e9b94 100644 --- a/pandapower/contingency/contingency.py +++ b/pandapower/contingency/contingency.py @@ -82,10 +82,17 @@ def run_contingency(net, nminus1_cases, pf_options=None, pf_options_nminus1=None # set up the dict for results and relevant variables # ".get" in case the options have been set in pp.set_user_pf_options: raise_errors = kwargs.get("raise_errors", False) - if "recycle" in kwargs: kwargs["recycle"] = False # so that we can be sure it doesn't happen - if pf_options is None: pf_options = net.user_pf_options.get("pf_options", net.user_pf_options) - if pf_options_nminus1 is None: pf_options_nminus1 = net.user_pf_options.get("pf_options_nminus1", - net.user_pf_options) + if "recycle" in kwargs: + kwargs["recycle"] = False # so that we can be sure it doesn't happen + if pf_options is None: + pf_options = net.user_pf_options.get("pf_options", net.user_pf_options) + if pf_options_nminus1 is None: + pf_options_nminus1 = net.user_pf_options.get("pf_options_nminus1", net.user_pf_options) + if kwargs is not None: + # avoid duplicate passing keys to contingency_evaluation_function() + pf_options = {key: val for key, val in pf_options.items() if key not in kwargs.keys()} + pf_options_nminus1 = {key: val for key, val in pf_options_nminus1.items() if key not in + kwargs.keys()} contingency_results = {element: {"index": net[element].index.values} for element in ("bus", "line", "trafo", "trafo3w") if len(net[element]) > 0} @@ -173,7 +180,8 @@ def run_contingency_ls2g(net, nminus1_cases, contingency_evaluation_function=pp. n_bus = len(net.bus) last_bus = net.bus.index[-1] if net.bus.index[0] != 0 or last_bus != n_bus - 1 or sum(net.bus.index) != last_bus * n_bus / 2: - raise UserWarning("bus index must be continuous and start with 0 (use pandapower.create_continuous_bus_index)") + raise UserWarning("bus index must be continuous and start with 0 " + "(use pandapower.create_continuous_bus_index)") contingency_evaluation_function(net, **kwargs) trafo_flag = False From 8cb0864a3265009983029f9d20c4fa435ae3320f Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Thu, 5 Dec 2024 12:43:31 +0100 Subject: [PATCH 100/135] required standard type parameters are made available by function required_std_type_parameters() --- CHANGELOG.rst | 1 + pandapower/std_types.py | 42 ++++++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9fc265b8d..1aa84751b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,7 @@ Change Log - [FIXED] Increasing geojson precision as the default precision might cause problems with pandahub - [ADDED] Add GeographicalRegion and SubGeographicalRegion names and ids to bus df in cim converter - [CHANGED] Capitalize first letter of columns busbar_id, busbar_name and substation_id in bus df for cim converter +- [CHANGED] required standard type parameters are made available by function :code:`required_std_type_parameters()` - [FIXED] Do not modify pandas options when importing pandapower - [ADDED] improved lightsim2grid documentation including compatibitliy issues - [FIXED] cim2pp: set default xml encoding to None to avoid error after changing to lxml diff --git a/pandapower/std_types.py b/pandapower/std_types.py index 612504653..3687b94e7 100644 --- a/pandapower/std_types.py +++ b/pandapower/std_types.py @@ -15,6 +15,26 @@ logger = logging.getLogger(__name__) +def required_std_type_parameters(element="line"): + if element == "line": + required = ["c_nf_per_km", "r_ohm_per_km", "x_ohm_per_km", "max_i_ka"] + elif element == "line_dc": + required = ["r_ohm_per_km","max_i_ka"] + elif element == "trafo": + required = ["sn_mva", "vn_hv_kv", "vn_lv_kv", "vk_percent", "vkr_percent", + "pfe_kw", "i0_percent", "shift_degree"] + elif element == "trafo3w": + required = ["sn_hv_mva", "sn_mv_mva", "sn_lv_mva", "vn_hv_kv", "vn_mv_kv", "vn_lv_kv", + "vk_hv_percent", "vk_mv_percent", "vk_lv_percent", "vkr_hv_percent", + "vkr_mv_percent", "vkr_lv_percent", "pfe_kw", "i0_percent", "shift_mv_degree", + "shift_lv_degree"] + elif element == "fuse": + required = ["fuse_type", "i_rated_a"] + else: + raise ValueError("Unkown element type %s" % element) + return required + + def create_std_type(net, data, name, element="line", overwrite=True, check_required=True): """ Creates type data in the type database. The parameters that are used for @@ -79,25 +99,9 @@ def create_std_type(net, data, name, element="line", overwrite=True, check_requi raise UserWarning("type data has to be given as a dictionary of parameters") if check_required: - if element == "line": - required = ["c_nf_per_km", "r_ohm_per_km", "x_ohm_per_km", "max_i_ka"] - elif element == "line_dc": - required = ["r_ohm_per_km","max_i_ka"] - elif element == "trafo": - required = ["sn_mva", "vn_hv_kv", "vn_lv_kv", "vk_percent", "vkr_percent", - "pfe_kw", "i0_percent", "shift_degree"] - elif element == "trafo3w": - required = ["sn_hv_mva", "sn_mv_mva", "sn_lv_mva", "vn_hv_kv", "vn_mv_kv", "vn_lv_kv", - "vk_hv_percent", "vk_mv_percent", "vk_lv_percent", "vkr_hv_percent", - "vkr_mv_percent", "vkr_lv_percent", "pfe_kw", "i0_percent", "shift_mv_degree", - "shift_lv_degree"] - elif element == "fuse": - required = ["fuse_type", "i_rated_a"] - else: - raise ValueError("Unkown element type %s" % element) - for par in required: - if par not in data: - raise UserWarning("%s is required as %s type parameter" % (par, element)) + missing = [par for par in required_std_type_parameters(element) if par not in data] + if len(missing): + raise UserWarning("%s are required as %s type parameters." % (missing, element)) library = net.std_types[element] if overwrite or not (name in library): library.update({name: data}) From 1687cf5e36fd04a81d8b29eb12040663515afb8d Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Fri, 6 Dec 2024 13:14:06 +0100 Subject: [PATCH 101/135] fixed copy-paste error in contingency results "max_limit_nminus1" and "min_limit_nminus1" --- CHANGELOG.rst | 1 + pandapower/contingency/contingency.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9fc265b8d..35ddfb6d8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,7 @@ Change Log - [ADDED] Add GeographicalRegion and SubGeographicalRegion names and ids to bus df in cim converter - [CHANGED] Capitalize first letter of columns busbar_id, busbar_name and substation_id in bus df for cim converter - [FIXED] Do not modify pandas options when importing pandapower +- [FIXED] fixed copy-paste error in contingency results "max_limit_nminus1" and "min_limit_nminus1" - [ADDED] improved lightsim2grid documentation including compatibitliy issues - [FIXED] cim2pp: set default xml encoding to None to avoid error after changing to lxml diff --git a/pandapower/contingency/contingency.py b/pandapower/contingency/contingency.py index 6b6b51d7e..edcaf2398 100644 --- a/pandapower/contingency/contingency.py +++ b/pandapower/contingency/contingency.py @@ -362,11 +362,11 @@ def get_element_limits(net): "max_limit": net.bus.loc[bus_index, "max_vm_pu"].values, "min_limit": net.bus.loc[bus_index, "min_vm_pu"].values, "max_limit_nminus1": - net.line.loc[bus_index, "max_vm_nminus1_pu"].values + net.bus.loc[bus_index, "max_vm_nminus1_pu"].values if "max_vm_nminus1_pu" in net.bus.columns else net.bus.loc[bus_index, "max_vm_pu"].values, "min_limit_nminus1": - net.line.loc[bus_index, "min_vm_nminus1_pu"].values + net.bus.loc[bus_index, "min_vm_nminus1_pu"].values if "min_vm_nminus1_pu" in net.bus.columns else net.bus.loc[bus_index, "min_vm_pu"].values}}) From 08bd19a999e066cb6043bf564716d8f19eed2da2 Mon Sep 17 00:00:00 2001 From: Joschka Thurner Date: Fri, 6 Dec 2024 14:38:14 +0100 Subject: [PATCH 102/135] fix sphinx build test --- .github/workflows/github_test_action.yml | 16 ++++------------ doc/requirements.txt | 3 --- pyproject.toml | 4 ++-- 3 files changed, 6 insertions(+), 17 deletions(-) delete mode 100644 doc/requirements.txt diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index a816bec26..c6b0d76f8 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -301,21 +301,13 @@ jobs: python -m pytest -W error --nbmake -n=auto --nbmake-timeout=900 "./tutorials" docs_check: + name: Sphinx docs check runs-on: ubuntu-latest - strategy: - matrix: - python-version: [ '3.9' ] steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Check docs for Python ${{ matrix.python-version }} - uses: e2nIEE/sphinx-action@master + - name: Check sphinx build + uses: ammaraskar/sphinx-action@7.4.7 with: - pre-build-command: "apt update && apt upgrade -y && apt install -y build-essential gfortran cmake pkg-config libopenblas-dev; - python -m pip install --upgrade pip; - python -m pip install .[docs];" + pre-build-command: "python -m pip install uv && uv pip install .[docs] --system --link-mode=copy" build-command: "sphinx-build -b html . _build -W" docs-folder: "doc/" diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index 2433ee980..000000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx>=5.3.0 -sphinx_rtd_theme>=1.1.1 -numpydoc>=1.5.0 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 98b08cea7..f3906d484 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ Download = "https://pypi.org/project/pandapower/#files" Changelog = "https://github.com/e2nIEE/pandapower/blob/develop/CHANGELOG.rst" [project.optional-dependencies] -docs = ["numpydoc", "matplotlib", "sphinx", "sphinx_rtd_theme", "sphinx-pyproject"] +docs = ["numpydoc>=1.5.0", "matplotlib", "sphinx>=5.3.0", "sphinx_rtd_theme>=1.1.1", "sphinx-pyproject"] plotting = ["plotly>=3.1.1", "matplotlib", "igraph", "geopandas>=1.0"] test = ["pytest~=8.1", "pytest-xdist", "nbmake"] performance = ["ortools", "numba>=0.25", "lightsim2grid==0.9.0"] @@ -68,7 +68,7 @@ converter = ["matpowercaseframes"] pgm = ["power-grid-model-io"] control = ["shapely"] all = [ - "numpydoc", "sphinx", "sphinx_rtd_theme", "sphinx-pyproject", + "numpydoc>=1.5.0", "sphinx>=5.3.0", "sphinx_rtd_theme>=1.1.1", "sphinx-pyproject", "plotly>=3.1.1", "matplotlib", "igraph", "geopandas>=1.0", "pytest~=8.1", "pytest-xdist", "nbmake", "ortools", "numba>=0.25", "lightsim2grid==0.9.0", From 02376b06f9ba346f8d6bcec768797308251fd15a Mon Sep 17 00:00:00 2001 From: Joschka Thurner Date: Mon, 9 Dec 2024 09:29:26 +0100 Subject: [PATCH 103/135] switch to uv in github test workflow --- .github/workflows/github_test_action.yml | 151 +++++++++++------------ 1 file changed, 70 insertions(+), 81 deletions(-) diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index a816bec26..08bc69abf 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -25,31 +25,31 @@ jobs: steps: - uses: actions/checkout@v4 #- uses: julia-actions/setup-julia@v1.5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a #v4.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install pytest pytest-split - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - python -m pip install .["all"] - if ${{ matrix.python-version == '3.9' }}; then python -m pip install pypower; fi - if ${{ matrix.python-version != '3.9' }}; then python -m pip install numba; fi - if ${{ matrix.python-version == '3.10' }}; then python -m pip install lightsim2grid; fi + uv sync --all-extras + if [ -f requirements.txt ]; then uv pip install -r requirements.txt; fi + uv pip install pytest-split + if ${{ matrix.python-version == '3.9' }}; then uv pip install pypower; fi + if ${{ matrix.python-version != '3.9' }}; then uv pip install numba; fi + if ${{ matrix.python-version == '3.10' }}; then uv pip install lightsim2grid; fi - name: List of installed packages run: | - python -m pip list + uv pip list - name: Test with pytest if: ${{ matrix.python-version != '3.9' }} run: | - python -m pytest --splits 2 --group ${{ matrix.group }} + uv run pytest --splits 2 --group ${{ matrix.group }} - name: Test with pytest, Codecov and Coverage if: ${{ matrix.python-version == '3.9' }} run: | - python -m pip install pytest-cov - python -m pytest -n=auto --cov=./ --cov-report=xml --splits 2 --group ${{ matrix.group }} + uv pip install pytest-cov + uv run pytest -n=auto --cov=./ --cov-report=xml --splits 2 --group ${{ matrix.group }} cp ./coverage.xml ./coverage-${{ matrix.group }}.xml - name: Upload coverage as artifact if: ${{ matrix.python-version == '3.9' }} @@ -71,29 +71,27 @@ jobs: steps: - uses: actions/checkout@v4 #- uses: julia-actions/setup-julia@v1.5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a #v4.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install pytest pytest-split - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - python -m pip install .["all"] - python -m pip install pypower + uv sync --all-extras + uv pip install pypower pytest-split + if [ -f requirements.txt ]; then uv pip install -r requirements.txt; fi - name: Install Julia run: | ./.install_julia.sh 1.10.4 - python -m pip install julia - python ./.install_pycall.py + uv pip install julia + uv run python ./.install_pycall.py - name: List of installed packages run: | - python -m pip list + uv pip list - name: Test with pytest, Codecov and Coverage run: | - python -m pip install pytest-cov - python -m pytest -n=auto --cov=./ --cov-report=xml --splits 2 --group ${{ matrix.group }} + uv pip install pytest-cov + uv run pytest -n=auto --cov=./ --cov-report=xml --splits 2 --group ${{ matrix.group }} cp ./coverage.xml ./coverage-${{ matrix.group }}.xml upload-coverage: @@ -137,22 +135,21 @@ jobs: group: [ 1, 2 ] steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a #v4.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install pytest pytest-split - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - python -m pip install .["all"] + uv sync --all-extras + uv pip install pytest-split + if [ -f requirements.txt ]; then uv pip install -r requirements.txt; fi - name: List of installed packages run: | - python -m pip list + uv pip list - name: Test with pytest run: | - python -m pytest -W error --splits 2 --group ${{ matrix.group }} + uv run pytest -W error --splits 2 --group ${{ matrix.group }} relying: # packages that rely on pandapower runs-on: ubuntu-latest @@ -161,31 +158,30 @@ jobs: python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a #v4.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install pytest setuptools - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - python -m pip install . - python -m pip install matplotlib - if ${{ matrix.python-version != '3.9' }}; then python -m pip install numba; fi + uv sync + uv pip install setuptools + if [ -f requirements.txt ]; then uv pip install -r requirements.txt; fi + uv pip install matplotlib + if ${{ matrix.python-version != '3.9' }}; then uv pip install numba; fi - name: Install pandapipes and simbench run: | - python -m pip install git+https://github.com/e2nIEE/pandapipes@develop#egg=pandapipes - python -m pip install git+https://github.com/e2nIEE/simbench@develop#egg=simbench + uv pip install git+https://github.com/e2nIEE/pandapipes@develop#egg=pandapipes + uv pip install git+https://github.com/e2nIEE/simbench@develop#egg=simbench - name: List of installed packages run: | - python -m pip list + uv pip list - name: Test pandapipes run: | - python -c 'from pandapipes import pp_dir; import pytest; import sys; ec = pytest.main([pp_dir]); sys.exit(ec)' + uv run python -c 'from pandapipes import pp_dir; import pytest; import sys; ec = pytest.main([pp_dir]); sys.exit(ec)' - name: Test simbench run: | - python -c 'from simbench import sb_dir; import pytest; import sys; ec = pytest.main([sb_dir]); sys.exit(ec)' + uv run python -c 'from simbench import sb_dir; import pytest; import sys; ec = pytest.main([sb_dir]); sys.exit(ec)' linting: # run flake8 and check for errors @@ -197,28 +193,26 @@ jobs: python-version: ['3.10'] steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a #v4.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install flake8 - python -m pip install . - python -m pip install matplotlib + uv sync + uv pip install flake8 matplotlib - name: List of installed packages run: | - python -m pip list + uv pip list - name: Lint with flake8 (sytax errors and undefined names) continue-on-error: true run: | # stop the build if there are Python syntax errors or undefined names (omitted by exit-zero) - flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics + uv run flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics - name: Lint with flake8 (all errors and warnings) run: | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + uv run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics postgresql: # for the one test to cover postgresql @@ -228,17 +222,16 @@ jobs: python-version: ['3.12'] steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a #v4.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install .[test,fileio] + uv sync --extra test --extra fileio - name: List of installed packages run: | - python -m pip list + uv pip list - name: Create PostgreSQL database run: | sudo systemctl start postgresql.service @@ -249,56 +242,52 @@ jobs: PGPASSWORD=secret psql --username=test_user --host=localhost --list sandbox - name: Test pandapower File I/O run: | - python -c "import os; import json; from pandapower import pp_dir; conn_data={'host': 'localhost', 'user': 'test_user', 'database': 'sandbox', 'password': 'secret', 'schema': 'test_schema'}; fp = open(os.path.join(pp_dir, 'test', 'test_files', 'postgresql_connect_data.json'), 'w'); json.dump(conn_data, fp); fp.close()" - python -c 'from pandapower import pp_dir; import pytest; import sys; import os; ec = pytest.main([os.path.join(pp_dir,"test","api","test_sql_io.py")]); sys.exit(ec)' + uv run python -c "import os; import json; from pandapower import pp_dir; conn_data={'host': 'localhost', 'user': 'test_user', 'database': 'sandbox', 'password': 'secret', 'schema': 'test_schema'}; fp = open(os.path.join(pp_dir, 'test', 'test_files', 'postgresql_connect_data.json'), 'w'); json.dump(conn_data, fp); fp.close()" + uv run python -c 'from pandapower import pp_dir; import pytest; import sys; import os; ec = pytest.main([os.path.join(pp_dir,"test","api","test_sql_io.py")]); sys.exit(ec)' tutorial_tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a #v4.2.0 with: python-version: '3.11' - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install pytest nbmake pytest-xdist igraph numba seaborn + uv sync --all-extras + uv pip install seaborn ./.install_julia.sh 1.10.4 - python -m pip install julia - python ./.install_pycall.py - python -m pip install jupyter - python -m pip install .["all"] + uv pip install julia seaborn jupyter + uv run python ./.install_pycall.py - name: List all installed packages run: | - python -m pip list + uv pip list - name: Test with pytest # Careful when copying this command. The PYTHONPATH setup is Linux specific syntax. run: | - python -m pytest --nbmake -n=auto --nbmake-timeout=900 "./tutorials" + uv run pytest --nbmake -n=auto --nbmake-timeout=900 "./tutorials" tutorial_warnings_tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a #v4.2.0 with: python-version: '3.11' - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install .[all] - python -m pip install pytest nbmake pytest-xdist igraph numba seaborn + uv sync --all-extras ./.install_julia.sh 1.10.4 - python -m pip install julia - python ./.install_pycall.py + uv pip install julia seaborn + uv run python ./.install_pycall.py - name: List all installed packages run: | - python -m pip list + uv pip list - name: Test with pytest run: | - python -m pytest -W error --nbmake -n=auto --nbmake-timeout=900 "./tutorials" + uv run pytest -W error --nbmake -n=auto --nbmake-timeout=900 "./tutorials" docs_check: runs-on: ubuntu-latest From d567b5e95c1de139a239bf7867216b93c76b66a4 Mon Sep 17 00:00:00 2001 From: Joschka Thurner Date: Mon, 9 Dec 2024 09:45:07 +0100 Subject: [PATCH 104/135] exclude .venv from flake --- .github/workflows/github_test_action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index 08bc69abf..37757eb59 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -208,11 +208,11 @@ jobs: continue-on-error: true run: | # stop the build if there are Python syntax errors or undefined names (omitted by exit-zero) - uv run flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics + uv run flake8 . --exclude .venv --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics - name: Lint with flake8 (all errors and warnings) run: | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - uv run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + uv run flake8 . --exclude .venv --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics postgresql: # for the one test to cover postgresql From 679d25b34d57e2d3cfe784f788ec57504c886acf Mon Sep 17 00:00:00 2001 From: Joschka Thurner Date: Mon, 9 Dec 2024 10:37:09 +0100 Subject: [PATCH 105/135] install test group in relying tests --- .github/workflows/github_test_action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index 37757eb59..fec7b0fef 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -164,7 +164,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - uv sync + uv sync --extra test uv pip install setuptools if [ -f requirements.txt ]; then uv pip install -r requirements.txt; fi uv pip install matplotlib From a9cfba2e4c71dc227507afeb6b1f8680f706edc3 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 11 Dec 2024 17:45:04 +0100 Subject: [PATCH 106/135] toolbox replace functions (e.g. gen replacement by sgens): improved result table implementation and added profiles consideration --- CHANGELOG.rst | 1 + pandapower/toolbox/grid_modification.py | 153 +++++++++++++++--------- 2 files changed, 99 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9fc265b8d..2d6b52f5d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,7 @@ Change Log - [FIXED] Increasing geojson precision as the default precision might cause problems with pandahub - [ADDED] Add GeographicalRegion and SubGeographicalRegion names and ids to bus df in cim converter - [CHANGED] Capitalize first letter of columns busbar_id, busbar_name and substation_id in bus df for cim converter +- [CHANGED] toolbox replace functions (e.g. gen replacement by sgens): improved result table implementation and added profiles consideration - [FIXED] Do not modify pandas options when importing pandapower - [ADDED] improved lightsim2grid documentation including compatibitliy issues - [FIXED] cim2pp: set default xml encoding to None to avoid error after changing to lxml diff --git a/pandapower/toolbox/grid_modification.py b/pandapower/toolbox/grid_modification.py index 9652b2671..ff8bd2cec 100644 --- a/pandapower/toolbox/grid_modification.py +++ b/pandapower/toolbox/grid_modification.py @@ -1099,26 +1099,14 @@ def replace_impedance_by_line(net, index=None, only_valid_replace=True, max_i_ka _replace_group_member_element_type(net, index, "impedance", new_index, "line", detach_from_gr=False) drop_elements_simple(net, "impedance", index) - return new_index + # --- result data + _adapt_result_tables_in_replace_functions(net, "impedance", index, "line", new_index) -def _replace_group_member_element_type( - net, old_elements, old_element_type, new_elements, new_element_type, detach_from_gr=True): - assert not isinstance(old_element_type, set) - assert not isinstance(new_element_type, set) - old_elements = pd.Series(old_elements) - new_elements = pd.Series(new_elements) + # --- adapt profiles + _adapt_profiles_in_replace_functions(net, "impedance", index, "line", new_index) - check_unique_group_rows(net) - gr_et = net.group.loc[net.group.element_type == old_element_type] - for gr_index in gr_et.index: - isin = old_elements.isin(gr_et.at[gr_index, "element_index"]) - if any(isin): - attach_to_group(net, gr_index, new_element_type, [new_elements.loc[isin].tolist()], - reference_columns=gr_et.at[gr_index, "reference_column"]) - if detach_from_gr: - detach_from_groups(net, old_element_type, old_elements) # sometimes done afterwarts when - # dropping the old elements + return new_index def replace_line_by_impedance(net, index=None, sn_mva=None, only_valid_replace=True): @@ -1180,6 +1168,13 @@ def replace_line_by_impedance(net, index=None, sn_mva=None, only_valid_replace=T _replace_group_member_element_type(net, index, "line", new_index, "impedance", detach_from_gr=False) drop_lines(net, index) + + # --- result data + _adapt_result_tables_in_replace_functions(net, "line", index, "impedance", new_index) + + # --- adapt profiles + _adapt_profiles_in_replace_functions(net, "line", index, "impedance", new_index) + return new_index @@ -1264,12 +1259,11 @@ def replace_ext_grid_by_gen(net, ext_grids=None, gen_indices=None, slack=False, new_idx, net[table]["element"].dtypes) # --- result data - if net.res_ext_grid.shape[0]: - in_res = pd.Series(ext_grids).isin(net["res_ext_grid"].index).values - to_add = net.res_ext_grid.loc[pd.Index(ext_grids)[in_res]] - to_add.index = pd.Index(new_idx)[in_res] - net.res_gen = pd.concat([net.res_gen, to_add], sort=True) - net.res_ext_grid = net.res_ext_grid.drop(pd.Index(ext_grids)[in_res]) + _adapt_result_tables_in_replace_functions(net, "ext_grid", ext_grids, "gen", new_idx) + + # --- adapt profiles + _adapt_profiles_in_replace_functions(net, "ext_grid", ext_grids, "gen", new_idx) + return new_idx @@ -1346,12 +1340,11 @@ def replace_gen_by_ext_grid(net, gens=None, ext_grid_indices=None, cols_to_keep= net[table].loc[to_change, "element"] = new_idx # --- result data - if net.res_gen.shape[0]: - in_res = pd.Series(gens).isin(net["res_gen"].index).values - to_add = net.res_gen.loc[pd.Index(gens)[in_res]] - to_add.index = pd.Index(new_idx)[in_res] - net.res_ext_grid = pd.concat([net.res_ext_grid, to_add], sort=True) - net.res_gen = net.res_gen.drop(pd.Index(gens)[in_res]) + _adapt_result_tables_in_replace_functions(net, "gen", gens, "ext_grid", new_idx) + + # --- adapt profiles + _adapt_profiles_in_replace_functions(net, "gen", gens, "ext_grid", new_idx) + return new_idx @@ -1431,12 +1424,11 @@ def replace_gen_by_sgen(net, gens=None, sgen_indices=None, cols_to_keep=None, new_idx, net[table]["element"].dtypes) # --- result data - if net.res_gen.shape[0]: - in_res = pd.Series(gens).isin(net["res_gen"].index).values - to_add = net.res_gen.loc[pd.Index(gens)[in_res]] - to_add.index = pd.Index(new_idx)[in_res] - net.res_sgen = pd.concat([net.res_sgen, to_add], sort=True) - net.res_gen = net.res_gen.drop(pd.Index(gens)[in_res]) + _adapt_result_tables_in_replace_functions(net, "gen", gens, "sgen", new_idx) + + # --- adapt profiles + _adapt_profiles_in_replace_functions(net, "gen", gens, "sgen", new_idx) + return new_idx @@ -1531,13 +1523,12 @@ def replace_sgen_by_gen(net, sgens=None, gen_indices=None, cols_to_keep=None, net[table].loc[to_change, "et"] = "gen" net[table].loc[to_change, "element"] = new_idx - # --- result data - if net.res_sgen.shape[0]: - in_res = pd.Series(sgens).isin(net["res_sgen"].index).values - to_add = net.res_sgen.loc[pd.Index(sgens)[in_res]] - to_add.index = pd.Index(new_idx)[in_res] - net.res_gen = pd.concat([net.res_gen, to_add], sort=True) - net.res_sgen = net.res_sgen.drop(pd.Index(sgens)[in_res]) + # --- adapt result data + _adapt_result_tables_in_replace_functions(net, "sgen", sgens, "gen", new_idx) + + # --- adapt profiles + _adapt_profiles_in_replace_functions(net, "sgen", sgens, "gen", new_idx) + return new_idx @@ -1652,14 +1643,14 @@ def replace_pq_elmtype(net, old_element_type, new_element_type, old_indices=None net[table].loc[to_change, "element"] = np.array( new_idx, net[table]["element"].dtypes) - # --- result data - if net["res_" + old_element_type].shape[0]: - in_res = pd.Series(old_indices).isin(net["res_" + old_element_type].index).values - to_add = net["res_" + old_element_type].loc[pd.Index(old_indices)[in_res]] - to_add.index = pd.Index(new_idx)[in_res] - net["res_" + new_element_type] = pd.concat([net["res_" + new_element_type], to_add], sort=True) - net["res_" + old_element_type] = net["res_" + old_element_type].drop(pd.Index(old_indices)[in_res]) + _adapt_result_tables_in_replace_functions( + net, old_element_type, old_indices, new_element_type, new_idx) + + # --- adapt profiles + _adapt_profiles_in_replace_functions( + net, old_element_type, old_indices, new_element_type, new_idx) + return new_idx @@ -1722,8 +1713,7 @@ def replace_ward_by_internal_elements(net, wards=None, log_level="warning"): drop_elements_simple(net, "ward", wards) -def replace_xward_by_internal_elements(net, xwards=None, set_xward_bus_limits=False, - log_level="warning"): +def replace_xward_by_internal_elements(net, xwards=None, set_xward_bus_limits=False): """ Replaces xward by loads, shunts, impedance and generators @@ -1735,9 +1725,6 @@ def replace_xward_by_internal_elements(net, xwards=None, set_xward_bus_limits=Fa indices of xwards which should be replaced. If None, all xwards are replaced, by default None set_xward_bus_limits : bool, optional if True, the buses internal in xwards get vm limits from the connected buses - log_level : str, optional - logging level of the message which element types of net2 got reindexed elements. Options - are, for example "debug", "info", "warning", "error", or None, by default "info" Returns ------- @@ -1775,7 +1762,7 @@ def replace_xward_by_internal_elements(net, xwards=None, set_xward_bus_limits=Fa # --- result data if net.res_xward.shape[0]: log_to_level("Implementations to move xward results to new internal elements are missing.", - logger, log_level) + logger, "info") net.res_xward = net.res_xward.drop(xwards) # --- drop replaced wards @@ -1813,6 +1800,9 @@ def replace_xward_by_ward(net, index=None, drop=True): The function ensures that the group membership and associated element type of the replaced elements are updated accordingly. """ + # TODO: parameter `drop` is implemented only to this replace function. needed if yes why not + # implementing at the other replace functions? + index = list(ensure_iterability(index)) if index is not None else list(net.impedance.index) new_index = [] @@ -1829,3 +1819,56 @@ def replace_xward_by_ward(net, index=None, drop=True): else: net.xward.loc[index, "in_service"] = False return new_index + + +def _replace_group_member_element_type( + net, old_elements, old_element_type, new_elements, new_element_type, detach_from_gr=True): + assert not isinstance(old_element_type, set) + assert not isinstance(new_element_type, set) + old_elements = pd.Series(old_elements) + new_elements = pd.Series(new_elements) + + check_unique_group_rows(net) + gr_et = net.group.loc[net.group.element_type == old_element_type] + for gr_index in gr_et.index: + isin = old_elements.isin(gr_et.at[gr_index, "element_index"]) + if any(isin): + attach_to_group(net, gr_index, new_element_type, [new_elements.loc[isin].tolist()], + reference_columns=gr_et.at[gr_index, "reference_column"]) + if detach_from_gr: + detach_from_groups(net, old_element_type, old_elements) # sometimes done afterwarts when + # dropping the old elements + + +def _adapt_result_tables_in_replace_functions( + net, element_type_old, element_index_old, element_type_new, element_index_new): + et_old, et_new = "res_" + element_type_old, "res_" + element_type_new + idx_old, idx_new = pd.Index(element_index_old), pd.Index(element_index_new) + if net[et_old].shape[0]: + in_res = pd.Series(et_old).isin(net[et_old].index).values + to_add = net[et_old].loc[idx_old[in_res]] + to_add.index = idx_new[in_res] + net[et_new] = pd.concat([net[et_new], to_add], sort=True) + net[et_old] = net[et_old].drop(idx_old[in_res]) + + +def _adapt_profiles_in_replace_functions( + net, element_type_old, element_index_old, element_type_new, element_index_new + ): + if "profiles" not in net or not isinstance(net.profiles, dict): + return + et_old, et_new = element_type_old, element_type_new + idx_old, idx_new = pd.Index(element_index_old), pd.Index(element_index_new) + + keys_old = [key for key in net.profiles.keys() if ( + key.startswith(f"{et_old}.") or key.startswith(f"res_{et_old}."))] + for key_old in keys_old: + key_new = key_old.replace(et_old, et_new) + in_prof = pd.Series(idx_old).isin(net.profiles[key_old].columns).values + to_add = net.profiles[key_old].loc[:, idx_old[in_prof]] + to_add.columns = idx_new[in_prof] + if key_new in net.profiles.keys(): + net.profiles[key_new] = pd.concat([net.profiles[key_new], to_add], sort=True) + else: + net.profiles[key_new] = to_add + net.profiles[key_old] = net.profiles[key_old].drop(idx_old[in_prof], axis=1) From 119eb517accd3b9a4b19d8f2991a3011eb1e7c50 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Wed, 11 Dec 2024 17:59:54 +0100 Subject: [PATCH 107/135] merge --- CHANGELOG.rst | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 885dfa779..cbf1d08a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,21 +12,6 @@ Change Log - [FIXED] fixed copy-paste error in contingency results "max_limit_nminus1" and "min_limit_nminus1" - [ADDED] improved lightsim2grid documentation including compatibitliy issues - [FIXED] cim2pp: set default xml encoding to None to avoid error after changing to lxml - -[2.14.11] - 2024-07-08 -------------------------------- -- [FIXED] Lightsim2grid version - -[2.14.10] - 2024-07-08 -------------------------------- -- [FIXED] geopandas version - -[2.14.9] - 2024-06-25 -------------------------------- -- [FIXED] scipy version - -[upcoming release] - 2024-..-.. -------------------------------- - [FIXED] PandaModels OPF with 'bus_dc' key errors - [FIXED] julia tests - [FIXED] copy array element to standard python scalar @@ -103,6 +88,18 @@ Change Log - [ADDED] converter for European EHV grid data from JAO, the "Single Allocation Platform (SAP) for all European Transmission System Operators (TSOs) that operate in accordance to EU legislation" - [ADDED] cim2pp converter: Using lxml to parse XML files (better performance) +[2.14.11] - 2024-07-08 +------------------------------- +- [FIXED] Lightsim2grid version + +[2.14.10] - 2024-07-08 +------------------------------- +- [FIXED] geopandas version + +[2.14.9] - 2024-06-25 +------------------------------- +- [FIXED] scipy version + [2.14.7] - 2024-06-14 ------------------------------- - [ADDED] added PathPatch TextPatch and Affine2D imports needed for ward and xward patches From 05eec6bd57c057d94edc558b562947fa2418447e Mon Sep 17 00:00:00 2001 From: marcopau Date: Thu, 12 Dec 2024 06:49:58 +0100 Subject: [PATCH 108/135] Creation of zero inj meas and ward results --- pandapower/create.py | 30 ++++++++++++------------ pandapower/estimation/ppc_conversion.py | 20 ++++++++-------- pandapower/estimation/results.py | 31 +++++++++++++++---------- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/pandapower/create.py b/pandapower/create.py index 7e98c7954..990ff28f0 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -5484,21 +5484,21 @@ def create_measurement(net, meas_type, element_type, value, std_dev, element, si raise UserWarning( "Voltage measurements can only be placed at buses, not at {}".format(element_type)) - if check_existing: - if side is None: - existing = net.measurement[(net.measurement.measurement_type == meas_type) & - (net.measurement.element_type == element_type) & - (net.measurement.element == element) & - (pd.isnull(net.measurement.side))].index - else: - existing = net.measurement[(net.measurement.measurement_type == meas_type) & - (net.measurement.element_type == element_type) & - (net.measurement.element == element) & - (net.measurement.side == side)].index - if len(existing) == 1: - index = existing[0] - elif len(existing) > 1: - raise UserWarning("More than one measurement of this type exists") + # if check_existing: + # if side is None: + # existing = net.measurement[(net.measurement.measurement_type == meas_type) & + # (net.measurement.element_type == element_type) & + # (net.measurement.element == element) & + # (pd.isnull(net.measurement.side))].index + # else: + # existing = net.measurement[(net.measurement.measurement_type == meas_type) & + # (net.measurement.element_type == element_type) & + # (net.measurement.element == element) & + # (net.measurement.side == side)].index + # if len(existing) == 1: + # index = existing[0] + # elif len(existing) > 1: + # raise UserWarning("More than one measurement of this type exists") columns = ["name", "measurement_type", "element_type", "element", "value", "std_dev", "side"] values = [name, meas_type.lower(), element_type, element, value, std_dev, side] diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index 45d37160b..c1cc5194a 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -180,15 +180,15 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): if meas_type in ("p", "q"): # Convert injection reference to consumption reference (P, Q) this_meas.value *= -1 - unique_bus_positions = np.unique(bus_positions) - if len(unique_bus_positions) < len(bus_positions): - std_logger.debug("P,Q Measurement duplication will be automatically merged!") - for bus in unique_bus_positions: - this_meas_on_bus = this_meas.iloc[np.argwhere(bus_positions == bus).ravel(), :] - bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["VALUE"]] = this_meas_on_bus.value.sum() - bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["STD"]] = this_meas_on_bus.std_dev.max() - bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["IDX"]] = this_meas_on_bus.index[0] - continue + unique_bus_positions = np.unique(bus_positions) + if len(unique_bus_positions) < len(bus_positions): + std_logger.debug("P,Q Measurement duplication will be automatically merged!") + for bus in unique_bus_positions: + this_meas_on_bus = this_meas.iloc[np.argwhere(bus_positions == bus).ravel(), :] + bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["VALUE"]] = this_meas_on_bus.value.sum() + bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["STD"]] = this_meas_on_bus.std_dev.max() + bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["IDX"]] = this_meas_on_bus.index[0] + continue bus_append[bus_positions, BUS_MEAS_PPCI_IX[meas_type]["VALUE"]] = this_meas.value.values bus_append[bus_positions, BUS_MEAS_PPCI_IX[meas_type]["STD"]] = this_meas.std_dev.values @@ -294,7 +294,7 @@ def _add_zero_injection(net, ppci, bus_append, zero_injection): if isinstance(zero_injection, str): if zero_injection == 'auto': # identify bus without elements and pq measurements as zero injection - zero_inj_bus_mask = (ppci["bus"][:, 1] == 1) & (ppci["bus"][:, 2:6] == 0).all(axis=1) & \ + zero_inj_bus_mask = (ppci["bus"][:, 1] == 1) & (ppci["bus"][:, 2:4] == 0).all(axis=1) & \ np.isnan(bus_append[:, P:(Q_STD + 1)]).all(axis=1) bus_append[zero_inj_bus_mask, ZERO_INJ_FLAG] = True elif zero_injection != "aux_bus": diff --git a/pandapower/estimation/results.py b/pandapower/estimation/results.py index d48bcc199..3e91e0dc4 100644 --- a/pandapower/estimation/results.py +++ b/pandapower/estimation/results.py @@ -51,18 +51,25 @@ def _extract_result_ppci_to_pp(net, ppc, ppci): net.res_bus_est.loc[merged_bus_idx, 'p_mw'] = 0 net.res_bus_est.loc[merged_bus_idx, "q_mvar"] = 0 # add shunt power because the injection at the node computed via Ybus is only the extra injection on top of the shunt - if ~net["shunt"].empty: - for i in range(net["shunt"].shape[0]): - bus = net.shunt.bus.iloc[i] - Sn = complex(net.shunt.p_mw.iloc[i],net.shunt.q_mvar.iloc[i])*net.shunt.step.iloc[i] - Ysh = Sn / (net.shunt.vn_kv.iloc[i]**2) - V = net["res_bus_est"].loc[bus,"vm_pu"]*net["bus"].loc[bus,"vn_kv"] - Sinj = Ysh*(V**2) - net["res_bus_est"].loc[bus,"p_mw"] += Sinj.real - net["res_bus_est"].loc[bus,"q_mvar"] += Sinj.imag - net["res_shunt_est"].loc[net["shunt"].loc[:,"bus"]==bus,"p_mw"] = Sinj.real - net["res_shunt_est"].loc[net["shunt"].loc[:,"bus"]==bus,"q_mvar"] = Sinj.imag - net["res_shunt_est"].loc[net["shunt"].loc[:,"bus"]==bus,"vm_pu"] = net["res_bus_est"].loc[bus,"vm_pu"] + for element in ["shunt", "ward", "xward"]: + if ~net[element].empty: + for i in range(net[element].shape[0]): + bus = net[element].bus.iloc[i] + if element == "shunt": + Sn = complex(net[element].p_mw.iloc[i],net[element].q_mvar.iloc[i])*net[element].step.iloc[i] + Ysh = Sn / (net[element].vn_kv.iloc[i]**2) + else: + Sn = complex(net[element].pz_mw.iloc[i],net[element].qz_mvar.iloc[i]) + Ysh = Sn / (net.bus.loc[bus,"vn_kv"]**2) + V = net["res_bus_est"].loc[bus,"vm_pu"]*net["bus"].loc[bus,"vn_kv"] + Sinj = Ysh*(V**2) + net["res_bus_est"].loc[bus,"p_mw"] += Sinj.real + net["res_bus_est"].loc[bus,"q_mvar"] += Sinj.imag + if element == "shunt": + element_res_est = "res_" + element + "_est" + net[element_res_est].loc[net[element].loc[:,"bus"]==bus,"p_mw"] = Sinj.real + net[element_res_est].loc[net[element].loc[:,"bus"]==bus,"q_mvar"] = Sinj.imag + net[element_res_est].loc[net[element].loc[:,"bus"]==bus,"vm_pu"] = net["res_bus_est"].loc[bus,"vm_pu"] return net From e8cfab8ca140fdb478ed2a720dae3cb398869a3d Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:07:55 +0100 Subject: [PATCH 109/135] fixed typo from removing ga wrapper in GetAttribute fixed geodata being passed as string to create_bus function --- .../powerfactory/pp_import_functions.py | 200 +++++++++--------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/pandapower/converter/powerfactory/pp_import_functions.py b/pandapower/converter/powerfactory/pp_import_functions.py index e6297797c..8db54c564 100644 --- a/pandapower/converter/powerfactory/pp_import_functions.py +++ b/pandapower/converter/powerfactory/pp_import_functions.py @@ -351,12 +351,12 @@ def add_additional_attributes(item, net, element, element_id, attr_list=None, at obj = item for a in attr.split('.'): if hasattr(obj, 'HasAttribute') and obj.HasAttribute(a): - obj = obj.GetAttributes(a) + obj = obj.GetAttribute(a) if obj is not None and isinstance(obj, str): net[element].loc[element_id, attr_dict[attr]] = obj elif item.HasAttribute(attr): - chr_name = item.GetAttributes(attr) + chr_name = item.GetAttribute(attr) if chr_name is not None: if isinstance(chr_name, (str, numbers.Number)): net[element].loc[element_id, attr_dict[attr]] = chr_name @@ -371,13 +371,13 @@ def add_additional_attributes(item, net, element, element_id, attr_list=None, at def create_bus(net, item, flag_graphics, is_unbalanced): # add geo data if flag_graphics == 'GPS': - x = item.GetAttributes('e:GPSlon') - y = item.GetAttributes('e:GPSlat') + x = item.GetAttribute('e:GPSlon') + y = item.GetAttribute('e:GPSlat') elif flag_graphics == 'graphic objects': graphic_object = get_graphic_object(item) if graphic_object: - x = graphic_object.GetAttributes('rCenterX') - y = graphic_object.GetAttributes('rCenterY') + x = graphic_object.GetAttribute('rCenterX') + y = graphic_object.GetAttribute('rCenterY') # add gr coord data else: x, y = 0, 0 @@ -397,7 +397,7 @@ def create_bus(net, item, flag_graphics, is_unbalanced): 'vn_kv': item.uknom, 'in_service': not bool(item.outserv), 'type': usage[item.iUsage], - 'geodata': geojson.dumps(geojson.Point((x, y))), + 'geodata': (x, y), } system_type = {0: "ac", 1: "dc", 2: "ac/bi"}[item.systype] @@ -477,7 +477,7 @@ def get_pf_bus_results(net, item, bid, is_unbalanced, system_type): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(0): - res = item.GetAttributes(res_var_pf) + res = item.GetAttribute(res_var_pf) # dc bus voltage can be negative: net[bus_type].at[bid, res_var_pp] = np.abs(res) if "vm_pu" in res_var_pp else res @@ -485,7 +485,7 @@ def get_pf_bus_results(net, item, bid, is_unbalanced, system_type): # # This one deletes all the results :( # # Don't use it # def find_bus_index_in_net(item, net=None): -# foreign_key = int(item.GetAttributes('for_name')) +# foreign_key = int(item.GetAttribute('for_name')) # return foreign_key @@ -494,32 +494,32 @@ def get_pf_bus_results(net, item, bid, is_unbalanced, system_type): # def find_bus_index_in_net(item, net): # usage = ["b", "m", "n"] # # to be sure that the bus is the correct one -# name = item.GetAttributes('loc_name') -# bus_type = usage[item.GetAttributes('iUsage')] +# name = item.GetAttribute('loc_name') +# bus_type = usage[item.GetAttribute('iUsage')] # logger.debug('looking for bus <%s> in net' % name) # # if item.HasAttribute('cpSubstat'): -# substat = item.GetAttributes('cpSubstat') +# substat = item.GetAttribute('cpSubstat') # if substat is not None: -# descr = substat.GetAttributes('loc_name') +# descr = substat.GetAttribute('loc_name') # logger.debug('bus <%s> has substat, descr is <%s>' % (name, descr)) # else: # # omg so ugly :( -# descr = item.GetAttributes('desc') +# descr = item.GetAttribute('desc') # descr = descr[0] if len(descr) > 0 else "" # logger.debug('substat is none, descr of bus <%s> is <%s>' % (name, descr)) # else: -# descr = item.GetAttributes('desc') +# descr = item.GetAttribute('desc') # descr = descr[0] if len(descr) > 0 else "" # logger.debug('no attribute "substat", descr of bus <%s> is <%s>' % (name, descr)) # # try: -# zone = item.GetAttributes('Grid') -# zone_name = zone.GetAttributes('loc_name').split('.ElmNet')[0] +# zone = item.GetAttribute('Grid') +# zone_name = zone.GetAttribute('loc_name').split('.ElmNet')[0] # logger.debug('zone "Grid" found: <%s>' % zone_name) # except: -# zone = item.GetAttributes('cpGrid') -# zone_name = zone.GetAttributes('loc_name').split('.ElmNet')[0] +# zone = item.GetAttribute('cpGrid') +# zone_name = zone.GetAttribute('loc_name').split('.ElmNet')[0] # logger.debug('zone "cpGrid" found: <%s>' % zone_name) # # temp_df_a = net.bus[net.bus.zone == zone_name] @@ -573,12 +573,12 @@ def get_connection_nodes(net, item, num_nodes): item, pf_class)) if pf_class == "ElmTr2": - v.append(item.GetAttributes('t:utrn_h')) - v.append(item.GetAttributes('t:utrn_l')) + v.append(item.GetAttribute('t:utrn_h')) + v.append(item.GetAttribute('t:utrn_l')) elif pf_class == "ElmTr3": - v.append(item.GetAttributes('t:utrn3_h')) - v.append(item.GetAttributes('t:utrn3_m')) - v.append(item.GetAttributes('t:utrn3_l')) + v.append(item.GetAttribute('t:utrn3_h')) + v.append(item.GetAttribute('t:utrn3_m')) + v.append(item.GetAttribute('t:utrn3_l')) else: v = [net[table].vn_kv.at[existing_bus] for _ in buses] @@ -920,7 +920,7 @@ def create_line_sections(net, item_list, line, bus1, bus2, coords, parallel, is_ item_list.sort(key=lambda x: x.index) # to ensure they are in correct order if line.HasResults(-1): # -1 for 'c' results (whatever that is...) - line_loading = line.GetAttributes('c:loading') + line_loading = line.GetAttribute('c:loading') else: line_loading = np.nan @@ -1122,7 +1122,7 @@ def get_pf_line_results(net, item, lid, is_unbalanced, ac): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(-1): # -1 for 'c' results (whatever that is...) - res = item.GetAttributes(res_var_pf) + res = item.GetAttribute(res_var_pf) net[line_type].at[lid, res_var_pp] = res @@ -1254,8 +1254,8 @@ def create_ext_net(net, item, pv_as_slack, is_unbalanced): # if item.HasResults(0): # 'm' results... # # sm:r, sm:i don't work... # logger.debug('<%s> has results' % name) - # net['res_' + elm].at[xid, "pf_p"] = item.GetAttributes('m:P:bus1') - # net['res_' + elm].at[xid, "pf_q"] = item.GetAttributes('m:Q:bus1') + # net['res_' + elm].at[xid, "pf_p"] = item.GetAttribute('m:P:bus1') + # net['res_' + elm].at[xid, "pf_q"] = item.GetAttribute('m:Q:bus1') # else: # net['res_' + elm].at[xid, "pf_p"] = np.nan # net['res_' + elm].at[xid, "pf_q"] = np.nan @@ -1292,7 +1292,7 @@ def get_pf_ext_grid_results(net, item, xid, is_unbalanced): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(0): - res = item.GetAttributes(res_var_pf) + res = item.GetAttribute(res_var_pf) net[ext_grid_type].at[xid, res_var_pp] = res @@ -1382,11 +1382,11 @@ def ask_load_params(item, pf_variable_p_loads, dict_net, variables): if pf_variable_p_loads == 'm:P:bus1' and not item.HasResults(0): raise RuntimeError('load %s does not have results and is ignored' % item.loc_name) if 'p_mw' in variables: - params.p_mw = item.GetAttributes(pf_variable_p_loads) * multiplier + params.p_mw = item.GetAttribute(pf_variable_p_loads) * multiplier if 'q_mvar' in variables: - params.q_mvar = item.GetAttributes(map_power_var(pf_variable_p_loads, 'q')) * multiplier + params.q_mvar = item.GetAttribute(map_power_var(pf_variable_p_loads, 'q')) * multiplier if 'sn_mva' in variables: - params.sn_mva = item.GetAttributes(map_power_var(pf_variable_p_loads, 's')) * multiplier + params.sn_mva = item.GetAttribute(map_power_var(pf_variable_p_loads, 's')) * multiplier kap = -1 if item.pf_recap == 1 else 1 try: @@ -1414,17 +1414,17 @@ def ask_unbalanced_load_params(item, pf_variable_p_loads, dict_net, variables): if pf_variable_p_loads == 'm:P:bus1' and not item.HasResults(0): raise RuntimeError('load %s does not have results and is ignored' % item.loc_name) if 'p_mw' in variables: - params.p_a_mw = item.GetAttributes(pf_variable_p_loads + "r") - params.p_b_mw = item.GetAttributes(pf_variable_p_loads + "s") - params.p_c_mw = item.GetAttributes(pf_variable_p_loads + "t") + params.p_a_mw = item.GetAttribute(pf_variable_p_loads + "r") + params.p_b_mw = item.GetAttribute(pf_variable_p_loads + "s") + params.p_c_mw = item.GetAttribute(pf_variable_p_loads + "t") if 'q_mvar' in variables: - params.q_a_mvar = item.GetAttributes(map_power_var(pf_variable_p_loads, 'q') + "r") - params.q_b_mvar = item.GetAttributes(map_power_var(pf_variable_p_loads, 'q') + "s") - params.q_c_mvar = item.GetAttributes(map_power_var(pf_variable_p_loads, 'q') + "t") + params.q_a_mvar = item.GetAttribute(map_power_var(pf_variable_p_loads, 'q') + "r") + params.q_b_mvar = item.GetAttribute(map_power_var(pf_variable_p_loads, 'q') + "s") + params.q_c_mvar = item.GetAttribute(map_power_var(pf_variable_p_loads, 'q') + "t") if 'sn_mva' in variables: - params.sn_a_mva = item.GetAttributes(map_power_var(pf_variable_p_loads, 's') + "r") - params.sn_b_mva = item.GetAttributes(map_power_var(pf_variable_p_loads, 's') + "s") - params.sn_c_mva = item.GetAttributes(map_power_var(pf_variable_p_loads, 's') + "t") + params.sn_a_mva = item.GetAttribute(map_power_var(pf_variable_p_loads, 's') + "r") + params.sn_b_mva = item.GetAttribute(map_power_var(pf_variable_p_loads, 's') + "s") + params.sn_c_mva = item.GetAttribute(map_power_var(pf_variable_p_loads, 's') + "t") kap = -1 if item.pf_recap == 1 else 1 try: @@ -1679,7 +1679,7 @@ def create_load(net, item, pf_variable_p_loads, dict_net, is_unbalanced): ask = ask_unbalanced_load_params if is_unbalanced else ask_load_params if load_class == 'ElmLodlv': - # if bool(item.GetAttributes('e:cHasPartLod')): + # if bool(item.GetAttribute('e:cHasPartLod')): # logger.info('ElmLodlv %s has partial loads - skip' % item.loc_name) # part_lods = item.GetContents('*.ElmLodlvp') # logger.debug('%s' % part_lods) @@ -1714,8 +1714,8 @@ def create_load(net, item, pf_variable_p_loads, dict_net, is_unbalanced): i = 0 z = 0 for cc, ee in zip(("aP", "bP", "cP"), ("kpu0", "kpu1", "kpu")): - c = load_type.GetAttributes(cc) - e = load_type.GetAttributes(ee) + c = load_type.GetAttribute(cc) + e = load_type.GetAttribute(ee) if e == 1: i += 100 * c elif e == 2: @@ -1801,8 +1801,8 @@ def create_load(net, item, pf_variable_p_loads, dict_net, is_unbalanced): # if not is_unbalanced: # if item.HasResults(0): # 'm' results... # logger.debug('<%s> has results' % params.name) - # net["res_load"].at[ld, "pf_p"] = item.GetAttributes('m:P:bus1') - # net["res_load"].at[ld, "pf_q"] = item.GetAttributes('m:Q:bus1') + # net["res_load"].at[ld, "pf_p"] = item.GetAttribute('m:P:bus1') + # net["res_load"].at[ld, "pf_q"] = item.GetAttribute('m:Q:bus1') # else: # net["res_load"].at[ld, "pf_p"] = np.nan # net["res_load"].at[ld, "pf_q"] = np.nan @@ -1833,7 +1833,7 @@ def get_pf_load_results(net, item, ld, is_unbalanced): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(0): - res = item.GetAttributes(res_var_pf) * get_power_multiplier(item, res_var_pf) + res = item.GetAttribute(res_var_pf) * get_power_multiplier(item, res_var_pf) net[load_type].at[ld, res_var_pp] = res @@ -1843,11 +1843,11 @@ def ask_gen_params(item, pf_variable_p_gen, *vars): if pf_variable_p_gen == 'm:P:bus1' and not item.HasResults(0): raise RuntimeError('generator %s does not have results and is ignored' % item.loc_name) if 'p_mw' in vars: - params.p_mw = item.GetAttributes(pf_variable_p_gen) * multiplier + params.p_mw = item.GetAttribute(pf_variable_p_gen) * multiplier if 'q_mvar' in vars: - params.q_mvar = item.GetAttributes(map_power_var(pf_variable_p_gen, 'q')) * multiplier + params.q_mvar = item.GetAttribute(map_power_var(pf_variable_p_gen, 'q')) * multiplier if 'sn_mva' in vars: - params.sn_mva = item.GetAttributes(map_power_var(pf_variable_p_gen, 'sn')) * multiplier + params.sn_mva = item.GetAttribute(map_power_var(pf_variable_p_gen, 'sn')) * multiplier params.scaling = item.scale0 if pf_variable_p_gen == 'pgini' else 1 # p_mw = p_mw, q_mvar = q_mvar, scaling = scaling @@ -1863,25 +1863,25 @@ def ask_unbalanced_sgen_params(item, pf_variable_p_sgen, *vars): technology = item.phtech if technology in [0, 1]: # (0-1: 3PH) if 'p_mw' in vars: - params.p_a_mw = item.GetAttributes(pf_variable_p_sgen) / 3 - params.p_b_mw = item.GetAttributes(pf_variable_p_sgen) / 3 - params.p_c_mw = item.GetAttributes(pf_variable_p_sgen) / 3 + params.p_a_mw = item.GetAttribute(pf_variable_p_sgen) / 3 + params.p_b_mw = item.GetAttribute(pf_variable_p_sgen) / 3 + params.p_c_mw = item.GetAttribute(pf_variable_p_sgen) / 3 if 'q_mvar' in vars: - params.q_a_mvar = item.GetAttributes(map_power_var(pf_variable_p_sgen, 'q')) / 3 - params.q_b_mvar = item.GetAttributes(map_power_var(pf_variable_p_sgen, 'q')) / 3 - params.q_c_mvar = item.GetAttributes(map_power_var(pf_variable_p_sgen, 'q')) / 3 + params.q_a_mvar = item.GetAttribute(map_power_var(pf_variable_p_sgen, 'q')) / 3 + params.q_b_mvar = item.GetAttribute(map_power_var(pf_variable_p_sgen, 'q')) / 3 + params.q_c_mvar = item.GetAttribute(map_power_var(pf_variable_p_sgen, 'q')) / 3 elif technology in [2, 3, 4]: # (2-4: 1PH) if 'p_mw' in vars: - params.p_a_mw = item.GetAttributes(pf_variable_p_sgen) + params.p_a_mw = item.GetAttribute(pf_variable_p_sgen) params.p_b_mw = 0 params.p_c_mw = 0 if 'q_mvar' in vars: - params.q_a_mvar = item.GetAttributes(map_power_var(pf_variable_p_sgen, 'q')) + params.q_a_mvar = item.GetAttribute(map_power_var(pf_variable_p_sgen, 'q')) params.q_b_mvar = 0 params.q_c_mvar = 0 if 'sn_mva' in vars: - params.sn_mva = item.GetAttributes(map_power_var(pf_variable_p_sgen, 's')) + params.sn_mva = item.GetAttribute(map_power_var(pf_variable_p_sgen, 's')) params.scaling = item.scale0 if pf_variable_p_sgen == 'pgini' else 1 return params @@ -2059,7 +2059,7 @@ def get_pf_sgen_results(net, item, sg, is_unbalanced, element='sgen'): res = np.nan if item.HasResults(0): if res_var_pf is not None: - res = item.GetAttributes(res_var_pf) * get_power_multiplier(item, res_var_pf) + res = item.GetAttribute(res_var_pf) * get_power_multiplier(item, res_var_pf) else: res = np.nan net[sgen_type].at[sg, res_var_pp] = res @@ -2097,8 +2097,8 @@ def create_sgen_neg_load(net, item, pf_variable_p_loads, dict_net): if item.HasResults(0): # 'm' results... logger.debug('<%s> has results' % params.name) - net.res_sgen.at[sg, "pf_p"] = -item.GetAttributes('m:P:bus1') - net.res_sgen.at[sg, "pf_q"] = -item.GetAttributes('m:Q:bus1') + net.res_sgen.at[sg, "pf_p"] = -item.GetAttribute('m:P:bus1') + net.res_sgen.at[sg, "pf_q"] = -item.GetAttribute('m:Q:bus1') else: net.res_sgen.at[sg, "pf_p"] = np.nan net.res_sgen.at[sg, "pf_q"] = np.nan @@ -2197,8 +2197,8 @@ def create_sgen_sym(net, item, pv_as_slack, pf_variable_p_gen, dict_net, export_ if item.HasResults(0): # 'm' results... logger.debug('<%s> has results' % name) - net['res_' + element].at[sid, "pf_p"] = item.GetAttributes('m:P:bus1') * multiplier - net['res_' + element].at[sid, "pf_q"] = item.GetAttributes('m:Q:bus1') * multiplier + net['res_' + element].at[sid, "pf_p"] = item.GetAttribute('m:P:bus1') * multiplier + net['res_' + element].at[sid, "pf_q"] = item.GetAttribute('m:Q:bus1') * multiplier else: net['res_' + element].at[sid, "pf_p"] = np.nan net['res_' + element].at[sid, "pf_q"] = np.nan @@ -2212,10 +2212,10 @@ def create_sgen_asm(net, item, pf_variable_p_gen, dict_net): dict_net['global_parameters']['global_generation_scaling'] multiplier = get_power_multiplier(item, pf_variable_p_gen) - p_res = item.GetAttributes('pgini') * multiplier - q_res = item.GetAttributes('qgini') * multiplier + p_res = item.GetAttribute('pgini') * multiplier + q_res = item.GetAttribute('qgini') * multiplier if item.HasResults(0): - q_res = item.GetAttributes('m:Q:bus1') / global_scaling * multiplier + q_res = item.GetAttribute('m:Q:bus1') / global_scaling * multiplier else: logger.warning('reactive power for asynchronous generator is not exported properly ' '(advanced modelling of asynchronous generators not implemented)') @@ -2250,8 +2250,8 @@ def create_sgen_asm(net, item, pf_variable_p_gen, dict_net): attr_list=["sernum", "chr_name", "cpSite.loc_name"]) if item.HasResults(0): - net.res_sgen.at[sid, 'pf_p'] = item.GetAttributes('m:P:bus1') * multiplier - net.res_sgen.at[sid, 'pf_q'] = item.GetAttributes('m:Q:bus1') * multiplier + net.res_sgen.at[sid, 'pf_p'] = item.GetAttribute('m:P:bus1') * multiplier + net.res_sgen.at[sid, 'pf_q'] = item.GetAttribute('m:Q:bus1') * multiplier else: net.res_sgen.at[sid, 'pf_p'] = np.nan net.res_sgen.at[sid, 'pf_q'] = np.nan @@ -2362,11 +2362,11 @@ def create_trafo(net, item, export_controller=True, tap_opt="nntap", is_unbalanc tap_pos = np.nan if pf_type.itapch: if tap_opt == "nntap": - tap_pos = item.GetAttributes("nntap") + tap_pos = item.GetAttribute("nntap") logger.debug("got tap %f from nntap" % tap_pos) elif tap_opt == "c:nntap": - tap_pos = item.GetAttributes("c:nntap") + tap_pos = item.GetAttribute("c:nntap") logger.debug("got tap %f from c:nntap" % tap_pos) else: raise ValueError('could not read current tap position: tap_opt = %s' % tap_opt) @@ -2375,9 +2375,9 @@ def create_trafo(net, item, export_controller=True, tap_opt="nntap", is_unbalanc # In PowerFactory, if the first tap changer is absent, the second is also, even if the check was there if pf_type.itapch and pf_type.itapch2: if tap_opt == "nntap": - tap_pos2 = item.GetAttributes("nntap2") + tap_pos2 = item.GetAttribute("nntap2") elif tap_opt == "c:nntap": - tap_pos2 = item.GetAttributes("c:nntap2") + tap_pos2 = item.GetAttribute("c:nntap2") if std_type is not None: tid = pp.create_transformer(net, hv_bus=bus1, lv_bus=bus2, name=name, @@ -2503,7 +2503,7 @@ def get_pf_trafo_results(net, item, tid, is_unbalanced): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(-1): # -1 for 'c' results (whatever that is...) - res = item.GetAttributes(res_var_pf) + res = item.GetAttribute(res_var_pf) net[trafo_type].at[tid, res_var_pp] = res @@ -2572,21 +2572,21 @@ def create_trafo3w(net, item, tap_opt='nntap'): ts = ["h", "m", "l"][side[0]] # figure out current tap position if tap_opt == "nntap": - tap_pos = item.GetAttributes('n3tap_' + ts) + tap_pos = item.GetAttribute('n3tap_' + ts) logger.debug("got tap %f from n3tap" % tap_pos) elif tap_opt == "c:nntap": - tap_pos = item.GetAttributes("c:n3tap_" + ts) + tap_pos = item.GetAttribute("c:n3tap_" + ts) logger.debug("got tap %f from c:n3tap" % tap_pos) else: raise ValueError('could not read current tap position: tap_opt = %s' % tap_opt) params.update({ 'tap_side': ts + 'v', # hv, mv, lv - 'tap_step_percent': item.GetAttributes('t:du3tp_' + ts), - 'tap_step_degree': item.GetAttributes('t:ph3tr_' + ts), - 'tap_min': item.GetAttributes('t:n3tmn_' + ts), - 'tap_max': item.GetAttributes('t:n3tmx_' + ts), - 'tap_neutral': item.GetAttributes('t:n3tp0_' + ts), + 'tap_step_percent': item.GetAttribute('t:du3tp_' + ts), + 'tap_step_degree': item.GetAttribute('t:ph3tr_' + ts), + 'tap_min': item.GetAttribute('t:n3tmn_' + ts), + 'tap_max': item.GetAttribute('t:n3tmx_' + ts), + 'tap_neutral': item.GetAttribute('t:n3tp0_' + ts), 'tap_pos': tap_pos }) @@ -2600,7 +2600,7 @@ def create_trafo3w(net, item, tap_opt='nntap'): logger.debug('successfully created trafo3w from parameters: %d' % tid) # testen - # net.trafo3w.loc[tid, 'tap_step_degree'] = item.GetAttributes('t:ph3tr_h') + # net.trafo3w.loc[tid, 'tap_step_degree'] = item.GetAttribute('t:ph3tr_h') # adding switches # False if open, True if closed, None if no switch @@ -2615,7 +2615,7 @@ def create_trafo3w(net, item, tap_opt='nntap'): # assign loading from power factory results if item.HasResults(-1): # -1 for 'c' results (whatever that is...) logger.debug('trafo3w <%s> has results' % item.loc_name) - loading = item.GetAttributes('c:loading') + loading = item.GetAttribute('c:loading') net.res_trafo3w.at[tid, "pf_loading"] = loading else: net.res_trafo3w.at[tid, "pf_loading"] = np.nan @@ -2625,12 +2625,12 @@ def create_trafo3w(net, item, tap_opt='nntap'): if pf_type.itapzdep: x_points = (net.trafo3w.at[tid, "tap_min"], net.trafo3w.at[tid, "tap_neutral"], net.trafo3w.at[tid, "tap_max"]) for side in ("hv", "mv", "lv"): - vk_min = pf_type.GetAttributes(f"uktr3mn_{side[0]}") + vk_min = pf_type.GetAttribute(f"uktr3mn_{side[0]}") vk_neutral = net.trafo3w.at[tid, f"vk_{side}_percent"] - vk_max = pf_type.GetAttributes(f"uktr3mx_{side[0]}") - vkr_min = pf_type.GetAttributes(f"uktrr3mn_{side[0]}") + vk_max = pf_type.GetAttribute(f"uktr3mx_{side[0]}") + vkr_min = pf_type.GetAttribute(f"uktrr3mn_{side[0]}") vkr_neutral = net.trafo3w.at[tid, f"vkr_{side}_percent"] - vkr_max = pf_type.GetAttributes(f"uktrr3mx_{side[0]}") + vkr_max = pf_type.GetAttribute(f"uktrr3mx_{side[0]}") # todo zero-sequence parameters (must be implemented in build_branch first) pp.control.create_trafo_characteristics(net, trafotable="trafo3w", trafo_index=tid, variable=f"vk_{side}_percent", x_points=x_points, @@ -2686,7 +2686,7 @@ def create_coup(net, item, is_fuse=False): # # false approach, completely irrelevant # def create_switch(net, item): # switch_types = {"cbk": "CB", "sdc": "LBS", "swt": "LS", "dct": "DS"} -# name = item.GetAttributes('loc_name') +# name = item.GetAttribute('loc_name') # logger.debug('>> creating switch <%s>' % name) # # pf_bus1 = item.GetNode(0) @@ -2701,8 +2701,8 @@ def create_coup(net, item, is_fuse=False): # bus2 = find_bus_index_in_net(pf_bus2, net) # logger.debug('switch %s connects buses <%d> and <%d>' % (name, bus1, bus2)) # -# switch_is_closed = bool(item.GetAttributes('on_off')) -# switch_usage = switch_types[item.GetAttributes('aUsage')] +# switch_is_closed = bool(item.GetAttribute('on_off')) +# switch_usage = switch_types[item.GetAttribute('aUsage')] # # cd = pp.create_switch(net, name=name, bus=bus1, element=bus2, et='b', # closed=switch_is_closed, type=switch_usage) @@ -2789,8 +2789,8 @@ def calc_p_mw_and_q_mvar(r: float, x: float) -> tuple[float, float]: raise AttributeError(f"Shunt type {item.shtype} not valid: {item}") if item.HasResults(0): - net.res_shunt.loc[sid, 'pf_p'] = item.GetAttributes('m:P:bus1') * multiplier - net.res_shunt.loc[sid, 'pf_q'] = item.GetAttributes('m:Q:bus1') * multiplier + net.res_shunt.loc[sid, 'pf_p'] = item.GetAttribute('m:P:bus1') * multiplier + net.res_shunt.loc[sid, 'pf_q'] = item.GetAttribute('m:Q:bus1') * multiplier else: net.res_shunt.loc[sid, 'pf_p'] = np.nan net.res_shunt.loc[sid, 'pf_q'] = np.nan @@ -2895,8 +2895,8 @@ def create_vac(net, item): params['name'], item.itype)) if item.HasResults(0): # -1 for 'c' results (whatever that is...) - net['res_%s' % elm].at[xid, "pf_p"] = -item.GetAttributes('m:P:bus1') - net['res_%s' % elm].at[xid, "pf_q"] = -item.GetAttributes('m:Q:bus1') + net['res_%s' % elm].at[xid, "pf_p"] = -item.GetAttribute('m:P:bus1') + net['res_%s' % elm].at[xid, "pf_q"] = -item.GetAttribute('m:Q:bus1') else: net['res_%s' % elm].at[xid, "pf_p"] = np.nan net['res_%s' % elm].at[xid, "pf_q"] = np.nan @@ -2964,8 +2964,8 @@ def _get_vsc_control_modes(item, mono=True): f" {item.loc_name} not implemented: {c_m}") if item.HasResults(0): - p_set_dc = -item.GetAttributes(f"m:P:{dc_bus_str}") - q_set_ac = -item.GetAttributes("m:Q:busac") * scaling + p_set_dc = -item.GetAttribute(f"m:P:{dc_bus_str}") + q_set_ac = -item.GetAttribute("m:Q:busac") * scaling else: p_set_dc = -item.psetp * scaling # does not work - in PowerFactory, the P set-point relates to AC side q_set_ac = -item.qsetp * scaling @@ -3033,7 +3033,7 @@ def create_vscmono(net, item): for res_var_pp, res_var_pf in result_variables.items(): res = np.nan if item.HasResults(0): - res = item.GetAttributes(res_var_pf) + res = item.GetAttribute(res_var_pf) net.res_vsc.at[vid, res_var_pp] = -res @@ -3081,11 +3081,11 @@ def create_vsc(net, item): if item.HasResults(0): for res_var_pp, res_var_pf in result_variables.items(): - res = item.GetAttributes(res_var_pf) + res = item.GetAttribute(res_var_pf) net.res_vsc.at[vid_1, res_var_pp] = -res / 2 net.res_vsc.at[vid_2, res_var_pp] = -res / 2 - net.res_vsc.at[vid_1, "pf_p_dc_mw"] = -item.GetAttributes("m:P:busdm") - net.res_vsc.at[vid_2, "pf_p_dc_mw"] = -item.GetAttributes("m:P:busdp") + net.res_vsc.at[vid_1, "pf_p_dc_mw"] = -item.GetAttribute("m:P:busdm") + net.res_vsc.at[vid_2, "pf_p_dc_mw"] = -item.GetAttribute("m:P:busdp") else: net.res_vsc.loc[vid_1, ["pf_p_mw", "pf_q_mvar", "pf_p_dc_mw"]] = np.nan net.res_vsc.loc[vid_2, ["pf_p_mw", "pf_q_mvar", "pf_p_dc_mw"]] = np.nan From f1cf03defdf06cb302574d95a8ccfb39802ed85d Mon Sep 17 00:00:00 2001 From: David Heck Date: Tue, 17 Dec 2024 10:53:55 +0100 Subject: [PATCH 110/135] fix cim2pp dcline description --- CHANGELOG.rst | 1 + .../cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py | 2 +- pandapower/test/converter/test_from_cim.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cbf1d08a0..aacb23791 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,7 @@ Change Log [upcoming release] - 2024-..-.. ------------------------------- +- [FIXED] cim2pp add missing description to dcline - [ADDED] pandas series accessor for geo column - [FIXED] Increasing geojson precision as the default precision might cause problems with pandahub - [ADDED] converter for European EHV grid data from JAO, the "Single Allocation Platform (SAP) for all European Transmission System Operators (TSOs) that operate in accordance to EU legislation" diff --git a/pandapower/converter/cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py index bf1fc0867..998465ca5 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py @@ -40,7 +40,7 @@ def _prepare_dc_line_segments_cim16(self) -> pd.DataFrame: # now join with the terminals dc_line_segments = pd.merge(self.cimConverter.cim['eq']['DCLineSegment'], self.cimConverter.bus_merge, how='left', on='rdfId') - dc_line_segments = dc_line_segments[['rdfId', 'name', 'ConnectivityNode', 'sequenceNumber']] + dc_line_segments = dc_line_segments[['rdfId', 'name', 'description', 'ConnectivityNode', 'sequenceNumber']] dc_line_segments[sc['o_cl']] = 'DCLineSegment' # now dc_line_segments looks like: # rdfId name rdfId_Terminal connected ... diff --git a/pandapower/test/converter/test_from_cim.py b/pandapower/test/converter/test_from_cim.py index 48880f8b2..41dad1e7a 100644 --- a/pandapower/test/converter/test_from_cim.py +++ b/pandapower/test/converter/test_from_cim.py @@ -983,6 +983,7 @@ def test_fullgrid_dcline(fullgrid_v2): assert 2 == len(fullgrid_v2.dcline.index) element_0 = fullgrid_v2.dcline[fullgrid_v2.dcline['origin_id'] == '_70a3750c-6e8e-47bc-b1bf-5a568d9733f7'] assert 'LDC-1230816355' == element_0['name'].item() + assert 'LDC-1230816355' == element_0['description'].item() assert '_27d57afa-6c9d-4b06-93ea-8c88d14af8b1' == fullgrid_v2.bus.iloc[element_0['from_bus'].item()]['origin_id'] assert '_d3d9c515-2ddb-436a-bf17-2f8be2394de3' == fullgrid_v2.bus.iloc[int(element_0['to_bus'].item())]['origin_id'] assert 0.0 == element_0['p_mw'].item() From 2aa966ec3c4e70aff9ebd0b7af9cdc092800a0cd Mon Sep 17 00:00:00 2001 From: David Heck Date: Tue, 17 Dec 2024 11:46:14 +0100 Subject: [PATCH 111/135] force specific numba version --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f3906d484..0851e7cd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ Changelog = "https://github.com/e2nIEE/pandapower/blob/develop/CHANGELOG.rst" docs = ["numpydoc>=1.5.0", "matplotlib", "sphinx>=5.3.0", "sphinx_rtd_theme>=1.1.1", "sphinx-pyproject"] plotting = ["plotly>=3.1.1", "matplotlib", "igraph", "geopandas>=1.0"] test = ["pytest~=8.1", "pytest-xdist", "nbmake"] -performance = ["ortools", "numba>=0.25", "lightsim2grid==0.9.0"] +performance = ["ortools", "numba==0.60.0", "lightsim2grid==0.9.0"] fileio = ["xlsxwriter", "openpyxl", "cryptography", "geopandas>=1.0", "psycopg2"] converter = ["matpowercaseframes"] pgm = ["power-grid-model-io"] @@ -71,7 +71,7 @@ all = [ "numpydoc>=1.5.0", "sphinx>=5.3.0", "sphinx_rtd_theme>=1.1.1", "sphinx-pyproject", "plotly>=3.1.1", "matplotlib", "igraph", "geopandas>=1.0", "pytest~=8.1", "pytest-xdist", "nbmake", - "ortools", "numba>=0.25", "lightsim2grid==0.9.0", + "ortools", "numba==0.60.0", "lightsim2grid==0.9.0", "xlsxwriter", "openpyxl", "cryptography", "psycopg2", "matpowercaseframes", "power-grid-model-io" From f0473f207686f448c7bbf6d701950aa764d0250c Mon Sep 17 00:00:00 2001 From: marcopau Date: Wed, 18 Dec 2024 11:07:32 +0100 Subject: [PATCH 112/135] Fixes to handle measurement duplication --- pandapower/create.py | 32 +++++----- pandapower/estimation/ppc_conversion.py | 85 +++++++++++++++++++++---- pandapower/results_branch.py | 2 +- 3 files changed, 88 insertions(+), 31 deletions(-) diff --git a/pandapower/create.py b/pandapower/create.py index 990ff28f0..f22e7a89d 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -5413,7 +5413,7 @@ def create_dcline(net, from_bus, to_bus, p_mw, loss_percent, loss_mw, vm_from_pu def create_measurement(net, meas_type, element_type, value, std_dev, element, side=None, - check_existing=True, index=None, name=None, **kwargs): + check_existing=False, index=None, name=None, **kwargs): """ Creates a measurement, which is used by the estimation module. Possible types of measurements are: v, p, q, i, va, ia @@ -5484,21 +5484,21 @@ def create_measurement(net, meas_type, element_type, value, std_dev, element, si raise UserWarning( "Voltage measurements can only be placed at buses, not at {}".format(element_type)) - # if check_existing: - # if side is None: - # existing = net.measurement[(net.measurement.measurement_type == meas_type) & - # (net.measurement.element_type == element_type) & - # (net.measurement.element == element) & - # (pd.isnull(net.measurement.side))].index - # else: - # existing = net.measurement[(net.measurement.measurement_type == meas_type) & - # (net.measurement.element_type == element_type) & - # (net.measurement.element == element) & - # (net.measurement.side == side)].index - # if len(existing) == 1: - # index = existing[0] - # elif len(existing) > 1: - # raise UserWarning("More than one measurement of this type exists") + if check_existing: + if side is None: + existing = net.measurement[(net.measurement.measurement_type == meas_type) & + (net.measurement.element_type == element_type) & + (net.measurement.element == element) & + (pd.isnull(net.measurement.side))].index + else: + existing = net.measurement[(net.measurement.measurement_type == meas_type) & + (net.measurement.element_type == element_type) & + (net.measurement.element == element) & + (net.measurement.side == side)].index + if len(existing) == 1: + index = existing[0] + elif len(existing) > 1: + raise UserWarning("More than one measurement of this type exists") columns = ["name", "measurement_type", "element_type", "element", "value", "std_dev", "side"] values = [name, meas_type.lower(), element_type, element, value, std_dev, side] diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index c1cc5194a..79619336f 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -185,8 +185,22 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): std_logger.debug("P,Q Measurement duplication will be automatically merged!") for bus in unique_bus_positions: this_meas_on_bus = this_meas.iloc[np.argwhere(bus_positions == bus).ravel(), :] - bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["VALUE"]] = this_meas_on_bus.value.sum() - bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["STD"]] = this_meas_on_bus.std_dev.max() + element_positions = this_meas_on_bus["element"].values.astype(np.int64) + unique_element_positions = np.unique(element_positions) + if meas_type in ("v", "va"): + merged_value, merged_std_dev = merge_measurements(this_meas_on_bus.value, this_meas_on_bus.std_dev) + bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["VALUE"]] = merged_value + bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["STD"]] = merged_std_dev + bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["IDX"]] = this_meas_on_bus.index[0] + continue + for element in unique_element_positions: + this_meas_on_element = this_meas_on_bus.iloc[np.argwhere(element_positions == element).ravel(), :] + merged_value, merged_std_dev = merge_measurements(this_meas_on_element.value, this_meas_on_element.std_dev) + this_meas_on_bus.loc[this_meas_on_element.index[0], ["value", "std_dev"]] = [merged_value, merged_std_dev] + this_meas_on_bus.loc[this_meas_on_element.index[1:], ["value", "std_dev"]] = [0, 0] + sum_value, sum_std_dev = sum_measurements(this_meas_on_bus.value, this_meas_on_bus.std_dev) + bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["VALUE"]] = sum_value + bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["STD"]] = sum_std_dev bus_append[bus, BUS_MEAS_PPCI_IX[meas_type]["IDX"]] = this_meas_on_bus.index[0] continue @@ -197,12 +211,12 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): # add zero injection measurement and labels defined in parameter zero_injection bus_append = _add_zero_injection(net, ppci, bus_append, zero_injection) # add virtual measurements for artificial buses, which were created because - # of an open line switch. p/q are 0. and std dev is 1. (small value) + # of an open line switch. p/q are 0. and std dev is 1e-6. (small value) new_in_line_buses = np.setdiff1d(np.arange(ppci["bus"].shape[0]), map_bus[map_bus >= 0]) bus_append[new_in_line_buses, 2] = 0. - bus_append[new_in_line_buses, 3] = 1. + bus_append[new_in_line_buses, 3] = 1e-6 bus_append[new_in_line_buses, 4] = 0. - bus_append[new_in_line_buses, 5] = 1. + bus_append[new_in_line_buses, 5] = 1e-6 # add 15 columns to mpc[branch] for Im_from, Im_from std dev, Im_to, Im_to std dev, # P_from, P_from std dev, P_to, P_to std dev, Q_from, Q_from std dev, Q_to, Q_to std dev, @@ -224,15 +238,19 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): net[br_type][BR_SIDE[br_type][br_side]+"_bus"] [this_meas.element]).values] ix_side = br_map[meas_this_side.element.values].values - branch_append[ix_side, - BR_MEAS_PPCI_IX[(meas_type, br_side)]["VALUE"]] =\ - meas_this_side.value.values - branch_append[ix_side, - BR_MEAS_PPCI_IX[(meas_type, br_side)]["STD"]] =\ - meas_this_side.std_dev.values - branch_append[ix_side, - BR_MEAS_PPCI_IX[(meas_type, br_side)]["IDX"]] =\ - meas_this_side.index.values + unique_ix_side = np.unique(ix_side) + if len(unique_ix_side) < len(ix_side): + for branch in unique_ix_side: + this_meas_on_branch = meas_this_side.iloc[np.argwhere(ix_side == branch).ravel(), :] + merged_value, merged_std_dev = merge_measurements(this_meas_on_branch.value, this_meas_on_branch.std_dev) + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, br_side)]["VALUE"]] = merged_value + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, br_side)]["STD"]] = merged_std_dev + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, br_side)]["IDX"]] = this_meas_on_branch.index[0] + continue + + branch_append[ix_side, BR_MEAS_PPCI_IX[(meas_type, br_side)]["VALUE"]] = meas_this_side.value.values + branch_append[ix_side, BR_MEAS_PPCI_IX[(meas_type, br_side)]["STD"]] = meas_this_side.std_dev.values + branch_append[ix_side, BR_MEAS_PPCI_IX[(meas_type, br_side)]["IDX"]] = meas_this_side.index.values # Add measurements for trafo3w if map_trafo3w is not None: @@ -250,6 +268,31 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): ix_hv = [map_trafo3w[t]['hv'] for t in meas_hv.element.values] ix_mv = [map_trafo3w[t]['mv'] for t in meas_mv.element.values] ix_lv = [map_trafo3w[t]['lv'] for t in meas_lv.element.values] + unique_ix_hv = np.unique(ix_hv) + unique_ix_mv = np.unique(ix_mv) + unique_ix_lv = np.unique(ix_lv) + if len(unique_ix_hv) < len(ix_hv): + for branch in unique_ix_hv: + this_meas_on_branch = meas_hv.iloc[np.argwhere(ix_hv == branch).ravel(), :] + merged_value, merged_std_dev = merge_measurements(this_meas_on_branch.value, this_meas_on_branch.std_dev) + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "f")]["VALUE"]] = merged_value + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "f")]["STD"]] = merged_std_dev + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "f")]["IDX"]] = this_meas_on_branch.index[0] + if len(unique_ix_mv) < len(ix_mv): + for branch in unique_ix_mv: + this_meas_on_branch = meas_mv.iloc[np.argwhere(ix_mv == branch).ravel(), :] + merged_value, merged_std_dev = merge_measurements(this_meas_on_branch.value, this_meas_on_branch.std_dev) + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "t")]["VALUE"]] = merged_value + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "t")]["STD"]] = merged_std_dev + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "t")]["IDX"]] = this_meas_on_branch.index[0] + if len(unique_ix_lv) < len(ix_lv): + for branch in unique_ix_lv: + this_meas_on_branch = meas_lv.iloc[np.argwhere(ix_lv == branch).ravel(), :] + merged_value, merged_std_dev = merge_measurements(this_meas_on_branch.value, this_meas_on_branch.std_dev) + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "t")]["VALUE"]] = merged_value + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "t")]["STD"]] = merged_std_dev + branch_append[branch, BR_MEAS_PPCI_IX[(meas_type, "t")]["IDX"]] = this_meas_on_branch.index[0] + continue branch_append[ix_hv, BR_MEAS_PPCI_IX[(meas_type, "f")]["VALUE"]] = meas_hv.value.values branch_append[ix_hv, BR_MEAS_PPCI_IX[(meas_type, "f")]["STD"]] = meas_hv.std_dev.values branch_append[ix_hv, BR_MEAS_PPCI_IX[(meas_type, "f")]["IDX"]] = meas_hv.index.values @@ -272,6 +315,20 @@ def _add_measurements_to_ppci(net, ppci, zero_injection, algorithm): ppci["branch"][:, branch_cols: branch_cols + branch_cols_se] = branch_append return ppci +def merge_measurements(value, std_dev): + weight = np.divide(1, np.square(std_dev)) + merged_variance = np.divide(1, weight.sum()) + merged_std_dev = np.sqrt(merged_variance) + weighted_value = np.multiply(value, weight) + merged_value = np.multiply(weighted_value.sum(), merged_variance) + return merged_value, merged_std_dev + +def sum_measurements(value, std_dev): + sum_values = value.values.sum() + variance = np.square(std_dev.values) + sum_variance = variance.sum() + sum_std_dev = np.sqrt(sum_variance) + return sum_values, sum_std_dev def _add_zero_injection(net, ppci, bus_append, zero_injection): """ diff --git a/pandapower/results_branch.py b/pandapower/results_branch.py index 86cd59348..91f26bdfc 100644 --- a/pandapower/results_branch.py +++ b/pandapower/results_branch.py @@ -415,7 +415,7 @@ def _get_trafo_results_3ph(net, ppc0, ppc1, ppc2, I012_f, V012_f, I012_t, V012_t Iabc_sum += abs(I_branch_abc[:, x]) # Loads - load_index = np.where(net.asymmetric_load['bus'] == lv_bus)[0] + load_index = net.asymmetric_load.index[net.asymmetric_load['bus'] == lv_bus] if len(load_index > 0): S_load_abc = abs(np.array([ np.array(net.res_asymmetric_load_3ph['p_a_mw'][load_index] From 1f85bbfac8bd5f890202c9b31e3edf181dcc366b Mon Sep 17 00:00:00 2001 From: "IEECNB215543\\gbanerjee" Date: Wed, 18 Dec 2024 11:09:57 +0100 Subject: [PATCH 113/135] fixed AttributeError: 'OCRelay' from "Name" to "switch_index" --- pandapower/protection/protection_devices/ocrelay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/protection/protection_devices/ocrelay.py b/pandapower/protection/protection_devices/ocrelay.py index 4734f590b..31bdbef7e 100644 --- a/pandapower/protection/protection_devices/ocrelay.py +++ b/pandapower/protection/protection_devices/ocrelay.py @@ -272,7 +272,7 @@ def plot_protection_characteristic(self, net, num=60, xlabel="I [A]", ylabel="ti plt.grid(True, which="both", ls="-") def __str__(self): - s = 'Protection Device: %s \nType: %s \nName: %s' % (self.__class__.__name__, self.oc_relay_type, self.name) + s = 'Protection Device: %s \nType: %s \nIndex: %s' % (self.__class__.__name__, self.oc_relay_type, self.switch_index) self.characteristic_index = 1 return s def time_grading(net,time_settings): From a56e3780121cf33b47b4f3d923f9e1446394ce2b Mon Sep 17 00:00:00 2001 From: marcopau <77792704+marcopau@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:15:53 +0100 Subject: [PATCH 114/135] Removed log messages used for debug --- pandapower/estimation/algorithm/base.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index 0a1217a4f..f5f252089 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -130,10 +130,9 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): eppci.update_E(E) # log data - current_error = np.max(np.abs(d_E)) - obj_func = (r.T*r_inv*r)[0,0] - self.logger.debug("Current delta_x: {:.7f}".format(current_error)) - self.logger.debug("Current objective function value: {:.1f}".format(obj_func)) + # obj_func = (r.T*r_inv*r)[0,0] + # self.logger.debug("Current delta_x: {:.7f}".format(current_error)) + # self.logger.debug("Current objective function value: {:.1f}".format(obj_func)) # Restore full weighting matrix with current measurements if cur_it == 0 and eppci.any_i_meas: @@ -150,7 +149,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # check if the estimation is successfull self.check_result(current_error, cur_it) self.iterations = cur_it - self.obj_func = obj_func + # self.obj_func = obj_func if self.successful: # store variables required for chi^2 and r_N_max test: self.R_inv = r_inv.toarray() From 6bd2adcc029f25b8cd922c9c93752409417e7acb Mon Sep 17 00:00:00 2001 From: marcopau Date: Wed, 18 Dec 2024 11:32:14 +0100 Subject: [PATCH 115/135] Removed print commands used for debug --- pandapower/pf/run_newton_raphson_pf.py | 1 - pandapower/pypower/newtonpf.py | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/pandapower/pf/run_newton_raphson_pf.py b/pandapower/pf/run_newton_raphson_pf.py index 74b9c2df3..48455e457 100644 --- a/pandapower/pf/run_newton_raphson_pf.py +++ b/pandapower/pf/run_newton_raphson_pf.py @@ -163,7 +163,6 @@ def _run_ac_pf_without_qlims_enforced(ppci, options): # run the newton power flow - options["lightsim2grid"] = False if options["lightsim2grid"]: V, success, iterations, J, Vm_it, Va_it = newton_ls(Ybus.tocsc(), Sbus, V0, ref, pv, pq, ppci, options) T = None diff --git a/pandapower/pypower/newtonpf.py b/pandapower/pypower/newtonpf.py index 3e98d0a9f..a22fee238 100644 --- a/pandapower/pypower/newtonpf.py +++ b/pandapower/pypower/newtonpf.py @@ -439,15 +439,6 @@ def newtonpf(Ybus, Sbus, V0, ref, pv, pq, ppci, options, makeYbus=None): J = J + J_m_hvdc dx = -1 * spsolve(J, F, permc_spec=permc_spec, use_umfpack=use_umfpack) - - # log data - initial_delta = np.max(np.abs(dx)) - print("Initial current delta_x: {:.7f}".format(initial_delta)) - # if initial_delta > 0.35: - # dx = dx*0.35/initial_delta - # new_delta = np.max(np.abs(dx)) - # print("Smoothed current delta_x: {:.7f}".format(new_delta)) - # update voltage if dist_slack: slack = slack + dx[j0:j1] @@ -474,9 +465,6 @@ def newtonpf(Ybus, Sbus, V0, ref, pv, pq, ppci, options, makeYbus=None): Vm = abs(V) # update Vm and Va again in case Va = angle(V) # we wrapped around with a negative Vm - print("Max voltage magnitude: {:.7f}".format(max(Vm))) - print("Min voltage magnitude: {:.7f}".format(min(Vm))) - if v_debug: Vm_it = column_stack((Vm_it, Vm)) Va_it = column_stack((Va_it, Va)) From 04e1f2987e790cb613626bb520c2fa1ba912deaa Mon Sep 17 00:00:00 2001 From: "IEECNB215543\\gbanerjee" Date: Wed, 18 Dec 2024 11:39:41 +0100 Subject: [PATCH 116/135] Added CHANGELOG info for OC relay attribute error --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 78237ac96..df0bc590e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -83,6 +83,7 @@ Change Log - [CHANGED] accelerate distributed slack power flow calculation by using sparse-aware operations in _subnetworks() - [ADDED] Discrete shunt controller for local voltage regulation with shunt steps - [ADDED] cim2pp converter: Using lxml to parse XML files (better performance) +- [FIXED] OC relay name attribute error [2.14.7] - 2024-06-14 ------------------------------- From 97df1aaac95f745ad69eb767e9f628f1cf73501d Mon Sep 17 00:00:00 2001 From: marcopau Date: Wed, 18 Dec 2024 12:06:41 +0100 Subject: [PATCH 117/135] cleaned code for AF-WLS --- pandapower/estimation/algorithm/base.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/pandapower/estimation/algorithm/base.py b/pandapower/estimation/algorithm/base.py index dc8d6a2c0..76d974b92 100644 --- a/pandapower/estimation/algorithm/base.py +++ b/pandapower/estimation/algorithm/base.py @@ -91,7 +91,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): r_inv = csr_matrix(np.diagflat(1 / eppci.r_cov ** 2)) E = eppci.E while current_error > self.tolerance and cur_it < self.max_iterations: - self.logger.debug("Starting iteration {:d}".format(1 + cur_it)) + # self.logger.debug("Starting iteration {:d}".format(1 + cur_it)) try: # residual r r = csr_matrix(sem.create_rx(E)).T @@ -301,7 +301,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): E = eppci.E num_clusters = len(self.eppci["clusters"]) while current_error > self.tolerance and cur_it < self.max_iterations: - self.logger.debug("Starting iteration {:d}".format(1 + cur_it)) + # self.logger.debug("Starting iteration {:d}".format(1 + cur_it)) try: # residual r r = csr_matrix(sem.create_rx(E)).T @@ -326,21 +326,14 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # state vector difference d_E d_E = spsolve(G_m, H.T * (r_inv * r)) - # # Scaling of Delta_X to avoid divergence due o ill-conditioning and - # # operating conditions far from starting state variables - # current_error = np.max(np.abs(d_E)) - # if current_error > 0.25: - # d_E = d_E*0.25/current_error - # Update E with d_E E += d_E.ravel() - # eppci.update_E(E1) # log data current_error = np.max(np.abs(d_E)) - obj_func = (r.T*r_inv*r)[0,0] - self.logger.debug("Current delta_x: {:.7f}".format(current_error)) - self.logger.debug("Current objective function value: {:.1f}".format(obj_func)) + # obj_func = (r.T*r_inv*r)[0,0] + # self.logger.debug("Current delta_x: {:.7f}".format(current_error)) + # self.logger.debug("Current objective function value: {:.1f}".format(obj_func)) # Restore full weighting matrix if cur_it == 0 and eppci.any_i_meas: @@ -357,7 +350,7 @@ def estimate(self, eppci: ExtendedPPCI, **kwargs): # check if the estimation is successfull self.check_result(current_error, cur_it) self.iterations = cur_it - self.obj_func = obj_func + # self.obj_func = obj_func if self.successful: # store variables required for chi^2 and r_N_max test: self.R_inv = r_inv.toarray() From 888586cea90c8b835402aa70adbfaf37a1743e84 Mon Sep 17 00:00:00 2001 From: marcopau <77792704+marcopau@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:17:42 +0100 Subject: [PATCH 118/135] Update CHANGELOG.rst with modification in state estimation code --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aacb23791..1b291cef2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,15 @@ Change Log [upcoming release] - 2024-..-.. ------------------------------- +- [ADDED] Implementation of Allocation Factor WLS (AF-WLS) for non observable distribution grids +- [FIXED] Deletion of multiple measurements at the same bus or branch +- [FIXED] Creation of zero injection measurements in WLS estimator +- [FIXED] Divergence of WLS estimator with flat start for highly loaded grids +- [ADDED] Computation of matrix conditioning and warning in case of ill-conditioning +- [FIXED] Issue with initialization of WLS estimator +- [FIXED] Handling of current magnitude measurements in WLS estimator +- [ADDED] Created estimation results for shunt elements +- [FIXED] Fixed issue with power injection results in WLS estimator - [FIXED] cim2pp add missing description to dcline - [ADDED] pandas series accessor for geo column - [FIXED] Increasing geojson precision as the default precision might cause problems with pandahub From 8943b400c9ee1a0c5421398bbde6a82f1f84058d Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:10:55 +0100 Subject: [PATCH 119/135] added dev1 to version --- pandapower/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandapower/_version.py b/pandapower/_version.py index a3a3d8636..a399734d3 100644 --- a/pandapower/_version.py +++ b/pandapower/_version.py @@ -1,4 +1,4 @@ import importlib.metadata __version__ = importlib.metadata.version("pandapower") -__format_version__ = "3.0.0" +__format_version__ = "3.0.0dev1" diff --git a/pyproject.toml b/pyproject.toml index 0851e7cd6..354c81959 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pandapower" -version = "3.0.0" # File format version '__format_version__' is tracked in _version.py +version = "3.0.0dev1" # File format version '__format_version__' is tracked in _version.py authors = [ { name = "Leon Thurner", email = "leon.thurner@retoflow.de" }, { name = "Alexander Scheidler", email = "alexander.scheidler@iee.fraunhofer.de" } From e790bc6a9f0bab2682551b6c4acb8f2b94c2e627 Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:28:09 +0100 Subject: [PATCH 120/135] updated upload_release.yml to use a workflow dispatch and include possibility to upload to testpypi --- .github/workflows/upload_release.yml | 30 +++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/upload_release.yml b/.github/workflows/upload_release.yml index 863a8bb47..b0188225e 100644 --- a/.github/workflows/upload_release.yml +++ b/.github/workflows/upload_release.yml @@ -6,9 +6,16 @@ name: upload # Controls when the action will run. on: # Allows you to run this workflow manually from the Actions tab - push: - branches: - - master + workflow_dispatch: + inputs: + upload_server: + description: 'upload server' + required: true + default: 'testpypi' + type: choice + options: + - 'testpypi' + - 'pypi' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -31,10 +38,23 @@ jobs: # Upgrade pip python3 -m pip install --upgrade pip # Install twine - python3 -m pip install setuptools wheel twine + python3 -m pip install build setuptools wheel twine + + # Upload to TestPyPI + - name: Build and Upload to TestPyPI + if: ${{ inputs.upload_server == 'testpypi' }} + run: | + python3 -m build + python3 -m twine check dist/* --strict + python3 -m twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TESTPYPI }} + TWINE_REPOSITORY: testpypi # Upload to PyPI - name: Build and Upload to PyPI + if: ${{ inputs.upload_server == 'pypi' }} run: | python3 -m build python3 -m twine check dist/* --strict @@ -59,7 +79,7 @@ jobs: ref_name: develop - # Run an install for testing + # Run an installation for testing - name: Install pandapower from PyPI run: | python3 -m pip install pandapower From 05f46d49696e6d175af7c1b421889a3bf9c04786 Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:41:16 +0100 Subject: [PATCH 121/135] fixed version identifier missing a dot. --- pandapower/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandapower/_version.py b/pandapower/_version.py index a399734d3..8ea6e701a 100644 --- a/pandapower/_version.py +++ b/pandapower/_version.py @@ -1,4 +1,4 @@ import importlib.metadata __version__ = importlib.metadata.version("pandapower") -__format_version__ = "3.0.0dev1" +__format_version__ = "3.0.0.dev0" diff --git a/pyproject.toml b/pyproject.toml index 354c81959..5d19789f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pandapower" -version = "3.0.0dev1" # File format version '__format_version__' is tracked in _version.py +version = "3.0.0.dev0" # File format version '__format_version__' is tracked in _version.py authors = [ { name = "Leon Thurner", email = "leon.thurner@retoflow.de" }, { name = "Alexander Scheidler", email = "alexander.scheidler@iee.fraunhofer.de" } From d02fbd99779959a983541262448808718c9d7451 Mon Sep 17 00:00:00 2001 From: Mike Vogt Date: Thu, 19 Dec 2024 14:21:50 +0100 Subject: [PATCH 122/135] fixed two small problems with se. --- pandapower/estimation/ppc_conversion.py | 3 ++- pandapower/test/estimation/test_wls_estimation.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index 86867871d..29ada0e2e 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -519,8 +519,9 @@ def pp2eppci(net, v_start=None, delta_start=None, calculate_voltage_angles=True, zero_injection="aux_bus", algorithm='wls', ppc=None, eppci=None): if isinstance(eppci, ExtendedPPCI): + eppci.algorithm = algorithm eppci.data = _add_measurements_to_ppci(net, eppci.data, zero_injection, algorithm) - eppci.update_meas(algorithm) + eppci.update_meas() return net, ppc, eppci else: # initialize ppc diff --git a/pandapower/test/estimation/test_wls_estimation.py b/pandapower/test/estimation/test_wls_estimation.py index 7a23e185a..371354a7d 100644 --- a/pandapower/test/estimation/test_wls_estimation.py +++ b/pandapower/test/estimation/test_wls_estimation.py @@ -437,7 +437,7 @@ def test_cigre_network(init='flat'): diff_delta = target_delta - delta_result assert (np.nanmax(abs(diff_v)) < 0.0043) - assert (np.nanmax(abs(diff_delta)) < 0.17) + assert (np.nanmax(abs(diff_delta)) < 0.2) def test_cigre_network_with_slack_init(): @@ -548,21 +548,22 @@ def test_check_existing_measurements(): m1 = pp.create_measurement(net, "v", "bus", 1.006, .004, 0) m2 = pp.create_measurement(net, "v", "bus", 1.006, .004, 0) - assert m1 == m2 - assert len(net.measurement) == 1 - m3 = pp.create_measurement(net, "v", "bus", 1.006, .004, 0, check_existing=False) - assert m3 != m2 + # assert m1 == m2 assert len(net.measurement) == 2 + m3 = pp.create_measurement(net, "v", "bus", 1.006, .004, 0, check_existing=False) + # assert m3 != m2 + assert len(net.measurement) == 3 m4 = pp.create_measurement(net, "p", "line", -0.0011, 0.01, side=0, element=0, check_existing=True) m5 = pp.create_measurement(net, "p", "line", -0.0011, 0.01, side=0, element=0, check_existing=True) - assert m4 == m5 + # assert m4 == m5 m6 = pp.create_measurement(net, "p", "line", -0.0011, 0.01, side=0, element=0, check_existing=False) - assert m5 != m6 + # assert m5 != m6 + assert len(net.measurement) == 5 def load_3bus_network(): From 4fdaee465404040d01ec9181ca77f28bb7a6ca59 Mon Sep 17 00:00:00 2001 From: marcopau Date: Thu, 19 Dec 2024 15:10:52 +0100 Subject: [PATCH 123/135] Fix to test_recycle for state estimation --- pandapower/estimation/util.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pandapower/estimation/util.py b/pandapower/estimation/util.py index 6d4f0f371..6fdb1efae 100644 --- a/pandapower/estimation/util.py +++ b/pandapower/estimation/util.py @@ -157,6 +157,8 @@ def add_virtual_meas_from_loadflow(net, v_std_dev=0.01, p_std_dev=0.03, q_std_de else: pp.create_measurement(net, meas_type=meas_type, element_type='bus', element=bus_ix, value=meas_value, std_dev=v_std_dev) + remove_shunt_injection_from_meas(net,"shunt") + remove_shunt_injection_from_meas(net,"ward") for br_type in branch_meas_type.keys(): if not net['res_' + br_type].empty: @@ -170,6 +172,21 @@ def add_virtual_meas_from_loadflow(net, v_std_dev=0.01, p_std_dev=0.03, q_std_de add_virtual_meas_error(net, v_std_dev=v_std_dev, p_std_dev=p_std_dev, q_std_dev=q_std_dev, with_random_error=with_random_error) +def remove_shunt_injection_from_meas(net,type): + index = net[type].index.tolist() + bus = net[type]["bus"].tolist() + for k in range(len(index)): + try: + idxp = net.measurement[((net.measurement["element_type"]=="bus") & (net.measurement["element"]==bus[k])) & (net.measurement["measurement_type"]=="p")].index[0] + idxq = net.measurement[((net.measurement["element_type"]=="bus") & (net.measurement["element"]==bus[k])) & (net.measurement["measurement_type"]=="q")].index[0] + if type == "shunt": + net.measurement.loc[idxp,"value"] -= net.res_shunt.loc[index[k], 'p_mw'] + net.measurement.loc[idxq,"value"] -= net.res_shunt.loc[index[k], 'q_mvar'] + if type == "ward": + net.measurement.loc[idxp,"value"] -= net.res_ward.loc[index[k], 'p_mw'] - net.ward.loc[index[k], 'ps_mw'] + net.measurement.loc[idxq,"value"] -= net.res_ward.loc[index[k], 'q_mvar'] - net.ward.loc[index[k], 'qs_mvar'] + except: + continue def add_virtual_pmu_meas_from_loadflow(net, v_std_dev=0.001, i_std_dev=0.1, p_std_dev=0.01, q_std_dev=0.01, dg_std_dev=0.1, From fb2f03d84bf7662e71db4adbc9f55e425b744d09 Mon Sep 17 00:00:00 2001 From: marcopau Date: Thu, 19 Dec 2024 15:14:42 +0100 Subject: [PATCH 124/135] Fix to recycle_test for state estimation --- pandapower/estimation/ppc_conversion.py | 2 +- pandapower/test/estimation/test_recycle.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pandapower/estimation/ppc_conversion.py b/pandapower/estimation/ppc_conversion.py index 86867871d..a1b596e0e 100644 --- a/pandapower/estimation/ppc_conversion.py +++ b/pandapower/estimation/ppc_conversion.py @@ -520,7 +520,7 @@ def pp2eppci(net, v_start=None, delta_start=None, algorithm='wls', ppc=None, eppci=None): if isinstance(eppci, ExtendedPPCI): eppci.data = _add_measurements_to_ppci(net, eppci.data, zero_injection, algorithm) - eppci.update_meas(algorithm) + eppci.update_meas() return net, ppc, eppci else: # initialize ppc diff --git a/pandapower/test/estimation/test_recycle.py b/pandapower/test/estimation/test_recycle.py index 687fa6769..4ae431dee 100644 --- a/pandapower/test/estimation/test_recycle.py +++ b/pandapower/test/estimation/test_recycle.py @@ -22,16 +22,17 @@ def test_recycle_case30(): add_virtual_meas_from_loadflow(net) se = StateEstimation(net, recycle=True) se.estimate() - assert np.allclose(net.res_bus.vm_pu, net.res_bus_est.vm_pu, atol=1e-2) - assert np.allclose(net.res_bus.va_degree, net.res_bus_est.va_degree, atol=5e-1) + assert np.allclose(net.res_bus.vm_pu, net.res_bus_est.vm_pu, atol=1e-5) + assert np.allclose(net.res_bus.va_degree, net.res_bus_est.va_degree, atol=1e-5) # Run SE again net.load.p_mw -= 10 pp.runpp(net) + net.measurement.drop(net.measurement.index,inplace=True) add_virtual_meas_from_loadflow(net) assert se.estimate() - assert np.allclose(net.res_bus.vm_pu, net.res_bus_est.vm_pu, atol=1e-2) - assert np.allclose(net.res_bus.va_degree, net.res_bus_est.va_degree, atol=1) + assert np.allclose(net.res_bus.vm_pu, net.res_bus_est.vm_pu, atol=1e-5) + assert np.allclose(net.res_bus.va_degree, net.res_bus_est.va_degree, atol=1e-5) if __name__ == '__main__': pytest.main([__file__, "-xs"]) From ff3f2ef6766a8d2ad1c83a1010cd3050847c28cc Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:10:00 +0100 Subject: [PATCH 125/135] reverted updates to images in readme as they are incompatible with pypi --- README.rst | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 87f691b25..0b3edb91e 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,7 @@ -.. raw:: html - - - - - logo - - +.. image:: https://www.pandapower.org/images/pp.svg + :target: https://www.pandapower.org + :alt: logo | @@ -96,15 +91,9 @@ Operation at the Fraunhofer Institute for Energy Economics and Energy System Tec | -.. raw:: html - - - - - - logo - - +.. image:: https://www.pandapower.org/images/contact/Logo_Fraunhofer_IEE.png + :target: https://www.iee.fraunhofer.de/en.html + :width: 500 | From 64e91d62a92f1545eda1fe6c9f7bd4d34f014cb3 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Thu, 19 Dec 2024 18:30:29 +0100 Subject: [PATCH 126/135] fix toolbox tests --- pandapower/create.py | 6 +++++- pandapower/toolbox/comparison.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pandapower/create.py b/pandapower/create.py index fbfee03a0..47d4e9931 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -872,6 +872,8 @@ def _geodata_to_geo_series(data: Union[Iterable[Tuple[float, float]], Tuple[int, _add_to_entries_if_not_nan(net, "bus", entries, index, "min_vm_pu", min_vm_pu) _add_to_entries_if_not_nan(net, "bus", entries, index, "max_vm_pu", max_vm_pu) _set_multiple_entries(net, "bus", index, **entries, **kwargs) + net.bus.loc[net.bus.geo == "", "geo"] = None # overwrite + # empty_defaults_per_dtype() applied in _set_multiple_entries() return index @@ -5821,7 +5823,9 @@ def create_group(net, element_types, element_indices, name="", reference_columns entries = dict(zip(["name", "element_type", "element_index", "reference_column"], [name, element_types, element_indices, reference_columns])) - _set_multiple_entries(net, "group", index, **entries, **kwargs) + _set_multiple_entries(net, "group", index, **entries) + net.group.loc[net.group.reference_column == "", "reference_column"] = None # overwrite + # empty_defaults_per_dtype() applied in _set_multiple_entries() return index[0] diff --git a/pandapower/toolbox/comparison.py b/pandapower/toolbox/comparison.py index 2f5a0b97d..0d805d38d 100644 --- a/pandapower/toolbox/comparison.py +++ b/pandapower/toolbox/comparison.py @@ -50,7 +50,9 @@ def dataframes_equal(df1, df2, ignore_index_order=True, **kwargs): if "geo" in df1.columns and "geo" in df2.columns: not_eq_warn = "DataFrames do not match in column 'geo'." notnull1 = df1.geo.index[~df1.geo.isnull()] + notnull1 = df1.geo.loc[notnull1].index[df1.geo.loc[notnull1].apply(len).astype(bool)] notnull2 = df2.geo.index[~df2.geo.isnull()] + notnull2 = df2.geo.loc[notnull2].index[df2.geo.loc[notnull2].apply(len).astype(bool)] if len(notnull1) + len(notnull2) == 0: return True From 90fc7c87ce1a4a691a4c85fed0a3e458020fd1f0 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Thu, 19 Dec 2024 18:39:03 +0100 Subject: [PATCH 127/135] tiny fix --- pandapower/toolbox/grid_modification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandapower/toolbox/grid_modification.py b/pandapower/toolbox/grid_modification.py index ff8bd2cec..512a4ff0b 100644 --- a/pandapower/toolbox/grid_modification.py +++ b/pandapower/toolbox/grid_modification.py @@ -1845,7 +1845,7 @@ def _adapt_result_tables_in_replace_functions( et_old, et_new = "res_" + element_type_old, "res_" + element_type_new idx_old, idx_new = pd.Index(element_index_old), pd.Index(element_index_new) if net[et_old].shape[0]: - in_res = pd.Series(et_old).isin(net[et_old].index).values + in_res = pd.Series(idx_old).isin(net[et_old].index).values to_add = net[et_old].loc[idx_old[in_res]] to_add.index = idx_new[in_res] net[et_new] = pd.concat([net[et_new], to_add], sort=True) From 785dd03d3e9c40f74432fd8eed400fb8d936a46a Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Thu, 19 Dec 2024 18:47:55 +0100 Subject: [PATCH 128/135] fix protection tests --- pandapower/create.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandapower/create.py b/pandapower/create.py index 47d4e9931..bd8e98e8c 100644 --- a/pandapower/create.py +++ b/pandapower/create.py @@ -2609,6 +2609,8 @@ def create_lines(net, from_buses, to_buses, length_km, std_type, name=None, inde _add_to_entries_if_not_nan(net, "line", entries, index, column, value, float64) _set_multiple_entries(net, "line", index, **entries, **kwargs) + net.line.loc[net.line.geo == "", "geo"] = None # overwrite + # empty_defaults_per_dtype() applied in _set_multiple_entries() if geodata: _add_multiple_branch_geodata(net, geodata, index) From 9c889e388126a2716fc87d72117436857cb7e389 Mon Sep 17 00:00:00 2001 From: Steffen Meinecke Date: Thu, 19 Dec 2024 19:03:41 +0100 Subject: [PATCH 129/135] some further fixes --- pandapower/test/api/test_create.py | 2 ++ pandapower/test/api/test_group.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pandapower/test/api/test_create.py b/pandapower/test/api/test_create.py index 1e1ed8dd7..955a1044a 100644 --- a/pandapower/test/api/test_create.py +++ b/pandapower/test/api/test_create.py @@ -1487,6 +1487,8 @@ def test_create_storages(): net.storage.test_kwargs.values == ["dummy_string_1", "dummy_string_2", "dummy_string_3"] ) + for col in ["name", "type"]: + net.storage.loc[net.storage[col].isnull(), col] = "" assert pp.nets_equal(net, net_bulk) diff --git a/pandapower/test/api/test_group.py b/pandapower/test/api/test_group.py index ab9761a2c..4834d3324 100644 --- a/pandapower/test/api/test_group.py +++ b/pandapower/test/api/test_group.py @@ -331,7 +331,7 @@ def test_detach_and_compare(): pp.detach_from_group(net, 3, "trafo", 1) assert pp.group_element_lists(net, 3)[0] == ["trafo"] assert pp.group_element_lists(net, 3)[1] == [typed_list([0, 2], type_)] - assert pp.group_element_lists(net, 3)[2] == [np.nan if type_ is int else "name"] + assert pp.group_element_lists(net, 3)[2] == [None if type_ is int else "name"] def test_res_power(): From 1094740ecd16e9d3e9b6b08b6111d558557363f0 Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:19:16 +0100 Subject: [PATCH 130/135] bumped version number in toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5d19789f9..0d67c6038 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pandapower" -version = "3.0.0.dev0" # File format version '__format_version__' is tracked in _version.py +version = "3.0.0.dev1" # File format version '__format_version__' is tracked in _version.py authors = [ { name = "Leon Thurner", email = "leon.thurner@retoflow.de" }, { name = "Alexander Scheidler", email = "alexander.scheidler@iee.fraunhofer.de" } From b323ab5981f018949b66d7fc693e24c8b0095ad0 Mon Sep 17 00:00:00 2001 From: V3 Date: Fri, 3 Jan 2025 15:53:33 +0100 Subject: [PATCH 131/135] Update upload_release.yml added an exception for testpypi and added --pre for installation testing. --- .github/workflows/upload_release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/upload_release.yml b/.github/workflows/upload_release.yml index b0188225e..1620310e2 100644 --- a/.github/workflows/upload_release.yml +++ b/.github/workflows/upload_release.yml @@ -66,12 +66,14 @@ jobs: # Waste some time - name: Sleep for 150s to make release available + if: ${{ inputs.upload_server == 'pypi' }} uses: juliangruber/sleep-action@v1 with: time: 150s # Notify fraunhofer ci about the new version - uses: eic/trigger-gitlab-ci@v3 + if: ${{ inputs.upload_server == 'pypi' }} with: url: https://gitlab.cc-asp.fraunhofer.de project_id: 27329 @@ -81,6 +83,7 @@ jobs: # Run an installation for testing - name: Install pandapower from PyPI + if: ${{ inputs.upload_server == 'pypi' }} run: | - python3 -m pip install pandapower - python3 -c "import pandapower; print(pandapower.__version__)" \ No newline at end of file + python3 -m pip install --pre pandapower + python3 -c "import pandapower; print(pandapower.__version__)" From 9f4aafd6b986942d63f24b60b44bee25ddf34a44 Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:52:10 +0100 Subject: [PATCH 132/135] moved manifest.in to pyproject.toml --- MANIFEST.in | 15 --------------- pyproject.toml | 31 ++++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 18 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 67aea8922..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,15 +0,0 @@ -include LICENSE AUTHORS README.rst CHANGELOG.rst -include pandapower/converter/powerfactory/power_factory_files/* -include pandapower/test/api/input_files/* -include pandapower/test/test_files/example_cim/* - -global-include *.p -global-include *.m -global-include *.mat -global-include *.json -global-include *.jl -global-include *.csv - -prune .git* -prune doc* -prune tutorials* diff --git a/pyproject.toml b/pyproject.toml index 0d67c6038..c3f9dd9ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,9 +79,34 @@ all = [ # "shapely", "pyproj", "Pyogrio" are dependencies of geopandas and should be already available ("Fiona" got dropped) # "hashlib", "zlib", "base64" produce install problems, so they are not included -[tool.setuptools.packages] -find = {} +[tool.setuptools] +license-files = [ + 'LICEN[CS]E*', + 'COPYING*', + 'NOTICE*', + 'AUTHORS*', + 'CHANGELOG*', + 'CITATION*', +] + +[tool.setuptools.packages.find] +include = [ + "pandapower*", +] +exclude = [ + "pandapower.test*", +] + +[tool.setuptools.package-data] +"*" = [ + "*.p", + "*.m", + "*.mat", + "*.json", + "*.jl", + "*.csv" +] [tool.pytest.ini_options] addopts = ["--strict-markers"] @@ -92,7 +117,7 @@ markers = [ [tool.coverage.run] omit = [ "pandapower/test/*" - ] +] [tool.coverage.report] exclude_lines = [ "pragma: no cover", From 5edd059debc7bf4d9099b98ab651ed438f82b987 Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:57:40 +0100 Subject: [PATCH 133/135] moved format.json to doc to keep it out of package build --- format.json => doc/format.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename format.json => doc/format.json (100%) diff --git a/format.json b/doc/format.json similarity index 100% rename from format.json rename to doc/format.json From 57bdbdcb66796ba8125b3c28fdfc0a9c0a12e5e9 Mon Sep 17 00:00:00 2001 From: hkoertge <2981026+KS-HTK@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:02:00 +0100 Subject: [PATCH 134/135] updated CHANGELOG.rst --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dcdf977e6..086e90f94 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,9 @@ Change Log ============= -[upcoming release] - 2024-..-.. +[upcoming release] - 2025-..-.. ------------------------------- +- [REMOVED] Excluding tests and test_files from built packages - [ADDED] Static Var Compensator with Voltage Control - [ADDED] Implementation of Allocation Factor WLS (AF-WLS) for non observable distribution grids - [FIXED] Deletion of multiple measurements at the same bus or branch From b03599339033c44a50e2abf945fb0fb818648792 Mon Sep 17 00:00:00 2001 From: srdm Date: Wed, 8 Jan 2025 11:09:15 +0100 Subject: [PATCH 135/135] adapted CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 086e90f94..f8aa158fa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,7 @@ Change Log [upcoming release] - 2025-..-.. ------------------------------- +- [FIXED] Allow to consider all oos components in nx graph creation - [REMOVED] Excluding tests and test_files from built packages - [ADDED] Static Var Compensator with Voltage Control - [ADDED] Implementation of Allocation Factor WLS (AF-WLS) for non observable distribution grids