def GetStaticEigenvectorCentrality(t, model='SECOND'): """Computes eigenvector centralities of nodes in the second-order aggregate network, and aggregates eigenvector centralities to obtain the eigenvector centrality of nodes in the first-order network. @param t: The temporalnetwork instance to work on @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value.""" if (model is "SECOND" or "NULL") == False: raise ValueError("model must be one of \"SECOND\" or \"NULL\"") name_map = Utilities.firstOrderNameMap(t) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() # Compute eigenvector centrality in second-order network A = Utilities.getSparseAdjacencyMatrix(g2, attribute="weight", transposed=True) evcent_2 = Utilities.StationaryDistribution(A, False) # Aggregate to obtain first-order eigenvector centrality evcent_1 = np.zeros(len(name_map)) sep = t.separator for i in range(len(evcent_2)): # Get name of target node target = g2.vs()[i]["name"].split(sep)[1] evcent_1[name_map[target]] += np.real(evcent_2[i]) return np.real(evcent_1 / sum(evcent_1))
def GetStaticEigenvectorCentrality(t, model='SECOND'): """Computes eigenvector centralities of nodes in the second-order aggregate network, and aggregates eigenvector centralities to obtain the eigenvector centrality of nodes in the first-order network. @param t: The temporalnetwork instance to work on @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value.""" if (model is "SECOND" or "NULL") == False: raise ValueError("model must be one of \"SECOND\" or \"NULL\"") name_map = Utilities.firstOrderNameMap(t) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() # Compute eigenvector centrality in second-order network A = Utilities.getSparseAdjacencyMatrix( g2, attribute="weight", transposed=True ) evcent_2 = Utilities.StationaryDistribution( A, False ) # Aggregate to obtain first-order eigenvector centrality evcent_1 = np.zeros(len(name_map)) sep = t.separator for i in range(len(evcent_2)): # Get name of target node target = g2.vs()[i]["name"].split(sep)[1] evcent_1[name_map[target]] += np.real(evcent_2[i]) return np.real(evcent_1/sum(evcent_1))
def EigenValueGap(t): """Returns the eigenvalue gap in the second-order transition matrix of a temporal network, as well as in the corresponding null model. Returns the tuple (lambda(T2), lambda(T2_null)) @param t: The temporalnetwork instance to work on """ #NOTE to myself: most of the time goes for construction of the 2nd order #NOTE null graph, then for the 2nd order null transition matrix g2 = t.igraphSecondOrder().components(mode="STRONG").giant() g2n = t.igraphSecondOrderNull().components(mode="STRONG").giant() Log.add('Calculating eigenvalue gap ... ', Severity.INFO) # Build transition matrices T2 = Utilities.RWTransitionMatrix(g2) T2n = Utilities.RWTransitionMatrix(g2n) # Compute eigenvector sequences # NOTE: ncv=13 sets additional auxiliary eigenvectors that are computed # NOTE: in order to be more confident to find the one with the largest # NOTE: magnitude, see # NOTE: https://github.com/scipy/scipy/issues/4987 w2 = sla.eigs(T2, which="LM", k=2, ncv=13, return_eigenvectors=False) evals2_sorted = np.sort(-np.absolute(w2)) w2n = sla.eigs(T2n, which="LM", k=2, ncv=13, return_eigenvectors=False) evals2n_sorted = np.sort(-np.absolute(w2n)) Log.add('finished.', Severity.INFO) return (np.abs(evals2_sorted[1]), np.abs(evals2n_sorted[1]))
def SlowDownFactor(t): """Returns a factor S that indicates how much slower (S>1) or faster (S<1) a diffusion process in the temporal network evolves on a second-order model compared to a first-order model. This value captures the effect of order correlations on a diffusion process in the temporal network. @param t: The temporalnetwork instance to work on """ #NOTE to myself: most of the time goes for construction of the 2nd order #NOTE null graph, then for the 2nd order null transition matrix g2 = t.igraphSecondOrder().components(mode="STRONG").giant() g2n = t.igraphSecondOrderNull().components(mode="STRONG").giant() Log.add('Calculating slow down factor ... ', Severity.INFO) # Build transition matrices T2 = Utilities.RWTransitionMatrix(g2) T2n = Utilities.RWTransitionMatrix(g2n) # Compute eigenvector sequences # NOTE: ncv=13 sets additional auxiliary eigenvectors that are computed # NOTE: in order to be more confident to find the one with the largest # NOTE: magnitude, see # NOTE: https://github.com/scipy/scipy/issues/4987 w2 = sla.eigs(T2, which="LM", k=2, ncv=13, return_eigenvectors=False) evals2_sorted = np.sort(-np.absolute(w2)) w2n = sla.eigs(T2n, which="LM", k=2, ncv=13, return_eigenvectors=False) evals2n_sorted = np.sort(-np.absolute(w2n)) Log.add('finished.', Severity.INFO) return np.log(np.abs(evals2n_sorted[1])) / np.log(np.abs(evals2_sorted[1]))
def RWDiffusion(g, samples=5, epsilon=0.01, max_iterations=100000): """Computes the average number of steps requires by a random walk process to fall below a total variation distance below epsilon (TVD computed between the momentary visitation probabilities \pi^t and the stationary distribution \pi = \pi^{\infty}. This time can be used to measure diffusion speed in a given (weighted and directed) network.""" avg_speed = 0 T = Utilities.RWTransitionMatrix(g) pi = Utilities.StationaryDistribution(T) n = len(g.vs()) for s in range(samples): x = np.zeros(n) seed = np.random.randint(n) x[seed] = 1 while Utilities.TVD(x, pi) > epsilon: avg_speed += 1 # NOTE x * T = (T^T * x^T)^T # NOTE T is already transposed to get the left EV x = (T.dot(x.transpose())).transpose() if avg_speed > max_iterations: Log.add("x[0:10] = " + str(x[0:10])) Log.add("pi[0:10] = " + str(pi[0:10])) raise RuntimeError( "Failed to converge within maximal number of iterations. Start of current x and pi are printed above" ) return avg_speed / samples
def exportDiffusionMovieFrames(g, file_prefix='diffusion', visual_style=None, steps=100, initial_index=-1): """Exports an animation showing the evolution of a diffusion process on the network""" T = Utilities.RWTransitionMatrix(g) if visual_style == None: visual_style = {} visual_style["vertex_color"] = "lightblue" visual_style["vertex_label"] = g.vs["name"] visual_style["edge_curved"] = .5 visual_style["vertex_size"] = 30 # lambda expression for the coloring of nodes according to some quantity p \in [0,1] # p = 1 ==> color red # p = 0 ==> color white color_p = lambda p: "rgb(255," + str(int( (1 - p) * 255)) + "," + str(int((1 - p) * 255)) + ")" # Initial state of random walker if initial_index < 0: initial_index = np.random.randint(0, len(g.vs())) x = np.zeros(len(g.vs())) x[initial_index] = 1 # compute stationary state pi = Utilities.StationaryDistribution(T) scale = np.mean(np.abs(x - pi)) # Plot network (useful as poster frame of video) igraph.plot(g, file_prefix + "_network.pdf", **visual_style) # Create frames for i in range(0, steps): visual_style["vertex_color"] = [color_p(p**0.1) for p in x] igraph.plot(g, file_prefix + "_frame_" + str(i).zfill(5) + ".png", **visual_style) if i % 10 == 0: Log.add('Step' + str(i) + '\tTVD = ' + str(Utilities.TVD(x, pi)), Severity.INFO) # NOTE x * T = (T^T * x^T)^T # NOTE T is already transposed to get the left EV x = (T.dot(x.transpose())).transpose()
def GetTemporalClosenessInstantaneous(t, start_t=0, delta=1): """Calculates the temporal closeness values of all nodes for a given start time start_t in a temporal network t. This function returns a numpy array of (temporal) closeness centrality values. The ordering of these values corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between node names and array indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network for which temporal closeness centralities will be computed @param start_t: the start time for which to consider time-respecting paths (default 0). This is important, since any unambigious definition of a shortest time-respecting path between two nodes must include the time range to be considered (c.f. Holme and Saramäki, Phys. Rep., 2012) @param delta: the maximum time difference time used in the time-respecting path definition (default 1) Note that this parameter is independent from the delta used internally for the extraction of two-paths by the class TemporalNetwork. """ closeness = np.array([0.]*len(t.nodes)) # Calculate all shortest time-respecting paths D, paths = Paths.GetTemporalDistanceMatrix(t, start_t, delta, collect_paths=False) # Get a mapping between node names and matrix indices name_map = Utilities.firstOrderNameMap( t ) # Calculate closeness for each node u, by summing the reciprocal of its # distances to all other nodes. Note that this definition of closeness centrality # is required for directed networks that are not strongly connected. for u in t.nodes: for v in t.nodes: if u!=v: closeness[name_map[u]] += 1./D[name_map[v], name_map[u]] return closeness
def GetTemporalBetweenness(t, delta=1, normalized=False): """Calculates the temporal betweenness centralities of all nodes in a temporal network t based on the shortest time-respecting paths with a maximum waiting time of delta. This function returns a numpy array of temporal betweenness centrality values of nodes. The ordering of these values corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between node names and array indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network for which temporal closeness centralities will be computed @param delta: the maximum time difference used in the time-respecting path definition (default 1). Note that this parameter is independent from the delta used internally for the extraction of two-paths by the class TemporalNetwork @param normalized: whether or not to normalize centralities by dividing each value byt the total number of shortest time-respecting paths. """ bw = np.array([0] * len(t.nodes)) S = 0 name_map = Utilities.firstOrderNameMap(t) minD, minPaths = Paths.GetMinTemporalDistance(t, delta=1, collect_paths=True) for v in t.nodes: for w in t.nodes: for p in minPaths[v][w]: for i in range(1, len(p) - 1): bw[name_map[p[i][0]]] += 1 S += 1 return bw
def GetStaticPageRank(t, model='SECOND'): """Computes PageRank of nodes based on the second-order aggregate network, and aggregates PageRank values to obtain the PageRank of nodes in the first-order network. @param t: The temporalnetwork instance to work on @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value. """ if (model is "SECOND" or "NULL") == False: raise ValueError("model must be one of \"SECOND\" or \"NULL\"") name_map = Utilities.firstOrderNameMap( t ) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() # Compute betweenness centrality in second-order network pagerank_2 = np.array(g2.pagerank(weights=g2.es()['weight'], directed=True)) # Aggregate to obtain first-order eigenvector centrality pagerank_1 = np.zeros(len(name_map)) sep = t.separator for i in range(len(pagerank_2)): # Get name of target node target = g2.vs()[i]["name"].split(sep)[1] pagerank_1[name_map[target]] += pagerank_2[i] return pagerank_1/sum(pagerank_1)
def GetStaticCloseness(t, model='SECOND'): """Computes closeness centralities of nodes based on the first- or second-order time-aggregated network. @param t: The temporal network instance for which closeness centralities will be computed @param model: either C{"FIRST"}, C{"SECOND"} or C{"SECONDNULL"}, where C{"SECOND"} is the the default value. """ if model =='FIRST': D = Paths.GetFirstOrderDistanceMatrix(t) else: D = Paths.GetSecondOrderDistanceMatrix(t, model) name_map = Utilities.firstOrderNameMap( t ) closeness = np.zeros(len(name_map)) # Calculate closeness for each node u, by summing the reciprocal of its # distances to all other nodes. Note that this definition of closeness centrality # is required for directed networks that are not strongly connected. for u in t.nodes: for v in t.nodes: if u!=v: closeness[name_map[u]] += 1./D[name_map[v], name_map[u]] return closeness
def GetTemporalCloseness(t, delta=1): """Calculates the temporal closeness centralities of all nodes in a temporal network t, based on the minimal shortest time-respecting paths with a maximum time difference of delta. This function then returns a numpy array of average (temporal) closeness centrality values of nodes. The ordering of these values corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between node names and array indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network for which temporal closeness centralities will be computed @param delta: the maximum waiting time used in the time-respecting path definition (default 1) Note that this parameter is independent from the delta used internally for the extraction of two-paths by the class TemporalNetwork """ cl = np.array([0.]*len(t.nodes)) name_map = Utilities.firstOrderNameMap( t ) minD, minPaths = Paths.GetMinTemporalDistance(t, delta, collect_paths=False) for u in t.nodes: for v in t.nodes: if u!= v: cl[name_map[v]] += 1./minD[name_map[u], name_map[v]] return cl
def GetFirstOrderDistanceMatrix(t): """Calculates a matrix D containing the shortest path lengths between all pairs of nodes calculated based on the topology of the *first-order* aggregate network. The ordering of rows/columns corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between nodes and indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network to calculate shortest path lengths for based on a first-order aggregate representation """ # This way of generating the first-order time-aggregated network makes sure that # links are not omitted even if they do not contribute to any time-respecting path g1 = t.igraphFirstOrder(all_links=False, force=True) name_map = Utilities.firstOrderNameMap(t) D = np.zeros(shape=(len(t.nodes), len(t.nodes))) D.fill(np.inf) np.fill_diagonal(D, 0) for v in g1.vs()["name"]: for w in g1.vs()["name"]: # Compute all shortest paths using igraph X = g1.get_shortest_paths(v, w) for p in X: if len(p) > 0: D[name_map[v], name_map[w]] = len(p) - 1 return D
def GetFirstOrderDistanceMatrix(t): """Calculates a matrix D containing the shortest path lengths between all pairs of nodes calculated based on the topology of the *first-order* aggregate network. The ordering of rows/columns corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between nodes and indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network to calculate shortest path lengths for based on a first-order aggregate representation """ # This way of generating the first-order time-aggregated network makes sure that # links are not omitted even if they do not contribute to any time-respecting path g1 = t.igraphFirstOrder(all_links=False, force=True) name_map = Utilities.firstOrderNameMap( t ) D = np.zeros(shape=(len(t.nodes),len(t.nodes))) D.fill(np.inf) np.fill_diagonal(D, 0) for v in g1.vs()["name"]: for w in g1.vs()["name"]: # Compute all shortest paths using igraph X = g1.get_shortest_paths(v,w) for p in X: if len(p)>0: D[name_map[v], name_map[w]] = len(p)-1 return D
def GetTemporalBetweenness(t, delta=1, normalized=False): """Calculates the temporal betweenness centralities of all nodes in a temporal network t based on the shortest time-respecting paths with a maximum waiting time of delta. This function returns a numpy array of temporal betweenness centrality values of nodes. The ordering of these values corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between node names and array indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network for which temporal closeness centralities will be computed @param delta: the maximum time difference used in the time-respecting path definition (default 1). Note that this parameter is independent from the delta used internally for the extraction of two-paths by the class TemporalNetwork @param normalized: whether or not to normalize centralities by dividing each value byt the total number of shortest time-respecting paths. """ bw = np.array([0]*len(t.nodes)) S = 0 name_map = Utilities.firstOrderNameMap(t) minD, minPaths = Paths.GetMinTemporalDistance(t, delta=1, collect_paths=True) for v in t.nodes: for w in t.nodes: for p in minPaths[v][w]: for i in range(1,len(p)-1): bw[name_map[p[i][0]]] += 1 S+=1 return bw
def GetTemporalCloseness(t, delta=1): """Calculates the temporal closeness centralities of all nodes in a temporal network t, based on the minimal shortest time-respecting paths with a maximum time difference of delta. This function then returns a numpy array of average (temporal) closeness centrality values of nodes. The ordering of these values corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between node names and array indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network for which temporal closeness centralities will be computed @param delta: the maximum waiting time used in the time-respecting path definition (default 1) Note that this parameter is independent from the delta used internally for the extraction of two-paths by the class TemporalNetwork """ cl = np.array([0.] * len(t.nodes)) name_map = Utilities.firstOrderNameMap(t) minD, minPaths = Paths.GetMinTemporalDistance(t, delta, collect_paths=False) for u in t.nodes: for v in t.nodes: if u != v: cl[name_map[v]] += 1. / minD[name_map[u], name_map[v]] return cl
def GetStaticCloseness(t, model='SECOND'): """Computes closeness centralities of nodes based on the first- or second-order time-aggregated network. @param t: The temporal network instance for which closeness centralities will be computed @param model: either C{"FIRST"}, C{"SECOND"} or C{"SECONDNULL"}, where C{"SECOND"} is the the default value. """ if model == 'FIRST': D = Paths.GetFirstOrderDistanceMatrix(t) else: D = Paths.GetSecondOrderDistanceMatrix(t, model) name_map = Utilities.firstOrderNameMap(t) closeness = np.zeros(len(name_map)) # Calculate closeness for each node u, by summing the reciprocal of its # distances to all other nodes. Note that this definition of closeness centrality # is required for directed networks that are not strongly connected. for u in t.nodes: for v in t.nodes: if u != v: closeness[name_map[u]] += 1. / D[name_map[v], name_map[u]] return closeness
def GetTemporalBetweennessInstantaneous(t, start_t=0, delta=1, normalized=False): """Calculates the temporal betweennness values of all nodes fir a given start time start_t in an empirical temporal network t. This function returns a numpy array of (temporal) betweenness centrality values. The ordering of these values corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between node names and array indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network for which temporal betweenness centralities will be computed @param start_t: the start time for which to consider time-respecting paths (default 0). This is important, since any unambigious definition of a shortest time-respecting path between two nodes must include the time range to be considered (c.f. Holme and Saramäki, Phys. Rep., 2012) @param delta: the maximum waiting time used in the time-respecting path definition (default 1) Note that this parameter is independent from the delta used internally for the extraction of two-paths by the class TemporalNetwork @param normalized: whether or not to normalize the temporal betweenness centrality values by dividing by the number of all shortest time-respecting paths in the temporal network. """ bw = np.array([0] * len(t.nodes)) # First calculate all shortest time-respecting paths starting at time start_t D, paths = Paths.GetTemporalDistanceMatrix(t, start_t, delta, collect_paths=True) # Get a mapping between node names and matrix indices name_map = Utilities.firstOrderNameMap(t) # Compute betweenness scores of all nodes based on shortest time-respecting paths k = 0 for u in t.nodes: for v in t.nodes: if u != v: for p in paths[u][v]: for i in range(1, len(p) - 1): bw[name_map[p[i][0]]] += 1 k += 1 # Normalize by dividing by the total number of shortest time-respecting paths if normalized: bw = bw / k return bw
def GetStaticPageRank(t, model='SECOND', projection='TARGET', normalization=False): """Computes PageRank of nodes based on the second-order aggregate network, and aggregates PageRank values to obtain the PageRank of nodes in the first-order network. @param t: The temporalnetwork instance to work on @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value. """ assert model is "SECOND" or model is "NULL" assert projection is 'TARGET' or projection is 'SOURCE' name_map = Utilities.firstOrderNameMap(t) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() # Compute pagerank centrality in second-order network pagerank_2 = np.array(g2.pagerank(weights=g2.es()['weight'], directed=True)) # Aggregate to obtain first-order pagerank centrality pagerank_1 = np.zeros(len(name_map)) counts = np.array([1] * len(name_map)) sep = t.separator for i in range(len(pagerank_2)): # Get name of target node if projection == 'TARGET': target = g2.vs()[i]["name"].split(sep)[1] pagerank_1[name_map[target]] += pagerank_2[i] counts[name_map[target]] += 1 else: source = g2.vs()[i]["name"].split(sep)[0] pagerank_1[name_map[source]] += pagerank_2[i] counts[name_map[source]] += 1 if normalization == True: pagerank_1 = pagerank_1 / counts return pagerank_1
def GetMinTemporalDistance(t, delta=1, collect_paths=True): """ Computes the minimum temporal distance between all pairs of nodes in terms of time-respecting paths (using a given maximum time difference delta), across all possible starting times in the temporal network @param t: the temporal network to calculate the distance for @param delta: the maximum waiting time to be used for the definition of time-respecting paths. Note that this is independent of the delta parameter set in the temporal networks instancd for the two-path extraction @param collect_paths: whether or not to return all shortest time-respecting paths. If False, only shortest path distances will be returned. """ Log.add('Computing minimum temporal distances for delta = ' + str(int(delta)) + ' ...') name_map = Utilities.firstOrderNameMap(t) minD = np.zeros(shape=(len(t.nodes), len(t.nodes))) minD.fill(np.inf) # Each node is connected to itself via a path of length zero np.fill_diagonal(minD, 0) minPaths = defaultdict(lambda: defaultdict(lambda: [])) for start_t in t.ordered_times: D, paths = GetTemporalDistanceMatrix(t, start_t, delta, collect_paths) for v in t.nodes: for w in t.nodes: if D[name_map[v], name_map[w]] < minD[name_map[v], name_map[w]]: minD[name_map[v], name_map[w]] = D[name_map[v], name_map[w]] minPaths[v][w] = paths[v][w] elif D[name_map[v], name_map[w]] == minD[name_map[v], name_map[w]] and minD[ name_map[v], name_map[w]] < np.inf: for p in paths[v][w]: if p not in minPaths[v][w]: minPaths[v][w] = minPaths[v][w] + [p] Log.add('finished.') return minD, minPaths
def GetStaticPageRank(t, model='SECOND', projection='TARGET', normalization=False): """Computes PageRank of nodes based on the second-order aggregate network, and aggregates PageRank values to obtain the PageRank of nodes in the first-order network. @param t: The temporalnetwork instance to work on @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value. """ assert model is "SECOND" or model is "NULL" assert projection is 'TARGET' or projection is 'SOURCE' name_map = Utilities.firstOrderNameMap( t ) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() # Compute pagerank centrality in second-order network pagerank_2 = np.array(g2.pagerank(weights=g2.es()['weight'], directed=True)) # Aggregate to obtain first-order pagerank centrality pagerank_1 = np.zeros(len(name_map)) counts = np.array([1]*len(name_map)) sep = t.separator for i in range(len(pagerank_2)): # Get name of target node if projection == 'TARGET': target = g2.vs()[i]["name"].split(sep)[1] pagerank_1[name_map[target]] += pagerank_2[i] counts[name_map[target]] +=1 else: source = g2.vs()[i]["name"].split(sep)[0] pagerank_1[name_map[source]] += pagerank_2[i] counts[name_map[source]] +=1 if normalization == True: pagerank_1 = pagerank_1 / counts return pagerank_1
def GetTemporalBetweennessInstantaneous(t, start_t=0, delta=1, normalized=False): """Calculates the temporal betweennness values of all nodes fir a given start time start_t in an empirical temporal network t. This function returns a numpy array of (temporal) betweenness centrality values. The ordering of these values corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between node names and array indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network for which temporal betweenness centralities will be computed @param start_t: the start time for which to consider time-respecting paths (default 0). This is important, since any unambigious definition of a shortest time-respecting path between two nodes must include the time range to be considered (c.f. Holme and Saramäki, Phys. Rep., 2012) @param delta: the maximum waiting time used in the time-respecting path definition (default 1) Note that this parameter is independent from the delta used internally for the extraction of two-paths by the class TemporalNetwork @param normalized: whether or not to normalize the temporal betweenness centrality values by dividing by the number of all shortest time-respecting paths in the temporal network. """ bw = np.array([0]*len(t.nodes)) # First calculate all shortest time-respecting paths starting at time start_t D, paths = Paths.GetTemporalDistanceMatrix(t, start_t, delta, collect_paths=True) # Get a mapping between node names and matrix indices name_map = Utilities.firstOrderNameMap( t ) # Compute betweenness scores of all nodes based on shortest time-respecting paths k=0 for u in t.nodes: for v in t.nodes: if u != v: for p in paths[u][v]: for i in range(1, len(p)-1): bw[name_map[p[i][0]]] += 1 k+=1 # Normalize by dividing by the total number of shortest time-respecting paths if normalized: bw = bw/k return bw
def GetSecondOrderDistanceMatrix(t, model='SECOND'): """Calculates a matrix D containing the shortest path lengths between all pairs of nodes calculated based on the topology of the *second-order* aggregate network. The ordering of rows/columns corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between nodes and indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network to calculate shortest path lengths for based on a second-order aggregate representation @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value. """ if (model is "SECOND" or "NULL") == False: raise ValueError("model must be one of \"SECOND\" or \"NULL\"") name_map = Utilities.firstOrderNameMap(t) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() D = np.zeros(shape=(len(t.nodes), len(t.nodes))) D.fill(np.inf) np.fill_diagonal(D, 0) sep = t.separator for v in g2.vs()["name"]: source = v.split(sep)[0] for w in g2.vs()["name"]: target = w.split(sep)[1] X = g2.get_shortest_paths(v, w) for p in X: if len(p) > 0: D[name_map[source], name_map[target]] = min( len(p), D[name_map[source], name_map[target]]) return D
def Laplacian(temporalnet, model="SECOND"): """Returns the transposed Laplacian matrix corresponding to the the second-order (model=SECOND) or the second-order null (model=NULL) model for a temporal network. @param temporalnet: The temporalnetwork instance to work on @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value. """ if (model is "SECOND" or "NULL") == False: raise ValueError("model must be one of \"SECOND\" or \"NULL\"") if model == "SECOND": network = temporalnet.igraphSecondOrder().components( mode="STRONG").giant() elif model == "NULL": network = temporalnet.igraphSecondOrderNull().components( mode="STRONG").giant() T2 = Utilities.RWTransitionMatrix(network) I = sparse.identity(len(network.vs())) return I - T2
def GetMinTemporalDistance(t, delta=1, collect_paths=True): """ Computes the minimum temporal distance between all pairs of nodes in terms of time-respecting paths (using a given maximum time difference delta), across all possible starting times in the temporal network @param t: the temporal network to calculate the distance for @param delta: the maximum waiting time to be used for the definition of time-respecting paths. Note that this is independent of the delta parameter set in the temporal networks instancd for the two-path extraction @param collect_paths: whether or not to return all shortest time-respecting paths. If False, only shortest path distances will be returned. """ Log.add('Computing minimum temporal distances for delta = ' + str(int(delta)) + ' ...') name_map = Utilities.firstOrderNameMap( t ) minD = np.zeros(shape=(len(t.nodes),len(t.nodes))) minD.fill(np.inf) # Each node is connected to itself via a path of length zero np.fill_diagonal(minD, 0) minPaths = defaultdict( lambda: defaultdict( lambda: [] ) ) for start_t in t.ordered_times: D, paths = GetTemporalDistanceMatrix(t, start_t, delta, collect_paths) for v in t.nodes: for w in t.nodes: if D[name_map[v], name_map[w]] < minD[name_map[v], name_map[w]]: minD[name_map[v], name_map[w]] = D[name_map[v], name_map[w]] minPaths[v][w] = paths[v][w] elif D[name_map[v], name_map[w]] == minD[name_map[v], name_map[w]] and minD[name_map[v], name_map[w]] < np.inf: for p in paths[v][w]: if p not in minPaths[v][w]: minPaths[v][w] = minPaths[v][w] + [p] Log.add('finished.') return minD, minPaths
def GetSecondOrderDistanceMatrix(t, model='SECOND'): """Calculates a matrix D containing the shortest path lengths between all pairs of nodes calculated based on the topology of the *second-order* aggregate network. The ordering of rows/columns corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between nodes and indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network to calculate shortest path lengths for based on a second-order aggregate representation @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value. """ if (model is "SECOND" or "NULL") == False: raise ValueError("model must be one of \"SECOND\" or \"NULL\"") name_map = Utilities.firstOrderNameMap( t ) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() D = np.zeros(shape=(len(t.nodes),len(t.nodes))) D.fill(np.inf) np.fill_diagonal(D, 0) sep = t.separator for v in g2.vs()["name"]: source = v.split(sep)[0] for w in g2.vs()["name"]: target = w.split(sep)[1] X = g2.get_shortest_paths(v,w) for p in X: if len(p)>0: D[name_map[source], name_map[target]] = min(len(p), D[name_map[source], name_map[target]]) return D
def GetStaticBetweenness(t, model='SECOND'): """Computes betweenness centralities of nodes based on the second-order aggregate network, and aggregates betweenness centralities to obtain the betweenness centrality of nodes in the first-order network. @param t: The temporalnetwork instance to work on @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value. """ if (model is "SECOND" or "NULL") == False: raise ValueError("model must be one of \"SECOND\" or \"NULL\"") D = Paths.GetSecondOrderDistanceMatrix(t) name_map = Utilities.firstOrderNameMap(t) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() # Compute betweenness centrality based on second-order network bwcent_1 = np.zeros(len(name_map)) sep = t.separator for v in g2.vs()["name"]: for w in g2.vs()["name"]: s = v.split(sep)[0] t = w.split(sep)[1] X = g2.get_all_shortest_paths(v, w) for p in X: if D[name_map[s], name_map[t]] == len(p) and len(p) > 1: for i in range(len(p)): source = g2.vs()["name"][p[i]].split(sep)[0] if i > 0: bwcent_1[name_map[source]] += 1 return bwcent_1
def GetTemporalClosenessInstantaneous(t, start_t=0, delta=1): """Calculates the temporal closeness values of all nodes for a given start time start_t in a temporal network t. This function returns a numpy array of (temporal) closeness centrality values. The ordering of these values corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between node names and array indices can be found in Utilities.firstOrderNameMap(). @param t: the temporal network for which temporal closeness centralities will be computed @param start_t: the start time for which to consider time-respecting paths (default 0). This is important, since any unambigious definition of a shortest time-respecting path between two nodes must include the time range to be considered (c.f. Holme and Saramäki, Phys. Rep., 2012) @param delta: the maximum time difference time used in the time-respecting path definition (default 1) Note that this parameter is independent from the delta used internally for the extraction of two-paths by the class TemporalNetwork. """ closeness = np.array([0.] * len(t.nodes)) # Calculate all shortest time-respecting paths D, paths = Paths.GetTemporalDistanceMatrix(t, start_t, delta, collect_paths=False) # Get a mapping between node names and matrix indices name_map = Utilities.firstOrderNameMap(t) # Calculate closeness for each node u, by summing the reciprocal of its # distances to all other nodes. Note that this definition of closeness centrality # is required for directed networks that are not strongly connected. for u in t.nodes: for v in t.nodes: if u != v: closeness[name_map[u]] += 1. / D[name_map[v], name_map[u]] return closeness
def GetStaticBetweenness(t, model='SECOND'): """Computes betweenness centralities of nodes based on the second-order aggregate network, and aggregates betweenness centralities to obtain the betweenness centrality of nodes in the first-order network. @param t: The temporalnetwork instance to work on @param model: either C{"SECOND"} or C{"NULL"}, where C{"SECOND"} is the the default value. """ if (model is "SECOND" or "NULL") == False: raise ValueError("model must be one of \"SECOND\" or \"NULL\"") D = Paths.GetSecondOrderDistanceMatrix(t) name_map = Utilities.firstOrderNameMap( t ) if model == 'SECOND': g2 = t.igraphSecondOrder() else: g2 = t.igraphSecondOrderNull() # Compute betweenness centrality based on second-order network bwcent_1 = np.zeros(len(name_map)) sep = t.separator for v in g2.vs()["name"]: for w in g2.vs()["name"]: s = v.split(sep)[0] t = w.split(sep)[1] X = g2.get_all_shortest_paths(v,w) for p in X: if D[name_map[s], name_map[t]] == len(p) and len(p) > 1: for i in range(len(p)): source = g2.vs()["name"][p[i]].split(sep)[0] if i>0: bwcent_1[name_map[source]] += 1 return bwcent_1
def EntropyGrowthRateRatio(t, mode='FIRSTORDER', method='MLE'): """Computes the ratio between the entropy growth rate ratio between the second-order and first-order model of a temporal network t. Ratios smaller than one indicate that the temporal network exhibits non-Markovian characteristics""" # NOTE to myself: most of the time here goes into computation of the # NOTE EV of the transition matrix for the bigger of the # NOTE two graphs below (either 2nd-order or 2nd-order null) assert method == 'MLE' or method == 'Miller' # Generate strongly connected component of second-order network g2 = t.igraphSecondOrder().components(mode="STRONG").giant() Log.add('Calculating entropy growth rate ratio ... ', Severity.INFO) # Compute entropy growth rate of observed transition matrix T2 = Utilities.RWTransitionMatrix(g2) T2_pi = Utilities.StationaryDistribution(T2) T2.data *= np.log2(T2.data) # For T2, we can apply a Miller correction to the entropy estimation if method == 'Miller': # K is the number of possible two-paths that can exist based on the # time-stamped edge sequence (or observed two paths) if len(t.tedges) > 0: edges = set((e[0], e[1]) for e in t.tedges) else: edges = [(tp[0], tp[1]) for tp in t.twopaths] for tp in t.twopaths: edges.append((tp[1], tp[2])) edges = set(edges) K = len(Utilities.getPossibleTwoPaths(edges)) #print('K = ', K) # N is the number of observations used to estimate the transition probabilities # in the second-order network. This corresponds to the total link weight in the # second-order network (accounting for the fractional couting of two-paths) N = np.sum(g2.es["weight"]) #print('N = ', N) H2 = np.sum(T2 * T2_pi) + (K - 1) / (2 * N) else: # simple MLE estimation H2 = -np.sum(T2 * T2_pi) H2 = np.absolute(H2) # Compute entropy rate of null model if mode == 'FIRSTORDER': g2n = t.igraphFirstOrder().components(mode="STRONG").giant() else: g2n = t.igraphSecondOrderNull().components(mode="STRONG").giant() # For the entropy rate of the null model, no Miller correction is needed # since we assume that transitions correspond to the true probabilities T2n = Utilities.RWTransitionMatrix(g2n) T2n_pi = Utilities.StationaryDistribution(T2n) T2n.data *= np.log2(T2n.data) H2n = -np.sum(T2n * T2n_pi) H2n = np.absolute(H2n) Log.add('finished.', Severity.INFO) # Return ratio return H2 / H2n
def exportDiffusionMovieFramesFirstOrder(t, file_prefix='diffusion', visual_style=None, steps=100, initial_index=-1, model='SECOND', dynamic=False, NWframesPerRWStep=5): """Exports an animation showing the evolution of a diffusion process on the first-order aggregate network, where random walk dynamics either follows a first-order (mode='NULL') or second-order (model='SECOND') Markov model""" assert model == 'SECOND' or model == 'NULL' g1 = t.igraphFirstOrder() if model == 'SECOND': g2 = t.igraphSecondOrder() temporal = tn.TemporalNetwork.ShuffleTwoPaths(t, l=steps) elif model == 'NULL': g2 = t.igraphSecondOrderNull() temporal = tn.TemporalNetwork.ShuffleEdges(t, l=steps) T = Utilities.RWTransitionMatrix(g2) # visual style is for *first-order* aggregate network if visual_style == None: visual_style = {} visual_style["vertex_color"] = "lightblue" visual_style["vertex_label"] = g1.vs["name"] visual_style["edge_curved"] = .5 visual_style["vertex_size"] = 30 visual_style["edge_color"] = ["darkgrey"] * g1.ecount() visual_style["edge_width"] = [.5] * g1.ecount() # Initial state of random walker if initial_index < 0: initial_index = np.random.randint(0, len(g2.vs())) exp = 1.0 / .75 x = np.zeros(len(g2.vs())) x[initial_index] = 1 # This index allows to quickly map node names to indices in the first-order network map_name_to_id = {} for i in range(len(g1.vs())): map_name_to_id[g1.vs()['name'][i]] = i # Index to quickly map second-order node indices to first-order node indices map_2_to_1 = {} for j in range(len(g2.vs())): # j is index of node in *second-order* network # we first get the name of the *target* of the underlying edge node = g2.vs()["name"][j].split(t.separator)[1] # we map the target of second-order node j to the index of the *first-order* node map_2_to_1[j] = map_name_to_id[node] # compute stationary state of random walk process pi = Utilities.StationaryDistribution(T) scale = np.mean(np.abs(x - pi)) # lambda expression for the coloring of nodes according to some quantity p \in [0,1] # p = 1 ==> color red # p = 0 ==> color white color_p = lambda p: "rgb(255," + str(int( (1 - p) * 255)) + "," + str(int((1 - p) * 255)) + ")" visual_style["edge_color"] = ["darkgrey"] * g1.ecount() visual_style["edge_width"] = [.5] * g1.ecount() # Create video frames for i in range(0, steps): # based on visitation probabilities in *second-order* aggregate network, # we need to compute visitation probabilities of nodes in the *first-order* # aggregate network x_firstorder = np.zeros(len(g1.vs())) # j is the index of nodes in the *second-order* network, which we need to map # to nodes in the *first-order* network for j in range(len(x)): if x[j] > 0: x_firstorder[ map_2_to_1[j]] = x_firstorder[map_2_to_1[j]] + x[j] # Perform some reasonable color scaling visual_style["vertex_color"] = [ color_p(np.power((p - min(x)) / (max(x) - min(x)), exp)) for p in x_firstorder ] # Visualize illustrative network dynamics if dynamic == True: #slice = igraph.Graph(n=len(g1.vs())) #slice.vs["name"] = g1.vs["name"] L = len(temporal.ordered_times) for e in temporal.time[temporal.ordered_times[i % L]]: e_id = g1.get_eid(e[0], e[1]) visual_style["edge_width"][e_id] = 5 visual_style["edge_color"][e_id] = "black" #slice.add_edge(e[0], e[1]) igraph.plot(g1, file_prefix + "_frame_" + str(i).zfill(5) + ".png", **visual_style) else: igraph.plot(g1, file_prefix + "_frame_" + str(i).zfill(5) + ".png", **visual_style) # Plot first-order aggregate network after 30 RW steps (particularly useful as poster frame of video) if i == 30: visual_style["edge_color"] = "black" visual_style["edge_width"] = 1 igraph.plot(g1, file_prefix + "_network.pdf", **visual_style) # Reset edge color and width visual_style["edge_color"] = ["darkgrey"] * g1.ecount() visual_style["edge_width"] = [.5] * g1.ecount() if i % 50 == 0: Log.add('Frame ' + str(i) + '\tTVD = ' + str(Utilities.TVD(x, pi))) # every NWframesPerRWStep frames, perform one random walk step if i % NWframesPerRWStep == 0: # NOTE x * T = (T^T * x^T)^T # NOTE T is already transposed to get the left EV x = (T.dot(x.transpose())).transpose()
def BetweennessPreference(t, v, normalized=False, method='MLE'): """Computes the betweenness preference of a node v in a temporal network t @param t: The temporalnetwork instance to work on @param v: Name of the node to compute its BetweennessPreference @param normalized: whether or not (default) to normalize @param method: which entropy estimation method to use. The method supports 'Miller' for Miller-corrected MLE or 'MLE' for a naive MLE estimation. """ g = t.igraphFirstOrder() assert method == 'MLE' or method == 'Miller' # If the network is empty, just return zero if len(g.vs) == 0: return 0.0 # First create the betweenness preference matrix (equation (2) of the paper) B_v = BWPrefMatrix(t, v) # Normalize matrix (equation (3) of the paper) # NOTE: P_v has the same shape as B_v P_v = np.zeros(shape=B_v.shape) S = np.sum(B_v) if S > 0: P_v = B_v / S ## Compute marginal probabilities ## Marginal probabilities P^v_d = \sum_s'{P_{s'd}} marginal_d = np.sum(P_v, axis=0) ## Marginal probabilities P^v_s = \sum_d'{P_{sd'}} marginal_s = np.sum(P_v, axis=1) if method == 'Miller': # print('v = ', v) v_vertex = g.vs.find(name=v) # total number of samples, i.e. observed two-paths N = np.sum(B_v) #print('N = ', N) #print('B = ', B_v) #print('marginal_s = ', marginal_s) #print('marginal_d = ', marginal_d) # marginal entropy H(S) H_s = Utilities.Entropy_Miller(marginal_s, len(marginal_s), N) # print('H(S) = ', H_s) # marginal entropy H(D) H_d = Utilities.Entropy_Miller(marginal_d, len(marginal_d), N) #print('H(D) = ', H_d) # we need the conditional entropy H(D|S) H_ds = 0 for s in range(len(marginal_s)): # number of two paths s -> v -> * observed in the data N_s = np.sum(B_v[s, :]) #print('N(s=' + str(s) + ') = ' + str(N_s)) # probabilities of all destinations, given the particular source s p_ds = B_v[s, :] / np.sum(B_v[s, :]) #print('P(D|S=' + str(s) + ') = '+ str(p_ds)) # number of possible destinations d K_s = len(p_ds) #print('K(s=' + str(s) + ') = ' + str(K_s)) # marginal_s[s] is the overall probability of source s p_s = marginal_s[s] # add to conditional entropy H_ds += p_s * Utilities.Entropy_Miller(p_ds, K_s, N_s) #print('H(D|S) = ', H_ds) else: # apply 'naive' MLE estimation, identical to above H_s = Utilities.Entropy(marginal_s) H_d = Utilities.Entropy(marginal_d) H_ds = 0 for s in range(len(marginal_s)): p_ds = P_v[s, :] / np.sum(P_v[s, :]) H_ds += marginal_s[s] * Utilities.Entropy(p_ds) # Alternative calculation (without explicit entropies) # build mask for non-zero elements # row, col = np.nonzero(P_v) # pv = P_v[(row,col)] # marginal = np.outer(marginal_s, marginal_d) # log_argument = np.divide( pv, marginal[(row,col)] ) # I = np.dot( pv, np.log2(log_argument) ) I = H_d - H_ds if normalized: I = I / np.min([H_s, H_d]) return I
def GetTemporalDistanceMatrix(t, start_t=-1, delta=1, collect_paths=True): """A new and faster method to compute the (topologically) shortest time-respecting paths between all pairs of nodes starting at time start_t in an empirical temporal network t. This function returns a tuple consisting of 1) a matrix D containing the shortest time-respecting path lengths between all pairs of nodes. The ordering of rows/columns corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between nodes and indices can be found in Utilities.firstOrderNameMap(). 2) a list of shortest time-respecting paths, each entry being an ordered sequence of nodes on the corresponding path. @param t: the temporal network to calculate shortest time-respecting paths for @param start_t: the start time for which to consider time-respecting paths (default is t.ordered_times[0]) @param delta: the maximum time difference to be used in the time-respecting path definition (default 1). Note that this parameter is independent from the internal parameter delta used for two-path extraction in the class TemporalNetwork @param collect_paths: whether or not to collect all shortest time-respecting paths (default = True). If this is set to False, the method will only compute the lengths of shortest time-respecting paths, but not return the actual paths. """ if start_t == -1: start_t = t.ordered_times[0] # Initialize dictionary taking shortest paths Paths = defaultdict( lambda: defaultdict( lambda: [] ) ) # Get a mapping between node names and matrix indices name_map = Utilities.firstOrderNameMap( t ) # Initialize topological distance matrix # TODO: This may yield a memory problem for large graphs D = np.zeros(shape=(len(t.nodes),len(t.nodes))) D.fill(np.inf) # For each node v, calculate shortest/fastest paths to all other nodes ... for v in t.nodes: # Mark node v as visited at the start time start_t... D[name_map[v], name_map[v]] = 0 Paths[v][v] = [ [(v,start_t)] ] stack = set([ (v, start_t) ]) # While there are nodes, which could possibly continue a time-respecting path while len(stack)>0: (x,ts) = stack.pop() # Get indices of time range within which a time-respecting path via x # can possibly be continued min_ix = bisect_left(t.activities[x], ts) max_ix = bisect_left(t.activities[x], ts+delta)-1 # For all time-stamps at which x is a source node ... for j in range(min_ix, max_ix+1): time = t.activities[x][j] # For all edges starting at node x at this time for e in t.sources[time][x]: # We found a new node on a time-respecting path new_node = (e[1], time+1) # This node can again continue time-respecting paths # The set will take care that no duplicates are recorded stack.add( new_node ) # Check whether we found a time-respecting path shorter than the current shortest one ... if D[name_map[v], name_map[e[1]]] > D[name_map[v], name_map[e[0]]] + 1: # In this case we update the distance matrix D[name_map[v], name_map[e[1]]] = D[name_map[v], name_map[e[0]]] + 1 if collect_paths == True: # Delete any previous shortest paths Paths[v][e[1]] = [] # Collect all paths to e[0] and concatenate with the current node e[1] for p in Paths[v][e[0]]: Paths[v][e[1]] = Paths[v][e[1]] + [p + [(e[1],time+1)]] # We may also have found a path that has the same length as other shortest paths ... elif collect_paths == True and D[name_map[v], name_map[e[1]]] == D[name_map[v], name_map[e[0]]] + 1: # Collect all paths to e[0] and concatenate with the current node e[1] for p in Paths[v][e[0]]: Paths[v][e[1]] = Paths[v][e[1]] + [p + [(e[1],time+1)]] # The algorithm terminates as soon as it is impossible to continue any of the time-respecting paths return D, Paths
def GetTemporalDistanceMatrix(t, start_t=-1, delta=1, collect_paths=True): """A new and faster method to compute the (topologically) shortest time-respecting paths between all pairs of nodes starting at time start_t in an empirical temporal network t. This function returns a tuple consisting of 1) a matrix D containing the shortest time-respecting path lengths between all pairs of nodes. The ordering of rows/columns corresponds to the ordering of nodes in the vertex sequence of the igraph first order time-aggregated network. A mapping between nodes and indices can be found in Utilities.firstOrderNameMap(). 2) a list of shortest time-respecting paths, each entry being an ordered sequence of nodes on the corresponding path. @param t: the temporal network to calculate shortest time-respecting paths for @param start_t: the start time for which to consider time-respecting paths (default is t.ordered_times[0]) @param delta: the maximum time difference to be used in the time-respecting path definition (default 1). Note that this parameter is independent from the internal parameter delta used for two-path extraction in the class TemporalNetwork @param collect_paths: whether or not to collect all shortest time-respecting paths (default = True). If this is set to False, the method will only compute the lengths of shortest time-respecting paths, but not return the actual paths. """ if start_t == -1: start_t = t.ordered_times[0] # Initialize dictionary taking shortest paths Paths = defaultdict(lambda: defaultdict(lambda: [])) # Get a mapping between node names and matrix indices name_map = Utilities.firstOrderNameMap(t) # Initialize topological distance matrix # TODO: This may yield a memory problem for large graphs D = np.zeros(shape=(len(t.nodes), len(t.nodes))) D.fill(np.inf) # For each node v, calculate shortest/fastest paths to all other nodes ... for v in t.nodes: # Mark node v as visited at the start time start_t... D[name_map[v], name_map[v]] = 0 Paths[v][v] = [[(v, start_t)]] stack = set([(v, start_t)]) # While there are nodes, which could possibly continue a time-respecting path while len(stack) > 0: (x, ts) = stack.pop() # Get indices of time range within which a time-respecting path via x # can possibly be continued min_ix = bisect_left(t.activities[x], ts) max_ix = bisect_left(t.activities[x], ts + delta) - 1 # For all time-stamps at which x is a source node ... for j in range(min_ix, max_ix + 1): time = t.activities[x][j] # For all edges starting at node x at this time for e in t.sources[time][x]: # We found a new node on a time-respecting path new_node = (e[1], time + 1) # This node can again continue time-respecting paths # The set will take care that no duplicates are recorded stack.add(new_node) # Check whether we found a time-respecting path shorter than the current shortest one ... if D[name_map[v], name_map[e[1]]] > D[name_map[v], name_map[e[0]]] + 1: # In this case we update the distance matrix D[name_map[v], name_map[e[1]]] = D[name_map[v], name_map[e[0]]] + 1 if collect_paths == True: # Delete any previous shortest paths Paths[v][e[1]] = [] # Collect all paths to e[0] and concatenate with the current node e[1] for p in Paths[v][e[0]]: Paths[v][e[1]] = Paths[v][e[1]] + [ p + [(e[1], time + 1)] ] # We may also have found a path that has the same length as other shortest paths ... elif collect_paths == True and D[name_map[v], name_map[ e[1]]] == D[name_map[v], name_map[e[0]]] + 1: # Collect all paths to e[0] and concatenate with the current node e[1] for p in Paths[v][e[0]]: Paths[v][e[1]] = Paths[v][e[1]] + [ p + [(e[1], time + 1)] ] # The algorithm terminates as soon as it is impossible to continue any of the time-respecting paths return D, Paths