4

I'm trying to generate a Steiner tree in Python using NetworkX. I have a range of geolocated nodes saved in the GeoJSON format, allowing me to visually validate the outputs in QGIS. A radially deployed straight-line network looks as follows:

enter image description here

This isn't very realistic however for a broadband network. A Steiner tree would reduce the cost for connecting all premises. Here is my artistic attempt for how I imagine it should look:

enter image description here

So far I've setup a function which loads in each GeoJSON and measures the distance from the central node for the edge weight:

import networkx as nx
import matplotlib.pyplot as plt
from shapely.geometry import Point
def load_nodes(premises_data, distribution_point_data):
 G = nx.Graph()
 for dist_point in distribution_point_data:
 dist_point_name = [dist_point['properties']['name']]
 dist_point_coordinates = dist_point['geometry']['coordinates']
 G.add_nodes_from(dist_point_name , pos=dist_point_coordinates )
 for premises in premises_data:
 if dist_point['properties']['name'] == premises ['properties']['link']:
 premises_name = [premises ['properties']['name']]
 premises_coordinates = premises['geometry']['coordinates']
 G.add_nodes_from(node_name, pos=premises_coordinates )
 for dist_point in distribution_point_data:
 for premises in premises_data:
 if dist_point['properties']['name'] == premises['properties']['link']: 
 dist_point_geom = Point(dist_point['geometry']['coordinates'])
 premises_geom = Point(premises['geometry']['coordinates'])
 distance = round(dist_point_geom.distance(premises_geom),2)
 G.add_edge(dist_point['properties']['name'],premises['properties']['name'],weight=distance)
 return G

I then use the source code for the Network X Steiner tree (and metric_closure function) as follows:

from itertools import combinations, chain
from networkx.utils import pairwise, not_implemented_for
def metric_closure(G, weight='weight'):
 """ Return the metric closure of a graph.
 The metric closure of a graph *G* is the complete graph in which each edge
 is weighted by the shortest path distance between the nodes in *G* .
 Parameters
 ----------
 G : NetworkX graph
 Returns
 -------
 NetworkX graph
 Metric closure of the graph `G`.
 """
 M = nx.Graph()
 Gnodes = set(G)
 # check for connected graph while processing first node
 all_paths_iter = nx.all_pairs_dijkstra(G, weight=weight)
 u, (distance, path) = next(all_paths_iter)
 if Gnodes - set(distance):
 msg = "G is not a connected graph. metric_closure is not defined."
 raise nx.NetworkXError(msg)
 Gnodes.remove(u)
 for v in Gnodes:
 M.add_edge(u, v, distance=distance[v], path=path[v])
 # first node done -- now process the rest
 for u, (distance, path) in all_paths_iter:
 Gnodes.remove(u)
 for v in Gnodes:
 M.add_edge(u, v, distance=distance[v], path=path[v])
 return M
def steiner_tree(G, terminal_nodes, weight='weight'):
 """ Return an approximation to the minimum Steiner tree of a graph.
 Parameters
 ----------
 G : NetworkX graph
 terminal_nodes : list
 A list of terminal nodes for which minimum steiner tree is
 to be found.
 Returns
 -------
 NetworkX graph
 Approximation to the minimum steiner tree of `G` induced by
 `terminal_nodes` .
 Notes
 -----
 Steiner tree can be approximated by computing the minimum spanning
 tree of the subgraph of the metric closure of the graph induced by the
 terminal nodes, where the metric closure of *G* is the complete graph in
 which each edge is weighted by the shortest path distance between the
 nodes in *G* .
 This algorithm produces a tree whose weight is within a (2 - (2 / t))
 factor of the weight of the optimal Steiner tree where *t* is number of
 terminal nodes.
 """
 # M is the subgraph of the metric closure induced by the terminal nodes of
 # G.
 M = metric_closure(G, weight=weight)
 # Use the 'distance' attribute of each edge provided by the metric closure
 # graph.
 H = M.subgraph(terminal_nodes)
 mst_edges = nx.minimum_spanning_edges(H, weight='distance', data=True)
 # Create an iterator over each edge in each shortest path; repeats are okay
 edges = chain.from_iterable(pairwise(d['path']) for u, v, d in mst_edges)
 T = G.edge_subgraph(edges)
 return T

