Skip to content

Synthetic Feeder, Chennai

Generating synthetic feeder using OpenStreet data

Developed by: Kapil Duwadi, 2022-07-31

In this tutorial you will build synthetic feeder for Chennai, India. You will perform following tasks to achieve final goal.

1. Preparing sandbox environment

To proceed with this tutorial, let's create a sandbox environment. It will take about a minute to make the environment ready.

Create a sandbox

Verify the screen you are seeing.

Creating your own environment (optional)

If you encounter problem launching sandbox or prefer using jupyter notebook in your own computer please follow the following steps. In windows we recommend using Anaconda or Miniconda to create the environment.

If you need help installing Anaconda please follow the instructions here.

conda create -n shift python==3.9
conda activate shift
conda install -c conda-forge osmnx
git clone https://github.com/NREL/shift.git
cd shift
pip install -e.
pip install jupyterlab
jupyter lab
conda create -n shift python==3.9
conda activate shift
git clone https://github.com/NREL/shift.git
cd shift
pip install -e.
pip install jupyterlab
jupyter lab

Tip

If you are using python virtual environment make sure you have python 3.9 or greater installed in your system.

Click on + symbol or Python Kernel to create a Jupyter Notebook.

You have sucessfully created an environment.


2. Get all the buildings data and create geometry object for all buildings.

Let's get all the geometries from Chennai, India. Copy and paste the following code in your jupyter notebook.

from shift.geometry import BuildingsFromPlace
g = BuildingsFromPlace("Chennai, India", max_dist=300)
geometries = g.get_geometries()
print(f"Total number of buildings: {len(geometries)}" +
f"\nSample geometry: {geometries[0]}")
Verify the output you are seeing.
Total number of buildings: 216
Sample geometry: Building( Latitude = 13.081990729184426,  Longitude = 80.27267988235701, Area = 4.81)

You have sucessfully fetched all the buildings.


3. Convert building geometries into load objects

In order to create power system loads from geometries we need to tell how to assign phases, voltages and connection type for loads. In this case let's use RandomPhaseAllocator class to allocate phases to all geometries. Here we are saying all geometries are of single phase type and there are no two phase and three phase loads and finally pass all the geometries.

Similarly we initialize simple voltage setter by passing line to line voltage of 13.2 kV. DefaultConnSetter class is created which will set the connection type to wye for all geometries.

from shift.load_builder import (RandomPhaseAllocator, 
                                SimpleVoltageSetter, DefaultConnSetter)
rpa = RandomPhaseAllocator(100, 0, 0, geometries)
svs = SimpleVoltageSetter(13.2)
dcs = DefaultConnSetter()

But wait how do we get power consumption data for the load. In order get consumption let's use building area info to get the kw. Let's say we know there is a piecewise linear function that relates building area with peak kW consumption.

import matplotlib.pyplot as plt
import plotly.express as px
import pandas as pd

area_to_kw_curve = [(0,5), (10, 5.0), (20, 18), (50, 30)]

df = pd.DataFrame({
    'Area in meter square' : [x[0] for x in area_to_kw_curve],
    'Peak consumption in kW' : [x[1] for x in area_to_kw_curve]
})
fig = px.line(df, x="Area in meter square", y="Peak consumption in kW") 
fig.show()
Verify the output you are seeing.

In order to use this piecewise linear function we can invoke PiecewiseBuildingAreaToConsumptionConverter class from load_builder and pass it as an argument to build the load. Let's see this class in action.

from shift.load_builder import PiecewiseBuildingAreaToConsumptionConverter
pbacc = PiecewiseBuildingAreaToConsumptionConverter(area_to_kw_curve)
area = 15
print(f"For area of {area} m^2 the consumption would be {pbacc.convert(area)}")
Verify the output you are seeing.
For area of 15 m^2 the consumption would be 11.5

Let's build the loads from the geometries and print one them. Here let's try to build constant power factor of 1.0 type loads for simplicity.

