Routing Strategies#
New in v0.7.0
Pluggable routing strategies allow you to control how the primary distribution network topology is constructed from road network data.
Overview#
Distribution networks are typically routed along public roads. The choice of how to select which road segments become power line paths is a key design decision. SHIFT provides multiple routing formulations drawn from the synthetic distribution grid generation literature.
Each strategy implements the RoutingStrategy interface and can be
passed to PRSG via the routing_strategy parameter.
Available Strategies#
SteinerTreeStrategy (default)#
Steiner tree approximation with uniform edge weights. This is the original SHIFT algorithm — preserved as the default for backward compatibility.
Algorithm: Mehlhorn approximation of the Steiner tree
Edge weights: All edges weighted equally (weight = 1)
Topology: Minimum-node tree connecting all terminals
Trade-off: Fast, but ignores physical distance → can produce jagged paths
WeightedSteinerTreeStrategy#
Steiner tree with geodesic distance as edge weight. Produces more realistic routing by preferring physically shorter paths.
Algorithm: Mehlhorn Steiner tree with distance-weighted edges
Edge weights: Geodesic distance (meters) or user-supplied
weight_fnTopology: Distance-minimizing tree
Trade-off: Slightly slower, but significantly more realistic paths
Custom weight functions
Pass a callable weight_fn(graph, u, v) -> float to implement custom weighting
policies such as penalizing road crossings or applying distance-zone penalties
[CGdO+26].
ShortestPathTreeStrategy#
Dijkstra shortest-path tree from the source node to all other terminals.
Algorithm: Dijkstra’s algorithm from source to each terminal
Edge weights: Geodesic distance or user-supplied
weight_fnTopology: Star-like trunk from substation, with shared path segments
Trade-off: Always follows the true shortest road path; may produce longer total wire length than Steiner tree
Reference: Ali et al. [APM+23]
MinimumSpanningTreeStrategy#
MST over terminals using pairwise shortest-path distances in the road graph.
Algorithm: (1) Compute all-pairs shortest paths between terminals, (2) Build complete distance graph, (3) Find MST, (4) Map back to road paths
Edge weights: Geodesic distance or user-supplied
weight_fnTopology: Trunk-branch (feeder backbone with laterals)
Trade-off: Optimal total wire length; more computation for many terminals
FullRoadGraphStrategy#
Returns the full road network without reduction — power lines follow all available road paths.
Algorithm: Connected-component extraction (no tree reduction)
Topology: Meshed (may contain cycles)
Trade-off: Most realistic for urban networks with redundant paths; not radial
Reference: Ali et al. [APM+23]
CostOptimizedStrategy (placeholder)#
MILP cost-minimization with binary decision variables per candidate edge.
Formulation: Minimize investment cost + operational losses subject to radiality and AC power flow constraints
Status: Interface defined; implementation requires external solver (PuLP/Pyomo)
Use case: Planning-grade synthetic grids matching utility practice
Reference: Trpovski et al. [TRH18]
Usage Examples#
Basic usage (weighted Steiner tree)#
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()
Custom weight function#
from shift import WeightedSteinerTreeStrategy
from shift.utils.split_network_edges import get_distance_between_points
from shift.data_model import GeoLocation
def penalized_weight(graph, u, v):
"""Super-linear distance penalty — discourages long edges."""
dist = get_distance_between_points(
GeoLocation(graph.nodes[u]["x"], graph.nodes[u]["y"]),
GeoLocation(graph.nodes[v]["x"], graph.nodes[v]["y"]),
).to("m").magnitude
return dist ** 1.5
strategy = WeightedSteinerTreeStrategy(weight_fn=penalized_weight)
Full road graph (no reduction)#
from shift import PRSG, FullRoadGraphStrategy, GeoLocation
builder = PRSG(
groups=clusters,
source_location=GeoLocation(-97.3, 32.75),
routing_strategy=FullRoadGraphStrategy(),
)
API Reference#
- class shift.RoutingStrategy#
Abstract base class for routing strategies.
A routing strategy determines how a subset of terminal nodes are connected through a candidate network graph, producing a tree subgraph that connects all terminals.
- abstract route(graph: Graph, terminal_nodes: list[str]) Graph#
Connect terminal nodes through the candidate graph.
- Parameters:
graph (nx.Graph) – Candidate network graph with node attributes ‘x’ and ‘y’ representing longitude and latitude respectively.
terminal_nodes (list[str]) – List of node names that must be connected in the result.
- Returns:
A connected subgraph (tree) containing all terminal nodes.
- Return type:
nx.Graph
- class shift.SteinerTreeStrategy#
Bases:
RoutingStrategySteiner tree with uniform edge weights (current default behavior).
Produces the Steiner tree approximation using the Mehlhorn method with all edges weighted equally at 1. This preserves the original SHIFT behavior for backward compatibility.
- route(graph: Graph, terminal_nodes: list[str]) Graph#
Connect terminal nodes through the candidate graph.
- Parameters:
graph (nx.Graph) – Candidate network graph with node attributes ‘x’ and ‘y’ representing longitude and latitude respectively.
terminal_nodes (list[str]) – List of node names that must be connected in the result.
- Returns:
A connected subgraph (tree) containing all terminal nodes.
- Return type:
nx.Graph
- class shift.WeightedSteinerTreeStrategy(weight_fn: Callable[[Graph, str, str], float] | None = None, crossing_penalty: float = 1.0)#
Bases:
RoutingStrategySteiner tree with distance-based edge weights and optional crossing penalty.
Uses geodesic distance as edge weights for the Steiner tree approximation. Optionally penalizes edges that would cross other edges in the graph, encouraging non-crossing topologies.
- Parameters:
weight_fn (Callable[[nx.Graph, str, str], float], optional) – Custom weight function. Defaults to geodesic distance in meters.
crossing_penalty (float, optional) – Multiplicative penalty applied for each crossing detected. Default 1.0 (no penalty). Values > 1 discourage crossings. Typical values: 2.0–5.0 for moderate penalty.
References
Ali et al. 2023: distance-based power line routing
Caetano et al. 2026: distance-zone weighting concept
- route(graph: Graph, terminal_nodes: list[str]) Graph#
Connect terminal nodes through the candidate graph.
- Parameters:
graph (nx.Graph) – Candidate network graph with node attributes ‘x’ and ‘y’ representing longitude and latitude respectively.
terminal_nodes (list[str]) – List of node names that must be connected in the result.
- Returns:
A connected subgraph (tree) containing all terminal nodes.
- Return type:
nx.Graph
- class shift.ShortestPathTreeStrategy(weight_fn: Callable[[Graph, str, str], float] | None = None)#
Bases:
RoutingStrategyShortest-path tree from a source node to all other terminals.
Builds a tree by computing shortest paths (Dijkstra) from the first terminal node (typically the source/substation) to all other terminals, using geodesic distance as edge weights. The result follows roads naturally, producing a star/trunk topology.
- Parameters:
weight_fn (Callable[[nx.Graph, str, str], float], optional) – Custom weight function. Defaults to geodesic distance.
References
Ali et al. 2023: full road graph routing where power lines = road paths
- route(graph: Graph, terminal_nodes: list[str]) Graph#
Connect terminal nodes through the candidate graph.
- Parameters:
graph (nx.Graph) – Candidate network graph with node attributes ‘x’ and ‘y’ representing longitude and latitude respectively.
terminal_nodes (list[str]) – List of node names that must be connected in the result.
- Returns:
A connected subgraph (tree) containing all terminal nodes.
- Return type:
nx.Graph
- class shift.MinimumSpanningTreeStrategy(weight_fn: Callable[[Graph, str, str], float] | None = None)#
Bases:
RoutingStrategyMinimum spanning tree over terminal nodes using shortest-path distances.
Computes pairwise shortest-path distances between all terminals in the candidate graph, builds a complete distance graph, finds its MST, then maps each MST edge back to the actual shortest path in the original graph. Produces realistic trunk-branch topology.
- Parameters:
weight_fn (Callable[[nx.Graph, str, str], float], optional) – Custom weight function for the candidate graph edges. Defaults to geodesic distance.
- route(graph: Graph, terminal_nodes: list[str]) Graph#
Connect terminal nodes through the candidate graph.
- Parameters:
graph (nx.Graph) – Candidate network graph with node attributes ‘x’ and ‘y’ representing longitude and latitude respectively.
terminal_nodes (list[str]) – List of node names that must be connected in the result.
- Returns:
A connected subgraph (tree) containing all terminal nodes.
- Return type:
nx.Graph
- class shift.FullRoadGraphStrategy#
Bases:
RoutingStrategyUses the road network graph directly without reduction.
Instead of computing a Steiner tree or MST, this strategy returns the full candidate graph (typically the road network) as-is. The assumption is that power lines follow road paths directly.
The resulting graph is pruned to only include nodes reachable from the first terminal (connected component), ensuring connectivity.
References
Ali et al. 2023: “power lines follow road paths” — road network = power line topology directly
- route(graph: Graph, terminal_nodes: list[str]) Graph#
Connect terminal nodes through the candidate graph.
- Parameters:
graph (nx.Graph) – Candidate network graph with node attributes ‘x’ and ‘y’ representing longitude and latitude respectively.
terminal_nodes (list[str]) – List of node names that must be connected in the result.
- Returns:
A connected subgraph (tree) containing all terminal nodes.
- Return type:
nx.Graph
- class shift.CostOptimizedStrategy#
Bases:
RoutingStrategyCost-optimized routing via MILP (placeholder).
Based on the formulation by Trpovski et al. 2018, this strategy conceptually uses binary edge-decision variables to minimize investment and operating costs under radiality and power-flow constraints.
This is a placeholder that documents the interface. Full implementation requires an external MILP solver (e.g., PuLP, scipy.optimize, or Pyomo).
References
Trpovski, Recalde, Hamacher. “Synthetic Distribution Grid Generation Using Power System Planning: Case Study of Singapore.” IEEE 2018.
- route(graph: Graph, terminal_nodes: list[str]) Graph#
Connect terminal nodes through the candidate graph.
- Parameters:
graph (nx.Graph) – Candidate network graph with node attributes ‘x’ and ‘y’ representing longitude and latitude respectively.
terminal_nodes (list[str]) – List of node names that must be connected in the result.
- Returns:
A connected subgraph (tree) containing all terminal nodes.
- Return type:
nx.Graph
References#
M. Ali, K. Prakash, C. Macana, M.Q. Raza, A.K. Bashir, and H. Pota. Modeling synthetic power distribution network and datasets with industrial validation. Journal of Industrial Information Integration, 31:100407, 2023. doi:10.1016/j.jii.2022.100407.
Henrique O. Caetano, Rahul K. Gupta, Cristhian G. da R. de Oliveira, João B.A. London Jr, and Carlos Dias Maciel. Bayesian model-based generation of synthetic unbalanced distribution networks incorporating reliability indices. Electric Power Systems Research, 262:113604, 2026. doi:10.1016/j.epsr.2026.113604.
Andrej Trpovski, Dante Recalde, and Thomas Hamacher. Synthetic distribution grid generation using power system planning: case study of singapore. In 2018 IEEE PES Innovative Smart Grid Technologies Conference Europe (ISGT-Europe), 1–6. IEEE, 2018. doi:10.1109/ISGTEurope.2018.8571800.