"""This file is part of DING0, the DIstribution Network GeneratOr.
DING0 is a tool to generate synthetic medium and low voltage power
distribution grids based on open data.
It is developed in the project open_eGo: https://openegoproject.wordpress.com
DING0 lives at github: https://github.com/openego/ding0/
The documentation is available on RTD: http://ding0.readthedocs.io"""
__copyright__ = "Reiner Lemoine Institut gGmbH"
__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)"
__url__ = "https://github.com/openego/ding0/blob/master/LICENSE"
__author__ = "nesnoj, gplssm"
from ding0.tools import config as cfg_ding0
from ding0.core.network import TransformerDing0, BranchDing0
from ding0.core.network.cable_distributors import LVCableDistributorDing0
from ding0.core.network.loads import LVLoadDing0
import logging
import math
logger = logging.getLogger(__name__)
[docs]def select_grid_model_ria(lvgd, sector):
"""Select a typified grid for retail/industrial and agricultural
Parameters
----------
lvgd : ding0.core.structure.regions.LVGridDistrictDing0
Low-voltage grid district object
sector : :obj:`str`
Either 'retail/industrial' or 'agricultural'. Depending on choice
different parameters to grid topology apply
Returns
-------
:obj:`dict`
Parameters that describe branch lines of a sector
"""
cable_lf = cfg_ding0.get('assumptions',
'load_factor_lv_cable_lc_normal')
cos_phi_load = cfg_ding0.get('assumptions',
'cos_phi_load')
max_lv_branch_line_load = cfg_ding0.get('assumptions',
'max_lv_branch_line')
# make a distinction between sectors
if sector == 'retail/industrial':
max_branch_length = cfg_ding0.get(
"assumptions",
"branch_line_length_retail_industrial")
peak_load = lvgd.peak_load_retail + \
lvgd.peak_load_industrial
count_sector_areas = lvgd.sector_count_retail + \
lvgd.sector_count_industrial
elif sector == 'agricultural':
max_branch_length = cfg_ding0.get(
"assumptions",
"branch_line_length_agricultural")
peak_load = lvgd.peak_load_agricultural
count_sector_areas = lvgd.sector_count_agricultural
else:
raise ValueError('Sector {} does not exist!'.format(sector))
# determine size of a single load
single_peak_load = peak_load / count_sector_areas
# if this single load exceeds threshold of 300 kVA it is splitted
while single_peak_load > (max_lv_branch_line_load * (cable_lf * cos_phi_load)):
single_peak_load = single_peak_load / 2
count_sector_areas = count_sector_areas * 2
grid_model = {}
# determine parameters of branches and loads connected to the branch
# line
if 0 < single_peak_load:
grid_model['max_loads_per_branch'] = math.floor(
(max_lv_branch_line_load * (cable_lf * cos_phi_load)) / single_peak_load)
grid_model['single_peak_load'] = single_peak_load
grid_model['full_branches'] = math.floor(
count_sector_areas / grid_model['max_loads_per_branch'])
grid_model['remaining_loads'] = count_sector_areas - (
grid_model['full_branches'] * grid_model['max_loads_per_branch']
)
grid_model['load_distance'] = max_branch_length / (
grid_model['max_loads_per_branch'] + 1)
grid_model['load_distance_remaining'] = max_branch_length / (
grid_model['remaining_loads'] + 1)
else:
if count_sector_areas > 0:
logger.warning(
'LVGD {lvgd} has in sector {sector} no load but area count'
'is {count}. This is maybe related to #153'.format(
lvgd=lvgd,
sector=sector,
count=count_sector_areas))
grid_model = None
# add consumption to grid_model for assigning it to the load object
# consumption is given per sector and per individual load
if sector == 'retail/industrial':
grid_model['consumption'] = {
'retail': lvgd.sector_consumption_retail / (
grid_model['full_branches'] *
grid_model['max_loads_per_branch'] +
grid_model['remaining_loads']),
'industrial': lvgd.sector_consumption_industrial / (
grid_model['full_branches'] *
grid_model['max_loads_per_branch'] +
grid_model['remaining_loads'])}
elif sector == 'agricultural':
grid_model['consumption'] = {
'agricultural': lvgd.sector_consumption_agricultural / (
grid_model['full_branches'] *
grid_model['max_loads_per_branch'] +
grid_model['remaining_loads'])}
return grid_model
[docs]def grid_model_params_ria(lvgd):
"""Determine grid model parameters for LV grids of sectors
retail/industrial and agricultural
Parameters
----------
lvgd : LVGridDistrictDing0
Low-voltage grid district object
Returns
-------
:obj:`dict`
Structural description of (parts of) LV grid topology
"""
# Choose retail/industrial and agricultural grid model
model_params_ria = {}
if ((lvgd.sector_count_retail +
lvgd.sector_count_industrial > 0) or
(lvgd.peak_load_retail +
lvgd.peak_load_industrial > 0)):
model_params_ria['retail/industrial'] = select_grid_model_ria(
lvgd, 'retail/industrial')
else:
model_params_ria['retail/industrial'] = None
if ((lvgd.sector_count_agricultural > 0) or
(lvgd.peak_load_agricultural > 0)):
model_params_ria['agricultural'] = select_grid_model_ria(lvgd,
'agricultural')
else:
model_params_ria['agricultural'] = None
return model_params_ria
[docs]def build_lv_graph_ria(lvgd, grid_model_params):
"""Build graph for LV grid of sectors retail/industrial and agricultural
Based on structural description of LV grid topology for sectors
retail/industrial and agricultural (RIA) branches for these sectors are
created and attached to the LV grid's MV-LV substation bus bar.
LV loads of the sectors retail/industrial and agricultural are located
in separat branches for each sector (in case of large load multiple of
these).
These loads are distributed across the branches by an equidistant
distribution.
This function accepts the dict `grid_model_params` with particular
structure
>>> grid_model_params = {
>>> ... 'agricultural': {
>>> ... 'max_loads_per_branch': 2
>>> ... 'single_peak_load': 140,
>>> ... 'full_branches': 2,
>>> ... 'remaining_loads': 1,
>>> ... 'load_distance': 800/3,
>>> ... 'load_distance_remaining': 400}}
Parameters
----------
lvgd : LVGridDistrictDing0
Low-voltage grid district object
grid_model_params : dict
Dict of structural information of sectoral LV grid branchwith particular
structure, e.g.::
grid_model_params = {
'agricultural': {
'max_loads_per_branch': 2
'single_peak_load': 140,
'full_branches': 2,
'remaining_loads': 1,
'load_distance': 800/3,
'load_distance_remaining': 400
}
}
Note
-----
We assume a distance from the load to the branch it is connected to of
30 m. This assumption is defined in the config files.
"""
def lv_graph_attach_branch():
"""Attach a single branch including its equipment (cable dist, loads
and line segments) to graph of `lv_grid`
"""
# determine maximum current occuring due to peak load
# of this load load_no
I_max_load = val['single_peak_load'] / (3 ** 0.5 * v_nom) / cos_phi_load
# determine suitable cable for this current
suitable_cables_stub = lvgd.lv_grid.network.static_data['LV_cables'][
(lvgd.lv_grid.network.static_data['LV_cables'][
'I_max_th'] * cable_lf) > I_max_load]
cable_type_stub = suitable_cables_stub.loc[
suitable_cables_stub['I_max_th'].idxmin(), :
]
# cable distributor to divert from main branch
lv_cable_dist = LVCableDistributorDing0(
grid=lvgd.lv_grid,
branch_no=branch_no,
load_no=load_no)
# add lv_cable_dist to graph
lvgd.lv_grid.add_cable_dist(lv_cable_dist)
# cable distributor within building (to connect load+geno)
lv_cable_dist_building = LVCableDistributorDing0(
grid=lvgd.lv_grid,
branch_no=branch_no,
load_no=load_no,
in_building=True)
# add lv_cable_dist_building to graph
lvgd.lv_grid.add_cable_dist(lv_cable_dist_building)
# create an instance of Ding0 LV load
lv_load = LVLoadDing0(grid=lvgd.lv_grid,
branch_no=branch_no,
load_no=load_no,
peak_load=val['single_peak_load'],
consumption=val['consumption'])
# add lv_load to graph
lvgd.lv_grid.add_load(lv_load)
# create branch line segment between either (a) station
# and cable distributor or (b) between neighboring cable
# distributors
if load_no == 1:
# case a: cable dist <-> station
lvgd.lv_grid.graph.add_edge(
lvgd.lv_grid.station(),
lv_cable_dist,
branch=BranchDing0(
length=val['load_distance'],
kind='cable',
grid=lvgd.lv_grid,
type=cable_type,
id_db='branch_{sector}{branch}_{load}'.format(
branch=branch_no,
load=load_no,
sector=sector_short)
))
else:
# case b: cable dist <-> cable dist
lvgd.lv_grid.graph.add_edge(
lvgd.lv_grid._cable_distributors[-4],
lv_cable_dist,
branch=BranchDing0(
length=val['load_distance'],
kind='cable',
grid=lvgd.lv_grid,
type=cable_type,
id_db='branch_{sector}{branch}_{load}'.format(
branch=branch_no,
load=load_no,
sector=sector_short)))
# create branch stub that connects the load to the
# lv_cable_dist located in the branch line
lvgd.lv_grid.graph.add_edge(
lv_cable_dist,
lv_cable_dist_building,
branch=BranchDing0(
length=cfg_ding0.get(
'assumptions',
'lv_ria_branch_connection_distance'),
kind='cable',
grid=lvgd.lv_grid,
type=cable_type_stub,
id_db='stub_{sector}{branch}_{load}'.format(
branch=branch_no,
load=load_no,
sector=sector_short))
)
lvgd.lv_grid.graph.add_edge(
lv_cable_dist_building,
lv_load,
branch=BranchDing0(
length=1,
kind='cable',
grid=lvgd.lv_grid,
type=cable_type_stub,
id_db='stub_{sector}{branch}_{load}'.format(
branch=branch_no,
load=load_no,
sector=sector_short))
)
cable_lf = cfg_ding0.get('assumptions',
'load_factor_lv_cable_lc_normal')
cos_phi_load = cfg_ding0.get('assumptions',
'cos_phi_load')
v_nom = cfg_ding0.get('assumptions', 'lv_nominal_voltage') / 1e3 # v_nom in kV
# iterate over branches for sectors retail/industrial and agricultural
for sector, val in grid_model_params.items():
if sector == 'retail/industrial':
sector_short = 'RETIND'
elif sector == 'agricultural':
sector_short = 'AGR'
else:
sector_short = ''
if val is not None:
for branch_no in list(range(1, val['full_branches'] + 1)):
# determine maximum current occuring due to peak load of branch
I_max_branch = (val['max_loads_per_branch'] *
val['single_peak_load']) / (3 ** 0.5 * v_nom) / (
cos_phi_load)
# determine suitable cable for this current
suitable_cables = lvgd.lv_grid.network.static_data['LV_cables'][
(lvgd.lv_grid.network.static_data['LV_cables'][
'I_max_th'] * cable_lf) > I_max_branch]
cable_type = suitable_cables.loc[
suitable_cables['I_max_th'].idxmin(), :
]
# create Ding0 grid objects and add to graph
for load_no in list(range(1, val['max_loads_per_branch'] + 1)):
# create a LV grid string and attached to station
lv_graph_attach_branch()
# add remaining branch
if val['remaining_loads'] > 0:
if 'branch_no' not in locals():
branch_no = 0
# determine maximum current occuring due to peak load of branch
I_max_branch = (val['max_loads_per_branch'] *
val['single_peak_load']) / (3 ** 0.5 * v_nom) / (
cos_phi_load)
# determine suitable cable for this current
suitable_cables = lvgd.lv_grid.network.static_data['LV_cables'][
(lvgd.lv_grid.network.static_data['LV_cables'][
'I_max_th'] * cable_lf) > I_max_branch]
cable_type = suitable_cables.loc[
suitable_cables['I_max_th'].idxmin(), :
]
branch_no += 1
for load_no in list(range(1, val['remaining_loads'] + 1)):
# create a LV grid string and attach to station
lv_graph_attach_branch()
[docs]def build_ret_ind_agr_branches(lvgd):
"""Determine topology of LV grid for retail/industrial and agricultural sector
and create representative graph of the grid
Parameters
----------
lvgd : LVGridDistrictDing0
Low-voltage grid district object
"""
# determine topology of grid branches
model_params = grid_model_params_ria(lvgd)
# attach branches for sectors retail/industrial and agricultural
build_lv_graph_ria(lvgd, model_params)
[docs]def select_grid_model_residential(lvgd):
"""Selects typified model grid based on population
Parameters
----------
lvgd : LVGridDistrictDing0
Low-voltage grid district object
Returns
-------
:pandas:`pandas.DataFrame<dataframe>`
Selected string of typified model grid
:pandas:`pandas.DataFrame<dataframe>`
Parameters of chosen Transformer
Note
-----
In total 196 distinct LV grid topologies are available that are chosen
by population in the LV grid district. Population is translated to
number of house branches. Each grid model fits a number of house
branches. If this number exceeds 196, still the grid topology of 196
house branches is used. The peak load of the LV grid district is
uniformly distributed across house branches.
"""
# Load properties of LV typified model grids
string_properties = lvgd.lv_grid.network.static_data['LV_model_grids_strings']
# Load relational table of apartment count and strings of model grid
apartment_string = lvgd.lv_grid.network.static_data[
'LV_model_grids_strings_per_grid']
# load assumtions
apartment_house_branch_ratio = cfg_ding0.get("assumptions",
"apartment_house_branch_ratio")
population_per_apartment = cfg_ding0.get("assumptions",
"population_per_apartment")
# calc count of apartments to select string types
apartments = round(lvgd.population / population_per_apartment)
if apartments > 196:
apartments = 196
# select set of strings that represent one type of model grid
strings = apartment_string.loc[apartments]
selected_strings = [int(s) for s in strings[strings >= 1].index.tolist()]
# slice dataframe of string parameters
selected_strings_df = string_properties.loc[selected_strings]
# add number of occurences of each branch to df
occurence_selector = [str(i) for i in selected_strings]
selected_strings_df['occurence'] = strings.loc[occurence_selector].tolist()
return selected_strings_df
[docs]def build_lv_graph_residential(lvgd, selected_string_df):
"""Builds nxGraph based on the LV grid model
Parameters
----------
lvgd : LVGridDistrictDing0
Low-voltage grid district object
selected_string_df: :pandas:`pandas.DataFrame<dataframe>`
Table of strings of the selected grid model
Note
-----
To understand what is happening in this method a few data table columns
are explained here
* `count house branch`: number of houses connected to a string
* `distance house branch`: distance on a string between two house branches
* `string length`: total length of a string
* `length house branch A|B`: cable from string to connection point of a house
A|B in general brings some variation in to the typified model grid and
refer to different length of house branches and different cable types
respectively different cable widths.
"""
houses_connected = (
selected_string_df['occurence'] * selected_string_df[
'count house branch']).sum()
average_load = lvgd.peak_load_residential / \
houses_connected
average_consumption = lvgd.sector_consumption_residential / \
houses_connected
hh_branch = 0
# iterate over each type of branch
for i, row in selected_string_df.iterrows():
# get overall count of branches to set unique branch_no
branch_count_sum = len(list(
lvgd.lv_grid.graph.neighbors(lvgd.lv_grid.station())))
# iterate over it's occurences
for branch_no in range(1, int(row['occurence']) + 1):
hh_branch += 1
# iterate over house branches
for house_branch in range(1, row['count house branch'] + 1):
if house_branch % 2 == 0:
variant = 'B'
else:
variant = 'A'
# cable distributor to divert from main branch
lv_cable_dist = LVCableDistributorDing0(
grid=lvgd.lv_grid,
string_id=i,
branch_no=branch_no + branch_count_sum,
load_no=house_branch)
# add lv_cable_dist to graph
lvgd.lv_grid.add_cable_dist(lv_cable_dist)
# cable distributor within building (to connect load+geno)
lv_cable_dist_building = LVCableDistributorDing0(
grid=lvgd.lv_grid,
string_id=i,
branch_no=branch_no + branch_count_sum,
load_no=house_branch,
in_building=True)
# add lv_cable_dist_building to graph
lvgd.lv_grid.add_cable_dist(lv_cable_dist_building)
lv_load = LVLoadDing0(grid=lvgd.lv_grid,
string_id=i,
branch_no=branch_no + branch_count_sum,
load_no=house_branch,
peak_load=average_load,
consumption={
'residential': average_consumption})
# add lv_load to graph
lvgd.lv_grid.add_load(lv_load)
cable_name = row['cable type'] + \
' 4x1x{}'.format(row['cable width'])
cable_type = lvgd.lv_grid.network.static_data[
'LV_cables'].loc[cable_name]
# connect current lv_cable_dist to station
if house_branch == 1:
# edge connect first house branch in branch with the station
lvgd.lv_grid.graph.add_edge(
lvgd.lv_grid.station(),
lv_cable_dist,
branch=BranchDing0(
length=row['distance house branch'],
kind='cable',
grid=lvgd.lv_grid,
type=cable_type,
id_db='branch_{sector}{branch}_{load}'.format(
branch=hh_branch,
load=house_branch,
sector='HH')
))
# connect current lv_cable_dist to last one
else:
lvgd.lv_grid.graph.add_edge(
lvgd.lv_grid._cable_distributors[-4],
lv_cable_dist,
branch=BranchDing0(
length=row['distance house branch'],
kind='cable',
grid=lvgd.lv_grid,
type=lvgd.lv_grid.network.static_data[
'LV_cables'].loc[cable_name],
id_db='branch_{sector}{branch}_{load}'.format(
branch=hh_branch,
load=house_branch,
sector='HH')))
# connect house to cable distributor
house_cable_name = row['cable type {}'.format(variant)] + \
' 4x1x{}'.format(
row['cable width {}'.format(variant)])
lvgd.lv_grid.graph.add_edge(
lv_cable_dist,
lv_cable_dist_building,
branch=BranchDing0(
length=row['length house branch {}'.format(
variant)],
kind='cable',
grid=lvgd.lv_grid,
type=lvgd.lv_grid.network.static_data['LV_cables']. \
loc[house_cable_name],
id_db='branch_{sector}{branch}_{load}'.format(
branch=hh_branch,
load=house_branch,
sector='HH'))
)
lvgd.lv_grid.graph.add_edge(
lv_cable_dist_building,
lv_load,
branch=BranchDing0(
length=1,
kind='cable',
grid=lvgd.lv_grid,
type=lvgd.lv_grid.network.static_data['LV_cables']. \
loc[house_cable_name],
id_db='branch_{sector}{branch}_{load}'.format(
branch=hh_branch,
load=house_branch,
sector='HH'))
)
[docs]def build_residential_branches(lvgd):
"""Based on population and identified peak load data, the according grid
topology for residential sector is determined and attached to the grid graph
Parameters
----------
lvgd : LVGridDistrictDing0
Low-voltage grid district object
"""
# Choice of typified lv model grid depends on population within lv
# grid district. If no population is given, lv grid is omitted and
# load is represented by lv station's peak load
if lvgd.population > 0 \
and lvgd.peak_load_residential > 0:
model_grid = select_grid_model_residential(lvgd)
build_lv_graph_residential(lvgd, model_grid)
# no residential load but population
elif lvgd.population > 0 \
and lvgd.peak_load_residential == 0:
logger.warning(
'{} has population but no residential load. '
'No grid is created.'.format(
repr(lvgd)))
# residential load but no population
elif lvgd.population == 0 \
and lvgd.peak_load_residential > 0:
logger.warning(
'{} has no population but residential load. '
'No grid is created and thus this load is '
'missing in overall balance!'.format(
repr(lvgd)))
else:
logger.info(
'{} has got no residential load. '
'No grid is created.'.format(
repr(lvgd)))