from shift.load_builder import ConstantPowerFactorBuildingGeometryLoadBuilder
from shift.load_builder import LoadBuilderEngineer
loads = []
for g in geometries:
    builder = ConstantPowerFactorBuildingGeometryLoadBuilder(g, 
                            rpa, pbacc, svs, dcs, 1.0)
    b = LoadBuilderEngineer(builder)
    loads.append(b.get_load())
print(len(loads), loads[0], loads[1])
Verify the output you are seeing.
216 ConstantPowerFactorLoad(Name = 80.26965403477561_13.083734923972695_load, 
Latitude = 13.083734923972695, Longitude = 80.26965403477561, Phase = Phase.BN 
NumPhase = NumPhase.SINGLE, Connection Type = LoadConnection.STAR, kw = 5.0, 
pf = 1.0, kv = 7.621) ConstantPowerFactorLoad(Name = 80.2680542788762_13.084093574520553_load, 
Latitude = 13.084093574520553, Longitude = 80.2680542788762, Phase = Phase.BN NumPhase = 
NumPhase.SINGLE, Connection Type = LoadConnection.STAR, kw = 5.0, pf = 1.0, kv = 7.621)

Let's visualize these loads on top of GIS map. That would be cool right ?. In order to do that let's invoke two layer distribution network first.

from shift.feeder_network import (SimpleTwoLayerDistributionNetworkBuilder, 
                                  TwoLayerNetworkBuilderDirector)
from shift.network_plots import PlotlyGISNetworkPlot
from shift.constants import PLOTLY_FORMAT_CUSTOMERS_ONLY

network_builder = SimpleTwoLayerDistributionNetworkBuilder()
network = TwoLayerNetworkBuilderDirector(loads, [], [], [], network_builder)

API_KEY = None
p = PlotlyGISNetworkPlot(
        network.get_network(),
        API_KEY,
        'carto-darkmatter',
        asset_specific_style=PLOTLY_FORMAT_CUSTOMERS_ONLY
    )
p.show()
Verify the output you are seeing.

You have sucessfully converted buildings to power system loads.


4. Use clustering algorithm to cluster loads

Next step is to use clustering algorithm to figure out best location for positioning distribution transformers. Kmeans clustering is one way of doing this. Let's say we want to have 20 disribution transformers for this feeder.

from shift.clustering import KmeansClustering
import numpy as np

x_array = np.array([[load.longitude, load.latitude] 
                    for load in loads])
cluster_ = KmeansClustering(2)
clusters = cluster_.get_clusters(x_array)
cluster_.plot_clusters()
Verify the output you are seeing.

You have sucessfully clustered the load objects.

5. Convert cluster center into distribution transformer objects

Let's use clustering algorithm to position distribution transformers. In order to create transformers from clusters we need to provide list of loads, clustering object, kV and connection type for both high tension and low tension side of transformers along with number of phase to be used to design transformers. The diversity factor function, power factor and adjustment_factor is used to compute kVA capacity for the transformer. The catalog is used to find nearest transformer capacity.

from shift.transformer_builder import (
    ClusteringBasedTransformerLoadMapper)
from shift.enums import (TransformerConnection, 
                         NumPhase, Phase)
from shift.constants import (
    PLOTLY_FORMAT_CUSTOMERS_AND_DIST_TRANSFORMERS_ONLY)

trans_builder = ClusteringBasedTransformerLoadMapper(
    loads,
    clustering_object = cluster_,
    diversity_factor_func = lambda x: 0.3908524*np.log(x) + 1.65180707,
    ht_kv = 13.2,
    lt_kv = 0.4,
    ht_conn = TransformerConnection.DELTA,
    lt_conn = TransformerConnection.STAR,
    ht_phase = Phase.ABC,
    lt_phase = Phase.ABCN,
    num_phase = NumPhase.THREE,
    power_factor=0.9,
    adjustment_factor=1.15
)
t  = trans_builder.get_transformer_load_mapping()

