-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathRgraph.py
377 lines (305 loc) · 13.3 KB
/
Rgraph.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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# python3
#
# Author: Pavlos Sermpezis
# Institute of Computer Science, Foundation for Research and Technology - Hellas (FORTH), Greece
#
# E-mail: [email protected]
#
#
# example for catchment inference, see related paper:
# [1] Pavlos Sermpezis and Vasileios Kotronis. “Inferring Catchment in Internet Routing”, ACM SIGMETRICS, 2019.
#
from collections import defaultdict, Counter
import networkx as nx # NOTE tested with networkx 2.1 (with versions 1.x it may not work)
class GraphNode:
def __init__(self,ID):
self.ID = ID
self.route = None
self.color = None
self.is_colored_by_measurement = False
'''
Rgraph is a graph of type nx.DiGraph (directed graph), with:
(a) nodes, with attributes
"color": dictionary with keys the root nodes of the graph, and values the probability the node to route towards the respective root node (or, "to have the color of the root node"); this information is either calculated from the Rgraph structure, or given as an oracle (from an external source to the Rgraph, e.g., from a simulation Topo).
[default value: None]
"route": the route the node has selected (i.e., the best path); this information is given as an oracle (from an external source to the Rgraph, e.g., from a simulation Topo).
[default value: None]
(b) directed edges, with attribute:
"local_preference": if the directed edge is (I-->J), then this attribute is the local preference of node I for its neighbor J.
'''
class Rgraph():
def __init__(self):
#self.dict_of_nodes = defaultdict()
self.nxG = nx.DiGraph()
self.set_of_node_IDs_with_route_valid = set()
self.routes_from_Topo = defaultdict()
self.paths_from_Topo = defaultdict(list)
self.routes_from_Graph = defaultdict()
self.colors = defaultdict(dict)
def print_info(self):
print(nx.info(self.nxG))
def has_node(self,ID):
return self.nxG.has_node(ID)
def has_edge(self,ID1,ID2):
return self.nxG.has_edge(ID1,ID2)
def add_node(self,ID):
if not self.has_node(ID):
self.nxG.add_node(ID, color=None, route=None)
def add_edge(self,ID1,ID2, local_preference=None): # directed edge ID1-->ID2, local_preference is the local preference of node ID2 to node ID1
if not self.has_node(ID1):
self.add_node(ID1)
if not self.has_node(ID2):
self.add_node(ID2)
if not self.has_edge(ID1,ID2):
self.nxG.add_edge(ID1,ID2,local_preference=local_preference)
def remove_node(self,ID):
self.nxG.remove_node(ID)
def remove_edge(self,ID1,ID2):
self.nxG.remove_edge(ID1,ID2)
def has_color(self,ID):
#if self.nxG.nodes[ID]['color'] is None:
if len(self.colors[ID]) == 0:
return False
else:
return True
def get_color(self,ID):
if self.has_color(ID):
#return self.nxG.nodes[ID]['color']
return self.colors[ID]
else:
raise Exception('Node does not have color.')
def remove_all_leaves(self):
topo_sort = nx.topological_sort(self.nxG)
for n in reversed(list(topo_sort)):
if (len(list(self.nxG.successors(n))) == 0) and (len(list(self.nxG.predecessors(n))) == 1):
self.remove_node(n)
'''
A color dictionary is "valid" when its values (that denote probabilities) sum to 1.0.
'''
def is_valid_color(self,color):
epsilon = 0.0001 # to avoid accuracy errors due to rounding
if (color is None) or ( abs(sum(color.values())-1.0) > epsilon ):
return False
else:
return True
'''
"certain color" is defined when a node routes to a root node (or, takes color of the root node) with probability 1.0 (and takes all other colors with probability 0.0).
'''
def has_certain_color(self,ID):
if self.has_color(ID) and ( max(self.get_color(ID).values())==1 ):
return True
else:
return False
def get_certain_color(self,ID):
if self.has_certain_color(ID):
for k,v in self.get_color(ID).items():
if v==1:
return k
raise Exception('Node does not have certain color.')
def has_route(self,ID):
if self.nxG.nodes[ID]['route'] is None:
return False
else:
return True
def get_route(self,ID):
if self.has_route(ID):
return self.nxG.nodes[ID]['route']
else:
raise Exception('Node does not have route.')
'''
checks if the given "route" corresponds to a node in the Graph
'''
def is_valid_route(self,route):
if route in self.nxG.nodes():
return True
else:
return False
def get_list_of_nodes(self, with_color=False, with_certain_color=False, with_route=False, subset_of_nodes=None):
conditions = [with_color, with_certain_color, with_route]
nb_active_conditions = sum([1 for c in conditions if c])
if subset_of_nodes is None:
list_of_nodes = self.nxG.nodes()
else:
list_of_nodes = [n for n in self.nxG.nodes() if n in subset_of_nodes]
if nb_active_conditions == 0:
return list_of_nodes
elif nb_active_conditions == 1:
if with_color:
return [n for n in list_of_nodes if self.has_color(n)]
if with_certain_color:
return [n for n in list_of_nodes if self.has_certain_color(n)]
if with_route:
return [n for n in list_of_nodes if self.has_route(n)]
else:
raise Exception('Cannot receive more than one active conditions.')
def get_nb_of_nodes(self, with_color=False, with_certain_color=False, with_route=False, subset_of_nodes=None):
return len(self.get_list_of_nodes(with_color=with_color, with_certain_color=with_certain_color, with_route=with_route, subset_of_nodes=subset_of_nodes))
def set_route(self,ID,route):
if self.has_route(ID):
raise Exception('Node has already route.')
if self.is_valid_route(route):
raise Exception('The route is invalid.')
self.nxG.nodes[ID]['route'] = route
'''
### COLORING FUNCTIONS ###
'''
def color_node(self, ID, color_dict=None):
if self.has_color(ID):
raise Exception('Node has already color.')
self.set_color(ID,color_dict=color_dict)
'''
colors node either it already as a color or not
'''
def recolor_node(self, ID, color_dict=None):
#if not self.has_color(ID):
# raise Exception('Node (to be recolored) does not have color.')
self.set_color(ID,color_dict=color_dict,recolor=True)
'''
Sets the color of a node
(a) to the given color (if the color input argument is given)
(b) from the colors of its predecessors in the Rgraph
'''
def set_color(self,ID,color_dict=None,recolor=False):
if color_dict is not None:
if not self.is_valid_color(color_dict):
raise Exception('The color dictionary is invalid.')
#self.nxG.nodes[ID]['color'] = color_dict
self.colors[ID] = color_dict
else:
self.color_node_from_neighbors(ID,recolor=recolor)
'''
Calculates the color of a node from its predecessors, as follows: for each color...
(i) sums the probabilities of the predecessors corresponding to this color
(ii) normalizes the resulting value by diving it to the number of predecessors
'''
def color_node_from_neighbors(self,ID, recolor=False):
set_of_predecessors = list(self.nxG.predecessors(ID))
if len(set_of_predecessors) ==0:
#print('WARNING: node does not have any predecessors.')
return
# create a color dict and values from color dicts of all predecessors
color_dict = defaultdict()
try:
for p_ID in set_of_predecessors:
color_dict = dict( Counter(color_dict) + Counter(self.get_color(p_ID)) ) # the method "get_color" raises an exception if the given node does not have a color
except:
raise Exception('Not all predecessors are colored.')
# normalize to [0,1] (i.e., dividing by the number of predecessors)
for k,v in color_dict.items():
color_dict[k] = 1.0*v/len(set_of_predecessors)
if not self.is_valid_color(color_dict):
raise Exception('Color from predecessors is not valid (sum of probabilities = {}).'.format(sum(color_dict.values())))
# set the color of the node
if recolor:
self.recolor_node(ID, color_dict)
else:
self.color_node(ID, color_dict)
'''
Oracle-enhanced algorithm (for one node at each time): Adds the given color as the certain color to node, and updates certain colors to its neighbors if needed.
IF the node has already a certain color AND this is the same with the given color, THEN does nothing and returns
IF the node has already a certain color BUT this is different than the given color, THEN it raises an exception
IF the node does NOT already have a certain color, THEN:
Sets this color as the certain color of the node.
Checks if any of its neighbors (predecessors and successors) need to update its color to a certain color. IF yes, runs the same method (nested call of the function) for this neighbor.
'''
def add_certain_color_to_node(self, ID, certain_color, update_color_of_neighbors=True):
if self.has_certain_color(ID):
# for debugging purposes
if self.get_certain_color(ID) != certain_color:
raise Exception('Node already has certain color, different than the given one.')
else:
return
#pass
#print('Node has already certain color.')
color_dict = {}
color_dict[certain_color] = 1.0
if not self.is_valid_color(color_dict):
raise Exception("The given color {} is not valid".format(color_dict))
self.set_color(ID,color_dict)
if update_color_of_neighbors:
# update colors of predecessors
list_of_possible_predecessors = []
for p_ID in self.nxG.predecessors(ID):
if self.get_color(p_ID).get(certain_color,0) > 0:
list_of_possible_predecessors.append(p_ID)
if len(list_of_possible_predecessors) == 0:
# for debugging purposes
raise Exception('This should not have happened: None of the predecessors of {} have its color {}'.format(ID, self.get_color(ID)))
elif len(list_of_possible_predecessors) == 1:
p_ID_to_color = list_of_possible_predecessors[0]
if not self.has_certain_color(p_ID_to_color):
self.add_certain_color_to_node(p_ID_to_color, certain_color, update_color_of_neighbors=True)
# update colors of successors
for s_ID in self.nxG.successors(ID):
if not self.has_certain_color(s_ID):
self.color_node_from_neighbors(s_ID, recolor=True)
if self.has_certain_color(s_ID):
if self.get_certain_color(s_ID) != certain_color:
raise Exception('Successor {} ({}) is colored with a different color than {} ({})'.format(s_ID, self.get_color(s_ID), ID, self.get_color(ID) ))
self.add_certain_color_to_node(s_ID, certain_color, update_color_of_neighbors=True)
'''
Updates the probabilistic coloring of the Rgraph.
(i) calculates a topological sorting of the nodes (https://en.wikipedia.org/wiki/Topological_sorting)
(ii) iterating over the nodes in the topological sorting that DO NOT have certain color, colors them based on their predecessors
'''
def update_forward_probabilistic_coloring(self):
nodes_to_skip = self.get_list_of_nodes(with_certain_color=True)
topo_sort = nx.topological_sort(self.nxG)
for ID in topo_sort:
if ID in nodes_to_skip:
continue
else:
self.recolor_node(ID) # no color_dict as input argument in color_node ==> color node from predecessors
'''
Sets the probabilistic coloring of the Rgraph.
(i) assigns to each source/root node the color of itself (i.e., source node I has as color {I:1.0})
(ii) calculates a topological sorting of the nodes (https://en.wikipedia.org/wiki/Topological_sorting)
(iii) iterating over the nodes in the topological sorting, colors them based on their predecessors
'''
def set_probabilistic_coloring(self, source_nodes):
# color the source nodes (i.e., the roots of the Rgraph)
for ID in source_nodes:
if not self.has_node(ID):
raise Exception("The source node {} is not in the graph".format(ID))
if len(list(self.nxG.predecessors(ID))) >0:
raise Exception("The source node {} is not a root".format(ID))
if self.has_color(ID):
raise Exception("The source node {} already has a color".format(ID))
color_dict = dict.fromkeys(source_nodes,0)
color_dict[ID] = 1.0
self.color_node(ID,color_dict)
# color the other nodes (non roots), based on the color of their neighbors
topo_sort = nx.topological_sort(self.nxG)
for ID in topo_sort:
if ID in source_nodes:
continue
else:
self.color_node(ID) # no color_dict as input argument in color_node ==> color node from predecessors
'''
Get the certain catchment for each anycaster (i.e., the number of nodes with the certain color of the anycaster).
Returns a dictionary with keys: anycaster and values: certain catchment
'''
def get_certain_catchment(self, in_percentage=False, subset_of_nodes=None):
dict_anycast_catchment = defaultdict(lambda:0)
for ID in self.get_list_of_nodes(with_certain_color=True, subset_of_nodes=subset_of_nodes):
dict_anycast_catchment[ self.get_certain_color(ID) ] += 1
if in_percentage:
total_nodes = self.get_nb_of_nodes()
for k, v in dict_anycast_catchment.items():
dict_anycast_catchment[k] = 1.0 * v / total_nodes
return dict_anycast_catchment
'''
Get the probabilistic catchment for each anycaster (i.e., the sum of probabilities of nodes for the color of the anycaster).
Returns a dictionary with keys: anycaster and values: probabilistic catchment
'''
def get_probabilistic_catchment(self, in_percentage=False, subset_of_nodes=None):
dict_anycast_catchment = defaultdict(lambda:0)
for ID in self.get_list_of_nodes(with_color=True, subset_of_nodes=subset_of_nodes):
color_dict = self.get_color(ID)
for anycaster, probability in color_dict.items():
dict_anycast_catchment[ anycaster ] += probability
if in_percentage:
total_nodes = self.get_nb_of_nodes()
for k, v in dict_anycast_catchment.items():
dict_anycast_catchment[k] = 1.0 * v / total_nodes
return dict_anycast_catchment