Building a Distribution Graph#

Once you have a list of ParcelModel objects (see Fetching Parcels), the next step is to construct a DistributionGraph — the connectivity model of nodes and edges that represents the distribution network.

Cluster Parcels#

First, cluster parcels into groups. Each cluster center becomes a candidate distribution transformer location. A common heuristic is roughly two customers per transformer, but you can adjust this to match your design criteria.

from shift import ParcelModel, GeoLocation, get_kmeans_clusters

def _get_parcel_points(parcels: list[ParcelModel]) -> list[GeoLocation]:
    """Extract a single GeoLocation per parcel (centroid for polygons)."""
    return [
        el.geometry[0] if isinstance(el.geometry, list) else el.geometry
        for el in parcels
    ]

num_clusters = max(len(parcels) // 2, 1)
clusters = get_kmeans_clusters(num_clusters, _get_parcel_points(parcels))

Build the Graph with PRSG#

PRSG (Primary–Secondary Road-network Graph) builds the distribution graph in two steps:

  1. Primary network — derived by routing through the road network in the area using a configurable RoutingStrategy.

  2. Secondary network — built using a configurable SecondaryNetworkStrategy connecting transformer locations to nearby customer parcels.

The node closest to source_location is treated as the substation.

from shift import PRSG, GeoLocation

builder = PRSG(
    groups=clusters,
    source_location=GeoLocation(-97.3, 32.75),  # substation coordinates
)
graph = builder.get_distribution_graph()

Choosing a Routing Strategy#

By default, PRSG uses a Steiner tree with uniform edge weights. For more realistic routing that follows shorter physical paths, use WeightedSteinerTreeStrategy:

from shift import PRSG, WeightedSteinerTreeStrategy, GeoLocation

builder = PRSG(
    groups=clusters,
    source_location=GeoLocation(-97.3, 32.75),
    routing_strategy=WeightedSteinerTreeStrategy(),
)
graph = builder.get_distribution_graph()

Available routing strategies (see Routing Strategies for details):

  • SteinerTreeStrategy — Steiner tree, uniform weights (default, backward-compatible)

  • WeightedSteinerTreeStrategy — Steiner tree, geodesic distance weights [APM+23]

  • ShortestPathTreeStrategy — Dijkstra shortest-path tree from source [APM+23]

  • MinimumSpanningTreeStrategy — MST over terminals via shortest paths

  • FullRoadGraphStrategy — Full road network as topology [APM+23]

Choosing a Secondary Network Strategy#

By default, PRSG uses a rectangular mesh grid with Steiner tree reduction. For simpler radial laterals or road-aware secondary routing:

from shift import PRSG, RadialStrategy, GeoLocation

builder = PRSG(
    groups=clusters,
    source_location=GeoLocation(-97.3, 32.75),
    secondary_strategy=RadialStrategy(),
)
graph = builder.get_distribution_graph()

Available secondary strategies (see Secondary Strategies for details):

  • MeshSteinerStrategy — Rectangular mesh + Steiner tree (default)

  • RadialStrategy — Direct star connection from transformer to loads

  • DelaunayStrategy — Delaunay triangulation + MST pruning

  • OpenStreetSecondaryStrategy — Routes secondary along actual roads [CGdO+26]

  • HubLineStrategy — k-NN consumer-to-transformer assignment [APM+23]

Visualize the Graph#

You can overlay the distribution graph on the parcel plot from the previous step:

from shift import add_distribution_graph_to_plot, PlotManager, GeoLocation
import osmnx as ox

center = GeoLocation(*reversed(ox.geocode("Fort Worth, TX")))
plot_manager = PlotManager(center=center)
add_distribution_graph_to_plot(graph, plot_manager)
plot_manager.show()

Next Step#

Proceed to Updating Branch Types to replace generic branch types with the specific equipment models your simulation requires.