Repository: openego/ding0 (language: python)

Issue:
Fix and refactor PyPSA CSV export
The export needs a refactoring aligned to #364 .
Please make sure the format is in line with the latest [PyPSA version](https://github.com/PyPSA/PyPSA/releases) as this is what is expected by eDisGo.

Consider #365, #363, #356, #336, #320
Population in csv Export always zero
The population in the MV grid district that is exported to the network.csv file is always zero. In eDisGo it would be helpful to have the correct population, if it is available information in ding0.
Fix the false metadata version
Fixes #252


Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 50bc541e..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-dist: bionic
-language: python
-python:
-  - "3.6.10"
-  # - "3.7"
-# command to install dependencies
-install:
-  - pip install -r requirements.txt -r dev_requirements.txt
-  - pip freeze
-# command to run tests
-script:
-  - pytest -vv
\ No newline at end of file
diff --git a/dev_requirements.txt b/dev_requirements.txt
index c5a08e72..997dd67b 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -2,4 +2,5 @@ pytest
 pytest-dependency
 pytest-parallel
 sphinx
-sphinx_rtd_theme
\ No newline at end of file
+sphinx_rtd_theme
+Cython
diff --git a/ding0/config/config_db_credentials.cfg b/ding0/config/config_db_credentials.cfg
new file mode 100644
index 00000000..863db212
--- /dev/null
+++ b/ding0/config/config_db_credentials.cfg
@@ -0,0 +1,6 @@
+[database_credentials]
+name = name
+host: ip_address
+port: port
+user: username
+password: password
diff --git a/ding0/config/config_db_tables.cfg b/ding0/config/config_db_tables.cfg
index 33ef9285..9b9d5480 100644
--- a/ding0/config/config_db_tables.cfg
+++ b/ding0/config/config_db_tables.cfg
@@ -26,5 +26,33 @@ re_generators = t_ego_dp_res_powerplant_sq_mview
 conv_generators = t_ego_dp_conv_powerplant_sq_mview
 version = v0.4.5
 
+[local]
+orm_mv_grid_districts = grid.egon_mv_grid_district
+orm_mv_stations = grid.egon_hvmv_substation
+orm_lv_load_areas = demand.egon_loadarea
+# OSM ways for street graphs
+osm_ways_with_segments = openstreetmap.osm_ways_with_segments
+# All residential and cts buildings, geoms and peak loads
+egon_map_zensus_mvgd_buildings = boundaries.egon_map_zensus_mvgd_buildings
+osm_buildings_filtered = openstreetmap.osm_buildings_filtered
+osm_buildings_synthetic = openstreetmap.osm_buildings_synthetic
+building_peak_loads = demand.egon_building_electricity_peak_loads
+household_electricity_profile = demand.egon_household_electricity_profile_of_buildings
+
+# Industrial loads 1 and geoms
+egon_sites_ind_load_curves_individual = demand.egon_sites_ind_load_curves_individual
+egon_industrial_sites = demand.egon_industrial_sites
+# Industrial loads 2 and geoms
+egon_osm_ind_load_curves_individual = demand.egon_osm_ind_load_curves_individual
+osm_landuse = openstreetmap.osm_landuse
+# Generators
+generators_pv = supply.egon_power_plants_pv
+generators_pv_rooftop = supply.egon_power_plants_pv_roof_building
+generators_wind = supply.egon_power_plants_wind
+generators_biomass = supply.egon_power_plants_biomass
+generators_water = supply.egon_power_plants_hydro
+# Weather cells
+weather_cells = supply.egon_era5_weather_cells
+
 [input_data_source]
-input_data = versioned
\ No newline at end of file
+input_data = local
diff --git a/ding0/config/config_lv_grids_osm.py b/ding0/config/config_lv_grids_osm.py
new file mode 100644
index 00000000..f8a3cdd2
--- /dev/null
+++ b/ding0/config/config_lv_grids_osm.py
@@ -0,0 +1,283 @@
+"""
+config file for synthetic low voltage grids 
+parameterized by load profiles.
+
+"""
+
+def get_config_osm(key):
+    
+    """
+    return config for osm processing.
+    ...
+    ...
+    mv_lv_threshold_capacity: loads with capacity < 200 kW are connected in grid
+                              loads wi. capacity >= 200 kW are separately
+    
+    avg_cluster_capacity    : n_cluster = sum(peak_load) / avg_cluster_capacity
+                              Clusters are NOT homogenous
+                              
+    additional_trafo_capacity: e.g. 1.5; so, transformers have max load factor of 70%.
+                               capacity_to_cover = capacity_of_area * 1.5
+                               
+    diversity_factor_not_residential: 0.6
+    src.: https://assets.new.siemens.com/siemens/assets/api/uuid:d683c81df25afb360b79c5d48441eeda8b23477b/planung-der-elektrischen-energieverteilung-technische-grundlagen.pdf, page 18. 23.12.2021.
+
+    """
+
+    config_osm = {
+        
+        'srid' : 3035,
+        'EARTH_RADIUS_M' : 6_371_009,  # deprecated!? not mandatory anymore?!
+        'lv_threshold_capacity' : 100,      # < 100kW connected to grid
+        'mv_lv_threshold_capacity' : 200,   # 100 to < 200kW connected to station directly
+        'hv_mv_threshold_capacity' : 5500,  # 200 to < 5.5MW own station with trafos
+        'additional_trafo_capacity' : 1.0,
+        'avg_trafo_size' : 500,
+        'avg_square_meters' : 100,
+        'quadrat_width' : 1000,
+        'dist_edge_segments' : 50,
+        'cluster_increment_counter_threshold' : 20,
+        'ons_dist_threshold' : 1500,
+        'buffer_distance' : [5, 25, 50, 100],
+        'unconn_nodes_ratio' : 0.02,
+        'major_ccs_ratio_threshold' : .30,
+        'get_fully_conn_graph_number_max_it' : 4,
+        'diversity_factor_not_residential' : 0.6,
+    }
+        
+    return config_osm[key]
+
+
+
+def get_charging_station_columns():
+    
+    """ get columns for charging_stations """
+    
+    return ["socket:type2:output", "charging_station:output", "socket:chademo:output", 
+            "socket:type2_cable:output", "socket:type2_combo:output"]
+
+
+
+def get_peak_loads():
+    
+    """ 
+    get dictionary containing categories and peak loads 
+    due to each amenity as average square meters of 100
+    but for charging_stations we use 22 kW, divide by
+    average square meters.
+    """
+    
+    
+    
+    load_profiles = {
+
+        'lodging'      :  73.0,
+        'education'    :  62.2,
+        'office'       :  64.9,
+        'retail'       : 128.9,
+        'fire_brigade' :  12.93,
+        'research'     :  91.51,
+        'health_care'  : 109.3,
+        'industry'     : 164.5,
+        'hotel'        :  62.7,
+        'aggriculture' :  38.29,
+        'restaurant'   :  91.51,
+        'swimming'     :  88.50,
+        'sport'        :  72.72,
+        'technical'    :  13.25,
+        'public'       :  115.8,
+        'charging_station' : 22000.0 / get_config_osm('avg_square_meters'), 
+        'residential' :   17.0847,
+        'leftover'         :  0.1
+
+    }
+        
+    return load_profiles
+    
+
+
+
+def get_load_profile_categories():
+
+    """ get dictionary containing categorie types assigned to categories """
+    #TODO exclude leftover category from building import
+
+    load_profile_categories = {
+        "lodging": "lodging",
+        "education": "education",
+        "language_school": "education",
+        "childcare": "education",
+        "kindergarten": "education",
+        "school": "education",
+        "art_school": "education",
+        "university": "education",
+        "college": "education",
+        "driving_school": "education",
+        "music_school": "education",
+        "office": "office",
+        "money_transfer": "office",
+        "bank": "office",
+        "prep_school": "office",
+        "internet_cafe": "office",
+        "courthouse": "office",
+        "post_depot": "office",
+        "post_office": "office",
+        "studio": "office",
+        "reception_desk": "office",
+        "coworking_space": "office",
+        "conference_centre": "office",
+        "exhibition_centre": "office",
+        "customs": "office",
+        "supermarket": "office",
+        "car_rental": "office",
+        "car_wash": "office",
+        "commercial": "office",
+        "warehouse": "office",
+        "police": "office",
+        "retail": "retail",
+        "fire_station": "fire_brigade",
+        "research": "research",
+        "health_care": "health_care",
+        "hospital": "health_care",
+        "clinic": "health_care",
+        "dentist": "health_care",
+        "doctors": "health_care",
+        "pharmacy": "health_care",
+        "social_facility": "health_care",
+        "veterinary": "health_care",
+        "industrial": "industry",
+        "manufacture": "industry",
+        "industry": "industry",
+        "hotel": "hotel",
+        "aggriculture": "aggriculture",
+        "agricultural": "aggriculture",
+        "barn": "aggriculture",
+        "conservatory": "aggriculture",
+        "cowshed": "aggriculture",
+        "farm_auxiliary": "aggriculture",
+        "greenhouse": "aggriculture",
+        "stable": "aggriculture",
+        "restaurant": "restaurant",
+        "bar": "restaurant",
+        "biergarten": "restaurant",
+        "cafe": "restaurant",
+        "fast_food": "restaurant",
+        "food_court": "restaurant",
+        "ice_cream": "restaurant",
+        "pub": "restaurant",
+        "canteen": "restaurant",
+        "swimming_bath": "swimming",
+        "swimming_pool": "swimming",
+        "public_bath": "swimming",
+        "sport_building": "sport",
+        "sports_centre": "sport",
+        "grandstand": "sport",
+        "pavilion": "sport",
+        "riding_hall": "sport",
+        "sports_hall": "sport",
+        "stadium": "sport",
+        "technique_building": "technical",
+        "digester": "technical",
+        "service": "technical",
+        "transformer_tower": "technical",
+        "water_tower": "technical",
+        "vehicle_inspection": "technical",
+        "residential": "residential",
+        "retirement_home": "residential",
+        "apartments": "residential",
+        "house": "residential",
+        "brothel": "residential",
+        "bungalow": "residential",
+        "detached": "residential",
+        "semidetached_house": "residential",
+        "semi": "residential",
+        "dormitory": "residential",
+        "farm": "residential",
+        "terrace": "residential",
+        "yes": "residential",
+        "charging_station": "charging_station",
+        "public": "public",
+        "archive": "public",
+        "public_building": "public",
+        "events_venue": "public",
+        "civic": "public",
+        "bakehouse": "public",
+        "community_centre": "public",
+        "library": "public",
+        "toy_library": "public",
+        "social_centre": "public",
+        "parking": "public",
+        "townhall": "public",
+        "government": "public",
+        "train_station": "public",
+        "transportation": "public",
+        "theatre": "public",
+        "arts_centre": "public",
+        "casino": "public",
+        "cinema": "public",
+        "nightclub": "public",
+        "prison": "public",
+        "cathedral": "public",
+        "church": "public",
+        "monastery": "public",
+        "mosque": "public",
+        "religious": "public",
+        "synagogue": "public",
+        "temple": "public",
+        "fuel": "public",
+        "leftover": "leftover",
+        "chapel": "leftover",
+        "place_of_worship": "leftover",
+        "toilets": "leftover",
+        "recycling": "industry",
+        "waste_transfer_station": "industry",
+        "shelter": "leftover",
+        "kiosk": "retail",
+        "nursing_home": "health_care",
+        "planetarium": "public",
+        "gambling": "public",
+        "static_caravan": "leftover",
+        "bicycle_parking": "leftover",
+        "parish_hall": "public",
+        "embassy": "public",
+        "emergency_service": "health_care",
+        "bench": "leftover", #TODO: drop amenities with obviously no consumption
+        "love_hotel": "public",
+        "stripclub": "public",
+        "swingerclub": "public",
+        "research_institute": "research",
+        "school;driving_school": "education",
+        "sailing_school": "education",
+        "job_centre": "public",
+        "dive_centre": "swimming",
+        "student_accommodation": "lodging",
+        "animal_boarding": "leftover",
+        "bicycle_repair_station": "leftover",
+        "parking_space": "leftover", #TODO: drop amenities with obviously no consumption
+        "motorcycle_parking": "leftover",  # TODO: drop amenities with obviously no consumption
+        "dressing_room": "leftover",
+        "funeral_hall": "leftover",
+        "waste_disposal": "leftover", # TODO: drop amenities with obviously no consumption
+        "station": "public",
+        "gatehouse": "leftover",
+        "fountain": "leftover",
+        "residentials_list": [
+            "residential",
+            "retirement_home",
+            "apartments",
+            "house",
+            "brothel",
+            "bungalow",
+            "detached",
+            "semidetached_house",
+            "semi",
+            "dormitory",
+            "farm",
+            "semidetached_house",
+            "terrace",
+            "yes",
+        ],
+    } 
+    
+    return load_profile_categories
+    
diff --git a/ding0/core/__init__.py b/ding0/core/__init__.py
index dc713c75..519cbe40 100644
--- a/ding0/core/__init__.py
+++ b/ding0/core/__init__.py
@@ -7,10 +7,11 @@
 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"
+__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"
+
 # TODO: check docstrings
 
 
@@ -24,35 +25,63 @@
 from ding0.core.powerflow import *
 from ding0.tools.pypsa_io import initialize_component_dataframes, fill_mvgd_component_dataframes
 from ding0.tools.animation import AnimationDing0
-from ding0.tools.plots import plot_mv_topology
+from ding0.tools.plots import plot_mv_topology, plot_lv_topology
 from ding0.flexopt.reinforce_grid import *
 from ding0.tools.logger import get_default_home_dir
 from ding0.tools.tools import merge_two_dicts_of_dataframes
+from ding0.core.network.loads import MVLoadDing0
+from ding0.grid.lv_grid.parameterization import get_peak_load_diversity
+
 
 import os
 import logging
 import pandas as pd
 import random
 import time
+import traceback
 from math import isnan
 
-from sqlalchemy.orm import sessionmaker
 from sqlalchemy import func
 from geoalchemy2.shape import from_shape
 import subprocess
 import json
-import oedialect
 
 if not 'READTHEDOCS' in os.environ:
     from shapely.wkt import loads as wkt_loads
-    from shapely.geometry import Point, MultiPoint, MultiLineString, LineString
+    from shapely.geometry import Point, MultiPoint, MultiLineString, LineString  # PAUL NEW
     from shapely.geometry import shape, mapping
     from shapely.wkt import dumps as wkt_dumps
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 package_path = ding0.__path__[0]
 
+############ NEW TODO CHECK IMPORTS
+
+if 'READTHEDOCS' in os.environ:
+    from shapely.wkt import loads as wkt_loads
+
+from ding0.config.config_lv_grids_osm import get_config_osm
+
+from ding0.grid.lv_grid.graph_processing import update_ways_geo_to_shape, \
+    build_graph_from_ways, create_buffer_polygons, graph_nodes_outside_buffer_polys, \
+    compose_graph, get_fully_conn_graph, split_conn_graph, get_outer_conn_graph, \
+    remove_detours, add_edge_geometry_entry, remove_unloaded_deadends, \
+    flatten_graph_components_to_lines, subdivide_graph_edges, simplify_graph_adv, \
+    create_simple_synthetic_graph
+
+from ding0.grid.lv_grid.clustering import get_cluster_numbers, distance_restricted_clustering
+
+from ding0.grid.lv_grid.routing import assign_nearest_nodes_to_buildings, \
+    get_lvgd_id, identify_street_loads, connect_mv_loads_to_graph
+
+from ding0.grid.lv_grid.geo import get_points_in_load_area, get_convex_hull_from_points, \
+    get_bounding_box_from_points, get_load_center_node, get_load_center_coords
+
+import ding0.tools.egon_data_integration as db_io
+
+############ NEW END
+
 
 class NetworkDing0:
     """
@@ -114,9 +143,9 @@ class NetworkDing0:
         These are used in many parts of ding0's calculations.
         Data values:
 
-        * Typical cable types, and typical line types' electrical impedences,
+        * Typical cable types, and typical line types' electrical impedances,
             thermal ratings, operating voltage level.
-        * Typical transformers types' electrical impedences, voltage drops,
+        * Typical transformers types' electrical impedances, voltage drops,
             thermal ratings, winding voltages
         * Typical LV grid topologies' line types, line lengths and
             distribution
@@ -127,7 +156,7 @@ class NetworkDing0:
 
     """
 
-    def __init__(self, **kwargs):
+    def __init__(self, session, **kwargs):
         self.name = kwargs.get('name', None)
         self._run_id = kwargs.get('run_id', None)
         self._mv_grid_districts = []
@@ -135,7 +164,8 @@ def __init__(self, **kwargs):
         self._config = self.import_config()
         self._pf_config = self.import_pf_config()
         self._static_data = self.import_static_data()
-        self._orm = self.import_orm()
+        self._orm = self.import_orm(session)
+        self.message = []
 
     def mv_grid_districts(self):
         """
@@ -205,7 +235,19 @@ def orm(self):
         """
         return self._orm
 
-    def run_ding0(self, session, mv_grid_districts_no=None, debug=False, export_figures=False):
+    def run_ding0(
+            self,
+            session,
+            mv_grid_districts_no=None,
+            load_area_to_debug=None,
+            debug=False,
+            export_mv_figures=False,
+            export_lv_figures=False,
+            ding0_legacy=False,
+            path=None,
+            peak_load_determination_mode="sum_of_loads"
+    ):
+
         """
         Let DING0 run by shouting at this method (or just call
         it from NetworkDing0 instance). This method is a wrapper
@@ -218,10 +260,16 @@ def run_ding0(self, session, mv_grid_districts_no=None, debug=False, export_figu
         mv_grid_districts_no : :obj:`list` of :obj:`int` objects.
             List of MV grid_districts/stations to be imported (if empty,
             all grid_districts & stations are imported)
+        load_area_to_debug: int or None
+            If load_area_id is set, only the load area is build.
         debug : obj:`bool`, defaults to False
             If True, information is printed during process
-        export_figures : :obj:`bool`, defaults to False
-            If True, figures are shown or exported (default path: ~/.ding0/) during run.
+        export_mv_figures : :obj:`bool`, defaults to False
+            If True, mv figures are shown or exported during run.
+        export_lv_figures : :obj:`bool`, defaults to False
+            If True, lv figures are shown or exported during run.
+        path : :obj:`str` or None , defaults to None
+            Set path to save the figures if not None
 
         Returns
         -------
@@ -294,73 +342,73 @@ def run_ding0(self, session, mv_grid_districts_no=None, debug=False, export_figu
             The rings are finally closed to hold a complete graph (if the SDs are open,
             the edges adjacent to a SD will not be exported!)
         """
+
+        # TODO run ding0 needs to consider new features
         if debug:
             start = time.time()
 
-        # STEP 1: Import MV Grid Districts and subjacent objects
-        self.import_mv_grid_districts(session,
-                                      mv_grid_districts_no=mv_grid_districts_no)
+        logger.info("STEP 1: Import MV Grid Districts and subjacent objects")
+        self.import_mv_grid_districts(
+            session,
+            mv_grid_districts_no,
+            ding0_legacy=ding0_legacy,
+            peak_load_determination_mode=peak_load_determination_mode,
+            load_area_to_debug=load_area_to_debug,
+        )
 
-        # STEP 2: Import generators
+        logger.info("STEP 2: Import generators")
         self.import_generators(session, debug=debug)
 
-        # STEP 3: Parametrize MV grid
+        logger.info("STEP 3: Parametrize MV grid")
         self.mv_parametrize_grid(debug=debug)
 
-        # STEP 4: Validate MV Grid Districts
+        logger.info("STEP 4: Validate MV Grid Districts")
         msg = self.validate_grid_districts()
+        self.message.append(msg)
 
-        # STEP 5: Build LV grids
+        logger.info("STEP 5: Build LV grids")
         self.build_lv_grids()
+        if export_lv_figures:
+            self.plot_lv_grids(path=path, filename="lv_grid_building_completed")
 
-        # STEP 6: Build MV grids
+        logger.info("STEP 6: Build MV grids")
         self.mv_routing(debug=False)
-        if export_figures:
-            grid = self._mv_grid_districts[0].mv_grid
-            plot_mv_topology(grid, subtitle='Routing completed', filename='1_routing_completed.png')
+        if export_mv_figures:
+            self.plot_mv_grids(path=path, filename='1_routing_completed')
 
-        # STEP 7: Connect MV and LV generators
+        logger.info("STEP 7: Connect MV and LV generators")
         self.connect_generators(debug=False)
-        if export_figures:
-            plot_mv_topology(grid, subtitle='Generators connected', filename='2_generators_connected.png')
+        if export_mv_figures:
+            self.plot_mv_grids(path=path, filename='2_generators_connected')
 
-        # STEP 8: Relocate switch disconnectors in MV grid
+        logger.info("STEP 8: Relocate switch disconnectors in MV grid")
         self.set_circuit_breakers(debug=debug)
-        if export_figures:
-            plot_mv_topology(grid, subtitle='Circuit breakers relocated', filename='3_circuit_breakers_relocated.png')
+        if export_mv_figures:
+            self.plot_mv_grids(path=path, filename='3_circuit_breakers_relocated')
 
-        # STEP 9: Open all switch disconnectors in MV grid
+        logger.info("STEP 9: Open all switch disconnectors in MV grid")
         self.control_circuit_breakers(mode='open')
 
-        # STEP 10: Do power flow analysis of MV grid
+        logger.info("STEP 10: Do power flow analysis of MV grid")
         self.run_powerflow(session, method='onthefly', export_pypsa=False, debug=debug)
-        if export_figures:
-            plot_mv_topology(grid, subtitle='PF result (load case)',
-                             filename='4_PF_result_load.png',
-                             line_color='loading', node_color='voltage', testcase='load')
-            plot_mv_topology(grid, subtitle='PF result (feedin case)',
-                             filename='5_PF_result_feedin.png',
-                             line_color='loading', node_color='voltage', testcase='feedin')
-
-        # STEP 11: Reinforce MV grid
+        if export_mv_figures:
+            self.plot_mv_grids(path=path, filename='4_PF_result_load')
+            self.plot_mv_grids(path=path, filename='5_PF_result_feedin')
+
+        logger.info("STEP 11: Reinforce MV grid")
         self.reinforce_grid()
 
-        # STEP 12: Close all switch disconnectors in MV grid
+        logger.info("STEP 12: Close all switch disconnectors in MV grid")
         self.control_circuit_breakers(mode='close')
-
-        if export_figures:
-            plot_mv_topology(grid, subtitle='Final grid PF result (load case)',
-                             filename='6_final_grid_PF_result_load.png',
-                             line_color='loading', node_color='voltage', testcase='load')
-            plot_mv_topology(grid, subtitle='Final grid PF result (feedin case)',
-                             filename='7_final_grid_PF_result_feedin.png',
-                             line_color='loading', node_color='voltage', testcase='feedin')
+        if export_mv_figures:
+            self.plot_mv_grids(path=path, filename='6_final_grid_PF_result_load')
+            self.plot_mv_grids(path=path, filename='7_final_grid_PF_result_feedin')
 
         if debug:
             logger.info('Elapsed time for {0} MV Grid Districts (seconds): {1}'.format(
                 str(len(mv_grid_districts_no)), time.time() - start))
 
-        return msg
+        return self.message
 
     def get_mvgd_lvla_lvgd_obj_from_id(self):
         """
@@ -426,18 +474,15 @@ def get_mvgd_lvla_lvgd_obj_from_id(self):
 
         return mv_grid_districts_dict, lv_load_areas_dict, lv_grid_districts_dict, lv_stations_dict
 
-    def build_mv_grid_district(self, poly_id, subst_id, grid_district_geo_data,
-                        station_geo_data):
+    def build_mv_grid_district(self, subst_id, grid_district_geo_data, station_geo_data):
         """
         Initiates single MV grid_district including station and grid
 
         Parameters
         ----------
 
-        poly_id: :obj:`int`
-            ID of grid_district according to database table. Also used as ID for created grid #TODO: check type
         subst_id: :obj:`int`
-            ID of station according to database table #TODO: check type
+            ID of station and mv_grid_district according to database table Also used as ID for created grid.
         grid_district_geo_data: :shapely:`Shapely Polygon object<polygons>`
             Polygon of grid district, The geo-spatial polygon
             in the coordinate reference system with the
@@ -455,12 +500,13 @@ def build_mv_grid_district(self, poly_id, subst_id, grid_district_geo_data,
 
         """
 
+        logger.info(f"Build MV grid district: {subst_id}")
         mv_station = MVStationDing0(id_db=subst_id, geo_data=station_geo_data)
 
         mv_grid = MVGridDing0(network=self,
-                              id_db=poly_id,
+                              id_db=subst_id,
                               station=mv_station)
-        mv_grid_district = MVGridDistrictDing0(id_db=poly_id,
+        mv_grid_district = MVGridDistrictDing0(id_db=subst_id,
                                                mv_grid=mv_grid,
                                                geo_data=grid_district_geo_data)
         mv_grid.grid_district = mv_grid_district
@@ -539,9 +585,9 @@ def build_lv_grid_district(self,
                 peak_load_industrial=row['peak_load_industrial'],
                 peak_load_agricultural=row['peak_load_agricultural'],
                 peak_load=(row['peak_load_residential'] +
-                               row['peak_load_retail'] +
-                               row['peak_load_industrial'] +
-                               row['peak_load_agricultural']),
+                           row['peak_load_retail'] +
+                           row['peak_load_industrial'] +
+                           row['peak_load_agricultural']),
                 sector_count_residential=int(row['sector_count_residential']),
                 sector_count_retail=int(row['sector_count_retail']),
                 sector_count_industrial=int(row['sector_count_industrial']),
@@ -567,8 +613,8 @@ def build_lv_grid_district(self,
                 grid=lv_grid,
                 lv_load_area=lv_load_area,
                 geo_data=wkt_loads(lv_stations.loc[id, 'geom'])
-                            if id in lv_stations.index.values
-                            else lv_load_area.geo_centre,
+                if id in lv_stations.index.values
+                else lv_load_area.geo_centre,
                 peak_load=lv_grid_district.peak_load)
 
             # assign created objects
@@ -578,7 +624,15 @@ def build_lv_grid_district(self,
             lv_grid_district.lv_grid = lv_grid
             lv_load_area.add_lv_grid_district(lv_grid_district)
 
-    def import_mv_grid_districts(self, session, mv_grid_districts_no=None):
+    def import_mv_grid_districts(
+            self,
+            session,
+            mv_grid_districts_no,
+            ding0_legacy=False,
+            create_lvgd_geo_method='convex_hull',
+            peak_load_determination_mode="sum_of_loads",
+            load_area_to_debug=None
+    ):
         """
         Imports MV Grid Districts, HV-MV stations, Load Areas, LV Grid Districts
         and MV-LV stations, instantiates and initiates objects.
@@ -590,6 +644,17 @@ def import_mv_grid_districts(self, session, mv_grid_districts_no=None):
         mv_grid_districts : :obj:`list` of :obj:`int`
             List of MV grid_districts/stations (int) to be imported (if empty,
             all grid_districts & stations are imported)
+        load_area_to_debug: int or None
+            If load_area_id is set, only the load area is build.
+        ding0_legacy: if True ding0 run
+                        else: build new lv_districts...
+
+        local_db: parameterize buildings from osm if True
+
+        peak_load_determination_mode: `str`
+            - "sum_of_loads" - sums all loads
+            - "diversity_equation" - calculate peak load with equation from literature
+            out of diversity factors and the number of households
 
         See Also
         --------
@@ -608,65 +673,328 @@ def import_mv_grid_districts(self, session, mv_grid_districts_no=None):
         except OSError:
             logger.exception('cannot open config file.')
 
-        # build SQL query
-        grid_districts = session.query(self.orm['orm_mv_grid_districts'].subst_id,
-                                       func.ST_AsText(func.ST_Transform(
-                                           self.orm['orm_mv_grid_districts'].geom, srid)). \
-                                       label('poly_geom'),
-                                       func.ST_AsText(func.ST_Transform(
-                                           self.orm['orm_mv_stations'].point, srid)). \
-                                       label('subs_geom')).\
-            join(self.orm['orm_mv_stations'], self.orm['orm_mv_grid_districts'].subst_id ==
-                 self.orm['orm_mv_stations'].subst_id).\
-            filter(self.orm['orm_mv_grid_districts'].subst_id.in_(mv_grid_districts_no)). \
-            filter(self.orm['version_condition_mvgd']). \
-            filter(self.orm['version_condition_mv_stations']). \
-            distinct()
-
-        # read MV data from db
-        mv_data = pd.read_sql_query(grid_districts.statement,
-                                    session.bind,
-                                    index_col='subst_id')
+        mv_data = db_io.get_mv_data(self.orm, session, mv_grid_districts_no)
 
         # iterate over grid_district/station datasets and initiate objects
-        for poly_id, row in mv_data.iterrows():
-            subst_id = poly_id
+        for subst_id, row in mv_data.iterrows():
             region_geo_data = wkt_loads(row['poly_geom'])
-
-            # transform `region_geo_data` to epsg 3035
-            # to achieve correct area calculation of mv_grid_district
             station_geo_data = wkt_loads(row['subs_geom'])
-            # projection = partial(
-            #     pyproj.transform,
-            #     pyproj.Proj(init='epsg:4326'),  # source coordinate system
-            #     pyproj.Proj(init='epsg:3035'))  # destination coordinate system
-            #
-            # region_geo_data = transform(projection, region_geo_data)
-
-            mv_grid_district = self.build_mv_grid_district(poly_id,
-                                             subst_id,
-                                             region_geo_data,
-                                             station_geo_data)
-
-            # import all lv_stations within mv_grid_district
-            lv_stations = self.import_lv_stations(session)
-
-            # import all lv_grid_districts within mv_grid_district
-            lv_grid_districts = self.import_lv_grid_districts(session, lv_stations)
-
-            # import load areas
-            self.import_lv_load_areas(session,
-                                      mv_grid_district,
-                                      lv_grid_districts,
-                                      lv_stations)
+            # transform to epsg 3035
+            proj_source = Transformer.from_crs("epsg:4326", "epsg:3035", always_xy=True).transform
+            station_geo_data = transform(proj_source, station_geo_data)
+            mv_grid_district = self.build_mv_grid_district(subst_id, region_geo_data, station_geo_data)
+
+            #### TODO: check ding0_legacy
+            if ding0_legacy:
+
+                # import all lv_stations within mv_grid_district
+                lv_stations = self.import_lv_stations(session)
+
+                # import all lv_grid_districts within mv_grid_district
+                lv_grid_districts = self.import_lv_grid_districts(session, lv_stations)
+
+                # import load areas
+                self.import_lv_load_areas(session, mv_grid_district,
+                                          lv_grid_districts, lv_stations)
+
+
+            else:
+                self.import_lv_load_areas_and_build_new_lv_districts(
+                    session,
+                    mv_grid_district,
+                    create_lvgd_geo_method,
+                    peak_load_determination_mode,
+                    load_area_to_debug=load_area_to_debug,
+                )
 
             # add sum of peak loads of underlying lv grid_districts to mv_grid_district
             mv_grid_district.add_peak_demand()
 
         logger.info('=====> MV Grid Districts imported')
 
-    def import_lv_load_areas(self, session, mv_grid_district, lv_grid_districts,
-                             lv_stations):
+    def import_lv_load_areas_and_build_new_lv_districts(
+            self,
+            session,
+            mv_grid_district,
+            create_lvgd_geo_method,
+            peak_load_determination_mode,
+            load_area_to_debug=None,
+    ):
+
+        """
+        Imports load_areas (load areas) from database for a single MV grid_district
+        And compute new lv_districts
+
+        Parameters
+        ----------
+        session : :sqlalchemy:`SQLAlchemy session object<orm/session_basics.html>`
+            Database session
+        mv_grid_district : MV grid_district/station (instance of MVGridDistrictDing0 class) for
+            which the import of load areas is performed
+        load_area_to_debug: int or None
+            If load_area_id is set, only the load area is build.
+        """
+
+        lv_load_areas = db_io.get_lv_load_areas(self.orm, session, mv_grid_district.mv_grid._station.id_db)
+        if load_area_to_debug:
+            lv_load_areas = lv_load_areas.loc[[load_area_to_debug], :]
+        # create load_area objects from rows and add them to graph
+        logger.info(f"Creating load areas: {lv_load_areas.index.to_list()}")
+        for id_db, row in lv_load_areas.iterrows():
+            logger.info(f"Build LV Load Area: {id_db}")
+            # Transform geo_load_area from str to shape.
+            # Note: Buildings without buffer, ways with buffer.
+            geo_load_area = wkt_loads(row.geo_area)
+            # Get buffer_poly_list.
+            buffer_poly_list = create_buffer_polygons(geo_load_area)
+            # Load ways from db for last element of buffer_poly_list.
+            ways_sql_df = db_io.get_egon_ways(self.orm, session, buffer_poly_list[-1].wkt)
+            # If ways found in query build graph from osm data.
+            if not ways_sql_df.empty:
+                # Transform ways to shape.
+                ways_sql_df = update_ways_geo_to_shape(ways_sql_df)
+                # Build graph
+                graph = build_graph_from_ways(ways_sql_df)
+                # Get nodes to remove from graph (per buffer polygon).
+                # "outlier_nodes_list" is a list of lists
+                outlier_nodes_list = graph_nodes_outside_buffer_polys(graph, ways_sql_df, buffer_poly_list)
+
+            # If no osm ways data could be found create synthetic graph.
+            else:
+                # Create synthetic graph with one street node instead.
+                graph, node_id = create_simple_synthetic_graph(geo_load_area)
+                # No outlier nodes in synthetic graph, nested list must be empty.
+                outlier_nodes_list = [[]]
+                logger.warning(f'ways_sql_df.empty. No ways found in '
+                               f'MV {mv_grid_district}, LA {id_db} '
+                               f'Build synthetic graph instead.')
+
+            # Inner_node_list define nodes without buffer.
+            inner_node_list = list(set(graph.nodes()) - set(outlier_nodes_list[0]))
+
+            if len(inner_node_list) < 1:
+                logger.warning(f'No graph found in origin polygon of MV {mv_grid_district}, LA {id_db}.')
+                continue
+
+            # Get connected graph.
+            conn_graph, synthetic_edges = get_fully_conn_graph(graph, outlier_nodes_list)
+            # Split "fully_conn_graph" in inner and outer part.
+            inner_graph, outer_graph = split_conn_graph(conn_graph, inner_node_list, synthetic_edges)
+
+            # "subdivide_graph_edges" for only graph in list.
+            # "edges_to_remove" are edges which are subdivided into 20m segments.
+            # Process inner graph
+            graph_subdiv = subdivide_graph_edges(inner_graph)
+
+            # Process outer graph
+            outer_graph = get_outer_conn_graph(outer_graph, inner_node_list)
+            outer_graph = add_edge_geometry_entry(outer_graph)
+            outer_graph = flatten_graph_components_to_lines(outer_graph, inner_node_list)
+            outer_graph = remove_detours(outer_graph)
+
+            # Compose graph
+            composed_graph = compose_graph(outer_graph, graph_subdiv)
+
+            # Get buildings with loads from database.
+            buildings_w_loads_df = db_io.get_egon_buildings(
+                self.orm, session, mv_grid_district.id_db, row
+            )
+            if buildings_w_loads_df.empty:
+                logger.error(f"Load area {id_db=} has no buildings!")
+                continue
+
+            # If composed graph of type synthetic (no osm ways have been found), then
+            # update graph node's coord (geo load center) using building's positions
+            # and peak loads.
+            if composed_graph.graph['source'] == 'synthetic':
+                x, y = get_load_center_coords(buildings_w_loads_df)
+                composed_graph.nodes[node_id]['x'] = x
+                composed_graph.nodes[node_id]['y'] = y
+
+            # Assign nearest nodes
+            buildings_w_loads_df = assign_nearest_nodes_to_buildings(composed_graph,
+                                                                     buildings_w_loads_df)
+
+            # Get nodes of graph to keep -> "street_loads_df".
+            # "street_loads_df" contains nearest nodes (nn) and cumulated load per nn.
+            street_loads_df, _ = identify_street_loads(buildings_w_loads_df, composed_graph)
+
+            # Simplify graph to keep overview
+            simp_graph = simplify_graph_adv(composed_graph, street_loads_df.index.tolist())
+            simp_graph = remove_unloaded_deadends(simp_graph, street_loads_df.index.tolist())
+
+            # Get n_clusters based on capacity of the load area.
+            n_cluster = get_cluster_numbers(buildings_w_loads_df, simp_graph)
+
+            # Cluster graph and locate lv stations based on load center per cluster.
+            clustering_successfully, cluster_graph, mvlv_subst_list, nodes_w_labels = \
+                distance_restricted_clustering(
+                    simp_graph, n_cluster, street_loads_df, mv_grid_district, id_db
+                )
+            if not clustering_successfully:
+                return f"Clustering not successful for " \
+                       f"MV {mv_grid_district}, LA {id_db}"
+
+            # Get loads on mv level.
+            loads_mv_df = buildings_w_loads_df.loc[
+                (get_config_osm('mv_lv_threshold_capacity') <= buildings_w_loads_df.capacity) &
+                (buildings_w_loads_df.capacity < get_config_osm('hv_mv_threshold_capacity'))]
+
+            for osm_id_building, loads_mv_df_row in loads_mv_df.iterrows():
+                connect_mv_loads_to_graph(cluster_graph, osm_id_building, loads_mv_df_row)
+
+            # Calculation peak load for load area with buildings < 200 kW.
+            loads_lv_df = buildings_w_loads_df.loc[
+                buildings_w_loads_df.capacity < get_config_osm('mv_lv_threshold_capacity')
+            ]
+
+            # Set cluster ID for buildings.
+            loads_lv_df['cluster'] = loads_lv_df.nn.map(nodes_w_labels.cluster)
+
+            # Create LVLoadAreaDing0
+            # TODO: if peak load smaller than threshold: do not add
+            if peak_load_determination_mode == "sum_of_loads":
+                # Calculate peak load base by sum of building loads.
+                peak_load = loads_lv_df.capacity.sum()
+            elif peak_load_determination_mode == "diversity_equation":
+                # Calculate peak load based on diversity of loads.
+                peak_load = get_peak_load_diversity(loads_lv_df)
+            else:
+                raise ValueError("False peak load determination mode.")
+
+            lv_load_area = LVLoadAreaDing0(id_db=id_db,
+                                           db_data=row,
+                                           mv_grid_district=mv_grid_district,
+                                           peak_load=peak_load,
+                                           load_area_graph=cluster_graph)
+
+            # Add MV Loads to LV Load Area
+            for building_id, row in loads_mv_df.iterrows():
+                # Create MVLoadDing0
+                mv_load = MVLoadDing0(geo_data=row.geometry,
+                                      grid=mv_grid_district,
+                                      peak_load=row.capacity,  # in kW
+                                      peak_load_residential=row.residential_capacity,
+                                      number_households=row.number_households,
+                                      peak_load_cts=row.cts_capacity,
+                                      peak_load_industrial=row.industrial_capacity,
+                                      building_id=building_id,
+                                      osmid_building=building_id,
+                                      osmid_nn=row.nn,
+                                      nn_coords=row.nn_coords,
+                                      lv_load_area=lv_load_area,
+                                      type="conventional_load")
+
+                # Add mv_load to mv_grid_district and lv_load_area
+                mv_grid_district.mv_grid.add_load(mv_load)
+                lv_load_area.add_mv_load(mv_load)
+
+            # Create LVGridDistrictDing0, LVGridDing and LVStationDing0
+            # for each cluster.
+            for mvlv_subst_loc in mvlv_subst_list:
+
+                cluster_id = mvlv_subst_loc.get('cluster')
+
+                lvgd_id = get_lvgd_id(id_db, cluster_id)
+                buildings = loads_lv_df.loc[loads_lv_df.cluster == cluster_id]
+
+                if (create_lvgd_geo_method == 'convex_hull') | (create_lvgd_geo_method == 'bounding_box'):
+                    # Get convex hull per cluster.
+                    cluster_geo_list = buildings.geometry.tolist()  # geo of building
+                    cluster_geo_list += nodes_w_labels.loc[
+                        nodes_w_labels.cluster == mvlv_subst_loc.get('cluster')].geometry.tolist()
+
+                    # Get convex hull for ding0 objects.
+                    points = get_points_in_load_area(cluster_geo_list)
+                    if create_lvgd_geo_method == 'convex_hull':
+                        polygon = get_convex_hull_from_points(points)
+                    elif create_lvgd_geo_method == 'bounding_box':
+                        polygon = get_bounding_box_from_points(points)
+                    else:
+                        logging.warning(f'create_lvgd_geo_method {create_lvgd_geo_method} not implemented.')
+
+                elif create_lvgd_geo_method == 'off':
+                    polygon = None
+
+                else:
+                    logging.warning(f'create_lvgd_geo_method {create_lvgd_geo_method} not implemented.')
+
+                # Create LVGridDistrictDing0
+                # Therefore calculate "peak_load_div"
+                if peak_load_determination_mode == "sum_of_loads":
+                    # Calculate peak load base by sum of building loads.
+                    peak_load_div = buildings.capacity.sum()
+                elif peak_load_determination_mode == "diversity_equation":
+                    # Calculate peak load based on diversity of loads.
+                    peak_load_div = get_peak_load_diversity(buildings)
+                else:
+                    raise ValueError("False peak load determination mode.")
+
+                if peak_load_div > 0:
+                    # Create LVGridDistrictDing0
+                    lv_grid_district = LVGridDistrictDing0(mvlv_subst_id=lvgd_id,
+                                                           geo_data=polygon,
+                                                           graph_district=mvlv_subst_loc.get('graph_district'),
+                                                           lv_load_area=lv_load_area,
+                                                           buildings_district=buildings,
+                                                           id_db=lvgd_id,
+                                                           peak_load=peak_load_div)
+
+                    # Create LVGridDing0
+                    # Be aware, lv_grid takes grid district's geom!
+                    lv_nominal_voltage = cfg_ding0.get('assumptions', 'lv_nominal_voltage')
+                    lv_grid = LVGridDing0(network=self,
+                                          grid_district=lv_grid_district,
+                                          id_db=lvgd_id,
+                                          geo_data=polygon,
+                                          v_level=lv_nominal_voltage)
+
+                    # Create LVStationDing0
+                    # osm_id_node: Defined node in graph where station is located
+                    lv_station = LVStationDing0(
+                        id_db=lvgd_id,
+                        grid=lv_grid,
+                        lv_load_area=lv_load_area,
+                        geo_data=Point(mvlv_subst_loc.get('x'), mvlv_subst_loc.get('y')),
+                        osm_id_node=mvlv_subst_loc.get('osmid')
+                    )
+
+                    # Assign created objects
+                    # Note: Creation of LV grid is done separately,
+                    # see NetworkDing0.build_lv_grids()
+                    lv_grid.add_station(lv_station)
+                    lv_grid_district.lv_grid = lv_grid
+                    lv_load_area.add_lv_grid_district(lv_grid_district)
+
+            # Calculate load center to set lv_load_area_centre_geo_data based on
+            # peak load and position of lvgd.station
+            # Note: MVLoads are not considered.
+            if len(lv_load_area._lv_grid_districts):  # just in case there are stations
+                la_centre_osmid, la_centre_geo_data, load_area_geo = get_load_center_node(lv_load_area)
+            else:
+                # raise ValueError("No lvgd available and load_area without lv_grid_district")
+                la_centre_osmid = None
+                la_centre_geo_data = lv_load_area.geo_centre
+                load_area_geo = lv_load_area.geo_area
+
+            # Update shape of load_area, if centre does not
+            # intersect with original load area.
+            lv_load_area.geo_area = load_area_geo
+
+            # Create new centre object for Load Area.
+            lv_load_area_centre = LVLoadAreaCentreDing0(id_db=id_db,
+                                                        geo_data=la_centre_geo_data,
+                                                        osm_id_node=la_centre_osmid,
+                                                        lv_load_area=lv_load_area,
+                                                        grid=mv_grid_district.mv_grid)
+
+            # Links the centre object to Load Area.
+            lv_load_area.lv_load_area_centre = lv_load_area_centre
+
+            # Add Load Area to MV grid district.
+            mv_grid_district.add_lv_load_area(lv_load_area)
+
+    def import_lv_load_areas(self, session, mv_grid_district, lv_grid_districts, lv_stations):
         """
         Imports load_areas (load areas) from database for a single MV grid_district
 
@@ -685,13 +1013,13 @@ def import_lv_load_areas(self, session, mv_grid_district, lv_grid_districts,
         # get ding0s' standard CRS (SRID)
         srid = str(int(cfg_ding0.get('geo', 'srid')))
         # SET SRID 3035 to achieve correct area calculation of lv_grid_district
-        #srid = '3035'
+        # srid = '3035'
 
         # threshold: load area peak load, if peak load < threshold => disregard
         # load area
         lv_loads_threshold = cfg_ding0.get('mv_routing', 'load_area_threshold')
 
-        gw2kw = 10 ** 6  # load in database is in GW -> scale to kW
+        mw2kw = 10 ** 3  # load in database is in GW -> scale to kW
 
         # build SQL query
         lv_load_areas_sqla = session.query(
@@ -714,30 +1042,31 @@ def import_lv_load_areas(self, session, mv_grid_district, lv_grid_districts,
             self.orm['orm_lv_load_areas'].sector_count_industrial,
             self.orm['orm_lv_load_areas'].sector_count_agricultural,
             self.orm['orm_lv_load_areas'].nuts.label('nuts_code'),
-            func.ST_AsText(func.ST_Transform(self.orm['orm_lv_load_areas'].geom, srid)).\
+            func.ST_AsText(func.ST_Transform(self.orm['orm_lv_load_areas'].geom, srid)). \
                 label('geo_area'),
-            func.ST_AsText(func.ST_Transform(self.orm['orm_lv_load_areas'].geom_centre, srid)).\
+            func.ST_AsText(func.ST_Transform(self.orm['orm_lv_load_areas'].geom_centre, srid)). \
                 label('geo_centre'),
-            (self.orm['orm_lv_load_areas'].sector_peakload_residential * gw2kw).\
+            (self.orm['orm_lv_load_areas'].sector_peakload_residential * mw2kw). \
                 label('peak_load_residential'),
-            (self.orm['orm_lv_load_areas'].sector_peakload_retail * gw2kw).\
+            (self.orm['orm_lv_load_areas'].sector_peakload_retail * mw2kw). \
                 label('peak_load_retail'),
-            (self.orm['orm_lv_load_areas'].sector_peakload_industrial * gw2kw).\
+            (self.orm['orm_lv_load_areas'].sector_peakload_industrial * mw2kw). \
                 label('peak_load_industrial'),
-            (self.orm['orm_lv_load_areas'].sector_peakload_agricultural * gw2kw).\
+            (self.orm['orm_lv_load_areas'].sector_peakload_agricultural * mw2kw). \
                 label('peak_load_agricultural'),
             ((self.orm['orm_lv_load_areas'].sector_peakload_residential
               + self.orm['orm_lv_load_areas'].sector_peakload_retail
               + self.orm['orm_lv_load_areas'].sector_peakload_industrial
               + self.orm['orm_lv_load_areas'].sector_peakload_agricultural)
-             * gw2kw).label('peak_load')). \
+             * mw2kw).label('peak_load')). \
             filter(self.orm['orm_lv_load_areas'].subst_id == mv_grid_district. \
-                   mv_grid._station.id_db).\
-            filter(((self.orm['orm_lv_load_areas'].sector_peakload_residential  # only pick load areas with peak load > lv_loads_threshold
+                   mv_grid._station.id_db). \
+            filter(((self.orm[
+                         'orm_lv_load_areas'].sector_peakload_residential  # only pick load areas with peak load > lv_loads_threshold
                      + self.orm['orm_lv_load_areas'].sector_peakload_retail
                      + self.orm['orm_lv_load_areas'].sector_peakload_industrial
                      + self.orm['orm_lv_load_areas'].sector_peakload_agricultural)
-                       * gw2kw) > lv_loads_threshold). \
+                    * mw2kw) > lv_loads_threshold). \
             filter(self.orm['version_condition_la'])
 
         # read data from db
@@ -747,7 +1076,6 @@ def import_lv_load_areas(self, session, mv_grid_district, lv_grid_districts,
 
         # create load_area objects from rows and add them to graph
         for id_db, row in lv_load_areas.iterrows():
-
             # create LV load_area object
             lv_load_area = LVLoadAreaDing0(id_db=id_db,
                                            db_data=row,
@@ -756,9 +1084,9 @@ def import_lv_load_areas(self, session, mv_grid_district, lv_grid_districts,
 
             # sub-selection of lv_grid_districts/lv_stations within one
             # specific load area
-            lv_grid_districts_per_load_area = lv_grid_districts.\
+            lv_grid_districts_per_load_area = lv_grid_districts. \
                 loc[lv_grid_districts['la_id'] == id_db]
-            lv_stations_per_load_area = lv_stations.\
+            lv_stations_per_load_area = lv_stations. \
                 loc[lv_stations['la_id'] == id_db]
 
             self.build_lv_grid_district(lv_load_area,
@@ -795,7 +1123,7 @@ def import_lv_grid_districts(self, session, lv_stations):
         # SET SRID 3035 to achieve correct area calculation of lv_grid_district
         # srid = '3035'
 
-        gw2kw = 10 ** 6  # load in database is in GW -> scale to kW
+        mw2kw = 10 ** 3  # load in database is in GW -> scale to kW
 
         # 1. filter grid districts of relevant load area
         lv_grid_districs_sqla = session.query(
@@ -803,21 +1131,21 @@ def import_lv_grid_districts(self, session, lv_stations):
             self.orm['orm_lv_grid_district'].la_id,
             self.orm['orm_lv_grid_district'].zensus_sum.label('population'),
             (self.orm[
-                 'orm_lv_grid_district'].sector_peakload_residential * gw2kw).
+                 'orm_lv_grid_district'].sector_peakload_residential * mw2kw).
                 label('peak_load_residential'),
-            (self.orm['orm_lv_grid_district'].sector_peakload_retail * gw2kw).
+            (self.orm['orm_lv_grid_district'].sector_peakload_retail * mw2kw).
                 label('peak_load_retail'),
             (self.orm[
-                 'orm_lv_grid_district'].sector_peakload_industrial * gw2kw).
+                 'orm_lv_grid_district'].sector_peakload_industrial * mw2kw).
                 label('peak_load_industrial'),
             (self.orm[
-                 'orm_lv_grid_district'].sector_peakload_agricultural * gw2kw).
+                 'orm_lv_grid_district'].sector_peakload_agricultural * mw2kw).
                 label('peak_load_agricultural'),
             ((self.orm['orm_lv_grid_district'].sector_peakload_residential
               + self.orm['orm_lv_grid_district'].sector_peakload_retail
               + self.orm['orm_lv_grid_district'].sector_peakload_industrial
               + self.orm['orm_lv_grid_district'].sector_peakload_agricultural)
-             * gw2kw).label('peak_load'),
+             * mw2kw).label('peak_load'),
             func.ST_AsText(func.ST_Transform(
                 self.orm['orm_lv_grid_district'].geom, srid)).label('geom'),
             self.orm['orm_lv_grid_district'].sector_count_residential,
@@ -825,15 +1153,15 @@ def import_lv_grid_districts(self, session, lv_stations):
             self.orm['orm_lv_grid_district'].sector_count_industrial,
             self.orm['orm_lv_grid_district'].sector_count_agricultural,
             (self.orm[
-                 'orm_lv_grid_district'].sector_consumption_residential * gw2kw). \
+                 'orm_lv_grid_district'].sector_consumption_residential * mw2kw). \
                 label('sector_consumption_residential'),
-            (self.orm['orm_lv_grid_district'].sector_consumption_retail * gw2kw). \
+            (self.orm['orm_lv_grid_district'].sector_consumption_retail * mw2kw). \
                 label('sector_consumption_retail'),
             (self.orm[
-                'orm_lv_grid_district'].sector_consumption_industrial * gw2kw). \
+                 'orm_lv_grid_district'].sector_consumption_industrial * mw2kw). \
                 label('sector_consumption_industrial'),
             (self.orm[
-                'orm_lv_grid_district'].sector_consumption_agricultural * gw2kw). \
+                 'orm_lv_grid_district'].sector_consumption_agricultural * mw2kw). \
                 label('sector_consumption_agricultural'),
             self.orm['orm_lv_grid_district'].mvlv_subst_id). \
             filter(self.orm['orm_lv_grid_district'].mvlv_subst_id.in_(
@@ -881,8 +1209,8 @@ def import_lv_stations(self, session):
         lv_stations_sqla = session.query(self.orm['orm_lv_stations'].mvlv_subst_id,
                                          self.orm['orm_lv_stations'].la_id,
                                          func.ST_AsText(func.ST_Transform(
-                                           self.orm['orm_lv_stations'].geom, srid)). \
-                                         label('geom')).\
+                                             self.orm['orm_lv_stations'].geom, srid)). \
+                                         label('geom')). \
             filter(self.orm['orm_lv_stations'].subst_id.in_(mv_grid_districts)). \
             filter(self.orm['version_condition_mvlvst'])
 
@@ -914,61 +1242,18 @@ def import_res_generators():
             """
             Imports renewable (res) generators
             """
-
-            # build query
-            generators_sqla = session.query(
-                self.orm['orm_re_generators'].columns.id,
-                self.orm['orm_re_generators'].columns.subst_id,
-                self.orm['orm_re_generators'].columns.la_id,
-                self.orm['orm_re_generators'].columns.mvlv_subst_id,
-                self.orm['orm_re_generators'].columns.electrical_capacity,
-                self.orm['orm_re_generators'].columns.generation_type,
-                self.orm['orm_re_generators'].columns.generation_subtype,
-                self.orm['orm_re_generators'].columns.voltage_level,
-                self.orm['orm_re_generators'].columns.w_id,
-                func.ST_AsText(func.ST_Transform(
-                    self.orm['orm_re_generators'].columns.rea_geom_new, srid)).label('geom_new'),
-                func.ST_AsText(func.ST_Transform(
-                    self.orm['orm_re_generators'].columns.geom, srid)).label('geom')
-            ). \
-                filter(
-                self.orm['orm_re_generators'].columns.subst_id.in_(list(mv_grid_districts_dict))). \
-                filter(self.orm['orm_re_generators'].columns.voltage_level.in_([4, 5, 6, 7])). \
-                filter(self.orm['version_condition_re'])
-
-            # read data from db
-            generators = pd.read_sql_query(generators_sqla.statement,
-                                           session.bind,
-                                           index_col='id')
-            # define generators with unknown subtype as 'unknown'
-            generators.loc[generators[
-                               'generation_subtype'].isnull(),
-                           'generation_subtype'] = 'unknown'
-
+            generators = db_io.get_res_generators(self.orm, session, list(mv_grid_districts_dict.values())[0])
+            logger.debug(f"Import {generators.shape[0]} renewable generators.")
             for id_db, row in generators.iterrows():
-
-                # treat generators' geom:
-                # use geom_new (relocated genos from data processing)
-                # otherwise use original geom from EnergyMap
-                if row['geom_new']:
-                    geo_data = wkt_loads(row['geom_new'])
-                elif not row['geom_new']:
-                    geo_data = wkt_loads(row['geom'])
-                    logger.warning(
-                        'Generator {} has no geom_new entry,'
-                        'EnergyMap\'s geom entry will be used.'.format(
-                        id_db))
-                # if no geom is available at all, skip generator
-                elif not row['geom']:
-                    #geo_data =
-                    logger.error('Generator {} has no geom entry either'
-                                 'and will be skipped.'.format(id_db))
-                    continue
-
                 # look up MV grid
                 mv_grid_district_id = row['subst_id']
                 mv_grid = mv_grid_districts_dict[mv_grid_district_id].mv_grid
 
+                if pd.notna(row["building_id"]):
+                    building_id = int(row["building_id"])
+                else:
+                    building_id = None
+
                 # create generator object
                 if row['generation_type'] in ['solar', 'wind']:
                     generator = GeneratorFluctuatingDing0(
@@ -978,7 +1263,10 @@ def import_res_generators():
                         type=row['generation_type'],
                         subtype=row['generation_subtype'],
                         v_level=int(row['voltage_level']),
-                        weather_cell_id=row['w_id'])
+                        weather_cell_id=int(row['w_id']),
+                        building_id=building_id,
+                        geo_data=wkt_loads(row['geom'])
+                    )
                 else:
                     generator = GeneratorDing0(
                         id_db=id_db,
@@ -986,82 +1274,26 @@ def import_res_generators():
                         capacity=float(row['electrical_capacity']),
                         type=row['generation_type'],
                         subtype=row['generation_subtype'],
-                        v_level=int(row['voltage_level']))
+                        v_level=int(row['voltage_level']),
+                        building_id=building_id,
+                        geo_data=wkt_loads(row['geom'])
+                    )
 
                 # MV generators
                 if generator.v_level in [4, 5]:
-                    generator.geo_data = geo_data
                     mv_grid.add_generator(generator)
 
                 # LV generators
                 elif generator.v_level in [6, 7]:
-
-                    # look up MV-LV substation id
-                    mvlv_subst_id = row['mvlv_subst_id']
-
-                    # if there's a LVGD id
-                    if mvlv_subst_id and not isnan(mvlv_subst_id):
-                        # assume that given LA exists
-                        try:
-                            # get LVGD
-                            lv_station = lv_stations_dict[mvlv_subst_id]
-                            lv_grid_district = lv_station.grid.grid_district
-                            generator.lv_grid = lv_station.grid
-
-                            # set geom (use original from db)
-                            generator.geo_data = geo_data
-
-                        # if LA/LVGD does not exist, choose random LVGD and move generator to station of LVGD
-                        # this occurs due to exclusion of LA with peak load < 1kW
-                        except:
-                            lv_grid_district = random.choice(list(lv_grid_districts_dict.values()))
-
-                            generator.lv_grid = lv_grid_district.lv_grid
-                            generator.geo_data = lv_grid_district.lv_grid.station().geo_data
-
-                            logger.warning('Generator {} cannot be assigned to '
-                                           'non-existent LV Grid District and was '
-                                           'allocated to a random LV Grid District ({}).'.format(
-                                            repr(generator), repr(lv_grid_district)))
-                            pass
-
-                    else:
-                        lv_grid_district = random.choice(list(lv_grid_districts_dict.values()))
-
-                        generator.lv_grid = lv_grid_district.lv_grid
-                        generator.geo_data = lv_grid_district.lv_grid.station().geo_data
-
-                        logger.warning('Generator {} has no la_id and was '
-                                       'assigned to a random LV Grid District ({}).'.format(
-                                        repr(generator), repr(lv_grid_district)))
-
-                    generator.lv_load_area = lv_grid_district.lv_load_area
-                    lv_grid_district.lv_grid.add_generator(generator)
+                    mv_grid.lv_generators_to_connect.append(generator)
+                else:
+                    ValueError("False voltage level")
 
         def import_conv_generators():
             """
             Imports conventional (conv) generators
             """
-
-            # build query
-            generators_sqla = session.query(
-                self.orm['orm_conv_generators'].columns.id,
-                self.orm['orm_conv_generators'].columns.subst_id,
-                self.orm['orm_conv_generators'].columns.name,
-                self.orm['orm_conv_generators'].columns.capacity,
-                self.orm['orm_conv_generators'].columns.fuel,
-                self.orm['orm_conv_generators'].columns.voltage_level,
-                func.ST_AsText(func.ST_Transform(
-                    self.orm['orm_conv_generators'].columns.geom, srid)).label('geom')). \
-                filter(
-                self.orm['orm_conv_generators'].columns.subst_id.in_(list(mv_grid_districts_dict))). \
-                filter(self.orm['orm_conv_generators'].columns.voltage_level.in_([4, 5, 6])). \
-                filter(self.orm['version_condition_conv'])
-
-            # read data from db
-            generators = pd.read_sql_query(generators_sqla.statement,
-                                           session.bind,
-                                           index_col='id')
+            generators = db_io.get_conv_generators(self.orm, session, list(mv_grid_districts_dict)[0])
 
             for id_db, row in generators.iterrows():
 
@@ -1095,9 +1327,9 @@ def import_conv_generators():
         random.seed(a=seed)
 
         # build dicts to map MV grid district and Load Area ids to related objects
-        mv_grid_districts_dict,\
-        lv_load_areas_dict,\
-        lv_grid_districts_dict,\
+        mv_grid_districts_dict, \
+        lv_load_areas_dict, \
+        lv_grid_districts_dict, \
         lv_stations_dict = self.get_mvgd_lvla_lvgd_obj_from_id()
 
         # import renewable generators
@@ -1120,6 +1352,7 @@ def import_config(self):
 
         # load parameters from configs
         cfg_ding0.load_config('config_db_tables.cfg')
+        cfg_ding0.load_config('config_db_credentials.cfg')
         cfg_ding0.load_config('config_calc.cfg')
         cfg_ding0.load_config('config_files.cfg')
         cfg_ding0.load_config('config_misc.cfg')
@@ -1147,7 +1380,7 @@ def import_pf_config(self):
 
         return PFConfigDing0(scenarios=[scenario],
                              timestep_start=start_time,
-                             timesteps_count=end_hour-start_hour,
+                             timesteps_count=end_hour - start_hour,
                              srid=srid,
                              resolution=resolution)
 
@@ -1168,88 +1401,90 @@ def import_static_data(self):
         equipment_mv_parameters_trafos = cfg_ding0.get('equipment',
                                                        'equipment_mv_parameters_trafos')
         static_data['MV_trafos'] = pd.read_csv(os.path.join(package_path, 'data',
-                                   equipment_mv_parameters_trafos),
-                                   comment='#',
-                                   delimiter=',',
-                                   decimal='.',
-                                   converters={'S_nom': lambda x: int(x)})
+                                                            equipment_mv_parameters_trafos),
+                                               comment='#',
+                                               delimiter=',',
+                                               decimal='.',
+                                               converters={'S_nom': lambda x: int(x)})
 
         # import equipment
         equipment_mv_parameters_lines = cfg_ding0.get('equipment',
                                                       'equipment_mv_parameters_lines')
         static_data['MV_overhead_lines'] = pd.read_csv(os.path.join(package_path, 'data',
-                                           equipment_mv_parameters_lines),
-                                           comment='#',
-                                           converters={'I_max_th': lambda x: int(x),
-                                                       'U_n': lambda x: int(x),
-                                                       'reinforce_only': lambda x: int(x)})
+                                                                    equipment_mv_parameters_lines),
+                                                       comment='#',
+                                                       converters={'I_max_th': lambda x: int(x),
+                                                                   'U_n': lambda x: int(x),
+                                                                   'reinforce_only': lambda x: int(x)})
 
         equipment_mv_parameters_cables = cfg_ding0.get('equipment',
                                                        'equipment_mv_parameters_cables')
         static_data['MV_cables'] = pd.read_csv(os.path.join(package_path, 'data',
-                                   equipment_mv_parameters_cables),
-                                   comment='#',
-                                   converters={'I_max_th': lambda x: int(x),
-                                               'U_n': lambda x: int(x),
-                                               'reinforce_only': lambda x: int(x)})
+                                                            equipment_mv_parameters_cables),
+                                               comment='#',
+                                               converters={'I_max_th': lambda x: int(x),
+                                                           'U_n': lambda x: int(x),
+                                                           'reinforce_only': lambda x: int(x)})
 
         equipment_lv_parameters_cables = cfg_ding0.get('equipment',
                                                        'equipment_lv_parameters_cables')
         static_data['LV_cables'] = pd.read_csv(os.path.join(package_path, 'data',
-                                   equipment_lv_parameters_cables),
-                                   comment='#',
-                                   index_col='name',
-                                   converters={'I_max_th': lambda x: int(x), 'U_n': lambda x: int(x)})
+                                                            equipment_lv_parameters_cables),
+                                               comment='#',
+                                               index_col='name',
+                                               converters={'I_max_th': lambda x: int(x), 'U_n': lambda x: int(x)})
 
         equipment_lv_parameters_trafos = cfg_ding0.get('equipment',
                                                        'equipment_lv_parameters_trafos')
         static_data['LV_trafos'] = pd.read_csv(os.path.join(package_path, 'data',
-                                   equipment_lv_parameters_trafos),
-                                   comment='#',
-                                   delimiter=',',
-                                   decimal='.',
-                                   converters={'S_nom': lambda x: int(x)})
-        static_data['LV_trafos']['r_pu'] = static_data['LV_trafos']['P_k'] / (static_data['LV_trafos']['S_nom']*1000)
-        static_data['LV_trafos']['x_pu'] = np.sqrt((static_data['LV_trafos']['u_kr']/100)**2-static_data['LV_trafos']['r_pu']**2)
+                                                            equipment_lv_parameters_trafos),
+                                               comment='#',
+                                               delimiter=',',
+                                               decimal='.',
+                                               converters={'S_nom': lambda x: int(x)})
+        static_data['LV_trafos']['r_pu'] = static_data['LV_trafos']['P_k'] / (static_data['LV_trafos']['S_nom'] * 1000)
+        static_data['LV_trafos']['x_pu'] = np.sqrt(
+            (static_data['LV_trafos']['u_kr'] / 100) ** 2 - static_data['LV_trafos']['r_pu'] ** 2)
 
         # import LV model grids
         model_grids_lv_string_properties = cfg_ding0.get('model_grids',
                                                          'model_grids_lv_string_properties')
         static_data['LV_model_grids_strings'] = pd.read_csv(os.path.join(package_path, 'data',
-                                                model_grids_lv_string_properties),
-                                                comment='#',
-                                                delimiter=',',
-                                                decimal='.',
-                                                index_col='string_id',
-                                                converters={'string_id': lambda x: int(x),
-                                                            'type': lambda x: int(x),
-                                                            'Kerber Original': lambda x: int(x),
-                                                            'count house branch': lambda x: int(x),
-                                                            'distance house branch': lambda x: int(x),
-                                                            'cable width': lambda x: int(x),
-                                                            'string length': lambda x: int(x),
-                                                            'length house branch A': lambda x: int(x),
-                                                            'length house branch B': lambda x: int(x),
-                                                            'cable width A': lambda x: int(x),
-                                                            'cable width B': lambda x: int(x)})
+                                                                         model_grids_lv_string_properties),
+                                                            comment='#',
+                                                            delimiter=',',
+                                                            decimal='.',
+                                                            index_col='string_id',
+                                                            converters={'string_id': lambda x: int(x),
+                                                                        'type': lambda x: int(x),
+                                                                        'Kerber Original': lambda x: int(x),
+                                                                        'count house branch': lambda x: int(x),
+                                                                        'distance house branch': lambda x: int(x),
+                                                                        'cable width': lambda x: int(x),
+                                                                        'string length': lambda x: int(x),
+                                                                        'length house branch A': lambda x: int(x),
+                                                                        'length house branch B': lambda x: int(x),
+                                                                        'cable width A': lambda x: int(x),
+                                                                        'cable width B': lambda x: int(x)})
 
         model_grids_lv_apartment_string = cfg_ding0.get('model_grids',
                                                         'model_grids_lv_apartment_string')
         converters_ids = {}
-        for id in range(1,47):  # create int() converter for columns 1..46
+        for id in range(1, 47):  # create int() converter for columns 1..46
             converters_ids[str(id)] = lambda x: int(x)
         static_data['LV_model_grids_strings_per_grid'] = pd.read_csv(os.path.join(package_path, 'data',
-                                                         model_grids_lv_apartment_string),
-                                                         comment='#',
-                                                         delimiter=',',
-                                                         decimal='.',
-                                                         index_col='apartment_count',
-                                                         converters=dict({'apartment_count': lambda x: int(x)},
+                                                                                  model_grids_lv_apartment_string),
+                                                                     comment='#',
+                                                                     delimiter=',',
+                                                                     decimal='.',
+                                                                     index_col='apartment_count',
+                                                                     converters=dict(
+                                                                         {'apartment_count': lambda x: int(x)},
                                                                          **converters_ids))
 
         return static_data
 
-    def import_orm(self):
+    def import_orm(self, session):
         """
         Import ORM classes names for  the correct connection to
         open energy platform and access tables depending on input in config in
@@ -1259,36 +1494,32 @@ def import_orm(self):
         -------
         :obj: `dict`
             key value pairs of names of datasets versus
-            sqlalchmey maps to acess
+            SQLAlchemy maps to access
             various tables where the datasets
             used to build grids are stored on the open
             energy platform.
         """
+        from ding0.tools import database
+        import saio
+        import sys
 
         orm = {}
-
         data_source = self.config['input_data_source']['input_data']
-        mv_grid_districts_name = self.config[data_source]['mv_grid_districts']
-        mv_stations_name = self.config[data_source]['mv_stations']
-        lv_load_areas_name = self.config[data_source]['lv_load_areas']
-        lv_grid_district_name = self.config[data_source]['lv_grid_district']
-        lv_stations_name = self.config[data_source]['lv_stations']
-        conv_generators_name = self.config[data_source]['conv_generators']
-        re_generators_name = self.config[data_source]['re_generators']
-
-        from egoio.db_tables import model_draft as orm_model_draft, \
-            supply as orm_supply, \
-            demand as orm_demand, \
-            grid as orm_grid
+        engine = database.engine()
+
+        def write_table_in_dict(orm, engine, name, table_str):
+            table_list = table_str.split(".")
+            table_schema = table_list[0]
+            table_name = table_list[1]
+            saio.register_schema(table_schema, engine)
+            orm[name] = sys.modules[f"saio.{table_schema}"].__getattr__(table_name)
+            return orm
+
+        for name, table_str in self.config[data_source].items():
+            if not name == "version":
+                orm = write_table_in_dict(orm, engine, name, table_str)
 
         if data_source == 'model_draft':
-            orm['orm_mv_grid_districts'] = orm_model_draft.__getattribute__(mv_grid_districts_name)
-            orm['orm_mv_stations'] = orm_model_draft.__getattribute__(mv_stations_name)
-            orm['orm_lv_load_areas'] = orm_model_draft.__getattribute__(lv_load_areas_name)
-            orm['orm_lv_grid_district'] = orm_model_draft.__getattribute__(lv_grid_district_name)
-            orm['orm_lv_stations'] = orm_model_draft.__getattribute__(lv_stations_name)
-            orm['orm_conv_generators'] = orm_model_draft.__getattribute__(conv_generators_name)
-            orm['orm_re_generators'] = orm_model_draft.__getattribute__(re_generators_name)
             orm['version_condition_mvgd'] = 1 == 1
             orm['version_condition_mv_stations'] = 1 == 1
             orm['version_condition_la'] = 1 == 1
@@ -1297,28 +1528,29 @@ def import_orm(self):
             orm['version_condition_re'] = 1 == 1
             orm['version_condition_conv'] = 1 == 1
         elif data_source == 'versioned':
-            orm['orm_mv_grid_districts'] = orm_grid.__getattribute__(mv_grid_districts_name)
-            orm['orm_mv_stations'] = orm_grid.__getattribute__(mv_stations_name)
-            orm['orm_lv_load_areas'] = orm_demand.__getattribute__(lv_load_areas_name)
-            orm['orm_lv_grid_district'] = orm_grid.__getattribute__(lv_grid_district_name)
-            orm['orm_lv_stations'] = orm_grid.__getattribute__(lv_stations_name)
-            orm['orm_conv_generators'] = orm_supply.__getattribute__(conv_generators_name)
-            orm['orm_re_generators'] = orm_supply.__getattribute__(re_generators_name)
             orm['data_version'] = self.config[data_source]['version']
-            orm['version_condition_mvgd'] =\
+            orm['version_condition_mvgd'] = \
                 orm['orm_mv_grid_districts'].version == orm['data_version']
             orm['version_condition_mv_stations'] = \
                 orm['orm_mv_stations'].version == orm['data_version']
-            orm['version_condition_la'] =\
+            orm['version_condition_la'] = \
                 orm['orm_lv_load_areas'].version == orm['data_version']
-            orm['version_condition_lvgd'] =\
+            orm['version_condition_lvgd'] = \
                 orm['orm_lv_grid_district'].version == orm['data_version']
-            orm['version_condition_mvlvst'] =\
+            orm['version_condition_mvlvst'] = \
                 orm['orm_lv_stations'].version == orm['data_version']
-            orm['version_condition_re'] =\
+            orm['version_condition_re'] = \
                 orm['orm_re_generators'].columns.version == orm['data_version']
-            orm['version_condition_conv'] =\
+            orm['version_condition_conv'] = \
                 orm['orm_conv_generators'].columns.version == orm['data_version']
+        elif data_source == "local":
+            orm['version_condition_mvgd'] = True
+            orm['version_condition_mv_stations'] = True
+            orm['version_condition_la'] = True
+            orm['version_condition_lvgd'] = True
+            orm['version_condition_mvlvst'] = True
+            orm['version_condition_re'] = True
+            orm['version_condition_conv'] = True
         else:
             logger.error("Invalid data source {} provided. Please re-check the file "
                          "`config_db_tables.cfg`".format(data_source))
@@ -1341,26 +1573,41 @@ def validate_grid_districts(self):
         msg_invalidity = []
         invalid_mv_grid_districts = []
 
-        for grid_district in self.mv_grid_districts():
+        # TODO PAUL: changed due to necessary import of aggregated load areas for urban routing
+
+        if True:
+
+            for grid_district in self.mv_grid_districts():
+
+                # there's only one node (MV station) => grid is empty
+                if len(grid_district.mv_grid.graph.nodes()) == 1:
+                    invalid_mv_grid_districts.append(grid_district)
+                    msg_invalidity.append('MV Grid District {} seems to be empty ' \
+                                          'and ' \
+                                          'was removed'.format(grid_district))
+        else:
+
+            for grid_district in self.mv_grid_districts():
 
-            # there's only one node (MV station) => grid is empty
-            if len(grid_district.mv_grid.graph.nodes()) == 1:
-                invalid_mv_grid_districts.append(grid_district)
-                msg_invalidity.append('MV Grid District {} seems to be empty ' \
-                                      'and ' \
-                                      'was removed'.format(grid_district))
-            # there're only aggregated load areas
-            elif all([lvla.is_aggregated for lvla in
-                      grid_district.lv_load_areas()]):
-                invalid_mv_grid_districts.append(grid_district)
-                msg_invalidity.append("MV Grid District {} contains only " \
-                                 "aggregated Load Areas and was removed" \
-                                 "".format(grid_district))
+                # there's only one node (MV station) => grid is empty
+                if len(grid_district.mv_grid.graph.nodes()) == 1:
+                    invalid_mv_grid_districts.append(grid_district)
+                    msg_invalidity.append('MV Grid District {} seems to be empty ' \
+                                          'and ' \
+                                          'was removed'.format(grid_district))
+
+                # there're only aggregated load areas
+                elif all([lvla.is_aggregated for lvla in
+                          grid_district.lv_load_areas()]):
+                    invalid_mv_grid_districts.append(grid_district)
+                    msg_invalidity.append("MV Grid District {} contains only " \
+                                          "aggregated Load Areas and was removed" \
+                                          "".format(grid_district))
 
         for grid_district in invalid_mv_grid_districts:
             self._mv_grid_districts.remove(grid_district)
-
-        logger.warning("\n".join(msg_invalidity))
+        if msg_invalidity:
+            logger.warning("\n".join(msg_invalidity))
         logger.info('=====> MV Grids validated')
         return msg_invalidity
 
@@ -1568,7 +1815,7 @@ def export_mv_grid_new(self, session, mv_grid_districts):
                         type_name=branch['branch'].type['name'],
                         type_kind=branch['branch'].kind,
                         type_v_nom=branch['branch'].type['U_n'],
-                        type_s_nom=3**0.5 * branch['branch'].type['I_max_th'] * branch['branch'].type['U_n'],
+                        type_s_nom=3 ** 0.5 * branch['branch'].type['I_max_th'] * branch['branch'].type['U_n'],
                         length=branch['branch'].length / 1e3,
                         geom=from_shape(LineString([branch['adj_nodes'][0].geo_data,
                                                     branch['adj_nodes'][1].geo_data]),
@@ -1589,7 +1836,7 @@ def export_mv_grid_new(self, session, mv_grid_districts):
                         type_name=branch['branch'].type['name'],
                         type_kind=branch['branch'].kind,
                         type_v_nom=branch['branch'].type['U_n'],
-                        type_s_nom=3**0.5 * branch['branch'].type['I_max_th'] * branch['branch'].type['U_n'],
+                        type_s_nom=3 ** 0.5 * branch['branch'].type['I_max_th'] * branch['branch'].type['U_n'],
                         length=branch['branch'].length / 1e3,
                         geom=from_shape(LineString([branch['adj_nodes'][0].geo_data,
                                                     branch['adj_nodes'][1].geo_data]),
@@ -1599,7 +1846,6 @@ def export_mv_grid_new(self, session, mv_grid_districts):
                     )
                     session.add(branch_dataset)
 
-
         # commit changes to db
         session.commit()
 
@@ -1665,9 +1911,9 @@ def to_dataframe_old(self):
                     peak_load = 0
                     generation_capacity = 0
                     type = 'Switch Disconnector'
-                    #round coordinates of circuit breaker
+                    # round coordinates of circuit breaker
                     geosjson = mapping(node.geo_data)
-                    decimal_places = 4 #with 4, its around 10m error N-S or E-W
+                    decimal_places = 4  # with 4, its around 10m error N-S or E-W
                     geosjson['coordinates'] = np.round(np.array(geosjson['coordinates']), decimal_places)
                     geom = from_shape(Point(shape(geosjson)), srid=srid)
                 else:
@@ -1693,7 +1939,7 @@ def to_dataframe_old(self):
                      'v_res1': v_res1,
                      'type': type,
                      'rings': len(grid_district.mv_grid._rings)
-                    }), ignore_index=True)
+                     }), ignore_index=True)
 
             # get branches (lines) from grid's graph and create datasets
             for branch in grid_district.mv_grid.graph_edges():
@@ -1707,24 +1953,24 @@ def to_dataframe_old(self):
 
                     edges_df = edges_df.append(pd.Series(
                         {'branch_id': branch_name,
-                        'grid_id': grid_district.mv_grid.id_db,
-                        'type_name': branch['branch'].type['name'],
-                        'type_kind': branch['branch'].kind,
-                        'type_v_nom': branch['branch'].type['U_n'],
-                        'type_s_nom': 3 ** 0.5 * branch['branch'].type[
-                            'I_max_th'] * branch['branch'].type['U_n'],
-                        'length': branch['branch'].length / 1e3,
-                        'geom': from_shape(
-                            LineString([branch['adj_nodes'][0].geo_data,
-                                        branch['adj_nodes'][
-                                            1].geo_data]),
-                            srid=srid),
-                        's_res0': branch['branch'].s_res[0],
-                        's_res1': branch['branch'].s_res[1]}), ignore_index=True)
+                         'grid_id': grid_district.mv_grid.id_db,
+                         'type_name': branch['branch'].type['name'],
+                         'type_kind': branch['branch'].kind,
+                         'type_v_nom': branch['branch'].type['U_n'],
+                         'type_s_nom': 3 ** 0.5 * branch['branch'].type[
+                             'I_max_th'] * branch['branch'].type['U_n'],
+                         'length': branch['branch'].length / 1e3,
+                         'geom': from_shape(
+                             LineString([branch['adj_nodes'][0].geo_data,
+                                         branch['adj_nodes'][
+                                             1].geo_data]),
+                             srid=srid),
+                         's_res0': branch['branch'].s_res[0],
+                         's_res1': branch['branch'].s_res[1]}), ignore_index=True)
 
         return nodes_df, edges_df
 
-    def to_csv(self, dir = '', only_export_mv = False):
+    def to_csv(self, dir='', only_export_mv=False):
         '''
         Function to export network to csv. Converts network in dataframes which are adapted to pypsa format.
         Respectively saves files for network, buses, lines, transformers, loads and generators.
@@ -1736,6 +1982,32 @@ def to_csv(self, dir = '', only_export_mv = False):
         only_export_mv: bool
             When True only mv topology is exported with aggregated lv grid districts
         '''
+        def transform_all_geodata(gd_components, network_df, grids_df):
+            from pyproj import Transformer
+            from shapely.ops import transform
+            import time
+
+            logger.info("Transform all geodata from 'EPSG:3035' to 'EPSG:4326'.")
+            t_start = time.perf_counter()
+
+            # initialize the Coordinate-Reference-System-Transformer
+            crs_transformer = Transformer.from_crs("EPSG:3035", "EPSG:4326", always_xy=True).transform
+
+            # Transform geodata
+            network_df["mv_grid_district_geom"] = [transform(crs_transformer, geom) for geom in
+                                                   network_df["mv_grid_district_geom"].to_list()]
+            grids_df["grid_district_geom"] = [transform(crs_transformer, geom) for geom in
+                                              grids_df["grid_district_geom"].to_list()]
+            for key, item in gd_components.items():
+                if key == 'Bus':
+                    gd_components[key]['x'], gd_components[key]['y'] = crs_transformer(gd_components[key]['x'],
+                                                                                       gd_components[key]['y'])
+                elif key == 'Line':
+                    gd_components[key]["geometry"] = [transform(crs_transformer, geom) for geom in
+                                                      gd_components[key]["geometry"].to_list()]
+
+            logger.debug(f"Transformed all geodata in {time.perf_counter() - t_start}s.")
+            return gd_components, network_df, grids_df
 
         buses_df, generators_df, lines_df, loads_df, transformer_df = initialize_component_dataframes()
         if (dir == ''):
@@ -1744,28 +2016,51 @@ def to_csv(self, dir = '', only_export_mv = False):
         self.control_circuit_breakers(mode='open')
         # start filling component dataframes
         for grid_district in self.mv_grid_districts():
-            gd_components, network_df, _ = fill_mvgd_component_dataframes(
-                grid_district, buses_df, generators_df,
-                lines_df, loads_df,transformer_df, only_export_mv)
-            # save network and components to csv
+            gd_components, network_df, grids_df, _ = fill_mvgd_component_dataframes(
+                grid_district,
+                buses_df,
+                generators_df,
+                lines_df,
+                loads_df,
+                transformer_df,
+                only_export_mv
+            )
+            # Transform all geodata from 'EPSG:3035' to 'EPSG:4326' for eDisGo
+            gd_components, network_df, grids_df = transform_all_geodata(
+                gd_components, network_df, grids_df
+            )
+            # Make directories for the grid data and save to csv
             path = os.path.join(dir, str(grid_district.id_db))
             if not os.path.exists(path):
                 os.makedirs(path)
-            network_df.to_csv(os.path.join(path, 'network.csv'))
+
+            network_df.to_csv(
+                os.path.join(path, 'network.csv')
+            )
+            grids_df.to_csv(
+                os.path.join(path, 'grids.csv')
+            )
             gd_components['HVMV_Transformer'].to_csv(
-                os.path.join(path, 'transformers_hvmv.csv'))
+                os.path.join(path, 'transformers_hvmv.csv')
+            )
             gd_components['Transformer'].to_csv(
-                os.path.join(path, 'transformers.csv'))
+                os.path.join(path, 'transformers.csv')
+            )
             gd_components['Bus'].to_csv(
-                os.path.join(path, 'buses.csv'))
+                os.path.join(path, 'buses.csv')
+            )
             gd_components['Line'].to_csv(
-                os.path.join(path, 'lines.csv'))
+                os.path.join(path, 'lines.csv')
+            )
             gd_components['Load'].to_csv(
-                os.path.join(path, 'loads.csv'))
+                os.path.join(path, 'loads.csv')
+            )
             gd_components['Generator'].to_csv(
-                os.path.join(path, 'generators.csv'))
+                os.path.join(path, 'generators.csv')
+            )
             gd_components['Switch'].to_csv(
-                os.path.join(path, 'switches.csv'))
+                os.path.join(path, 'switches.csv')
+            )
 
             # Merge metadata of multiple runs
             if 'metadata' not in locals():
@@ -1780,10 +2075,9 @@ def to_csv(self, dir = '', only_export_mv = False):
             # Save metadata to disk
         with open(os.path.join(path, 'Ding0_{}.meta'.format(metadata['run_id'])),
                   'w') as f:
-            json.dump(metadata, f)
-
+            json.dump(metadata, f, indent=4)
 
-    def to_dataframe(self,  only_export_mv = False):
+    def to_dataframe(self, only_export_mv=False):
         '''
         Function to export network to csv. Converts network in dataframes which are adapted to pypsa format.
         Respectively saves files for network, buses, lines, transformers, loads and generators.
@@ -1800,8 +2094,15 @@ def to_dataframe(self,  only_export_mv = False):
         self.control_circuit_breakers(mode='open')
         # start filling component dataframes
         for grid_district in self.mv_grid_districts():
-            gd_components, network_df, _ = fill_mvgd_component_dataframes(grid_district, buses_df, generators_df,
-                                                                            lines_df, loads_df, transformer_df, only_export_mv)
+            gd_components, network_df, _, _ = fill_mvgd_component_dataframes(
+                grid_district,
+                buses_df,
+                generators_df,
+                lines_df,
+                loads_df,
+                transformer_df,
+                only_export_mv
+            )
             if len(components) == 0:
                 components = gd_components
                 networks = network_df
@@ -1812,7 +2113,6 @@ def to_dataframe(self,  only_export_mv = False):
         components['Network'] = network_df
         return components
 
-
     def mv_routing(self, debug=False, animation=False):
         """
         Performs routing on all MV grids.
@@ -1837,27 +2137,51 @@ def mv_routing(self, debug=False, animation=False):
         else:
             anim = None
 
-        for grid_district in self.mv_grid_districts():
-            grid_district.mv_grid.routing(debug=debug, anim=anim)
+        chunk_size = 10
+        mvgd_chunks = [list(self.mv_grid_districts())[i:i + chunk_size] for i in
+                       range(0, len(list(self.mv_grid_districts())), chunk_size)]
+
+        for mvgd_chunk in mvgd_chunks:
 
-        logger.info('=====> MV Routing (Routing, Connection of Satellites & '
-                    'Stations) performed')
+            for grid_district in mvgd_chunk:  # self.mv_grid_districts():
+                logger.info(f"Urban routing for: {grid_district}")
+                # 1) urban routing in aggregated load area(s)
+                if True:
+                    grid_district.mv_grid.urban_routing(debug=debug, anim=anim)
+                    logger.info('=====> Urban MV Routing (Routing, Connection of Stations) performed')
+                # 2) rural routing between remaining load areas
+                grid_district.mv_grid.routing(debug=debug, anim=anim)
+
+            logger.info('=====> MV Routing (Routing, Connection of Satellites & '
+                        'Stations) performed')
 
     def build_lv_grids(self):
         """
         Builds LV grids for every non-aggregated LA in every MV grid
         district using model grids.
         """
-
         for mv_grid_district in self.mv_grid_districts():
-            for load_area in mv_grid_district.lv_load_areas():
-                if not load_area.is_aggregated:
+            if True:  # new approach
+                for load_area in mv_grid_district.lv_load_areas():
+                    # logger.warning(f'LV grid building for la {str(load_area)}')
                     for lv_grid_district in load_area.lv_grid_districts():
-
+                        # logger.warning(f'LVGD building for {str(lv_grid_district)}')
+                        if len(list(nx.connected_components(nx.Graph(lv_grid_district.graph_district)))) > 1:
+                            raise ValueError(f"Isolates in lv_grid_district.graph_district: {lv_grid_district.lv_grid}")
+                        # Save number of isolated nodes, these nodes are the unconnected generators and station_bus.
+                        number_of_subgraphs = len(list(nx.connected_components(lv_grid_district.lv_grid.graph)))
                         lv_grid_district.lv_grid.build_grid()
-                else:
-                    logger.info(
-                        '{} is of type aggregated. No grid is created.'.format(repr(load_area)))
+                        # Error if more isolates than before.
+                        if len(list(nx.connected_components(lv_grid_district.lv_grid.graph))) > number_of_subgraphs:
+                            raise ValueError(f"Isolate Nodes in LV-Grid: {lv_grid_district.lv_grid}")
+            else:  # ding0 default
+                for mv_grid_district in self.mv_grid_districts():
+                    for load_area in mv_grid_district.lv_load_areas():
+                        if not load_area.is_aggregated:
+                            for lv_grid_district in load_area.lv_grid_districts():
+                                lv_grid_district.lv_grid.build_grid()
+                        else:
+                            logger.info('{} is of type aggregated. No grid is created.'.format(repr(load_area)))
 
         logger.info('=====> LV model grids created')
 
@@ -1870,24 +2194,13 @@ def connect_generators(self, debug=False):
         debug: :obj:`bool`, defaults to False
             If True, information is printed during process.
         """
+        # get predefined random seed and initialize random generator
+        seed = int(cfg_ding0.get('random', 'seed'))
+        random.seed(a=seed)
 
         for mv_grid_district in self.mv_grid_districts():
             mv_grid_district.mv_grid.connect_generators(debug=debug)
-
-            # get predefined random seed and initialize random generator
-            seed = int(cfg_ding0.get('random', 'seed'))
-            random.seed(a=seed)
-
-            for load_area in mv_grid_district.lv_load_areas():
-                if not load_area.is_aggregated:
-                    for lv_grid_district in load_area.lv_grid_districts():
-
-                        lv_grid_district.lv_grid.connect_generators(debug=debug)
-                        if debug:
-                            lv_grid_district.lv_grid.graph_draw(mode='LV')
-                else:
-                    logger.info(
-                        '{} is of type aggregated. LV generators are not connected to LV grids.'.format(repr(load_area)))
+            mv_grid_district.mv_grid.connect_lv_generators(debug=debug)
 
         logger.info('=====> Generators connected')
 
@@ -1954,7 +2267,8 @@ def control_circuit_breakers(self, mode=None):
         elif mode == 'close':
             logger.info('=====> MV Circuit Breakers closed')
 
-    def run_powerflow(self, session = None, method='onthefly', only_calc_mv = True, export_pypsa=False, debug=False, export_result_dir=None):
+    def run_powerflow(self, session=None, method='onthefly', only_calc_mv=True, export_pypsa=False, debug=False,
+                      export_result_dir=None):
         """
         Performs power flow calculation for all MV grids
 
@@ -1997,7 +2311,7 @@ def run_powerflow(self, session = None, method='onthefly', only_calc_mv = True,
                 else:
                     export_pypsa_dir = None
                 grid_district.mv_grid.run_powerflow(method='onthefly',
-                                                    only_calc_mv = only_calc_mv,
+                                                    only_calc_mv=only_calc_mv,
                                                     export_pypsa_dir=export_pypsa_dir,
                                                     debug=debug,
                                                     export_result_dir=export_result_dir)
@@ -2009,15 +2323,12 @@ def reinforce_grid(self):
         # TODO: Finish method and enable LV case
 
         for grid_district in self.mv_grid_districts():
-
             # reinforce MV grid
             grid_district.mv_grid.reinforce_grid()
-
-            # reinforce LV grids
-            for lv_load_area in grid_district.lv_load_areas():
-                if not lv_load_area.is_aggregated:
-                    for lv_grid_district in lv_load_area.lv_grid_districts():
-                        lv_grid_district.lv_grid.reinforce_grid()
+            # # reinforce LV grids
+            # for lv_load_area in grid_district.lv_load_areas():
+            #     for lv_grid_district in lv_load_area.lv_grid_districts():
+            #         lv_grid_district.lv_grid.reinforce_grid()
 
     @property
     def metadata(self, run_id=None):
@@ -2038,7 +2349,8 @@ def metadata(self, run_id=None):
         # Get latest version and/or git commit hash
         try:
             version = subprocess.check_output(
-                ["git", "describe", "--tags", "--always"]).decode('utf8')
+                ["git", "describe", "--tags", "--always"], cwd=package_path
+            ).decode('utf8')
         except:
             version = None
 
@@ -2080,7 +2392,6 @@ def metadata(self, run_id=None):
 
         return metadata
 
-
     def __repr__(self):
         """"
         A repr string representation of the NetworkDing0 object.
@@ -2113,66 +2424,66 @@ def list_generators(self, session):
         srid = str(int(cfg_ding0.get('geo', 'srid')))
 
         # build dicts to map MV grid district and Load Area ids to related objects
-        mv_grid_districts_dict,\
-        lv_load_areas_dict,\
-        lv_grid_districts_dict,\
+        mv_grid_districts_dict, \
+        lv_load_areas_dict, \
+        lv_grid_districts_dict, \
         lv_stations_dict = self.get_mvgd_lvla_lvgd_obj_from_id()
 
         # import renewable generators
         # build query
         generators_sqla = session.query(
-                self.orm['orm_re_generators'].columns.id,
-                self.orm['orm_re_generators'].columns.subst_id,
-                self.orm['orm_re_generators'].columns.la_id,
-                self.orm['orm_re_generators'].columns.mvlv_subst_id,
-                self.orm['orm_re_generators'].columns.electrical_capacity,
-                self.orm['orm_re_generators'].columns.generation_type,
-                self.orm['orm_re_generators'].columns.generation_subtype,
-                self.orm['orm_re_generators'].columns.voltage_level,
-                func.ST_AsText(func.ST_Transform(
-                   self.orm['orm_re_generators'].columns.rea_geom_new, srid)).label('geom_new'),
-                func.ST_AsText(func.ST_Transform(
-                   self.orm['orm_re_generators'].columns.geom, srid)).label('geom')
-                ).filter(
-                self.orm['orm_re_generators'].columns.subst_id.in_(list(mv_grid_districts_dict))). \
-                filter(self.orm['orm_re_generators'].columns.voltage_level.in_([4, 5, 6, 7])). \
-                filter(self.orm['version_condition_re'])
+            self.orm['orm_re_generators'].columns.id,
+            self.orm['orm_re_generators'].columns.subst_id,
+            self.orm['orm_re_generators'].columns.la_id,
+            self.orm['orm_re_generators'].columns.mvlv_subst_id,
+            self.orm['orm_re_generators'].columns.electrical_capacity,
+            self.orm['orm_re_generators'].columns.generation_type,
+            self.orm['orm_re_generators'].columns.generation_subtype,
+            self.orm['orm_re_generators'].columns.voltage_level,
+            func.ST_AsText(func.ST_Transform(
+                self.orm['orm_re_generators'].columns.rea_geom_new, srid)).label('geom_new'),
+            func.ST_AsText(func.ST_Transform(
+                self.orm['orm_re_generators'].columns.geom, srid)).label('geom')
+        ).filter(
+            self.orm['orm_re_generators'].columns.subst_id.in_(list(mv_grid_districts_dict))). \
+            filter(self.orm['orm_re_generators'].columns.voltage_level.in_([4, 5, 6, 7])). \
+            filter(self.orm['version_condition_re'])
 
         # read data from db
         generators_res = pd.read_sql_query(generators_sqla.statement,
-                                        session.bind,
-                                        index_col='id')
+                                           session.bind,
+                                           index_col='id')
 
-        generators_res.columns = ['GenCap' if c=='electrical_capacity' else
-                                  'type' if c=='generation_type' else
-                                  'subtype' if c=='generation_subtype' else
-                                  'v_level' if c=='voltage_level' else
+        generators_res.columns = ['GenCap' if c == 'electrical_capacity' else
+                                  'type' if c == 'generation_type' else
+                                  'subtype' if c == 'generation_subtype' else
+                                  'v_level' if c == 'voltage_level' else
                                   c for c in generators_res.columns]
         ###########################
         # Imports conventional (conv) generators
         # build query
         generators_sqla = session.query(
-                self.orm['orm_conv_generators'].columns.id,
-                self.orm['orm_conv_generators'].columns.subst_id,
-                self.orm['orm_conv_generators'].columns.name,
-                self.orm['orm_conv_generators'].columns.capacity,
-                self.orm['orm_conv_generators'].columns.fuel,
-                self.orm['orm_conv_generators'].columns.voltage_level,
-                func.ST_AsText(func.ST_Transform(
-                   self.orm['orm_conv_generators'].columns.geom, srid)).label('geom')
-                ).filter(
-                self.orm['orm_conv_generators'].columns.subst_id.in_(list(mv_grid_districts_dict))). \
-                filter(self.orm['orm_conv_generators'].columns.voltage_level.in_([4, 5, 6])). \
-                filter(self.orm['version_condition_conv'])
+            self.orm['orm_conv_generators'].columns.id,
+            self.orm['orm_conv_generators'].columns.subst_id,
+            self.orm['orm_conv_generators'].columns.name,
+            self.orm['orm_conv_generators'].columns.capacity,
+            self.orm['orm_conv_generators'].columns.fuel,
+            self.orm['orm_conv_generators'].columns.voltage_level,
+            func.ST_AsText(func.ST_Transform(
+                self.orm['orm_conv_generators'].columns.geom, srid)).label('geom')
+        ).filter(
+            self.orm['orm_conv_generators'].columns.subst_id.in_(list(mv_grid_districts_dict))). \
+            filter(self.orm['orm_conv_generators'].columns.voltage_level.in_([4, 5, 6])). \
+            filter(self.orm['version_condition_conv'])
 
         # read data from db
         generators_conv = pd.read_sql_query(generators_sqla.statement,
-                                           session.bind,
-                                           index_col='id')
+                                            session.bind,
+                                            index_col='id')
 
-        generators_conv.columns = ['GenCap' if c=='capacity' else
-                                   'type' if c=='fuel' else
-                                   'v_level' if c=='voltage_level' else
+        generators_conv.columns = ['GenCap' if c == 'capacity' else
+                                   'type' if c == 'fuel' else
+                                   'v_level' if c == 'voltage_level' else
                                    c for c in generators_conv.columns]
         ###########################
         generators = pd.concat([generators_conv, generators_res], axis=0)
@@ -2193,32 +2504,33 @@ def list_load_areas(self, session, mv_districts):
         # threshold: load area peak load, if peak load < threshold => disregard
         # load area
         lv_loads_threshold = cfg_ding0.get('mv_routing', 'load_area_threshold')
-        #lv_loads_threshold = 0
+        # lv_loads_threshold = 0
 
-        gw2kw = 10 ** 6  # load in database is in GW -> scale to kW
+        mw2kw = 10 ** 3  # load in database is in GW -> scale to kW
 
-        #filter list for only desired MV districts
+        # filter list for only desired MV districts
         stations_list = [d.mv_grid._station.id_db for d in mv_districts]
 
         # build SQL query
         lv_load_areas_sqla = session.query(
             self.orm['orm_lv_load_areas'].id.label('id_db'),
-            (self.orm['orm_lv_load_areas'].sector_peakload_residential * gw2kw).\
+            (self.orm['orm_lv_load_areas'].sector_peakload_residential * mw2kw). \
                 label('peak_load_residential'),
-            (self.orm['orm_lv_load_areas'].sector_peakload_retail * gw2kw).\
+            (self.orm['orm_lv_load_areas'].sector_peakload_retail * mw2kw). \
                 label('peak_load_retail'),
-            (self.orm['orm_lv_load_areas'].sector_peakload_industrial * gw2kw).\
+            (self.orm['orm_lv_load_areas'].sector_peakload_industrial * mw2kw). \
                 label('peak_load_industrial'),
-            (self.orm['orm_lv_load_areas'].sector_peakload_agricultural * gw2kw).\
+            (self.orm['orm_lv_load_areas'].sector_peakload_agricultural * mw2kw). \
                 label('peak_load_agricultural'),
-            #self.orm['orm_lv_load_areas'].subst_id
-            ). \
-            filter(self.orm['orm_lv_load_areas'].subst_id.in_(stations_list)).\
-            filter(((self.orm['orm_lv_load_areas'].sector_peakload_residential  # only pick load areas with peak load > lv_loads_threshold
+            # self.orm['orm_lv_load_areas'].subst_id
+        ). \
+            filter(self.orm['orm_lv_load_areas'].subst_id.in_(stations_list)). \
+            filter(((self.orm[
+                         'orm_lv_load_areas'].sector_peakload_residential  # only pick load areas with peak load > lv_loads_threshold
                      + self.orm['orm_lv_load_areas'].sector_peakload_retail
                      + self.orm['orm_lv_load_areas'].sector_peakload_industrial
                      + self.orm['orm_lv_load_areas'].sector_peakload_agricultural)
-                       * gw2kw) > lv_loads_threshold). \
+                    * mw2kw) > lv_loads_threshold). \
             filter(self.orm['version_condition_la'])
 
         # read data from db
@@ -2244,23 +2556,23 @@ def list_lv_grid_districts(self, session, lv_stations):
             Pandas Data Frame
             Table of lv_grid_districts
         """
-        gw2kw = 10 ** 6  # load in database is in GW -> scale to kW
+        mw2kw = 10 ** 3  # load in database is in GW -> scale to kW
 
         # 1. filter grid districts of relevant load area
         lv_grid_districs_sqla = session.query(
             self.orm['orm_lv_grid_district'].mvlv_subst_id,
             (self.orm[
-                 'orm_lv_grid_district'].sector_peakload_residential * gw2kw).
+                 'orm_lv_grid_district'].sector_peakload_residential * mw2kw).
                 label('peak_load_residential'),
-            (self.orm['orm_lv_grid_district'].sector_peakload_retail * gw2kw).
+            (self.orm['orm_lv_grid_district'].sector_peakload_retail * mw2kw).
                 label('peak_load_retail'),
             (self.orm[
-                 'orm_lv_grid_district'].sector_peakload_industrial * gw2kw).
+                 'orm_lv_grid_district'].sector_peakload_industrial * mw2kw).
                 label('peak_load_industrial'),
             (self.orm[
-                 'orm_lv_grid_district'].sector_peakload_agricultural * gw2kw).
+                 'orm_lv_grid_district'].sector_peakload_agricultural * mw2kw).
                 label('peak_load_agricultural'),
-            ). \
+        ). \
             filter(self.orm['orm_lv_grid_district'].mvlv_subst_id.in_(
             lv_stations)). \
             filter(self.orm['version_condition_lvgd'])
@@ -2271,3 +2583,52 @@ def list_lv_grid_districts(self, session, lv_stations):
                                               index_col='mvlv_subst_id')
 
         return lv_grid_districts
+
+    def plot_mv_grids(self, path=None, filename=None):
+        kwargs = {}
+
+        kwargs["path"] = path
+        kwargs["filename"] = filename
+
+        if filename == '1_routing_completed':
+            kwargs["subtitle"] = 'Routing completed',
+        elif filename == '2_generators_connected':
+            kwargs["subtitle"] = 'Generators connected',
+        elif filename == '3_circuit_breakers_relocated':
+            kwargs["subtitle"] = 'Circuit breakers relocated',
+        elif filename == '4_PF_result_load':
+            kwargs["subtitle"] = 'PF result (load case)',
+            kwargs["line_color"] = 'loading',
+            kwargs["node_color"] = 'voltage',
+            kwargs["testcase"] = 'load'
+        elif filename == '5_PF_result_feedin':
+            kwargs["subtitle"] = 'PF result (feedin case)',
+            kwargs["line_color"] = 'loading',
+            kwargs["node_color"] = 'voltage',
+            kwargs["testcase"] = 'feedin'
+        elif filename == '6_final_grid_PF_result_load':
+            kwargs["subtitle"] = 'Final grid PF result (load case)',
+            kwargs["line_color"] = 'loading',
+            kwargs["node_color"] = 'voltage',
+            kwargs["testcase"] = 'load'
+        elif filename == '7_final_grid_PF_result_feedin':
+            kwargs["subtitle"] = 'Final grid PF result (feedin case)',
+            kwargs["line_color"] = 'loading',
+            kwargs["node_color"] = 'voltage',
+            kwargs["testcase"] = 'feedin'
+
+        for mv_grid_district in self.mv_grid_districts():
+            plot_mv_topology(mv_grid_district.mv_grid, **kwargs)
+
+
+
+    def plot_lv_grids(self, path=None, filename=None):
+        for mv_grid_district in self.mv_grid_districts():
+            for load_area in mv_grid_district.lv_load_areas():
+                for lv_grid_district in load_area.lv_grid_districts():
+                    plot_lv_topology(
+                        lv_grid_district.lv_grid,
+                        path=path,
+                        mv_grid_id=mv_grid_district.id_db,
+                        filename=filename,
+                    )
diff --git a/ding0/core/network/__init__.py b/ding0/core/network/__init__.py
index 002c9986..dc242e99 100644
--- a/ding0/core/network/__init__.py
+++ b/ding0/core/network/__init__.py
@@ -22,7 +22,7 @@
 
 class GridDing0:
     """
-    The fundamental abstract class used to encapsulated
+    The fundamental abstract class used to encapsulate
     the networkx graph and the relevant attributes of a
     power grid irrespective of voltage level. By design,
     this class is not expected to be instantiated directly.
@@ -41,7 +41,7 @@ class GridDing0:
         class, area that is covered by the lv grid
     v_level: :obj:`int`
         The integer value of the voltage level of the Grid in kV.
-        Typically either 10 or 20.
+        Typically, either 10 or 20.
 
 
     Attributes
@@ -184,7 +184,8 @@ def graph_add_node(self, node_object):
             :class:`~.ding0.core.network.LoadDing0` or
             :class:`~.ding0.core.network.StationDing0` or
             :class:`~.ding0.core.network.CircuitBreakerDing0` or
-            :class:`~.ding0.core.network.CableDistributorDing0`
+            :class:`~.ding0.core.network.CableDistributorDing0` or
+            :class:`~.ding0.core.network.loads.MVLoadDing0`
 
         """
         if ((node_object not in self.graph.nodes()) and
@@ -192,7 +193,8 @@ def graph_add_node(self, node_object):
                                       CableDistributorDing0,
                                       LVLoadAreaCentreDing0,
                                       CircuitBreakerDing0,
-                                      GeneratorDing0)))):
+                                      GeneratorDing0,
+                                      LoadDing0)))):
             self.graph.add_node(node_object)
 
     def graph_draw(self, mode):
@@ -675,6 +677,9 @@ def __init__(self, **kwargs):
 
         # add circ breaker to grid and graph
         self._grid.add_ring(self)
+        
+        #PAUL new add ring demand, cum. demand from nodes part of respective ring
+        self._demand = kwargs.get('demand', None)
 
     @property
     def network(self):
@@ -758,20 +763,32 @@ class BranchDing0:
     Note
     -----
     Important: id_db is not set until whole grid is finished (setting at the end).
+    
+    
+        
+    ADDED FOR NEW LV GRID APPROACH:
+    geometry : shapely.LineString
+        due to for lv grids the coordinates of nodes and
+        edges are known coordinates are stored as LineString 
+        to enable visualisation of the right course of the road.
     """
 
     def __init__(self, **kwargs):
 
         self.id_db = kwargs.get('id_db', None)
-        self.ring = kwargs.get('ring', None)
+        self.ring = kwargs.get('ring', None) # MV ring allocation
+        self.feeder = kwargs.get('feeder', None) # LV feeder allocation
         self.grid = kwargs.get('grid', None)
         self.length = kwargs.get('length', None)  # branch (line/cable) length in m
         self.kind = kwargs.get('kind', None)  # 'line' or 'cable'
         self.type = kwargs.get('type', None)  # DataFrame with attributes of line/cable
         self.connects_aggregated = kwargs.get('connects_aggregated', False)
         self.circuit_breaker = kwargs.get('circuit_breaker', None)
-
+        self.geometry = kwargs.get('geometry', None) # branch coordinates
+        self.num_parallel = 1 # number of parallel lines, initially one
         self.critical = False
+        # workaround for connecting more than one component to a bus
+        self.helper_component = kwargs.get('helper_component', None)
 
     @property
     def network(self):
@@ -951,6 +968,8 @@ def __init__(self, **kwargs):
         self.subtype = kwargs.get('subtype', None)
         self.v_level = kwargs.get('v_level', None)
 
+        self.building_id = kwargs.get('building_id', None)
+
     @property
     def network(self):
         """
@@ -989,7 +1008,7 @@ def __repr__(self):
         -------
         :obj:`str`
         """
-        if self.subtype != None:
+        if self.subtype not in [None, 'unknown']:
             type = self.subtype
         elif self.type != None:
             type = self.type
@@ -1060,6 +1079,8 @@ def __init__(self, **kwargs):
         self.id_db = kwargs.get('id_db', None)
         self.geo_data = kwargs.get('geo_data', None)
         self.grid = kwargs.get('grid', None)
+        # workaround for connecting more than one component to a bus
+        self.helper_component = kwargs.get('helper_component', None)
 
     @property
     def network(self):
@@ -1089,17 +1110,25 @@ class LoadDing0:
         The MV or LV grid that this Load is to be a part of.
     peak_load : :obj:`float`
         Peak load of the current object
+    building_id : :obj:`int`
+        refers to OSM oder eGo^n ID, depending on chosen database
 
     """
+    #ToDo: Add consumption, type and sector to the documentation
 
     def __init__(self, **kwargs):
-        self.id_db = kwargs.get('id', None)
+        self.id_db = kwargs.get('id_db', None)
         self.geo_data = kwargs.get('geo_data', None)
         self.grid = kwargs.get('grid', None)
         self.peak_load = kwargs.get('peak_load', None)
+        self.peak_load_residential = kwargs.get('peak_load_residential', None)
+        self.number_households = kwargs.get('number_households', None)
+        self.peak_load_cts = kwargs.get('peak_load_cts', None)
+        self.peak_load_industrial = kwargs.get('peak_load_industrial', None)
         self.consumption = kwargs.get('consumption', None)
-
-        self.id_db = self.grid.loads_count() + 1
+        self.building_id = kwargs.get('building_id', None)
+        self.sector = kwargs.get('sector', None)
+        self.type = kwargs.get('type', None)
 
     @property
     def network(self):
@@ -1112,8 +1141,9 @@ def network(self):
         :class:`~.ding0.core.network.NetworkDing0`
         """
         return self.grid.network
-
-    @property
+    
+    # has to be shifted to LV / MV load because needs to be distinguished
+    '''@property
     def pypsa_bus_id(self):
         """
         Creates a unique identification for the generator
@@ -1125,7 +1155,7 @@ def pypsa_bus_id(self):
         :obj:`str`
         """
         return '_'.join(['Bus', 'mvgd', str(self.grid.grid_district.lv_load_area.mv_grid_district.mv_grid.\
-                id_db), 'lvgd', str(self.grid.id_db), 'loa', str(self.id_db)])
+                id_db), 'lvgd', str(self.grid.id_db), 'loa', str(self.id_db)])'''
 
 
 class CircuitBreakerDing0:
diff --git a/ding0/core/network/cable_distributors.py b/ding0/core/network/cable_distributors.py
index 98453daf..38cf0a20 100644
--- a/ding0/core/network/cable_distributors.py
+++ b/ding0/core/network/cable_distributors.py
@@ -28,6 +28,7 @@ class MVCableDistributorDing0(CableDistributorDing0):
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
 
+        self.osm_id_node = kwargs.get('osm_id_node', None)  # defined node in graph where cable dist is located
         self.lv_load_area_group = kwargs.get('lv_load_area_group', None)
         self.id_db = self.grid.cable_distributors_count() + 1
 
@@ -61,7 +62,6 @@ class LVCableDistributorDing0(CableDistributorDing0):
         Description #TODO
     in_building : 
         Description #TODO
-    
     """
 
     def __init__(self, **kwargs):
@@ -70,7 +70,6 @@ def __init__(self, **kwargs):
         self.string_id = kwargs.get('string_id', None)
         self.branch_no = kwargs.get('branch_no', None)
         self.load_no = kwargs.get('load_no', None)
-        self.id_db = self.grid.cable_distributors_count() + 1
         self.in_building = kwargs.get('in_building', False)
 
     @property
diff --git a/ding0/core/network/grids.py b/ding0/core/network/grids.py
index ff969462..6f5d807c 100644
--- a/ding0/core/network/grids.py
+++ b/ding0/core/network/grids.py
@@ -19,8 +19,8 @@
 from ding0.core.network import RingDing0, CircuitBreakerDing0
 from ding0.core.network.loads import *
 from ding0.core.network.cable_distributors import MVCableDistributorDing0, LVCableDistributorDing0
-from ding0.grid.mv_grid import mv_routing, mv_connect
-from ding0.grid.lv_grid import build_grid, lv_connect
+from ding0.grid.mv_grid import mv_routing, mv_connect, urban_mv_routing
+from ding0.grid.lv_grid import build_grid, build_grid_on_osm_ways, lv_connect
 from ding0.tools import config as cfg_ding0, pypsa_io, tools
 from ding0.tools.geo import calc_geo_dist
 from ding0.grid.mv_grid.tools import set_circuit_breakers
@@ -37,7 +37,7 @@
 if not 'READTHEDOCS' in os.environ:
     from shapely.ops import transform
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 class MVGridDing0(GridDing0):
@@ -70,6 +70,8 @@ def __init__(self, **kwargs):
         self.default_branch_kind_aggregated = kwargs.get('default_branch_kind_aggregated', None)
         self.default_branch_type_aggregated = kwargs.get('default_branch_type_aggregated', None)
 
+        self.lv_generators_to_connect = []
+
         self.add_station(kwargs.get('station', None))
 
     def station(self):
@@ -132,7 +134,7 @@ def add_station(self, mv_station, force=False):
             else:
                 raise Exception('MV Station already set, use argument `force=True` to override.')
 
-    def add_load(self, lv_load):
+    def add_load(self, mv_load):
         """Adds a MV load to _loads and grid graph if not already existing
         
         Args
@@ -140,10 +142,10 @@ def add_load(self, lv_load):
         lv_load : float
             Desription #TODO
         """
-        if lv_load not in self._loads and isinstance(lv_load,
+        if mv_load not in self._loads and isinstance(mv_load,
                                                      MVLoadDing0):
-            self._loads.append(lv_load)
-            self.graph_add_node(lv_load)
+            self._loads.append(mv_load)
+            self.graph_add_node(mv_load)
 
     def add_cable_distributor(self, cable_dist):
         """Adds a cable distributor to _cable_distributors if not already existing
@@ -214,7 +216,8 @@ def rings_nodes(self, include_root_node=False, include_satellites=False):
         for ring in nx.cycle_basis(self.graph, root=self._station):
             
             if not include_root_node:
-                ring.remove(self._station)
+                if self._station in ring:
+                    ring.remove(self._station)
 
             # make sure rings are always returned in same order, starting with
             # node of which representative is smaller
@@ -224,7 +227,6 @@ def rings_nodes(self, include_root_node=False, include_satellites=False):
                 ring.reverse()
 
             if include_satellites:
-                ring_nodes = ring
                 satellites = []
                 for ring_node in ring:
                     # determine all branches diverging from each ring node
@@ -368,7 +370,7 @@ def routing(self, debug=False, anim=None):
                                                        debug=debug)
         logger.info('==> MV Sat1 for {} done'.format(repr(self)))
 
-        # connect satellites to closest line/station on a MV ring that have not been connected in step 1
+        # connect satellites to the closest line/station to an MV ring that has not been connected in step 1
         self._graph = mv_connect.mv_connect_satellites(mv_grid=self,
                                                        graph=self.graph,
                                                        mode='isolated',
@@ -380,6 +382,27 @@ def routing(self, debug=False, anim=None):
                                                      graph=self.graph,
                                                      debug=debug)
         logger.info('==> MV Stations for {} done'.format(repr(self)))
+    
+    #PAUL new: introduced function in grids to access urban_mv_routing
+        
+    def urban_routing(self, debug=False, anim=None):
+        """ Performs routing on aggregated Load Area to build MV grid with ring topology.
+
+        Args
+        ----
+        debug: bool, defaults to False
+            If True, information is printed while routing
+        anim: type, defaults to None
+            Descr #TODO
+        """
+
+        # do the routing
+        # PAUL new: pass mv_grid instead of graph, in order to be able to navigate to
+        # aggregated load area
+        self._graph = urban_mv_routing.solve(mv_grid=self,
+                                       debug=debug,
+                                       anim=anim)
+        logger.info('==> Urban MV Routing for {} done'.format(repr(self)))
 
     def connect_generators(self, debug=False):
         """ Connects MV generators (graph nodes) to grid (graph)
@@ -392,6 +415,17 @@ def connect_generators(self, debug=False):
 
         self._graph = mv_connect.mv_connect_generators(self.grid_district, self.graph, debug)
 
+    def connect_lv_generators(self, debug=False):
+        """ Connects LV generators (graph nodes) to grid (graph)
+
+        Args
+        ----
+        debug: bool, defaults to False
+             If True, information is printed during process
+        """
+
+        lv_connect.lv_connect_generators(self, debug)
+
     def parametrize_grid(self, debug=False):
         """ Performs Parametrization of grid equipment:
         
@@ -497,6 +531,7 @@ def set_voltage_level(self, mode='distance'):
             for node in self.graph_nodes_sorted():
                 if isinstance(node, LVLoadAreaCentreDing0):
                     # calc distance from MV-LV station to LA centre
+                    srid = 3035  # param due to station is in 4326 and needs to bre transformed to 3035 
                     dist_node = calc_geo_dist(self.station(), node) / 1e3
                     if dist_node > dist_max:
                         dist_max = dist_node
@@ -719,7 +754,7 @@ def export_to_pypsa(self, session, method='onthefly', only_calc_mv = True):
                                                   start_time=start_time)
         elif method == 'onthefly':
             buses_df, generators_df, lines_df, loads_df, transformer_df = initialize_component_dataframes()
-            components, _, components_data = fill_mvgd_component_dataframes(self.grid_district, buses_df, generators_df,
+            components, _, _, components_data = fill_mvgd_component_dataframes(self.grid_district, buses_df, generators_df,
                                                                             lines_df, loads_df, transformer_df,
                                                                             only_export_mv=only_calc_mv,
                                                                             return_time_varying_data=True)
@@ -763,7 +798,7 @@ def run_powerflow(self, method='onthefly', only_calc_mv = True,  export_pypsa_di
 
         elif method == 'onthefly':
             buses_df, generators_df, lines_df, loads_df, transformer_df = initialize_component_dataframes()
-            components, _,  components_data = fill_mvgd_component_dataframes(self.grid_district, buses_df, generators_df,
+            components, _, _, components_data = fill_mvgd_component_dataframes(self.grid_district, buses_df, generators_df,
                                                                          lines_df, loads_df, transformer_df,  only_export_mv=only_calc_mv,
                                                                          return_time_varying_data=True)
             pypsa_io.run_powerflow_onthefly(components,
@@ -899,36 +934,59 @@ def add_cable_dist(self, lv_cable_dist):
         lv_cable_dist : 
             Description #TODO
         """
+        # THIS IS NOT WORKING FOR ME. e.g.
+        # Create two LVCableDist as a and b
+        # a = LVCableDist_mvgd_40_lvgd_4488000000_2274291202
+        # b = LVCableDist_mvgd_40_lvgd_4488000000_2274291202
+        # self._cable_distributors = []
+        # self._cable_distributors.append(a)
+        # if b not in self._cable_distributors:
+        #     self._cable_distributors.append(b)
+        # resulting in 
+        # self._cable_distributors.append = [LVCableDist_mvgd_40_lvgd_4488000000_2274291202, LVCableDist_mvgd_40_lvgd_4488000000_2274291202]
+        # Thus get_cable_dist is added
         if lv_cable_dist not in self._cable_distributors and isinstance(lv_cable_dist,
                                                                         LVCableDistributorDing0):
             self._cable_distributors.append(lv_cable_dist)
             self.graph_add_node(lv_cable_dist)
+    
+    def get_cable_dist(self, lv_cable_dist_id_db):
+        """Get a LV cable_dist if already existing
+        
+        Parameters
+        ----------
+        lv_cable_dist : 
+            Description #TODO
+        """
+        for lv_cable_dist in self._cable_distributors:
+            if lv_cable_dist.id_db == lv_cable_dist_id_db:
+                return lv_cable_dist
 
     def build_grid(self):
         """Create LV grid graph
         """
+    
+        if True:  # new approach
 
-        # add required transformers
-        build_grid.transformer(self)
+            # own grid building
+            build_grid_on_osm_ways.build_branches_on_osm_ways(self.grid_district) # Roberts Implemenataion skip build grid
 
-        # add branches of sectors retail/industrial and agricultural
-        build_grid.build_ret_ind_agr_branches(self.grid_district)
+            # add required transformers
+            build_grid.transformer(self)
+            
+        else:     # ding0 default
 
-        # add branches of sector residential
-        build_grid.build_residential_branches(self.grid_district)
+            # add required transformers
+            build_grid.transformer(self)
 
-        #self.graph_draw(mode='LV')
+            # add branches of sectors retail/industrial and agricultural
+            build_grid.build_ret_ind_agr_branches(self.grid_district)
 
-    def connect_generators(self, debug=False):
-        """ Connects LV generators (graph nodes) to grid (graph)
+            # add branches of sector residential
+            build_grid.build_residential_branches(self.grid_district)
 
-        Args
-        ----
-        debug: bool, defaults to False
-             If True, information is printed during process
-        """
+        #self.graph_draw(mode='LV')
 
-        self._graph = lv_connect.lv_connect_generators(self.grid_district, self.graph, debug)
 
     def reinforce_grid(self):
         """ Performs grid reinforcement measures for current LV grid.
diff --git a/ding0/core/network/loads.py b/ding0/core/network/loads.py
index 29965daa..6973b285 100644
--- a/ding0/core/network/loads.py
+++ b/ding0/core/network/loads.py
@@ -28,6 +28,11 @@ class MVLoadDing0(LoadDing0):
 
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
+        self.id_db = self.grid.mv_grid.loads_count() + 1
+        self.osmid_building = kwargs.get('osmid_building', None)
+        self.osmid_nn = kwargs.get('osmid_nn', None)
+        self.nn_coords = kwargs.get('nn_coords', None)
+        self.lv_load_area = kwargs.get('lv_load_area', None)
 
     def __repr__(self):
         """
@@ -40,6 +45,19 @@ def __repr__(self):
         """
         return '_'.join(['Load', 'mvgd', str(self.grid.id_db),
                          str(self.id_db)])
+    
+    @property
+    def pypsa_bus_id(self):
+        """
+        Creates a unique identification for the generator
+        to export to pypsa using the id_db of the mv_grid
+        and the current object
+
+        Returns
+        -------
+        :obj:`str`
+        """
+        return '_'.join(['Bus', 'mvgd', str(self.grid.id_db), 'mvload', str(self.id_db)])
 
 
 class LVLoadDing0(LoadDing0):
@@ -70,3 +88,17 @@ def __repr__(self):
         return '_'.join(['Load', 'mvgd', str(
             self.grid.grid_district.lv_load_area.mv_grid_district.mv_grid.\
             id_db), 'lvgd', str(self.grid.id_db), str(self.id_db)])
+    
+    @property
+    def pypsa_bus_id(self):
+        """
+        Creates a unique identification for the generator
+        to export to pypsa using the id_db of the mv_grid
+        and the current object
+
+        Returns
+        -------
+        :obj:`str`
+        """
+        return '_'.join(['Bus', 'mvgd', str(self.grid.grid_district.lv_load_area.mv_grid_district.mv_grid.\
+                id_db), 'lvgd', str(self.grid.id_db), 'loa', str(self.id_db)])
diff --git a/ding0/core/network/stations.py b/ding0/core/network/stations.py
index 6f7828e6..d65ea720 100644
--- a/ding0/core/network/stations.py
+++ b/ding0/core/network/stations.py
@@ -220,6 +220,7 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
 
         self.lv_load_area = kwargs.get('lv_load_area', None)
+        self.osm_id_node = kwargs.get('osm_id_node', None)  # defined node in graph where station is located
 
     @property
     def peak_generation(self):
diff --git a/ding0/core/structure/regions.py b/ding0/core/structure/regions.py
index a078cc15..315cfdf6 100644
--- a/ding0/core/structure/regions.py
+++ b/ding0/core/structure/regions.py
@@ -16,6 +16,7 @@
 import os
 from . import RegionDing0
 from ding0.tools import config as cfg_ding0
+# from ding0.core.network.loads import MVLoadDing0
 
 if not 'READTHEDOCS' in os.environ:
     from shapely.wkt import loads as wkt_loads
@@ -23,7 +24,7 @@
 
 class MVGridDistrictDing0(RegionDing0):
         # TODO: check docstring
-    """Defines a MV-grid_district in DINGO
+    """Defines a MV-grid_district in DfINGO
     
     Attributes
     ----------
@@ -122,10 +123,15 @@ def add_peak_demand(self):
         (peak load sum and peak load of satellites)
         """
         peak_load = peak_load_satellites = 0
+        
         for lv_load_area in self.lv_load_areas():
             peak_load += lv_load_area.peak_load
             if lv_load_area.is_satellite:
                 peak_load_satellites += lv_load_area.peak_load
+        # PAUL new add mv load to peak load of mvgd    
+        for mv_load in self.mv_grid._loads:
+            peak_load += mv_load.peak_load
+            
         self.peak_load = peak_load
         self.peak_load_satellites = peak_load_satellites
 
@@ -161,6 +167,12 @@ class LVLoadAreaDing0(RegionDing0):
        Descr
     db_data: :pandas:`pandas.DatetimeIndex<datetimeindex>`
        Descr
+
+    # new osm approach
+    load_area_graph: networkx.MultiDiGraph
+        contains all streets in load_area
+    MVLoads: list
+        list containing ding0.MVLoads in lvla
     """
 
     def __init__(self, **kwargs):
@@ -169,6 +181,7 @@ def __init__(self, **kwargs):
 
         # more params
         self._lv_grid_districts = []
+        self._mv_loads = []
         self.ring = kwargs.get('ring', None)
         self.mv_grid_district = kwargs.get('mv_grid_district', None)
         self.lv_load_area_centre = kwargs.get('lv_load_area_centre', None)
@@ -188,7 +201,7 @@ def __init__(self, **kwargs):
             for attribute in list(db_data.keys()):
                 setattr(self, attribute, db_data[attribute])
 
-        # convert geo attributes to to shapely objects
+        # convert geo attributes to shapely objects
         if hasattr(self, 'geo_area'):
             self.geo_area = wkt_loads(self.geo_area)
         if hasattr(self, 'geo_centre'):
@@ -203,12 +216,16 @@ def __init__(self, **kwargs):
             self.peak_load_industrial = self.peak_load_industrial
         if hasattr(self, 'peak_load_agricultural'):
             self.peak_load_agricultural = self.peak_load_agricultural
+        
         if hasattr(self, 'peak_load'):
             self.peak_load = self.peak_load
+            
+        self.peak_load = kwargs.get('peak_load', None)
+        self.load_area_graph = kwargs.get('load_area_graph', False)
 
-            # if load area has got a peak load less than load_area_sat_threshold, it's a satellite
-            if self.peak_load < load_area_sat_load_threshold:
-                self.is_satellite = True
+        # if load area has got a peak load less than load_area_sat_threshold, it's a satellite
+        if self.peak_load < load_area_sat_load_threshold:
+            self.is_satellite = True
 
     @property
     def network(self):
@@ -235,8 +252,30 @@ def lv_grid_districts_count(self):
         """
         return len(self._lv_grid_districts)
 
+    def mv_loads_count(self):
+        """Returns the count of MV loads
+
+        Returns
+        -------
+        int
+            Number of MV loads.
+        """
+        return len(self._mv_loads)
+
+    def add_mv_load(self, mv_load):
+        """Adds a MVLoad to _lv_grid_districts if not already existing
+        
+        Parameters
+        ----------
+        mv_load: ding0.core.network.MVLoadDing0
+        """
+
+        if mv_load not in self._mv_loads:
+        # TODO: CHECK IF ITS AN MV LOAD
+        # isinstance(mv_load, MVLoadDing0):
+            self._mv_loads.append(mv_load)
+
     def add_lv_grid_district(self, lv_grid_district):
-        # TODO: check docstring
         """Adds a LV grid district to _lv_grid_districts if not already existing
         
         Parameters
@@ -295,6 +334,9 @@ def __init__(self, **kwargs):
         self.grid = kwargs.get('grid', None)
         self.geo_data = kwargs.get('geo_data', None)
         self.lv_load_area = kwargs.get('lv_load_area', None)
+        # PAUL new
+        self.osm_id_node = kwargs.get('osm_id_node', None)
+        
 
     @property
     def network(self):
@@ -350,6 +392,9 @@ class LVGridDistrictDing0(RegionDing0):
        Descr
     sector_count_agricultural: :obj:`int`
        Descr
+       
+       
+    TODO UPDATE DESCR FOR GRAPH AND BUILDINGS
     """
 
     def __init__(self, **kwargs):
@@ -385,6 +430,13 @@ def __init__(self, **kwargs):
         self.sector_consumption_agricultural = kwargs.get(
             'sector_consumption_agricultural',
             None)
+        
+        # todo: do doc
+        self.mvlv_subst_id = kwargs.get('mvlv_subst_id', None)
+        self.graph_district = kwargs.get('graph_district', None)
+        self.buildings = kwargs.get('buildings_district', None)
+        self.peak_load = kwargs.get('peak_load', None)
+        
 
     @property
     def network(self):
diff --git a/ding0/examples/example_multiple_grid_districts.py b/ding0/examples/example_multiple_grid_districts.py
old mode 100755
new mode 100644
index 7de79216..16645921
--- a/ding0/examples/example_multiple_grid_districts.py
+++ b/ding0/examples/example_multiple_grid_districts.py
@@ -55,6 +55,8 @@ def create_results_dirs(base_path):
         os.mkdir(os.path.join(base_path, 'info'))
     if not os.path.exists(os.path.join(base_path, 'log')):
         os.mkdir(os.path.join(base_path, 'log'))
+    if not os.path.exists(os.path.join(base_path, 'grids')):
+        os.mkdir(os.path.join(base_path, 'grids'))
 
 
 def run_multiple_grid_districts(mv_grid_districts, run_id, failsafe=False,
@@ -111,13 +113,12 @@ def run_multiple_grid_districts(mv_grid_districts, run_id, failsafe=False,
         # instantiate ding0  network object
         nd = NetworkDing0(name='network', run_id=run_id)
 
-        if not os.path.exists(os.path.join(base_path, "grids")):
-            os.mkdir(os.path.join(base_path, "grids"))
-
         if not failsafe:
             # run DING0 on selected MV Grid District
             msg = nd.run_ding0(session=session,
-                               mv_grid_districts_no=[mvgd])
+                               mv_grid_districts_no=[mvgd],
+                               # TODO: use new method and egon-data
+                               ding0_legacy=False, local_db=False, egon_db=True)
 
             # save results
             if save_as == 'csv':
@@ -131,7 +132,8 @@ def run_multiple_grid_districts(mv_grid_districts, run_id, failsafe=False,
             # try to perform ding0 run on grid district
             try:
                 msg = nd.run_ding0(session=session,
-                                   mv_grid_districts_no=[mvgd])
+                                   mv_grid_districts_no=[mvgd],
+                                   ding0_legacy=False, local_db=False, egon_db=True)
                 # if not successful, put grid district to report
                 if msg:
                     corrupt_grid_districts = corrupt_grid_districts.append(
@@ -196,8 +198,8 @@ def run_multiple_grid_districts(mv_grid_districts, run_id, failsafe=False,
     create_results_dirs(base_path)
 
     # define grid district by its id (int)
-    mv_grid_districts = list(range(1729, 1732))
-
+    # mv_grid_districts = list(range(1729, 1732))
+    mv_grid_districts = [2534]
     # run grid districts
     run_multiple_grid_districts(mv_grid_districts,
                                 run_id,
diff --git a/ding0/examples/example_parallel_multiple_grid_districts.py b/ding0/examples/example_parallel_multiple_grid_districts.py
index 9172eacf..5e0149de 100644
--- a/ding0/examples/example_parallel_multiple_grid_districts.py
+++ b/ding0/examples/example_parallel_multiple_grid_districts.py
@@ -174,7 +174,8 @@ def process_runs(mv_districts, n_of_districts, output_info, run_id, base_path,
             nw_name = nw_name+'_to_'+str(cl[-1])
         nw = NetworkDing0(name=nw_name)
         try:
-            msg = nw.run_ding0(session=session, mv_grid_districts_no=cl)
+            msg = nw.run_ding0(session=session, mv_grid_districts_no=cl, debug=True,
+                               ding0_legacy=False, local_db=False, egon_db=True)
             if msg:
                 status = 'run error'
             else:
@@ -190,7 +191,7 @@ def process_runs(mv_districts, n_of_districts, output_info, run_id, base_path,
                     results.save_nd_to_pickle(nw, os.path.join(base_path, run_id))
                 else:
                     msg = 'save_as not correct, network not saved.'
-            output_clusters.append((nw_name,status,msg, nw.metadata))
+            output_clusters.append((nw_name, status, msg, nw.metadata))
         except Exception as e:
             output_clusters.append((nw_name,  'corrupt dist', e, nw.metadata))
             continue
@@ -235,10 +236,11 @@ def process_metadata(meta):
     base_path = BASEPATH
 
     # set run_id to current timestamp
-    run_id = datetime.now().strftime("%Y%m%d%H%M%S")
+    run_id = datetime.now().strftime("run_%Y-%m-%d-%H-%M-%S")
 
     # run in parallel
-    mv_grid_districts = list(range(1, 3609))
+    # mv_grid_districts = list(range(1, 3609))
+    mv_grid_districts = [2534]
     n_of_processes = mp.cpu_count() #number of parallel threaths
     n_of_districts = 1 #n° of districts in each serial cluster
 
diff --git a/ding0/flexopt/check_tech_constraints.py b/ding0/flexopt/check_tech_constraints.py
index 53d03468..7ef358f3 100644
--- a/ding0/flexopt/check_tech_constraints.py
+++ b/ding0/flexopt/check_tech_constraints.py
@@ -26,7 +26,7 @@
 import math
 
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 def check_load(grid, mode):
@@ -111,9 +111,12 @@ def check_load(grid, mode):
             try:
                 # check if s_res exceeds allowed values for laod and feedin case
                 # CAUTION: The order of values is fix! (1. load case, 2. feedin case)
-                if any([s_res * mw2kw > _ for s_res, _ in zip(branch['branch'].s_res, s_max_th_lcfc)]):
-                    # save max. relative overloading
-                    crit_branches[branch] = max(branch['branch'].s_res) * mw2kw / s_max_th
+                s_res_lcfc = branch['branch'].s_res
+                if any([s_res * mw2kw > _ for s_res, _ in zip(s_res_lcfc, s_max_th_lcfc)]):
+                    # save max. relative overloading with respect to load or feedin case
+                    s_res_max = max(s_res_lcfc)
+                    lcfc_idx = s_res_lcfc.index(s_res_max)
+                    crit_branches[branch['branch']] = s_res_max * mw2kw / s_max_th_lcfc[lcfc_idx]
             except:
                 pass
 
@@ -424,12 +427,15 @@ def get_critical_voltage_at_nodes(grid):
     # fill two above lists
     for node in list(nx.descendants(tree, grid._station)):
         successors = list(tree.successors(node))
-        if successors and all(isinstance(successor, LVCableDistributorDing0)
-               for successor in successors):
+        if (
+                successors
+                and all(isinstance(successor, LVCableDistributorDing0) for successor in successors)
+        ):
             main_branch.append(node)
-        elif (isinstance(node, LVCableDistributorDing0) and
-            all(isinstance(successor, (GeneratorDing0, LVLoadDing0))
-               for successor in successors)):
+        elif (
+                isinstance(node, LVCableDistributorDing0)
+                and all(isinstance(successor, (GeneratorDing0, LVLoadDing0)) for successor in successors)
+        ):
             grid_conn_points.append(node)
 
     v_delta_load_case_bus_bar, v_delta_gen_case_bus_bar  = get_voltage_at_bus_bar(grid, tree)
@@ -443,8 +449,7 @@ def get_critical_voltage_at_nodes(grid):
 
 
     # voltage at main route nodes
-    for first_node in [b for b in tree.successors(grid._station)
-                   if b in main_branch]:
+    for first_node in [b for b in tree.successors(grid._station) if b in main_branch]:
 
         # initiate loop over feeder
         successor = first_node
@@ -461,30 +466,46 @@ def get_critical_voltage_at_nodes(grid):
             v_delta_gen_cum += voltage_delta_gen
 
             # roughly estimate transverse voltage drop
-            stub_node = [_ for _ in tree.successors(successor) if
-                         _ not in main_branch][0]
-            v_delta_load_stub, v_delta_gen_stub  = get_delta_voltage_preceding_line(grid, tree, stub_node)
+            stub_nodes = [_ for _ in tree.successors(successor) if _ not in main_branch]
+            if stub_nodes:
+                stub_node = stub_nodes[0]
+                v_delta_load_stub, v_delta_gen_stub = get_delta_voltage_preceding_line(grid, tree, stub_node)
 
             # check if voltage drop at node exceeds tolerable voltage drop
-            if (abs(v_delta_gen_cum) > (v_delta_tolerable_fc)
-                or abs(v_delta_load_cum) > (
-                            v_delta_tolerable_lc)):
+            if (
+                    abs(v_delta_gen_cum) > (v_delta_tolerable_fc)
+                    or abs(v_delta_load_cum) > (v_delta_tolerable_lc)
+            ):
                 # add node and successing stub node to critical nodes
-                crit_nodes.append({'node': successor,
-                                   'v_diff': [v_delta_load_cum,
-                                              v_delta_gen_cum]})
-                crit_nodes.append({'node': stub_node,
-                                   'v_diff': [
-                                       v_delta_load_cum + v_delta_load_stub,
-                                       v_delta_gen_cum + v_delta_gen_stub]})
+                crit_nodes.append({
+                    'node': successor,
+                    'v_diff': [
+                        v_delta_load_cum,
+                        v_delta_gen_cum
+                    ]
+                })
+                if stub_nodes:
+                    crit_nodes.append({
+                        'node': stub_node,
+                        'v_diff': [
+                            v_delta_load_cum + v_delta_load_stub,
+                            v_delta_gen_cum + v_delta_gen_stub
+                        ]
+                    })
             # check if voltage drop at stub node exceeds tolerable voltage drop
-            elif ((abs(v_delta_gen_cum + v_delta_gen_stub) > v_delta_tolerable_fc)
-                or (abs(v_delta_load_cum + v_delta_load_stub) > v_delta_tolerable_lc)):
-                # add stub node to critical nodes
-                crit_nodes.append({'node': stub_node,
-                                   'v_diff': [
-                                       v_delta_load_cum + v_delta_load_stub,
-                                       v_delta_gen_cum + v_delta_gen_stub]})
+            elif stub_nodes:
+                if (
+                        (abs(v_delta_gen_cum + v_delta_gen_stub) > v_delta_tolerable_fc)
+                        or (abs(v_delta_load_cum + v_delta_load_stub) > v_delta_tolerable_lc)
+                ):
+                    # add stub node to critical nodes
+                    crit_nodes.append({
+                        'node': stub_node,
+                        'v_diff': [
+                            v_delta_load_cum + v_delta_load_stub,
+                            v_delta_gen_cum + v_delta_gen_stub
+                        ]
+                    })
 
 
             successor = [_ for _ in tree.successors(successor)
diff --git a/ding0/flexopt/reinforce_grid.py b/ding0/flexopt/reinforce_grid.py
index 4dc45dd9..02262758 100644
--- a/ding0/flexopt/reinforce_grid.py
+++ b/ding0/flexopt/reinforce_grid.py
@@ -22,7 +22,7 @@
 import logging
 
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 def reinforce_grid(grid, mode):
diff --git a/ding0/flexopt/reinforce_measures.py b/ding0/flexopt/reinforce_measures.py
index b1c4620c..c66e695a 100644
--- a/ding0/flexopt/reinforce_measures.py
+++ b/ding0/flexopt/reinforce_measures.py
@@ -25,7 +25,7 @@
 import logging
 
 package_path = ding0.__path__[0]
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 def reinforce_branches_current(grid, crit_branches):
@@ -63,13 +63,13 @@ def reinforce_branches_current(grid, crit_branches):
         try:
             type = branch_parameters.loc[
                 branch_parameters[
-                    branch_parameters['I_max_th'] >= branch['branch']
+                    branch_parameters['I_max_th'] >= branch 
                     .type['I_max_th'] * rel_overload
                 ].loc[
                     :, 'I_max_th'
                 ].idxmin(), :
             ]
-            branch['branch'].type = type
+            branch.type = type
             branch_ctr += 1
         except:
             logger.warning('Branch {} could not be reinforced (current '
diff --git a/ding0/grid/lv_grid/build_grid.py b/ding0/grid/lv_grid/build_grid.py
index 92c5b803..ad56e94a 100644
--- a/ding0/grid/lv_grid/build_grid.py
+++ b/ding0/grid/lv_grid/build_grid.py
@@ -21,7 +21,7 @@
 import logging
 import math
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 def select_transformers(grid, s_max=None):
@@ -155,6 +155,9 @@ def transformer(grid):
     ----------
     grid: LVGridDing0
         LV grid data
+        
+    s_max_from_buildings: boolean
+        new approach if s_max_from_buildings
     """
     v_nom = cfg_ding0.get('assumptions', 'lv_nominal_voltage') / 1e3  # v_nom in kV
     # choose size and amount of transformers
diff --git a/ding0/grid/lv_grid/build_grid_on_osm_ways.py b/ding0/grid/lv_grid/build_grid_on_osm_ways.py
new file mode 100644
index 00000000..7c2a829e
--- /dev/null
+++ b/ding0/grid/lv_grid/build_grid_on_osm_ways.py
@@ -0,0 +1,521 @@
+"""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"
+
+import networkx as nx
+import nxmetis
+# G. Karypis, V. Kumar: A fast and high quality multilevel scheme for partitioning irregular graphs
+# https://www.cs.utexas.edu/~pingali/CS395T/2009fa/papers/metis.pdf
+
+import osmnx as ox
+import numpy as np
+from shapely.geometry import LineString, Point
+
+from ding0.core.network import BranchDing0
+from ding0.core.network.cable_distributors import LVCableDistributorDing0
+from ding0.core.network.loads import LVLoadDing0
+from ding0.tools import config as cfg_ding0
+from ding0.grid.lv_grid.routing import identify_street_loads
+from ding0.grid.mv_grid.tools import get_shortest_path_shp_single_target, get_shortest_path_shp_multi_target
+
+from ding0.config.config_lv_grids_osm import get_config_osm, get_load_profile_categories
+from ding0.grid.lv_grid.parameterization import get_peak_load_for_residential
+from ding0.grid.lv_grid.graph_processing import simplify_graph_adv
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def get_routed_graph(
+        graph, station_node, building_nodes, generator_nodes=None):
+    '''
+    routing via shortest path from station to loads and gens
+    input graph representing all streets in lvgd and
+        node of station in graph, all nodes of buildings.
+        generator_nodes is none by default due to they are connected
+        in a ding0 default way at a later point corresponding to load
+    remove edges which are not mandatory and return routed graph
+    '''
+    routed_graph = graph.copy()
+
+    edges_to_keep = []
+    for n in building_nodes:
+        route = nx.shortest_path(graph, n, station_node, weight='length')
+        for i, r in enumerate(route[:-1]):
+            edges_to_keep.append((r, route[i + 1]))
+    edges_to_keep = list(set(edges_to_keep))
+    edges_to_del = []
+    for edge in list(graph.edges()):
+        if edge not in edges_to_keep:
+            if station_node not in edge:
+                edges_to_del.append(edge)
+    routed_graph.remove_edges_from(edges_to_del)
+    logging.info('Routing shortest path for each node to station. '
+                 f'{len(edges_to_del)} edges removed in graph to obtain '
+                 'shortest path tree.')
+    # due to edges are removed, nodes which are not connected anymore needs to be removed also
+    # e.g. nodes which representing intersections on ways but not mandatory for shortest tree
+    # graph to route loads to station need to be removed to dont add as ding0CableDist
+    for loose_components in list(nx.weakly_connected_components(routed_graph)):
+        if len(loose_components) == 1:  # there is only one not conencted node
+            for node in loose_components:
+                routed_graph.remove_node(node)
+
+    return routed_graph
+
+
+##############
+
+def relocate_buildings_with_station_as_nn(full_graph, station_id, lv_loads_grid):
+    station_nbs = [n for n in full_graph.neighbors(station_id)]
+
+    if station_nbs:
+
+        G = full_graph.copy()
+        G = G.subgraph(station_nbs)
+
+        station_as_nn = lv_loads_grid.loc[lv_loads_grid['nn'] == station_id]
+
+        # if all or one building has station as nearest nodes, pass over relocation
+        if len(station_as_nn) != len(lv_loads_grid) or len(station_as_nn) > 1:
+
+            for building_node, row in station_as_nn.iterrows():
+                node_id, nn_dist = ox.nearest_nodes(G, row["geometry"].x, row["geometry"].y, return_dist=True)
+                lv_loads_grid.at[building_node, 'nn'] = node_id
+                lv_loads_grid.at[building_node, 'nn_coords'] = Point(G.nodes[node_id]['x'], G.nodes[node_id]['y'])
+                lv_loads_grid.at[building_node, 'nn_dist'] = nn_dist
+
+    return lv_loads_grid
+
+
+from ding0.grid.mv_grid.tools import get_edge_tuples_from_path
+
+
+def get_shortest_path_tree(graph, station_node, building_nodes, generator_nodes=None):
+    '''
+    routing via the shortest path from station to loads and gens
+    input graph representing all streets in lvgd and
+        node of station in graph, all nodes of buildings.
+        generator_nodes is none by default due to they are connected
+        in a ding0 default way at a later point corresponding to load
+    remove edges which are not mandatory and return shortest path tree
+    '''
+
+    G = graph.copy()
+
+    if (len(building_nodes) == 1 and building_nodes[0] == station_node) or (len(building_nodes) == 0):
+
+        sp_tree = G.subgraph([station_node])
+
+    else:
+
+        if len(G) > 1:
+
+            edges = []
+
+            for n in building_nodes:
+                route = nx.shortest_path(G, n, station_node, weight='length')
+
+                edge_path = get_edge_tuples_from_path(G, route)
+
+                edges.extend(edge_path)
+
+            edges_to_keep = set(edges)
+
+            sp_tree = G.edge_subgraph(edges_to_keep)
+
+        else:
+
+            sp_tree = G
+
+    return sp_tree
+
+
+###############
+
+def allocate_street_load_nodes(lv_loads_grid, shortest_tree_district_graph, station_id):
+    # in order to do a size constrainted tree partioning
+    # allocate capacity to street nodes (as integer)
+    # allocate number_households to household nodes
+    street_loads, household_loads = identify_street_loads(lv_loads_grid, shortest_tree_district_graph)
+    street_loads_map = street_loads.to_dict()['capacity']
+    household_loads_map = household_loads.to_dict()['number_households']
+
+    for node in shortest_tree_district_graph.nodes:
+
+        if node in street_loads_map.keys():
+            shortest_tree_district_graph.nodes[node]['load'] = int(
+                np.ceil(street_loads_map[node] * 1e3))  # as int * 1000
+        else:
+            shortest_tree_district_graph.nodes[node]['load'] = 0
+        if node in household_loads_map:
+            shortest_tree_district_graph.nodes[node]['n_households'] = household_loads_map[node]
+        else:
+            shortest_tree_district_graph.nodes[node]['n_households'] = 0
+
+    # make sure substation's capacity is always zero
+    shortest_tree_district_graph.nodes[station_id]['load'] = 0
+
+    return shortest_tree_district_graph, street_loads, household_loads
+
+
+def get_cable_type_by_load(lvgd, capacity, cable_lf, cos_phi_load, v_nom):
+    """ get cable type for given capacity as param """
+    I_max_load = capacity / (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]
+    if len(suitable_cables_stub):
+        cable_type_stub = suitable_cables_stub.loc[suitable_cables_stub['I_max_th'].idxmin(), :]
+    else:  # TODO: what to do if no cable is suitable for I_max_load, e.g. many loads connected to feeder?
+        cable_type_stub = lvgd.lv_grid.network.static_data['LV_cables'].iloc[0]  # take strongest cable if no one suits
+
+    return cable_type_stub
+
+
+def get_n_feeder_mandatory(capacity, v_nom, cos_phi_load):
+    I_max_allowed = 275  ##TODO: get value via config from standard cable type # refers to 150 mm2 standard type ref: dena Verteilnetzstudie
+    I_max_load_at_feeder = capacity / (3 ** 0.5 * v_nom) / cos_phi_load
+    return np.ceil(I_max_load_at_feeder / I_max_allowed)
+
+
+def build_branches_on_osm_ways(lvgd):
+    """
+    Based on osm ways, the according grid topology for
+    residential sector is determined and attached to the grid graph
+    Parameters
+    ----------
+    lvgd : LVGridDistrictDing0
+        Low-voltage grid district object
+    """
+    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
+
+    # obtain shortest_tree_graph_district from graph_district
+    # due to graph_district contains all osm ways in district
+    # e.g. circles and it is not shortest paths
+    # 1.) start with filtering for loads < 100kW.
+    #     they will be connected in grid
+    # 2.) get loads w. capacity >= 100kW and capacity < 200 kW
+    #     and route em via shortest path to station. connect em
+    #     with em own feeder/ cabel
+    # 3.) each load with capacity >= 200kW and < 5.5 MW have its
+    #     own station to which it is connected
+
+    # get overall count of branches to set unique branch_no
+    # mandatory to add loads between 100 - 200 kW
+    # branch_count_sum = len(list(
+    #    lvgd.lv_grid.graph.neighbors(lvgd.lv_grid.station())))
+
+    # get diversity factor for non residential
+    diversity_factor = get_config_osm('diversity_factor_not_residential')
+    load_profile_categories = get_load_profile_categories()
+
+    # station_id is node in graph which is root node
+    station_id = lvgd.lv_grid._station.osm_id_node
+    logger.info(f"LVGD: {lvgd}")
+    # separate loads w. capacity: loads < 100 kW connected to grid
+    lv_loads_grid = lvgd.buildings.loc[
+        lvgd.buildings.capacity < get_config_osm('lv_threshold_capacity')]
+
+    # full graph for routing 100 - 200 kW to station
+    full_graph = lvgd.graph_district.copy()  # .to_undirected()
+    full_graph_simple = nx.Graph(full_graph)
+
+    # if possible change allocation of buildings to station and find another street node nearby
+    lv_loads_grid = relocate_buildings_with_station_as_nn(full_graph, station_id, lv_loads_grid)
+
+    # get graph for loads < 100 kW
+    shortest_tree_district_graph = get_shortest_path_tree(
+        full_graph,  # graph in district
+        station_id,  # osmid of node representating station
+        lv_loads_grid.nn.tolist())  # list of nodes connected to station
+
+
+    # pre-process graph
+    # allocate loads to street nodes
+    shortest_tree_district_graph, street_loads, household_loads = allocate_street_load_nodes(lv_loads_grid,
+                                                                                             shortest_tree_district_graph,
+                                                                                             station_id)
+    # simplify graph (get rid of cable dists for loads > 100 kW)
+    shortest_tree_district_graph = simplify_graph_adv(shortest_tree_district_graph,
+                                                      lv_loads_grid.nn.tolist() + [station_id])
+    # split the graph into subtrees based on station's incident edges
+    # if number of edges larger 1
+    # prepare the shortest path tree graph
+    g = shortest_tree_district_graph.copy()
+    station_attr = g.nodes[station_id]
+
+    # find subtrees in shortest path tree graph using substation's incident edges
+    # TODO: Check if len(G) is bigger 1 / G.has_edges: lvgd graph with one node -> remove station leads to empty graph
+    if len(g.nodes) > 1:  # only if there are at least 2 nodes, else keep working on full graph
+        g.remove_node(station_id)
+        graph_has_edges = True
+    else:
+        graph_has_edges = False
+
+    nodelists = list(nx.weakly_connected_components(g))
+
+    feederID = 0  # starting with feederId=0 for station. All leaving feeders will get an incremented feederId
+    new_lvgd_peak_load_considering_simultaneity = 0
+    feeder_graph_list = []  # append each feeder here
+
+    # create subtrees from tree graph based on number of with station incident edges
+    for nodelist in nodelists:
+
+        subtree_graph = shortest_tree_district_graph.subgraph(list(nodelist) + [station_id]).to_undirected()
+
+        cum_subtree_load = sum([subtree_graph.nodes[node]['load'] for node in subtree_graph.nodes]) / 1e3
+
+        n_feeder = get_n_feeder_mandatory(cum_subtree_load, v_nom, cos_phi_load)
+
+        # print(f"load {cum_subtree_load} needs n_feeder {n_feeder}")
+
+        if n_feeder > 1:
+
+            # Partition graph without station node
+            G = nx.Graph(subtree_graph.subgraph(list(nodelist)).copy())
+
+            # If the Graph has to be portioned to more than the number of nodes without the station node,
+            # make every Node to a feeder, because METIS sometimes portion not contiguous.
+            if len(G) - 1 > n_feeder:
+                # transform edge length to int
+                for edge in G.edges:
+                    G.edges[edge]['length'] = int(np.ceil(G.edges[edge]['length']))
+
+                (_, parts) = nxmetis.partition(G, int(n_feeder),
+                                                 node_weight='load', edge_weight='length',
+                                                 options=nxmetis.MetisOptions(contig=True))
+
+            else:
+                parts = [list(nodelist)]
+                msg = f"In the LVGrid {lvgd} every grid_district.graph node becomes a feeder."
+                logger.warning(msg)
+                lvgd.network.message.append(msg)
+
+            checked_contiguos_parts = []
+            for cluster in parts:
+                if len(cluster):
+                    for nodes in nx.connected_components(subtree_graph.subgraph(cluster)):
+                        checked_contiguos_parts.append(list(nodes))
+
+            for cluster in checked_contiguos_parts:
+
+                feederID += 1
+
+                # get feeder graphs separately
+                feeder_graph = subtree_graph.subgraph(cluster).copy()
+
+                cum_feeder_graph_load = sum([feeder_graph.nodes[node]['load'] for node in feeder_graph.nodes]) / 1e3
+
+                cable_type_stub = get_cable_type_by_load(lvgd, cum_feeder_graph_load, cable_lf, cos_phi_load, v_nom)
+                for node in cluster:
+                    feeder_graph.nodes[node]['feederID'] = feederID
+
+                for edge in feeder_graph.edges:
+                    feeder_graph.edges[edge]['feederID'] = feederID
+                    feeder_graph.edges[edge]['cable_type_stub'] = cable_type_stub
+
+                if not station_id in feeder_graph.nodes:
+                    line_shp, line_length, line_path = get_shortest_path_shp_multi_target(subtree_graph, station_id,
+                                                                                          cluster)
+                    feeder_graph.add_edge(line_path[0], station_id, 0, geometry=line_shp, length=line_length,
+                                          feederID=feederID, cable_type_stub=cable_type_stub)
+                    feeder_graph.add_node(station_id, **station_attr)
+
+                feeder_graph_list.append(feeder_graph)
+
+        else:
+
+            feederID += 1
+
+            feeder_graph = subtree_graph.copy()
+
+            cable_type_stub = get_cable_type_by_load(lvgd, cum_subtree_load, cable_lf, cos_phi_load, v_nom)
+
+            for node in feeder_graph.nodes:
+                feeder_graph.nodes[node]['feederID'] = feederID
+
+            for edge in feeder_graph.edges:
+                feeder_graph.edges[edge]['feederID'] = feederID
+                feeder_graph.edges[edge]['cable_type_stub'] = cable_type_stub
+
+            feeder_graph_list.append(feeder_graph)
+
+    # Check if a graph of the feeder_graph_list has more than one connected components
+    if max([len(list(nx.connected_components(_))) for _ in feeder_graph_list]) > 1:
+        raise ValueError("Probably partitioned not contiguously.")
+    G = nx.compose_all(feeder_graph_list)
+    G.nodes[station_id]['feederID'] = 0
+
+    station_node = G.nodes[station_id]
+    # reset loads to zero due to adding buildings to graph with em real load
+    # to avoid adding loads twice
+    for node in G.nodes:
+        G.nodes[node]['load'] = 0
+        G.nodes[node]['ding0_node_type'] = "cable_distributor"
+
+    G.nodes[station_id]['ding0_node_type'] = "station"
+
+    # add loads < 100 kW to Graph G
+    for building_node, row in lv_loads_grid.iterrows():
+        nn_attr = G.nodes[row.nn]
+        attr = {'x': row.geometry.x,
+                'y': row.geometry.y,
+                'node_type': 'non_synthetic',
+                'ding0_node_type': "load",
+                'cluster': nn_attr['cluster'],
+                'load': row.capacity,
+                'capacity': row.capacity,
+                'residential_capacity': row.residential_capacity,
+                'number_households': row.number_households,
+                'cts_capacity': row.cts_capacity,
+                'industrial_capacity': row.industrial_capacity,
+                'feederID': nn_attr['feederID'],
+                }
+
+        cable_type_stub = get_cable_type_by_load(lvgd, row.capacity, cable_lf, cos_phi_load, v_nom)
+        G.add_node(building_node, **attr)
+        G.add_edge(building_node, row.nn, 0, geometry=LineString([row.geometry, row.nn_coords]),
+                   length=row.nn_dist, feederID=nn_attr['feederID'], cable_type_stub=cable_type_stub)
+
+    # loads 100 - 200 kW connected to lv station directly
+    lv_loads_to_station = lvgd.buildings.loc[
+        (get_config_osm('lv_threshold_capacity') <= lvgd.buildings.capacity) &
+        (lvgd.buildings.capacity < get_config_osm('mv_lv_threshold_capacity'))]
+
+    for building_node, row in lv_loads_to_station.iterrows():
+        feederID += 1
+        # todo: update capacity with load for residentials
+        attr = {'x': row.geometry.x,
+                'y': row.geometry.y,
+                'node_type': 'non_synthetic',
+                'ding0_node_type': "load",
+                'capacity': row.capacity,
+                'residential_capacity': row.residential_capacity,
+                'number_households': row.number_households,
+                'cts_capacity': row.cts_capacity,
+                'industrial_capacity': row.industrial_capacity,
+                'feederID': feederID,
+                }
+        G.add_node(building_node, **attr)
+        # add new edge to street graph
+        stub_line_shp = LineString([row.geometry, row.nn_coords])
+        # add edge to full graph for shortest path calculation
+        full_graph.add_edge(building_node, row.nn, geometry=stub_line_shp, length=stub_line_shp.length)
+        # route singular cable
+        line_shp, line_length, line_path = get_shortest_path_shp_single_target(full_graph, building_node, station_id,
+                                                                               return_path=True, nodes_as_str=False)
+        cable_type_stub = get_cable_type_by_load(lvgd, row.capacity, cable_lf, cos_phi_load, v_nom)
+        G.add_edge(building_node, station_id, geometry=line_shp,
+                   length=line_length, feederID=feederID, cable_type_stub=cable_type_stub)
+
+    lvgd.graph_district = G  # update graph for district.
+
+    def transform_graph_to_ding0_graph(G, grid):
+        # New approach of transforming graph to ding0 objects graph by mltja
+        # Add nodes
+        cable_distributor_no = 0
+        helper_no = 0
+        load_no = 0
+        relabel_nodes_dict = {}
+
+        for node in G.nodes(data=True):
+            node_name = node[0]
+            node_data = node[1]
+            node_coordinates = Point(node_data['x'], node_data['y'])
+
+            if node_data["ding0_node_type"] == "cable_distributor":
+                cable_distributor_no += 1
+                cable_distributor = LVCableDistributorDing0(
+                    grid=grid,
+                    id_db=cable_distributor_no,
+                    geo_data=node_coordinates,
+                    in_building=False,
+                    osm_id=node_name
+                )
+                grid.add_cable_dist(cable_distributor)
+                relabel_nodes_dict[node_name] = cable_distributor
+            elif node_data["ding0_node_type"] == "load":
+                load_no += 1  # load_number in every component
+                helper_no += 1
+                # Helper components are used for better pypsa export
+                # Add helper cable distributor
+                cable_distributor = LVCableDistributorDing0(
+                        grid=grid,
+                        id_db=f"building_{node_name}",
+                        geo_data=node_coordinates,
+                        helper_component=True,
+                    )
+                # Add helper cable
+                branch = BranchDing0(
+                    grid=grid,
+                    id_db=f"helper_{helper_no}",
+                    helper_component=True,
+                )
+                load = LVLoadDing0(
+                    grid=grid,
+                    id_db=load_no,
+                    load_no=load_no,
+                    peak_load=node_data["capacity"],
+                    peak_load_residential=node_data["residential_capacity"],
+                    number_households=node_data["number_households"],
+                    peak_load_cts=node_data["cts_capacity"],
+                    peak_load_industrial=node_data["industrial_capacity"],
+                    geo_data=node_coordinates,
+                    building_id=node_name,
+                    type="conventional_load"
+                )
+                grid.add_cable_dist(cable_distributor)
+                grid.graph.add_edge(
+                    cable_distributor,
+                    load,
+                    branch=branch
+                )
+                grid.add_load(load)
+                relabel_nodes_dict[node_name] = cable_distributor
+
+            elif node_data["ding0_node_type"] == "station":
+                # Station node is already in the graph
+                relabel_nodes_dict[node_name] = grid._station
+            else:
+                raise ValueError(f"No ding0_node_type or the false: {node_data['ding0_node_type']}")
+
+        G = nx.relabel_nodes(G, relabel_nodes_dict)
+
+        # Add edges
+        branch_no = 0
+        for edge in G.edges(data=True):
+            branch_no += 1
+
+            edge_data = edge[2]
+
+            branch = BranchDing0(
+                grid=grid,
+                id_db=branch_no,
+                length=edge_data["length"],
+                kind='cable',
+                type=edge_data["cable_type_stub"],
+                geometry=edge_data["geometry"],
+                feeder=edge_data['feederID'],
+            )
+            grid.graph.add_edge(edge[0], edge[1], branch=branch)
+
+    transform_graph_to_ding0_graph(G, lvgd.lv_grid)
+    # Todo: Add function to remove unnecessary cable distributors
diff --git a/ding0/grid/lv_grid/clustering.py b/ding0/grid/lv_grid/clustering.py
new file mode 100644
index 00000000..41940b49
--- /dev/null
+++ b/ding0/grid/lv_grid/clustering.py
@@ -0,0 +1,122 @@
+from sklearn.cluster import AgglomerativeClustering
+import numpy as np
+import networkx as nx
+
+from ding0.config.config_lv_grids_osm import get_config_osm
+from ding0.grid.lv_grid.routing import get_mvlv_subst_loc_list, get_cluster_graph_and_nodes
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+def get_cluster_numbers(la_peak_loads, simp_graph):
+    """
+    caculate the number of clusters for load areal based on peak load / avg
+    peak loads < 200 kW are accumulated
+    add additional_trafo_capacity to ensure a trafos is loaded to 80% max.
+    trafo_loading = 500 / 630
+    """
+    cum_peak_load = la_peak_loads.loc[la_peak_loads.capacity < get_config_osm('mv_lv_threshold_capacity')].capacity.sum()
+
+    n_cluster = int(np.ceil(cum_peak_load / get_config_osm('avg_trafo_size')) * get_config_osm('additional_trafo_capacity'))
+    # workaround if peak load of low voltage loads is zero,
+    # TODO: check if load area should be removed -> cfg_ding0.get('mv_routing', 'load_area_threshold')
+    if n_cluster == 0:
+        n_cluster = 1
+
+    elif n_cluster > len(simp_graph):
+        n_cluster = len(simp_graph)
+
+    return n_cluster
+
+
+def apply_AgglomerativeClustering(G, k, round_decimals=True):
+    
+    """
+    for graph: G apply_AgglomerativeClustering for k cluster
+    round_decimals: True makes it reproducible due to coordinates
+    may be rounded in different ways, depending on ram/ hardware.
+    return labels
+    """
+
+    if len(G.nodes) > 1:
+
+        X = []    # collect nodes
+        for node in G.nodes:
+            X.append((G.nodes[node]['x'],G.nodes[node]['y']))
+        X = np.array(X)
+
+        adj_mat_sparse = nx.adjacency_matrix(G)
+
+        # ensure number of clusters <= number of buildings 
+        if k > len(X):
+            k=len(X)
+
+        if round_decimals:
+
+            X = np.round_(X, decimals=4, out=None)
+
+        clustering = AgglomerativeClustering(n_clusters=k, linkage='ward', connectivity=adj_mat_sparse).fit(X)
+
+        return clustering.labels_
+
+
+    # graph has only one node
+    else: return np.array([0])
+
+
+def distance_restricted_clustering(simp_graph, n_cluster, street_loads_df, mv_grid_district, id_db):
+    """
+    Apply ward hierarchical AgglomerativeClustering with connectivity constraints for underlying graph
+    https://scikit-learn.org/stable/modules/clustering.html#hierarchical-clustering
+    Linkage criteria: ward. Connectivity constraints: adjacent matrix from graph
+    Locate stations in fct: get_mvlv_subst_loc_list()
+    return True if clustering_successfully else False
+    """
+    clustering_successfully = False  # init False
+    cluster_increment_counter = 0  # init 0
+    check_distance_criterion = True
+    cluster_increment_counter_threshold = get_config_osm('cluster_increment_counter_threshold')
+
+    for i in range(len(simp_graph.nodes)):
+
+        # increment n_cluster. n_cluster += 1
+        labels = apply_AgglomerativeClustering(simp_graph, n_cluster)
+
+        cluster_graph, nodes_w_labels = get_cluster_graph_and_nodes(simp_graph, labels)
+
+        if cluster_increment_counter > cluster_increment_counter_threshold:
+            check_distance_criterion = False
+
+            message = f"cluster_increment_counter > {cluster_increment_counter_threshold} -> " \
+                      f"{get_config_osm('ons_dist_threshold')}m distance is not ensured. " \
+                      f"Check export: MV {mv_grid_district}, LA {id_db} does not ensure " \
+                      f"max dist of {get_config_osm('ons_dist_threshold')}m between station and loads."
+
+            mv_grid_district.network.message.append(message)
+            logger.warning(message)
+
+            clustering_successfully = True
+
+        # locate stations for districts
+        mvlv_subst_list, valid_cluster_distance = get_mvlv_subst_loc_list(cluster_graph, nodes_w_labels, \
+                                                                          street_loads_df, labels, n_cluster, \
+                                                                          check_distance_criterion)
+
+        if valid_cluster_distance:
+
+            logger.debug('All clusters are in range.')
+
+            clustering_successfully = True
+            break
+
+        else:
+
+            if cluster_increment_counter <= cluster_increment_counter_threshold:
+
+                cluster_increment_counter += 1
+                n_cluster += 1
+                logger.debug(f'At least one node trespasses dist to substation, for n_clusters = {n_cluster}. '
+                             f'Cluster again with n_clusters+=1')
+
+    return clustering_successfully, cluster_graph, mvlv_subst_list, nodes_w_labels
diff --git a/ding0/grid/lv_grid/db_conn_load_osm_data.py b/ding0/grid/lv_grid/db_conn_load_osm_data.py
new file mode 100644
index 00000000..f37d74b8
--- /dev/null
+++ b/ding0/grid/lv_grid/db_conn_load_osm_data.py
@@ -0,0 +1,73 @@
+"""
+Load Buildings_with_Amenities, Building_wo_Amenity, Amenities_ni_Buildings, Way
+From local DB
+"""
+
+
+from ding0.config.classes_db_conn.osm_load_classes import Buildings_with_Amenities, Building_wo_Amenity, Amenities_ni_Buildings, Way
+
+from ding0.config.config_lv_grids_osm import get_config_osm
+
+from sqlalchemy import func
+
+import pandas as pd
+
+
+
+def get_osm_ways(geo_area, session_osm):
+    
+    """ load ways from db for given polygon as geo_area_wkt """
+    
+    return session_osm.query(Way).filter(func.st_intersects(func.ST_GeomFromText(geo_area, get_config_osm('srid')), Way.geometry)) 
+
+
+    
+
+
+def get_osm_buildings_w_a(geo_area, session_osm):
+    
+    """ load buildings_with_amenities from db for given polygon as geo_area_wkt """
+    
+    buildings_w_a = session_osm.query(Buildings_with_Amenities).filter(func.st_intersects(
+        func.ST_GeomFromText(geo_area, get_config_osm('srid')), Buildings_with_Amenities.geo_center))
+    
+    buildings_w_a_sql_df = pd.read_sql(
+        buildings_w_a.statement,
+        con=session_osm.bind 
+    )
+    
+    return buildings_w_a_sql_df
+
+
+
+def get_osm_buildings_wo_a(geo_area, session_osm):
+    
+    """ load buildings_without_amenities from db for given polygon as geo_area_wkt """
+    
+    buildings_wo_a = session_osm.query(Building_wo_Amenity).filter(func.st_intersects(
+        func.ST_GeomFromText(geo_area, get_config_osm('srid')), Building_wo_Amenity.geo_center))
+    
+    buildings_wo_a_sql_df = pd.read_sql(
+        buildings_wo_a.statement,
+        con=session_osm.bind 
+    )
+    
+    return buildings_wo_a_sql_df
+
+
+
+def get_osm_amenities_ni_Buildings(geo_area, session_osm):
+    
+    """ load amenities_notin_Buildings from db for given polygon as geo_area_wkt """
+    
+    amenities_ni_Buildings = session_osm.query(Amenities_ni_Buildings).filter(func.st_intersects(
+        func.ST_GeomFromText(geo_area, get_config_osm('srid')), Amenities_ni_Buildings.geometry)) 
+    
+    amenities_ni_Buildings_sql_df = pd.read_sql(
+        amenities_ni_Buildings.statement,
+        con=session_osm.bind 
+        # both ways are working. select the easier/ more appropriate one
+        #con=engine_osm
+    )
+    
+    return amenities_ni_Buildings_sql_df
diff --git a/ding0/grid/lv_grid/geo.py b/ding0/grid/lv_grid/geo.py
new file mode 100644
index 00000000..a22cb735
--- /dev/null
+++ b/ding0/grid/lv_grid/geo.py
@@ -0,0 +1,133 @@
+from shapely.geometry import MultiPoint, Point, shape, box, GeometryCollection
+
+import pyproj
+import pandas as pd
+
+import numpy as np
+from scipy.spatial.distance import pdist, squareform
+from ding0.core.network.loads import MVLoadDing0
+from ding0.core.network.stations import LVStationDing0
+
+
+
+def convertCoords(coordx, coordy, from_proj, to_proj):
+    """pyproj.transform from_proj, to_proj"""
+
+    from_proj = pyproj.Proj(init='epsg:'+str(from_proj))
+    to_proj   = pyproj.Proj(init='epsg:'+str(to_proj))
+
+    x2,y2 = pyproj.transform(from_proj, to_proj, coordx, coordy)
+    return pd.Series([x2, y2])
+
+
+def get_Point_from_xy(x, y):
+    """ 
+    get point for given x,y
+    """
+    return Point(x,y)
+
+
+def get_points_in_load_area(geos_list):
+    
+    """ 
+    get all points in a load area
+    based on buildings, e.g.:
+        buildings_with_amenities
+        buildings_without_amenities
+        amenities_not_in_buildings
+    """
+    point_lists = []
+    
+    for geometry in geos_list:
+
+        if geometry.geom_type == 'Polygon':
+            point_lists += [Point(point) for point in geometry.exterior.coords[:-1]]
+
+        elif geometry.geom_type == 'Point':
+
+            point_lists.append(geometry)
+
+        else:
+            raise IOError('Shape is not a polygon neither a point.')
+
+    return point_lists
+
+
+def get_bounding_box_from_points(points):
+    """
+    create bounding_box based on points.
+    """
+    mpt = MultiPoint([shape(point) for point in points])
+    return box(*mpt.bounds, ccw=True)
+
+
+def get_convex_hull_from_points(points):
+    """ return convex hull for given points"""
+    
+    mpt = MultiPoint([shape(point) for point in points])
+    return mpt.convex_hull
+
+
+def get_load_center_node(lv_load_area):
+    """
+    get station which is load center to set its
+    geo_data as load center of load areal.
+    """
+
+    '''if len(lv_load_area._lv_grid_districts) <= 2:
+
+        logger.warning(lv_load_area._lv_grid_districts)
+        station = lv_load_area._lv_grid_districts[0].lv_grid._station
+
+    else:'''
+
+    peak_loads = []
+    coordinates = []
+
+    lv_load_area_supply_nodes = list(lvgd.lv_grid.station() for lvgd in lv_load_area.lv_grid_districts()) \
+                                + list(lv_load_area._mv_loads)
+
+    for node in lv_load_area_supply_nodes:
+        peak_loads.append(node.peak_load)
+        coordinates.append(node.geo_data)
+
+    coords = [[p.x, p.y] for p in coordinates]
+    coords_array = np.array(coords)
+    dist_array = pdist(coords_array)
+    dist_matrix = squareform(dist_array)
+    unweighted_nodes = dist_matrix.dot(peak_loads)
+    load_center_ix = int(np.where(unweighted_nodes == np.amin(unweighted_nodes))[0][0])
+
+    centre = lv_load_area_supply_nodes[load_center_ix]
+
+    # check if load are centre intersects with load area polygon
+    # if not extend border such that all supply nodes are inside load area
+    if lv_load_area.geo_area.intersects(centre.geo_data):
+        load_area_geo = lv_load_area.geo_area
+    else:
+        points = get_points_in_load_area(coordinates)
+        polygon_extended = get_convex_hull_from_points(points)
+        load_area_geo = lv_load_area.geo_area.union(polygon_extended)
+        if isinstance(load_area_geo, GeometryCollection):
+            load_area_geo = load_area_geo.convex_hull
+
+    if isinstance(centre, LVStationDing0):  # TODO make consistent
+        centre_osm = centre.osm_id_node
+    elif isinstance(centre, MVLoadDing0):
+        centre_osm = centre.osmid_building
+
+    return centre_osm, centre.geo_data, load_area_geo
+
+
+def get_load_center_coords(buildings_w_loads_df):
+
+    x = buildings_w_loads_df["geometry"].apply(lambda point: point.x)
+    y = buildings_w_loads_df["geometry"].apply(lambda point: point.y)
+    peak_loads = buildings_w_loads_df.capacity.tolist()
+
+    coords_array = np.array(list(zip(x, y)))
+    peak_loads_array = np.array(peak_loads)
+
+    centre_coords = list(np.average(coords_array, axis=0, weights=peak_loads_array))
+
+    return centre_coords
diff --git a/ding0/grid/lv_grid/graph_processing.py b/ding0/grid/lv_grid/graph_processing.py
new file mode 100644
index 00000000..2174dad8
--- /dev/null
+++ b/ding0/grid/lv_grid/graph_processing.py
@@ -0,0 +1,724 @@
+"""
+Graph processing.
+"""
+from geoalchemy2.shape import to_shape
+
+from ding0.config.config_lv_grids_osm import get_config_osm
+import logging
+logger = logging.getLogger(__name__)
+
+import pandas as pd
+from itertools import combinations
+
+import networkx as nx
+import osmnx as ox
+
+import geopandas as gpd
+from shapely.geometry import LineString, Point, MultiLineString, Polygon
+from shapely.ops import linemerge
+
+from scipy.spatial.distance import cdist
+import numpy as np
+
+# src: https://stackoverflow.com/questions/28246425/python-convert-a-list-of-nested-tuples-into-a-dict
+flatten = lambda *n: (e for a in n for e in (flatten(*a) if isinstance(a, (tuple, list)) else (a,)))
+
+
+def update_ways_geo_to_shape(ways_sql_df):
+    """
+    update_ways_geo_to_shape
+    after loading from DB
+    """
+
+    ways_sql_df['geometry'] = ways_sql_df.apply(lambda way: to_shape(way.geometry).coords, axis=1)
+
+    return ways_sql_df
+
+
+def build_graph_from_ways(ways_df):
+
+    """ 
+    Build graph based on osm ways
+    Based on this graph, the routing will be implemented.
+    """
+    # init a graph and set srid.
+    graph = nx.MultiGraph()
+    graph.graph["crs"] = 'epsg:' + str(get_config_osm('srid'))
+    graph.graph["source"] = 'osm'
+
+    for index, osm_id, nodes, geometry, highway, length_segments in ways_df.itertuples():
+        # add edges
+        ix = 0
+        for node in nodes[:-1]:
+            graph.add_edge(nodes[ix],
+                           nodes[ix + 1],
+                           length=length_segments[ix],
+                           osmid=osm_id,
+                           highway=highway)
+            ix += 1
+
+        # add coords
+        ix = 0
+        coords_list = list(geometry)
+        for node in nodes:
+            graph.nodes[node]['x'] = coords_list[ix][0]
+            graph.nodes[node]['y'] = coords_list[ix][1]
+            graph.nodes[node]['node_type'] = 'non_synthetic'
+
+            ix += 1
+
+    # convert undirected graph to digraph due to truncate_graph_polygon
+    graph = graph.to_directed()
+
+    return graph
+
+
+def create_simple_synthetic_graph(geo_load_area):
+    """
+    Build synthetic graph with one node containing
+    coordinates of geo_load_area centroid
+    """
+    # init a graph and set srid and source parameter.
+    graph = nx.MultiDiGraph()
+    graph.graph["crs"] = 'epsg:' + str(get_config_osm('srid'))
+    graph.graph["source"] = 'synthetic'
+
+    # add id and coordinates of area centroid
+    node_id = 1
+    la_geo_center = geo_load_area.centroid
+    node_data = {'x': la_geo_center.x, 'y': la_geo_center.y, 'node_type': 'synthetic'}
+    graph.add_node(node_id, **node_data)
+
+    return graph, node_id
+
+
+def create_buffer_polygons(polygon):
+
+    """
+    Create buffer polygons based on origin load area geometry.
+    Exterior load area geometry is used.
+    """
+    # load buffer_distance from config
+    buffer_list = get_config_osm('buffer_distance')
+
+    buffer_poly_list = [polygon.convex_hull.buffer(buffer_dist) for buffer_dist in buffer_list] 
+    buffer_poly_list.insert(0, Polygon(polygon.exterior).buffer(buffer_list[0]))
+    # PAUL NEW # due to fast within operator, import poly needs to be buffered
+    
+    return buffer_poly_list
+
+
+def graph_nodes_outside_buffer_polys(graph, ways_sql_df, buffer_poly_list):
+    
+    """
+    src: https://www.matecdev.com/posts/point-in-polygon.html
+    For each buffer polygon identify graph nodes outside buffer polygon
+    Return nested node list of nodes to remove.
+    """
+
+    nodes = flatten(ways_sql_df.nodes.tolist())
+    points = flatten(ways_sql_df.geometry.tolist())
+
+    nodes_list = list(nodes)
+    points_list = [list(p) for p in list(points)]
+    points_list = [Point(item) for sublist in points_list for item in sublist]
+
+    nodes = pd.DataFrame(points_list, index=nodes_list, columns=['geometry'])
+    nodes_gdf = gpd.GeoDataFrame(nodes, geometry='geometry', crs=graph.graph["crs"])
+
+    polys_df = pd.DataFrame(buffer_poly_list, index=[i for i in range(len(buffer_poly_list))], columns=['geometry'])
+    polys_gdf = gpd.GeoDataFrame(polys_df, geometry='geometry', crs=graph.graph["crs"])
+
+    PointsInPolys = gpd.tools.sjoin(nodes_gdf, polys_gdf, op="within", how='left')
+    
+    nodes_to_remove = [list(set(nodes_list) - set(PointsInPolys.loc[PointsInPolys['index_right'] == poly].index.tolist())) for poly in range(len(buffer_poly_list))]
+
+    return nodes_to_remove
+
+
+def truncate_graph_nodes(graph, nested_node_list, poly_idx):
+    """
+    Remove nodes from buffer graphs based on nested node list
+    and index of buffer polygon
+    """
+    G = graph.copy()
+    G.remove_nodes_from(nested_node_list[poly_idx])
+    return G
+
+
+def compose_graph(outer_graph, graph_subdiv):
+
+    """
+    composed_graph has all nodes & edges of both graphs, including attributes
+    Where the attributes conflict, it uses the attributes of graph_subdiv_directed.
+    compose conn_graph with graph_subdiv_directed to have subdivides edges of
+    graph_subdiv_directed in conn_graph
+    """
+    composed_graph = nx.compose(outer_graph, graph_subdiv)
+
+    if not nx.is_weakly_connected(composed_graph):
+        logger.warning('Composed_graph not connected.')
+        composed_graph = ox.utils_graph.get_largest_component(composed_graph)
+
+    return composed_graph
+
+
+def nodes_connected_component(G, inner_node_list):
+    """
+    Return largest connected component of origin graph based on passed node list.
+    """
+    nodes = [(cc, len(set(cc)&set(inner_node_list))) for cc in nx.weakly_connected_components(G)]
+    largest_cc = max(nodes,key=lambda item:item[1])[0]
+    G = nx.MultiDiGraph(G.subgraph(largest_cc))
+    return G
+
+def connect_graph_components(graph, synthetic_edges, method='all_ccs'):
+
+    # get all connected components in a nested list and sort for reproducible results
+    conn_comps = [list(c) for c in nx.weakly_connected_components(graph)]
+    conn_comps = sorted(conn_comps, key=lambda x: x[0])
+
+    # get largest connected component
+    largest_cc = max(conn_comps, key=len)
+
+    # if just major (larger) connected components should be connected
+    if method == 'major_ccs':
+        conn_comps = [list(cc) for cc in conn_comps \
+                      if len(cc) / len(largest_cc) > get_config_osm('major_ccs_ratio_threshold')]
+
+    # delete largest connect component from components list
+    conn_comps.remove(largest_cc)
+
+    while len(conn_comps):
+
+        # flatten components_list to get unconnected node-ids
+        nodes_unconn = list(flatten(conn_comps))
+
+        # get coordinates of largest and unconnected component(s)
+        coords_largest_cc = [(graph.nodes[node]['x'], graph.nodes[node]['y']) for node in largest_cc]
+        coords_unconn_ccs = [(graph.nodes[node]['x'], graph.nodes[node]['y']) for node in nodes_unconn]
+
+        # find nearest unconnected component
+        distance_array = cdist(coords_largest_cc, coords_unconn_ccs)
+        index = np.where(distance_array == distance_array.min())
+
+        # get node ids to connect synthetic edge
+        conn_node_largest_cc = largest_cc[int(index[0])]
+        conn_node_unconn_ccs = nodes_unconn[int(index[1])]
+
+        # connect both components using a synthetic edge
+        graph.add_edge(
+            conn_node_largest_cc,
+            conn_node_unconn_ccs,
+            highway="synthetic",
+            length=distance_array.min(),
+            osmid=0  # random.randint(100000000, 200000000)
+        )
+        graph.add_edge(
+            conn_node_unconn_ccs,
+            conn_node_largest_cc,
+            highway="synthetic",
+            length=distance_array.min(),
+            osmid=0  # random.randint(100000000, 200000000)
+        )
+
+        # save synthetic edges as set
+        synthetic_edges.add((conn_node_largest_cc, conn_node_unconn_ccs))
+        synthetic_edges.add((conn_node_unconn_ccs, conn_node_largest_cc))
+
+        # get nodes of synthetically connected component
+        synth_conn_cc = \
+            [cc for cc in conn_comps if conn_node_unconn_ccs in cc][0]
+
+        # update lists containing components nodes
+        largest_cc = largest_cc + synth_conn_cc
+        conn_comps.remove(synth_conn_cc)
+
+    return graph, synthetic_edges
+
+def get_fully_conn_graph(G, nlist): #nested_node_list
+    """
+    Return largest connected component of buffered graph that contains all nodes from nodes to connect
+    Iterating over all buffered polygon graphs
+    """
+
+    poly_idx = 0
+    max_it = len(nlist) - 1
+    synthetic_edges = set()
+    G_min = truncate_graph_nodes(G, nlist, poly_idx)
+
+    if nx.number_weakly_connected_components(G_min) > 1:
+
+        G, synthetic_edges = connect_graph_components(G, synthetic_edges, method='major_ccs')
+        G_c_max = nodes_connected_component(G, G_min.nodes())
+        nodes_to_connect = set(G_c_max.nodes) & set(G_min.nodes)
+        G_c = nodes_connected_component(G_min, G_min.nodes())
+
+        while not all(node in G_c.nodes for node in nodes_to_connect):
+
+            poly_idx += 1
+            G_buff = truncate_graph_nodes(G, nlist, poly_idx)
+            G_c = nodes_connected_component(G_buff, nodes_to_connect) 
+
+            unconn_nodes = list(set(nodes_to_connect)-set(G_c.nodes))
+
+            if len(unconn_nodes) == 0:
+
+                logger.debug(f'Finding connected graph, iteration {poly_idx} of max. {max_it}.')
+                break
+
+            if poly_idx >= max_it:
+
+                G_c, synthetic_edges = connect_graph_components(G, synthetic_edges, method='all_ccs')
+                logger.debug(f'Created connected graph using synthetic edges, max. iterations {max_it} trespassed.')
+                break
+
+        else:
+            logger.debug(f'Found connected graph, iteration {poly_idx} of max. {max_it}.')
+            return G_c, synthetic_edges
+
+    else:
+
+        logger.debug(f'Graph already fully connected.')
+        G_c = G_min
+
+    return G_c, synthetic_edges
+
+def split_conn_graph(conn_graph, inner_node_list, synthetic_edges):
+    """
+    Divide graoh in inner and outer components.
+    based on plygon borders.
+    """
+    
+    inner_graph = conn_graph.subgraph(inner_node_list).copy()
+    inner_graph.remove_edges_from(synthetic_edges)
+    inner_edges = inner_graph.edges()
+
+    outer_graph = conn_graph
+    outer_graph.remove_edges_from(inner_edges)
+    outer_graph.remove_nodes_from(list(nx.isolates(outer_graph)))
+    
+    return(inner_graph, outer_graph)
+
+
+def get_outer_conn_graph(G, inner_node_list):
+    """
+    Identify components of outter graph suitable for connecting
+    inner graph components.
+    """
+
+    common_nodes = set(G.nodes()) & set(inner_node_list)
+    G = simplify_graph_adv(G, list(common_nodes))
+    G = remove_unloaded_deadends(G, list(common_nodes))
+    G.remove_nodes_from(list(nx.isolates(G)))
+
+    return G
+
+
+def add_edge_geometry_entry(G):
+    """
+    todo: check if to remove if not then write doc.
+    looks like deprecated?
+    """
+    edges_wo_geometry = [(u,v,k,d) for u,v,k,d in G.edges(keys=True, data=True) if 'geometry' not in d]
+    for u,v,k,d in edges_wo_geometry:
+        G.edges[u,v,k]['geometry'] = LineString([Point((G.nodes[node]["x"], G.nodes[node]["y"])) for node in [u,v]])
+        
+    return G
+
+
+#### modified osmnx functions
+
+# modified osmnx function (extended by nodes_to_keep)
+# feststellen welche knoten behalten und welche weg. build paths.
+def _get_paths_to_simplify(G, nodes_to_keep, strict=True):
+    """
+    # modified osmnx function (extended by nodes_to_keep)
+    street_load_nodes are treated as endpoints and are to keep.
+
+    Generate all the paths to be simplified between endpoint nodes.
+    The path is ordered from the first endpoint, through the interstitial nodes,
+    to the second endpoint.
+    Parameters
+    ----------
+    G : networkx.MultiDiGraph
+        input graph
+    strict : bool
+        if False, allow nodes to be end points even if they fail all other rules
+        but have edges with different OSM IDs
+    Yields
+    ------
+    path_to_simplify : list
+    """    
+    
+    # for each endpoint node, look at each of its successor nodes
+    for endpoint in nodes_to_keep:
+        for successor in G.successors(endpoint):
+            if successor not in nodes_to_keep:
+                # if endpoint node's successor is not an endpoint, build path
+                # from the endpoint node, through the successor, and on to the
+                # next endpoint node
+                yield ox.simplification._build_path(G, endpoint, successor, nodes_to_keep)
+
+
+def simplify_graph_adv(G, street_load_nodes, strict=True, remove_rings=True):
+    """
+    # modified osmnx function (extended by street_load_nodes=nodes_to_keep)
+    street_load_nodes are treated as endpoints and are to keep.
+    fct is adapted and graph can be simplified several times.
+
+    Simplify a graph's topology by removing interstitial nodes.
+    Simplifies graph topology by removing all nodes that are not intersections
+    or dead-ends. Create an edge directly between the end points that
+    encapsulate them, but retain the geometry of the original edges, saved as
+    a new `geometry` attribute on the new edge. Note that only simplified
+    edges receive a `geometry` attribute. Some of the resulting consolidated
+    edges may comprise multiple OSM ways, and if so, their multiple attribute
+    values are stored as a list.
+    Parameters
+    ----------
+    G : networkx.MultiDiGraph
+        input graph
+    strict : bool
+        if False, allow nodes to be end points even if they fail all other
+        rules but have incident edges with different OSM IDs. Lets you keep
+        nodes at elbow two-way intersections, but sometimes individual blocks
+        have multiple OSM IDs within them too.
+    remove_rings : bool
+        if True, remove isolated self-contained rings that have no endpoints
+    Returns
+    -------
+    G : networkx.MultiDiGraph
+        topologically simplified graph, with a new `geometry` attribute on
+        each simplified edge
+    """
+    ### PAUL
+    #if "simplified" in G.graph and G.graph["simplified"]:  # pragma: no cover
+        #raise Exception("This graph has already been simplified, cannot simplify it again.")
+
+    #utils.log("Begin topologically simplifying the graph...")
+    logger.debug("Begin topologically simplifying the graph...")
+
+    # define edge segment attributes to sum upon edge simplification
+    attrs_to_sum = {"length", "travel_time"}
+
+    # make a copy to not mutate original graph object caller passed in
+    G = G.copy()
+    initial_node_count = len(G)
+    initial_edge_count = len(G.edges)
+    all_nodes_to_remove = []
+    all_edges_to_add = []
+    
+    
+    endpoints = [n for n in G.nodes if ox.simplification._is_endpoint(G, n, strict=True)]
+    nodes_to_keep = list(set(endpoints + street_load_nodes))
+    
+    if len(endpoints) < 1:
+        return G # TODO: check how to simplify without endpoints
+
+    # generate each path that needs to be simplified
+    for path in _get_paths_to_simplify(G, nodes_to_keep, strict=strict):
+        
+        if "simplified" in G.graph and G.graph["simplified"]: ### PAUL
+            
+            edge_path = [(path[i], path[i+1], next(iter(G.get_edge_data(path[i],path[i+1]).keys()))) for i in range(len(path)-1)]
+            linestring = linemerge(MultiLineString([G.edges[edge]['geometry'] for edge in edge_path]))
+
+        # add the interstitial edges we're removing to a list so we can retain
+        # their spatial geometry
+        path_attributes = dict()
+        for u, v in zip(path[:-1], path[1:]):
+
+            # there should rarely be multiple edges between inter_get_paths_to_simplify(G, nodes_to_keep, strict=True):stitial nodes
+            # usually happens if OSM has duplicate ways digitized for just one
+            # street... we will keep only one of the edges (see below)
+            edge_count = G.number_of_edges(u, v)
+            if edge_count != 1:
+                #utils.log(f"Found {edge_count} edges between {u} and {v} when simplifying")
+                logger.info(f"Found {edge_count} edges between {u} and {v} when simplifying")
+
+            # get edge between these nodes: if multiple edges exist between
+            # them (see above), we retain only one in the simplified graph
+            
+            edge_data = G.edges[u, v, next(iter(G.get_edge_data(u,v).keys()))] ### PAUL
+            #edge_data = G.edges[u, v, 0]
+            edge_data.pop('geometry', None) ### NECESSARY FOR THIS PARTIC. CASE (PAUL: if geometry in attributes)
+            
+            for attr in edge_data:
+                if attr in path_attributes:
+                    # if this key already exists in the dict, append it to the
+                    # value list
+                    path_attributes[attr].append(edge_data[attr])
+                else:
+                    # if this key doesn't already exist, set the value to a list
+                    # containing the one value
+                    path_attributes[attr] = [edge_data[attr]]
+
+        # consolidate the path's edge segments' attribute values
+        for attr in path_attributes:
+            path_attributes[attr] = list(flatten(path_attributes[attr])) ### PAUL
+            if attr in attrs_to_sum:
+                # if this attribute must be summed, sum it now
+                path_attributes[attr] = sum(path_attributes[attr])
+            elif len(set(path_attributes[attr])) == 1:
+                # if there's only 1 unique value in this attribute list,
+                # consolidate it to the single value (the zero-th):
+                path_attributes[attr] = path_attributes[attr][0]
+            else:
+                # otherwise, if there are multiple values, keep one of each
+                path_attributes[attr] = list(set(path_attributes[attr]))
+        
+
+        if "simplified" in G.graph and G.graph["simplified"]:
+            path_attributes["geometry"] = linestring
+
+        else:
+            # construct the new consolidated edge's geometry for this path
+            path_attributes["geometry"] = LineString(
+                [Point((G.nodes[node]["x"], G.nodes[node]["y"])) for node in path]
+            )
+
+        # add the nodes and edge to their lists for processing at the end
+        all_nodes_to_remove.extend(path[1:-1])
+        all_edges_to_add.append(
+            {"origin": path[0], "destination": path[-1], "attr_dict": path_attributes}
+        )
+
+    # for each edge to add in the list we assembled, create a new edge between
+    # the origin and destination
+    for edge in all_edges_to_add:
+        G.add_edge(edge["origin"], edge["destination"], **edge["attr_dict"])
+
+    # finally remove all the interstitial nodes between the new edges
+    G.remove_nodes_from(set(all_nodes_to_remove))
+
+    if remove_rings:
+        # remove any connected components that form a self-contained ring
+        # without any endpoints
+        wccs = nx.weakly_connected_components(G)
+        nodes_in_rings = set()
+        for wcc in wccs:
+            if not any(ox.simplification._is_endpoint(G, n) for n in wcc):
+                nodes_in_rings.update(wcc)
+        G.remove_nodes_from(nodes_in_rings)
+
+    # mark graph as having been simplified
+    G.graph["simplified"] = True
+
+    msg = (
+        f"Simplified graph: {initial_node_count} to {len(G)} nodes, "
+        f"{initial_edge_count} to {len(G.edges)} edges"
+    )
+    logger.debug(msg)
+    return G
+
+def flatten_graph_components_to_lines(G, inner_node_list):
+    """
+    Build single edges based on outter graph component to connect
+    isolated components/ nodes in inner graph/ not buffered area.
+    """
+    # TODO: add edge tags 'highway' and 'osmid' to shortest path edge
+
+    components = list(nx.weakly_connected_components(G))
+    sp_path = lambda p1, p2: nx.shortest_path(G, p1, p2, weight='length') if nx.has_path(G, p1, p2) else None
+
+    nodes_to_remove = []
+    edges_to_add = []
+    common_nodes = set(G.nodes()) & set(inner_node_list)
+
+    for comp in components:
+        conn_nodes = list(comp & common_nodes)
+        if len(comp) > 2 and len(conn_nodes) == 1:
+            G.remove_nodes_from(comp) # removes unwanted islands / loops
+
+        else: 
+            endpoints = combinations(conn_nodes, 2)
+            paths = [sp_path(n[0], n[1]) for n in endpoints]
+            for path in paths:
+                geoms = []
+                for u, v in zip(path[:-1], path[1:]):
+                    geom = G.edges[u,v,0]['geometry']
+                    # deprecated due to add_edge_geometry_entry(G)
+                    # try: geom = G.edges[u,v,0]['geometry']
+                    # except: geom = LineString([Point((G.nodes[node]["x"], G.nodes[node]["y"])) for node in [u,v]])
+                    geoms.append(geom)
+
+                merged_line = linemerge(MultiLineString(geoms))
+                edges_to_add.append([path[0], path[-1], merged_line])
+                nodes_to_remove.append(list(set(comp) - set(conn_nodes)))
+
+    for nodes in nodes_to_remove:
+        G.remove_nodes_from(nodes)
+
+    for edge in edges_to_add:
+        G.add_edge(edge[0], edge[1], 0, geometry=edge[2], length=edge[2].length)
+        G.add_edge(edge[1], edge[0], 0, geometry=edge[2], length=edge[2].length)
+
+    return G
+
+def remove_detours(G):
+    """
+    Identify edges with length > 1500m and
+    Remove em if path dist > 3 * euclid distance
+    """
+    edges = [(u,v,k,d['length'], LineString(d['geometry'].boundary).length) for u,v,k,d in G.edges(keys=True, data=True) if d['length'] >= get_config_osm('ons_dist_threshold')]
+    for u,v,k,path_dist,euc_dist in edges:
+        if path_dist > 3 * euc_dist:
+            G.remove_edge(u,v,k)
+    return G
+
+
+def remove_unloaded_deadends(G, nodes_of_interest):
+    """
+    remove_unloaded_deadends
+    nodes_of_interest are mandatory nodes to keep
+    """
+    while True:
+        dead_end_nodes = [node for node,degree in dict(G.degree()).items() if degree == 2]
+        dead_end_nodes_to_remove = list(set(dead_end_nodes) - set(nodes_of_interest))
+        if len(dead_end_nodes_to_remove) > 0:
+            G.remove_nodes_from(dead_end_nodes_to_remove)
+            #logger.warning(len(G.nodes))
+        else:
+            break 
+    return G
+
+def remove_parallels(G):
+    """
+    remove the parallel edge with greater "weight" attribute value
+    deprecated ? check remove_parallels_and_loops.
+    src: https://github.com/gboeing/osmnx/blob/main/osmnx/utils_graph.py#L341
+    """
+    # from https://github.com/gboeing/osmnx/blob/main/osmnx/utils_graph.py#L341
+    #G = G.copy()
+    to_remove = []
+
+    # identify all the parallel edges in the MultiDiGraph
+    parallels = ((u, v) for u, v, k in G.edges(keys=True) if k > 0)
+
+    # remove the parallel edge with greater "weight" attribute value
+    for u, v in set(parallels):
+        k, _ = max(G.get_edge_data(u, v).items(), key=lambda x: x[1]['length'])
+        to_remove.append((u, v, k))
+
+    G.remove_edges_from(to_remove)
+    return G
+
+def remove_parallels_and_loops(G):
+    """
+    remove the parallel edge with greater "weight" attribute value
+    remove selfloop_edges
+    src: https://github.com/gboeing/osmnx/blob/main/osmnx/utils_graph.py#L341
+    """
+    # from https://github.com/gboeing/osmnx/blob/main/osmnx/utils_graph.py#L341
+    #G = G.copy()
+    to_remove = []
+
+    # identify all the parallel edges in the MultiDiGraph
+    parallels = ((u, v) for u, v, k in G.edges(keys=True) if k > 0)
+
+    # remove the parallel edge with greater "weight" attribute value
+    for u, v in set(parallels):
+        k, _ = max(G.get_edge_data(u, v).items(), key=lambda x: x[1]['length'])
+        to_remove.append((u, v, k))
+
+    G.remove_edges_from(to_remove)
+    G.remove_edges_from(nx.selfloop_edges(G))
+    
+    return G
+
+
+def remove_unloaded_loops(G, nodes_of_interest):
+    """
+    remove_unloaded_loops and remove corresponding node to loops
+    nodes_of_interest are mandatory nodes to keep
+    """
+    #G = G.copy()
+    simple_node_loops = list(set([edge[0] for edge in nx.selfloop_edges(G) if len(G[edge[0]]) <= 2])) # set due to digraph
+    node_loops_to_remove = list(set(simple_node_loops) - set(nodes_of_interest))
+    G.remove_edges_from(nx.selfloop_edges(G))
+    G.remove_nodes_from(node_loops_to_remove)
+    return G
+
+
+def subdivide_graph_edges(inner_graph): #(inner_graph, inner_node_list):
+
+    """
+    subdivide_graph_edges by defined distance value get_config_osm('dist_edge_segments')
+    and build new graph considering subdivided edges with additional synthetic nodes.
+    TODO: keep information about edge name
+          ensure edge name does not exist when adding
+    """ 
+
+    graph_subdiv = inner_graph.copy()
+    edges = inner_graph.edges()
+    edge_data = []
+    node_data = []
+    unique_syn_nodes = []
+    origin_vertices = []
+
+    for u,v in edges:
+
+        origin_vertices.append((v,u))
+
+        if (u,v) in origin_vertices:
+            pass
+        else:
+
+            linestring = LineString([(inner_graph.nodes[u]['x'],inner_graph.nodes[u]['y']), 
+                                     (inner_graph.nodes[v]['x'],inner_graph.nodes[v]['y'])])
+            vertices_gen = ox.utils_geo.interpolate_points(linestring, get_config_osm('dist_edge_segments')) #### config
+            vertices = list(vertices_gen) 
+            highway = inner_graph.edges[u,v,0]['highway']
+            osmid = inner_graph.edges[u,v,0]['osmid']
+            # fromid = graph.edges[u,v,0]['from'] ### PAUL
+            # toid = graph.edges[u,v,0]['to'] ### PAUL
+            edge_id = u + v
+            vertex_node_id = []
+
+            for num,node in enumerate(list(vertices)[1:-1], start=1):
+                x,y = node[0],node[1] ###
+
+                # ensure synthetic node id does not exist in graph.
+                # increment name by 1 if exist and check again
+                skip_num_by=0
+                while True:
+
+                    # first name edge_id + 0 + 1(=num) + 0(=skip_num_by)
+                    concat_0 = 0 # todo: check concat_0 is to change. check if node name needs to be str
+                    name = int(str(edge_id) + str(concat_0) + str(num+skip_num_by))
+                    if name in unique_syn_nodes: #graph_subdiv.nodes():
+
+                        skip_num_by +=1
+                        name = int(str(edge_id) + str(concat_0) + str(num+skip_num_by)) ####
+
+                    else:
+
+                        break
+
+                node_data.append([name,x,y,(u,v)])
+                vertex_node_id.append(name)
+                unique_syn_nodes.append(name)
+
+            if vertices[0] == (inner_graph.nodes[v]['x'],inner_graph.nodes[v]['y']): ###
+                vertex_node_id.insert(0, v)
+                vertex_node_id.append(u)
+            else:
+                vertex_node_id.insert(0, u)
+                vertex_node_id.append(v)
+
+            for i,j in zip(range(len(list(vertices))-1), range(len(vertex_node_id)-1)):
+                line = LineString([vertices[i], vertices[i+1]])
+                edge_data.append([vertex_node_id[j], vertex_node_id[j+1], line, line.length, highway, osmid])
+
+    #build new graph 
+    graph_subdiv.remove_edges_from(edges)
+
+    for u,v,line,leng,highway,osmid in edge_data:
+        graph_subdiv.add_edge(u,v,geometry=line,length=leng,highway=highway,osmid=osmid)
+        graph_subdiv.add_edge(v,u,geometry=line,length=leng,highway=highway,osmid=osmid)
+
+    for name,x,y,pos in node_data:
+        graph_subdiv.add_node(name,x=x,y=y,node_type='synthetic')
+
+    return graph_subdiv #graph_subdiv, edges
diff --git a/ding0/grid/lv_grid/lv_connect.py b/ding0/grid/lv_grid/lv_connect.py
index 351ac83f..fea742bf 100644
--- a/ding0/grid/lv_grid/lv_connect.py
+++ b/ding0/grid/lv_grid/lv_connect.py
@@ -12,19 +12,25 @@
 __url__        = "https://github.com/openego/ding0/blob/master/LICENSE"
 __author__     = "nesnoj, gplssm"
 
+import pandas as pd
 
 from ding0.core.network import BranchDing0
+from ding0.core.network.cable_distributors import LVCableDistributorDing0
+from ding0.core.network.loads import LVLoadDing0
 
 from ding0.tools import config as cfg_ding0
-from ding0.tools.geo import calc_geo_dist
+from ding0.tools.geo import calc_geo_dist, calc_edge_geometry
 from ding0.grid.tools import cable_type
 import logging
 import random
 
-logger = logging.getLogger('ding0')
+from scipy.spatial.distance import cdist
+import numpy as np
 
+logger = logging.getLogger(__name__)
 
-def lv_connect_generators(lv_grid_district, graph, debug=False):
+
+def lv_connect_generators(mv_grid, graph, debug=False):
 
     """ Connect LV generators to LV grid
     
@@ -51,101 +57,92 @@ def lv_connect_generators(lv_grid_district, graph, debug=False):
     seed = int(cfg_ding0.get('random', 'seed'))
     random.seed(a=seed)
 
-    # generate random list (without replacement => unique elements)
-    # of loads (residential) to connect genos (P <= 30kW) to.
-    lv_loads_res = sorted(lv_grid_district.lv_grid.loads_sector(sector='res'),
-                          key=lambda _: repr(_))
-    if len(lv_loads_res) > 0:
-        lv_loads_res_rnd = (random.sample(lv_loads_res,
-                                             len(lv_loads_res)))
-    else:
-        lv_loads_res_rnd = None
-
-    # generate random list (without replacement => unique elements)
-    # of loads (retail, industrial, agricultural) to connect genos
-    # (30kW <= P <= 100kW) to.
-    lv_loads_ria = sorted(lv_grid_district.lv_grid.loads_sector(sector='ria'),
-                          key=lambda _: repr(_))
-    if len(lv_loads_ria) > 0:
-        lv_loads_ria_rnd = (random.sample(lv_loads_ria,
-                                             len(lv_loads_ria)))
-    else:
-        lv_loads_ria_rnd = None
-
-    for generator in sorted(lv_grid_district.lv_grid.generators(), key=lambda x: repr(x)):
-
-        # generator is of v_level 6 -> connect to LV station
+    # Get positions of all LV nodes
+    lv_nodes_df = pd.DataFrame(columns=["node", "type", "building_id", "x", "y"])
+    for mv_grid_district in mv_grid.network.mv_grid_districts():
+        for load_area in mv_grid_district.lv_load_areas():
+            for lv_grid_district in load_area.lv_grid_districts():
+                for node in lv_grid_district.lv_grid.graph.nodes():
+                    lv_nodes_df.loc[str(node)] = [
+                        node,
+                        type(node).__name__,
+                        node.building_id if isinstance(node, LVLoadDing0) else None,
+                        node.geo_data.x,
+                        node.geo_data.y
+                    ]
+    lv_stations_df = lv_nodes_df[lv_nodes_df["type"] == "LVStationDing0"]
+    lv_loads_df = lv_nodes_df[lv_nodes_df["type"] == "LVLoadDing0"]
+
+    def add_generator_to_station(generator, station):
+        grid = station.grid
+        graph = grid.graph
+
+        branch_shp, branch_length = calc_edge_geometry(generator, station)
+        # Calculate length with detour factor
+        branch_length = calc_geo_dist(generator, station)
+        branch_type = cable_type(
+            generator.capacity / (cable_lf * cos_phi_gen),
+            v_nom,
+            lv_grid_district.lv_grid.network.static_data['LV_cables'])
+
+        branch = BranchDing0(length=branch_length,
+                             kind='cable',
+                             grid=grid,
+                             type=branch_type,
+                             geometry=branch_shp)
+
+        generator.lv_grid = grid
+        generator.lv_load_area = grid.grid_district.lv_load_area
+
+        graph.add_edge(generator, station, branch=branch)
+        grid.add_generator(generator)
+
+    def add_generator_to_node(generator, node_to_connect):
+        grid = node_to_connect.grid
+        graph = grid.graph
+
+        # Add helper cable
+        branch = BranchDing0(
+            grid=grid,
+            id_db=f"helper_{generator}",
+            helper_component=True,
+        )
+
+        if isinstance(node_to_connect, LVLoadDing0):
+            cable_distributor = list(graph.neighbors(node_to_connect))[0]
+        elif isinstance(node_to_connect, LVCableDistributorDing0):
+            cable_distributor = node_to_connect
+        else:
+            raise ValueError("False node as connection target!")
+
+        generator.lv_grid = grid
+        generator.lv_load_area = grid.grid_district.lv_load_area
+        generator.geo_data = cable_distributor.geo_data
+
+        grid.add_generator(generator)
+        graph.add_edge(cable_distributor, generator, branch=branch)
+
+    def get_nearest_nodes_of_generator(nearest_node, generator):
+        distance = cdist(
+            nearest_node.loc[:, ["x", "y"]],
+            [(generator.geo_data.x, generator.geo_data.y)],
+            'euclidean'
+        )
+        nearest_node = nearest_node.iloc[np.where(distance == distance.min())[0][0]]["node"]
+        return nearest_node
+
+    for generator in sorted(mv_grid.lv_generators_to_connect, key=lambda x: repr(x)):
         if generator.v_level == 6:
-            lv_station = lv_grid_district.lv_grid.station()
-
-            branch_length = calc_geo_dist(generator, lv_station)
-            branch_type = cable_type(
-                generator.capacity / (cable_lf * cos_phi_gen),
-                v_nom,
-                lv_grid_district.lv_grid.network.static_data['LV_cables'])
-
-            branch = BranchDing0(length=branch_length,
-                                 kind='cable',
-                                 grid = lv_grid_district.lv_grid,
-                                 type=branch_type)
-
-            graph.add_edge(generator, lv_station, branch=branch)
-
-        # generator is of v_level 7 -> assign geno to load
+            nearest_station = get_nearest_nodes_of_generator(lv_stations_df, generator)
+            add_generator_to_station(generator, nearest_station)
         elif generator.v_level == 7:
-
-            # connect genos with P <= 30kW to residential loads, if available
-            if (generator.capacity <= 30) and (lv_loads_res_rnd is not None):
-                if len(lv_loads_res_rnd) > 0:
-                    lv_load = lv_loads_res_rnd.pop()
-                # if random load list is empty, create new one
-                else:
-                    lv_loads_res_rnd = (random.sample(lv_loads_res,
-                                                     len(lv_loads_res))
-                                       )
-                    lv_load = lv_loads_res_rnd.pop()
-
-                # get cable distributor of building
-                lv_conn_target = list(graph.neighbors(lv_load))[0]
-
-            # connect genos with 30kW <= P <= 100kW to residential loads
-            # to retail, industrial, agricultural loads, if available
-            elif (generator.capacity > 30) and (lv_loads_ria_rnd is not None):
-                if len(lv_loads_ria_rnd) > 0:
-                    lv_load = lv_loads_ria_rnd.pop()
-                # if random load list is empty, create new one
-                else:
-                    lv_loads_ria_rnd = (random.sample(lv_loads_ria,
-                                                         len(lv_loads_ria))
-                                           )
-                    lv_load = lv_loads_ria_rnd.pop()
-
-                # get cable distributor of building
-                lv_conn_target = list(graph.neighbors(lv_load))[0]
-
-            # fallback: connect to station
+            if (
+                    generator.building_id
+                    and not lv_loads_df[lv_loads_df["building_id"] == generator.building_id].empty
+            ):
+                node_to_connect = lv_loads_df[lv_loads_df["building_id"] == generator.building_id]["node"].values[0]
             else:
-                lv_conn_target = lv_grid_district.lv_grid.station()
-
-                logger.warning(
-                    'No valid conn. target found for {}.'
-                    'Connected to {}.'.format(
-                        repr(generator),
-                        repr(lv_conn_target)
-                    ))
-
-            # determine appropriate type of cable
-            branch_type = cable_type(
-                generator.capacity / (cable_lf * cos_phi_gen),
-                v_nom,
-                lv_grid_district.lv_grid.network.static_data['LV_cables'])
-
-            # connect to cable dist. of building
-            branch = BranchDing0(length=1,
-                                 kind='cable',
-                                 grid=lv_grid_district.lv_grid,
-                                 type=branch_type)
-
-            graph.add_edge(generator, lv_conn_target, branch=branch)
-
-    return graph
+                node_to_connect = get_nearest_nodes_of_generator(lv_loads_df, generator)
+            add_generator_to_node(generator, node_to_connect)
+        else:
+            raise ValueError("False Voltage Level!")
diff --git a/ding0/grid/lv_grid/parameterization.py b/ding0/grid/lv_grid/parameterization.py
new file mode 100644
index 00000000..bf8030ea
--- /dev/null
+++ b/ding0/grid/lv_grid/parameterization.py
@@ -0,0 +1,432 @@
+import pandas as pd
+
+from geoalchemy2.shape import to_shape
+
+from ding0.config.config_lv_grids_osm import get_peak_loads, get_load_profile_categories, get_config_osm
+
+def get_peak_load_diversity(buildings):
+    """ get get_peak_load_diversity for each given area """
+    diversity_factor = get_config_osm('diversity_factor_not_residential')
+
+    total_number_of_households = buildings["number_households"].sum()
+    peak_load_res_sum = buildings["residential_capacity"].sum()
+    if total_number_of_households > 0 and peak_load_res_sum > 0:
+        peak_load_res = get_peak_load_for_residential(total_number_of_households) * total_number_of_households
+    elif peak_load_res_sum == 0:
+        peak_load_res = 0
+    else:
+        raise ValueError("Total number of households is 0, "
+                         "but residential load is not 0!")
+
+    peak_load_not_res = (buildings[["cts_capacity", "industrial_capacity"]]
+                         .sum().sum() * diversity_factor)
+
+    return peak_load_res + peak_load_not_res 
+
+def get_peak_load(category, load_profile_categories, load_profiles, area, n_amenities_inside, number_households):
+    
+    """ 
+    Get peak load by given category of load profiles. 
+    Multiply by 1e-3 to transform to kW.
+    """
+    
+    if category in load_profile_categories['residentials_list']:
+    
+        # capacity for residentials will be updated after clustering
+        # due to number of residentials per feeder are mandatory
+        return load_profiles[load_profile_categories[category]] * 100 * number_households * 1e-3
+    
+    if category not in load_profile_categories:
+        
+        print(category, 'is not matched in config and will be categorized as leftover')
+        category = 'leftover'
+        
+    return load_profiles[load_profile_categories[category]] * area / n_amenities_inside * 1e-3
+
+
+def get_peak_load_for_residential(number_residentials, formula='vdew'):
+    '''
+    SELECT FORMULA TO CALCULATE DIVERSITY FOR RESIDENTIALS
+    FORMULA STETZ
+    calculate peak load in MW for residential depending on its number at feeder
+    SOURCE: Dissertation
+    AUTHOR: Thomas Stetz
+    TITEL: Autonomous Voltage Control Strategies in Distribution Grids with Photovoltaic Systems
+    - Technical and Economic Assessment - Stetz
+    Page: 14 
+    Formula: (2.4)
+    $\sqrt[k]{n}$ = k root n or check mardown
+    pl = a - ( b / $\sqrt[k]{n}$)
+    e.g. calculation        tranformation kW to MW before return
+    number residentials     peak load [kW]
+                     10     2.5291793904118034
+    Threshold after  35     1.7180038451156543
+    Threshold exceed 36     1.706869146916332
+                     70     1.500011219348335
+                    110     1.406355083981595
+
+    FORMULA VDEW
+    calculate peak load in MW for residential depending on its number at feeder
+    SOURCE: Book
+    AUTHOR: VDEW 
+    TITEL: Planung und Betrieb städtische Niederspannungsgebiete
+    Page: 14 
+    Assumption: fully electrified household with
+    connected power rating = 30kW
+    diversity factor against infinity = 0.06 (see page 13)
+    vdew formula leads to higher peak load results per household
+
+
+    return peak_load_per_residential
+    '''
+
+    if formula == 'stetz':
+        a = 1.16221
+        b = - 7.14717
+        k = 1.39203
+        pl = a - b / number_residentials**(1/k)
+
+
+    elif formula == 'vdew':
+        p_c = 30
+        g_inf = 0.06
+        g_n = g_inf + ((1-g_inf) / (number_residentials**(3/4)))
+        pl = p_c * g_n
+
+    else:
+        return 'No known formula'
+
+    return pl
+
+
+def parameterize_by_load_profiles(amenities_ni_Buildings_sql_df, buildings_w_a_sql_df, buildings_wo_a_sql_df): 
+    
+    """Parameterize:
+        buildings_w_a:  buildings_with_amenities
+        buildings_wo_a: buildings_without_amenities
+        amenities_ni_Buildings: amenities_not_in:buildings
+        
+       return:
+         df_buildings_w_loads
+         
+         
+        To distinguish between amenities and buildings, 
+        due to they have different geometry (Point/ Polygon)
+        amenities will init with p.x to be able to filter 
+        where p.x == 'p.x' to set x,y in right way
+    """
+    
+    # prepare parameterization info
+    load_profile_categories = get_load_profile_categories()
+    load_profiles = get_peak_loads()
+        
+    # set avg square meter from config
+    avg_mxm = get_config_osm('avg_square_meters')
+    
+    
+    # prepare pd.read_sql to concat as one df
+    # preprocess amenities_ni_Buildings_sql_df for df
+    if len(amenities_ni_Buildings_sql_df) > 0:
+        amenities_ni_Buildings_sql_df.index = amenities_ni_Buildings_sql_df['osm_id']
+        del amenities_ni_Buildings_sql_df['osm_id']
+        amenities_ni_Buildings_sql_df['number_households'] = 1
+        amenities_ni_Buildings_sql_df = amenities_ni_Buildings_sql_df.rename(
+            {'amenity': 'category'}, axis=1)
+        amenities_ni_Buildings_sql_df['area'] = avg_mxm
+        amenities_ni_Buildings_sql_df['n_amenities_inside'] = 1
+        amenities_ni_Buildings_sql_df['capacity'] = amenities_ni_Buildings_sql_df.apply(
+            lambda row: get_peak_load(row.category, load_profile_categories, 
+                                      load_profiles, row.area, row.n_amenities_inside,
+                                      row.number_households), axis=1)
+        amenities_ni_Buildings_sql_df['x'], amenities_ni_Buildings_sql_df['y'] = 'p.x', 'p.y'
+        amenities_ni_Buildings_sql_df['raccordement_building'] = amenities_ni_Buildings_sql_df['geometry']
+        concat_a = True
+    else: concat_a=False
+    
+    # preprocess buildings_wo_a_sql_df for df
+    if len(buildings_wo_a_sql_df) > 0:
+        buildings_wo_a_sql_df['n_amenities_inside'] = 1
+        buildings_wo_a_sql_df.index = buildings_wo_a_sql_df['osm_id']
+        del buildings_wo_a_sql_df['osm_id']
+        concat_b1 = True
+    else: concat_b1=False
+    
+    # preprocess buildings_w_a_sql_df for df
+    if len(buildings_w_a_sql_df) > 0:
+        buildings_w_a_sql_df.index = buildings_w_a_sql_df['osm_id_amenity']
+        buildings_w_a_sql_df = buildings_w_a_sql_df.rename({'geometry_building': 'geometry'}, axis=1)
+        del buildings_w_a_sql_df['osm_id_amenity']
+        concat_b2 = True
+    else: concat_b2=False
+    
+    # concat buildings_wo_a_sql_df and buildings_w_a_sql_df
+    if concat_b1 & concat_b2:
+        buildings_w_a = pd.concat([buildings_wo_a_sql_df, buildings_w_a_sql_df]) 
+        buildings_w_a_existing = True
+    elif concat_b1:
+        buildings_w_a = buildings_wo_a_sql_df.copy()
+        buildings_w_a_existing = True
+    elif concat_b2:
+        buildings_w_a = buildings_w_a_sql_df.copy()
+        buildings_w_a_existing = True
+    else: buildings_w_a_existing=False
+    
+    
+    existing_loads=True
+    if buildings_w_a_existing:
+        # preprocess concatted buildings_wo_a_sql_df and buildings_w_a_sql_df
+        buildings_w_a['n_apartments'].replace(0,1, inplace=True)
+        buildings_w_a['x'] = buildings_w_a['geo_center']
+        buildings_w_a['y'] = 'p.y'
+        buildings_w_a = buildings_w_a.rename({'building': 'category', 'n_apartments': 'number_households', 
+                                              'geo_center': 'raccordement_building'}, axis=1)
+        
+        buildings_w_a['capacity'] = buildings_w_a.apply(lambda row: 
+                                                        get_peak_load(row.category, 
+                                                                      load_profile_categories, 
+                                                                      load_profiles,
+                                                                      row.area,
+                                                                      row.n_amenities_inside,
+                                                                      row.number_households),
+                                                        axis=1)
+        
+        
+        if concat_a:
+            buildings_w_loads_df = pd.concat([buildings_w_a, amenities_ni_Buildings_sql_df])
+        else:
+            buildings_w_loads_df = buildings_w_a.copy()
+    
+    elif concat_a:
+        buildings_w_loads_df = amenities_ni_Buildings_sql_df.copy()
+    else:
+        existing_loads = False
+    
+    if existing_loads:
+        
+        # check for duplicates
+        ids = buildings_w_loads_df.index
+        dupl_ids = list(set(buildings_w_loads_df[ids.isin(ids[ids.duplicated()])].index.tolist()))
+
+        if len(dupl_ids) > 0: # drop dupl if exists e.g. one amenity is located in multiple buildings.
+
+            dupl_df = buildings_w_loads_df.loc[buildings_w_loads_df.index.isin(dupl_ids)]
+
+            for ix, dupl_id in enumerate(dupl_ids):
+
+                if ix < 1:
+
+                    # todo dff rename but not dupl_df = dupl_df[] du to reameining with 1 entry
+                    dupl_dff = dupl_df[(dupl_df.index==dupl_id) & (dupl_df.area!=dupl_df.loc[dupl_df.index==dupl_id].area.max())]
+
+                else:
+
+                    tempf_df = dupl_df[(dupl_df.index==dupl_id) & (dupl_df.area!=dupl_df.loc[dupl_df.index==dupl_id].area.max())]
+                    dupl_dff = pd.concat([dupl_dff, tempf_df])
+                    
+            
+            buildings_w_loads_df = buildings_w_loads_df[~buildings_w_loads_df.index.isin(dupl_ids)]
+
+            buildings_w_loads_df = pd.concat([buildings_w_loads_df, dupl_dff])
+
+        
+        # update to_shape(geometry), to_shape(raccordement_building)
+        buildings_w_loads_df['geometry'] = buildings_w_loads_df.apply(
+            lambda amenity: to_shape(amenity.geometry), axis=1)
+        buildings_w_loads_df['raccordement_building'] = buildings_w_loads_df.apply(
+            lambda building: to_shape(building.raccordement_building), axis=1)
+        # to_shape only working if not nan
+        if 'geometry_amenity' in buildings_w_loads_df.columns:
+            buildings_w_loads_df.loc[buildings_w_loads_df.geometry_amenity.notnull(), 'geometry_amenity'] = \
+            buildings_w_loads_df.loc[buildings_w_loads_df.geometry_amenity.notnull()].apply(
+                lambda building: to_shape(building.geometry_amenity), axis=1)
+
+
+        # for amenities update x and y
+        buildings_w_loads_df.loc[buildings_w_loads_df.x == 'p.x', 'y'] = \
+        buildings_w_loads_df.loc[buildings_w_loads_df.x == 'p.x'].apply(
+            lambda amenity: amenity.geometry.y, axis=1)
+        buildings_w_loads_df.loc[buildings_w_loads_df.x == 'p.x', 'x'] = \
+        buildings_w_loads_df.loc[buildings_w_loads_df.x == 'p.x'].apply(
+            lambda amenity: amenity.geometry.x, axis=1)
+
+        # for amenities update x and y
+        buildings_w_loads_df.loc[buildings_w_loads_df.y == 'p.y', 'x'] = \
+        buildings_w_loads_df.loc[buildings_w_loads_df.y == 'p.y'].apply(
+            lambda amenity: amenity.raccordement_building.x, axis=1)
+        buildings_w_loads_df.loc[buildings_w_loads_df.y == 'p.y', 'y'] = \
+        buildings_w_loads_df.loc[buildings_w_loads_df.y == 'p.y'].apply(
+            lambda amenity: amenity.raccordement_building.y, axis=1)
+        
+        # replace yes to residentials
+        buildings_w_loads_df['category'].replace('yes','residential', inplace=True)
+    
+        return buildings_w_loads_df
+    
+    else:
+        return None
+        
+        
+def parameterize_by_load_profiles_IT_DEPRECATED(buildings_w_a, buildings_wo_a, amenities_ni_Buildings): 
+    
+    """Parameterize:
+        buildings_w_a:  buildings_with_amenities
+        buildings_wo_a: buildings_without_amenities
+        amenities_ni_Buildings: amenities_not_in:buildings
+        
+       return:
+         df_buildings_w_loads
+         
+         
+        To distinguish between amenities and buildings, due to they have different geometry (Point/ Polygon)
+        amenities will init with p.x to be able to filter for where p.x == 'p.x' to set x,y in right way
+    """
+    
+    # prepare parameterization info
+    load_profile_categories = get_load_profile_categories()
+    load_profiles = get_peak_loads()
+    
+    # set avg square meter from config
+    avg_mxm = get_config_osm('avg_square_meters')
+    
+    # TODO: CHECK IF TEMPORARY df needed    
+    # assign nearest nodes more efficient thru dataframe instead calling iterative for each building
+    # temporary: init empty df for buildings w. loads
+    # will be replaced by oop when ding0 contains building-data
+    # sqlalchemy does not allow: for building in buildings: building.capacity = new_Value 
+
+    df_buildings_w_loads = pd.DataFrame(columns=['osm_id', 'category', 'capacity', 'area', 'number_households', 
+                                                 'x', 'y', 'geometry', 'raccordement_building'])
+    df_buildings_w_loads.index = df_buildings_w_loads['osm_id']
+    del df_buildings_w_loads['osm_id']
+    
+    
+
+    # check capacity
+    for amenity in amenities_ni_Buildings:  
+
+        peak_load = get_peak_load(amenity.amenity, load_profile_categories, 
+                                  load_profiles, 100, 1) #  100 as avg square meter. 1 as n_amenities_inside
+
+        # if type does not exist in load profiles, set capacity=0 
+        if peak_load is None:
+
+            peak_load = 0
+
+
+        df_buildings_w_loads.loc[amenity.osm_id] = [amenity.amenity, peak_load, avg_mxm, 0, 
+                                                        'p.x', 'p.y', amenity.geometry, amenity.geometry]
+
+
+
+
+    for building in buildings_w_a:
+        
+        number_households = building.n_apartments 
+        peak_load = get_peak_load(building.building, load_profile_categories, load_profiles, building.area, building.n_amenities_inside)
+
+        # if type does not exist in load profiles, set capacity=0 
+        if peak_load is None:
+
+            peak_load = 0
+
+
+        # check if building is residential and add 1 household if no other number of households is known.
+        # set peak load 0. will be updated when number of all households per feeder are known.
+        if building.building in load_profile_categories['residentials_list']:
+
+            if building.building == 'yes':
+
+                building.building = 'residential'
+
+            if number_households == 0:
+
+                number_households += 1
+
+            df_buildings_w_loads.loc[building.osm_id_amenity] = [building.building, peak_load, avg_mxm, 
+                                                                 number_households, building.geo_center, 'p.y', 
+                                                                 building.geometry_building, building.geo_center]
+
+
+
+        # parameterize all categories but residentials
+        else:
+
+            # peak_load in kW. if building contains multiple amenities, area is shared unifromly.
+            df_buildings_w_loads.loc[building.osm_id_amenity] = [building.building, peak_load, building.area,
+                                                                 number_households, building.geo_center, 'p.y', 
+                                                                 building.geometry_building, building.geo_center]
+
+
+
+
+
+
+    for building in buildings_wo_a:  
+
+        # 1 as n_amenities_inside
+        peak_load = get_peak_load(building.building, load_profile_categories, load_profiles, building.area, 1)
+
+        # if type does not exist in load profiles, set capacity=0 
+        if peak_load is None:
+
+            peak_load = 0
+
+
+        number_households = building.n_apartments 
+
+
+        # check if building is residential and add 1 household if no other number of households is known.
+        # set peak load by load profile. will be updated when number of all households per feeder are known.
+        if building.building in load_profile_categories['residentials_list']:
+
+            if building.building == 'yes':
+
+                building.building = 'residential'
+
+            if number_households == 0:
+
+                number_households += 1
+
+            df_buildings_w_loads.loc[building.osm_id] = [building.building, peak_load, avg_mxm, 
+                                                         number_households, building.geo_center, 'p.y', 
+                                                         building.geometry, building.geo_center]
+
+
+
+        # parameterize all categories but residentials
+        else:
+
+            # peak_load = peak_load_per_square_meter * square_meter * 1e-3.        
+            df_buildings_w_loads.loc[building.osm_id] = [building.building, peak_load, 
+                                                         building.area, number_households, building.geo_center, 
+                                                         'p.y', building.geometry, building.geo_center]
+
+    
+
+    # update to_shape(geometry), to_shape(raccordement_building)
+    df_buildings_w_loads['geometry'] = df_buildings_w_loads.apply(
+        lambda amenity: to_shape(amenity.geometry), axis=1)
+    df_buildings_w_loads['raccordement_building'] = df_buildings_w_loads.apply(
+        lambda building: to_shape(building.raccordement_building), axis=1)
+    df_buildings_w_loads['geometry_amenity'] = df_buildings_w_loads.apply(
+        lambda building: to_shape(building.geometry_amenity), axis=1)
+
+
+    # for amenities update x and y
+    df_buildings_w_loads.loc[df_buildings_w_loads.x == 'p.x', 'y'] = \
+    df_buildings_w_loads.loc[df_buildings_w_loads.x == 'p.x'].apply(
+        lambda amenity: amenity.geometry.y, axis=1)
+    df_buildings_w_loads.loc[df_buildings_w_loads.x == 'p.x', 'x'] = \
+    df_buildings_w_loads.loc[df_buildings_w_loads.x == 'p.x'].apply(
+        lambda amenity: amenity.geometry.x, axis=1)
+
+    # for amenities update x and y
+    df_buildings_w_loads.loc[df_buildings_w_loads.y == 'p.y', 'x'] = \
+    df_buildings_w_loads.loc[df_buildings_w_loads.y == 'p.y'].apply(
+        lambda amenity: amenity.raccordement_building.x, axis=1)
+    df_buildings_w_loads.loc[df_buildings_w_loads.y == 'p.y', 'y'] = \
+    df_buildings_w_loads.loc[df_buildings_w_loads.y == 'p.y'].apply(
+        lambda amenity: amenity.raccordement_building.y, axis=1)
+
+    
+    return df_buildings_w_loads
diff --git a/ding0/grid/lv_grid/routing.py b/ding0/grid/lv_grid/routing.py
new file mode 100644
index 00000000..41619b65
--- /dev/null
+++ b/ding0/grid/lv_grid/routing.py
@@ -0,0 +1,364 @@
+"""
+Routing based on ways from OSM.
+TODO: Separate routing.py to graph_processing.py
+"""
+
+import osmnx as ox
+import networkx as nx
+
+#from pyproj import CRS
+
+import pandas as pd
+#import geopandas as gpd
+
+import numpy as np
+
+#from itertools import combinations
+
+from shapely.geometry import LineString, Point #, MultiLineString, Polygon # PAUL NEW
+#from shapely.ops import linemerge
+#from shapely.wkt import dumps as wkt_dumps
+
+from ding0.config.config_lv_grids_osm import get_config_osm
+
+#from ding0.grid.lv_grid.db_conn_load_osm_data import get_osm_ways
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+
+# scipy is optional dependency for projected nearest-neighbor search
+try:
+    from scipy.spatial import cKDTree
+except ImportError:  # pragma: no cover
+    cKDTree = None
+    
+
+# scikit-learn is optional dependency for unprojected nearest-neighbor search
+try:
+    from sklearn.neighbors import BallTree
+except ImportError:  # pragma: no cover
+    BallTree = None
+
+
+## https://stackoverflow.com/questions/28246425/python-convert-a-list-of-nested-tuples-into-a-dict
+flatten = lambda *n: (e for a in n for e in (flatten(*a) if isinstance(a, (tuple, list)) else (a,)))
+
+
+
+
+def assign_nearest_nodes_to_buildings(graph_subdiv, buildings_w_loads_df):
+    
+    """
+    assign nearest nodes of graph to buildings by euclidean distance.
+    """
+
+    X = buildings_w_loads_df["geometry"].apply(lambda point: point.x)
+    Y = buildings_w_loads_df["geometry"].apply(lambda point: point.y)
+
+    buildings_w_loads_df['nn'], buildings_w_loads_df['nn_dist'] = ox.nearest_nodes(
+        graph_subdiv, X, Y, return_dist=True)
+    buildings_w_loads_df['nn_coords'] = buildings_w_loads_df['nn'].apply(
+        lambda row : Point(graph_subdiv.nodes[row]['x'], graph_subdiv.nodes[row]['y']))
+
+    return buildings_w_loads_df
+
+
+def identify_street_loads(buildings_w_loads_df, graph, get_number_households=False):
+    """
+    identify street_loads
+    street_loads are grouped for lv level only.
+    capacity of loads of mv level are not included.
+    """
+
+    mv_lv_level_threshold = get_config_osm('mv_lv_threshold_capacity')
+
+    # keep only street_load_nodes and endpoints
+    loads = buildings_w_loads_df.copy()
+
+    loads.loc[loads.capacity > mv_lv_level_threshold, 'capacity'] = 0
+    street_loads = loads.groupby(['nn']).capacity.sum().reset_index().set_index('nn')
+
+    if get_number_households:
+        household_loads = loads.groupby(['nn']).number_households.sum().reset_index().set_index('nn')
+        return street_loads, household_loads
+
+    else:
+
+        return street_loads, pd.DataFrame(columns=["number_households"])
+
+
+
+def get_cluster_graph_and_nodes(simp_graph, labels):
+    """
+    assign cluster Id to graph nodes
+    """
+
+    # assign cluster Id to graph nodes
+    node_cluster_dict = dict(zip(list(simp_graph.nodes), labels))
+    nx.set_node_attributes(simp_graph, node_cluster_dict, name='cluster')   
+    nodes_w_labels = ox.graph_to_gdfs(simp_graph, nodes=True, edges=False)
+
+    return simp_graph, nodes_w_labels
+
+
+def loads_in_ons_dist_threshold(dm_cluster, cluster_nodes, osmid):
+    """
+    check max dist between loads and station
+    return False if loads_ not in_ons_dist_threshold
+    e.g. max dist > 1500m
+    """
+
+    ons_dist_threshold = get_config_osm('ons_dist_threshold')
+    ons_max_branch_dist = dm_cluster[cluster_nodes.index(osmid)].max()
+
+    if ons_max_branch_dist > ons_dist_threshold:
+        return False
+
+    else:
+        return True
+
+
+def get_mvlv_subst_loc_list(cluster_graph, nodes, street_loads_df, labels, n_cluster, check_distance_criterion=True):
+    """
+    identify position of station at street load center
+    get list of location of mvlv substations for load areal
+    n_cluster: number of cluster
+    """
+
+    mvlv_subst_list = []
+    valid_cluster_distance = True
+
+    for i in range(n_cluster):
+
+        df_cluster = nodes[nodes['cluster'] == i]
+        cluster_nodes = list(df_cluster.index)
+
+        # create distance matrix for cluster
+        cluster_subgraph = cluster_graph.subgraph(cluster_nodes)
+        dm_cluster = nx.floyd_warshall_numpy(cluster_subgraph, nodelist=cluster_nodes, weight='length')
+
+        # map cluster_loads with capacity of street_loads
+        cluster_loads = pd.Series(cluster_nodes).map(street_loads_df.capacity).fillna(0).tolist()
+
+        # compute location of substation based on load center
+        load_vector = np.array(cluster_loads) #weighted
+        unweighted_nodes = dm_cluster.dot(load_vector)
+
+        osmid = cluster_nodes[int(np.where(unweighted_nodes == np.amin(unweighted_nodes))[0][0])]
+
+        if check_distance_criterion:
+            if not loads_in_ons_dist_threshold(dm_cluster, cluster_nodes, osmid):
+                # return empty list and False for cluster validity
+                # due to distance threshold to ons is trespassed
+                valid_cluster_distance = False
+                return mvlv_subst_list, valid_cluster_distance
+
+        mvlv_subst_loc = cluster_graph.nodes[osmid]
+        mvlv_subst_loc['osmid'] = osmid
+        mvlv_subst_loc['graph_district'] = cluster_subgraph
+        mvlv_subst_list.append(mvlv_subst_loc)
+
+    return mvlv_subst_list, valid_cluster_distance
+
+
+# mv trafo placement
+def connect_mv_loads_to_graph(cluster_graph, osm_id_building, row):
+    """
+    For mv loads, add edge from building to graph instead
+    keeping trafo at nearest node of building in graph.
+    """
+    # row geometry is the coordinate of the building
+    # set values for mv
+    name = osm_id_building
+    x = row.geometry.x
+    y = row.geometry.y
+
+    # add node to graph
+    cluster_graph.add_node(name,x=x,y=y, node_type='non_synthetic', cluster=None)
+    # add edge to graph
+    line = LineString([row.geometry, row.nn_coords])
+    cluster_graph.add_edge(name, row.nn,
+                           geometry=line,length=line.length,highway='trafo_graph_connect')
+    cluster_graph.add_edge(row.nn, name,
+                           geometry=line,length=line.length,highway='trafo_graph_connect')
+
+
+# logic for filling zeros
+def get_lvgd_id(la_id_db, cluster_id, max_n_digits=10):
+    """
+    param: la_id_db, cluster_id, max_n_digits
+           max_n_digits defines number of digits a la or lvgd should have
+    due to la ids and lvgd ids should have a length of 10 fill zeros
+    new id looks like la_id (what is id_db) + filling zeros + cluster id of lvgd
+    return new id
+    """
+    # getting la_id and its length given by ding0
+    la_id = str(la_id_db)
+
+    # getting cluster id and its length computed in new approach
+    lvgd_id = str(cluster_id)
+    need_to_fill_n_digits = max_n_digits - len(la_id)
+
+    return int(la_id + lvgd_id.zfill(need_to_fill_n_digits))
+
+
+'''
+DEPRECATED DUE TO MV LOADS ARE NOT IN LOAD AREA ANYMORE
+def add_mv_load_station_to_mvlv_subst_list(loads_mv_df, mvlv_subst_list, nodes_w_labels):
+
+    """
+    add_mv_load_station_to_mvlv_subst_list
+    """
+
+    loads_mv_df['x'] = loads_mv_df.apply(lambda x: x.nn_coords.x, axis=1)
+    loads_mv_df['y'] = loads_mv_df.apply(lambda x: x.nn_coords.y, axis=1)
+
+    loads_mv_df['node_type'] = loads_mv_df.index.map(nodes_w_labels.node_type)
+
+    loads_mv_df['osmid'] = loads_mv_df['nn']
+    loads_mv_df['load_level'] = 'mv'
+    loads_mv_df['graph_district'] = None
+
+
+    mvlv_subst_list += loads_mv_df.to_dict(orient='records')
+
+    return mvlv_subst_list
+'''
+
+
+
+'''
+def simplify_graph(G, street_load_nodes, strict=True, remove_rings=True):
+    """
+    Simplify a graph's topology by removing interstitial nodes.
+    Simplifies graph topology by removing all nodes that are not intersections
+    or dead-ends. Create an edge directly between the end points that
+    encapsulate them, but retain the geometry of the original edges, saved as
+    a new `geometry` attribute on the new edge. Note that only simplified
+    edges receive a `geometry` attribute. Some of the resulting consolidated
+    edges may comprise multiple OSM ways, and if so, their multiple attribute
+    values are stored as a list.
+    Parameters
+    ----------
+    G : networkx.MultiDiGraph
+        input graph
+    strict : bool
+        if False, allow nodes to be end points even if they fail all other
+        rules but have incident edges with different OSM IDs. Lets you keep
+        nodes at elbow two-way intersections, but sometimes individual blocks
+        have multiple OSM IDs within them too.
+    remove_rings : bool
+        if True, remove isolated self-contained rings that have no endpoints
+    Returns
+    -------
+    G : networkx.MultiDiGraph
+        topologically simplified graph, with a new `geometry` attribute on
+        each simplified edge
+    """
+    if "simplified" in G.graph and G.graph["simplified"]:  # pragma: no cover
+        raise Exception("This graph has already been simplified, cannot simplify it again.")
+
+    #utils.log("Begin topologically simplifying the graph...")
+    logger.info("Begin topologically simplifying the graph...")
+
+    # define edge segment attributes to sum upon edge simplification
+    attrs_to_sum = {"length", "travel_time"}
+
+    # make a copy to not mutate original graph object caller passed in
+    G = G.copy()
+    initial_node_count = len(G)
+    initial_edge_count = len(G.edges)
+    all_nodes_to_remove = []
+    all_edges_to_add = []
+    
+    
+    endpoints = [n for n in G.nodes if ox.simplification._is_endpoint(G, n, strict=True)]
+    nodes_to_keep = list(set(endpoints + street_load_nodes))
+
+    # generate each path that needs to be simplified
+    for path in _get_paths_to_simplify(G, nodes_to_keep, strict=strict):
+
+        # add the interstitial edges we're removing to a list so we can retain
+        # their spatial geometry
+        path_attributes = dict()
+        for u, v in zip(path[:-1], path[1:]):
+
+            # there should rarely be multiple edges between inter_get_paths_to_simplify(G, nodes_to_keep, strict=True):stitial nodes
+            # usually happens if OSM has duplicate ways digitized for just one
+            # street... we will keep only one of the edges (see below)
+            edge_count = G.number_of_edges(u, v)
+            if edge_count != 1:
+                #utils.log(f"Found {edge_count} edges between {u} and {v} when simplifying")
+                logger.info(f"Found {edge_count} edges between {u} and {v} when simplifying")
+
+            # get edge between these nodes: if multiple edges exist between
+            # them (see above), we retain only one in the simplified graph
+            edge_data = G.edges[u, v, 0]
+            
+            edge_data.pop('geometry', None) ### NECESSARY FOR THIS PARTIC. CASE (PAUL: if geometry in attributes)
+            
+            for attr in edge_data:
+                if attr in path_attributes:
+                    # if this key already exists in the dict, append it to the
+                    # value list
+                    path_attributes[attr].append(edge_data[attr])
+                else:
+                    # if this key doesn't already exist, set the value to a list
+                    # containing the one value
+                    path_attributes[attr] = [edge_data[attr]]
+
+        # consolidate the path's edge segments' attribute values
+        for attr in path_attributes:
+            if attr in attrs_to_sum:
+                # if this attribute must be summed, sum it now
+                path_attributes[attr] = sum(path_attributes[attr])
+            elif len(set(path_attributes[attr])) == 1:
+                # if there's only 1 unique value in this attribute list,
+                # consolidate it to the single value (the zero-th):
+                path_attributes[attr] = path_attributes[attr][0]
+            else:
+                # otherwise, if there are multiple values, keep one of each
+                path_attributes[attr] = list(set(path_attributes[attr]))
+
+        # construct the new consolidated edge's geometry for this path
+        path_attributes["geometry"] = LineString(
+            [Point((G.nodes[node]["x"], G.nodes[node]["y"])) for node in path]
+        )
+
+        # add the nodes and edge to their lists for processing at the end
+        all_nodes_to_remove.extend(path[1:-1])
+        all_edges_to_add.append(
+            {"origin": path[0], "destination": path[-1], "attr_dict": path_attributes}
+        )
+
+    # for each edge to add in the list we assembled, create a new edge between
+    # the origin and destination
+    for edge in all_edges_to_add:
+        G.add_edge(edge["origin"], edge["destination"], **edge["attr_dict"])
+
+    # finally remove all the interstitial nodes between the new edges
+    G.remove_nodes_from(set(all_nodes_to_remove))
+
+    if remove_rings:
+        # remove any connected components that form a self-contained ring
+        # without any endpoints
+        wccs = nx.weakly_connected_components(G)
+        nodes_in_rings = set()
+        for wcc in wccs:
+            if not any(ox.simplification._is_endpoint(G, n) for n in wcc):
+                nodes_in_rings.update(wcc)
+        G.remove_nodes_from(nodes_in_rings)
+
+    # mark graph as having been simplified
+    G.graph["simplified"] = True
+
+    msg = (
+        f"Simplified graph: {initial_node_count} to {len(G)} nodes, "
+        f"{initial_edge_count} to {len(G.edges)} edges"
+    )
+    
+    logger.warning(msg)
+    return G
+'''
diff --git a/ding0/grid/mv_grid/analysis.py b/ding0/grid/mv_grid/analysis.py
new file mode 100644
index 00000000..0f4a8426
--- /dev/null
+++ b/ding0/grid/mv_grid/analysis.py
@@ -0,0 +1,400 @@
+#### RESULTS
+
+import numpy as np
+import networkx as nx
+import pandas as pd
+from sklearn.neighbors import NearestNeighbors
+from scipy.spatial import distance
+
+from ding0.core.structure.regions import LVLoadAreaCentreDing0, LVGridDistrictDing0
+from ding0.core.network.stations import LVStationDing0
+from ding0.core.network.loads import MVLoadDing0
+from ding0.core import MVCableDistributorDing0
+
+
+def evaluate_grid_length(nd):
+    
+    ring_length = [] # total length of grid's main rings
+    stub_length = [] # total length of grid's stub lines
+
+    for mvgd in nd.mv_grid_districts():
+
+        g = mvgd.mv_grid.graph.copy()
+
+        # k-core of graph describes main ring topology
+        g_core = nx.k_core(g, k=2)
+        rings_edges = [n for n in g.edges if n in g_core.edges]
+        # stub graph described by residual components 
+        g_stub = g.copy()
+        g_stub.remove_edges_from(rings_edges)
+        g_stub.remove_nodes_from(list(nx.isolates(g_stub)))
+
+        # get edges separately per ring
+        for c in nx.cycle_basis(g_core, mvgd.mv_grid._station):
+            ring_edges = g_core.subgraph(c).edges
+            ring_length.append(sum([mvgd.mv_grid._graph.edges[e]['branch'].length for e in ring_edges]))
+
+        # get edges separately per stub component
+        for c in nx.connected_components(g_stub):
+            stub_edges = g_stub.subgraph(c).edges
+            stub_length.append(sum([mvgd.mv_grid._graph.edges[e]['branch'].length for e in stub_edges]))
+
+    # length data output row-wise per ring stub / component
+    df = pd.DataFrame.from_dict({'ring_length': ring_length, 'stub_length': stub_length}, orient='index')
+    df = df.transpose()
+    
+    return df
+
+
+def get_load_specific_results(nd):
+    
+    #mvgd
+    mvgd_no_tot = len(nd._mv_grid_districts)
+    mvgd_area_tot = sum([mvgd.geo_data.area for mvgd in nd.mv_grid_districts()])
+
+    #la
+    las = []
+    las_urban = []
+    las_rural = []
+
+    las_area = []
+    las_area_urban = []
+    las_area_rural = []
+
+    las_urban_isolated = []
+    las_rural_isolated = []
+
+    for mvgd in nd.mv_grid_districts():
+        for la in mvgd._lv_load_areas:
+            las.append(la)
+            las_area.append(la.geo_area.area)
+            if la.is_aggregated:
+                las_urban.append(la)
+                las_area_urban.append(la.geo_area.area)
+                if nx.is_empty(la.load_area_graph):
+                    las_urban_isolated.append(la)
+            else:
+                las_rural.append(la)
+                las_area_rural.append(la.geo_area.area)
+                if nx.is_empty(la.load_area_graph):
+                    las_rural_isolated.append(la)
+
+    la_no_total = len(las)
+    la_urban_no_total = len(las_urban)
+    la_rural_no_total = len(las_rural)
+
+    la_area_total = sum(las_area)
+    la_urban_area_total = sum(las_area_urban)
+    la_urban_area_mean = np.mean(las_area_urban)
+    la_rural_area_total = sum(las_area_rural)
+    la_rural_area_mean = np.mean(las_area_rural)
+    
+    if la_urban_no_total == 0:
+        la_urban_isolated_tot_rate = 0
+    else:
+        la_urban_isolated_tot_rate = len(las_urban_isolated) / la_urban_no_total * 100
+        
+    if la_rural_no_total == 0:
+        la_rural_isolated_tot_rate = 0
+    else:
+        la_rural_isolated_tot_rate = len(las_rural_isolated) / la_rural_no_total * 100
+
+    # nodes
+    mvgd_nodes_tot = []
+    mvgd_nodes_tot_isl = []
+
+    mvgd_urban_load_nodes = []
+    mvgd_rural_load_nodes = []
+
+    ring_stations_tot = []
+    ring_loads_tot = []
+    stub_stations_tot = []
+    stub_loads_tot = []
+
+    for mvgd in nd.mv_grid_districts():
+        # total
+        mvgd_g_nodes = [node for node in mvgd.mv_grid.graph.nodes() if isinstance(node, LVStationDing0) or isinstance(node, MVLoadDing0) or isinstance(node, MVCableDistributorDing0)] 
+        mvgd_g_nodes_isl = [node for node in mvgd.mv_grid.graph_isolated_nodes() if isinstance(node, LVStationDing0) or isinstance(node, MVLoadDing0) or isinstance(node, MVCableDistributorDing0)]
+        mvgd_nodes_tot.extend(mvgd_g_nodes)
+        mvgd_nodes_tot_isl.extend(mvgd_g_nodes_isl)
+        # urban
+        for la in mvgd._lv_load_areas:
+            if la.is_aggregated:
+                mvgd_urban_load_nodes.extend(la._mv_loads)
+                mvgd_urban_load_nodes.extend(la._lv_grid_districts)
+            else:
+                mvgd_rural_load_nodes.extend(la._mv_loads)
+                mvgd_rural_load_nodes.extend(la._lv_grid_districts)
+
+        g = mvgd.mv_grid.graph.copy() # remove isolated nodes
+        g.remove_nodes_from(list(nx.isolates(g)))
+        g_core = nx.k_core(g, k=2)
+        ring_nodes = g_core.nodes
+        stub_nodes = list(set(g.nodes) - set(ring_nodes))
+
+        ring_stations = [node for node in ring_nodes if isinstance(node, LVStationDing0)]
+        ring_stations_tot.extend(ring_stations)
+        ring_loads = [node for node in ring_nodes if isinstance(node, MVLoadDing0)]
+        ring_loads_tot.extend(ring_loads)
+        stub_stations = [node for node in stub_nodes if isinstance(node, LVStationDing0)]
+        stub_stations_tot.extend(stub_stations)
+        stub_loads = [node for node in stub_nodes if isinstance(node, MVLoadDing0)]
+        stub_loads_tot.extend(stub_loads)
+
+    mvgd_nodes_tot_no = len(mvgd_nodes_tot)
+    mvgd_nodes_dist_no = len([node for node in mvgd_nodes_tot if isinstance(node, MVCableDistributorDing0)])
+    mvgd_nodes_subst_no = len([node for node in mvgd_nodes_tot if isinstance(node, LVStationDing0)])
+    mvgd_nodes_load_no = len([node for node in mvgd_nodes_tot if isinstance(node, MVLoadDing0)])
+    mvgd_nodes_tot_isl_no = len(mvgd_nodes_tot_isl)
+    mvgd_urban_load_nodes_no = len(mvgd_urban_load_nodes) - len([node for node in mvgd_urban_load_nodes if isinstance(node, LVGridDistrictDing0) and int(node.peak_load) == 0])
+    mvgd_rural_load_nodes_no = len(mvgd_rural_load_nodes) 
+    mvgd_urban_isolated_nodes_no = len(set(mvgd_nodes_tot_isl) & set([node.lv_grid._station if isinstance(node, LVGridDistrictDing0) else node for node in mvgd_urban_load_nodes]))
+    mvgd_rural_isolated_nodes_no = len(set(mvgd_nodes_tot_isl) & set([node.lv_grid._station if isinstance(node, LVGridDistrictDing0) else node for node in mvgd_rural_load_nodes]))
+
+    ring_stations_tot_no = len(ring_stations_tot)
+    ring_loads_tot_no = len(ring_loads_tot)
+    stub_stations_tot_no = len(stub_stations_tot)
+    stub_loads_tot_no = len(stub_loads_tot)
+
+    res = {}
+    
+    res['mvgd_tot_no'] = int(mvgd_no_tot)
+    res['mvgd_area_tot [km]'] = np.round_(mvgd_area_tot/1000000, 1)
+
+    res['la_no_total [km]'] = la_no_total
+    res['la_area_total [km]'] = np.round_(la_area_total/1000000, 1)
+    res['la_urban_no_total [km]'] = la_urban_no_total
+    res['la_urban_area_total [km]'] = np.round_(la_urban_area_total/1000000, 1)
+    res['la_urban_area_mean [km]'] = np.round_(la_urban_area_mean/1000000, 1)
+    res['la_urban_isolated_tot_rate [%]'] = np.round_(la_urban_isolated_tot_rate, 1)
+    res['la_rural_no_total [km]'] = la_rural_no_total
+    res['la_rural_area_total [km]'] = np.round_(la_rural_area_total/1000000, 1)
+    res['la_rural_area_mean [km]'] = np.round_(la_rural_area_mean/1000000, 1)
+    res['la_rural_isolated_tot_rate [%]'] = np.round_(la_rural_isolated_tot_rate, 1)
+
+    res['mvgd_nodes_tot_no'] = mvgd_nodes_tot_no
+    res['mvgd_nodes_dist_no'] = mvgd_nodes_dist_no
+    res['mvgd_nodes_subst_no'] = mvgd_nodes_subst_no
+    res['mvgd_nodes_load_no'] = mvgd_nodes_load_no
+    res['mvgd_nodes_tot_isl_no'] = mvgd_nodes_tot_isl_no
+    res['mvgd_urban_load_nodes_no'] = mvgd_urban_load_nodes_no
+    res['mvgd_urban_isolated_nodes_no'] = mvgd_urban_isolated_nodes_no
+    res['mvgd_rural_load_nodes_no'] = mvgd_rural_load_nodes_no
+    res['mvgd_rural_isolated_nodes_no'] = mvgd_rural_isolated_nodes_no
+    res['ring_stations_tot_no'] = ring_stations_tot_no
+    res['stub_stations_tot_no'] = stub_stations_tot_no
+    res['ring_loads_tot_no'] = ring_loads_tot_no
+    res['stub_loads_tot_no'] = stub_loads_tot_no
+
+    df = pd.DataFrame.from_dict(res, orient='index')
+    
+    return df
+
+
+# suitable for comparison with simbench grid
+
+def get_structural_data_per_mvgd(nd):
+    
+    # returns data per row-wise per mvgd
+
+    data = {}
+
+    for mvgd in nd.mv_grid_districts():
+
+        data[mvgd] = {}
+
+        #### MVGD
+        # nominal voltage
+        data[mvgd]['v_level [kV]'] = mvgd.mv_grid.v_level
+        # hsms trafo sizes (list)
+        hs_ms_trafo_sizes = [t.s_max_a for t in mvgd.mv_grid._station._transformers]
+        data[mvgd]['hs_ms_trafo_sizes [MVA]'] = [int(t / 1000) for t in hs_ms_trafo_sizes]
+        # geo area (not simbench)
+        data[mvgd]['mvgd_geo_area [km2]'] = np.round_(mvgd.geo_data.area / 1000000, 1)
+
+        #### LA / LVGD
+
+        urban_la_area = sum([la.geo_area.area for la in mvgd._lv_load_areas if la.is_aggregated == True])
+        tot_la_area = sum([la.geo_area.area for la in mvgd._lv_load_areas])
+        # urbanisation ratio
+        data[mvgd]['urban_area_ratio'] = int(urban_la_area / tot_la_area * 100)
+
+        ms_ns_inst_power = 0 # per mvgd
+        ms_ns_load_power_s = 0
+
+        for la in mvgd._lv_load_areas:
+
+            for lvgd in la._lv_grid_districts: # Buildings
+
+                for trafo in lvgd.lv_grid._station._transformers:
+                    ms_ns_inst_power += trafo.s_max_a
+
+                ms_ns_load_power_s += lvgd.peak_load
+
+        ms_load_power_s = sum([load.peak_load * 0.6 for load in mvgd.mv_grid._loads]) # no diversity
+
+        # installed ms-ns-power
+        data[mvgd]['ms_ns_inst_power [MVA]'] = np.round_(ms_ns_inst_power / 1000, 1)
+        # ms-ns power / hs-ms power ratio
+        data[mvgd]['ms_ns_hs_ms_power_ratio [%]'] = np.round_(ms_ns_inst_power / sum(hs_ms_trafo_sizes) * 100, 1)
+        # ms-ns power + ms_load / hs-ms power ratio
+        data[mvgd]['load_hs_ms_power_ratio [%]'] = np.round_((ms_ns_inst_power + ms_load_power_s) / sum(hs_ms_trafo_sizes) * 100, 1)
+        # load_power_sum
+        data[mvgd]['load_power_sum_p [MW]'] = np.round_((ms_load_power_s + ms_ns_load_power_s) * 0.97 / 1000, 1)
+
+        #### BRANCHES
+
+        branch_lengths = [edge['branch'].length for edge in list(mvgd.mv_grid.graph_edges())]
+        cum_branch_length = int(sum(branch_lengths))
+        cum_cable_length = int(sum([edge['branch'].length for edge in list(mvgd.mv_grid.graph_edges()) if edge['branch'].kind == 'cable']))
+        mean_branch_length = np.mean(branch_lengths)
+        no_branches = len(branch_lengths)
+        data[mvgd]['cum_branch_length [km]'] = np.round_(cum_branch_length / 1000, 1)
+        if cum_branch_length == 0:
+            data[mvgd]['cable_ratio [%]'] = 0
+        else:
+            data[mvgd]['cable_ratio [%]'] = int(cum_cable_length / cum_branch_length * 100) # calculation not necessary, is set in validate mvgd
+        
+        data[mvgd]['branch_lengths [km]'] = list(np.round_(np.divide(branch_lengths, 1000), 3))
+        data[mvgd]['mean_branch_length [km]'] = np.round_(mean_branch_length / 1000, 3)
+        data[mvgd]['no_branches'] = no_branches
+
+        ### GRAPH
+
+        # open rings to extraxt feeders
+        nd.control_circuit_breakers(mode='open')
+
+        mv_station = mvgd.mv_grid._station
+
+        G = mvgd.mv_grid._graph.copy()
+        data[mvgd]['nodes_no'] = len([node for node in G.nodes])
+        data[mvgd]['load_nodes_no'] = len([node for node in G.nodes if isinstance(node, LVStationDing0) or isinstance(node, MVLoadDing0)])
+        G.remove_node(mv_station)
+        G.remove_nodes_from(mvgd.mv_grid.graph_isolated_nodes())
+
+        # feeder start nodes
+        root_edges = [(mv_station,n) for n in mvgd.mv_grid._graph.neighbors(mv_station)] 
+        # feeders (contain nodes)
+        feeder_comp = list(nx.connected_components(G)) 
+        # number of feeders
+        data[mvgd]['no_feeders'] = len(feeder_comp) 
+
+        feeder_branch_len_mvgd = []
+        supply_nodes_per_feeder_mvgd = []
+
+        for feeder in feeder_comp:
+
+            station_nodes = [node for node in feeder if isinstance(node, LVStationDing0)]
+            load_nodes = [node for node in feeder if isinstance(node, MVLoadDing0)]
+            dist_nodes = [node for node in feeder if isinstance(node, MVCableDistributorDing0)]
+
+            # nodes per feeder
+            nodes_per_feeder = feeder
+            # supply nodes per feeder
+            supply_nodes_per_feeder = len([node for node in feeder if isinstance(node, LVStationDing0) or isinstance(node, MVLoadDing0)])
+            supply_nodes_per_feeder_mvgd.append(supply_nodes_per_feeder)
+            # feeder branches
+            edges = list(G.edges(nodes_per_feeder))
+            edges.append([e_tuple for e_tuple in root_edges if e_tuple[1] in nodes_per_feeder][0])
+            # feeder len
+            feeder_branch_len = sum([mvgd.mv_grid._graph.edges[e]['branch'].length for e in edges])
+            if feeder_branch_len > 1:
+                feeder_branch_len_mvgd.append(feeder_branch_len)
+        
+        data[mvgd]['feeder_lengths [km]'] = list(np.round_(np.divide(feeder_branch_len_mvgd, 1000), 1))
+        data[mvgd]['feeder_len_mean [km]'] = np.round_(np.mean(feeder_branch_len_mvgd) / 1000, 1)
+        data[mvgd]['feeder_len_min [km]'] = np.round_(min(feeder_branch_len_mvgd) / 1000, 1)
+        data[mvgd]['feeder_len_max [km]'] = np.round_(max(feeder_branch_len_mvgd) / 1000, 1)
+        
+        data[mvgd]['supply_nodes_per_feeder'] = supply_nodes_per_feeder_mvgd
+        data[mvgd]['supply_nodes_per_feeder_mean'] = np.round_(np.mean(supply_nodes_per_feeder_mvgd), 1)
+        data[mvgd]['supply_nodes_per_feeder_min'] = min(supply_nodes_per_feeder_mvgd)
+        data[mvgd]['supply_nodes_per_feeder_max'] = max(supply_nodes_per_feeder_mvgd)
+
+        nd.control_circuit_breakers(mode='close')
+
+    df = pd.DataFrame.from_dict(data, orient='index')
+    
+    return df
+
+
+def evaluate_substation_geo_parameters(nd):
+
+    ## BUILDINGS PER STATION
+
+    buildings_per_station = []
+    average_dist_stations = []
+    max_supply_radius = []
+    max_urban_radius = []
+
+    for mvgd in nd.mv_grid_districts():
+        for la in mvgd._lv_load_areas:
+            if la.is_aggregated:
+
+                # buildings_per_station
+                buildings_per_station.extend([int(sum(lvgd.buildings.number_households)) for lvgd in la._lv_grid_districts])
+
+                # average_dist_stations
+                coords = [[lvgd.lv_grid._station.geo_data.x,lvgd.lv_grid._station.geo_data.y] for lvgd in la._lv_grid_districts]
+                if len(coords) > 1:
+                    nbrs = NearestNeighbors(n_neighbors=2, algorithm='auto', metric='euclidean').fit(coords)
+                    distances, indices = nbrs.kneighbors(coords)
+                    distances = [d[1] for d in distances]
+                    average_dist_stations.extend(distances)
+
+                # max_supply_radius_ms_ns
+                for lvgd in la._lv_grid_districts:
+                    station_coords = list(lvgd.lv_grid._station.geo_data.coords)
+                    building_coords = [(p.x,p.y) for p in lvgd.buildings.raccordement_building.to_list()]
+                    if len(building_coords) > 1:
+                        radius_max = distance.cdist(station_coords, building_coords, 'euclidean').max()
+                        max_supply_radius.append(radius_max)
+
+                # max_supply_radius_hs_ms
+                station_coords = list(la.mv_grid_district.mv_grid._station.geo_data.coords)
+                poly_ext_coords = list(la.geo_area.exterior.coords)
+                if len(poly_ext_coords) > 1:
+                    radius_max = distance.cdist(station_coords, poly_ext_coords, 'euclidean').max()
+                    max_urban_radius.append(radius_max)
+                    
+    lit = {}
+    lit['buildings_per_station'] = str([int(b) for b in buildings_per_station])
+    lit['average_dist_stations'] = str([int(b) for b in average_dist_stations])
+    lit['max_supply_radius'] = str([int(b) for b in max_supply_radius])
+    lit['max_urban_radius'] = str([int(b) for b in max_urban_radius])
+
+    df = pd.DataFrame(lit, index=[0])
+
+    return df
+
+
+def evaluate_supply_point_parameters(nd):
+    
+    mv_peak_loads = [] # peak loads of mv loads
+    station_load_factor = [] # ratio of lvgd peak load / installed trafo capacity of mv/lv substation 
+    trafo_sizes = [] # distribution of  installed trafo capacity of mv/lv substation(s) 
+    trafo_per_station = [] # nested list with installed trafos per mv_lv substation
+
+    for mvgd in nd.mv_grid_districts():
+        load_val = [load.peak_load for load in mvgd.mv_grid._loads]
+        mv_peak_loads.extend(load_val)
+        for la in mvgd._lv_load_areas:
+            for lvgd in la._lv_grid_districts:
+                station_pl = lvgd.peak_load
+                trafo_power = [t.s_max_a for t in lvgd.lv_grid._station._transformers]
+                trafo_sizes.extend(trafo_power)
+                trafo_per_station.append(trafo_power)
+                ratio = station_pl / sum(trafo_power)
+                station_load_factor.append(ratio)
+                
+        
+    d = {}
+    d['mv_peak_loads'] = mv_peak_loads
+    d['station_load_factor'] = station_load_factor
+    d['trafo_sizes'] = trafo_sizes
+    d['trafo_per_station'] = trafo_per_station
+    
+    df = pd.DataFrame.from_dict(d, orient='index')
+    df = df.transpose()
+    
+    return df
\ No newline at end of file
diff --git a/ding0/grid/mv_grid/models/models.py b/ding0/grid/mv_grid/models/models.py
index 3000f84b..bff29968 100644
--- a/ding0/grid/mv_grid/models/models.py
+++ b/ding0/grid/mv_grid/models/models.py
@@ -27,7 +27,7 @@
 import logging
 
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 class Route(object):
diff --git a/ding0/grid/mv_grid/mv_connect.py b/ding0/grid/mv_grid/mv_connect.py
index a5e60f78..bc3d316b 100644
--- a/ding0/grid/mv_grid/mv_connect.py
+++ b/ding0/grid/mv_grid/mv_connect.py
@@ -19,19 +19,24 @@
 from pyproj import Transformer
 
 from ding0.core.network.stations import *
+from ding0.core.network.loads import MVLoadDing0
 from ding0.core.network import BranchDing0, GeneratorDing0
 from ding0.core import MVCableDistributorDing0
 from ding0.core.structure.groups import LoadAreaGroupDing0
 from ding0.core.structure.regions import LVLoadAreaCentreDing0
 from ding0.tools import config as cfg_ding0
 from ding0.tools.geo import calc_geo_branches_in_buffer,calc_geo_dist,\
-                            calc_geo_centre_point, calc_geo_branches_in_polygon
+                            calc_geo_centre_point, calc_geo_branches_in_polygon, \
+                            calc_edge_geometry
+from ding0.grid.mv_grid.tools import update_branch_shps_settle, relocate_cable_dists_settle, \
+                                     relabel_graph_nodes, get_shortest_path_shp_multi_target, \
+                                     conn_ding0_obj_to_osm_graph
 
 if not 'READTHEDOCS' in os.environ:
     from shapely.geometry import LineString
     from shapely.ops import transform
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 def find_nearest_conn_objects(node_shp, branches, proj, conn_dist_weight, debug, branches_only=False):
@@ -80,8 +85,11 @@ def find_nearest_conn_objects(node_shp, branches, proj, conn_dist_weight, debug,
         stations = branch['adj_nodes']
 
         # create shapely objects for 2 stations and line between them, transform to equidistant CRS
-        station1_shp = transform(proj, stations[0].geo_data)
-        station2_shp = transform(proj, stations[1].geo_data)
+        # station1_shp = transform(proj, stations[0].geo_data)
+        # station2_shp = transform(proj, stations[1].geo_data)
+        # THEY ARE ALREADY IN SRID=3035
+        station1_shp = stations[0].geo_data
+        station2_shp = stations[1].geo_data
         line_shp = LineString([station1_shp, station2_shp])
 
         # create dict with DING0 objects (line & 2 adjacent stations), shapely objects and distances
@@ -383,7 +391,9 @@ def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_rin
 
         # find nearest point on MV line
         conn_point_shp = target_obj['shp'].interpolate(target_obj['shp'].project(node_shp))
-        conn_point_shp = transform(proj, conn_point_shp)
+        # conn_point_shp = transform(proj, conn_point_shp)
+        # already in srid=3035
+        conn_point_shp = conn_point_shp
 
         # target MV line does currently not connect a load area of type aggregated
         if not target_obj['obj']['branch'].connects_aggregated:
@@ -405,9 +415,12 @@ def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_rin
                 # split old ring main route into 2 segments (delete old branch and create 2 new ones
                 # along node)
                 graph.remove_edge(adj_node1, adj_node2)
-
+                
+                # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+                branch_shp, branch_length = calc_edge_geometry(adj_node1, node)
                 branch_length = calc_geo_dist(adj_node1, node)
-                branch = BranchDing0(length=branch_length,
+                branch = BranchDing0(geometry=branch_shp,
+                                     length=branch_length,
                                      circuit_breaker=circ_breaker,
                                      kind=branch_kind,
                                      grid=mv_grid,
@@ -416,9 +429,12 @@ def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_rin
                 if circ_breaker is not None:
                     circ_breaker.branch = branch
                 graph.add_edge(adj_node1, node, branch=branch)
-
+                
+                # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+                branch_shp, branch_length = calc_edge_geometry(adj_node2, node)
                 branch_length = calc_geo_dist(adj_node2, node)
-                graph.add_edge(adj_node2, node, branch=BranchDing0(length=branch_length,
+                graph.add_edge(adj_node2, node, branch=BranchDing0(geometry=branch_shp,
+                                                                   length=branch_length,
                                                                    kind=branch_kind,
                                                                    grid=mv_grid,
                                                                    type=branch_type,
@@ -454,9 +470,13 @@ def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_rin
                 branch_ring = graph.adj[adj_node1][adj_node2]['branch'].ring
 
                 graph.remove_edge(adj_node1, adj_node2)
-
+                
+                
+                # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+                branch_shp, branch_length = calc_edge_geometry(adj_node1, cable_dist)
                 branch_length = calc_geo_dist(adj_node1, cable_dist)
-                branch = BranchDing0(length=branch_length,
+                branch = BranchDing0(geometry=branch_shp,
+                                     length=branch_length,
                                      circuit_breaker=circ_breaker,
                                      kind=branch_kind,
                                      grid=mv_grid,
@@ -465,9 +485,12 @@ def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_rin
                 if circ_breaker is not None:
                     circ_breaker.branch = branch
                 graph.add_edge(adj_node1, cable_dist, branch=branch)
-
+                
+                # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+                branch_shp, branch_length = calc_edge_geometry(adj_node2, cable_dist)
                 branch_length = calc_geo_dist(adj_node2, cable_dist)
-                graph.add_edge(adj_node2, cable_dist, branch=BranchDing0(length=branch_length,
+                graph.add_edge(adj_node2, cable_dist, branch=BranchDing0(geometry=branch_shp,
+                                                                         length=branch_length,
                                                                          kind=branch_kind,
                                                                          grid=mv_grid,
                                                                          type=branch_type,
@@ -479,9 +502,12 @@ def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_rin
                 # get default branch kind and type from grid to use it for new branch
                 branch_kind = mv_grid.default_branch_kind
                 branch_type = mv_grid.default_branch_type
-
+                
+                # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+                branch_shp, branch_length = calc_edge_geometry(node, cable_dist)
                 branch_length = calc_geo_dist(node, cable_dist)
-                graph.add_edge(node, cable_dist, branch=BranchDing0(length=branch_length,
+                graph.add_edge(node, cable_dist, branch=BranchDing0(geometry=branch_shp,
+                                                                    length=branch_length,
                                                                     kind=branch_kind,
                                                                     grid=mv_grid,
                                                                     type=branch_type,
@@ -502,23 +528,27 @@ def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_rin
         #   LVStationDing0: Connect to LVLoadAreaCentreDing0, LVStationDing0 or MVCableDistributorDing0
         #   GeneratorDing0: Connect to LVLoadAreaCentreDing0, LVStationDing0, MVCableDistributorDing0 or GeneratorDing0
         if isinstance(node, LVLoadAreaCentreDing0):
-            valid_conn_objects = LVLoadAreaCentreDing0
+            valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0, MVCableDistributorDing0)
         elif isinstance(node, LVStationDing0):
-            valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVCableDistributorDing0)
+            valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0, MVCableDistributorDing0)
+        elif isinstance(node, MVLoadDing0):
+            valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0, MVCableDistributorDing0)
         elif isinstance(node, GeneratorDing0):
-            valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVCableDistributorDing0, GeneratorDing0)
+            valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0, MVCableDistributorDing0, GeneratorDing0)
         else:
             raise ValueError('Oops, the node you are trying to connect is not a valid connection object')
 
         # if target is Load Area centre or LV station, check if it belongs to a load area of type aggregated
-        # (=> connection not allowed)
-        if isinstance(target_obj['obj'], (LVLoadAreaCentreDing0, LVStationDing0)):
+        # deprecated: (=> connection of nodes belonging to aggregated load areas not allowed)
+        # updated: connection between satellites and nodes of aggregated load areas is allowed
+        # connection from satellite to lines of aggregated load area is still not allowed
+        if isinstance(target_obj['obj'], (LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0)):
             target_is_aggregated = target_obj['obj'].lv_load_area.is_aggregated
         else:
             target_is_aggregated = False
 
         # target node is not a load area of type aggregated
-        if isinstance(target_obj['obj'], valid_conn_objects) and not target_is_aggregated:
+        if isinstance(target_obj['obj'], valid_conn_objects): # and not target_is_aggregated:
 
             # get default branch kind and type from grid to use it for new branch
             branch_kind = mv_grid.default_branch_kind
@@ -528,8 +558,11 @@ def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_rin
             branch_ring = mv_grid.get_ring_from_node(target_obj['obj'])
 
             # add new branch for satellite (station to station)
+            # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+            branch_shp, branch_length = calc_edge_geometry(node, target_obj['obj'])
             branch_length = calc_geo_dist(node, target_obj['obj'])
-            graph.add_edge(node, target_obj['obj'], branch=BranchDing0(length=branch_length,
+            graph.add_edge(node, target_obj['obj'], branch=BranchDing0(geometry=branch_shp,
+                                                                       length=branch_length,
                                                                        kind=branch_kind,
                                                                        grid=mv_grid,
                                                                        type=branch_type,
@@ -574,9 +607,12 @@ def disconnect_node(node, target_obj_result, graph, debug):
 
         if len(neighbor_nodes) == 2:
             node.grid.remove_cable_distributor(target_obj_result)
-
+            
+            # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+            branch_shp, branch_length = calc_edge_geometry(neighbor_nodes[0], neighbor_nodes[1])
             branch_length = calc_geo_dist(neighbor_nodes[0], neighbor_nodes[1])
-            graph.add_edge(neighbor_nodes[0], neighbor_nodes[1], branch=BranchDing0(length=branch_length,
+            graph.add_edge(neighbor_nodes[0], neighbor_nodes[1], branch=BranchDing0(geometry=branch_shp,
+                                                                                    length=branch_length,
                                                                                     kind=branch_kind,
                                                                                     grid=node.grid,
                                                                                     type=branch_type,
@@ -683,7 +719,9 @@ def mv_connect_satellites(mv_grid, graph, mode='normal', debug=False):
             # satellites only
             if node.lv_load_area.is_satellite:
 
-                node_shp = transform(proj1, node.geo_data)
+                # node_shp = transform(proj1, node.geo_data)
+                # is already in srid=3035
+                node_shp = node.geo_data
 
                 if mode == 'normal':
                     # get branches within a the predefined radius `load_area_sat_buffer_radius`
@@ -755,10 +793,13 @@ def mv_connect_stations(mv_grid_district, graph, debug=False):
 
             lv_load_area_centre = lv_load_area.lv_load_area_centre
 
-            # there's only one station: Replace Load Area centre by station in graph
-            if lv_load_area.lv_grid_districts_count() == 1:
-                # get station
-                lv_station = list(lv_load_area.lv_grid_districts())[0].lv_grid.station()
+            # there's only one supply node: Replace Load Area centre by station/load in graph
+            if lv_load_area.lv_grid_districts_count() + lv_load_area.mv_loads_count() == 1:
+                # get supply node as station/load
+                if lv_load_area.lv_grid_districts_count() == 1:
+                    supply_node = list(lv_load_area.lv_grid_districts())[0].lv_grid.station()
+                elif lv_load_area.mv_loads_count() == 1:
+                    supply_node = list(lv_load_area._mv_loads)[0]
 
                 # get branches that are connected to Load Area centre
                 branches = mv_grid_district.mv_grid.graph_branches_from_node(lv_load_area_centre)
@@ -773,13 +814,16 @@ def mv_connect_stations(mv_grid_district, graph, debug=False):
                     # respect circuit breaker if existent
                     circ_breaker = branch['branch'].circuit_breaker
                     if circ_breaker is not None:
-                        branch['branch'].circuit_breaker.geo_data = calc_geo_centre_point(lv_station, node)
+                        branch['branch'].circuit_breaker.geo_data = calc_geo_centre_point(supply_node, node)
 
                     # delete old branch to Load Area centre and create a new one to LV station
                     graph.remove_edge(lv_load_area_centre, node)
 
-                    branch_length = calc_geo_dist(lv_station, node)
-                    branch = BranchDing0(length=branch_length,
+                    # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+                    branch_shp, branch_length = calc_edge_geometry(supply_node, node)
+                    branch_length = calc_geo_dist(supply_node, node) #TODO del, but more precise than preceding method
+                    branch = BranchDing0(geometry=branch_shp,
+                                         length=branch_length,
                                          circuit_breaker=circ_breaker,
                                          kind=branch_kind,
                                          grid=mv_grid_district.mv_grid,
@@ -787,66 +831,40 @@ def mv_connect_stations(mv_grid_district, graph, debug=False):
                                          ring=branch_ring)
                     if circ_breaker is not None:
                         circ_breaker.branch = branch
-                    graph.add_edge(lv_station, node, branch=branch)
+                    graph.add_edge(supply_node, node, branch=branch)
 
                 # delete Load Area centre from graph
                 graph.remove_node(lv_load_area_centre)
 
-            # there're more than one station: Do normal connection process (as in satellites)
+            # there is more than one supply node: Do normal connection process (as in satellites)
             else:
-                # connect LV stations of all grid districts
-                # =========================================
-                for lv_grid_district in lv_load_area.lv_grid_districts():
-                    # get branches that are partly or fully located in load area
-                    branches = calc_geo_branches_in_polygon(mv_grid_district.mv_grid,
-                                                            lv_load_area.geo_area,
-                                                            mode='intersects',
-                                                            proj=proj1)
-
-                    # filter branches that belong to satellites (load area groups) if Load Area is not a satellite
-                    # itself
-                    if not lv_load_area.is_satellite:
-                        branches_valid = []
-                        for branch in branches:
-                            node1 = branch['adj_nodes'][0]
-                            node2 = branch['adj_nodes'][1]
-                            lv_load_area_group = get_lv_load_area_group_from_node_pair(node1, node2)
-
-                            # delete branch as possible conn. target if it belongs to a group (=satellite) or
-                            # if it belongs to a ring different from the ring of the current LVLA
-                            if (lv_load_area_group is None) and\
-                               (branch['branch'].ring is lv_load_area.ring):
-                                branches_valid.append(branch)
-                        branches = branches_valid
-
-                    # find possible connection objects
-                    lv_station = lv_grid_district.lv_grid.station()
-                    lv_station_shp = transform(proj1, lv_station.geo_data)
-                    conn_objects_min_stack = find_nearest_conn_objects(lv_station_shp, branches, proj1,
-                                                                       conn_dist_weight, debug,
-                                                                       branches_only=False)
-
-                    # connect!
-                    connect_node(lv_station,
-                                 lv_station_shp,
-                                 mv_grid_district.mv_grid,
-                                 conn_objects_min_stack[0],
-                                 proj2,
-                                 graph,
-                                 conn_dist_ring_mod,
-                                 debug)
-
-                # Replace Load Area centre by cable distributor
+
+                # get supply nodes in load area
+                lv_load_area_supply_nodes = list(lvgd.lv_grid.station() for lvgd in lv_load_area.lv_grid_districts()) \
+                                            + list(lv_load_area._mv_loads)
+
+                # Replace Load Area centre by supply node / cable distributor
                 # ================================================
-                # create cable distributor and add it to grid
-                cable_dist = MVCableDistributorDing0(geo_data=lv_load_area_centre.geo_data,
-                                                     grid=mv_grid_district.mv_grid)
-                mv_grid_district.mv_grid.add_cable_distributor(cable_dist)
+                # check if coords of load area centre are identical with one supply node
+                supply_node_as_centre = [node for node in lv_load_area_supply_nodes \
+                                        if node.geo_data == lv_load_area_centre.geo_data]
+
+                # either supply node or cable distributor replaces load area centre
+                # load area centre's adjacent branches will be used to connect new centre
+                if supply_node_as_centre:
+                    centre_node = supply_node_as_centre[0]
+                    # remove node from supply node list (only includes non-connected nodes)
+                    lv_load_area_supply_nodes.remove(centre_node)
+                else:
+                    # create cable distributor and add it to grid
+                    centre_node = MVCableDistributorDing0(geo_data=lv_load_area_centre.geo_data,
+                                                          grid=mv_grid_district.mv_grid)
+                    mv_grid_district.mv_grid.add_cable_distributor(centre_node)
 
                 # get branches that are connected to Load Area centre
                 branches = mv_grid_district.mv_grid.graph_branches_from_node(lv_load_area_centre)
 
-                # connect LV station, delete Load Area centre
+                # connect station / load, delete Load Area centre
                 for node, branch in branches:
                     # backup kind and type of branch
                     branch_kind = branch['branch'].kind
@@ -856,13 +874,16 @@ def mv_connect_stations(mv_grid_district, graph, debug=False):
                     # respect circuit breaker if existent
                     circ_breaker = branch['branch'].circuit_breaker
                     if circ_breaker is not None:
-                        branch['branch'].circuit_breaker.geo_data = calc_geo_centre_point(cable_dist, node)
+                        branch['branch'].circuit_breaker.geo_data = calc_geo_centre_point(centre_node, node)
 
                     # delete old branch to Load Area centre and create a new one to LV station
                     graph.remove_edge(lv_load_area_centre, node)
 
-                    branch_length = calc_geo_dist(cable_dist, node)
-                    branch = BranchDing0(length=branch_length,
+                    # add straight LineString as geometry to branch, replaces calc_geo_dist
+                    branch_shp, branch_length = calc_edge_geometry(centre_node, node)
+                    branch_length = calc_geo_dist(centre_node, node)
+                    branch = BranchDing0(geometry=branch_shp,
+                                         length=branch_length,
                                          circuit_breaker=circ_breaker,
                                          kind=branch_kind,
                                          grid=mv_grid_district.mv_grid,
@@ -870,11 +891,127 @@ def mv_connect_stations(mv_grid_district, graph, debug=False):
                                          ring=branch_ring)
                     if circ_breaker is not None:
                         circ_breaker.branch = branch
-                    graph.add_edge(cable_dist, node, branch=branch)
+                    graph.add_edge(centre_node, node, branch=branch)
 
                 # delete Load Area centre from graph
                 graph.remove_node(lv_load_area_centre)
 
+                # connect stations/loads of all grid districts
+                # =========================================
+
+                # in case load area has just one unconnected supply node
+                # cable routing is done without considering the street topology
+                if len(lv_load_area_supply_nodes) < 2:
+
+                    for supply_node in lv_load_area_supply_nodes:
+                        # get branches that are partly or fully located in load area
+                        branches = calc_geo_branches_in_polygon(mv_grid_district.mv_grid,
+                                                                lv_load_area.geo_area,
+                                                                mode='intersects',
+                                                                proj=proj1)
+
+                        # filter branches that belong to satellites (load area groups) if Load Area is not a satellite
+                        # itself
+                        if not lv_load_area.is_satellite:
+                            branches_valid = []
+                            for branch in branches:
+                                node1, node2 = branch['adj_nodes']
+                                lv_load_area_group = get_lv_load_area_group_from_node_pair(node1, node2)
+
+                                # delete branch as possible conn. target if it belongs to a group (=satellite) or
+                                # if it belongs to a ring different from the ring of the current LVLA
+                                if (lv_load_area_group is None) and\
+                                   (branch['branch'].ring is lv_load_area.ring):
+                                    branches_valid.append(branch)
+                            branches = branches_valid
+
+                        # find possible connection objects
+                        supply_node_shp = supply_node.geo_data
+                        conn_objects_min_stack = find_nearest_conn_objects(supply_node_shp, branches, proj1,
+                                                                           conn_dist_weight, debug,
+                                                                           branches_only=False)
+
+                        # connect!
+                        connect_node(supply_node,
+                                     supply_node_shp,
+                                     mv_grid_district.mv_grid,
+                                     conn_objects_min_stack[0],
+                                     proj2,
+                                     graph,
+                                     conn_dist_ring_mod,
+                                     debug)
+
+                # load area has more than one unconnected supply node
+                # do cable routing by considering the street topology
+                else:
+
+                    # adapt ding0 graph's branches geometry and cable distributor positions
+                    # get branches that are partly or fully located in load area
+                    branches = calc_geo_branches_in_polygon(mv_grid_district.mv_grid,
+                                                lv_load_area.geo_area,
+                                                mode='intersects',
+                                                proj=proj1)
+
+                    # relocate positions of cable distributosrs, obtained
+                    # coordinates are identical with available street graph nodes
+                    # realted osm_id is allocated to cable distributor
+                    cable_dists = relocate_cable_dists_settle(lv_load_area, branches)
+
+                    # relabel street graph geoms to ease assignment to ding0 graph
+                    street_graph, _ = relabel_graph_nodes(lv_load_area, cable_dists)
+
+                    # branch geometries inside / incoming (load area) will be adapated
+                    # by following the street courses
+                    path_passed_osmids, street_graph = update_branch_shps_settle(lv_load_area, branches, street_graph)
+
+                    for supply_node in lv_load_area_supply_nodes:
+
+                        branches = calc_geo_branches_in_polygon(mv_grid_district.mv_grid,
+                                                lv_load_area.geo_area,
+                                                mode='intersects',
+                                                proj=proj1)
+
+                        if not lv_load_area.is_satellite:
+                            branches_valid = []
+                            for branch in branches:
+                                node1, node2 = branch['adj_nodes']
+                                lv_load_area_group = get_lv_load_area_group_from_node_pair(node1, node2)
+
+                                # in case updated branch shapes are additionally intersecting with load area
+                                for node in branch['adj_nodes']:
+                                    if not street_graph.has_node(str(node)):
+                                        street_graph = conn_ding0_obj_to_osm_graph(street_graph, node)
+
+                                # delete branch as possible conn. target if it belongs to a group (=satellite) or
+                                # if it belongs to a ring different from the ring of the current LVLA
+                                if (lv_load_area_group is None) and\
+                                   (branch['branch'].ring is lv_load_area.ring):
+                                    # just consider branches that have supply nodes located inside load area
+                                    if any([lv_load_area.geo_area.intersects(node.geo_data)
+                                            for node in branch['adj_nodes']]):
+                                        branches_valid.append(branch)
+                            branches = branches_valid
+
+                        conn_objects_min_stack, path_passed_osmids = find_nearest_conn_objects_settle(supply_node, branches,
+                                                                                                      lv_load_area_supply_nodes,
+                                                                                                      street_graph,
+                                                                                                      path_passed_osmids,
+                                                                                                      conn_dist_weight,
+                                                                                                      debug, branches_only=False)
+
+
+                        target_obj_result, street_graph, path_passed_osmids = connect_node_settle(supply_node,
+                                                                                                 supply_node.geo_data,
+                                                                                                 mv_grid_district.mv_grid,
+                                                                                                 conn_objects_min_stack[0],
+                                                                                                 proj2,
+                                                                                                 street_graph,
+                                                                                                 path_passed_osmids,
+                                                                                                 graph,
+                                                                                                 conn_dist_ring_mod,
+                                                                                                 debug)
+
+
             # Replace all overhead lines by cables
             # ====================================
             # if grid's default type is overhead line
@@ -892,6 +1029,7 @@ def mv_connect_stations(mv_grid_district, graph, debug=False):
     return graph
 
 
+
 def mv_connect_generators(mv_grid_district, graph, debug=False):
     """Connect MV generators to MV grid
 
@@ -924,14 +1062,17 @@ def mv_connect_generators(mv_grid_district, graph, debug=False):
         # ===== voltage level 4: generator has to be connected to MV station =====
         if generator.v_level == 4:
             mv_station = mv_grid_district.mv_grid.station()
-
+            
+            # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+            branch_shp, branch_length = calc_edge_geometry(generator, mv_station)
             branch_length = calc_geo_dist(generator, mv_station)
 
             # TODO: set branch type to something reasonable (to be calculated)
             branch_kind = mv_grid_district.mv_grid.default_branch_kind
             branch_type = mv_grid_district.mv_grid.default_branch_type
 
-            branch = BranchDing0(length=branch_length,
+            branch = BranchDing0(geometry=branch_shp,
+                                 length=branch_length,
                                  kind=branch_kind,
                                  grid=mv_grid_district.mv_grid,
                                  type=branch_type,
@@ -944,7 +1085,9 @@ def mv_connect_generators(mv_grid_district, graph, debug=False):
 
         # ===== voltage level 5: generator has to be connected to MV grid (next-neighbor) =====
         elif generator.v_level == 5:
-            generator_shp = transform(proj1, generator.geo_data)
+            # generator_shp = transform(proj1, generator.geo_data)
+            # alreaedy in srid=3035
+            generator_shp = generator.geo_data
 
             # get branches within a the predefined radius `generator_buffer_radius`
             branches = calc_geo_branches_in_buffer(generator,
@@ -994,3 +1137,318 @@ def mv_connect_generators(mv_grid_district, graph, debug=False):
                     'connection points.'.format(generator))
 
     return graph
+
+# functions for settlement routing
+from ding0.grid.mv_grid.tools import get_shortest_path_shp_single_target, cut_line_by_distance
+from shapely.ops import linemerge
+from shapely.geometry import Point
+import networkx as nx
+
+def find_nearest_conn_objects_settle(supply_node, branches, lv_load_area_supply_nodes, street_graph,
+                                     path_passed_osmids, conn_dist_weight, debug, branches_only=False):
+
+    # threshold which is used to determine if 2 objects are on the same position (see below for details on usage)
+    conn_diff_tolerance = cfg_ding0.get('mv_routing', 'conn_diff_tolerance')
+
+    conn_node = supply_node
+    conn_node_shp = conn_node.geo_data
+    G = street_graph
+
+    conn_objects_min_stack = []
+
+    for branch in branches:
+
+        end_node1, end_node2 = branch['adj_nodes']
+        end_node1_shp, end_node2_shp = end_node1.geo_data, end_node2.geo_data
+        line_shp = branch['branch'].geometry
+        try:
+            line_path = path_passed_osmids[branch['adj_nodes']]
+        except:
+            line_path = [str(end_node1), str(end_node2)]
+
+        line_s1_shp, line_s1_length, line_s1_path = get_shortest_path_shp_single_target(G, conn_node,
+                                                                                        end_node1, return_path=True)
+
+        line_s2_shp, line_s2_length, line_s2_path = get_shortest_path_shp_single_target(G, conn_node,
+                                                                                        end_node2, return_path=True)
+
+        line_b_shp, line_b_length, line_b_path = get_shortest_path_shp_multi_target(G, str(conn_node), line_path)
+
+        # create dict with DING0 objects (line & 2 adjacent stations), shapely objects and distances
+        if not branches_only:
+            conn_objects = {'s1': {'obj': end_node1,
+                                   'shp': end_node1_shp,
+                                   'dist': line_s1_length * conn_dist_weight * 0.999,
+                                   'line_shp': line_s1_shp,
+                                   'line_path': line_s1_path},
+                            's2': {'obj': end_node2,
+                                   'shp': end_node2_shp,
+                                   'dist': line_s2_length * conn_dist_weight * 0.999,
+                                   'line_shp': line_s2_shp,
+                                   'line_path': line_s2_path},
+                            'b': {'obj': branch,
+                                  'shp': line_shp,
+                                  'dist': line_b_length,
+                                  'line_shp': line_b_shp,
+                                  'line_path': line_b_path}}
+
+            # Remove branch from the dict of possible conn. objects if it is too close to a node.
+            # Without this solution, the target object is not unique for different runs (and so
+            # were the topology)
+            if (
+                    abs(conn_objects['s1']['dist'] - conn_objects['b']['dist']) < conn_diff_tolerance
+                    or abs(conn_objects['s2']['dist'] - conn_objects['b']['dist']) < conn_diff_tolerance
+            ):
+                del conn_objects['b']
+
+            # remove branch as possible connection, if endpoint is
+            # identical with origin of shortest path (line splittage
+            # is not possible for this particular case)
+            elif conn_objects['b']['line_path'][0] in [str(end_node1), str(end_node2)]:
+                del conn_objects['b']
+            # remove branch as possible connection, if root node
+            # of shortest path is a ding0 object (supply node)
+            elif conn_objects['b']['line_path'][0] in map(str, lv_load_area_supply_nodes):
+                del conn_objects['b']
+
+            # remove MV station as possible connection point
+            if isinstance(conn_objects['s1']['obj'], MVStationDing0):
+                del conn_objects['s1']
+            elif isinstance(conn_objects['s2']['obj'], MVStationDing0):
+                del conn_objects['s2']
+
+        else:
+            conn_objects = {'b': {'obj': branch,
+                                  'shp': line_shp,
+                                  'dist': line_b_shp.length,
+                                  'line_shp': line_b_shp,
+                                  'line_path': line_b_path}}
+
+        # find nearest connection point on given triple dict (2 branch-adjacent supply nodes + cable dist. on line)
+        conn_objects_min = min(conn_objects.values(), key=lambda v: v['dist'])
+        # if not branches_only:
+        #    conn_objects_min_stack.append(conn_objects_min)
+        # elif isinstance(conn_objects_min['shp'], LineString):
+        #    conn_objects_min_stack.append(conn_objects_min)
+        conn_objects_min_stack.append(conn_objects_min)
+
+    # sort all objects by distance from node
+    conn_objects_min_stack = [_ for _ in sorted(conn_objects_min_stack, key=lambda x: x['dist'])]
+
+    if debug:
+        logger.debug('Stack length: {}'.format(len(conn_objects_min_stack)))
+
+    return conn_objects_min_stack, path_passed_osmids
+
+
+def connect_node_settle(node, node_shp, mv_grid, target_obj, proj, street_graph, path_passed_osmids, graph,
+                        conn_dist_ring_mod, debug):
+    # street_graph
+    target_obj_result = None
+
+    # MV line is nearest connection point
+    if isinstance(target_obj['shp'], LineString):
+
+        # get branch geoms
+        adj_node1, adj_node2 = target_obj['obj']['adj_nodes']
+
+        # find nearest point on MV line
+        # nearest point is origin of shortest path to supply node
+        conn_node = target_obj['line_path'][0]  # osm_id
+        conn_point_shp = Point(street_graph.nodes[conn_node]['x'], street_graph.nodes[conn_node]['y'])
+
+        # target MV line does currently not connect a load area of type aggregated
+        if not target_obj['obj']['branch'].connects_aggregated:
+
+            # Node is close to line
+            # -> insert node into route (change existing route)
+            if (target_obj['dist'] < conn_dist_ring_mod):
+                # backup kind and type of branch
+                branch_type = graph.adj[adj_node1][adj_node2]['branch'].type
+                branch_kind = graph.adj[adj_node1][adj_node2]['branch'].kind
+                branch_ring = graph.adj[adj_node1][adj_node2]['branch'].ring
+
+                # find new line geoms connecting supply node with adjacent nodes
+                line_1_shp, line_1_length, line_1_path = get_shortest_path_shp_single_target(street_graph, adj_node1,
+                                                                                             node, return_path=True)
+                line_2_shp, line_2_length, line_2_path = get_shortest_path_shp_single_target(street_graph, adj_node2,
+                                                                                             node, return_path=True)
+
+                # check if there's a circuit breaker on current branch,
+                # if yes set new position between first node (adj_node1) and newly inserted node
+                circ_breaker = graph.adj[adj_node1][adj_node2]['branch'].circuit_breaker
+                if circ_breaker is not None:
+                    circ_breaker.geo_data = cut_line_by_distance(line_1_shp, 0.5, normalized=True)[0]
+
+                # split old ring main route into 2 segments (delete old branch and create 2 new ones
+                # along node)
+                graph.remove_edge(adj_node1, adj_node2)
+
+                branch = BranchDing0(geometry=line_1_shp,
+                                     length=line_1_length,
+                                     circuit_breaker=circ_breaker,
+                                     kind=branch_kind,
+                                     grid=mv_grid,
+                                     type=branch_type,
+                                     ring=branch_ring)
+                if circ_breaker is not None:
+                    circ_breaker.branch = branch
+                graph.add_edge(adj_node1, node, branch=branch)
+
+                graph.add_edge(adj_node2, node, branch=BranchDing0(geometry=line_2_shp,
+                                                                   length=line_2_length,
+                                                                   kind=branch_kind,
+                                                                   grid=mv_grid,
+                                                                   type=branch_type,
+                                                                   ring=branch_ring))
+                # update branch path dict
+                path_passed_osmids.update(dict.fromkeys([(node, adj_node1),
+                                                         (adj_node1, node)], line_1_path))
+                path_passed_osmids.update(dict.fromkeys([(node, adj_node2),
+                                                         (adj_node2, node)], line_2_path))
+                path_passed_osmids.pop(target_obj['obj']['adj_nodes'])
+
+                target_obj_result = 're-routed'
+
+                if debug:
+                    logger.debug('Ring main route modified to include '
+                                 'node {}'.format(node))
+
+            # Node is too far away from route
+            # => keep main route and create new line from node to (cable distributor on) route.
+            else:
+
+                # create cable distributor and add it to grid
+                cable_dist = MVCableDistributorDing0(geo_data=conn_point_shp,
+                                                     grid=mv_grid)
+                mv_grid.add_cable_distributor(cable_dist)
+
+                # split old branch into 2 segments (delete old branch and create 2 new ones along cable_dist)
+                # ===========================================================================================
+                # find new line geoms connecting supply node with adjacent nodes
+                # note: splittage also possible, but this way is more intuitive for obtaining path nodes
+
+                line_1_shp, line_1_length, line_1_path = get_shortest_path_shp_single_target(street_graph, adj_node1,
+                                                                                             conn_node, return_path=True)
+                line_2_shp, line_2_length, line_2_path = get_shortest_path_shp_single_target(street_graph, adj_node2,
+                                                                                             conn_node, return_path=True)
+
+                # check if there's a circuit breaker on current branch,
+                # if yes set new position between first node (adj_node1) and newly created cable distributor
+                circ_breaker = graph.adj[adj_node1][adj_node2]['branch'].circuit_breaker
+                if circ_breaker is not None:
+                    circ_breaker.geo_data = cut_line_by_distance(line_1_shp, 0.5, normalized=True)[0]
+
+                # backup kind and type of branch
+                branch_kind = graph.adj[adj_node1][adj_node2]['branch'].kind
+                branch_type = graph.adj[adj_node1][adj_node2]['branch'].type
+                branch_ring = graph.adj[adj_node1][adj_node2]['branch'].ring
+
+                graph.remove_edge(adj_node1, adj_node2)
+
+                branch = BranchDing0(geometry=line_1_shp,
+                                     length=line_1_length,
+                                     circuit_breaker=circ_breaker,
+                                     kind=branch_kind,
+                                     grid=mv_grid,
+                                     type=branch_type,
+                                     ring=branch_ring)
+                if circ_breaker is not None:
+                    circ_breaker.branch = branch
+                graph.add_edge(adj_node1, cable_dist, branch=branch)
+
+                graph.add_edge(adj_node2, cable_dist, branch=BranchDing0(geometry=line_2_shp,
+                                                                         length=line_2_length,
+                                                                         kind=branch_kind,
+                                                                         grid=mv_grid,
+                                                                         type=branch_type,
+                                                                         ring=branch_ring))
+
+                # add new branch for satellite (station to cable distributor)
+                # ===========================================================
+
+                # get default branch kind and type from grid to use it for new branch
+                branch_kind = mv_grid.default_branch_kind
+                branch_type = mv_grid.default_branch_type
+
+                graph.add_edge(node, cable_dist, branch=BranchDing0(geometry=target_obj['line_shp'],
+                                                                    length=target_obj['dist'],
+                                                                    kind=branch_kind,
+                                                                    grid=mv_grid,
+                                                                    type=branch_type,
+                                                                    ring=branch_ring))
+
+                # update branch path dict
+                path_passed_osmids.update(dict.fromkeys([(cable_dist, adj_node1),
+                                                         (adj_node1, cable_dist)], line_1_path))
+                path_passed_osmids.update(dict.fromkeys([(cable_dist, adj_node2),
+                                                         (adj_node2, cable_dist)], line_2_path))
+                path_passed_osmids.update(dict.fromkeys([(node, cable_dist),
+                                                         (cable_dist, node)], target_obj['line_path']))
+                path_passed_osmids.pop(target_obj['obj']['adj_nodes'])
+
+                # relabel osm graph and path dict
+                street_graph = nx.relabel_nodes(street_graph, {conn_node: str(cable_dist)})
+                for key, path in path_passed_osmids.items():
+                    if conn_node in path:
+                        idx = path.index(conn_node)
+                        path[idx] = str(cable_dist)
+
+                target_obj_result = cable_dist
+
+                # debug info
+                if debug:
+                    logger.debug('Nearest connection point for object {0} '
+                                 'is branch {1} (distance={2} m)'.format(
+                        node, target_obj['obj']['adj_nodes'], target_obj['dist']))
+
+    # node ist nearest connection point
+    else:
+
+        # what kind of node is to be connected? (which type is node of?)
+        #   LVLoadAreaCentreDing0: Connect to LVLoadAreaCentreDing0 only
+        #   LVStationDing0: Connect to LVLoadAreaCentreDing0, LVStationDing0 or MVCableDistributorDing0
+        #   GeneratorDing0: Connect to LVLoadAreaCentreDing0, LVStationDing0, MVCableDistributorDing0 or GeneratorDing0
+        if isinstance(node, LVLoadAreaCentreDing0):
+            valid_conn_objects = LVLoadAreaCentreDing0
+        elif isinstance(node, LVStationDing0):
+            valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0, MVCableDistributorDing0)
+        elif isinstance(node, MVLoadDing0):
+            valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0, MVCableDistributorDing0)
+        elif isinstance(node, GeneratorDing0):
+            valid_conn_objects = (
+            LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0, MVCableDistributorDing0, GeneratorDing0)
+        else:
+            raise ValueError('Oops, the node you are trying to connect is not a valid connection object')
+
+        # if target is Load Area centre or LV station, check if it belongs to a load area of type aggregated
+        # (=> connection not allowed)
+        if isinstance(target_obj['obj'], (LVLoadAreaCentreDing0, LVStationDing0, MVLoadDing0)):
+            target_is_aggregated = target_obj['obj'].lv_load_area.is_aggregated
+        else:
+            target_is_aggregated = False
+
+        # target node is not a load area of type aggregated
+        if isinstance(target_obj['obj'], valid_conn_objects) and not target_is_aggregated:
+            # get default branch kind and type from grid to use it for new branch
+            branch_kind = mv_grid.default_branch_kind
+            branch_type = mv_grid.default_branch_type
+
+            # get branch ring obj
+            branch_ring = mv_grid.get_ring_from_node(target_obj['obj'])
+
+            # add new branch for satellite (station to station)
+            graph.add_edge(node, target_obj['obj'], branch=BranchDing0(geometry=target_obj['line_shp'],
+                                                                       length=target_obj['dist'],
+                                                                       kind=branch_kind,
+                                                                       grid=mv_grid,
+                                                                       type=branch_type,
+                                                                       ring=branch_ring))
+
+            # update branch path dict
+            path_passed_osmids.update(dict.fromkeys([(node, target_obj['obj']),
+                                                    (target_obj['obj'], node)], target_obj['line_path']))
+
+            target_obj_result = target_obj['obj']
+
+    return target_obj_result, street_graph, path_passed_osmids
diff --git a/ding0/grid/mv_grid/mv_routing.py b/ding0/grid/mv_grid/mv_routing.py
index 88601eb6..99d10925 100644
--- a/ding0/grid/mv_grid/mv_routing.py
+++ b/ding0/grid/mv_grid/mv_routing.py
@@ -18,7 +18,7 @@
 from ding0.grid.mv_grid.models.models import Graph, Node
 from ding0.grid.mv_grid.util import util, data_input
 from ding0.grid.mv_grid.solvers import savings, local_search
-from ding0.tools.geo import calc_geo_dist, calc_geo_dist_matrix, calc_geo_centre_point
+from ding0.tools.geo import calc_geo_dist, calc_geo_dist_matrix, calc_geo_centre_point, calc_edge_geometry
 from ding0.tools import config as cfg_ding0
 from ding0.core.network.stations import *
 from ding0.core.structure.regions import LVLoadAreaCentreDing0
@@ -27,7 +27,7 @@
 import logging
 
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 def ding0_graph_to_routing_specs(graph):
@@ -57,20 +57,28 @@ def ding0_graph_to_routing_specs(graph):
     nodes_pos = {}
     nodes_agg = {}
 
+    load_area_center_agg = []
+
     # check if there are only load areas of type aggregated and satellite
-    # -> treat satellites as normal load areas (allow for routing)
+    # if only satellites and aggregated load area no routing will be done
+    # and satellite will be connected via stubs
+    # in case there is no aggregated load area available
+    # treat satellites as normal load areas (allow for routing)
     satellites_only = True
+    has_aggregated = False
     for node in graph.nodes():
         if isinstance(node, LVLoadAreaCentreDing0):
             if not node.lv_load_area.is_satellite and not node.lv_load_area.is_aggregated:
                 satellites_only = False
+            if node.lv_load_area.is_aggregated:
+                has_aggregated = True
 
     for node in graph.nodes():
         # station is LV station
         if isinstance(node, LVLoadAreaCentreDing0):
             # only major stations are connected via MV ring
             # (satellites in case of there're only satellites in grid district)
-            if not node.lv_load_area.is_satellite or satellites_only:
+            if not node.lv_load_area.is_satellite or (satellites_only and not has_aggregated):
                 # get demand and position of node
                 # convert node's demand to int for performance purposes and to avoid that node
                 # allocation with subsequent deallocation results in demand<0 due to rounding errors.
@@ -79,6 +87,8 @@ def ding0_graph_to_routing_specs(graph):
                 # get aggregation flag
                 if node.lv_load_area.is_aggregated:
                     nodes_agg[str(node)] = True
+                    # collect load area center of aggregated load area(s)
+                    load_area_center_agg.append(node)
                 else:
                     nodes_agg[str(node)] = False
 
@@ -96,6 +106,11 @@ def ding0_graph_to_routing_specs(graph):
     specs['MATRIX'] = calc_geo_dist_matrix(nodes_pos)
     specs['IS_AGGREGATED'] = nodes_agg
 
+    # remove aggregated load area center(s) from graph
+    for lac in load_area_center_agg:
+        if graph.has_node(lac):
+            graph.remove_node(lac)
+
     return specs
 
 
@@ -146,6 +161,12 @@ def routing_solution_to_ding0_graph(graph, solution):
 
                     # set circ breaker pos manually
                     circ_breaker_pos = 1
+            
+            # PAUL new: aggregated branch placement will be done in urban mv routing,
+            # therefor route to aggregated LA centre will be skipped an no branch build
+            if len(r._nodes) == 1:
+                if solution._problem._is_aggregated[r._nodes[0]._name]:
+                    continue
 
             # build edge list
             n1 = r._nodes[0:len(r._nodes)-1]
@@ -153,7 +174,8 @@ def routing_solution_to_ding0_graph(graph, solution):
             edges = list(zip(n1, n2))
             edges.append((depot, r._nodes[0]))
             edges.append((r._nodes[-1], depot))
-
+            
+            
             # create MV Branch object for every edge in `edges`
             mv_branches = [BranchDing0(grid=depot_node.grid) for _ in edges]
             edges_with_branches = list(zip(edges, mv_branches))
@@ -182,7 +204,7 @@ def routing_solution_to_ding0_graph(graph, solution):
                     node2 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][0].name()]):
                 branch = mv_branches[circ_breaker_pos - 1]
                 circ_breaker = CircuitBreakerDing0(grid=depot_node.grid, branch=branch,
-                                                   geo_data=calc_geo_centre_point(node1, node2))
+                                                   geo_data=calc_geo_centre_point(node1, node2, 3035))  # set calc for srid=3035
                 branch.circuit_breaker = circ_breaker
 
             # create new ring object for route
@@ -204,7 +226,9 @@ def routing_solution_to_ding0_graph(graph, solution):
                     node1.lv_load_area.ring = ring
 
                 # set branch length
-                b.length = calc_geo_dist(node1, node2)
+                # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+                b.geometry, b.length = calc_edge_geometry(node1, node2, srid=3035)
+                b.length = calc_geo_dist(node1, node2, srid=3035)  # new param: srid=3035
 
                 # set branch kind and type
                 # 1) default
diff --git a/ding0/grid/mv_grid/solvers/local_search.py b/ding0/grid/mv_grid/solvers/local_search.py
index 867ddfd9..16c733ef 100644
--- a/ding0/grid/mv_grid/solvers/local_search.py
+++ b/ding0/grid/mv_grid/solvers/local_search.py
@@ -29,7 +29,7 @@
 import logging
 
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
 class LocalSearchSolution(BaseSolution):
@@ -201,6 +201,12 @@ def operator_oropt(self, graph, solution, op_diff_round_digits, anim=None):
                             if length_new < length_best:
                                 length_best = length_new
                                 s_best, i_best, j_best = s, i, j
+
+                # fix floating point error, length and length_best have different number of
+                # decimal places (in some cases). round values in order to exit the while loop
+                length = round(length, 5)
+                length_best = round(length_best, 5)
+
                 if length_best < length:
                     tour = tour[0:i_best] + tour[i_best+s_best:j_best] + tour[i_best:i_best+s_best] + tour[j_best:n+1]
 
diff --git a/ding0/grid/mv_grid/tools.py b/ding0/grid/mv_grid/tools.py
index 9ae99721..2506b8bd 100644
--- a/ding0/grid/mv_grid/tools.py
+++ b/ding0/grid/mv_grid/tools.py
@@ -14,6 +14,8 @@
 
 
 from ding0.core.network.stations import LVStationDing0
+from ding0.core.network.loads import MVLoadDing0
+from ding0.core import MVCableDistributorDing0
 from ding0.core.network import CableDistributorDing0, GeneratorDing0, LoadDing0
 from ding0.tools.geo import calc_geo_centre_point
 from ding0.tools import config as cfg_ding0
@@ -87,18 +89,18 @@ def relocate_circuit_breaker():
             # of neighboring nodes
             diff2 = abs(sum(node_peak_data[0:position+1]) -
                         sum(node_peak_data[position+1:len(node_peak_data)]))
-            if diff2 < diff_min:
+            if diff2 < diff_min or position == 0:
                 node2 = ring[position+1]
             else:
                 node2 = ring[position-1]
         else:
             node2 = ring[position-1]
-
         circ_breaker.branch = mv_grid.graph.adj[node_cb][node2]['branch']
         circ_breaker.branch_nodes = (node_cb, node2)
         circ_breaker.switch_node = node_cb
         circ_breaker.branch.circuit_breaker = circ_breaker
-        circ_breaker.geo_data = calc_geo_centre_point(node_cb, node2)
+        line_shp = circ_breaker.branch.geometry
+        circ_breaker.geo_data = cut_line_by_distance(line_shp, 0.5, normalized=True)[0] # PAUL new
 
 
     # get power factor for loads and generators
@@ -124,6 +126,10 @@ def relocate_circuit_breaker():
                 nodes_peak_load.append(node.peak_load / cos_phi_load)
                 nodes_peak_generation.append(
                     node.peak_generation / cos_phi_feedin)
+                
+            # node is LV station -> get peak load and peak generation
+            elif isinstance(node, MVLoadDing0):
+                nodes_peak_load.append(node.peak_load / cos_phi_load)
 
             # node is cable distributor -> get all connected nodes of subtree using graph_nodes_from_subtree()
             elif isinstance(node, CableDistributorDing0):
@@ -132,18 +138,18 @@ def relocate_circuit_breaker():
                 nodes_subtree_peak_generation = 0
 
                 for node_subtree in nodes_subtree:
-
+                    # node is MVLoad -> get peak load
+                    if isinstance(node, MVLoadDing0):
+                        nodes_subtree_peak_load += node_subtree.peak_load / cos_phi_load
                     # node is LV station -> get peak load and peak generation
-                    if isinstance(node_subtree, LVStationDing0):
-                        nodes_subtree_peak_load += node_subtree.peak_load / \
-                                                   cos_phi_load
-                        nodes_subtree_peak_generation += node_subtree.peak_generation / \
-                                                         cos_phi_feedin
-
+                    elif isinstance(node_subtree, LVStationDing0):
+                        nodes_subtree_peak_load += node_subtree.peak_load / cos_phi_load
+                        nodes_subtree_peak_generation += node_subtree.peak_generation / cos_phi_feedin
                     # node is LV station -> get peak load and peak generation
-                    if isinstance(node_subtree, GeneratorDing0):
-                        nodes_subtree_peak_generation += node_subtree.capacity / \
-                                                         cos_phi_feedin
+                    elif isinstance(node_subtree, GeneratorDing0):
+                        nodes_subtree_peak_generation += node_subtree.capacity / cos_phi_feedin
+                    else:
+                        TypeError("False node type!")
 
                 nodes_peak_load.append(nodes_subtree_peak_load)
                 nodes_peak_generation.append(nodes_subtree_peak_generation)
@@ -170,14 +176,13 @@ def relocate_circuit_breaker():
         diff_min = 10e9
 
         # if none of the nodes is of the type LVStation, a switch
-        # disconnecter will be installed anyways.
+        # disconnecter will be installed anyway.
         if any([isinstance(n, LVStationDing0) for n in ring]):
             has_lv_station = True
         else:
             has_lv_station = False
-            logging.debug("Ring {} does not have a LV station. "
-                          "Switch disconnecter is installed at arbitrary "
-                          "node.".format(ring))
+            logging.debug(f"Ring {ring} does not have a LV station. "
+                          f"Switch disconnecter is installed at arbitrary node.")
 
         # check where difference of demand/generation in two half-rings is minimal
         for ctr in range(len(node_peak_data)):
@@ -200,13 +205,568 @@ def relocate_circuit_breaker():
         relocate_circuit_breaker()
 
         if debug:
-            logger.debug('Ring: {}'.format(ring))
-            logger.debug('Circuit breaker {0} was relocated to edge {1} '
-                  '(position on route={2})'.format(
-                    circ_breaker, repr(circ_breaker.branch), position)
-                )
-            logger.debug('Peak load sum: {}'.format(sum(nodes_peak_load)))
-            logger.debug('Peak loads: {}'.format(nodes_peak_load))
+            logger.debug(f'Ring: {ring}')
+            logger.debug(f'Circuit breaker {circ_breaker} was relocated to edge '
+                         f'{repr(circ_breaker.branch)} (position on route={position})')
+            logger.debug(f'Peak load sum: {sum(nodes_peak_load)}')
+            logger.debug(f'Peak loads: {nodes_peak_load}')
+
+from shapely.ops import linemerge
+from shapely.geometry import Point, LineString
+from ding0.grid.lv_grid.graph_processing import simplify_graph_adv, remove_unloaded_deadends, remove_parallels_and_loops
+import networkx as nx
+import pandas as pd
+
+
+def get_edge_tuples_from_path(G, path_list):
+    
+    if G is None:
+    
+        edge_path = [(path_list[i], path_list[i+1]) for i in range(len(path_list)-1)]
+        
+    else:
+
+        edge_path = [(path_list[i], path_list[i+1], next(iter(G.get_edge_data(path_list[i],path_list[i+1]).keys()))) for i in range(len(path_list)-1)]
+    
+    return edge_path
+
+
+def get_shortest_path_shp_single_target(osm_graph, node1, node2, return_path=False, nodes_as_str=True):
+    if nodes_as_str:
+        node1, node2 = str(node1), str(node2)
+
+    sp = nx.shortest_path(osm_graph, node1, node2, weight='length')
+    edge_path = get_edge_tuples_from_path(osm_graph, sp)
+    line_shp = linemerge([osm_graph.edges[edge]['geometry'] for edge in edge_path])
+
+    if line_shp.is_empty:  # in case of route_length == 1
+        line_shp = osm_graph.edges[node1, node2, next(iter(osm_graph.get_edge_data(node1, node2).keys()))]['geometry']  # TODO make universal
+
+    line_length = line_shp.length
+
+    if line_length == 0:
+        line_length = 1
+        logger.warning('Geo distance is zero, check objects positions. Distance is set to 1m')
+
+    if return_path:
+        return line_shp, line_length, sp
+    else:
+        return line_shp, line_length
+
+
+def get_shortest_path_shp_multi_target(G, source, targets):
+
+    _, line_path = nx.multi_source_dijkstra(G, targets, source, weight='length')
+    edge_path = get_edge_tuples_from_path(G, line_path)
+    line_shp = linemerge([G.edges[edge]['geometry'] for edge in edge_path])
+    line_length = line_shp.length
+
+    # make sure length is greater 1m
+    if line_length == 0:
+        line_length = 1
+        logger.warning('Geo distance is zero, check objects positions. Distance is set to 1m')
+
+    return line_shp, line_length, line_path
+
+
+def cut_line_by_distance(line, distance, normalized=True):
+    # inspried from shapely
+
+    if normalized:
+        distance = distance*line.length
+    # Cuts a line in two at a distance from its starting point
+    if distance <= 0.0 or distance >= line.length:
+        return [Point(line.coords[0]), line]
+    coords = list(line.coords)
+    for i, p in enumerate(coords):
+        pd = line.project(Point(p))
+        if pd == distance:
+            cp = line.interpolate(distance)
+            return [Point(cp),
+                LineString(coords[:i+1]),
+                LineString(coords[i:])]
+        if pd > distance:
+            cp = line.interpolate(distance)
+            return [Point(cp),
+                LineString(coords[:i] + [(cp.x, cp.y)]),
+                LineString([(cp.x, cp.y)] + coords[i:])]
+        
+
+def reduce_graph_for_dist_matrix_calc(graph, nodes_to_keep):
+    
+    '''
+    simplifies graph, until number of graph nodes between two iterations
+    is almost the same (ratio=0.95), keeps nodes_of_interest in graph
+    during simplification
+    returns graph
+
+    '''
+    
+    post_graph_no, pre_graph_no = 0, len(graph)
+    
+    while post_graph_no/pre_graph_no <= 0.95: 
+        pre_graph_no = len(graph)
+        graph = simplify_graph_adv(graph, nodes_to_keep)
+        graph = remove_parallels_and_loops(graph)
+        graph = remove_unloaded_deadends(graph, nodes_to_keep)
+        post_graph_no = len(graph)
+    
+    return graph
+
+
+def calc_street_dist_matrix(G, matrix_node_list):
+
+    node_list = matrix_node_list.copy()
+    node_list.extend(list(set(G.nodes()) - set(matrix_node_list)))
+
+    dm_floyd = nx.floyd_warshall_numpy(G, nodelist=node_list, weight='length')
+    df_floyd = pd.DataFrame(dm_floyd)
+    #reduce matrix
+    df_floyd = df_floyd.iloc[:len(matrix_node_list),:len(matrix_node_list)]
+    df_floyd = df_floyd.div(1000) # unit is km
+    df_floyd = df_floyd.round(3) # accuracy in m
+    # pass osmid nodes as indices
+    df_floyd = df_floyd.set_index(pd.Series(matrix_node_list))
+    df_floyd.columns = matrix_node_list
+    # transform matrix to dict
+    matrix_dict = df_floyd.to_dict('dict')
+    
+    return matrix_dict
+
+
+def conn_ding0_obj_to_osm_graph(osm_graph, ding0_obj, search_shp=None):
+
+    '''
+    connects ding0 object with shortest euclidic distance to existing osm graph node
+    if search_shp is not None but shapely Point, those coordinates are used for
+    finding nearest graph node
+    create new node and edges
+    osm_graph is of type MultiDiGraph
+    '''
+
+    # search_shp is None, take ding0 objects' shp
+    if not search_shp:
+        x,y = ding0_obj.geo_data.x, ding0_obj.geo_data.y
+    # search_shp is passed
+    else:
+        x,y = search_shp.x, search_shp.y
+
+    nn = ox.distance.nearest_nodes(osm_graph, x, y, return_dist=False)
+    line_shp = LineString([(osm_graph.nodes[nn]['x'], osm_graph.nodes[nn]['y']),
+                           (ding0_obj.geo_data.x, ding0_obj.geo_data.y)])
+    osm_graph.add_node(str(ding0_obj), x=ding0_obj.geo_data.x, y=ding0_obj.geo_data.y, node_type='synthetic')
+    osm_graph.add_edge(nn, str(ding0_obj), geometry=line_shp, length=line_shp.length) #missing: highway, osmid
+    osm_graph.add_edge(str(ding0_obj), nn, geometry=line_shp, length=line_shp.length) #missing: highway, osmid
+
+    return osm_graph
+
+
+
+# graph processing
+
+def get_core_graph(G):
+    C = ox.utils_graph.get_digraph(G, weight='length')
+    C.remove_edges_from(nx.selfloop_edges(C))
+    C = nx.k_core(C, k=3, core_number=None)
+    C = nx.MultiDiGraph(G.subgraph(C.nodes))
+
+    return C
+
+
+def get_stub_graph(osm_graph_red, core_graph):
+    stub_edges = [n for n in osm_graph_red.edges if n in core_graph.edges]
+    R = osm_graph_red.copy()
+    R.remove_edges_from(stub_edges)
+    R.remove_nodes_from(list(nx.isolates(R)))
+
+    return R
+
+def split_graph_by_core(street_graph, depot):
+
+    G = ox.utils_graph.get_digraph(street_graph, weight='length')
+    G.remove_edges_from(nx.selfloop_edges(G))
+
+    core_graph = nx.k_core(G, k=3, core_number=None)
+    core_graph = nx.MultiDiGraph(G.subgraph(core_graph.nodes))
+
+    core_edges = [n for n in street_graph.edges if n in core_graph.edges]
+    stub_graph = street_graph.copy()
+    stub_graph.remove_edges_from(core_edges)
+    stub_graph.remove_nodes_from(list(nx.isolates(stub_graph)))
+
+    # make sure mv_station node is connected to core graph
+    core_graph = conn_ding0_obj_to_osm_graph(core_graph, depot)
+    if stub_graph.has_node(str(depot)):
+        stub_graph.remove_node(str(depot))
+
+    return core_graph, stub_graph
+
+
+def update_graphs(core_graph, stub_graph, nodes_to_switch):
+
+    switch_graph = stub_graph.subgraph(nodes_to_switch)
+    core_graph = nx.compose(core_graph, switch_graph)
+    edges_to_swith = list(switch_graph.edges)
+
+    stub_graph.remove_edges_from(edges_to_swith)
+    stub_graph.remove_nodes_from(list(nx.isolates(stub_graph)))
+    stub_graph = nx.MultiDiGraph(stub_graph) # vielleicht nicht notwegnig, auch eher DiGraph
+    
+    return core_graph, stub_graph
+
+
+# stub processing
+
+
+# stores node data of stubs
+# comp being all nodes part of stub
+# root...root_node, load...nodes with demand, dist...nodes have function as cable_dist
+
+def create_stub_dict(stub_graph, root_nodes, node_list):
+    
+    node_set = set(node_list.keys())
+    
+    stub_dict = {}
+
+    if nx.number_weakly_connected_components(stub_graph):
+
+        for i, comp in enumerate(nx.weakly_connected_components(stub_graph)):
+
+            root_node_set = comp & root_nodes
+            load_node_set = comp & node_set - root_node_set
+            cable_dist_set = comp - root_node_set - load_node_set
+
+            stub_dict[i] = {}
+            stub_dict[i]['comp'] = comp
+            stub_dict[i]['root'] = list(root_node_set)[0] #next(iter())
+            stub_dict[i]['load'] = {node: int(node_list[node].peak_load / cfg_ding0.get('assumptions', 'cos_phi_load'))
+                                    for node in load_node_set} #{node: demand}
+            stub_dict[i]['dist'] = cable_dist_set
+            
+    return stub_dict
+
+
+def check_stub_criterion(stub_dict, stub_graph):
+
+    stubs_to_del_dict = {}
+    mod_stubs_list = []
+
+    for key, stub_data in stub_dict.items():
+
+        if not stub_data['load']: # delete stubs without load
+
+            stubs_to_del_dict[key] = stub_data['comp']
+
+        cum_stub_load = sum(stub_data['load'].values())
+
+        if cum_stub_load > cfg_ding0.get('mv_connect', 'load_area_sat_string_load_threshold'):
+
+            if len(stub_data['load']) == 1:
+
+                # save key and nodes in dict
+                stubs_to_del_dict[key] = stub_data['comp']
+
+            else: # stub is tree or stub with more than one edge
+
+                #build directed tree subgraph from stub_graph for analysis
+                root = stub_data['root']
+                tree = stub_graph.subgraph(stub_data['comp'])
+                tree = nx.dfs_tree(tree, root) #directed from root
+                deg_root = tree.degree[root]
+                deg_tree_max = max(dict(tree.degree).values())
+
+                if deg_tree_max <= 2 and deg_root == 1: #tree is stub with more than one edge
+
+                    load_nodes = list(tree)[1:]
+
+                    for n in load_nodes:
+                        cum_load = 0
+                        for inner_n in load_nodes:
+                            try:
+                                cum_load += stub_data['load'][inner_n]
+                            except KeyError:
+                                # ToDo: Find origin of problem that in load_nodes is not only loads
+                                logger.error(f"{inner_n} not in stub_data['load']")
+                        if cum_load <= cfg_ding0.get('mv_connect', 'load_area_sat_string_load_threshold'):
+                            comp = [root] + load_nodes
+                            mod_stubs_list.append(comp)
+                            break
+                        else:
+                            root = load_nodes[0]
+                            load_nodes = load_nodes[1:]
+
+                else: # tree is tree, get stubs from tree
+
+                    for node in list(tree.nodes)[1:]:
+
+                        subtree = nx.dfs_tree(tree, node)
+                        deg_subtree_max = max(dict(subtree.degree(list(subtree))).values())
+
+                        root = list(tree.predecessors(node))[0]
+                        deg_root = len(list((tree.successors(root)))) #tree.degree[root]
+
+                        #identify stubs by degree
+                        if deg_root >= 2 and deg_subtree_max <= 1:
+
+                            load_nodes = list(subtree)
+
+                            if not all(n in stub_data['load'] for n in load_nodes):
+                                root = load_nodes[0]
+                                load_nodes = [load_nodes[1:] for n in load_nodes if n not in stub_data['load']][0]
+
+                            for n in load_nodes:
+
+                                cum_load = sum([stub_data['load'][n] for n in load_nodes])
+                                if cum_load <= cfg_ding0.get('mv_connect', 'load_area_sat_string_load_threshold'):
+                                    comp = [root] + load_nodes
+                                    mod_stubs_list.append(comp)
+                                    break
+                                else:
+                                    root = load_nodes[0]
+                                    load_nodes = load_nodes[1:]
+
+                #remove old comp not satisfying stub criterion
+                stubs_to_del_dict[key] = stub_data['comp']
+
+    for key in stubs_to_del_dict.keys(): del stub_dict[key]
+
+    old_stub_nodes = {x for v in stubs_to_del_dict.values() for x in v}
+    mod_stub_roots = {x[0] for x in mod_stubs_list}
+    mod_stub_loads = {x for v in mod_stubs_list for x in v} - mod_stub_roots
+
+    nodes_to_switch = old_stub_nodes - mod_stub_loads
+
+
+    return mod_stubs_list, nodes_to_switch
+
+
+def update_stub_dict(stub_dict, mod_stubs_list, node_list):
+
+    for comp in mod_stubs_list:
+
+        root = comp[0]
+        load_node_set = comp[1:]
+
+        i = max(stub_dict.keys()) + 1 
+
+        stub_dict[i] = {}
+        stub_dict[i]['load'] = {node: int(node_list[node].peak_load / cfg_ding0.get('assumptions', 'cos_phi_load'))
+                                for node in load_node_set} #{node: demand}
+        stub_dict[i]['root'] = root
+        stub_dict[i]['comp'] = set(comp)
+        stub_dict[i]['dist'] = set()
+
+        i+=1 
+    
+    return stub_dict
+
+
+import geopandas as gpd
+from requests import get
+from shapely import wkb, wkt
+import osmnx as ox
+
+def get_mvgd_ids_for_city(osm_city_name):
+    
+    version='v0.4.5'
+
+    mvgds = get('https://openenergy-platform.org/api/v0/schema/grid/tables/ego_dp_mv_griddistrict/rows/?where=version='+version,)#+'&where=subst_id='+subst_id,)
+    gdf_mvgds = gpd.GeoDataFrame(mvgds.json())
+    gdf_mvgds['geom'] = gdf_mvgds['geom'].apply(lambda x: wkb.loads(x, hex=True))
+    gdf_mvgds = gdf_mvgds.set_geometry('geom')
+    gdf_mvgds.crs = 'epsg:3035'
+
+    city = ox.geocode_to_gdf(osm_city_name)
+    gdf_city = ox.project_gdf(city, to_crs='epsg:3035')
+
+    gdf_mvgds['poly_inside']  = gdf_mvgds.geom.apply(lambda x: gdf_city.geometry[0].contains(x.centroid))
+    gdf_mvgds_in_city = gdf_mvgds.loc[gdf_mvgds['poly_inside'] == True]
+
+    return gdf_mvgds_in_city['subst_id'].to_list()
+
+### functions for cable routing in settlements
+
+def relocate_cable_dists_settle(load_area, branches):
+
+    # returns list of relocated cable dists in load area
+    cable_dist_settle = set()
+
+    # find cable distributors inside load area
+    for branch in branches:
+        cd_in_load_area = [node for node in branch['adj_nodes'] if isinstance(node, MVCableDistributorDing0) \
+                           and node.geo_data.intersects(load_area.geo_area)]
+
+        cable_dist_settle.update(cd_in_load_area)
+
+    # if found, update cable distributors' coordinates by finding
+    # nearest node's position in street graph, update branch geoms as well
+    if cable_dist_settle:
+
+        # osm_ids allocated to ding0 supply nodes are unique, can't be used for cable dists
+        locked_osm_ids = set([mv_load.osmid_building for mv_load in load_area._mv_loads] +
+                             [lvgd.lv_grid._station.osm_id_node for lvgd in load_area._lv_grid_districts])
+
+        for cd in cable_dist_settle:
+
+            # get copy of street graph and remove locked osm ids
+            G = load_area.load_area_graph.copy()
+            G.remove_nodes_from(locked_osm_ids)
+
+            # get cable dists point geometry
+            cd_shp = cd.geo_data
+
+            # osm nodes are available for relocalisation
+            if not nx.is_empty(G):
+                osm_id = ox.nearest_nodes(G, cd_shp.x, cd_shp.y, return_dist=False)
+                osm_node_shp = Point([(G.nodes[osm_id]['x'], G.nodes[osm_id]['y'])])
+            # nos osm node available, add synthetic node to graph instead by using
+            # original cable distributor coordinates
+            else:
+                load_area.load_area_graph = conn_ding0_obj_to_osm_graph(load_area.load_area_graph, cd)
+                osm_id = str(cd)
+                osm_node_shp = cd_shp
+
+            # update cable distributors position
+            cd.geo_data = osm_node_shp
+            cd.osm_id_node = osm_id
+
+            # lock cable distributor's osm id , update locked osm ids set
+            locked_osm_ids.add(osm_id)
+
+            # update adjacent branch geoms
+            branches_upd = load_area.mv_grid_district.mv_grid.graph_branches_from_node(cd)
+
+            for node, branch in branches_upd:
+
+                line_shp_upd = LineString([node.geo_data, cd.geo_data])
+
+                branch['branch'].geometry = line_shp_upd
+
+                circ_breaker = branch['branch'].circuit_breaker
+                if circ_breaker is not None:
+                    circ_breaker.geo_data = cut_line_by_distance(line_shp_upd, 0.5, normalized=True)[0]
+
+    return list(cable_dist_settle)
+
+import networkx as nx
+
+def relabel_graph_nodes(load_area, cable_dists):
+    # get ding0 nodes
+    lv_stations = [lvgd.lv_grid._station for lvgd in load_area._lv_grid_districts]
+    mv_loads = [mv_load for mv_load in load_area._mv_loads]
+
+    # create mapping dicts: get nodes as mapping dicts {osmid: str(ding0_name)}
+    lv_stations_map = {lv_station.osm_id_node: str(lv_station) for lv_station in
+                       lv_stations}  # TODO: make osmid access consistent
+    mv_loads_map = {mv_load.osmid_building: str(mv_load) for mv_load in mv_loads}
+
+    if cable_dists:
+        cable_dists_map = {cd.osm_id_node: str(cd) for cd in cable_dists}
+        ding0_nodes_map = {**lv_stations_map, **mv_loads_map, **cable_dists_map}
+    else:
+        ding0_nodes_map = {**lv_stations_map, **mv_loads_map}
+
+    # update
+    street_graph = load_area.load_area_graph
+    osmids_to_str_map = {osmid: str(int(osmid)) for osmid in street_graph.nodes}
+    street_graph = nx.relabel_nodes(street_graph, {**osmids_to_str_map, **ding0_nodes_map})
+
+    return street_graph, ding0_nodes_map
+
+from shapely.geometry import MultiPoint, LinearRing, GeometryCollection
+from shapely.ops import nearest_points
+import numpy as np
+from ding0.core.network.stations import MVStationDing0
+
+def update_branch_shps_settle(load_area, branches, street_graph):
+    # update branches routing in settlement areas
+    # if both endpoints in la update whole shape
+    # if one endpoint in la update part of shape
+    # if no endpoints in la, do nothing (maybe check if part of linestring crosses same/other la
+
+    path_passed_osmids = {}
+
+    for branch in branches:
+
+        mv_station = [node for node in branch['adj_nodes'] if isinstance(node, MVStationDing0)]
+        G = street_graph
+
+        # add node to street graph, if not element of it
+        if mv_station:
+            if not G.has_node(str(mv_station[0])):
+                G = conn_ding0_obj_to_osm_graph(G, mv_station[0])
+
+        endpoints = [load_area.geo_area.intersects(node.geo_data) for node in branch['adj_nodes']]
+
+        # both branch endpoints are within load area - update whole branch geometry
+        if all(endpoints):
+
+            node1, node2 = branch['adj_nodes']
+            line_shp, line_length, path = get_shortest_path_shp_single_target(G, node1, node2, return_path=True)
+
+            branch['branch'].geometry = line_shp
+            branch['branch'].length = line_length
+
+            circ_breaker = branch['branch'].circuit_breaker
+            if circ_breaker is not None:
+                circ_breaker.geo_data = cut_line_by_distance(line_shp, 0.5, normalized=True)[0]
+
+            # fill branch path dict
+            path_passed_osmids.update(dict.fromkeys([(branch['adj_nodes'][0], branch['adj_nodes'][1]),
+                                                     (branch['adj_nodes'][1], branch['adj_nodes'][0])], path))
+
+        # one branch endpoint is within load area - update inner part of branch geometry
+        elif endpoints.count(True) == 1:
+
+            node_in = list(np.array(branch['adj_nodes'])[endpoints])[0]
+            node_out = list(set(branch['adj_nodes']) - {node_in})[0]
+            line_shp = branch['branch'].geometry
+            branch_coords = line_shp.coords[:]
+
+            # make sure branch shp is directed with origin inside of load area
+            if branch_coords[0] != node_in.geo_data.coords[0]:
+                branch_coords = list(reversed(branch_coords))
+                line_shp = LineString(branch_coords)
+
+            # retrieve intersection point(s) with load area polygon
+            ring_shp = LinearRing(load_area.geo_area.exterior.coords)
+            try:
+                intersect_shp = line_shp.intersection(ring_shp)
+            except:
+                from shapely.validation import explain_validity
+                print(explain_validity(line_shp))
+                from shapely.validation import make_valid
+                line_shp = make_valid(line_shp)
+                intersect_shp = line_shp.intersection(ring_shp)
+
+            # if no intersection shape found use representative point of line (rare case)
+            if intersect_shp.is_empty:
+                intersect_shp = line_shp.representative_point()
+
+            # in case of more than one intersection, find first intersection point
+            if not isinstance(intersect_shp, Point):
+                # in case shape is GeoCollection and has geometries apart from Points
+                if isinstance(intersect_shp, GeometryCollection):
+                    intersect_shp = MultiPoint([shp.representative_point() for shp in intersect_shp])
+                mp = MultiPoint(intersect_shp)
+                intersect_shp = nearest_points(mp, node_out.geo_data)[0]
+
+            # find nearest street graph node from intersection point
+            G = conn_ding0_obj_to_osm_graph(G, node_out, search_shp=intersect_shp)
+
+            # retrieve new geoemtry for branch
+            line_shp, line_length, path = get_shortest_path_shp_single_target(G, node_in, node_out, return_path=True)
+
+            branch['branch'].geometry = line_shp
+            branch['branch'].length = line_length
+
+            circ_breaker = branch['branch'].circuit_breaker
+            if circ_breaker is not None:
+                circ_breaker.geo_data = cut_line_by_distance(line_shp, 0.5, normalized=True)[0]
 
+            # fill branch path dict
+            path_passed_osmids.update(dict.fromkeys([(branch['adj_nodes'][0], branch['adj_nodes'][1]),
+                                                     (branch['adj_nodes'][1], branch['adj_nodes'][0])], path))
 
+    return path_passed_osmids, G
 
diff --git a/ding0/grid/mv_grid/tools_for_ways.py b/ding0/grid/mv_grid/tools_for_ways.py
new file mode 100644
index 00000000..8c746684
--- /dev/null
+++ b/ding0/grid/mv_grid/tools_for_ways.py
@@ -0,0 +1,51 @@
+import networkx as nx
+import pandas as pd
+
+from ding0.grid.lv_grid.graph_processing import simplify_graph_adv, remove_unloaded_deadends, remove_parallels_and_loops
+
+
+
+
+def reduce_graph_for_dist_matrix_calc(graph, nodes_to_keep):
+    
+    '''
+    simplifies graph, until number of graph nodes between two iterations
+    is almost the same (ratio=0.95), keeps nodes_of_interest in graph
+    during simplification
+    returns graph
+
+    '''
+    
+    post_graph_no, pre_graph_no = 0, len(graph)
+    
+    while post_graph_no/pre_graph_no <= 0.95: 
+        pre_graph_no = len(graph)
+        graph = simplify_graph_adv(graph, nodes_to_keep)
+        graph = remove_parallels_and_loops(graph)
+        graph = remove_unloaded_deadends(graph, nodes_to_keep)
+        post_graph_no = len(graph)
+    
+    return graph
+
+
+
+def calc_street_dist_matrix(G, matrix_node_list):
+
+    node_list = matrix_node_list.copy()
+    node_list.extend(list(set(G.nodes()) - set(matrix_node_list)))
+
+    dm_floyd = nx.floyd_warshall_numpy(G, nodelist=node_list, weight='length')
+    df_floyd = pd.DataFrame(dm_floyd)
+    #reduce matrix
+    df_floyd = df_floyd.iloc[:len(matrix_node_list),:len(matrix_node_list)]
+    df_floyd = df_floyd.div(1000) # unit is km
+    df_floyd = df_floyd.round(3) # accuracy in m
+    # pass osmid nodes as indices
+    df_floyd = df_floyd.set_index(pd.Series(matrix_node_list))
+    df_floyd.columns = matrix_node_list
+    # transform matrix to dict
+    matrix_dict = df_floyd.to_dict('dict')
+    
+    return matrix_dict
+
+
diff --git a/ding0/grid/mv_grid/urban_mv_connect.py b/ding0/grid/mv_grid/urban_mv_connect.py
new file mode 100644
index 00000000..a678b2b8
--- /dev/null
+++ b/ding0/grid/mv_grid/urban_mv_connect.py
@@ -0,0 +1,261 @@
+"""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"
+
+
+import time
+
+from ding0.tools import config as cfg_ding0
+from ding0.core.network.stations import *
+from ding0.core.structure.regions import LVLoadAreaCentreDing0
+from ding0.core.network.cable_distributors import MVCableDistributorDing0
+from ding0.core.network import RingDing0, BranchDing0, CircuitBreakerDing0
+import logging
+
+#PAUl new
+from ding0.grid.mv_grid.tools import cut_line_by_distance, get_shortest_path_shp_multi_target
+from shapely.ops import linemerge, split
+import networkx as nx
+import osmnx as ox
+from shapely.geometry import LineString, Point
+
+
+logger = logging.getLogger(__name__)
+
+def mv_urban_connect(mv_grid, osm_graph_red, core_graph, stub_graph, stub_dict, osmid_branch_dict):
+
+    node_list = {str(n):n for n in mv_grid.graph.nodes()}
+    routed_graph_node_set = set(osmid_branch_dict.keys())
+    forbidden_object_set = {n for n in routed_graph_node_set if n in node_list \
+                            and isinstance(node_list[n], (MVStationDing0, MVCableDistributorDing0))}
+    routed_graph_node_set = routed_graph_node_set - forbidden_object_set
+    root_nodes_to_remove = []
+
+    for key, stub_data in stub_dict.items():
+
+        # get root_node, loads (mv_station or mv_load) and cable distributor nodes in osm stub graph
+        load_node_set = set(stub_data['load'].keys())
+        cabledist_node_set = stub_data['dist']
+        root_node = stub_data['root']
+        comp = load_node_set.union({root_node}, cabledist_node_set)
+
+        # stub root node does not intersect with routed branches
+        if not root_node in osmid_branch_dict:
+
+            root_deg = stub_graph.out_degree(root_node)
+
+            # root node has branching, old root node will be cable dist, new root node intersect with routed branches
+            if root_deg > 1 and len(comp) > 2:
+                # in case root node has deg > 1, build new edge between old and new root
+                node = root_node 
+                if not node in node_list.keys():
+                    cabledist_node_set.add(node)
+
+                line_shp, line_length, path = get_shortest_path_shp_multi_target(osm_graph_red,
+                                                                                 node, routed_graph_node_set)
+
+                root_node = path[0]
+                stub_graph.add_node(root_node, x=osm_graph_red.nodes[root_node]['x'], y=osm_graph_red.nodes[root_node]['y'])
+                stub_graph.add_edge(root_node, node, geometry=line_shp)
+                comp.add(root_node)
+
+            else:
+                # in case deg = 1, delete old root and compute new edge from root neignbor to graph
+                # compute new edge geometry to ding0 graph 
+                root_nb = list({n for n in stub_graph.neighbors(root_node)} & set(comp))[0]
+                line_shp, line_length, path = get_shortest_path_shp_multi_target(osm_graph_red,
+                                                                                 root_nb, routed_graph_node_set)
+                #remove old root node from graph and component
+                stub_graph.remove_edge(root_node, root_nb)
+                comp.discard(root_node)
+                # introduce new root node and update edge
+                root_node = path[0]
+                stub_graph.add_node(root_node, x=osm_graph_red.nodes[root_node]['x'], y=osm_graph_red.nodes[root_node]['y'])
+                stub_graph.add_edge(root_node, root_nb, geometry=line_shp)
+                #print(root_node, root_nb)
+                comp.add(root_node)
+
+    #### SPLIT MAIN ROUTE EDGE INTO 2 SEGMENTS
+
+        # root node is not station or mv load
+        if not root_node in node_list.keys():
+
+            # get branch to split, due to possible overlapping branch geometries of different rings
+            # choose branch with lowest ring demand
+            # branch_to_split = min([(branch, branch.ring._demand) for branch in osmid_branch_dict[root_node]], key=lambda x: x[1])[0]
+            # print(branch_to_split.length, branch_to_split.ring)
+            if root_node in osmid_branch_dict: # in case of the same root node for different stubs just do the splittage one time
+
+                #add cable_dist  
+                root_shp = Point(core_graph.nodes[root_node]['x'], core_graph.nodes[root_node]['y']) #core_graph
+                cable_dist = MVCableDistributorDing0(geo_data=root_shp, grid=mv_grid)
+                mv_grid.add_cable_distributor(cable_dist)
+
+                # find branch to split in branch dict, do a shapely line.contains(point) check up 
+                # in order to ensure successful splittage #check up has to be done because the 2 
+                # new segements are added to the branch_dict for every iteration
+                root_branches = sorted([(branch, branch.ring._demand) for branch in osmid_branch_dict[root_node]], key=lambda x: x[1]) #TODO sort both values
+
+                i=0
+                while not root_branches[i][0].geometry.contains(root_shp):
+                    i+=1
+                branch_to_split = root_branches[i][0]
+
+                #get adjacent nodes
+                adj_node1, adj_node2 = mv_grid.graph_nodes_from_branch(branch_to_split)
+
+                # split branch geometry
+                shp_to_split = branch_to_split.geometry
+                line_shps = split(shp_to_split, root_shp)
+
+                if len(line_shps) == 2: # general case
+                # find respective geometry for adjacent nodes
+                    if adj_node1.geo_data == line_shps[0].boundary[0]:
+                        line_shp_1 = line_shps[0]
+                        line_shp_2 = line_shps[1]
+                    elif adj_node1.geo_data == line_shps[1].boundary[1]:
+                        line_shp_1 = line_shps[1]
+                        line_shp_2 = line_shps[0]
+                else: #workaround in case len(line_shps) == 1
+                    line_shp_1 = LineString([adj_node1.geo_data, root_shp])
+                    line_shp_2 = LineString([root_shp, adj_node2.geo_data])
+
+                # check if there's a circuit breaker on current branch,
+                # if yes set new position between first node (adj_node1) and newly created cable distributor
+                circ_breaker = branch_to_split.circuit_breaker
+                if circ_breaker is not None:
+                    circ_breaker.geo_data = cut_line_by_distance(line_shp_1, 0.5, normalized=True)[0]
+
+                branch_length = line_shp_1.length
+                branch1 = BranchDing0(geometry=line_shp_1,
+                                     length=branch_length,
+                                     circuit_breaker=circ_breaker,
+                                     kind=branch_to_split.kind,
+                                     grid=mv_grid,
+                                     type=branch_to_split.type,
+                                     ring=branch_to_split.ring)
+
+                if circ_breaker is not None:
+                    circ_breaker.branch = branch1
+
+                mv_grid.graph.add_edge(adj_node1, cable_dist, branch=branch1)
+
+                branch_length = line_shp_2.length
+                branch2=BranchDing0(geometry=line_shp_2,
+                                   length=branch_length,
+                                   kind=branch_to_split.kind,
+                                   grid=mv_grid,
+                                   type=branch_to_split.type,
+                                   ring=branch_to_split.ring)
+
+                mv_grid.graph.add_edge(adj_node2, cable_dist, branch=branch2)
+
+                # update branch dict: once a branch has been splitted -> remove,
+                # once a root_node has been used for splittage -> remove, add
+                # new resulting branches to branch_dict
+                for osmid, branch_set in osmid_branch_dict.items():
+                    if branch_to_split in branch_set:
+                        branch_set.discard(branch_to_split)
+                        branch_set.add(branch1)
+                        branch_set.add(branch2)
+
+                osmid_branch_dict.pop(root_node, None)
+
+                # split old ring main route into 2 segments (delete old branch and create 2 new ones
+                # along node)
+                mv_grid.graph.remove_edge(adj_node1, adj_node2)
+
+                root_node_ding0 = cable_dist
+                branch_ring = branch_to_split.ring
+
+                node_list[str(root_node_ding0)] = root_node_ding0
+
+        # root node is station or mvload
+        else: 
+            root_node_ding0 = node_list[root_node] #type, kind, ring, circuit_breaker from station (hopefully)
+            branch_ring = mv_grid.get_ring_from_node(root_node_ding0) # get branch ring obj
+
+        #### CREATE & ADD NEW STUB BRANCHES
+
+        # get default branch kind and type from grid to use it for new branch
+        # branch_ring parameter from above
+        branch_kind = mv_grid.default_branch_kind
+        branch_type = mv_grid.default_branch_type
+
+         # for simple stubs, there is just one load (one edge)
+        if len(comp) == 2: 
+
+            load_node = list(load_node_set)[0] #next(iter(
+            #edge_key = next(iter(stub_graph.get_edge_data(root_node, load_node).keys())) #TODO uncomment
+            #print(root_node, load_node)
+            branch_shp = stub_graph.edges[root_node, load_node, 0]['geometry']
+            branch_length = branch_shp.length
+
+            mv_grid.graph.add_edge(root_node_ding0, node_list[load_node], branch=BranchDing0(geometry=branch_shp,
+                                                                       length=branch_length,
+                                                                       kind=branch_kind,
+                                                                       grid=mv_grid,
+                                                                       type=branch_type,
+                                                                       ring=branch_ring))
+
+            # add demand to ring
+            branch_ring._demand += int(node_list[load_node].peak_load / cfg_ding0.get('assumptions', 'cos_phi_load'))
+
+        else: #for stubs having more than one edge
+
+                dist_mapping = {} #for converting osm names to ding0 names in comp_graph
+                dist_mapping[root_node] = str(root_node_ding0)
+
+                # add cable distributor for root, if is not station or load
+                # add cable distributor for branchings if necessary
+                #if not root_node in node_list.keys():
+                #    cabledist_node_set.add(root_node)
+
+                if len(cabledist_node_set):
+
+                    for node in cabledist_node_set:
+
+                        cable_dist_shp = Point(stub_graph.nodes[node]['x'], stub_graph.nodes[node]['y'])
+                        cable_dist = MVCableDistributorDing0(geo_data=cable_dist_shp, grid=mv_grid)
+                        mv_grid.add_cable_distributor(cable_dist)
+                        # relabel cable dist osmid in stub graph, node_list, component with ding0 name
+                        #comp.discard(node)
+                        #comp.add(str(cable_dist))
+                        dist_mapping[node] = str(cable_dist)
+                        #stub_graph = nx.relabel_nodes(stub_graph, {node:str(cable_dist)})
+                        node_list[str(cable_dist)] = cable_dist
+
+                # build subgraph from component in order to extract edges
+                comp_graph = nx.Graph(stub_graph.subgraph(comp)) #not stub_graph
+                comp_graph = nx.relabel_nodes(comp_graph, dist_mapping)
+
+                for edge in comp_graph.edges:
+
+                    #add branches of component
+                    n1, n2 = edge[0], edge[1]
+                    node1, node2 = node_list[n1], node_list[n2]
+                    branch_shp = comp_graph.edges[n1, n2]['geometry']
+                    branch_length = branch_shp.length
+
+                    mv_grid.graph.add_edge(node1, node2, branch=BranchDing0(geometry=branch_shp,
+                                                                           length=branch_length,
+                                                                           kind=branch_kind,
+                                                                           grid=mv_grid,
+                                                                           type=branch_type,
+                                                                           ring=branch_ring))
+
+                #add demand to ring
+                comp_demand = sum([node_list[n].peak_load for n in load_node_set]) / cfg_ding0.get('assumptions', 'cos_phi_load')
+                branch_ring._demand += int(comp_demand)
+        
+    return mv_grid
diff --git a/ding0/grid/mv_grid/urban_mv_routing.py b/ding0/grid/mv_grid/urban_mv_routing.py
new file mode 100644
index 00000000..71976e31
--- /dev/null
+++ b/ding0/grid/mv_grid/urban_mv_routing.py
@@ -0,0 +1,633 @@
+"""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"
+
+
+import time
+
+from ding0.grid.mv_grid.models.models import Graph, Node
+from ding0.grid.mv_grid.util import util, data_input
+from ding0.grid.mv_grid.solvers import savings, local_search
+from ding0.tools.geo import calc_geo_dist, calc_geo_dist_matrix, calc_geo_centre_point, calc_edge_geometry
+from ding0.tools import config as cfg_ding0
+from ding0.core.network.stations import *
+from ding0.core.structure.regions import LVLoadAreaCentreDing0
+from ding0.core.network import RingDing0, BranchDing0, CircuitBreakerDing0
+from ding0.core.network.cable_distributors import MVCableDistributorDing0
+import logging
+
+#PAUl new
+from ding0.grid.mv_grid.tools import get_edge_tuples_from_path, cut_line_by_distance, reduce_graph_for_dist_matrix_calc, \
+calc_street_dist_matrix, conn_ding0_obj_to_osm_graph, get_shortest_path_shp_single_target, \
+update_graphs, create_stub_dict, check_stub_criterion, update_stub_dict, split_graph_by_core, relabel_graph_nodes
+from shapely.ops import linemerge
+import networkx as nx
+import osmnx as ox
+from shapely.geometry import LineString
+#from ding0.tools.plots import plot_mv_topology
+from ding0.grid.mv_grid.urban_mv_connect import mv_urban_connect
+
+logger = logging.getLogger(__name__)
+
+def osm_graph_to_routing_specs_urban(agg_load_area, core_graph, depot_node, nodes_pos, nodes_demands):
+    
+    # agg_load_area, core_graph, depot_node, nodes_pos, nodes_demands 
+
+    G = core_graph
+
+    specs = {} 
+
+    nodes_demands = {str(k):v for k,v in nodes_demands.items() if str(k) in core_graph.nodes}
+    nodes_pos = {str(k):v for k,v in nodes_pos.items() if str(k) in core_graph.nodes}
+
+    #depot node
+    nodes_demands[str(depot_node)] = 0
+    nodes_pos[str(depot_node)] = (depot_node.geo_data.x, depot_node.geo_data.y)
+
+    # fill specs for routing
+    specs['DEPOT'] = str(depot_node)
+    specs['BRANCH_KIND'] = agg_load_area.mv_grid_district.mv_grid.default_branch_kind
+    specs['BRANCH_TYPE'] = agg_load_area.mv_grid_district.mv_grid.default_branch_type
+    specs['V_LEVEL'] = agg_load_area.mv_grid_district.mv_grid.v_level
+    specs['NODE_COORD_SECTION'] = nodes_pos
+    specs['DEMAND'] = nodes_demands
+    # nodes_agg excludes node from ring routing, therefore set to False
+    specs['IS_AGGREGATED'] = {k: False for k,v in nodes_demands.items() if k in core_graph.nodes}
+
+    # compute distance street matrix
+    matrix_node_list = list(nodes_demands.keys())
+    specs['MATRIX'] = calc_street_dist_matrix(core_graph, matrix_node_list)
+    
+    return specs
+
+
+def routing_solution_to_ding0_graph(mv_grid, core_graph, solution):
+    #mv_grid
+    """ Insert `solution` from routing into `graph`
+
+    Parameters
+    ----------
+    graph: :networkx:`NetworkX Graph Obj< >`
+    NetworkX graph object with nodes
+    solution: BaseSolution
+    Instance of `BaseSolution` or child class (e.g. `LocalSearchSolution`) (=solution from routing)
+
+    Returns
+    -------
+    :networkx:`NetworkX Graph Obj< >` 
+    NetworkX graph object with nodes and edges
+    """
+
+    osmid_branch_dict = {}
+
+    # build node dict (name: obj) from graph nodes to map node names on node objects
+    node_list = {str(n): n for n in mv_grid.graph.nodes()}
+
+    # add edges from solution to graph
+    try:
+        depot = solution._nodes[solution._problem._depot.name()]
+        depot_node = node_list[depot.name()]
+
+        for r in solution.routes():
+            circ_breaker_pos = None
+
+            # if route has only one node and is not aggregated, it wouldn't be possible to add two lines from and to
+            # this node (undirected graph of NetworkX). So, as workaround, an additional MV cable distributor is added
+            # at nodes' position (resulting route: HV/MV_subst --- node --- cable_dist --- HV/MV_subst.
+            if len(r._nodes) == 1:
+                if not solution._problem._is_aggregated[r._nodes[0]._name]:
+
+                    node = node_list[r._nodes[0]._name]
+                    # create new cable dist
+                    cable_dist = MVCableDistributorDing0(geo_data=node.geo_data,
+                                                         grid=mv_grid)
+                    mv_grid.add_cable_distributor(cable_dist)
+                    #add cable_dist to osm_graph
+                    core_graph.add_node(str(cable_dist), geometry=node.geo_data)
+                    core_graph.add_edge(str(cable_dist), str(node), 0, geometry=LineString([node.geo_data, node.geo_data]))
+                    core_graph.add_edge(str(node), str(cable_dist), 0, geometry=LineString([node.geo_data, node.geo_data]))
+
+                    # create new node (as dummy) an allocate to route r
+                    r.allocate([Node(name=repr(cable_dist), demand=0)])
+
+                    # add it to node list and allocated-list manually
+                    node_list[str(cable_dist)] = cable_dist
+                    solution._problem._is_aggregated[str(cable_dist)] = False
+
+                    # set circ breaker pos manually
+                    circ_breaker_pos = 1
+
+            # build edge list
+            n1 = r._nodes[0:len(r._nodes)-1]
+            n2 = r._nodes[1:len(r._nodes)]
+            edges = list(zip(n1, n2))
+            edges.insert(0, (depot, r._nodes[0]))
+            edges.append((r._nodes[-1], depot))
+
+            # create MV Branch object for every edge in `edges`
+            mv_branches = [BranchDing0(grid=mv_grid) for _ in edges]
+            edges_with_branches = list(zip(edges, mv_branches))
+
+            # recalculate circuit breaker positions for final solution, create it and set associated branch.
+            # if circ. breaker position is not set manually (routes with more than one load area, see above)
+            if not circ_breaker_pos:
+                circ_breaker_pos = r.calc_circuit_breaker_position()
+
+            node1 = node_list[edges[circ_breaker_pos - 1][0].name()]
+            node2 = node_list[edges[circ_breaker_pos - 1][1].name()]
+
+            # ALTERNATIVE TO METHOD ABOVE: DO NOT CREATE 2 BRANCHES (NO RING) -> LA IS CONNECTED AS SATELLITE
+            # IF THIS IS COMMENTED-IN, THE IF-BLOCK IN LINE 87 HAS TO BE COMMENTED-OUT
+            # See issue #114
+            # ===============================
+            # do not add circuit breaker for routes which are aggregated load areas or
+            # routes that contain only one load area
+            # if not (node1 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][1].name()] or
+            #         node2 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][0].name()] or
+            #         len(r._nodes) == 1):
+            # ===============================
+
+            # do not add circuit breaker for routes which are aggregated load areas
+            if not (node1 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][1].name()] or
+                    node2 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][0].name()]):
+                branch = mv_branches[circ_breaker_pos - 1]
+
+                line_shp = get_shortest_path_shp_single_target(core_graph, node1, node2)[0]
+
+                circ_breaker = CircuitBreakerDing0(grid=mv_grid, branch=branch,
+                                                   geo_data=cut_line_by_distance(line_shp, 0.5, normalized=True)[0])
+                branch.circuit_breaker = circ_breaker
+
+            # create new ring object for route
+            ring = RingDing0(grid=mv_grid)
+
+            #PAUL new ring_demand # demand is apparent power [kVA] 
+            ring._demand = sum([node.demand() for node in r._nodes])
+
+            # translate solution's node names to graph node objects using dict created before
+            # note: branch object is assigned to edge using an attribute ('branch' is used here), it can be accessed
+            # using the method `graph_edges()` of class `GridDing0`
+            edges_graph = []
+            for ((n1, n2), b) in edges_with_branches:
+                # get node objects
+                node1 = node_list[n1.name()]
+                node2 = node_list[n2.name()]
+
+                # set branch's ring attribute
+                b.ring = ring
+
+                # set LVLA's ring attribute
+                if isinstance(node1, LVLoadAreaCentreDing0):
+                    node1.lv_load_area.ring = ring
+
+                # set branch length
+                # PAUL new: add straight LineString as geometry to branch, replaces calc_geo_dist
+                #b.geometry, b.length = calc_edge_geometry(node1, node2, srid=3035)
+                #b.length = calc_geo_dist(node1, node2, srid=3035)  # new param: srid=3035
+
+                # calculate branch geometry and length for street courses
+                ##TODO introduce, new fct::
+                line_shp, line_length, sp = get_shortest_path_shp_single_target(core_graph, node1, node2, return_path=True)
+
+                b.geometry = line_shp
+                b.length = line_length
+
+                # set branch kind and type
+                # 1) default
+                b.kind = mv_grid.default_branch_kind
+                b.type = mv_grid.default_branch_type
+                # 2) aggregated load area types
+                if node1 == depot_node and solution._problem._is_aggregated[n2.name()]:
+                    b.connects_aggregated = True
+                    b.kind = mv_grid.default_branch_kind_aggregated
+                    b.type = mv_grid.default_branch_type_aggregated
+                elif node2 == depot_node and solution._problem._is_aggregated[n1.name()]:
+                    b.connects_aggregated = True
+                    b.kind = mv_grid.default_branch_kind_aggregated
+                    b.type = mv_grid.default_branch_type_aggregated
+
+                # append to branch list
+                edges_graph.append((node1, node2, dict(branch=b)))
+
+                #osmid_branch_dict saves branches overlapping with osmid, format osmid: [branches]
+                #PAUL new
+                for osmid in sp:
+                    if osmid in osmid_branch_dict: #set due to better performance while iterating later
+                        osmid_branch_dict[osmid].add(b)
+                    else:
+                        osmid_branch_dict[osmid] = {b}
+
+            # add branches to graph
+            mv_grid.graph.add_edges_from(edges_graph)
+
+    except:
+        logger.exception('unexpected error while converting routing solution to DING0 graph (NetworkX).')
+
+    return mv_grid, osmid_branch_dict
+
+
+def solve(mv_grid, debug=False, anim=None):
+
+    for load_area in mv_grid.grid_district._lv_load_areas:
+
+        if load_area.is_aggregated:
+
+            ##### initialize parameters for urban mv grid #####
+
+            # import required objects
+            mv_station = mv_grid._station
+            la, la_centre = load_area, load_area.lv_load_area_centre
+            supply_nodes = [mv_load for mv_load in la._mv_loads] + \
+                           [lvgd.lv_grid._station for lvgd in la._lv_grid_districts]
+
+            # nodes required to be supplied by urban mv grid
+            # get demand (apparent power) and position of supply nodes as dict {ding0_name: value}
+            nodes_pos = {node: (node.geo_data.x, node.geo_data.y) for node in supply_nodes}
+            nodes_demands = {node: int(node.peak_load /
+                                       cfg_ding0.get('assumptions', 'cos_phi_load')) for node in supply_nodes}
+
+            ##### prepare osm graph for routing and stub connections
+
+            # relabel street_graph
+            # 1. convert all osmids to str, 2. relabel supply_nodes' osmid with str(ding0_name)
+            street_graph, ding0_nodes_map = relabel_graph_nodes(la, cable_dists=None)
+
+            # add mv_station to street_graph
+            # reduce street_graph to the least necessary size (supply_nodes, mv_station are kept in graph)
+            street_graph = conn_ding0_obj_to_osm_graph(street_graph, mv_station)
+            street_graph = reduce_graph_for_dist_matrix_calc(street_graph,
+                                                             list(ding0_nodes_map.values()) + [str(mv_station)])
+
+            # split street_graph using k-core
+            core_graph, stub_graph = split_graph_by_core(street_graph, mv_station)
+
+            # 11. initial core and stub graph will be adapted based on stub criterion
+            # all lv stations / mv loads from stub graph surpassing a load of 1 MVA
+            # will be assigned to core graph
+            root_nodes = set(stub_graph.nodes) & set(core_graph.nodes)
+            node_list = {str(n):n for n in mv_grid.graph.nodes()}
+            # 11.1 create dictionary for every stub component of stub graph, 
+            # containing all involved nodes: {'comp': {all nodes}, 'root': node,
+            # 'load': {node: load}, 'dist': node}
+            stub_dict = create_stub_dict(stub_graph, root_nodes, node_list)
+            # 11.2 identify nodes to switch by stub criterion
+            mod_stubs_list, nodes_to_switch = check_stub_criterion(stub_dict, stub_graph)
+            # 11.3 update the stub dict such that just stubs exists fulfilling
+            # stub criterion
+            stub_dict = update_stub_dict(stub_dict, mod_stubs_list, node_list)
+            # 11.4 update graphs respectively
+            core_graph, stub_graph = update_graphs(core_graph, stub_graph, nodes_to_switch)
+
+            ##### urban mv routing
+
+            # 12. mv routing is done just for the core graph and related nodes
+            # stubs connection will be done later
+
+            # 12.1 prepare routing specs
+            specs = osm_graph_to_routing_specs_urban(la, core_graph, mv_station, nodes_pos, nodes_demands)
+            # 12.2 create routing graph and solver objects
+            RoutingGraph = Graph(specs)
+            savings_solver = savings.ClarkeWrightSolver()
+            # 12.3 solve problem
+            timeout = 300000
+            solution = savings_solver.solve(RoutingGraph, timeout)
+            # 12.4 transfer solution to ding0 graph
+            mv_grid, osmid_branch_dict = routing_solution_to_ding0_graph(mv_grid, core_graph, solution)
+            ##### urban mv connect
+            mv_grid = mv_urban_connect(mv_grid, street_graph, core_graph, stub_graph, stub_dict, osmid_branch_dict)
+            # remove load area centre (has been shifted to mv_routing)
+
+    return mv_grid.graph
+
+
+'''def solve(mv_grid, debug=False, anim=None):
+    # TODO: check docstring
+    """ Do MV routing for given nodes in `graph`.
+    
+    Translate data from node objects to appropriate format before.
+
+    Parameters
+    ----------
+    graph: :networkx:`NetworkX Graph Obj< >`
+        NetworkX graph object with nodes
+    debug: bool, defaults to False
+        If True, information is printed while routing
+    anim: AnimationDing0
+        AnimationDing0 object
+
+    Returns
+    -------
+    :networkx:`NetworkX Graph Obj< >`
+        NetworkX graph object with nodes and edges
+        
+    See Also
+    --------
+    ding0.tools.animation.AnimationDing0 : for a more detailed description on anim parameter.
+    """     
+    
+    ##### PAUL new: 
+    
+    agg_la_list=[]
+    # catch aggregated load area in MVGD
+    for load_area in mv_grid.grid_district._lv_load_areas:
+        if load_area.is_aggregated:
+            agg_load_area = load_area
+            agg_la_list.append(load_area)
+
+    if len(agg_la_list) > 1:
+        print('logger.warning')
+
+    # get osm street graph
+    osm_graph = agg_load_area.load_area_graph
+
+    # get mv station of MVGD
+    mv_station = mv_grid._station
+
+    #TODO create fct universally applicable, not just for MVStation
+    # add mv_station to graph
+    # get nearest node of mv_station and connect
+    osm_graph = conn_ding0_obj_to_osm_graph(osm_graph, mv_station)
+
+    #TODO: check distance between MVstation and Load area centre, create to LA centre if necessary, steps to make in connect generators
+    #remove unloaded stations from mv_grid
+    #remove load area centre, if no SPS and MVstation is in center
+    load_area_centre = agg_load_area.lv_load_area_centre
+
+    # get lv stations in agg load area as dict {osmid: lv_station} of strings
+    lv_stations = {str(lvgd.lv_grid._station.osm_id_node): str(lvgd.lv_grid._station) for lvgd in agg_load_area._lv_grid_districts}
+
+    # convert osm graph node names to str # rename osm graph nodes with ding0 name as str
+    nodes_to_str = {node: str(node) for node in osm_graph.nodes}
+    osm_graph = nx.relabel_nodes(osm_graph, nodes_to_str)
+    osm_graph = nx.relabel_nodes(osm_graph, lv_stations)
+
+    # prepare routing graph
+    # get nodes to keep
+    node_name_list = list(lv_stations.values())
+    node_name_list.append(str(mv_station))
+    #reduce graph to necessary size
+    osm_graph_red = reduce_graph_for_dist_matrix_calc(osm_graph, node_name_list)
+    
+    #####
+
+    # translate DING0 graph to routing specs
+    specs = osm_graph_to_routing_specs_urban(agg_load_area, osm_graph_red, mv_station)
+
+    # create routing graph using specs
+    RoutingGraph = Graph(specs)
+
+    timeout = 30000
+
+    # create solver objects
+    savings_solver = savings.ClarkeWrightSolver()
+    local_search_solver = local_search.LocalSearchSolver()
+
+    start = time.time()
+
+    # create initial solution using Clarke and Wright Savings methods
+    savings_solution = savings_solver.solve(RoutingGraph, timeout, debug, anim)
+
+    if debug:
+        logger.debug('ClarkeWrightSolver solution:')
+        util.print_solution(savings_solution)
+        logger.debug('Elapsed time (seconds): {}'.format(time.time() - start))
+        #savings_solution.draw_network()
+    
+    # PAUL new: start with return of savings solution
+    graph = routing_solution_to_ding0_graph(mv_grid, osm_graph_red, savings_solution)
+    
+    return graph'''
+
+'''
+def routing_solution_to_ding0_graph_old(mv_grid, osm_graph, solution):
+    """ Insert `solution` from routing into `graph`
+
+    Parameters
+    ----------
+    graph: :networkx:`NetworkX Graph Obj< >`
+    NetworkX graph object with nodes
+    solution: BaseSolution
+    Instance of `BaseSolution` or child class (e.g. `LocalSearchSolution`) (=solution from routing)
+
+    Returns
+    -------
+    :networkx:`NetworkX Graph Obj< >` 
+    NetworkX graph object with nodes and edges
+    """
+
+
+    # build node dict (name: obj) from graph nodes to map node names on node objects
+    node_list = {str(n): n for n in mv_grid.graph.nodes()}
+
+    # add edges from solution to graph
+    try:
+        depot = solution._nodes[solution._problem._depot.name()]
+        depot_node = node_list[depot.name()]
+        
+        for r in solution.routes():
+            circ_breaker_pos = None
+
+            # if route has only one node and is not aggregated, it wouldn't be possible to add two lines from and to
+            # this node (undirected graph of NetworkX). So, as workaround, an additional MV cable distributor is added
+            # at nodes' position (resulting route: HV/MV_subst --- node --- cable_dist --- HV/MV_subst.
+            if len(r._nodes) == 1:
+                if not solution._problem._is_aggregated[r._nodes[0]._name]:
+                    # create new cable dist
+                    cable_dist = MVCableDistributorDing0(geo_data=node_list[r._nodes[0]._name].geo_data,
+                                                         grid=mv_grid)
+                    mv_grid.add_cable_distributor(cable_dist)
+
+                    # create new node (as dummy) an allocate to route r
+                    r.allocate([Node(name=repr(cable_dist), demand=0)])
+
+                    # add it to node list and allocated-list manually
+                    node_list[str(cable_dist)] = cable_dist
+                    solution._problem._is_aggregated[str(cable_dist)] = False
+
+                    # set circ breaker pos manually
+                    circ_breaker_pos = 1
+
+            # build edge list
+            n1 = r._nodes[0:len(r._nodes)-1]
+            n2 = r._nodes[1:len(r._nodes)]
+            edges = list(zip(n1, n2))
+            edges.append((depot, r._nodes[0]))
+            edges.append((r._nodes[-1], depot))
+
+            # create MV Branch object for every edge in `edges`
+            mv_branches = [BranchDing0(grid=mv_grid) for _ in edges]
+            edges_with_branches = list(zip(edges, mv_branches))
+
+            # recalculate circuit breaker positions for final solution, create it and set associated branch.
+            # if circ. breaker position is not set manually (routes with more than one load area, see above)
+            if not circ_breaker_pos:
+                circ_breaker_pos = r.calc_circuit_breaker_position()
+
+            node1 = node_list[edges[circ_breaker_pos - 1][0].name()]
+            node2 = node_list[edges[circ_breaker_pos - 1][1].name()]
+
+            # ALTERNATIVE TO METHOD ABOVE: DO NOT CREATE 2 BRANCHES (NO RING) -> LA IS CONNECTED AS SATELLITE
+            # IF THIS IS COMMENTED-IN, THE IF-BLOCK IN LINE 87 HAS TO BE COMMENTED-OUT
+            # See issue #114
+            # ===============================
+            # do not add circuit breaker for routes which are aggregated load areas or
+            # routes that contain only one load area
+            # if not (node1 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][1].name()] or
+            #         node2 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][0].name()] or
+            #         len(r._nodes) == 1):
+            # ===============================
+
+            # do not add circuit breaker for routes which are aggregated load areas
+            if not (node1 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][1].name()] or
+                    node2 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][0].name()]):
+                branch = mv_branches[circ_breaker_pos - 1]
+                
+                ##TODO introduce, new fct::
+                sp = nx.shortest_path(osm_graph, str(node1), str(node2), weight='length')
+                edge_path = get_edge_tuples_from_path(osm_graph, sp)
+                line_shp = linemerge([osm_graph.edges[edge]['geometry'] for edge in edge_path])
+
+                circ_breaker = CircuitBreakerDing0(grid=mv_grid, branch=branch, \
+                                                   geo_data=cut_line_by_distance(line_shp, 0.5, normalized=True)[0])
+                branch.circuit_breaker = circ_breaker
+
+            # create new ring object for route
+            ring = RingDing0(grid=mv_grid)
+
+            # translate solution's node names to graph node objects using dict created before
+            # note: branch object is assigned to edge using an attribute ('branch' is used here), it can be accessed
+            # using the method `graph_edges()` of class `GridDing0`
+            edges_graph = []
+            for ((n1, n2), b) in edges_with_branches:
+                # get node objects
+                node1 = node_list[n1.name()]
+                node2 = node_list[n2.name()]
+
+                # set branch's ring attribute
+                b.ring = ring
+                # set LVLA's ring attribute
+                if isinstance(node1, LVLoadAreaCentreDing0):
+                    node1.lv_load_area.ring = ring
+
+                # calculate branch geometry and length for straight LineStrings
+                # b.geometry, b.length = calc_edge_geometry(node1, node2, srid=3035)
+                # b.length = calc_geo_dist(node1, node2, srid=3035)  # new param: srid=3035
+                
+                # calculate branch geometry and length for street courses
+                sp = nx.shortest_path(osm_graph, n1.name(), n2.name(), weight='length')
+                edge_path = get_edge_tuples_from_path(osm_graph, sp)
+                line_shp = linemerge([osm_graph.edges[edge]['geometry'] for edge in edge_path])
+                b.geometry = line_shp
+                b.length = line_shp.length
+                
+                
+                # set branch kind and type
+                # 1) default
+                b.kind = mv_grid.default_branch_kind
+                b.type = mv_grid.default_branch_type
+                # 2) aggregated load area types
+                if node1 == depot_node and solution._problem._is_aggregated[n2.name()]:
+                    b.connects_aggregated = True
+                    b.kind = mv_grid.default_branch_kind_aggregated
+                    b.type = mv_grid.default_branch_type_aggregated
+                elif node2 == depot_node and solution._problem._is_aggregated[n1.name()]:
+                    b.connects_aggregated = True
+                    b.kind = mv_grid.default_branch_kind_aggregated
+                    b.type = mv_grid.default_branch_type_aggregated
+
+                # append to branch list
+                edges_graph.append((node1, node2, dict(branch=b)))
+
+            # add branches to graph
+            mv_grid.graph.add_edges_from(edges_graph)
+
+    except:
+        logger.exception(
+            'unexpected error while converting routing solution to DING0 graph (NetworkX).')
+
+    return mv_grid.graph
+
+
+
+
+
+### old:
+
+def ding0_graph_to_routing_specs(graph):
+    """ Build data dictionary from graph nodes for routing (translation)
+
+    Parameters
+    ----------
+    graph: :networkx:`NetworkX Graph Obj< >`
+        NetworkX graph object with nodes
+
+    Returns
+    -------
+    :obj:`dict`
+        Data dictionary for routing.
+        
+    See Also
+    --------
+    ding0.grid.mv_grid.models.models.Graph : for keys of return dict
+        
+    """
+    
+    # get power factor for loads
+    cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load')
+
+    specs = {}
+    nodes_demands = {}
+    nodes_pos = {}
+    nodes_agg = {}
+
+    # check if there are only load areas of type aggregated and satellite
+    # -> treat satellites as normal load areas (allow for routing)
+    satellites_only = True
+    for node in graph.nodes():
+        if isinstance(node, LVLoadAreaCentreDing0):
+            if not node.lv_load_area.is_satellite and not node.lv_load_area.is_aggregated:
+                satellites_only = False
+
+    for node in graph.nodes():
+        # station is LV station
+        if isinstance(node, LVLoadAreaCentreDing0):
+            # only major stations are connected via MV ring
+            # (satellites in case of there're only satellites in grid district)
+            if not node.lv_load_area.is_satellite or satellites_only:
+                # get demand and position of node
+                # convert node's demand to int for performance purposes and to avoid that node
+                # allocation with subsequent deallocation results in demand<0 due to rounding errors.
+                nodes_demands[str(node)] = int(node.lv_load_area.peak_load / cos_phi_load)
+                nodes_pos[str(node)] = (node.geo_data.x, node.geo_data.y)
+                # get aggregation flag
+                if node.lv_load_area.is_aggregated:
+                    nodes_agg[str(node)] = True
+                else:
+                    nodes_agg[str(node)] = False
+
+        # station is MV station
+        elif isinstance(node, MVStationDing0):
+            nodes_demands[str(node)] = 0
+            nodes_pos[str(node)] = (node.geo_data.x, node.geo_data.y)
+            specs['DEPOT'] = str(node)
+            specs['BRANCH_KIND'] = node.grid.default_branch_kind
+            specs['BRANCH_TYPE'] = node.grid.default_branch_type
+            specs['V_LEVEL'] = node.grid.v_level
+
+    specs['NODE_COORD_SECTION'] = nodes_pos
+    specs['DEMAND'] = nodes_demands
+    specs['MATRIX'] = calc_geo_dist_matrix(nodes_pos)
+    specs['IS_AGGREGATED'] = nodes_agg
+
+    return specs
+'''
diff --git a/ding0/tools/config.py b/ding0/tools/config.py
index ff85d5ab..792ea64f 100644
--- a/ding0/tools/config.py
+++ b/ding0/tools/config.py
@@ -37,40 +37,48 @@
 __author__     = "nesnoj, gplssm"
 
 
-import os.path as path
-import ding0
+import configparser as cp
 import logging
+import os
+import shutil
 
+import ding0
 
-logger = logging.getLogger('ding0')
-
-try:
-    import configparser as cp
-except:
-    # to be compatible with Python2.7
-    import ConfigParser as cp
+logger = logging.getLogger(__name__)
 
 cfg = cp.RawConfigParser()
 _loaded = False
 
+
 def load_config(filename):
-    """ Read config file specified by `filename`
-    
+    """Read config file specified by `filename`
+
     Parameters
     ----------
     filename : :obj:`str`
         Description of filename
     """
     package_path = ding0.__path__[0]
-    FILE = path.join(package_path, 'config', filename)
+
+    if filename == "config_db_credentials.cfg":
+        file = os.path.join(
+            os.path.expanduser("~"), cfg.get("config", "config_dir"), "config", filename
+        )
+        if not os.path.isfile(file):
+            print("Set your database config in '{file}'.")
+            os.makedirs(os.path.dirname(file), exist_ok=True)
+            shutil.copy(os.path.join(package_path, "config", filename), file)
+    else:
+        file = os.path.join(package_path, "config", filename)
 
     try:
-        cfg.read(FILE)
+        cfg.read(file)
         global _loaded
         _loaded = True
     except:
         logger.exception("configfile not found.")
 
+
 def get(section, key):
     """Returns the value of a given key of a given section of the main
     config file.
diff --git a/ding0/tools/database.py b/ding0/tools/database.py
new file mode 100644
index 00000000..ace34fd7
--- /dev/null
+++ b/ding0/tools/database.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+
+import logging
+from contextlib import contextmanager
+
+import ding0
+from ding0.tools import config as cfg_ding0
+from egoio.tools import db
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+logger = logging.getLogger()
+
+package_path = ding0.__path__[0]
+cfg_ding0.load_config('config_db_tables.cfg')
+cfg_ding0.load_config('config_db_credentials.cfg')
+
+
+def get_database_type_from_config():
+    input_data = cfg_ding0.get("input_data_source", "input_data")
+    if input_data in ["model_draft", "versioned"]:
+        database = "oedb"
+    elif input_data == "local":
+        database = "local"
+    else:
+        raise ValueError("Selected not implemented database.")
+    return database
+
+
+def engine(overwrite_database=None):
+    """
+    Engine for local database.
+    """
+    if overwrite_database:
+        database = overwrite_database
+    else:
+        database = get_database_type_from_config()
+    if database == "oedb":
+        conn = db.connection(readonly=True)
+    elif database == "local":
+        conn = create_engine(
+            f"postgresql+psycopg2://{cfg_ding0.get('database_credentials', 'user')}:"
+            f"{cfg_ding0.get('database_credentials', 'password')}@{cfg_ding0.get('database_credentials', 'host')}:"
+            f"{int(cfg_ding0.get('database_credentials', 'port'))}/{cfg_ding0.get('database_credentials', 'name')}",
+            echo=False,
+        )
+    return conn
+
+
+@contextmanager
+def session_scope(overwrite_database=None):
+    """
+    Provide a transactional scope around a series of operations.
+    """
+    if overwrite_database:
+        database = overwrite_database
+    else:
+        database = get_database_type_from_config()
+    Session = sessionmaker(bind=engine(overwrite_database=database))
+    session = Session()
+    logger.info(f"Start session with database: {database}")
+    try:
+        yield session
+        session.commit()
+    except:
+        session.rollback()
+        raise
+    finally:
+        session.close()
diff --git a/ding0/tools/debug.py b/ding0/tools/debug.py
index 1f34bcc3..418b48b5 100644
--- a/ding0/tools/debug.py
+++ b/ding0/tools/debug.py
@@ -12,10 +12,35 @@
 __url__        = "https://github.com/openego/ding0/blob/master/LICENSE"
 __author__     = "nesnoj, gplssm"
 
+import os
 
 import ding0
 import os.path as path
 import networkx as nx
+import traceback
+
+from functools import wraps
+import logging
+
+logger = logging.getLogger(__name__)
+
+def log_errors(f):
+    """
+    Decorator object that logs every exception into the defined logger object.
+    """
+    @wraps(f)
+    def exception_wrapper(*args, **kwargs):
+        if os.environ.get("DING0_DEBUG") == "true":
+            return f(*args, **kwargs)
+        else:
+            try:
+                return f(*args, **kwargs)
+            except Exception as e:
+                args[0].network.message.append(str(traceback.format_exc()))
+                logger.warning(f"Can't plot: {kwargs['filename']}")
+                logger.exception(traceback.format_exc())
+
+    return exception_wrapper
 
 
 def compare_graphs(graph1, mode, graph2=None):
diff --git a/ding0/tools/egon_data_integration.py b/ding0/tools/egon_data_integration.py
new file mode 100644
index 00000000..408f2e11
--- /dev/null
+++ b/ding0/tools/egon_data_integration.py
@@ -0,0 +1,798 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+import pandas as pd
+import shapely.wkb
+from ding0.config.config_lv_grids_osm import get_config_osm
+from ding0.tools import config as cfg_ding0
+from geoalchemy2.shape import to_shape
+from sqlalchemy import Integer, func, cast
+
+
+def get_mv_data(orm, session, mv_grid_districts_no):
+    # build SQL query
+    grid_districts = (
+        session.query(
+            orm["orm_mv_grid_districts"].bus_id,
+            func.ST_AsText(orm["orm_mv_grid_districts"].geom).label("poly_geom"),
+            func.ST_AsText(orm["orm_mv_stations"].point).label("subs_geom"),
+        )
+        .join(
+            orm["orm_mv_stations"],
+            orm["orm_mv_grid_districts"].bus_id == orm["orm_mv_stations"].bus_id,
+        )
+        .filter(orm["orm_mv_grid_districts"].bus_id.in_(mv_grid_districts_no))
+        .filter(orm["version_condition_mvgd"])
+        .filter(orm["version_condition_mv_stations"])
+        .distinct()
+    )
+
+    # read MV data from db
+    mv_data = pd.read_sql_query(
+        grid_districts.statement, session.bind, index_col="bus_id"
+    )
+
+    return mv_data
+
+
+def get_lv_load_areas(orm, session, mv_grid_id):
+    # threshold: load area peak load, if peak load < threshold => disregard
+    # load area
+    lv_loads_threshold = cfg_ding0.get("mv_routing", "load_area_threshold")
+    mw2kw = 10**3  # load in database is in MW -> scale to kW
+    lv_nominal_voltage = cfg_ding0.get("assumptions", "lv_nominal_voltage")
+
+    # build SQL query
+    lv_load_areas_sqla = session.query(
+        orm["orm_lv_load_areas"].id.label("id_db"),
+        orm["orm_lv_load_areas"].zensus_sum.label("population"),
+        # orm["orm_lv_load_areas"].zensus_count.label("zensus_cnt"),
+        orm["orm_lv_load_areas"].area_ha.label("area"),
+        # orm["orm_lv_load_areas"].sector_area_agricultural,
+        # orm["orm_lv_load_areas"].sector_area_cts,
+        # orm["orm_lv_load_areas"].sector_area_industrial,
+        # orm["orm_lv_load_areas"].sector_area_residential,
+        # orm["orm_lv_load_areas"].sector_share_agricultural,
+        # orm["orm_lv_load_areas"].sector_share_cts,
+        # orm["orm_lv_load_areas"].sector_share_industrial,
+        # orm["orm_lv_load_areas"].sector_share_residential,
+        # orm["orm_lv_load_areas"].sector_count_agricultural,
+        # orm["orm_lv_load_areas"].sector_count_cts,
+        # orm["orm_lv_load_areas"].sector_count_industrial,
+        # orm["orm_lv_load_areas"].sector_count_residential,
+        # orm["orm_lv_load_areas"].nuts.label("nuts_code"),
+        func.ST_AsText(orm["orm_lv_load_areas"].geom).label("geo_area"),
+        func.ST_AsText(orm["orm_lv_load_areas"].geom_centre).label("geo_centre"),
+        orm["orm_lv_load_areas"].sector_peakload_residential.label(
+            "peak_load_residential"
+        ),
+        orm["orm_lv_load_areas"].sector_peakload_cts.label("peak_load_cts"),
+        orm["orm_lv_load_areas"].sector_peakload_industrial.label(
+            "peak_load_industrial"
+        ),
+        (orm["orm_lv_load_areas"].sector_peakload_residential/orm["orm_lv_load_areas"].sector_peakload_residential_2035).label("peak_load_residential_scaling_factor"),
+        (orm["orm_lv_load_areas"].sector_peakload_cts/orm["orm_lv_load_areas"].sector_peakload_cts_2035).label("peak_load_cts_scaling_factor"),
+        (orm["orm_lv_load_areas"].sector_peakload_industrial/orm["orm_lv_load_areas"].sector_peakload_industrial_2035).label("peak_load_industrial_scaling_factor"),
+    ).filter(
+        orm["orm_lv_load_areas"].bus_id == mv_grid_id,
+        orm["version_condition_la"],
+    )
+
+    # read data from db
+    lv_load_areas = pd.read_sql_query(
+        lv_load_areas_sqla.statement, session.bind, index_col="id_db"
+    )
+    peak_load_columns = [
+        "peak_load_residential",
+        "peak_load_cts",
+        "peak_load_industrial",
+    ]
+    lv_load_areas[peak_load_columns] = lv_load_areas[peak_load_columns].fillna(
+        value=0.0
+    )
+    lv_load_areas[peak_load_columns] = lv_load_areas[peak_load_columns] * mw2kw
+    lv_load_areas["peak_load"] = lv_load_areas[peak_load_columns].sum(axis="columns")
+    lv_load_areas = lv_load_areas[lv_load_areas["peak_load"] > lv_loads_threshold]
+    return lv_load_areas
+
+
+def get_egon_ways(orm, session, geo_area):
+    """
+    retrieve ways
+    """
+    query = session.query(
+        orm["osm_ways_with_segments"].osm_id,
+        orm["osm_ways_with_segments"].nodes,
+        orm["osm_ways_with_segments"].geom.label("geometry"),
+        orm["osm_ways_with_segments"].highway,
+        orm["osm_ways_with_segments"].length_segments,
+    ).filter(
+        func.ST_Intersects(
+            func.ST_GeomFromText(geo_area, get_config_osm("srid")),
+            orm["osm_ways_with_segments"].geom,
+        )
+    )
+    df = pd.read_sql(sql=query.statement, con=session.bind, index_col=None)
+    return df
+
+
+def get_egon_residential_buildings(orm, session, subst_id, load_area):
+    """
+    Get the residential bildings from egon-data. Buildings of orm["egon_map_zensus_mvgd_buildings"],
+    filtered by:
+        - sector == 'residential',
+        - electricity == true,
+        - bus_id == subst_id.
+    Two queries one for the osm, the other for the synthetic buildings.
+    Get the geometries from:
+        - osm -> orm["osm_buildings_filtered"]
+        - synthetic -> orm["osm_buildings_synthetic"]
+    Get the capacity/peak_load from orm["building_peak_loads"].
+    """
+    logger.debug("Get residential buildings by 'subst_id' from database.")
+
+    # TODO: which scenario should be taken?
+    scenario = "eGon2035"
+    sector = "residential"
+
+    query = (
+        session.query(
+            # orm["egon_map_zensus_mvgd_buildings"],
+            orm["egon_map_zensus_mvgd_buildings"].building_id,
+            orm["egon_map_zensus_mvgd_buildings"].sector,
+            # orm["osm_buildings_filtered"]
+            orm["osm_buildings_filtered"].geom_point.label("geometry"),
+            orm["osm_buildings_filtered"].geom_building.label("footprint"),
+        )
+        .join(
+            orm["osm_buildings_filtered"],
+            orm["egon_map_zensus_mvgd_buildings"].building_id
+            == orm["osm_buildings_filtered"].id,
+        )
+        .filter(
+            orm["egon_map_zensus_mvgd_buildings"].sector == sector,
+            # orm["egon_map_zensus_mvgd_buildings"].bus_id == subst_id,
+            orm["egon_map_zensus_mvgd_buildings"].electricity == True,
+            orm["egon_map_zensus_mvgd_buildings"].osm == True,
+            func.ST_Intersects(
+                func.ST_GeomFromText(load_area.geo_area, get_config_osm("srid")),
+                orm["osm_buildings_filtered"].geom_point,
+            ),
+        )
+    )
+    residential_buildings_osm_df = pd.read_sql(
+        sql=query.statement, con=query.session.bind, index_col=None
+    )
+
+    query = (
+        session.query(
+            # orm["egon_map_zensus_mvgd_buildings"],
+            orm["egon_map_zensus_mvgd_buildings"].building_id,
+            orm["egon_map_zensus_mvgd_buildings"].sector,
+            # orm["osm_buildings_synthetic"]
+            orm["osm_buildings_synthetic"].geom_point.label("geometry"),
+            orm["osm_buildings_synthetic"].geom_building.label("footprint"),
+        )
+        .join(
+            orm["osm_buildings_synthetic"],
+            orm["egon_map_zensus_mvgd_buildings"].building_id
+            == orm["osm_buildings_synthetic"].id.cast(Integer),
+        )
+        .filter(
+            orm["egon_map_zensus_mvgd_buildings"].sector == sector,
+            # orm["egon_map_zensus_mvgd_buildings"].bus_id == subst_id,
+            orm["egon_map_zensus_mvgd_buildings"].electricity == True,
+            orm["egon_map_zensus_mvgd_buildings"].osm == False,
+            func.ST_Intersects(
+                func.ST_GeomFromText(load_area.geo_area, get_config_osm("srid")),
+                orm["osm_buildings_synthetic"].geom_point,
+            ),
+        )
+    )
+    residential_buildings_synthetic_df = pd.read_sql(
+        sql=query.statement, con=query.session.bind, index_col=None
+    )
+
+    residential_buildings_df = pd.concat(
+        [residential_buildings_osm_df, residential_buildings_synthetic_df],
+        ignore_index=True,
+    )
+    residential_buildings_df["geometry"] = residential_buildings_df["geometry"].apply(
+        to_shape
+    )
+    residential_buildings_df["footprint"] = residential_buildings_df["footprint"].apply(
+        to_shape
+    )
+
+    if residential_buildings_df["building_id"].duplicated(keep=False).any():
+        raise ValueError(
+            "There are duplicated building_ids, for residential osm and synthetic buildings."
+        )
+
+    query = session.query(
+        orm["building_peak_loads"].building_id,
+        (orm["building_peak_loads"].peak_load_in_w / 1000).label("capacity"),
+    ).filter(
+        orm["building_peak_loads"].sector == "residential",
+        orm["building_peak_loads"].scenario == scenario,
+        orm["building_peak_loads"].building_id.in_(
+            residential_buildings_df["building_id"].values
+        ),
+    )
+
+    capacity_per_building_df = pd.read_sql(
+        sql=query.statement, con=query.session.bind, index_col=None
+    )
+
+    residential_buildings_df = pd.merge(
+        left=residential_buildings_df,
+        right=capacity_per_building_df,
+        left_on="building_id",
+        right_on="building_id",
+        how="left",
+    )
+    residential_buildings_df["capacity"] = (
+        residential_buildings_df["capacity"]
+        * load_area.peak_load_residential_scaling_factor
+    )
+    if not round(load_area.peak_load_residential) == round(
+        residential_buildings_df.capacity.sum()
+    ):
+        logger.error(
+            f"{load_area.peak_load_residential=} != {residential_buildings_df.capacity.sum()=}"
+        )
+
+    query = (
+        session.query(
+            orm["household_electricity_profile"].building_id.label("building_id"),
+            func.count(orm["household_electricity_profile"].building_id).label(
+                "number_households"
+            ),
+        )
+        .filter(
+            orm["household_electricity_profile"].building_id.in_(
+                residential_buildings_df["building_id"].values
+            )
+        )
+        .group_by(
+            orm["household_electricity_profile"].building_id,
+        )
+    )
+
+    apartments_per_building_df = pd.read_sql(
+        sql=query.statement, con=query.session.bind, index_col=None
+    )
+
+    residential_buildings_df = pd.merge(
+        left=residential_buildings_df,
+        right=apartments_per_building_df,
+        left_on="building_id",
+        right_on="building_id",
+        how="left",
+    )
+
+    return residential_buildings_df
+
+
+def get_egon_cts_buildings(orm, session, subst_id, load_area):
+    logger.debug("Get cts buildings by 'subst_id' from database.")
+
+    scenario = "eGon2035"
+    sector = "cts"
+
+    query = (
+        session.query(
+            # orm["egon_map_zensus_mvgd_buildings"],
+            orm["egon_map_zensus_mvgd_buildings"].building_id,
+            orm["egon_map_zensus_mvgd_buildings"].sector,
+            # orm["osm_buildings_filtered"]
+            orm["osm_buildings_filtered"].geom_point.label("geometry"),
+            orm["osm_buildings_filtered"].geom_building.label("footprint"),
+        )
+        .join(
+            orm["osm_buildings_filtered"],
+            orm["egon_map_zensus_mvgd_buildings"].building_id
+            == orm["osm_buildings_filtered"].id,
+        )
+        .filter(
+            orm["egon_map_zensus_mvgd_buildings"].sector == sector,
+            # orm["egon_map_zensus_mvgd_buildings"].bus_id == subst_id,
+            orm["egon_map_zensus_mvgd_buildings"].electricity == True,
+            orm["egon_map_zensus_mvgd_buildings"].osm == True,
+            func.ST_Intersects(
+                func.ST_GeomFromText(load_area.geo_area, get_config_osm("srid")),
+                orm["osm_buildings_filtered"].geom_point,
+            ),
+        )
+    )
+    cts_buildings_osm_df = pd.read_sql(
+        sql=query.statement, con=query.session.bind, index_col=None
+    )
+
+    query = (
+        session.query(
+            # orm["egon_map_zensus_mvgd_buildings"],
+            orm["egon_map_zensus_mvgd_buildings"].building_id,
+            orm["egon_map_zensus_mvgd_buildings"].sector,
+            # orm["osm_buildings_synthetic"]
+            orm["osm_buildings_synthetic"].geom_point.label("geometry"),
+            orm["osm_buildings_synthetic"].geom_building.label("footprint"),
+        )
+        .join(
+            orm["osm_buildings_synthetic"],
+            orm["egon_map_zensus_mvgd_buildings"].building_id
+            == orm["osm_buildings_synthetic"].id.cast(Integer),
+        )
+        .filter(
+            orm["egon_map_zensus_mvgd_buildings"].sector == sector,
+            # orm["egon_map_zensus_mvgd_buildings"].bus_id == subst_id,
+            orm["egon_map_zensus_mvgd_buildings"].electricity == True,
+            orm["egon_map_zensus_mvgd_buildings"].osm == False,
+            func.ST_Intersects(
+                func.ST_GeomFromText(load_area.geo_area, get_config_osm("srid")),
+                orm["osm_buildings_synthetic"].geom_point,
+            ),
+        )
+    )
+    cts_buildings_synthetic_df = pd.read_sql(
+        sql=query.statement, con=query.session.bind, index_col=None
+    )
+
+    cts_buildings_df = pd.concat(
+        [cts_buildings_osm_df, cts_buildings_synthetic_df],
+        ignore_index=True,
+    )
+    cts_buildings_df["geometry"] = cts_buildings_df["geometry"].apply(to_shape)
+    cts_buildings_df["footprint"] = cts_buildings_df["footprint"].apply(to_shape)
+
+    if cts_buildings_df["building_id"].duplicated(keep=False).any():
+        raise ValueError(
+            "There are duplicated building_ids, for residential osm and synthetic buildings."
+        )
+
+    # retrieve peak load of cts
+    cells_query = session.query(
+        orm["building_peak_loads"].building_id,
+        (orm["building_peak_loads"].peak_load_in_w / 1000).label("capacity"),
+    ).filter(
+        orm["building_peak_loads"].scenario == scenario,
+        orm["building_peak_loads"].sector == "cts",
+        orm["building_peak_loads"].building_id.in_(
+            cts_buildings_df["building_id"].values
+        ),
+    )
+    capacity_per_building_df = pd.read_sql(
+        sql=cells_query.statement, con=cells_query.session.bind, index_col=None
+    )
+
+    cts_buildings_df = pd.merge(
+        left=cts_buildings_df,
+        right=capacity_per_building_df,
+        left_on="building_id",
+        right_on="building_id",
+        how="left",
+    )
+    cts_buildings_df["capacity"] = (
+        cts_buildings_df["capacity"] * load_area.peak_load_cts_scaling_factor
+    )
+    if not round(load_area.peak_load_cts) == round(cts_buildings_df.capacity.sum()):
+        logger.error(
+            f"{load_area.peak_load_cts=} != {cts_buildings_df.capacity.sum()=}"
+        )
+
+    return cts_buildings_df
+
+
+def get_egon_industrial_buildings(orm, session, subst_id, load_area):
+    logger.debug("Get industrial buildings by 'subst_id' from database.")
+    # Industrial loads 1
+    # demand.egon_sites_ind_load_curves_individual, geom from demand.egon_industrial_sites
+    # Filter: voltage level, scenario, subst_id
+    scn_name = "eGon2021"
+    mw2kw = 10**3
+    query = (
+        session.query(
+            (orm["egon_sites_ind_load_curves_individual"].peak_load * mw2kw).label(
+                "capacity"
+            ),
+            func.ST_AsText(
+                func.ST_Transform(orm["egon_industrial_sites"].geom, 3035)
+            ).label("geometry"),
+            func.ST_AsText(
+                func.ST_Transform(orm["egon_industrial_sites"].geom, 3035)
+            ).label("footprint"),
+            orm["egon_sites_ind_load_curves_individual"].site_id,
+        )
+        .join(
+            orm["egon_sites_ind_load_curves_individual"],
+            orm["egon_sites_ind_load_curves_individual"].site_id
+            == orm["egon_industrial_sites"].id,
+        )
+        .filter(
+            # orm["egon_sites_ind_load_curves_individual"].bus_id == subst_id,
+            orm["egon_sites_ind_load_curves_individual"].scn_name == scn_name,
+            orm["egon_sites_ind_load_curves_individual"].voltage_level.in_(
+                [4, 5, 6, 7]
+            ),
+            func.ST_Intersects(
+                func.ST_GeomFromText(load_area.geo_area, get_config_osm("srid")),
+                func.ST_Transform(orm["egon_industrial_sites"].geom, 3035),
+            ),
+        )
+    )
+    industrial_loads_1_df = pd.read_sql(
+        sql=query.statement, con=session.bind, index_col=None
+    )
+
+    # Industrial loads 2
+    # demand.egon_osm_ind_load_curves_individual, geom from openstreetmap.osm_landuse
+    # Filter: voltage level, scenario, subst_id
+
+    query = (
+        session.query(
+            orm["egon_osm_ind_load_curves_individual"].peak_load.label("capacity"),
+            func.ST_AsText(func.ST_PointOnSurface(orm["osm_landuse"].geom)).label(
+                "geometry"
+            ),
+            func.ST_AsText(orm["osm_landuse"].geom).label("footprint"),
+            orm["egon_osm_ind_load_curves_individual"].osm_id,
+        )
+        .join(
+            orm["osm_landuse"],
+            orm["egon_osm_ind_load_curves_individual"].osm_id == orm["osm_landuse"].id,
+        )
+        .filter(
+            orm["egon_osm_ind_load_curves_individual"].bus_id == subst_id,
+            orm["egon_osm_ind_load_curves_individual"].scn_name == scn_name,
+            orm["egon_osm_ind_load_curves_individual"].voltage_level.in_([4, 5, 6, 7]),
+            func.ST_Intersects(
+                func.ST_GeomFromText(load_area.geo_area, get_config_osm("srid")),
+                orm["osm_landuse"].geom,
+            ),
+        )
+    )
+    industrial_loads_2_df = pd.read_sql(
+        sql=query.statement, con=session.bind, index_col=None
+    )
+
+    industrial_loads_1_df["site_id"] = industrial_loads_1_df["site_id"].astype(int)
+    industrial_loads_1_df["building_id"] = industrial_loads_1_df["site_id"]
+    industrial_loads_2_df["osm_id"] = industrial_loads_2_df["osm_id"].astype(int)
+    industrial_loads_2_df["building_id"] = industrial_loads_2_df["osm_id"]
+
+    industrial_buildings_df = pd.concat(
+        [industrial_loads_1_df, industrial_loads_2_df], ignore_index=True
+    )
+    industrial_buildings_df = industrial_buildings_df.rename(
+        columns={"site_id": "industrial_site_id", "osm_id": "industrial_osm_id"}
+    )
+    industrial_buildings_df["capacity"] = industrial_buildings_df["capacity"] * mw2kw
+    industrial_buildings_df["geometry"] = industrial_buildings_df["geometry"].apply(
+        shapely.wkt.loads
+    )
+    industrial_buildings_df["sector"] = "industrial"
+    if not round(load_area.peak_load_industrial) == round(
+        industrial_buildings_df.capacity.sum()
+    ):
+        logger.error(
+            f"{load_area.peak_load_industrial=} != {industrial_buildings_df.capacity.sum()=}"
+        )
+
+    return industrial_buildings_df
+
+
+def get_egon_buildings(orm, session, subst_id, load_area):
+    logger.info("Get buildings by 'subst_id' and 'load_area' from database.")
+
+    residential_buildings_df = get_egon_residential_buildings(
+        orm, session, subst_id, load_area
+    )
+    cts_buildings_df = get_egon_cts_buildings(orm, session, subst_id, load_area)
+    industrial_buildings_df = get_egon_industrial_buildings(
+        orm, session, subst_id, load_area
+    )
+
+    residential_buildings_df.drop(columns="sector", inplace=True)
+    residential_buildings_df.rename(
+        columns={
+            "capacity": "residential_capacity",
+            "geometry": "residential_geometry",
+            "footprint": "residential_footprint",
+        },
+        inplace=True,
+    )
+    cts_buildings_df.drop(columns="sector", inplace=True)
+    cts_buildings_df.rename(
+        columns={
+            "capacity": "cts_capacity",
+            "geometry": "cts_geometry",
+            "footprint": "cts_footprint",
+        },
+        inplace=True,
+    )
+    industrial_buildings_df.drop(
+        columns=["sector", "industrial_site_id", "industrial_osm_id"], inplace=True
+    )
+    industrial_buildings_df.rename(
+        columns={
+            "capacity": "industrial_capacity",
+            "geometry": "industrial_geometry",
+            "footprint": "industrial_footprint",
+        },
+        inplace=True,
+    )
+
+    buildings_df = pd.merge(
+        left=residential_buildings_df,
+        right=cts_buildings_df,
+        left_on="building_id",
+        right_on="building_id",
+        how="outer",
+    )
+
+    buildings_df = pd.merge(
+        left=buildings_df,
+        right=industrial_buildings_df,
+        left_on="building_id",
+        right_on="building_id",
+        how="outer",
+    )
+
+    if buildings_df.empty:
+        logger.error(f"No buildings in LoadArea {load_area.name}")
+        return buildings_df
+
+    buildings_df.fillna(
+        {
+            "number_households": 0,
+            "residential_capacity": 0,
+            "cts_capacity": 0,
+            "industrial_capacity": 0,
+        },
+        inplace=True,
+    )
+    buildings_df = buildings_df.astype({"building_id": int, "number_households": int})
+
+    columns_to_sum = ["residential_capacity", "cts_capacity", "industrial_capacity"]
+    buildings_df["capacity"] = buildings_df[columns_to_sum].sum(axis=1)
+
+    def reduce_geometry_columns(x):
+        # Check if all coordinates are the same
+        coordinates = x[x.notna()]
+        for i in range(1, coordinates.size):
+            if not coordinates.iat[i - 1].almost_equals(coordinates.iat[i]):
+                logger.error("Coordinates of buildings with the same id are not equal!")
+
+        return coordinates.iat[0]
+
+    geometry_columns = ["residential_geometry", "cts_geometry", "industrial_geometry"]
+    buildings_df["geometry"] = buildings_df[geometry_columns].apply(
+        reduce_geometry_columns, axis="columns"
+    )
+    buildings_df.drop(columns=geometry_columns, inplace=True)
+
+    footprint_columns = [
+        "residential_footprint",
+        "cts_footprint",
+        "industrial_footprint",
+    ]
+    buildings_df["footprint"] = buildings_df[footprint_columns].apply(
+        reduce_geometry_columns, axis="columns"
+    )
+    buildings_df.drop(columns=footprint_columns, inplace=True)
+
+    if not round(load_area.peak_load, -1) == round(buildings_df.capacity.sum(), -1):
+        logger.error(f"{load_area.peak_load=} != {buildings_df.capacity.sum()=}")
+    buildings_df.set_index("building_id", inplace=True)
+
+    return buildings_df
+
+
+def func_within(geom_a, geom_b, srid=3035):
+    return func.ST_Within(
+        func.ST_Transform(
+            geom_a,
+            srid,
+        ),
+        func.ST_Transform(
+            geom_b,
+            srid,
+        ),
+    )
+
+
+def func_intersect(geom_a, geom_b, srid=3035):
+    return func.ST_Intersects(
+        func.ST_Transform(
+            geom_a,
+            srid,
+        ),
+        func.ST_Transform(
+            geom_b,
+            srid,
+        ),
+    )
+
+
+def get_res_generators(orm, session, mv_grid_district):
+    srid = 3035  # new to calc distance matrix in step 6
+    subst_id = str(mv_grid_district.id_db)
+    geo_area = mv_grid_district.geo_data
+
+    # Get PV open space join weather cell id
+    query = (
+        session.query(
+            orm["generators_pv"].bus_id,
+            orm["generators_pv"].gens_id,
+            (orm["generators_pv"].capacity * 1000).label("electrical_capacity"),
+            orm["generators_pv"].voltage_level,
+            orm["weather_cells"].w_id,
+            func.ST_AsText(func.ST_Transform(orm["generators_pv"].geom, srid)).label(
+                "geom"
+            ),
+        )
+        .join(
+            orm["weather_cells"],
+            func_within(orm["generators_pv"].geom, orm["weather_cells"].geom),
+        )
+        .filter(
+            # orm["generators_pv"].bus_id == subst_id,
+            orm["generators_pv"].site_type == "Freifläche",
+            orm["generators_pv"].status == "InBetrieb",
+            orm["generators_pv"].voltage_level.in_([4, 5, 6, 7]),
+            func.ST_Intersects(
+                func.ST_GeomFromText(geo_area.wkt, get_config_osm("srid")),
+                func.ST_Transform(orm["generators_pv"].geom, srid),
+            ),
+        )
+    )
+    generators_pv_open_space_df = pd.read_sql(
+        sql=query.statement, con=session.bind, index_col=None
+    )
+    generators_pv_open_space_df["generation_type"] = "solar"
+    generators_pv_open_space_df["generation_subtype"] = "open_space"
+
+    # Get pv rooftop join geoms
+    osm_buildings_filtered = session.query(
+        orm["osm_buildings_filtered"].id.cast(Integer).label("building_id"),
+        func.ST_AsText(orm["osm_buildings_filtered"].geom_point).label("geom"),
+    )
+    osm_buildings_synthetic = session.query(
+        orm["osm_buildings_synthetic"].id.cast(Integer).label("building_id"),
+        func.ST_AsText(orm["osm_buildings_synthetic"].geom_point).label("geom"),
+    )
+    building_geoms = osm_buildings_filtered.union(osm_buildings_synthetic).subquery(
+        name="building_geoms"
+    )
+    query = (
+        session.query(
+            orm["generators_pv_rooftop"].bus_id,
+            orm["generators_pv_rooftop"].gens_id,
+            orm["generators_pv_rooftop"].building_id,
+            (orm["generators_pv_rooftop"].capacity * 1000).label("electrical_capacity"),
+            orm["generators_pv_rooftop"].voltage_level,
+            orm["generators_pv_rooftop"].weather_cell_id.label("w_id"),
+            building_geoms.c.geom,
+        )
+        .join(
+            building_geoms,
+            orm["generators_pv_rooftop"].building_id == building_geoms.c.building_id,
+        )
+        .filter(
+            # orm["generators_pv_rooftop"].bus_id == subst_id,
+            orm["generators_pv_rooftop"].scenario == "status_quo",
+            orm["generators_pv_rooftop"].voltage_level.in_([4, 5, 6, 7]),
+            func.ST_Intersects(
+                func.ST_GeomFromText(geo_area.wkt, get_config_osm("srid")),
+                func.ST_GeomFromText(building_geoms.c.geom, get_config_osm("srid")),
+            ),
+        )
+    )
+    generators_pv_rooftop_df = pd.read_sql(
+        sql=query.statement, con=session.bind, index_col=None
+    )
+    generators_pv_rooftop_df["generation_type"] = "solar"
+    generators_pv_rooftop_df["generation_subtype"] = "pv_rooftop"
+
+    # Get generators wind join weather cells
+    query = (
+        session.query(
+            orm["generators_wind"].bus_id,
+            orm["generators_wind"].gens_id,
+            (orm["generators_wind"].capacity * 1000).label("electrical_capacity"),
+            orm["generators_wind"].voltage_level,
+            orm["weather_cells"].w_id,
+            func.ST_AsText(func.ST_Transform(orm["generators_wind"].geom, srid)).label(
+                "geom"
+            ),
+        )
+        .join(
+            orm["weather_cells"],
+            func_within(orm["generators_wind"].geom, orm["weather_cells"].geom),
+        )
+        .filter(
+            # orm["generators_wind"].bus_id == subst_id,
+            orm["generators_wind"].site_type == "Windkraft an Land",
+            orm["generators_wind"].status == "InBetrieb",
+            orm["generators_wind"].voltage_level.in_([4, 5, 6, 7]),
+            func.ST_Intersects(
+                func.ST_GeomFromText(geo_area.wkt, get_config_osm("srid")),
+                func.ST_Transform(orm["generators_wind"].geom, srid),
+            ),
+        )
+    )
+    generators_wind_df = pd.read_sql(
+        sql=query.statement, con=session.bind, index_col=None
+    )
+    generators_wind_df["generation_type"] = "wind"
+
+    # Generators biomass
+    query = session.query(
+        orm["generators_biomass"].bus_id,
+        orm["generators_biomass"].gens_id,
+        (orm["generators_biomass"].capacity * 1000).label("electrical_capacity"),
+        orm["generators_biomass"].voltage_level,
+        func.ST_AsText(func.ST_Transform(orm["generators_biomass"].geom, srid)).label(
+            "geom"
+        ),
+    ).filter(
+        # orm["generators_biomass"].bus_id == subst_id,
+        orm["generators_biomass"].status == "InBetrieb",
+        orm["generators_biomass"].voltage_level.in_([4, 5, 6, 7]),
+        func.ST_Intersects(
+            func.ST_GeomFromText(geo_area.wkt, get_config_osm("srid")),
+            func.ST_Transform(orm["generators_biomass"].geom, srid),
+        ),
+    )
+    generators_biomass_df = pd.read_sql(
+        sql=query.statement, con=session.bind, index_col=None
+    )
+    generators_biomass_df["generation_type"] = "biomass"
+
+    # Generators water
+    query = session.query(
+        orm["generators_water"].bus_id,
+        orm["generators_water"].gens_id,
+        (orm["generators_water"].capacity * 1000).label("electrical_capacity"),
+        orm["generators_water"].voltage_level,
+        func.ST_AsText(func.ST_Transform(orm["generators_water"].geom, srid)).label(
+            "geom"
+        ),
+    ).filter(
+        # orm["generators_water"].bus_id == subst_id,
+        orm["generators_water"].status == "InBetrieb",
+        orm["generators_water"].voltage_level.in_([4, 5, 6, 7]),
+        func.ST_Intersects(
+            func.ST_GeomFromText(geo_area.wkt, get_config_osm("srid")),
+            func.ST_Transform(orm["generators_water"].geom, srid),
+        ),
+    )
+    generators_water_df = pd.read_sql(
+        sql=query.statement, con=session.bind, index_col=None
+    )
+    generators_water_df["generation_type"] = "water"
+
+    renewable_generators_df = pd.concat(
+        [
+            generators_pv_open_space_df,
+            generators_pv_rooftop_df,
+            generators_wind_df,
+            generators_biomass_df,
+            generators_water_df,
+        ],
+        ignore_index=True,
+    )
+    renewable_generators_df.rename(columns={"bus_id": "subst_id"}, inplace=True)
+    # define generators with unknown subtype as 'unknown'
+    renewable_generators_df["generation_subtype"].fillna(value="unknown", inplace=True)
+    # Overwrite subst_id of the data, to correct faulty data
+    renewable_generators_df["subst_id"] = int(subst_id)
+
+    return renewable_generators_df
+
+
+def get_conv_generators(orm, session, subst_id):
+    logger.warning("Database query of conventional is generators not integrated.")
+    conventional_generators_df = pd.DataFrame()
+    return conventional_generators_df
diff --git a/ding0/tools/geo.py b/ding0/tools/geo.py
index 9e1a10c3..35bccaed 100644
--- a/ding0/tools/geo.py
+++ b/ding0/tools/geo.py
@@ -24,11 +24,15 @@
     from shapely.geometry import LineString
     from shapely.ops import transform
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
 
-def calc_geo_branches_in_polygon(mv_grid, polygon, mode, proj):
-    """ Calculate geographical branches in polygon.
+def calc_geo_branches_in_polygon(mv_grid, polygon, mode, proj, srid=3035):
+    """
+    NEW PARAM srid=3035 to calculate calc_geo_branches_in_buffer.
+    Before srid=4326 was assumed.
+    
+    Calculate geographical branches in polygon.
 
     For a given `mv_grid` all branches (edges in the graph of the grid) are
     tested if they are in the given `polygon`. You can choose different modes
@@ -52,29 +56,47 @@ def calc_geo_branches_in_polygon(mv_grid, polygon, mode, proj):
         List of branches
 
     """
-
-    branches = []
-    polygon_shp = transform(proj, polygon)
-    for branch in mv_grid.graph_edges():
-        nodes = branch['adj_nodes']
-        branch_shp = transform(proj, LineString([nodes[0].geo_data, nodes[1].geo_data]))
-
-        # check if branches intersect with polygon if mode = 'intersects'
-        if mode == 'intersects':
-            if polygon_shp.intersects(branch_shp):
-                branches.append(branch)
-        # check if polygon contains branches if mode = 'contains'
-        elif mode == 'contains':
-            if polygon_shp.contains(branch_shp):
-                branches.append(branch)
-        # error
-        else:
-            raise ValueError('Mode is invalid!')
+    if srid == 3035:
+        branches = []
+        for branch in mv_grid.graph_edges():
+            branch_shp = branch['branch'].geometry
+            # check if branches intersect with polygon if mode = 'intersects'
+            if mode == 'intersects':
+                if polygon.intersects(branch_shp):
+                    branches.append(branch)
+            # check if polygon contains branches if mode = 'contains'
+            elif mode == 'contains':
+                if polygon.contains(branch_shp):
+                    branches.append(branch)
+            # error
+            else:
+                raise ValueError('Mode is invalid!')
+    else:
+        branches = []
+        polygon_shp = transform(proj, polygon)
+        for branch in mv_grid.graph_edges():
+            branch_shp = transform(proj, branch['branch'].geometry)
+
+            # check if branches intersect with polygon if mode = 'intersects'
+            if mode == 'intersects':
+                if polygon_shp.intersects(branch_shp):
+                    branches.append(branch)
+            # check if polygon contains branches if mode = 'contains'
+            elif mode == 'contains':
+                if polygon_shp.contains(branch_shp):
+                    branches.append(branch)
+            # error
+            else:
+                raise ValueError('Mode is invalid!')
     return branches
 
 
-def calc_geo_branches_in_buffer(node, mv_grid, radius, radius_inc, proj):
-    """ Determines branches in nodes' associated graph that are at least partly
+def calc_geo_branches_in_buffer(node, mv_grid, radius, radius_inc, proj, srid=3035):
+    """
+    NEW PARAM srid=3035 to calculate calc_geo_branches_in_buffer.
+    Before srid=4326 was assumed.
+    
+    Determines branches in nodes' associated graph that are at least partly
     within buffer of `radius` from `node`.
     
     If there are no nodes, the buffer is successively extended by `radius_inc`
@@ -100,22 +122,34 @@ def calc_geo_branches_in_buffer(node, mv_grid, radius, radius_inc, proj):
 
     """
 
-    branches = []
-
-    while not branches:
-        node_shp = transform(proj, node.geo_data)
-        buffer_zone_shp = node_shp.buffer(radius)
-        for branch in mv_grid.graph_edges():
-            nodes = branch['adj_nodes']
-            branch_shp = transform(proj, LineString([nodes[0].geo_data, nodes[1].geo_data]))
-            if buffer_zone_shp.intersects(branch_shp):
-                branches.append(branch)
-        radius += radius_inc
+    if srid == 3035:
+        branches = []
+
+        while not branches:
+            node_shp = node.geo_data
+            buffer_zone_shp = node_shp.buffer(radius)
+            for branch in mv_grid.graph_edges():
+                branch_shp = branch['branch'].geometry
+                if buffer_zone_shp.intersects(branch_shp):
+                    branches.append(branch)
+            radius += radius_inc
+
+    else:
+        branches = []
+
+        while not branches:
+            node_shp = transform(proj, node.geo_data)
+            buffer_zone_shp = node_shp.buffer(radius)
+            for branch in mv_grid.graph_edges():
+                branch_shp = transform(proj, branch['branch'].geometry)
+                if buffer_zone_shp.intersects(branch_shp):
+                    branches.append(branch)
+            radius += radius_inc
 
     return branches
 
 
-def calc_geo_dist(node_source, node_target):
+def calc_geo_dist(node_source, node_target, srid=3035):
     """ Calculates the geodesic distance between `node_source` and `node_target`
     incorporating the detour factor specified in :file:`ding0/ding0/config/config_calc.cfg`.
 
@@ -125,7 +159,7 @@ def calc_geo_dist(node_source, node_target):
         source node, member of GridDing0.graph
     node_target: LVStationDing0, GeneratorDing0, or CableDistributorDing0
         target node, member of GridDing0.graph
-
+    srid: defines crs. 4326 by default check config_misc.cfg
     Returns
     -------
     :any:`float`
@@ -134,9 +168,13 @@ def calc_geo_dist(node_source, node_target):
 
     branch_detour_factor = cfg_ding0.get('assumptions', 'branch_detour_factor')
 
-    # notice: geodesic takes (lat,lon)
-    branch_length = branch_detour_factor * geodesic((node_source.geo_data.y, node_source.geo_data.x),
-                                                    (node_target.geo_data.y, node_target.geo_data.x)).m
+    if srid == 4326:
+        # notice: geodesic takes (lat,lon)
+        branch_length = branch_detour_factor * geodesic((node_source.geo_data.y, node_source.geo_data.x),
+                                                        (node_target.geo_data.y, node_target.geo_data.x)).m
+    elif srid == 3035:
+        # NEU weil srid=4326 in config.misc.cfg
+        branch_length = LineString([node_source.geo_data, node_target.geo_data]).length * branch_detour_factor
 
     # ========= BUG: LINE LENGTH=0 WHEN CONNECTING GENERATORS ===========
     # When importing generators, the geom_new field is used as position. If it is empty, EnergyMap's geom
@@ -144,14 +182,13 @@ def calc_geo_dist(node_source, node_target):
     # line is 0. See issue #76
     if branch_length == 0:
         branch_length = 1
-        logger.warning('Geo distance is zero, check objects\' positions. '
-                       'Distance is set to 1m')
+        logger.warning('Geo distance is zero, check objects positions. Distance is set to 1m')
     # ===================================================================
 
     return branch_length
 
 
-def calc_geo_dist_matrix(nodes_pos):
+def calc_geo_dist_matrix(nodes_pos, srid=3035):
     """ Calculates the geodesic distance between all nodes in `nodes_pos` incorporating the detour factor in config_calc.cfg.
         
     For every two points/coord it uses geopy's geodesic function. As default ellipsoidal model of the earth WGS-84 is used.
@@ -186,22 +223,36 @@ def calc_geo_dist_matrix(nodes_pos):
     branch_detour_factor = cfg_ding0.get('assumptions', 'branch_detour_factor')
 
     matrix = {}
+    
+    if srid == 3035:
+        
+        for i in nodes_pos:
+            pos_origin = tuple(nodes_pos[i])  # pos_origin to pos_dest
+
+            matrix[i] = {}
+
+            for j in nodes_pos:
+                pos_dest = tuple(nodes_pos[j])
+                distance = LineString([pos_origin, pos_dest]).length * branch_detour_factor / 1000  # km
+                matrix[i][j] = distance
+        
+    else:  # ding0 default old
 
-    for i in nodes_pos:
-        pos_origin = tuple(nodes_pos[i])
+        for i in nodes_pos:
+            pos_origin = tuple(nodes_pos[i])
 
-        matrix[i] = {}
+            matrix[i] = {}
 
-        for j in nodes_pos:
-            pos_dest = tuple(nodes_pos[j])
-            # notice: geodesic takes (lat,lon), thus the (x,y)/(lon,lat) tuple is reversed
-            distance = branch_detour_factor * geodesic(tuple(reversed(pos_origin)), tuple(reversed(pos_dest))).km
-            matrix[i][j] = distance
+            for j in nodes_pos:
+                pos_dest = tuple(nodes_pos[j])
+                # notice: geodesic takes (lat,lon), thus the (x,y)/(lon,lat) tuple is reversed
+                distance = branch_detour_factor * geodesic(tuple(reversed(pos_origin)), tuple(reversed(pos_dest))).km
+                matrix[i][j] = distance
 
     return matrix
 
 
-def calc_geo_centre_point(node_source, node_target):
+def calc_geo_centre_point(node_source, node_target, srid=3035):
     """ Calculates the geodesic distance between `node_source` and `node_target`
     incorporating the detour factor specified in config_calc.cfg.
     
@@ -218,16 +269,54 @@ def calc_geo_centre_point(node_source, node_target):
         Distance in m.
     """
 
-    # WGS84 (conformal) to ETRS (equidistant) projection
-    proj_source = Transformer.from_crs("epsg:4326", "epsg:3035", always_xy=True).transform
-    # ETRS (equidistant) to WGS84 (conformal) projection
-    proj_target = Transformer.from_crs("epsg:3035", "epsg:4326", always_xy=True).transform
+    if srid == 3035:  # calc new approach
+        branch_shp = LineString([node_source.geo_data, node_target.geo_data])
+        distance = branch_shp.length
+        centre_point_shp = branch_shp.interpolate(distance/2)
+    else:
+        # WGS84 (conformal) to ETRS (equidistant) projection
+        proj_source = Transformer.from_crs("epsg:4326", "epsg:3035", always_xy=True).transform
+        # ETRS (equidistant) to WGS84 (conformal) projection
+        proj_target = Transformer.from_crs("epsg:3035", "epsg:4326", always_xy=True).transform
 
-    branch_shp = transform(proj_source, LineString([node_source.geo_data, node_target.geo_data]))
+        branch_shp = transform(proj_source, LineString([node_source.geo_data, node_target.geo_data]))
 
-    distance = geodesic((node_source.geo_data.y, node_source.geo_data.x),
-                        (node_target.geo_data.y, node_target.geo_data.x)).m
+        distance = geodesic((node_source.geo_data.y, node_source.geo_data.x),
+                            (node_target.geo_data.y, node_target.geo_data.x)).m
 
-    centre_point_shp = transform(proj_target, branch_shp.interpolate(distance/2))
+        centre_point_shp = transform(proj_target, branch_shp.interpolate(distance/2))
 
     return centre_point_shp
+
+
+def calc_edge_geometry(node_source, node_target, srid=3035):
+    """ returns straight edge geometry and related length between two nodes as LineString 
+
+    Parameters
+    ----------
+    node_source: LVStationDing0, GeneratorDing0, or CableDistributorDing0
+        source node, member of GridDing0.graph
+    node_target: LVStationDing0, GeneratorDing0, or CableDistributorDing0
+        target node, member of GridDing0.graph
+    srid: defines crs. 4326 by default check config_misc.cfg
+    Returns
+    -------
+    geometry: LineString,
+    :any:`float`
+        Distance in m
+    """
+
+    if srid == 3035:
+        branch_geometry = LineString([node_source.geo_data, node_target.geo_data])
+        branch_length = branch_geometry.length
+        
+    # ========= BUG: LINE LENGTH=0 WHEN CONNECTING GENERATORS ===========
+    # When importing generators, the geom_new field is used as position. If it is empty, EnergyMap's geom
+    # is used and so there are a couple of generators at the same position => length of interconnecting
+    # line is 0. See issue #76
+    if branch_length == 0:
+        branch_length = 1
+        logger.warning('Geo distance is zero, check objects positions. Distance is set to 1m')
+    # ===================================================================
+
+    return branch_geometry, branch_length
diff --git a/ding0/tools/logger.py b/ding0/tools/logger.py
index 7e5a2f7b..f402fd1e 100644
--- a/ding0/tools/logger.py
+++ b/ding0/tools/logger.py
@@ -66,7 +66,7 @@ def get_default_home_dir():
     return os.path.join(os.path.expanduser('~'), ding0_dir)
 
 
-def setup_logger(log_dir=None, loglevel=logging.DEBUG):
+def setup_logger(log_dir=None, filename="ding0.log", loglevel=logging.DEBUG):
     """
     Instantiate logger
 
@@ -74,6 +74,8 @@ def setup_logger(log_dir=None, loglevel=logging.DEBUG):
     ----------
     log_dir: :obj:`str`
         Directory to save log, default: ~/.ding0/logging/
+    filename: :obj:`str`
+        Name of log file, default: ding0.log
     loglevel: 
         Level of logger.
     """
@@ -85,26 +87,26 @@ def setup_logger(log_dir=None, loglevel=logging.DEBUG):
         log_dir = os.path.join(get_default_home_dir(), 'log')
 
     logger = logging.getLogger('ding0') # use filename as name in log
+    logger.propagate = False
     logger.setLevel(loglevel)
 
     # create a file handler
-    handler = logging.FileHandler(os.path.join(log_dir, 'ding0.log'))
+    handler = logging.FileHandler(os.path.join(log_dir, filename))
     handler.setLevel(logging.DEBUG)
     formatter = logging.Formatter(
-        '%(asctime)s-%(funcName)s-%(message)s (%(levelname)s)')
+        "%(asctime)s %(name)-35s %(levelname)-8s - %(funcName)s: %(message)s"
+    )
     handler.setFormatter(formatter)
 
     # create a stream handler (print to prompt)
     stream = logging.StreamHandler()
-    stream.setLevel(logging.INFO)
+    stream.setLevel(logging.DEBUG)
     stream_formatter = logging.Formatter(
-        '%(message)s (%(levelname)s)')
+        '%(name)-35s - %(levelname)8s: %(message)s ')
     stream.setFormatter(stream_formatter)
 
     # add the handlers to the logger
     logger.addHandler(handler)
     logger.addHandler(stream)
 
-    logger.info('########## New run of Ding0 issued #############')
-
     return logger
diff --git a/ding0/tools/plots.py b/ding0/tools/plots.py
index 70feac4b..5fbd7cf3 100644
--- a/ding0/tools/plots.py
+++ b/ding0/tools/plots.py
@@ -2,15 +2,19 @@
 import math
 import numpy as np
 import matplotlib.pyplot as plt
+from matplotlib.patches import Patch
 import networkx as nx
-from pyproj import Proj, transform
+from pyproj import Proj, transform, Transformer
 import logging
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 from ding0.tools.logger import get_default_home_dir
 from ding0.core.network.grids import MVGridDing0
+from ding0.config.config_lv_grids_osm import get_config_osm
 
-use_gpd = False
+from ding0.tools.debug import log_errors
+
+use_gpd = True
 use_ctx = False
 
 if 'READTHEDOCS' not in os.environ:
@@ -27,8 +31,9 @@
         use_ctx = False
 
 
-def plot_mv_topology(grid, subtitle='', filename=None, testcase='load',
-                     line_color=None, node_color='type',
+@log_errors
+def plot_mv_topology(grid, path = None, subtitle='', filename=None, testcase='load',
+                     line_color='ring', node_color='type',
                      limits_cb_lines=None, limits_cb_nodes=None,
                      background_map=True):
     """ Draws MV grid graph using networkx
@@ -37,6 +42,8 @@ def plot_mv_topology(grid, subtitle='', filename=None, testcase='load',
     ----------
     grid : :obj:`MVGridDing0`
         MV grid to plot.
+    path : :obj:`str`
+        Path to save the plot.
     subtitle : :obj:`str`
         Extend plot's title by this string.
     filename : :obj:`str`
@@ -45,7 +52,7 @@ def plot_mv_topology(grid, subtitle='', filename=None, testcase='load',
     testcase : :obj:`str`
         Defines which case is to be used. Refer to config_calc.cfg to see further
         assumptions for the cases. Possible options are:
-        
+
         * 'load' (default)
           Heavy-load-flow case
         * 'feedin'
@@ -53,6 +60,8 @@ def plot_mv_topology(grid, subtitle='', filename=None, testcase='load',
     line_color : :obj:`str`
         Defines whereby to choose line colors. Possible options are:
 
+        * 'ring'
+          Line color is set according to related ring of the line.
         * 'loading'
           Line color is set according to loading of the line in heavy load case.
           You can use parameter `limits_cb_lines` to adjust the color range.
@@ -80,7 +89,7 @@ def plot_mv_topology(grid, subtitle='', filename=None, testcase='load',
         adjust the colorbar to 90%..100% voltage.
         Default: None (min and max loading are used).
     background_map : bool, optional
-        If True, a background map is plotted (default: stamen toner light).
+        If True, a background map is plotted (default: carto b positron).
         The additional package `contextily` is needed for this functionality.
         Default: True
 
@@ -91,43 +100,49 @@ def plot_mv_topology(grid, subtitle='', filename=None, testcase='load',
 
     """
 
+    model_proj = get_config_osm('srid')
+
     def set_nodes_style_and_position(nodes):
 
         # TODO: MOVE settings to config
         # node types (name of classes)
         node_types = ['MVStationDing0',
                       'LVStationDing0',
+                      'MVLoadDing0',  # PAUL new
                       'LVLoadAreaCentreDing0',
                       'MVCableDistributorDing0',
                       'GeneratorDing0',
                       'GeneratorFluctuatingDing0',
                       'CircuitBreakerDing0',
                       'n/a']
-        
+
         # node styles
         colors_dict = {'MVStationDing0': '#f2ae00',
                        'LVStationDing0': 'grey',
+                       'MVLoadDing0': 'cyan',  # PAUL new
                        'LVLoadAreaCentreDing0': '#fffc3d',
-                       'MVCableDistributorDing0': '#000000',
+                       'MVCableDistributorDing0': '#ffa500',  # PAUL new'#000000',
                        'GeneratorDing0': '#00b023',
                        'GeneratorFluctuatingDing0': '#0078b0',
                        'CircuitBreakerDing0': '#c20000',
                        'n/a': 'orange'}
-        sizes_dict = {'MVStationDing0': 120,
-                      'LVStationDing0': 7,
-                      'LVLoadAreaCentreDing0': 30,
-                      'MVCableDistributorDing0': 5,
-                      'GeneratorDing0': 50,
-                      'GeneratorFluctuatingDing0': 50,
-                      'CircuitBreakerDing0': 50,
-                      'n/a': 5}
-        zindex_by_type = {'MVStationDing0': 16,
+        sizes_dict = {'MVStationDing0': 40,
+                      'LVStationDing0': 3,
+                      'MVLoadDing0': 3,  # PAUL new
+                      'LVLoadAreaCentreDing0': 25,
+                      'MVCableDistributorDing0': 3,
+                      'GeneratorDing0': 15,
+                      'GeneratorFluctuatingDing0': 15,
+                      'CircuitBreakerDing0': 15,
+                      'n/a': 3}
+        zindex_by_type = {'MVStationDing0': 3,
                           'LVStationDing0': 12,
-                          'LVLoadAreaCentreDing0': 11,
-                          'MVCableDistributorDing0': 13,
-                          'GeneratorDing0': 14,
-                          'GeneratorFluctuatingDing0': 14,
-                          'CircuitBreakerDing0': 15,
+                          'MVLoadDing0': 12,  # PAUL new
+                          'LVLoadAreaCentreDing0': 2,
+                          'MVCableDistributorDing0': 9,
+                          'GeneratorDing0': 2,
+                          'GeneratorFluctuatingDing0': 2,
+                          'CircuitBreakerDing0': 3,
                           'n/a': 10}
 
         # dict of node class names: list of nodes
@@ -153,48 +168,92 @@ def set_nodes_style_and_position(nodes):
                 node_sizes_by_type['all'].append(sizes_dict['n/a'])
             nodes_pos[n] = (n.geo_data.x, n.geo_data.y)
 
-        return node_types, nodes_by_type, node_colors_by_type,\
+        return node_types, nodes_by_type, node_colors_by_type, \
                node_sizes_by_type, zindex_by_type, nodes_pos
 
-    def reproject_nodes(nodes_pos, model_proj='4326'):
-        inProj = Proj(init='epsg:{srid}'.format(srid=model_proj))
-        outProj = Proj(init='epsg:3857')
+    def reproject_nodes(nodes_pos, model_proj=str(model_proj)):  # changed from 4326 to new CRS 3035
+        transformer = Transformer.from_crs(int(model_proj), 3857, always_xy=True)
         nodes_pos2 = {}
         for k, v in nodes_pos.items():
-            x2, y2 = transform(inProj, outProj,
-                               v[0],
-                               v[1])
-            nodes_pos2[k] = (x2, y2)
+            nodes_pos2[k] = (transformer.transform(v[0], v[1]))
         return nodes_pos2
 
     def plot_background_map(ax):
-        url = ctx.sources.ST_TONER_LITE
+        url = ctx.providers.CartoDB.Positron
         xmin, xmax, ymin, ymax = ax.axis()
         basemap, extent = ctx.bounds2img(xmin, ymin, xmax, ymax,
-                                         zoom=12, url=url)
+                                         source=url)
         ax.imshow(basemap, extent=extent, interpolation='bilinear', zorder=0)
         ax.axis((xmin, xmax, ymin, ymax))
 
+    # regions style by type
+    mvgd_style = {'facecolor': '#ffffff', 'alpha': 1, 'edgecolor': '#fdcc02', 'linewidth': 1, 'zorder': 1}
+
+    if not plot_background_map:
+        alpha = 1
+    else:
+        alpha = 0.4
+
+    la_style_sat = {'facecolor': '#ffffde', 'alpha': alpha, 'edgecolor': 'k', 'linewidth': 0.2, 'zorder': 1}
+    la_style_reg = {'facecolor': '#e1c938', 'alpha': alpha, 'edgecolor': 'k', 'linewidth': 0.2, 'zorder': 1}
+    la_style_agg = {'facecolor': '#c3c3c3', 'alpha': alpha, 'edgecolor': 'k', 'linewidth': 0.2, 'zorder': 1}
+
     def plot_region_data(ax):
         # get geoms of MV grid district, load areas and LV grid districts
         mv_grid_district = gpd.GeoDataFrame({'geometry': grid.grid_district.geo_data},
-                                            crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
-        load_areas = gpd.GeoDataFrame({'geometry': [la.geo_area for la in grid.grid_district.lv_load_areas()]},
-                                      crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
+                                            crs='epsg:{srid}'.format(srid=model_proj))
+        load_areas_sat = gpd.GeoDataFrame({'geometry': [la.geo_area for la in grid.grid_district.lv_load_areas()
+                                                        if la.is_satellite]},
+                                      crs='epsg:{srid}'.format(srid=model_proj))
+        load_areas_reg = gpd.GeoDataFrame({'geometry': [la.geo_area for la in grid.grid_district.lv_load_areas()
+                                                        if not la.is_satellite and not la.is_aggregated]},
+                                          crs='epsg:{srid}'.format(srid=model_proj))
+        load_areas_agg = gpd.GeoDataFrame({'geometry': [la.geo_area for la in grid.grid_district.lv_load_areas()
+                                                        if la.is_aggregated]},
+                                          crs='epsg:{srid}'.format(srid=model_proj))
         lv_grid_districts = gpd.GeoDataFrame({'geometry': [lvgd.geo_data
                                                            for la in grid.grid_district.lv_load_areas()
                                                            for lvgd in la.lv_grid_districts()]},
-                                             crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
+                                             crs='epsg:{srid}'.format(srid=model_proj))
 
         # reproject to WGS84 pseudo mercator
         mv_grid_district = mv_grid_district.to_crs(epsg=3857)
-        load_areas = load_areas.to_crs(epsg=3857)
+        load_areas_sat = load_areas_sat.to_crs(epsg=3857)
+        load_areas_reg = load_areas_reg.to_crs(epsg=3857)
+        load_areas_agg = load_areas_agg.to_crs(epsg=3857)
         lv_grid_districts = lv_grid_districts.to_crs(epsg=3857)
 
-        # plot
-        mv_grid_district.plot(ax=ax, color='#ffffff', alpha=0.2, edgecolor='k', linewidth=2, zorder=2)
-        load_areas.plot(ax=ax, color='#fffea3', alpha=0.1, edgecolor='k', linewidth=0.5, zorder=3)
-        lv_grid_districts.plot(ax=ax, color='#ffffff', alpha=0.05, edgecolor='k', linewidth=0.5, zorder=4)
+        # plot with
+        # patches
+        patches = []
+
+        if not load_areas_sat.empty:
+            patches.append(Patch(**la_style_sat, label='Satellite load area'))
+            load_areas_sat.plot(ax=ax, **la_style_sat)
+        if not load_areas_reg.empty:
+            patches.append(Patch(**la_style_reg, label='Regular load area'))
+            load_areas_reg.plot(ax=ax, **la_style_reg)
+        if not load_areas_agg.empty:
+            patches.append(Patch(**la_style_agg, label='Aggregated load area'))
+            load_areas_agg.plot(ax=ax, **la_style_agg)
+
+        patches.append(Patch(**mvgd_style, label='MV grid district'))
+        mv_grid_district.boundary.plot(ax=ax, edgecolor='#fdcc02', linewidth=1, zorder=1)
+
+        if not plot_background_map:
+            lv_grid_districts.plot(ax=ax, color='#ffffff', alpha=0.1, edgecolor='k', linewidth=0.1, zorder=2)
+
+        return patches
+
+
+    # plot legend including patches (regions)
+    def plt_legend_without_duplicate_labels(ax, patches):
+        handles, labels = ax.get_legend_handles_labels()
+        unique = [(h, l) for i, (h, l) in enumerate(zip(handles, labels)) if l not in labels[:i]]
+        if patches is not None:
+            for patch in patches:
+                unique.insert(0, (patch, patch.get_label()))
+        ax.legend(*zip(*unique), fontsize=5)
 
     if not isinstance(grid, MVGridDing0):
         logger.warning('Sorry, but plotting is currently only available for MV grids but you did not pass an'
@@ -202,39 +261,44 @@ def plot_region_data(ax):
         return
 
     g = grid.graph
-    model_proj = grid.network.config['geo']['srid']
 
     if testcase == 'feedin':
         case_idx = 1
     else:
         case_idx = 0
 
-    nodes_types, nodes_by_type, node_colors_by_type, node_sizes_by_type, zindex_by_type, nodes_pos =\
+    nodes_types, nodes_by_type, node_colors_by_type, node_sizes_by_type, zindex_by_type, nodes_pos = \
         set_nodes_style_and_position(g.nodes())
 
     # reproject to WGS84 pseudo mercator
-    nodes_pos = reproject_nodes(nodes_pos, model_proj=model_proj)
+    nodes_pos = reproject_nodes(nodes_pos, model_proj=str(model_proj))
 
     plt.figure(figsize=(9, 6))
     ax = plt.gca()
 
     if line_color == 'loading':
+        color = None
         edges_color = []
+        edges_geom = []
         for n1, n2 in g.edges():
             edge = g.adj[n1][n2]
             if hasattr(edge['branch'], 's_res'):
                 edges_color.append(edge['branch'].s_res[case_idx] * 1e3 /
                                    (3 ** 0.5 * edge['branch'].type['U_n'] * edge['branch'].type['I_max_th']))
+                edges_geom.append(edge['branch'].geometry)
             else:
                 edges_color.append(0)
         edges_cmap = plt.get_cmap('jet')
-        #edges_cmap.set_over('#952eff')
+
     else:
-        edges_color = ['black'] * len(list(grid.graph_edges()))
+        color = 'black'
+        edges_color = [color] * len(list(grid.graph_edges()))
+        edges_geom = [edge['branch'].geometry for edge in list(grid.graph_edges())]
         edges_cmap = None
 
     # plot nodes by voltage
     if node_color == 'voltage':
+
         voltage_station = grid._station.voltage_res[case_idx]
         nodes_color = []
         for n in g.nodes():
@@ -271,14 +335,14 @@ def plot_region_data(ax):
 
         # colorbar nodes
         if limits_cb_nodes is None:
-            limits_cb_nodes = (math.floor(min(nodes_color)*100)/100,
-                               math.ceil(max(nodes_color)*100)/100)
+            limits_cb_nodes = (math.floor(min(nodes_color) * 100) / 100,
+                               math.ceil(max(nodes_color) * 100) / 100)
         v_range = np.linspace(limits_cb_nodes[0], limits_cb_nodes[1], 101)
         cb_voltage = plt.colorbar(nodes, boundaries=v_range,
                                   ticks=v_range[0:101:10],
                                   fraction=0.04, pad=0.1)
-        cb_voltage.set_clim(vmin=limits_cb_nodes[0],
-                            vmax=limits_cb_nodes[1])
+        cb_voltage.mappable.set_clim(vmin=limits_cb_nodes[0],
+                                     vmax=limits_cb_nodes[1])
         cb_voltage.set_label('Node voltage deviation in %', size='smaller')
         cb_voltage.ax.tick_params(labelsize='smaller')
 
@@ -301,36 +365,95 @@ def plot_region_data(ax):
                 nodes.set_zorder(zindex_by_type[node_type])
                 nodes.set_edgecolor('k')
 
+    # plotting based on geometry linestring
+
+    # edges with geometry and color
+    edges = gpd.GeoDataFrame({'geometry': [edge['branch'].geometry for edge in list(grid.graph_edges())],
+                              'ring': [str(edge['branch'].ring) for edge in list(grid.graph_edges())]},
+                             crs='epsg:{srid}'.format(srid=model_proj))
+
+    edges = edges.to_crs(epsg=3857)
+
+    if not edges.empty:
+
+        if line_color == 'loading': # plots line loading
+
+            edges = gpd.GeoDataFrame({'geometry': edges_geom,
+                                      'color': edges_color},
+                                     crs='epsg:{srid}'.format(srid=model_proj))
+            edges = edges.to_crs(epsg=3857)
+
+            edges.plot(ax=ax,
+                       cmap=edges_cmap,
+                       linewidth=0.7,
+                       alpha=1,
+                       zorder=2)
+
+        elif line_color == 'ring': # plots different color for each ring
+
+            edges = gpd.GeoDataFrame({'geometry': [edge['branch'].geometry for edge in list(grid.graph_edges())],
+                                      'ring': [str(edge['branch'].ring) for edge in list(grid.graph_edges())]},
+                                     crs='epsg:{srid}'.format(srid=model_proj))
+            edges = edges.to_crs(epsg=3857)
+
+            try:
+                edges['color'] = edges['ring'].str.extract('(\d+)').astype(int)
+            except ValueError:
+                edges['color'] = edges['ring'].str.extract('(\d+)').fillna(0).astype(int)
+                logger.error("ValueError: Branch has no ring assigned. Fill nan with 0")
+
+            edges.plot(column=edges['color'],
+                       ax=ax,
+                       cmap='jet',
+                       linewidth=0.7,
+                       alpha=1,
+                       zorder=2)
+
+        else: # plots only black lines
+
+            edges = gpd.GeoDataFrame({'geometry': [edge['branch'].geometry for edge in list(grid.graph_edges())]},
+                                     crs='epsg:{srid}'.format(srid=model_proj))
+            edges = edges.to_crs(epsg=3857)
+
+            edges.plot(ax=ax,
+                       color=edges_color,
+                       linewidth=0.7,
+                       # lw=1,
+                       zorder=2)
+
     edges = nx.draw_networkx_edges(g,
                                    pos=nodes_pos,
                                    edge_color=edges_color,
                                    edge_cmap=edges_cmap,
                                    edge_vmin=0,
                                    edge_vmax=1,
-                                   #width=1,
+                                   width=0,
                                    ax=ax)
-    edges.set_zorder(5)
 
     if line_color == 'loading':
         # colorbar edges
         if limits_cb_lines is None:
-            limits_cb_lines = (math.floor(min(edges_color)*100)/100,
-                               math.ceil(max(edges_color)*100)/100)
+            limits_cb_lines = (math.floor(min(edges_color) * 100) / 100,
+                               math.ceil(max(edges_color) * 100) / 100)
         loading_range = np.linspace(limits_cb_lines[0], limits_cb_lines[1], 101)
         cb_loading = plt.colorbar(edges, boundaries=loading_range,
                                   ticks=loading_range[0:101:10],
                                   fraction=0.04, pad=0.04)
-        cb_loading.set_clim(vmin=limits_cb_lines[0],
-                            vmax=limits_cb_lines[1])
+        cb_loading.mappable.set_clim(vmin=limits_cb_lines[0],
+                                     vmax=limits_cb_lines[1])
         cb_loading.set_label('Line loading in % of nominal capacity', size='smaller')
         cb_loading.ax.tick_params(labelsize='smaller')
 
     if use_ctx and background_map:
         plot_background_map(ax=ax)
     if use_gpd:
-        plot_region_data(ax=ax)
+        patches = plot_region_data(ax=ax)
+
+    if patches:
+        plt_legend_without_duplicate_labels(ax, patches)
+    else:
+        plt_legend_without_duplicate_labels(ax, patches=None)
 
-    plt.legend(fontsize=7)
     plt.title('MV Grid District {id} - {st}'.format(id=grid.id_db,
                                                     st=subtitle))
 
@@ -342,8 +465,317 @@ def plot_region_data(ax):
         plt.tight_layout()
         plt.show()
     else:
-        path = os.path.join(get_default_home_dir(), 'ding0_grid_{id}_{filename}'.format(id=str(grid.id_db),
-                                                                                        filename=filename))
+        if path:
+            path = os.path.join(path, str(grid.id_db), "plot_mv")
+            if not os.path.exists(path):
+                os.makedirs(path)
+        else:
+            path = get_default_home_dir()
+        path = os.path.join(path, f'ding0_grid_{str(grid.id_db)}_{filename}.pdf')
+        plt.savefig(path, dpi=300, bbox_inches='tight')
+        plt.close()
+        logger.info('==> Figure saved to {path}'.format(path=path))
+
+
+@log_errors
+def plot_lv_topology(
+        grid,
+        path=None,
+        mv_grid_id=None,
+        subtitle="",
+        testcase='load',
+        line_color='feeder',
+        node_color='type',
+        background_map=True,
+        filename=None
+):
+    """ Draws LV grid graph using networkx
+
+    Parameters
+    ----------
+    grid : :obj:`LVGridDing0`
+        LV grid to plot.
+    subtitle : :obj:`str`
+        Extend plot's title by this string.
+    filename : :obj:`str`
+        If provided, the figure will be saved and not displayed (default path: ~/.ding0/).
+        A prefix is added to the file name.
+    testcase : :obj:`str`
+        Defines which case is to be used. Refer to config_calc.cfg to see further
+        assumptions for the cases. Possible options are:
+
+        * 'load' (default)
+          Heavy-load-flow case
+        * 'feedin'
+          Feedin-case
+    line_color : :obj:`str`
+        Defines whereby to choose line colors. Possible options are:
+
+        * 'feeder'
+          Line color is set according to related feeder of the line.
+        * 'loading'
+          Line color is set according to loading of the line in heavy load case.
+          You can use parameter `limits_cb_lines` to adjust the color range.
+        * None (default)
+          Lines are plotted in black. Is also the fallback option in case of
+          wrong input.
+    node_color : :obj:`str`
+        Defines whereby to choose node colors. Possible options are:
+
+        * 'type' (default)
+          Node color as well as size is set according to type of node
+          (generator, MV station, etc.). Is also the fallback option in case of
+          wrong input.
+        * 'voltage'
+          Node color is set according to voltage deviation from 1 p.u..
+          You can use parameter `limits_cb_nodes` to adjust the color range.
+    limits_cb_lines : :obj:`tuple`
+        Tuple with limits for colorbar of line color. First entry is the
+        minimum and second entry the maximum value. E.g. pass (0, 1) to
+        adjust the colorbar to 0..100% loading.
+        Default: None (min and max loading are used).
+    limits_cb_nodes : :obj:`tuple`
+        Tuple with limits for colorbar of nodes. First entry is the
+        minimum and second entry the maximum value. E.g. pass (0.9, 1) to
+        adjust the colorbar to 90%..100% voltage.
+        Default: None (min and max loading are used).
+    background_map : bool, optional
+        If True, a background map is plotted (default: carto b positron).
+        The additional package `contextily` is needed for this functionality.
+        Default: True
+
+    Note
+    -----
+    WGS84 pseudo mercator (epsg:3857) is used as coordinate reference system (CRS).
+    Therefore, the drawn graph representation may be falsified!
+
+    """
+
+    model_proj = get_config_osm('srid')
+
+    def plt_legend_without_duplicate_labels(ax, patches):
+
+        handles, labels = ax.get_legend_handles_labels()
+        unique = [(h, l) for i, (h, l) in enumerate(zip(handles, labels)) if l not in labels[:i]]
+        if patches is not None:
+            for patch in patches:
+                unique.insert(0, (patch, patch.get_label()))
+        ax.legend(*zip(*unique), fontsize=7)
+
+    def set_nodes_style_and_position(nodes):
+
+        # TODO: MOVE settings to config
+        # node types (name of classes)
+        node_types = ['LVStationDing0',
+                      'LVLoadDing0',  # PAUL new
+                      'LVCableDistributorDing0',
+                      'GeneratorFluctuatingDing0',
+                      'GeneratorDing0',
+                      'n/a']
+
+        # node styles
+        colors_dict = {'LVStationDing0': '#f2ae00',
+                       'LVLoadDing0': '#d62728',  # PAUL new
+                       'LVCableDistributorDing0': '#ffa500',  # PAUL new'#000000',
+                       'GeneratorDing0': '#097969',
+                       'n/a': 'orange'}
+        sizes_dict = {'LVStationDing0': 50,
+                      'LVLoadDing0': 6,  # PAUL new
+                      'GeneratorDing0': 12,
+                      'LVCableDistributorDing0': 4,
+                      'n/a': 3}
+        zindex_by_type = {'LVStationDing0': 12,
+                          'LVLoadDing0': 12,  # PAUL new
+                          'GeneratorDing0': 11,
+                          'LVCableDistributorDing0': 9,
+                          'n/a': 10}
+
+        # dict of node class names: list of nodes
+        nodes_by_type = {_: [] for _ in node_types}
+        # dict of node class names: list of node-individual color
+        node_colors_by_type = {_: [] for _ in node_types}
+        # dict of node class names: list of node-individual size
+        node_sizes_by_type = {_: [] for _ in node_types}
+        node_sizes_by_type['all'] = []
+        # dict of nodes:node-individual positions
+        nodes_pos = {}
+
+        for n in nodes:
+
+            if type(n).__name__ in node_types:
+
+                type_n = type(n).__name__
+                if type_n == 'GeneratorFluctuatingDing0':
+                    type_n = 'GeneratorDing0'
+
+                nodes_by_type[type_n].append(n)
+                node_colors_by_type[type_n].append(colors_dict[type_n])
+                node_sizes_by_type[type_n].append(sizes_dict[type_n])
+                node_sizes_by_type['all'].append(sizes_dict[type_n])
+            else:
+                nodes_by_type['n/a'].append(n)
+                node_colors_by_type['n/a'].append(colors_dict['n/a'])
+                node_sizes_by_type['n/a'].append(sizes_dict['n/a'])
+                node_sizes_by_type['all'].append(sizes_dict['n/a'])
+            nodes_pos[n] = (n.geo_data.x, n.geo_data.y)
+
+        return node_types, nodes_by_type, node_colors_by_type, \
+               node_sizes_by_type, zindex_by_type, nodes_pos
+
+    def reproject_nodes(nodes_pos, model_proj=str(model_proj)):  # changed from 4326 to new CRS 3035
+        # PAUL new: replace transform with Transformer in pyproj, to keep syntax up-to-date
+        transformer = Transformer.from_crs(int(model_proj), 3857, always_xy=True)
+        nodes_pos2 = {}
+        for k, v in nodes_pos.items():
+            nodes_pos2[k] = (transformer.transform(v[0], v[1]))
+        return nodes_pos2
+
+    def plot_background_map(ax):
+        url = ctx.providers.CartoDB.Positron
+        xmin, xmax, ymin, ymax = ax.axis()
+        basemap, extent = ctx.bounds2img(xmin, ymin, xmax, ymax,
+                                         source=url)
+        ax.imshow(basemap, extent=extent, interpolation='bilinear', zorder=0)
+        ax.axis((xmin, xmax, ymin, ymax))
+
+    lvgd_style = {'facecolor': '#ffffff', 'alpha': 1, 'edgecolor': '#fdcc02', 'linewidth': 1, 'zorder': 1}
+    buildings_style = {'alpha': 0.5, 'facecolor': '#1f77b4', 'edgecolor': 'darkblue', 'linewidth': 0.5, 'zorder': 2}
+
+    def plot_region_data(ax):
+        # get geoms of MV grid district, load areas and LV grid districts
+        buildings = gpd.GeoDataFrame({'geometry': grid.grid_district.buildings.footprint.tolist()},
+                                     crs='epsg:{srid}'.format(srid=model_proj))
+        lv_grid_district = gpd.GeoDataFrame({'geometry': [grid.grid_district.geo_data]},
+                                            crs='epsg:{srid}'.format(srid=model_proj))
+
+        # reproject to WGS84 pseudo mercator
+        buildings = buildings.to_crs(epsg=3857)
+        lv_grid_district = lv_grid_district.to_crs(epsg=3857)
+
+        # plot with
+        # patches
+        lv_grid_district.boundary.plot(ax=ax, edgecolor='#fdcc02', linewidth=1, zorder=1)
+        patches = []
+
+        if not buildings.empty:
+            patches.append(Patch(**buildings_style, label='Building geometries'))
+            buildings.plot(ax=ax, **buildings_style)
+
+        patches.append(Patch(**lvgd_style, label='LV grid district'))
+
+        return patches
+
+    g = grid._graph
+
+
+    if testcase == 'feedin':
+        case_idx = 1
+    else:
+        case_idx = 0
+
+    nodes_types, nodes_by_type, node_colors_by_type, node_sizes_by_type, zindex_by_type, nodes_pos = \
+        set_nodes_style_and_position(g.nodes())
+
+    # reproject to WGS84 pseudo mercator
+    nodes_pos = reproject_nodes(nodes_pos, model_proj=str(model_proj))
+
+    plt.figure(figsize=(9, 6))
+    ax = plt.gca()
+
+    for node_type in nodes_types:
+        if len(nodes_by_type[node_type]) != 0:
+            nodes = nx.draw_networkx_nodes(g,
+                                           nodelist=nodes_by_type[node_type],
+                                           pos=nodes_pos,
+                                           # node_shape='o', # TODO: Add additional symbols here
+                                           node_color=node_colors_by_type[node_type],
+                                           cmap=None,
+                                           vmin=None,
+                                           vmax=None,
+                                           node_size=node_sizes_by_type[node_type],
+                                           linewidths=0.25,
+                                           label=node_type,
+                                           ax=ax)
+            nodes.set_zorder(zindex_by_type[node_type])
+            nodes.set_edgecolor('k')
+
+    # edges with geometry and color
+    edges = gpd.GeoDataFrame({'geometry': [edge['branch'].geometry for edge in grid.graph_edges()],
+                              'feeder': [str(edge['branch'].feeder) for edge in grid.graph_edges()]},
+                             crs='epsg:{srid}'.format(srid=model_proj))
+
+    edges = edges.to_crs(epsg=3857)
+
+    if not edges.empty:
+
+        if line_color == 'feeder': # plots differnt color for feeder
+            try:
+                edges['color'] = edges['feeder'].str.extract('(\d+)').astype(int)
+            except ValueError:
+                edges['color'] = edges['feeder'].str.extract('(\d+)').fillna(0).astype(int)
+            edges = edges.sort_values(by='color')
+            color = None
+            edges_color = edges['color']
+            edges_cmap = 'jet'
+
+        elif line_color == 'loading': # plots line loading
+
+            color = None
+            edges_color = []
+            for edge in grid.graph_edges():
+                # plotting based on line loading or node voltages not possible
+                # TODO: s_res value from powerflow (pypsa.io) are missing
+                # TODO: node voltages are not available as well
+                if hasattr(edge['branch'], 's_res'):
+                    edges_color.append(edge['branch'].s_res[case_idx] * 1e3 /
+                                       (3 ** 0.5 * edge['branch'].type['U_n'] * edge['branch'].type['I_max_th']))
+                else:
+                    edges_color.append(0)
+            edges_cmap = 'jet'
+
+        else: # plots black lines
+
+            color = 'black'
+            edges_color = None
+            edges_cmap = None
+
+        edges.plot(column=edges_color,
+                   ax=ax,
+                   color=color,
+                   cmap=edges_cmap,
+                   linewidth=1,
+                   alpha=1,
+                   zorder=3)
+
+    if use_gpd:
+        patches = plot_region_data(ax=ax)
+
+    if use_ctx and background_map:
+        plot_background_map(ax=ax)
+
+    if patches:
+        plt_legend_without_duplicate_labels(ax, patches)
+    else:
+        plt_legend_without_duplicate_labels(ax, patches=None)
+
+    plt.title('LV Grid District {id} - {st}'.format(id=grid.grid_district.id_db,
+                                                    st=subtitle))
+
+    # hide axes labels (coords)
+    ax.get_xaxis().set_visible(False)
+    ax.get_yaxis().set_visible(False)
+
+    if filename is None:
+        plt.tight_layout()
+        plt.show()
+    else:
+        if path:
+            path = os.path.join(path, str(mv_grid_id), "plot_lv")
+            if not os.path.exists(path):
+                os.makedirs(path)
+        else:
+            path = get_default_home_dir()
+        path = os.path.join(path, f'ding0_lv_grid_{str(grid.id_db)}_{filename}.pdf')
         plt.savefig(path, dpi=300, bbox_inches='tight')
         plt.close()
         logger.info('==> Figure saved to {path}'.format(path=path))
diff --git a/ding0/tools/plots_old.py b/ding0/tools/plots_old.py
new file mode 100644
index 00000000..7a877dfa
--- /dev/null
+++ b/ding0/tools/plots_old.py
@@ -0,0 +1,349 @@
+import os
+import math
+import numpy as np
+import matplotlib.pyplot as plt
+import networkx as nx
+from pyproj import Proj, transform
+import logging
+
+logger = logging.getLogger(__name__)
+from ding0.tools.logger import get_default_home_dir
+from ding0.core.network.grids import MVGridDing0
+
+use_gpd = False
+use_ctx = False
+
+if 'READTHEDOCS' not in os.environ:
+    use_gpd = True
+    try:
+        import geopandas as gpd
+    except:
+        use_gpd = False
+
+    use_ctx = True
+    try:
+        import contextily as ctx
+    except:
+        use_ctx = False
+
+
+def plot_mv_topology(grid, subtitle='', filename=None, testcase='load',
+                     line_color=None, node_color='type',
+                     limits_cb_lines=None, limits_cb_nodes=None,
+                     background_map=True):
+    """ Draws MV grid graph using networkx
+
+    Parameters
+    ----------
+    grid : :obj:`MVGridDing0`
+        MV grid to plot.
+    subtitle : :obj:`str`
+        Extend plot's title by this string.
+    filename : :obj:`str`
+        If provided, the figure will be saved and not displayed (default path: ~/.ding0/).
+        A prefix is added to the file name.
+    testcase : :obj:`str`
+        Defines which case is to be used. Refer to config_calc.cfg to see further
+        assumptions for the cases. Possible options are:
+        
+        * 'load' (default)
+          Heavy-load-flow case
+        * 'feedin'
+          Feedin-case
+    line_color : :obj:`str`
+        Defines whereby to choose line colors. Possible options are:
+
+        * 'loading'
+          Line color is set according to loading of the line in heavy load case.
+          You can use parameter `limits_cb_lines` to adjust the color range.
+        * None (default)
+          Lines are plotted in black. Is also the fallback option in case of
+          wrong input.
+    node_color : :obj:`str`
+        Defines whereby to choose node colors. Possible options are:
+
+        * 'type' (default)
+          Node color as well as size is set according to type of node
+          (generator, MV station, etc.). Is also the fallback option in case of
+          wrong input.
+        * 'voltage'
+          Node color is set according to voltage deviation from 1 p.u..
+          You can use parameter `limits_cb_nodes` to adjust the color range.
+    limits_cb_lines : :obj:`tuple`
+        Tuple with limits for colorbar of line color. First entry is the
+        minimum and second entry the maximum value. E.g. pass (0, 1) to
+        adjust the colorbar to 0..100% loading.
+        Default: None (min and max loading are used).
+    limits_cb_nodes : :obj:`tuple`
+        Tuple with limits for colorbar of nodes. First entry is the
+        minimum and second entry the maximum value. E.g. pass (0.9, 1) to
+        adjust the colorbar to 90%..100% voltage.
+        Default: None (min and max loading are used).
+    background_map : bool, optional
+        If True, a background map is plotted (default: stamen toner light).
+        The additional package `contextily` is needed for this functionality.
+        Default: True
+
+    Note
+    -----
+    WGS84 pseudo mercator (epsg:3857) is used as coordinate reference system (CRS).
+    Therefore, the drawn graph representation may be falsified!
+
+    """
+
+    def set_nodes_style_and_position(nodes):
+
+        # TODO: MOVE settings to config
+        # node types (name of classes)
+        node_types = ['MVStationDing0',
+                      'LVStationDing0',
+                      'LVLoadAreaCentreDing0',
+                      'MVCableDistributorDing0',
+                      'GeneratorDing0',
+                      'GeneratorFluctuatingDing0',
+                      'CircuitBreakerDing0',
+                      'n/a']
+        
+        # node styles
+        colors_dict = {'MVStationDing0': '#f2ae00',
+                       'LVStationDing0': 'grey',
+                       'LVLoadAreaCentreDing0': '#fffc3d',
+                       'MVCableDistributorDing0': '#000000',
+                       'GeneratorDing0': '#00b023',
+                       'GeneratorFluctuatingDing0': '#0078b0',
+                       'CircuitBreakerDing0': '#c20000',
+                       'n/a': 'orange'}
+        sizes_dict = {'MVStationDing0': 120,
+                      'LVStationDing0': 7,
+                      'LVLoadAreaCentreDing0': 30,
+                      'MVCableDistributorDing0': 5,
+                      'GeneratorDing0': 50,
+                      'GeneratorFluctuatingDing0': 50,
+                      'CircuitBreakerDing0': 50,
+                      'n/a': 5}
+        zindex_by_type = {'MVStationDing0': 16,
+                          'LVStationDing0': 12,
+                          'LVLoadAreaCentreDing0': 11,
+                          'MVCableDistributorDing0': 13,
+                          'GeneratorDing0': 14,
+                          'GeneratorFluctuatingDing0': 14,
+                          'CircuitBreakerDing0': 15,
+                          'n/a': 10}
+
+        # dict of node class names: list of nodes
+        nodes_by_type = {_: [] for _ in node_types}
+        # dict of node class names: list of node-individual color
+        node_colors_by_type = {_: [] for _ in node_types}
+        # dict of node class names: list of node-individual size
+        node_sizes_by_type = {_: [] for _ in node_types}
+        node_sizes_by_type['all'] = []
+        # dict of nodes:node-individual positions
+        nodes_pos = {}
+
+        for n in nodes:
+            if type(n).__name__ in node_types:
+                nodes_by_type[type(n).__name__].append(n)
+                node_colors_by_type[type(n).__name__].append(colors_dict[type(n).__name__])
+                node_sizes_by_type[type(n).__name__].append(sizes_dict[type(n).__name__])
+                node_sizes_by_type['all'].append(sizes_dict[type(n).__name__])
+            else:
+                nodes_by_type['n/a'].append(n)
+                node_colors_by_type['n/a'].append(colors_dict['n/a'])
+                node_sizes_by_type['n/a'].append(sizes_dict['n/a'])
+                node_sizes_by_type['all'].append(sizes_dict['n/a'])
+            nodes_pos[n] = (n.geo_data.x, n.geo_data.y)
+
+        return node_types, nodes_by_type, node_colors_by_type,\
+               node_sizes_by_type, zindex_by_type, nodes_pos
+
+    def reproject_nodes(nodes_pos, model_proj='4326'):
+        inProj = Proj(init='epsg:{srid}'.format(srid=model_proj))
+        outProj = Proj(init='epsg:3857')
+        nodes_pos2 = {}
+        for k, v in nodes_pos.items():
+            x2, y2 = transform(inProj, outProj,
+                               v[0],
+                               v[1])
+            nodes_pos2[k] = (x2, y2)
+        return nodes_pos2
+
+    def plot_background_map(ax):
+        url = ctx.sources.ST_TONER_LITE
+        xmin, xmax, ymin, ymax = ax.axis()
+        basemap, extent = ctx.bounds2img(xmin, ymin, xmax, ymax,
+                                         zoom=12, url=url)
+        ax.imshow(basemap, extent=extent, interpolation='bilinear', zorder=0)
+        ax.axis((xmin, xmax, ymin, ymax))
+
+    def plot_region_data(ax):
+        # get geoms of MV grid district, load areas and LV grid districts
+        mv_grid_district = gpd.GeoDataFrame({'geometry': grid.grid_district.geo_data},
+                                            crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
+        load_areas = gpd.GeoDataFrame({'geometry': [la.geo_area for la in grid.grid_district.lv_load_areas()]},
+                                      crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
+        lv_grid_districts = gpd.GeoDataFrame({'geometry': [lvgd.geo_data
+                                                           for la in grid.grid_district.lv_load_areas()
+                                                           for lvgd in la.lv_grid_districts()]},
+                                             crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
+
+        # reproject to WGS84 pseudo mercator
+        mv_grid_district = mv_grid_district.to_crs(epsg=3857)
+        load_areas = load_areas.to_crs(epsg=3857)
+        lv_grid_districts = lv_grid_districts.to_crs(epsg=3857)
+
+        # plot
+        mv_grid_district.plot(ax=ax, color='#ffffff', alpha=0.2, edgecolor='k', linewidth=2, zorder=2)
+        load_areas.plot(ax=ax, color='#fffea3', alpha=0.1, edgecolor='k', linewidth=0.5, zorder=3)
+        lv_grid_districts.plot(ax=ax, color='#ffffff', alpha=0.05, edgecolor='k', linewidth=0.5, zorder=4)
+
+    if not isinstance(grid, MVGridDing0):
+        logger.warning('Sorry, but plotting is currently only available for MV grids but you did not pass an'
+                       'instance of MVGridDing0. Plotting is skipped.')
+        return
+
+    g = grid.graph
+    model_proj = grid.network.config['geo']['srid']
+
+    if testcase == 'feedin':
+        case_idx = 1
+    else:
+        case_idx = 0
+
+    nodes_types, nodes_by_type, node_colors_by_type, node_sizes_by_type, zindex_by_type, nodes_pos =\
+        set_nodes_style_and_position(g.nodes())
+
+    # reproject to WGS84 pseudo mercator
+    nodes_pos = reproject_nodes(nodes_pos, model_proj=model_proj)
+
+    plt.figure(figsize=(9, 6))
+    ax = plt.gca()
+
+    if line_color == 'loading':
+        edges_color = []
+        for n1, n2 in g.edges():
+            edge = g.adj[n1][n2]
+            if hasattr(edge['branch'], 's_res'):
+                edges_color.append(edge['branch'].s_res[case_idx] * 1e3 /
+                                   (3 ** 0.5 * edge['branch'].type['U_n'] * edge['branch'].type['I_max_th']))
+            else:
+                edges_color.append(0)
+        edges_cmap = plt.get_cmap('jet')
+        #edges_cmap.set_over('#952eff')
+    else:
+        edges_color = ['black'] * len(list(grid.graph_edges()))
+        edges_cmap = None
+
+    # plot nodes by voltage
+    if node_color == 'voltage':
+        voltage_station = grid._station.voltage_res[case_idx]
+        nodes_color = []
+        for n in g.nodes():
+            if hasattr(n, 'voltage_res'):
+                nodes_color.append(n.voltage_res[case_idx])
+            else:
+                nodes_color.append(voltage_station)
+
+        if testcase == 'feedin':
+            nodes_cmap = plt.get_cmap('Reds')
+            nodes_vmax = voltage_station + float(grid.network.config
+                                                 ['mv_routing_tech_constraints']
+                                                 ['mv_max_v_level_fc_diff_normal'])
+            nodes_vmin = voltage_station
+        else:
+            nodes_cmap = plt.get_cmap('Reds_r')
+            nodes_vmin = voltage_station - float(grid.network.config
+                                                 ['mv_routing_tech_constraints']
+                                                 ['mv_max_v_level_lc_diff_normal'])
+            nodes_vmax = voltage_station
+
+        nodes = nx.draw_networkx_nodes(g,
+                                       pos=nodes_pos,
+                                       node_color=nodes_color,
+                                       # node_shape='o', # TODO: Add additional symbols here
+                                       cmap=nodes_cmap,
+                                       vmin=nodes_vmin,
+                                       vmax=nodes_vmax,
+                                       node_size=node_sizes_by_type['all'],
+                                       linewidths=0.25,
+                                       ax=ax)
+        nodes.set_zorder(10)
+        nodes.set_edgecolor('k')
+
+        # colorbar nodes
+        if limits_cb_nodes is None:
+            limits_cb_nodes = (math.floor(min(nodes_color)*100)/100,
+                               math.ceil(max(nodes_color)*100)/100)
+        v_range = np.linspace(limits_cb_nodes[0], limits_cb_nodes[1], 101)
+        cb_voltage = plt.colorbar(nodes, boundaries=v_range,
+                                  ticks=v_range[0:101:10],
+                                  fraction=0.04, pad=0.1)
+        cb_voltage.set_clim(vmin=limits_cb_nodes[0],
+                            vmax=limits_cb_nodes[1])
+        cb_voltage.set_label('Node voltage deviation in %', size='smaller')
+        cb_voltage.ax.tick_params(labelsize='smaller')
+
+    # plot nodes by type
+    else:
+        for node_type in nodes_types:
+            if len(nodes_by_type[node_type]) != 0:
+                nodes = nx.draw_networkx_nodes(g,
+                                               nodelist=nodes_by_type[node_type],
+                                               pos=nodes_pos,
+                                               # node_shape='o', # TODO: Add additional symbols here
+                                               node_color=node_colors_by_type[node_type],
+                                               cmap=None,
+                                               vmin=None,
+                                               vmax=None,
+                                               node_size=node_sizes_by_type[node_type],
+                                               linewidths=0.25,
+                                               label=node_type,
+                                               ax=ax)
+                nodes.set_zorder(zindex_by_type[node_type])
+                nodes.set_edgecolor('k')
+
+    edges = nx.draw_networkx_edges(g,
+                                   pos=nodes_pos,
+                                   edge_color=edges_color,
+                                   edge_cmap=edges_cmap,
+                                   edge_vmin=0,
+                                   edge_vmax=1,
+                                   #width=1,
+                                   ax=ax)
+    edges.set_zorder(5)
+
+    if line_color == 'loading':
+        # colorbar edges
+        if limits_cb_lines is None:
+            limits_cb_lines = (math.floor(min(edges_color)*100)/100,
+                               math.ceil(max(edges_color)*100)/100)
+        loading_range = np.linspace(limits_cb_lines[0], limits_cb_lines[1], 101)
+        cb_loading = plt.colorbar(edges, boundaries=loading_range,
+                                  ticks=loading_range[0:101:10],
+                                  fraction=0.04, pad=0.04)
+        cb_loading.set_clim(vmin=limits_cb_lines[0],
+                            vmax=limits_cb_lines[1])
+        cb_loading.set_label('Line loading in % of nominal capacity', size='smaller')
+        cb_loading.ax.tick_params(labelsize='smaller')
+
+    if use_ctx and background_map:
+        plot_background_map(ax=ax)
+    if use_gpd:
+        plot_region_data(ax=ax)
+
+    plt.legend(fontsize=7)
+    plt.title('MV Grid District {id} - {st}'.format(id=grid.id_db,
+                                                    st=subtitle))
+
+    # hide axes labels (coords)
+    ax.get_xaxis().set_visible(False)
+    ax.get_yaxis().set_visible(False)
+
+    if filename is None:
+        plt.tight_layout()
+        plt.show()
+    else:
+        path = os.path.join(get_default_home_dir(), 'ding0_grid_{id}_{filename}'.format(id=str(grid.id_db),
+                                                                                        filename=filename))
+        plt.savefig(path, dpi=300, bbox_inches='tight')
+        plt.close()
+        logger.info('==> Figure saved to {path}'.format(path=path))
diff --git a/ding0/tools/pypsa_io.py b/ding0/tools/pypsa_io.py
index aeb226c5..1b9d9ec5 100644
--- a/ding0/tools/pypsa_io.py
+++ b/ding0/tools/pypsa_io.py
@@ -18,13 +18,13 @@
 from ding0.tools import config as cfg_ding0
 from ding0.tools.tools import merge_two_dicts
 from ding0.core.network.stations import LVStationDing0, MVStationDing0
+from ding0.core.network.loads import LVLoadDing0, MVLoadDing0
 from ding0.core.network import LoadDing0, CircuitBreakerDing0, \
-    GeneratorDing0, GeneratorFluctuatingDing0, TransformerDing0
-from ding0.core import MVCableDistributorDing0
+    GeneratorDing0, GeneratorFluctuatingDing0, TransformerDing0, CableDistributorDing0
 from ding0.core.structure.regions import LVLoadAreaCentreDing0, \
     LVGridDistrictDing0, LVLoadAreaDing0
 from ding0.core.powerflow import q_sign
-from ding0.core.network.cable_distributors import LVCableDistributorDing0
+from ding0.core.network.cable_distributors import LVCableDistributorDing0, MVCableDistributorDing0
 from ding0.core import network as ding0_nw
 from ding0.tools.tools import merge_two_dicts_of_dataframes
 from ding0.grid.lv_grid.build_grid import select_transformers
@@ -48,8 +48,12 @@
 if not 'READTHEDOCS' in os.environ:
     from shapely.geometry import LineString
 
-logger = logging.getLogger('ding0')
+logger = logging.getLogger(__name__)
 
+# parameter to enable sector-specific consumption data
+# for egon data buildings set to False, because
+# dataset do not contain consumptional data
+consider_consumption = False
 
 def export_to_dir(network, export_dir):
     """
@@ -83,25 +87,25 @@ def initialize_component_dataframes():
         lv_grid_id, in_building
     lines_df: :pandas:`pandas.DataFrame<dataframe>`
         Dataframe of lines with entries name, bus0, bus1, length, x, r, s_nom,
-        num_parallel, type
+        num_parallel, type, geometry (LineString)
     transformer_df: :pandas:`pandas.DataFrame<dataframe>`
         Dataframe of trafos with entries name, bus0, bus1, x, r, s_nom, type
     generators_df: :pandas:`pandas.DataFrame<dataframe>`
         Dataframe of generators with entries name, bus, control, p_nom, type,
         weather_cell_id, subtype
     loads_df: :pandas:`pandas.DataFrame<dataframe>`
-        Dataframe of loads with entries name, bus, peak_load,
+        Dataframe of loads with entries name, bus, p_set,
         annual_consumption, sector
     """
     cols = {'buses_columns': ['name', 'x', 'y', 'mv_grid_id',
                               'lv_grid_id', 'v_nom', 'in_building'],
             'lines_columns': ['name', 'bus0', 'bus1', 'length', 'r', 'x',
-                              's_nom', 'num_parallel', 'kind', 'type_info'],
+                              's_nom', 'num_parallel', 'kind', 'type_info', 'geometry'],
             'transformer_columns': ['name', 'bus0', 'bus1', 's_nom', 'r',
                                     'x', 'type', 'type_info'],
             'generators_columns': ['name', 'bus', 'control', 'p_nom', 'type',
                                    'weather_cell_id', 'subtype'],
-            'loads_columns': ['name', 'bus', 'peak_load',
+            'loads_columns': ['name', 'bus', 'p_set', 'building_id',
                               'annual_consumption', 'sector']}
     # initialize dataframes
     buses_df = pd.DataFrame(columns=cols['buses_columns'])
@@ -112,10 +116,16 @@ def initialize_component_dataframes():
     return buses_df, generators_df, lines_df, loads_df, transformer_df
 
 
-def fill_mvgd_component_dataframes(mv_grid_district, buses_df, generators_df,
-                                   lines_df, loads_df, transformer_df,
-                                   only_export_mv=False,
-                                   return_time_varying_data=False):
+def fill_mvgd_component_dataframes(
+        mv_grid_district,
+        buses_df,
+        generators_df,
+        lines_df,
+        loads_df,
+        transformer_df,
+        only_export_mv=False,
+        return_time_varying_data=False
+):
     """
     Returns component and if necessary time varying data for power flow
     or csv export of inserted mv grid district
@@ -135,7 +145,7 @@ def fill_mvgd_component_dataframes(mv_grid_district, buses_df, generators_df,
         Dataframe of generators with entries name, bus, control, p_nom, type,
         weather_cell_id, subtype
     loads_df: :pandas:`pandas.DataFrame<dataframe>`
-        Dataframe of loads with entries name, bus, peak_load,
+        Dataframe of loads with entries name, bus, p_set, building_id,
         annual_consumption, sector
     only_export_mv: :obj:`bool`
         Bool that determines export modes for grid district,
@@ -160,48 +170,90 @@ def fill_mvgd_component_dataframes(mv_grid_district, buses_df, generators_df,
     """
     srid = str(int(cfg_ding0.get('geo', 'srid')))
     # fill dataframes
+
+    # fill grids_df with population and geoms of grids
+    grids_df = pd.DataFrame(
+        columns=["name", "grid_district_population", "srid", "area", "grid_district_geom"]
+    ).set_index("name")
+    grids_df.loc[f"mvgd_{mv_grid_district.id_db}"] = (0, srid, mv_grid_district.geo_data.area, mv_grid_district.geo_data)
+    mvgd_population = 0
+    for lv_load_area in mv_grid_district.lv_load_areas():
+        if pd.isna(lv_load_area.population):
+            lv_load_area_population = 0
+            logger.warning(f"Population of {lv_load_area} is NaN")
+        else:
+            lv_load_area_population = lv_load_area.population
+        grids_df.loc[f"lvgd_{lv_load_area.id_db}"] = (lv_load_area.population, srid, lv_load_area.geo_area.area, lv_load_area.geo_area)
+        mvgd_population += lv_load_area_population
+    grids_df.loc[f"mvgd_{mv_grid_district.id_db}", "grid_district_population"] = mvgd_population
+
+
     network_df = pd.DataFrame(
         {'name': [mv_grid_district.id_db], 'srid': [srid],
          'mv_grid_district_geom': [mv_grid_district.geo_data],
-         'mv_grid_district_population': [0]}).set_index('name')
+         'mv_grid_district_population': [int(mvgd_population)]}).set_index('name')
+
     # add mv grid components
     mv_grid = mv_grid_district.mv_grid
-    mv_components, mv_component_data = \
-        fill_component_dataframes(mv_grid, buses_df, lines_df, transformer_df,
-                                  generators_df, loads_df, only_export_mv,
-                                  return_time_varying_data)
+    mv_components, mv_component_data = fill_component_dataframes(
+        mv_grid,
+        buses_df,
+        lines_df,
+        transformer_df,
+        generators_df,
+        loads_df,
+        only_export_mv,
+        return_time_varying_data
+    )
     # move hvmv transformer to own entry to handle differently and ignore in 
     # power flow
     hvmv_transformers = mv_components['Transformer'].loc[
-        ~mv_components['Transformer'].bus0.isin(mv_components['Bus'].index)]
+        ~mv_components['Transformer'].bus0.isin(mv_components['Bus'].index)
+    ]
     mv_components['HVMV_Transformer'] = hvmv_transformers
     mv_components['Transformer'].drop(hvmv_transformers.index, inplace=True)
     logger.info('MV grid {} exported to pypsa format.'.format(
-        str(mv_grid_district.id_db)))
+        str(mv_grid_district.id_db))
+    )
     if not only_export_mv:
         # add lv grid components
         for lv_load_area in mv_grid_district.lv_load_areas():
             for lv_grid_district in lv_load_area.lv_grid_districts():
                 lv_grid = lv_grid_district.lv_grid
-                lv_components_tmp, lv_component_data = \
-                    fill_component_dataframes(lv_grid, buses_df, lines_df,
-                                              transformer_df, generators_df,
-                                              loads_df, only_export_mv,
-                                              return_time_varying_data)
-                mv_components = \
-                    merge_two_dicts_of_dataframes(mv_components,
-                                                  lv_components_tmp)
-                mv_component_data = \
-                    merge_two_dicts_of_dataframes(mv_component_data,
-                                                  lv_component_data)
+                lv_components_tmp, lv_component_data = fill_component_dataframes(
+                    lv_grid,
+                    buses_df,
+                    lines_df,
+                    transformer_df,
+                    generators_df,
+                    loads_df,
+                    only_export_mv,
+                    return_time_varying_data
+                )
+                mv_components = merge_two_dicts_of_dataframes(
+                    mv_components,
+                    lv_components_tmp
+                )
+                mv_component_data = merge_two_dicts_of_dataframes(
+                    mv_component_data,
+                    lv_component_data
+                )
                 logger.info('LV grid {} exported to pypsa format.'.format(
-                    str(lv_grid.id_db)))
-    return mv_components, network_df, mv_component_data
-
-
-def fill_component_dataframes(grid, buses_df, lines_df, transformer_df,
-                              generators_df, loads_df, only_export_mv=False,
-                              return_time_varying_data=False):
+                    str(lv_grid.id_db))
+                )
+    return mv_components, network_df, grids_df, mv_component_data
+
+
+def fill_component_dataframes(
+        grid,
+        buses_df,
+        lines_df,
+        transformer_df,
+        generators_df,
+        loads_df,
+        only_export_mv=False,
+        return_time_varying_data=False
+):
     """
     Returns component and if necessary time varying data for power flow
     or csv export of inserted mv or lv grid
@@ -222,7 +274,7 @@ def fill_component_dataframes(grid, buses_df, lines_df, transformer_df,
         Dataframe of generators with entries name, bus, control, p_nom, type,
         weather_cell_id, subtype
     loads_df: :pandas:`pandas.DataFrame<dataframe>`
-        Dataframe of loads with entries name, bus, peak_load,
+        Dataframe of loads with entries name, bus, p_set,
         annual_consumption, sector
     only_export_mv: :obj:`bool`
     return_time_varying_data: :obj:`bool`
@@ -250,32 +302,47 @@ def fill_component_dataframes(grid, buses_df, lines_df, transformer_df,
     # get all grid nodes
     nodes = grid.graph.nodes()
     # get all grid edges
-    edges = [edge for edge in list(grid.graph_edges())
-             if (edge['adj_nodes'][0] in nodes and not isinstance(
-            edge['adj_nodes'][0], LVLoadAreaCentreDing0))
-             and (edge['adj_nodes'][1] in nodes and not isinstance(
-            edge['adj_nodes'][1], LVLoadAreaCentreDing0))]
+    edges = [
+        edge for edge in list(grid.graph_edges())
+        if (
+                   edge['adj_nodes'][0] in nodes
+                   and not isinstance(edge['adj_nodes'][0], LVLoadAreaCentreDing0)
+           )
+           and
+           (
+                   edge['adj_nodes'][1] in nodes
+                   and not isinstance(edge['adj_nodes'][1], LVLoadAreaCentreDing0)
+           )
+    ]
     # add station transformers to respective dataframe
     for trafo in grid.station()._transformers:
         if trafo.x_pu == None:
             type = '{} MVA 110/{} kV'.format(int(trafo.s_max_a/1e3), grid.v_level)
-            transformer_df = append_transformers_df(transformer_df,
-                                                    trafo, type)
+            transformer_df = append_transformers_df(
+                transformer_df,trafo, type
+            )
         else:
             transformer_df = append_transformers_df(transformer_df, trafo)
     # handle all nodes and append to respective dataframes
-    node_components, component_data = \
-        nodes_to_dict_of_dataframes(grid, nodes, buses_df, generators_df,
-                                    loads_df, transformer_df, only_export_mv,
-                                    return_time_varying_data)
+    node_components, component_data = nodes_to_dict_of_dataframes(
+        grid,
+        nodes,
+        buses_df,
+        generators_df,
+        loads_df,
+        transformer_df,
+        only_export_mv,
+        return_time_varying_data
+    )
     # handle all edges and append to respective dataframe
-    branch_components = edges_to_dict_of_dataframes(edges, lines_df,
-                                                    node_components['Bus'])
+    branch_components = edges_to_dict_of_dataframes(
+        edges, lines_df, node_components['Bus']
+    )
     # merge node and edges
     components = merge_two_dicts(node_components, branch_components)
-    components, component_data = \
-        circuit_breakers_to_df(grid, components, component_data,
-                               open_circuit_breakers, return_time_varying_data)
+    components, component_data = circuit_breakers_to_df(
+        grid, components, component_data, open_circuit_breakers, return_time_varying_data
+    )
     return components, component_data
 
 
@@ -297,8 +364,8 @@ def nodes_to_dict_of_dataframes(grid, nodes, buses_df, generators_df, loads_df,
         Dataframe of generators with entries name, bus, control, p_nom, type,
         weather_cell_id, subtype
     loads_df: :pandas:`pandas.DataFrame<dataframe>`
-        Dataframe of loads with entries name,bus,peak_load, annual_consumption,
-        sector
+        Dataframe of loads with entries name,bus,p_set, building_id,
+        annual_consumption,sector
     transformer_df: :pandas:`pandas.DataFrame<dataframe>`
         Dataframe of trafos with entries name, bus0, bus1, x, r, s_nom, type
     only_export_mv: :obj:`bool`
@@ -319,17 +386,40 @@ def nodes_to_dict_of_dataframes(grid, nodes, buses_df, generators_df, loads_df,
         return_time_varying_data is True empty dict otherwise.
     """
 
+    def next_branch_is_helper_component(node):
+        import networkx as nx
+        if isinstance(node, LVLoadDing0):
+            graph = node.grid.graph
+        elif isinstance(node, MVLoadDing0):
+            graph = node.grid.mv_grid.graph
+        elif isinstance(node, GeneratorDing0):
+            if node.lv_grid is not None:
+                graph = node.lv_grid.graph
+            else:
+                graph = node.mv_grid.graph
+        else:
+            TypeError("No ding0 load!")
+        adjacent_edges = list(nx.edges(graph, node))
+        if len(adjacent_edges) == 1:
+            branch = graph.get_edge_data(
+                adjacent_edges[0][0], adjacent_edges[0][1]
+            )["branch"]
+            if branch.helper_component:
+                return list(graph.neighbors(node))[0]
+            else:
+                return False
+        else:
+            ValueError("More than one adjacent branch!")
+
     srid = int(cfg_ding0.get('geo', 'srid'))
     # check if there are islanded nodes which do not belong to aggregated
     # load area
     for isl_node in grid.graph_isolated_nodes():
         if isinstance(isl_node, CircuitBreakerDing0):
             continue
-        elif isl_node.lv_load_area.is_aggregated:
+        elif isl_node.lv_load_area.is_aggregated: #TODO consider aggregated
             continue
-        elif isinstance(isl_node, LVStationDing0) \
-                and isl_node.peak_load == 0 \
-                and isl_node.peak_generation == 0:
+        elif isinstance(isl_node, LVStationDing0):
             # Todo: This is only a workaround for empty grids (see
             #  build_grid.py line 800). Aim should be to solve the issue
             #  there and remove this exception here afterwards
@@ -374,37 +464,38 @@ def nodes_to_dict_of_dataframes(grid, nodes, buses_df, generators_df, loads_df,
     for node in nodes:
         if node not in grid.graph_isolated_nodes():
             # buses only
-            if isinstance(node, MVCableDistributorDing0) or \
-                    isinstance(node, LVCableDistributorDing0):
+            if isinstance(node, CableDistributorDing0):
                 buses_df = append_buses_df(buses_df, grid, node)
                 # add time varying elements
                 if return_time_varying_data:
-                    bus_v_mag_set_df = \
-                        append_bus_v_mag_set_df(bus_v_mag_set_df, node)
+                    bus_v_mag_set_df = append_bus_v_mag_set_df(bus_v_mag_set_df, node)
       
             # slack generator
             elif isinstance(node, MVStationDing0):
                 # add dummy generator
-                slack = pd.Series({'name':('_'.join(['MV', str(grid.id_db),
-                                                     'slack'])),
-                                   'bus':node.pypsa_bus_id, 'control':'Slack',
-                                   'p_nom':0, 'type': 'station',
-                                   'subtype':'mv_station'})
+                slack = pd.Series({'name': ('_'.join(['MV', str(grid.id_db), 'slack'])),
+                                   'bus': node.pypsa_bus_id,
+                                   'control': 'Slack',
+                                   'p_nom': 0,
+                                   'type': 'station',
+                                   'subtype': 'mv_station'})
                 generators_df = generators_df.append(slack, ignore_index=True)
                 # add MV side bus
                 buses_df = append_buses_df(buses_df, grid, node)
                 # add time varying elements
                 if return_time_varying_data:
-                    slack_v_mag = pd.Series({'name':node.pypsa_bus0_id,
-                                             'temp_id':1,
-                                             'v_mag_pu_set':
-                                                 [voltage_set_slack,
-                                                  voltage_set_slack]})
-                    bus_v_mag_set_df = \
-                        bus_v_mag_set_df.append(slack_v_mag, ignore_index=True)
+                    slack_v_mag = pd.Series({
+                        'name':node.pypsa_bus0_id,
+                        'temp_id':1,
+                        'v_mag_pu_set': [voltage_set_slack,  voltage_set_slack]
+                    })
+                    bus_v_mag_set_df = bus_v_mag_set_df.append(
+                        slack_v_mag, ignore_index=True
+                    )
                     slack_v_mag['name'] = node.pypsa_bus_id
-                    bus_v_mag_set_df = \
-                        bus_v_mag_set_df.append(slack_v_mag, ignore_index=True)
+                    bus_v_mag_set_df = bus_v_mag_set_df.append(
+                        slack_v_mag, ignore_index=True
+                    )
 
             # other generators
             elif isinstance(node, GeneratorDing0):
@@ -413,68 +504,80 @@ def nodes_to_dict_of_dataframes(grid, nodes, buses_df, generators_df, loads_df,
                 else:
                     branches = node.mv_grid.graph_branches_from_node(node)
                 # check if generator is in building generator
-                if len(branches) == 1 and hasattr(branches[0][0],
-                                                  'in_building') \
-                        and branches[0][0].in_building \
-                        and branches[0][1]['branch'].length == 1:
-                    # append generator to preceding bus
-                    generators_df = append_generators_df(
-                        generators_df, node,
-                        name_bus=branches[0][0].pypsa_bus_id)
-                    if return_time_varying_data:
-                        generator_pq_set_df = \
-                            append_generator_pq_set_df(
-                                conf, generator_pq_set_df, node)
+                neighbor = next_branch_is_helper_component(node)
+                if neighbor:
+                    bus_name = neighbor.pypsa_bus_id
                 else:
-                    # append generator and new bus
-                    generators_df = append_generators_df(generators_df, node)
+                    bus_name = node.pypsa_bus_id
                     buses_df = append_buses_df(buses_df, grid, node)
-                    # add time varying elements
                     if return_time_varying_data:
-                        bus_v_mag_set_df = \
-                            append_bus_v_mag_set_df(bus_v_mag_set_df, node)
-                        generator_pq_set_df = \
-                            append_generator_pq_set_df(conf,
-                                                       generator_pq_set_df,
-                                                       node)
+                        bus_v_mag_set_df = append_bus_v_mag_set_df(
+                            bus_v_mag_set_df, node, node_name=bus_name
+                        )
+                generators_df = append_generators_df(
+                    generators_df,
+                    node,
+                    name_bus=bus_name
+                )
+                # add time varying elements
+                if return_time_varying_data:
+                    generator_pq_set_df = append_generator_pq_set_df(
+                        conf, generator_pq_set_df, node
+                    )
 
             elif isinstance(node, LoadDing0):
-                # choose sector with highest consumption and assign sector
-                # accordingly
-                # Todo: replace when loads are seperated in a cleaner way
-                #  (retail, industrial)
-                sorted_consumption = [(value, key) for key, value in
-                                      node.consumption.items()]
-                sector = max(sorted_consumption)[1]
+                # egon data buildings do not contain consumptional data
+                # consumption is not considered (at the moment)
+                if consider_consumption:
+                    # choose sector with highest consumption and assign sector
+                    # accordingly
+                    # Todo: replace when loads are seperated in a cleaner way
+                    #  (retail, industrial)
+                    sorted_consumption = [
+                        (value, key) for key, value in node.consumption.items()
+                    ]
+                    sector = max(sorted_consumption)[1]
+                    annual_consumption = node.consumption[sector]/1e3
+                else:
+                    sector, annual_consumption = None, None
                 # check whether load is in building
-                branches = node.grid.graph_branches_from_node(node)
-                if len(branches) == 1 and hasattr(branches[0][0],
-                                                  'in_building') \
-                        and branches[0][0].in_building \
-                        and branches[0][1]['branch'].length == 1:
-                    # connect load to preceding bus if is in building
-                    bus_name = branches[0][0].pypsa_bus_id
+
+                neighbor = next_branch_is_helper_component(node)
+                if neighbor:
+                    bus_name = neighbor.pypsa_bus_id
                 else:
-                    # add new bus to connect load to
                     bus_name = node.pypsa_bus_id
                     buses_df = append_buses_df(buses_df, grid, node)
                     if return_time_varying_data:
-                        bus_v_mag_set_df = \
-                            append_bus_v_mag_set_df(bus_v_mag_set_df, node)
+                        bus_v_mag_set_df = append_bus_v_mag_set_df(
+                            bus_v_mag_set_df, node, node_name=bus_name
+                        )
+
                 # add load
-                load = pd.Series({'name': repr(node), 'bus': bus_name,
-                                  'peak_load': node.peak_load/1e3,
-                                  'annual_consumption':
-                                      node.consumption[sector]/1e3,
-                                  'sector': sector})
-                loads_df = loads_df.append(load, ignore_index=True)
-                buses_df = append_buses_df(buses_df, grid, node)
-                # add time varying elements
-                if return_time_varying_data:
-                    bus_v_mag_set_df = \
-                        append_bus_v_mag_set_df(bus_v_mag_set_df, node)
-                    load_pq_set_df = \
-                        append_load_pq_set_df(conf, load_pq_set_df, node)
+                for sector in ["residential", "cts", "industrial"]:
+                    if sector == "residential":
+                        number_households = node.number_households
+                    else:
+                        number_households = 0
+                    sector_peak_load = getattr(node, f"peak_load_{sector}")
+                    if sector_peak_load:
+                        load = pd.Series({'name': f"{repr(node)}_{sector}",
+                                          'bus': bus_name,
+                                          'p_set': sector_peak_load/1e3,
+                                          'number_households': int(number_households),
+                                          'building_id': node.building_id,
+                                          'annual_consumption': annual_consumption,
+                                          'sector': sector,
+                                          'type': node.type})
+                        loads_df = loads_df.append(load, ignore_index=True)
+                        # add time varying elements
+                        if return_time_varying_data:
+                            load_pq_set_df = append_load_pq_set_df(
+                                conf,
+                                load_pq_set_df,
+                                node,
+                                node_name=f"{repr(node)}_{sector}"
+                            )
 
             # aggregated load at hv/mv substation
             elif isinstance(node, LVLoadAreaCentreDing0):
@@ -493,16 +596,14 @@ def nodes_to_dict_of_dataframes(grid, nodes, buses_df, generators_df, loads_df,
                                                            transformer_df)
                     if return_time_varying_data:
                         loads_df, generators_df, load_pq_set_df, \
-                        generator_pq_set_df = \
-                            append_load_areas_to_df(loads_df, generators_df,
-                                                    node,
-                                                    return_time_varying_data,
-                                                    conf=conf,
-                                                    load_pq_set_df=
-                                                    load_pq_set_df,
-                                                    generator_pq_set_df=
-                                                    generator_pq_set_df,
-                                                    node_name=node_name)
+                        generator_pq_set_df = append_load_areas_to_df(
+                                loads_df, generators_df,
+                                node,
+                                return_time_varying_data,
+                                conf=conf,
+                                load_pq_set_df=load_pq_set_df,
+                                generator_pq_set_df=generator_pq_set_df,
+                        )
                     else:
                         loads_df, generators_df = \
                             append_load_areas_to_df(loads_df, generators_df,
@@ -665,7 +766,7 @@ def append_load_pq_set_df(conf, load_pq_set_df, node, node_name = None,
     node: obj:node object of generator
     node_name: :obj:`str`
         Optional parameter for name of load
-    peak_load: :obj:`float`
+    p_set: :obj:`float`
         Optional parameter for peak_load
 
     Returns
@@ -708,11 +809,14 @@ def append_bus_v_mag_set_df(bus_v_mag_set_df, node, node_name = None):
     """
     if node_name is None:
         node_name = node.pypsa_bus_id
-    bus_v_mag_set_df = bus_v_mag_set_df.append(pd.Series({'name': node_name,
-                                                          'temp_id': 1,
-                                                          'v_mag_pu_set':
+
+    # due to lv loads may have same connection bus, just append once
+    if node_name not in bus_v_mag_set_df['name'].tolist():
+        bus_v_mag_set_df = bus_v_mag_set_df.append(pd.Series({'name': node_name,
+                                                              'temp_id': 1,
+                                                              'v_mag_pu_set':
                                                               [1, 1]}),
-                                               ignore_index=True)
+                                                   ignore_index=True)
     return bus_v_mag_set_df
 
 
@@ -731,7 +835,7 @@ def append_load_areas_to_df(loads_df, generators_df, node,
     Parameters
     ----------
     loads_df: :pandas:`pandas.DataFrame<dataframe>`
-        Dataframe of loads with entries name, bus, peak_load,
+        Dataframe of loads with entries name, bus, p_set,
         annual_consumption, sector
     generators_df: :pandas:`pandas.DataFrame<dataframe>`
         Dataframe of generators with entries name, bus, control, p_nom, type,
@@ -747,7 +851,7 @@ def append_load_areas_to_df(loads_df, generators_df, node,
     Returns
     -------
     loads_df: :pandas:`pandas.DataFrame<dataframe>`
-        Dataframe of loads with entries name, bus, peak_load,
+        Dataframe of loads with entries name, bus, p_set,
         annual_consumption, sector
     generators_df: :pandas:`pandas.DataFrame<dataframe>`
         Dataframe of generators with entries name, bus, control, p_nom, type,
@@ -796,9 +900,29 @@ def append_load_areas_to_df(loads_df, generators_df, node,
 
 
     # Handling of loads
-    sectors = ['agricultural', 'industrial', 'residential', 'retail']
+    # egon data buildings do not contain consumptional data
+    # sector specific consumption is not considered (at the moment)
+    if consider_consumption:
+        sectors = ['agricultural', 'industrial', 'residential', 'retail']
+    else:
+        sectors = [None]
+
     for sector in sectors:
-        if (getattr(load_area, '_'.join(['peak_load', sector]))!= 0):
+        if sector:
+            if (getattr(load_area, '_'.join(['peak_load', sector]))!= 0):
+                if return_time_varying_data:
+                    loads_df, load_pq_set_df = \
+                        append_load_area_to_load_df(sector, load_area, loads_df,
+                                                    name_bus, name_load,
+                                                    return_time_varying_data,
+                                                    conf=conf,
+                                                    load_pq_set_df=load_pq_set_df)
+                else:
+                    loads_df = \
+                        append_load_area_to_load_df(sector, load_area,
+                                                    loads_df, name_bus, name_load)
+        # return data without sector specific resolution (egon data buildings)
+        else:
             if return_time_varying_data:
                 loads_df, load_pq_set_df = \
                     append_load_area_to_load_df(sector, load_area, loads_df,
@@ -827,11 +951,11 @@ def append_load_area_to_load_df(sector, load_area, loads_df, name_bus,
     ----------
     sector: str
         load sector: 'agricultural', 'industrial', 'residential' or 'retail'
-    load_are: :obj: ding0 region
+    load_area: :obj: ding0 region
         LVGridDistrictDing0 or LVLoadAreaDing0, load area of which load is to
         be aggregated and added
     loads_df: :pandas:`pandas.DataFrame<dataframe>`
-        Dataframe of loads with entries name, bus, peak_load,
+        Dataframe of loads with entries name, bus, p_set,
         annual_consumption and sector
     name_bus: :obj:`str`
         name of bus to which load is connected
@@ -846,38 +970,60 @@ def append_load_area_to_load_df(sector, load_area, loads_df, name_bus,
     Returns
     -------
     loads_df: :pandas:`pandas.DataFrame<dataframe>`
-        Dataframe of loads with entries name, bus, peak_load,
+        Dataframe of loads with entries name, bus, p_set,
         annual_consumption and sector
     load_pq_set_df: :pandas:`pandas.DataFrame<dataframe>`
         Dataframe of loads with entries name, temp_id, p_set and q_set,
         only exported if return_time_varying_data is True
     """
-    # get annual consumption
-    if isinstance(load_area, LVGridDistrictDing0):
-        consumption = getattr(load_area, '_'.join(['sector_consumption',
-                                                   sector]))
-    elif isinstance(load_area,LVLoadAreaDing0):
-        consumption = 0
-        for lv_grid_district in load_area.lv_grid_districts():
-            consumption += getattr(lv_grid_district,
-                                   '_'.join(['sector_consumption', sector]))
-    # create and append load to df
-    name_load = '_'.join([name_load, sector])
-    peak_load = getattr(load_area, '_'.join(['peak_load', sector]))
-    load = pd.Series(
-        {'name': name_load, 'bus': name_bus,
-         'peak_load': peak_load/1e3,
-         'annual_consumption': consumption/1e3, 'sector': sector})
-    loads_df = loads_df.append(load, ignore_index=True)
-    # handle time varying data
-    if return_time_varying_data:
-        conf = kwargs.get('conf', None)
-        load_pq_set_df = kwargs.get('load_pq_set_df', None)
-        load_pq_set_df = append_load_pq_set_df(conf, load_pq_set_df,
-                                               None, name_load, peak_load)
-        return loads_df,load_pq_set_df
+    # if egon data buildings used, no sector-specific consumptional data are available
+    # consumption is set to None
+    if not sector:
+
+        consumption = None
+        peak_load = load_area.peak_load
+        load = pd.Series(
+            {'name': name_load, 'bus': name_bus,
+             'p_set': peak_load / 1e3,
+             'annual_consumption': consumption, 'sector': sector})
+        loads_df = loads_df.append(load, ignore_index=True)
+        # handle time varying data
+        if return_time_varying_data:
+            conf = kwargs.get('conf', None)
+            load_pq_set_df = kwargs.get('load_pq_set_df', None)
+            load_pq_set_df = append_load_pq_set_df(conf, load_pq_set_df,
+                                                   None, name_load, peak_load)
+            return loads_df, load_pq_set_df
+        else:
+            return loads_df
+
     else:
-        return loads_df
+        # get annual consumption
+        if isinstance(load_area, LVGridDistrictDing0):
+            consumption = getattr(load_area, '_'.join(['sector_consumption',
+                                                       sector]))
+        elif isinstance(load_area,LVLoadAreaDing0):
+            consumption = 0
+            for lv_grid_district in load_area.lv_grid_districts():
+                consumption += getattr(lv_grid_district,
+                                       '_'.join(['sector_consumption', sector]))
+        # create and append load to df
+        name_load = '_'.join([name_load, sector])
+        peak_load = getattr(load_area, '_'.join(['peak_load', sector])) # TODO change sctor specific load allocation
+        load = pd.Series(
+            {'name': name_load, 'bus': name_bus,
+             'p_set': peak_load/1e3,
+             'annual_consumption': consumption/1e3, 'sector': sector})
+        loads_df = loads_df.append(load, ignore_index=True)
+        # handle time varying data
+        if return_time_varying_data:
+            conf = kwargs.get('conf', None)
+            load_pq_set_df = kwargs.get('load_pq_set_df', None)
+            load_pq_set_df = append_load_pq_set_df(conf, load_pq_set_df,
+                                                   None, name_load, peak_load)
+            return loads_df, load_pq_set_df
+        else:
+            return loads_df
 
 
 def append_generators_df(generators_df, node, name_bus = None):
@@ -1028,7 +1174,8 @@ def append_transformers_df(transformers_df, trafo, type = np.NaN,
                            'bus0': bus0,
                            'bus1': bus1,
                            'x': trafo.x_pu, 'r': trafo.r_pu,
-                           's_nom': trafo.s_max_a/1e3, 'type': type,
+                           's_nom': trafo.s_max_a/1e3,
+                           'type': type,
                            'type_info': type_info})
     transformers_df = transformers_df.append(trafo_tmp, ignore_index=True)
     return transformers_df
@@ -1056,7 +1203,7 @@ def edges_to_dict_of_dataframes(edges, lines_df, buses_df):
 
     # iterate over edges and add them one by one
     for edge in edges:
-        if not edge['branch'].connects_aggregated:
+        if not edge['branch'].connects_aggregated: #TODO
             lines_df = append_lines_df(edge, lines_df, buses_df)
 
     return {'Line': lines_df.set_index('name')}
@@ -1072,7 +1219,7 @@ def append_lines_df(edge, lines_df, buses_df):
         Edge of Ding0.Network graph
     lines_df: :pandas:`pandas.DataFrame<dataframe>`
             Dataframe of lines with entries name, bus0, bus1, length, x, r,
-            s_nom, num_parallel, type
+            s_nom, num_parallel, type, geometry
     buses_df: :pandas:`pandas.DataFrame<dataframe>`
         Dataframe of buses with entries name, v_nom, geom, mv_grid_id,
         lv_grid_id, in_building
@@ -1083,6 +1230,9 @@ def append_lines_df(edge, lines_df, buses_df):
             Dataframe of lines with entries name, bus0, bus1, length, x, r,
             s_nom, num_parallel, type
     """
+    if edge["branch"].helper_component:
+        return lines_df
+
     freq = cfg_ding0.get('assumptions', 'frequency')
     omega = 2 * pi * freq
     # set grid_ids
@@ -1099,7 +1249,6 @@ def append_lines_df(edge, lines_df, buses_df):
     if (isinstance(edge['branch'].type['L_per_km'], Series)):
         x_per_km = omega * edge['branch'].type['L_per_km'].values[0] * 1e-3
     else:
-
         x_per_km = omega * edge['branch'].type['L_per_km'] * 1e-3
     if isinstance(edge['branch'].type['R_per_km'], Series):
         r_per_km = edge['branch'].type['R_per_km'].values[0]
@@ -1114,6 +1263,9 @@ def append_lines_df(edge, lines_df, buses_df):
                 edge['branch'].type['U_n']/unitconversion
     # get lengths of line
     length = edge['branch'].length / 1e3
+    # Add minimal length of 1m
+    if length < 0.001:
+        length = 0.001
     #Todo: change into same format
     if 'name' in edge['branch'].type:
         type = edge['branch'].type['name']
@@ -1130,20 +1282,17 @@ def append_lines_df(edge, lines_df, buses_df):
     else:
         name_bus1 = edge['adj_nodes'][1].pypsa_bus_id
 
-    # check if line is to be added to lines_df
-    add_line = _check_branch_for_in_building_buses(buses_df, edge,
-                                                   name_bus0, name_bus1)
-
-    # create new line
-    if add_line:
-        line = pd.Series({'name': repr(edge['branch']),
-                          'bus0': name_bus0,
-                          'bus1': name_bus1,
-                          'x': x_per_km * length, 'r':r_per_km * length,
-                          's_nom': s_nom, 'length': length,
-                          'num_parallel': 1, 'kind': edge['branch'].kind,
-                          'type_info': type})
-        lines_df = lines_df.append(line, ignore_index=True)
+    line = pd.Series({'name': repr(edge['branch']),
+                      'bus0': name_bus0,
+                      'bus1': name_bus1,
+                      'x': x_per_km * length, 'r':r_per_km * length,
+                      's_nom': s_nom, 'length': length,
+                      'num_parallel': edge['branch'].num_parallel,
+                      'kind': edge['branch'].kind,
+                      'type_info': type,
+                      'geometry': edge['branch'].geometry})
+    lines_df = lines_df.append(line, ignore_index=True)
+
     return lines_df
 
 
@@ -1393,20 +1542,28 @@ def data_integrity(components, components_data):
 
     data_check = {}
 
-    for comp in ['Bus', 'Load']:  # list(components_data.keys()):
+    for comp in ['Bus', 'Load', 'Generator']:  # list(components_data.keys()):
         data_check[comp] = {}
-        data_check[comp]['length_diff'] = len(components[comp]) - len(
-            components_data[comp])
+        if comp == "Bus":
+            # HV Bus is not in components but in components_data
+            offset = 1
+        elif comp == "Generator":
+            # Slack Generator is in components but not in components_data
+            offset = -1
+        else:
+            offset = 0
+        data_check[comp]['length_diff'] = (
+                len(components[comp]) - len(components_data[comp]) + offset
+        )
 
     # print short report to user and exit program if not integer
     for comp in list(data_check.keys()):
         if data_check[comp]['length_diff'] != 0:
-            logger.exception("{comp} data is invalid. You supplied {no_comp} "
+            logger.error("{comp} data is invalid. You supplied {no_comp} "
                 "{comp} objects and {no_data} datasets. Check you grid data "
                 "and try again".format(comp=comp,
                                      no_comp=len(components[comp]),
                                      no_data=len(components_data[comp])))
-            sys.exit(1)
 
 
 def _check_integrity_of_pypsa(pypsa_network):
diff --git a/ding0/tools/write_openego_header.py b/ding0/tools/write_openego_header.py
old mode 100755
new mode 100644
diff --git a/doc/development.rst b/doc/development.rst
index cdc25f54..2f354342 100644
--- a/doc/development.rst
+++ b/doc/development.rst
@@ -113,3 +113,20 @@ This suite will run three tests:
 
 * Compare the results of two fresh runs of ding0 in district [3545].
 
+Database integration
+====================
+
+Different databases can be used. All communication between ding0 and the used database happens in
+`ding0/tools/egon_data_integration.py`. The function to start a session is in `ding0/tools/database.py`.
+Start the session with a with statement:
+
+.. code-block:: python
+
+    from ding0.tools import database
+    from ding0.core import NetworkDing0
+    with database.session_scope() as session:
+        nd = NetworkDing0(name="network", session=session)
+
+The Object-relational mapping is done in `core.NetworkDing0.import_orm`. The ORM uses sqlalchemy and saio. The mapping
+is configured in `ding0/config/config_db_tables.cfg`. You can select there the database type
+(model_draft/versioned/local) and submit your local database credentials.
diff --git a/export_grids.py b/export_grids.py
deleted file mode 100644
index 10bc3e90..00000000
--- a/export_grids.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from ding0.tools.results import export_network, load_nd_from_pickle, export_data_tocsv
-import json
-
-
-grid_id = '76';
-base_path = "/home/local/RL-INSTITUT/inga.loeser/ding0/20170922152523/ding0_grids__"
-path = ''.join([base_path, grid_id])
-
-# Load network and run export function
-nw = load_nd_from_pickle(filename=''.join([path, '.pkl']))
-run_id, lv_grid, lv_gen, lv_cd, lv_stations, lv_trafos, lv_loads, mv_grid, mv_gen, mv_cb, mv_cd, mv_stations, mv_trafos, mv_loads, edges, mapping = export_network(nw)
-
-# Create directory for exported data
-import os
-if not os.path.exists(os.path.join(path, run_id)):
-    os.makedirs(os.path.join(path, run_id))
-
-export_data_tocsv(path, run_id, lv_grid, lv_gen, lv_cd, lv_stations, lv_trafos, lv_loads, mv_grid, mv_gen, mv_cb, mv_cd, mv_stations, mv_trafos, mv_loads, edges, mapping )
-
-
-# Save metadata
-
-metadata = nw.metadata #keys: version, mv_grid_districts, database_tables, data_version, assumptions, run_id
-with open(os.path.join(path, run_id, 'Ding0_{}.meta'.format(run_id)),'w') as f: #'Ding0.meta'),'w') as f: #
-    json.dump(metadata, f)
-
-print(mapping)
diff --git a/manual_requirements.txt b/manual_requirements.txt
new file mode 100644
index 00000000..8308dd8a
--- /dev/null
+++ b/manual_requirements.txt
@@ -0,0 +1,1 @@
+git+https://github.com/networkx/networkx-metis.git
diff --git a/requirements.txt b/requirements.txt
index 8004d363..cd85f647 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,10 +6,21 @@ sqlalchemy <= 1.3.24
 geoalchemy2 == 0.6.3
 matplotlib >= 1.5.3, <= 3.4.2
 egoio == 0.4.8
-shapely == 1.7.1
+shapely >= 1.8, < 2.0.0
 pypsa >= 0.11.0, <= 0.17.1
 seaborn == 0.11.1
 unittest2 == 1.1.0
 oedialect == 0.0.10
-geopandas == 0.9.0
-descartes == 1.1.0
\ No newline at end of file
+geopandas >= 0.9.0
+descartes == 1.1.0
+osmnx~=1.1.1
+ding0~=0.2.1
+numpy~=1.21.2
+pyyaml~=6.0
+saio~=0.2.1
+scipy~=1.7.1
+scikit-learn~=0.24.2
+# requests~=2.26.0
+contextily~=1.2.0
+argparse~=1.4.0
+setuptools~=57.4.0


Test patch:
diff --git a/tests/tools/test_database.py b/tests/tools/test_database.py
new file mode 100644
index 00000000..46e8a642
--- /dev/null
+++ b/tests/tools/test_database.py
@@ -0,0 +1,14 @@
+from ding0.tools import database
+
+class TestDatabase:
+    def test_oedb(self):
+        with database.session_scope(overwrite_database="oedb") as session:
+            print()
+            print(session)
+
+    def test_local_database(self):
+        with database.session_scope("local") as session:
+            print()
+            print(session)
+
+
diff --git a/tests/tools/test_egon_data_integration.py b/tests/tools/test_egon_data_integration.py
new file mode 100644
index 00000000..2d90be33
--- /dev/null
+++ b/tests/tools/test_egon_data_integration.py
@@ -0,0 +1,149 @@
+import pytest
+import subprocess
+
+from ding0.tools import database
+from ding0.tools import egon_data_integration as db_io
+from ding0.core import NetworkDing0
+import pandas as pd
+
+class TestEgonDataIntegration:
+    @pytest.fixture
+    def session(self):
+        with database.session_scope() as session:
+            self.nd = NetworkDing0(name="network", session=session)
+            print("Open session")
+            yield session
+            print("Close session")
+
+    @pytest.fixture
+    def grid_id(self, session):
+        import saio
+        import geopandas as gpd
+
+        engine = session.bind
+
+        saio.register_schema("grid", engine)
+        saio.register_schema("boundaries", engine)
+
+        from saio.grid import egon_hvmv_substation as MVGridDistricts
+        from saio.boundaries import vg250_lan
+
+        table = MVGridDistricts
+        cells_query = session.query(
+            table.bus_id.label("subst_id"), table.point.label("geom")
+        )
+        df = gpd.read_postgis(
+            sql=cells_query.statement, con=cells_query.session.bind, index_col=None
+        )
+
+        cells_query = session.query(
+            vg250_lan.gen.label("name"), vg250_lan.geometry.label("geom")
+        ).filter(vg250_lan.gf == 4)
+
+        boundaries_gdf = gpd.read_postgis(
+            sql=cells_query.statement,
+            con=cells_query.session.bind,
+            index_col="name",
+        )
+
+        df = df[
+            df.geometry.intersects(boundaries_gdf.loc["Schleswig-Holstein", "geom"])
+        ]["subst_id"]
+
+        print("Got the DATA!")
+        return [int(df[1])]
+
+    @pytest.fixture
+    def grid_id_2(self, session):
+
+        sql_statement = """
+        SELECT *
+        FROM
+        (
+            SELECT
+                generators.bus_id,
+                count(*) AS count,
+                count(*) FILTER (WHERE generators.type = 'wind') AS wind,
+                count(*) FILTER (WHERE generators.type = 'pv_open_place') AS pv_open_place,
+                count(*) FILTER (WHERE generators.type = 'pv_rooftop') AS pv_rooftop,
+                count(*) FILTER (WHERE generators.type = 'biomass') AS biomass,
+                count(*) FILTER (WHERE generators.type = 'hydro') AS hydro
+            FROM
+            (
+                (
+                    SELECT bus_id, 'wind' AS type
+                    FROM supply.egon_power_plants_wind
+                )
+            UNION ALL
+                (
+                    SELECT bus_id, 'pv_open_place' AS type
+                    FROM supply.egon_power_plants_pv
+                )
+            UNION ALL
+                (
+                    SELECT bus_id, 'pv_rooftop' AS type
+                    FROM supply.egon_power_plants_pv_roof_building
+                )
+            UNION ALL
+                (
+                    SELECT bus_id, 'biomass' AS type
+                    FROM supply.egon_power_plants_biomass
+                )
+            UNION ALL
+                (
+                    SELECT bus_id, 'hydro' AS type
+                    FROM supply.egon_power_plants_hydro
+                )
+            )
+            AS generators
+            GROUP BY generators.bus_id
+            ORDER BY count DESC
+        )
+        AS
+        test
+        WHERE test.wind > 1 AND test.pv_open_place > 1 AND test.pv_rooftop > 1 AND test.biomass > 1 AND test.hydro > 1
+        """
+        df = pd.read_sql_query(
+            sql=sql_statement,
+            con=session.bind,
+            index_col=None,
+        )
+
+        return [int(df.bus_id[0])]
+
+    @pytest.fixture
+    def lv_load_area(self, session, grid_id):
+        print(grid_id)
+        df = db_io.get_lv_load_areas(self.nd.orm, session, grid_id[0])
+        return df.loc[3479]  # df.loc[3479,"geo_area"]
+
+    def test_import_mvgd(self, session, grid_id):
+        print(grid_id)
+        df = db_io.get_mv_data(self.nd.orm, session, grid_id)
+
+        assert True
+
+    def test_import_load_area(self, session, grid_id):
+        df = db_io.get_lv_load_areas(self.nd.orm, session, grid_id[0])
+
+        assert True
+
+    def test_get_egon_ways(self, session, lv_load_area):
+        df = db_io.get_egon_ways(self.nd.orm, session, lv_load_area.geo_area)
+
+        assert True
+
+    def test_get_egon_buildings(self, session, grid_id, lv_load_area):
+        df = db_io.get_egon_buildings(self.nd.orm, session, grid_id[0], lv_load_area)
+
+        assert True
+
+    def test_get_res_generators(self, session, grid_id_2):
+        df = db_io.get_res_generators(self.nd.orm, session, grid_id_2[0])
+
+        assert True
+
+    def test_get_conv_generators(self, session, grid_id):
+        df = db_io.get_conv_generators(self.nd.orm, session, grid_id[0])
+
+        assert True