network = TwoLayerNetworkBuilderDirector(loads, 
            list(t.keys()), [], [], network_builder)
p = PlotlyGISNetworkPlot(
        network.get_network(),
        API_KEY,
        'carto-darkmatter',
        asset_specific_style=PLOTLY_FORMAT_CUSTOMERS_AND_DIST_TRANSFORMERS_ONLY
    )
p.show()
Verify the output you are seeing.

You have sucessfully created transformer objects.

6. Get road network and create primary network

Let's try to get road network from chennai india. We will convert the road network to undirected graph and use minimum spanning tree alogorithm to remove any loops.

from shift.graph import RoadNetworkFromPlace
from shift.constants import PLOTLY_FORMAT_SIMPLE_NETWORK
graph = RoadNetworkFromPlace('chennai, india', max_dist=300)
graph.get_network()
p = PlotlyGISNetworkPlot(
            graph.updated_network,
            API_KEY,
            'carto-darkmatter',
            asset_specific_style=PLOTLY_FORMAT_SIMPLE_NETWORK
        )
p.show()
Verify the output you are seeing.

Let's create an instance of Primary Network Builder class and visulaize the generated network.

from shift.primary_network_builder import PrimaryNetworkFromRoad
pnet = PrimaryNetworkFromRoad(
        graph,
        t,
        (80.2786311, 13.091658),
        lambda x: 0.3908524*np.log(x) + 1.65180707,
        13.2,
        100
    )
pnet.update_network_with_ampacity()
p = PlotlyGISNetworkPlot(
            pnet.get_primary_network(),
            API_KEY,
            'carto-darkmatter',
            asset_specific_style=PLOTLY_FORMAT_SIMPLE_NETWORK
        )
p.show()
Verify the output you are seeing.

You have sucessfully fetched road network and created primary network.

7. Convert primary network into list of primary line sections

Let's take above primary network and convert it into primary line sections.

from shift.primary_network_builder import PrimarySectionsBuilder
from shift.enums import ConductorType, NumPhase
from shift.line_section import HorizontalThreePhaseConfiguration
from shift.feeder_network import update_transformer_locations
psections = PrimarySectionsBuilder(
    pnet.get_network(),
    ConductorType.OVERHEAD,
    {NumPhase.THREE: HorizontalThreePhaseConfiguration(9, 0.4, "m")},
    NumPhase.THREE,
    Phase.ABC,
    False,
    material="ACSR",
)
longest_length = pnet.get_longest_length_in_kvameter() / 1609.34
k_drop = 2 / (longest_length)
l_sections = psections.generate_primary_line_sections(k_drop, 11.0)
r_nodes = pnet.get_trans_node_mapping()
print(psections[0])

Verify the output you are seeing.

The output may not be exactly the same which is okay as long as you are seeing something similar.

GeometryBasedLine(Name = 80.2696283_13.0829503_node__80.2697735_13.08341_node, 
FromNode = 80.2696283_13.0829503_node, ToNode = 80.2697735_13.08341_node, Length = 
53.2391782199997 NumPhase = NumPhase.THREE, Length unit = m,  Geometry = OverheadLineGeometry(Name = 475b3162-f00e-4652-be27-cbf953c2b104_linegeometry, NumPhase = NumPhase.THREE, Number of conductors 
= 3, Configuration = <shift.line_section.HorizontalThreePhaseConfiguration object at 0x000001DFDC312610>, 
Phase wire = Wire(Name = 6_6/1_ACSR, Resistance unit = mi, GMR unit = ft, Radius unit = in, AC resistance (ohm per) 
= 3.98, Diameter = 0.198, GMR AC = 0.00394, Ampacity = 100)))

You have sucessfully created primary line sections.

8. Update transformer objects to use nearest primary node from primary network