Here is the GeoJSON data for a reproducible example for the premises on the far right of the pictures above:

GEOJSON_PREMISES = [
 {
 'type': "Feature",
 'geometry':{
 "type": "Point",
 "coordinates": [0.129960,52.220977]
 },
 'properties': {
 'name': 'premises_1',
 'link': 'distribution_point'
 }
 },
 {
 'type': "Feature",
 'geometry':{
 "type": "Point",
 "coordinates": [0.130056,52.220895]
 },
 'properties': {
 'name': 'premises_2',
 'link': 'distribution_point'
 }
 },
 {
 'type': "Feature",
 'geometry':{
 "type": "Point",
 "coordinates": [0.130318,52.220782]
 },
 'properties': {
 'name': 'premises_3',
 'link': 'distribution_point'
 }
 },
 ]
GEOJSON_DISTRIBUTION_POINT = [
 {
 'type': "Feature",
 'geometry':{
 "type": "Point",
 "coordinates": [0.129829,52.220802]
 },
 'properties': {
 'name': 'distribution_point'
 }
 }
]

And I run the following to generate a graph, and then (supposedly) a Steiner tree:

graph = load_nodes(GEOJSON_PREMISES, GEOJSON_DISTRIBUTION_POINT)
graph2 = steiner_tree(graph, graph.nodes, weight = 'distance') 
plt.figure()
nx.draw_networkx(graph2)
plt.show()

This is the output I get, which was not what I was expecting:

enter image description here

Firstly, what am I doing wrong for generating a Steiner tree graph which allows the network to run between the premises points, like in my artistic impression?

Secondly, why does this output plot not use the geographic coordinates of the nodes? (I think I'm configuring this incorrectly because I think Network X can deal with node coordinates as well as weights).

Answers should be for Python and not QGIS/ArcMap, as this needs to eventually be run on a large-scale computer cluster.

asked Jan 1, 2019 at 20:19
4
  • You are talking about Steiner tree, not mst Commented Jan 1, 2019 at 20:33
  • @FelixIP Question now updated to reflect this. Commented Jan 2, 2019 at 11:35
  • Number of nodes connected by Steiner tree << total nodes in network. Commented Jan 2, 2019 at 19:05
  • Missing node at intersection of p2-p3 and p1-source. Commented Jan 2, 2019 at 19:20

2 Answers 2

5

Steiner tree connects some(!) of the network's nodes (terminals) shown as selected nodes:

enter image description here

However don't get over excited about this feature of networkX, there is a good reason they called it "approximation.steinertree.steiner_tree", e.g. tree length in below picture is 380 m (4%) less than in the first one:

enter image description here

Computation of Steiner tree is ginormous task, it involves search for so called Steiners' points. Result shown on second picture is my poor attempt to improve default outcome (1st picture) by considering 3 terminals at a time in a search for such points.

NetworkX algorithm not searching for that points. It assumes that Steiner tree is minimum spanning tree of graph represented by OD matrix for terminals. That is pragmatic approach working well enough with dense terminals, where chances of Steiner points sitting on a shortest path between 2 terminals are big, e.g. difference in tree length shown on 2 pictures below is only 2%:

enter image description here enter image description here

To improve your results I second @Spacedman and suggest to add all possible edges between premises to your graph.

answered Jan 3, 2019 at 0:38
3

The minimum spanning tree is computed on the input graph. Your input graph is the star network from the distribution point to the three premises - it doesn't contain edges from the premises to the other premises, so the output MST can't have those links in it.

You want to create a full graph with all edges between all the pairs of locations - distribution point and premises points. Then the MST will span over that.

answered Jan 1, 2019 at 20:32
1
  • I've taken on board the comment from FelixIP that this should be a Steiner Tree, and have edited the question to reflect this. In the code posted, the Steiner Tree function initially obtains the full metric closure for the graph, as you suggest. However, even after doing this I am still unable to generate the Steiner graph. Help would be much appreciated. Commented Jan 2, 2019 at 11:40

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.