The Group module allows you to combine several elements of a pandapower net into a group. Various functions are available, which are then automatically applied to all elements in this group. This tutorial shows you how to create groups and how to use some helpful simple group functions.
To analyse the group functionality we use the CIGRE MV net.
import numpy as np
import pandapower as pp
from pandapower.networks import create_cigre_network_mv
from pandapower.create import create_group
net = create_cigre_network_mv(with_der="all")
net.switch["closed"] = True
import pandapower.create
As examples, we define two groups, one to represent a virtual power plant with sgens, loads and a storage and for all elements of the second feeder of the net. You can create groups using lists of element types and element indices, or you can pass all that information in one dict.
# define a Group as virtual power plant
gr1_name = "virtual power plant"
vpp_element_types = ["storage", "sgen", "load"]
vpp_elements = [[1], [6, 8, 9, 10, 11, 12], [5, 6]]
gr1_idx = create_group(net, vpp_element_types, vpp_elements, name=gr1_name)
#gr1_idx = pp.group(net, vpp_element_types, vpp_elements)
# define a Group of a Feeder 2
gr2_name = "Feeder2"
feeder2buses = [12, 13, 14]
feeder2_elements_dict = pp.get_connected_elements_dict(net, feeder2buses)
feeder2_elements_dict["bus"] = feeder2buses
gr2_idx = pp.create_group_from_dict(net, feeder2_elements_dict, name=gr2_name)
#gr2_idx = pp.branch_element_bus_dict(net, feeder2_elements_dict)
Now we can see that there are entries in net.group
for both groups. As usual in pandapower, the information is accessed by means of the index. For groups with multiple element types, e.g. the virtual power plant includes storages, sgens and loads, multiple rows with the same index are created to net.group
.
print(net.group)
name element_type element reference_column 0 virtual power plant storage [1] None 0 virtual power plant sgen [6, 8, 9, 10, 11, 12] None 0 virtual power plant load [5, 6] None 1 Feeder2 line [10, 11, 14] None 1 Feeder2 switch [5] None 1 Feeder2 bus [12, 13, 14] None 1 Feeder2 trafo [1] None 1 Feeder2 load [8, 9, 15, 16, 17] None
reference_column
¶However, the user can manipulate the indices of the net element dataframes and some helper functions change/may not preserve the element indices, such as pp.create_continuous_elements_index(net)
. As a result, by means of the indices, a group can no longer find its members.
For that reason, groups can also detect their members by a column of the elements dataframes. That can be applied directly at the group definition or later using set_group_reference_column()
. Then we can see, that the group does no longer store the indices but the values of the set reference column:
print("Group 1 data with indices:\n")
print(net.group.loc[gr1_idx])
pp.set_group_reference_column(net, gr1_idx, "name")
print("\nAfter setting 'name' as reference column, the group stores the members by the names:\n")
print(net.group.loc[gr1_idx])
Group 1 data with indices: name element_type element reference_column 0 virtual power plant storage [1] None 0 virtual power plant sgen [6, 8, 9, 10, 11, 12] None 0 virtual power plant load [5, 6] None After setting 'name' as reference column, the group stores the members by the names: name element_type \ 0 virtual power plant storage 0 virtual power plant sgen 0 virtual power plant load element reference_column 0 [Battery 2] name 0 [PV 10, WKA 7, Residential fuel cell 1, CHP di... name 0 [Load R8, Load R10] name
As we can see, the second group now stores the names of the members and not indices. Using group_element_index()
you can nevertheless get the indices of the group members. You can see that these are still the same as given in the definition of the group above.
print(pp.group_element_index(net, gr1_idx, "load"))
Int64Index([5, 6], dtype='int64')
Attention: Be aware that reference_column
only works fine if there are no duplicated values in net[element][reference_column]
for all members of the groups!
The following code block shows you how to set the value to all members of a group. A specific use case of the is to set all members in service or out of service. For that reason, these got explicit function names.
# setting a name value to group 2 members
pp.set_value_to_group(net, gr2_idx, "member of group '%s'" % gr2_name, "name")
# visualize the effect
print("The load names:\n")
print(net.load.name)
# set all elements of group 2 out of service
pp.set_group_out_of_service(net, gr2_idx)
pp.runpp(net)
print("\nThe bus results with Feeder 2 out of service:\n")
print(net.res_bus) # the Feeder 2 buses should now have nan values
# and back in service...
pp.set_group_in_service(net, gr2_idx)
pp.runpp(net)
print("\nThe bus results with Feeder 2 back in service:\n")
print(net.res_bus)
The load names: 0 Load R1 1 Load R3 2 Load R4 3 Load R5 4 Load R6 5 Load R8 6 Load R10 7 Load R11 8 member of group 'Feeder2' 9 member of group 'Feeder2' 10 Load CI1 11 Load CI3 12 Load CI7 13 Load CI9 14 Load CI10 15 member of group 'Feeder2' 16 member of group 'Feeder2' 17 member of group 'Feeder2' Name: name, dtype: object The bus results with Feeder 2 out of service: vm_pu va_degree p_mw q_mvar 0 1.030000 0.000000 -22.818564 -8.646534 1 0.993935 -6.110560 19.839000 4.637136 2 0.976713 -6.707834 0.000000 0.000000 3 0.949406 -7.665482 0.481700 0.208882 4 0.947294 -7.750513 0.411650 0.108182 5 0.946493 -7.784148 1.264500 0.182329 6 0.947350 -7.674175 0.518050 0.137354 7 0.947704 -7.645199 -1.423500 0.047410 8 0.947124 -7.719307 0.556850 0.147078 9 0.946759 -7.729477 0.021750 0.355578 10 0.946407 -7.773404 0.689300 0.161264 11 0.946652 -7.770208 0.319800 0.082656 12 NaN NaN 0.000000 0.000000 13 NaN NaN 0.000000 0.000000 14 NaN NaN 0.000000 0.000000 The bus results with Feeder 2 back in service: vm_pu va_degree p_mw q_mvar 0 1.030000 0.000000 -43.40813 -15.655818 1 0.996932 -5.726043 19.83900 4.637136 2 0.986772 -6.003966 0.00000 0.000000 3 0.970355 -6.429326 0.48170 0.208882 4 0.968860 -6.482045 0.41165 0.108182 5 0.968247 -6.505724 1.26450 0.182329 6 0.969547 -6.377311 0.51805 0.137354 7 0.969965 -6.346000 -1.42350 0.047410 8 0.969896 -6.391227 0.55685 0.147078 9 0.969338 -6.411158 0.02175 0.355578 10 0.968510 -6.477714 0.68930 0.161264 11 0.968542 -6.485215 0.31980 0.082656 12 0.997599 -5.859926 20.01000 4.693341 13 0.982794 -6.130536 0.03400 0.021071 14 0.973929 -6.298161 0.54005 0.257713
Groups can sum its complete power consumption including losses. Since the virtual power plant group predominantly consists of generation units, its active power value is negative.
for gr_idx, gr_name in zip([gr1_idx, gr2_idx], [gr1_name, gr2_name]):
print("Group '%s' consumes %.2f MW and %.2f Mvar." % (
gr_name, pp.group_res_p_mw(net, gr_idx), pp.group_res_q_mvar(net, gr_idx)))
# a validation of Feeder 2 group values is easy since there is only one trafo and one line
# which supply the feeder:
p_val = net.res_line.p_to_mw.at[14] + net.res_trafo.p_hv_mw.at[1]
assert np.isclose(pp.group_res_p_mw(net, gr2_idx), p_val)
Group 'virtual power plant' consumes -0.85 MW and 0.27 Mvar. Group 'Feeder2' consumes 20.66 MW and 7.45 Mvar.
Once defined, it still easy to change the members of a group:
p_before = pp.group_res_p_mw(net, gr1_idx)
# append the virtual power plant
pp.attach_to_group(net, gr1_idx, ["storage"], [[net.storage.name.at[0]]], "name")
# drop an sgen from the virtual power plant
pp.detach_from_group(net, gr1_idx, "load", [5])
# validate via compare the active power consumption
assert np.isclose(p_before + net.res_storage.p_mw.at[0] - net.res_load.p_mw.at[5], pp.group_res_p_mw(net, gr1_idx))
For small groups, print(net.group.loc[[group_index]]
can be used to see quickly how many members are included. For larger groups, count_group_elements()
is available to access the number of group members per element type:
no_member = pp.count_group_elements(net, gr2_idx)
print("Number of member per element type:")
print(no_member)
print(f"\n Overall number of members: {no_member.sum()}")
Number of member per element type: line 3 switch 1 bus 3 trafo 1 load 5 dtype: int32 Overall number of members: 13
If net.group
has been corrupted, ensure_lists_in_group_element_column()
and remove_not_existing_group_members()
may help:
# assume net.group is corrupted for some reason, as...
net.group.element.iat[-1] = 5 # no list
net.trafo.drop(1, inplace=True) # net elements were dropped without using toolbox functions like drop_trafos()
pp.ensure_lists_in_group_element_column(net) # fixes no list data
pp.remove_not_existing_group_members(net) # detects that transformer 1 does not exist and drops it
print(net.group)
hp.pandapower.groups - INFO: net.group row 6 is dropped because no fitting elements exist in net[trafo].
name element_type \ 0 virtual power plant storage 0 virtual power plant sgen 0 virtual power plant load 1 Feeder2 line 1 Feeder2 switch 1 Feeder2 bus 1 Feeder2 load element reference_column 0 [Battery 2, Battery 1] name 0 [PV 10, WKA 7, Residential fuel cell 1, CHP di... name 0 [Load R10] name 1 [10, 11, 14] None 1 [5] None 1 [12, 13, 14] None 1 [5] None
To explore some more functions, such as groups_equal()
, compare_group_elements()
and others, please have a look to the Code.