-
Notifications
You must be signed in to change notification settings - Fork 0
/
steinernetworkx.py
266 lines (232 loc) · 9.66 KB
/
steinernetworkx.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import itertools as it
from itertools import combinations, chain
from collections import namedtuple,defaultdict
from networkx.utils import pairwise
import networkx as nx
__all__ = ['metric_closure', 'steiner_tree']
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
def _ordered(u, v):
"""Returns the nodes in an undirected edge in lower-triangular order"""
return (u, v) if u < v else (v, u)
def modded_one_edge_augmentation(subG, G, avail, weight=None):
avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=subG)
# Collapse CCs in the original graph into nodes in a metagraph
# Then find an MST of the metagraph instead of the original graph
C = collapse(G,nx.connected_components(subG))
mapping = C.graph['mapping']
# Assign each available edge to an edge in the metagraph
candidate_mapping = _lightest_meta_edges(mapping, avail_uv, avail_w)
# nx.set_edge_attributes(C, name='weight', values=0)
C.add_edges_from(
(mu, mv, {'weight': w, 'generator': uv})
for (mu, mv), uv, w in candidate_mapping
)
# Find MST of the meta graph
#print(C.nodes())
leaves = [mapping[node] for node in subG.nodes()]
#print(leaves)
meta_steiner = steiner_tree(C,leaves)
#if nx.is_connected(meta_steiner):
# print('connected meta graph!')
if not nx.is_connected(meta_steiner):
raise nx.NetworkXUnfeasible(
'Not possible to connect G with available edges')
# Yield the edge that generated the meta-edge
for mu, mv, d in meta_steiner.edges(data=True):
if 'generator' in d:
#print('doublechecked')
edge = d['generator']
yield edge
def connect_graph(subG,G,avail,weight=None):
aug_edges = modded_one_edge_augmentation(subG,G,avail,weight=weight)
for edge in list(aug_edges):
yield edge
def _unpack_available_edges(avail, weight=None, G=None):
"""Helper to separate avail into edges and corresponding weights"""
if weight is None:
weight = 'weight'
if isinstance(avail, dict):
avail_uv = list(avail.keys())
avail_w = list(avail.values())
else:
def _try_getitem(d):
try:
return d[weight]
except TypeError:
return d
avail_uv = [tup[0:2] for tup in avail]
avail_w = [1 if len(tup) == 2 else _try_getitem(tup[-1])
for tup in avail]
if G is not None:
# Edges already in the graph are filtered
flags = [not G.has_edge(u, v) for u, v in avail_uv]
avail_uv = list(it.compress(avail_uv, flags))
avail_w = list(it.compress(avail_w, flags))
return avail_uv, avail_w
def collapse(G, grouped_nodes):
"""Collapses each group of nodes into a single node.
This is similar to condensation, but works on undirected graphs.
Parameters
----------
G : NetworkX Graph
grouped_nodes: list or generator
Grouping of nodes to collapse. The grouping must be disjoint.
If grouped_nodes are strongly_connected_components then this is
equivalent to :func:`condensation`.
Returns
-------
C : NetworkX Graph
The collapsed graph C of G with respect to the node grouping. The node
labels are integers corresponding to the index of the component in the
list of grouped_nodes. C has a graph attribute named 'mapping' with a
dictionary mapping the original nodes to the nodes in C to which they
belong. Each node in C also has a node attribute 'members' with the set
of original nodes in G that form the group that the node in C
represents.
Examples
--------
>>> # Collapses a graph using disjoint groups, but not necesarilly connected
>>> G = nx.Graph([(1, 0), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (5, 7)])
>>> G.add_node('A')
>>> grouped_nodes = [{0, 1, 2, 3}, {5, 6, 7}]
>>> C = collapse(G, grouped_nodes)
>>> members = nx.get_node_attributes(C, 'members')
>>> sorted(members.keys())
[0, 1, 2, 3]
>>> member_values = set(map(frozenset, members.values()))
>>> assert {0, 1, 2, 3} in member_values
>>> assert {4} in member_values
>>> assert {5, 6, 7} in member_values
>>> assert {'A'} in member_values
"""
mapping = {}
members = {}
C = G.__class__()
i = 0 # required if G is empty
remaining = set(G.nodes())
for i, group in enumerate(grouped_nodes):
group = set(group)
assert remaining.issuperset(group), (
'grouped nodes must exist in G and be disjoint')
remaining.difference_update(group)
members[i] = group
mapping.update((n, i) for n in group)
# remaining nodes are in their own group
for i, node in enumerate(remaining, start=i + 1):
group = set([node])
members[i] = group
mapping.update((n, i) for n in group)
number_of_groups = i + 1
C.add_nodes_from(range(number_of_groups))
C.add_edges_from((mapping[u], mapping[v]) for u, v in G.edges()
if mapping[u] != mapping[v])
# Add a list of members (ie original nodes) to each node (ie scc) in C.
nx.set_node_attributes(C, name='members', values=members)
# Add mapping dict as graph attribute
C.graph['mapping'] = mapping
return C
MetaEdge = namedtuple('MetaEdge', ('meta_uv', 'uv', 'w'))
def _lightest_meta_edges(mapping, avail_uv, avail_w):
"""Maps available edges in the original graph to edges in the metagraph.
Parameters
----------
mapping : dict
mapping produced by :func:`collapse`, that maps each node in the
original graph to a node in the meta graph
avail_uv : list
list of edges
avail_w : list
list of edge weights
Notes
-----
Each node in the metagraph is a k-edge-connected component in the original
graph. We don't care about any edge within the same k-edge-connected
component, so we ignore self edges. We also are only intereseted in the
minimum weight edge bridging each k-edge-connected component so, we group
the edges by meta-edge and take the lightest in each group.
Example
-------
>>> # Each group represents a meta-node
>>> groups = ([1, 2, 3], [4, 5], [6])
>>> mapping = {n: meta_n for meta_n, ns in enumerate(groups) for n in ns}
>>> avail_uv = [(1, 2), (3, 6), (1, 4), (5, 2), (6, 1), (2, 6), (3, 1)]
>>> avail_w = [ 20, 99, 20, 15, 50, 99, 20]
>>> sorted(_lightest_meta_edges(mapping, avail_uv, avail_w))
[MetaEdge(meta_uv=(0, 1), uv=(5, 2), w=15), MetaEdge(meta_uv=(0, 2), uv=(6, 1), w=50)]
"""
grouped_wuv = defaultdict(list)
for w, (u, v) in zip(avail_w, avail_uv):
# Order the meta-edge so it can be used as a dict key
meta_uv = _ordered(mapping[u], mapping[v])
# Group each available edge using the meta-edge as a key
grouped_wuv[meta_uv].append((w, u, v))
# Now that all available edges are grouped, choose one per group
for (mu, mv), choices_wuv in grouped_wuv.items():
# Ignore available edges within the same meta-node
if mu != mv:
# Choose the lightest available edge belonging to each meta-edge
w, u, v = min(choices_wuv)
yield MetaEdge((mu, mv), (u, v), w)