After creating primary network we need to connect transformers to the primary network. To do this we can simply call update_transformer_locations function.

t = update_transformer_locations(r_nodes, t, l_sections)

You have sucessfully updated transformer locations.

9. Develop secondary network and line sections for all transformer objects

Let's take each transformer and it's load and convert it into secondary network.

from shift.secondary_network_builder import (SecondaryNetworkBuilder, 
    SecondarySectionsBuilder)
from shift.line_section import (HorizontalThreePhaseNeutralConfiguration, 
    HorizontalSinglePhaseConfiguration)
s_sections = []
counter = 0
load_to_node_mapping_dict = {}
for trans, cust_list in t.items():
    sn = SecondaryNetworkBuilder(cust_list, trans, 
        lambda x: 0.3908524*np.log(x) + 1.65180707, 0.4, f"_secondary_{counter}")
    sn.update_network_with_ampacity()
    load_to_node_mapping_dict.update(sn.get_load_to_node_mapping())
    k_drop = 5 / (200 * 2)
    sc = SecondarySectionsBuilder(
        sn.get_network(),
        ConductorType.OVERHEAD,
        {
            NumPhase.THREE: HorizontalThreePhaseNeutralConfiguration(
                9, 0.4, 9.4, "m"
            )
        },
        {
            NumPhase.SINGLE: HorizontalSinglePhaseConfiguration(9, "m"),
        },
        NumPhase.THREE,
        Phase.ABCN,
        True,
        material="ACSR"
    )
    s_sections.extend(sc.generate_secondary_line_sections(k_drop, 0.4))
    counter +=1
Verify the output you are seeing.

You have sucessfully created secondary line sections and network.

10. Create a substation transformer object

Finally let's create a substation transformer.

from shift.transformer_builder import SingleTransformerBuilder
s_node = pnet.substation_node
s_coords = pnet.substation_coords
sub_trans_builder = SingleTransformerBuilder(
    loads,
    s_coords[0],
    s_coords[1],
    diversity_factor_func=lambda x: 0.3908524 * np.log(x) + 1.65180707,
    ht_kv=33,
    lt_kv=11,
    ht_conn=TransformerConnection.DELTA,
    lt_conn=TransformerConnection.STAR,
    ht_phase=Phase.ABC,
    lt_phase=Phase.ABCN,
    num_phase=NumPhase.THREE,
    power_factor=0.9,
    adjustment_factor=1.15,
)
st = sub_trans_builder.get_transformer_load_mapping()

You have sucessfully created substation transformer.

11. Export OpenDSS model

Let's export distribution model in OpenDSS format.

from shift.exporter.opendss import (
ConstantPowerFactorLoadWriter,
TwoWindingSimpleTransformerWriter,
GeometryBasedLineWriter,
OpenDSSExporter
)
lw = ConstantPowerFactorLoadWriter(
    loads, load_to_node_mapping_dict, "loads.dss"
)
tw = TwoWindingSimpleTransformerWriter(list(t.keys()), "dist_xfmrs.dss")
stw = TwoWindingSimpleTransformerWriter(list(st.keys()), "sub_trans.dss")
sw = GeometryBasedLineWriter(
    l_sections + s_sections,
    line_file_name="lines.dss",
    geometry_file_name="line_geometry.dss",
    wire_file_name="wiredata.dss",
    cable_file_name="cabledata.dss",
)


dw = OpenDSSExporter(
    [tw, stw, sw, lw],
    r"C:\Users\KDUWADI\Desktop\NREL_Projects\shift_output",
    "master.dss",
    "chennai",
    33.0,
    50,
    Phase.ABCN,
    NumPhase.THREE,
    f"{s_node.split('_')[0]}_{s_node.split('_')[1]}_htnode",
    [0.001, 0.001],
    [0.001, 0.001],
    [0.4, 11.0, 33.0],
)
dw.export()

You have sucessfully created substation transformer.