def aggregate_to_src_idx(netw_src_idx: int, node_data: np.ndarray, edge_data: np.ndarray, node_edge_map: Dict, data_map: np.ndarray, max_dist: float, angular: bool = False): # this function is typically called iteratively, so do type checks from parent methods netw_x_arr = node_data[:, 0] netw_y_arr = node_data[:, 1] netw_src_x = netw_x_arr[netw_src_idx] netw_src_y = netw_y_arr[netw_src_idx] d_x_arr = data_map[:, 0] d_y_arr = data_map[:, 1] d_assign_nearest = data_map[:, 2] d_assign_next_nearest = data_map[:, 3] # run the shortest tree dijkstra # keep in mind that predecessor map is based on impedance heuristic - which can be different from metres # NOTE -> use np.inf for max distance so as to explore all paths # In some cases the predecessor nodes will be within reach even if the closest node is not # Total distance is checked later tree_map, tree_edges = centrality.shortest_path_tree( edge_data, node_edge_map, netw_src_idx, max_dist=max_dist, angular=angular) # turn off checks! This is called iteratively... tree_preds = tree_map[:, 1] tree_dists = tree_map[:, 2] # filter the data by distance # in this case, the source x, y is the same as for the networks filtered_data = radial_filter(netw_src_x, netw_src_y, d_x_arr, d_y_arr, max_dist) # arrays for writing the reachable data points and their distances reachable_data = np.full(len(data_map), False) reachable_data_dist = np.full(len(data_map), np.inf) # iterate the distance trimmed data points reachable_idx = np.where(filtered_data)[0] for data_idx in reachable_idx: # find the primary assigned network index for the data point if np.isfinite(d_assign_nearest[data_idx]): netw_idx = int(d_assign_nearest[data_idx]) # if the assigned network node is within the threshold if tree_dists[netw_idx] < max_dist: # get the distance from the data point to the network node d_d = np.hypot(d_x_arr[data_idx] - netw_x_arr[netw_idx], d_y_arr[data_idx] - netw_y_arr[netw_idx]) # add to the distance assigned for the network node dist = tree_dists[netw_idx] + d_d # only assign distance if within max distance if dist <= max_dist: reachable_data[data_idx] = True reachable_data_dist[data_idx] = dist # the next-nearest may offer a closer route depending on the direction the shortest path approaches from if np.isfinite(d_assign_next_nearest[data_idx]): netw_idx = int(d_assign_next_nearest[data_idx]) # if the assigned network node is within the threshold if tree_dists[netw_idx] < max_dist: # get the distance from the data point to the network node d_d = np.hypot(d_x_arr[data_idx] - netw_x_arr[netw_idx], d_y_arr[data_idx] - netw_y_arr[netw_idx]) # add to the distance assigned for the network node dist = tree_dists[netw_idx] + d_d # only assign distance if within max distance # AND only if closer than other direction if dist <= max_dist and dist < reachable_data_dist[data_idx]: reachable_data[data_idx] = True reachable_data_dist[data_idx] = dist # note that some entries will be nan values if the max distance was exceeded return reachable_data, reachable_data_dist, tree_preds
def test_aggregate_to_src_idx(primal_graph): node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX(primal_graph) # generate data data_dict = mock.mock_data_dict(primal_graph, random_seed=13) data_uids, data_map = layers.data_map_from_dict(data_dict) for max_dist in [400, 750]: # in this case, use same assignment max dist as search max dist data_map_temp = data_map.copy() data_map_temp = data.assign_to_network(data_map_temp, node_data, edge_data, node_edge_map, max_dist=max_dist) for angular in [True, False]: for netw_src_idx in range(len(node_data)): # aggregate to src... reachable_data, reachable_data_dist, tree_preds = data.aggregate_to_src_idx(netw_src_idx, node_data, edge_data, node_edge_map, data_map_temp, max_dist, angular=angular) # for debugging # from cityseer.tools import plot # plot.plot_graph_maps(node_uids, node_data, edge_data, data_map) # compare to manual checks on distances: netw_x_arr = node_data[:, 0] netw_y_arr = node_data[:, 1] data_x_arr = data_map_temp[:, 0] data_y_arr = data_map_temp[:, 1] # get the network distances tree_map, tree_edges = centrality.shortest_path_tree(edge_data, node_edge_map, netw_src_idx, max_dist=max_dist, angular=angular) tree_dists = tree_map[:, 2] # verify distances vs. the max for d_idx in range(len(data_map_temp)): # check the integrity of the distances and classes reachable = reachable_data[d_idx] reachable_dist = reachable_data_dist[d_idx] # get the distance via the nearest assigned index nearest_dist = np.inf # if a nearest node has been assigned if np.isfinite(data_map_temp[d_idx, 2]): # get the index for the assigned network node netw_idx = int(data_map_temp[d_idx, 2]) # if this node is within the cutoff distance: if tree_dists[netw_idx] < max_dist: # get the distances from the data point to the assigned network node d_d = np.hypot(data_x_arr[d_idx] - netw_x_arr[netw_idx], data_y_arr[d_idx] - netw_y_arr[netw_idx]) # and add it to the network distance path from the source to the assigned node n_d = tree_dists[netw_idx] nearest_dist = d_d + n_d # also get the distance via the next nearest assigned index next_nearest_dist = np.inf # if a nearest node has been assigned if np.isfinite(data_map_temp[d_idx, 3]): # get the index for the assigned network node netw_idx = int(data_map_temp[d_idx, 3]) # if this node is within the radial cutoff distance: if tree_dists[netw_idx] < max_dist: # get the distances from the data point to the assigned network node d_d = np.hypot(data_x_arr[d_idx] - netw_x_arr[netw_idx], data_y_arr[d_idx] - netw_y_arr[netw_idx]) # and add it to the network distance path from the source to the assigned node n_d = tree_dists[netw_idx] next_nearest_dist = d_d + n_d # now check distance integrity if np.isinf(reachable_dist): assert not reachable assert nearest_dist > max_dist and next_nearest_dist > max_dist else: assert reachable assert reachable_dist <= max_dist if nearest_dist < next_nearest_dist: assert reachable_dist == nearest_dist else: assert reachable_dist == next_nearest_dist
def test_shortest_path_tree(primal_graph, dual_graph): node_uids_p, node_data_p, edge_data_p, node_edge_map_p = graphs.graph_maps_from_nX( primal_graph) # prepare round-trip graph for checks G_round_trip = graphs.nX_from_graph_maps(node_uids_p, node_data_p, edge_data_p, node_edge_map_p) # prepare dual graph node_uids_d, node_data_d, edge_data_d, node_edge_map_d = graphs.graph_maps_from_nX( dual_graph) assert len(node_uids_d) > len(node_uids_p) # test all shortest paths against networkX version of dijkstra for max_dist in [0, 500, 2000, np.inf]: for src_idx in range(len(primal_graph)): # check shortest path maps tree_map, tree_edges = centrality.shortest_path_tree( edge_data_p, node_edge_map_p, src_idx, max_dist=max_dist, angular=False) tree_preds_p = tree_map[:, 1] tree_short_dists_p = tree_map[:, 2] # compare against networkx dijkstra nx_dist, nx_path = nx.single_source_dijkstra(G_round_trip, src_idx, weight='length', cutoff=max_dist) for j in range(len(primal_graph)): if j in nx_path: assert find_path(j, src_idx, tree_preds_p) == nx_path[j] assert np.allclose(tree_short_dists_p[j], nx_dist[j], atol=0.001, rtol=0) # compare angular simplest paths for a selection of targets on primal vs. dual # remember, this is angular change not distance travelled # can be compared from primal to dual in this instance because edge segments are straight # i.e. same amount of angular change whether primal or dual graph # plot.plot_nX_primal_or_dual(primal=primal_graph, dual=dual_graph, labels=True, node_size=80) p_source_idx = node_uids_p.index(0) primal_targets = (15, 20, 37) dual_sources = ('0_1', '0_16', '0_31') dual_targets = ('13_15', '17_20', '36_37') for p_target, d_source, d_target in zip(primal_targets, dual_sources, dual_targets): p_target_idx = node_uids_p.index(p_target) d_source_idx = node_uids_d.index( d_source) # dual source index changes depending on direction d_target_idx = node_uids_d.index(d_target) tree_map_p, tree_edges_p = centrality.shortest_path_tree( edge_data_p, node_edge_map_p, p_source_idx, max_dist=max_dist, angular=True) tree_simpl_dists_p = tree_map_p[:, 3] tree_map_d, tree_edges_d = centrality.shortest_path_tree( edge_data_d, node_edge_map_d, d_source_idx, max_dist=max_dist, angular=True) tree_simpl_dists_d = tree_map_d[:, 3] assert np.allclose(tree_simpl_dists_p[p_target_idx], tree_simpl_dists_d[d_target_idx], atol=0.001, rtol=0) # angular impedance should take a simpler but longer path - test basic case on dual # source and target are the same for either src_idx = node_uids_d.index('11_6') target = node_uids_d.index('39_40') # SIMPLEST PATH: get simplest path tree using angular impedance tree_map, tree_edges = centrality.shortest_path_tree( edge_data_d, node_edge_map_d, src_idx, max_dist=np.inf, angular=True) # ANGULAR = TRUE # find path tree_preds = tree_map[:, 1] path = find_path(target, src_idx, tree_preds) path_transpose = [node_uids_d[n] for n in path] # takes 1597m route via long outside segment # tree_dists[int(full_to_trim_idx_map[node_labels.index('39_40')])] assert path_transpose == [ '11_6', '11_14', '10_14', '10_43', '43_44', '40_44', '39_40' ] # SHORTEST PATH: # get shortest path tree using non angular impedance tree_map, tree_edges = centrality.shortest_path_tree( edge_data_d, node_edge_map_d, src_idx, max_dist=np.inf, angular=False) # ANGULAR = FALSE # find path tree_preds = tree_map[:, 1] path = find_path(target, src_idx, tree_preds) path_transpose = [node_uids_d[n] for n in path] # takes 1345m shorter route # tree_dists[int(full_to_trim_idx_map[node_labels.index('39_40')])] assert path_transpose == [ '11_6', '6_7', '3_7', '3_4', '1_4', '0_1', '0_31', '31_32', '32_34', '34_37', '37_39', '39_40' ] # NO SIDESTEPS - explicit check that sidesteps are prevented src_idx = node_uids_d.index('10_43') target = node_uids_d.index('10_5') tree_map, tree_edges = centrality.shortest_path_tree(edge_data_d, node_edge_map_d, src_idx, max_dist=np.inf, angular=True) # find path tree_preds = tree_map[:, 1] path = find_path(target, src_idx, tree_preds) path_transpose = [node_uids_d[n] for n in path] # print(path_transpose) assert path_transpose == ['10_43', '10_5'] # WITH SIDESTEPS - set angular flag to False # manually overwrite distance impedances with angular for this test # (angular has to be false otherwise shortest-path sidestepping avoided) edge_data_d_temp = edge_data_d.copy() # angular impedances at index 3 copied to distance impedances at distance 2 edge_data_d_temp[:, 2] = edge_data_d_temp[:, 3] tree_map, tree_edges = centrality.shortest_path_tree(edge_data_d_temp, node_edge_map_d, src_idx, max_dist=np.inf, angular=False) # find path tree_preds = tree_map[:, 1] path = find_path(target, src_idx, tree_preds) path_transpose = [node_uids_d[n] for n in path] assert path_transpose == ['10_43', '10_14', '10_5']
def test_local_node_centrality(primal_graph): """ Also tested indirectly via test_networks.test_compute_centrality Test centrality methods where possible against NetworkX - i.e. harmonic closeness and betweenness Note that NetworkX improved closeness is not the same as derivation used in this package NetworkX doesn't have a maximum distance cutoff, so run on the whole graph (low beta / high distance) """ # generate node and edge maps node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX( primal_graph) G_round_trip = graphs.nX_from_graph_maps(node_uids, node_data, edge_data, node_edge_map) # needs a large enough beta so that distance thresholds aren't encountered betas = np.array([0.02, 0.01, 0.005, 0.0008, 0.0]) distances = networks.distance_from_beta(betas) # set the keys - add shuffling to be sure various orders work measure_keys = [ 'node_density', 'node_farness', 'node_cycles', 'node_harmonic', 'node_beta', 'node_betweenness', 'node_betweenness_beta' ] np.random.shuffle(measure_keys) # in place measure_keys = tuple(measure_keys) # generate the measures measures_data = centrality.local_node_centrality(node_data, edge_data, node_edge_map, distances, betas, measure_keys) node_density = measures_data[measure_keys.index('node_density')] node_farness = measures_data[measure_keys.index('node_farness')] node_cycles = measures_data[measure_keys.index('node_cycles')] node_harmonic = measures_data[measure_keys.index('node_harmonic')] node_beta = measures_data[measure_keys.index('node_beta')] node_betweenness = measures_data[measure_keys.index('node_betweenness')] node_betweenness_beta = measures_data[measure_keys.index( 'node_betweenness_beta')] # improved closeness is derived after the fact improved_closness = node_density / node_farness / node_density # test node density # node density count doesn't include self-node # connected component == 49 == len(G) - 1 # isolated looping component == 3 # isolated edge == 1 # isolated node == 0 for n in node_density[4]: # infinite distance - exceeds cutoff clashes assert n in [49, 3, 1, 0] # test harmonic closeness vs NetworkX nx_harm_cl = nx.harmonic_centrality(G_round_trip, distance='length') nx_harm_cl = np.array([v for v in nx_harm_cl.values()]) assert np.allclose(nx_harm_cl, node_harmonic[4], atol=0.001, rtol=0) # test betweenness vs NetworkX # set endpoint counting to false and do not normalise # nx node centrality NOT implemented for MultiGraph G_non_multi = nx.Graph() # don't change to MultiGraph!!! G_non_multi.add_nodes_from(G_round_trip.nodes()) for s, e, k, d in G_round_trip.edges(keys=True, data=True): assert k == 0 G_non_multi.add_edge(s, e, **d) nx_betw = nx.betweenness_centrality(G_non_multi, weight='length', endpoints=False, normalized=False) nx_betw = np.array([v for v in nx_betw.values()]) # nx betweenness gives 0.5 instead of 1 for all disconnected looping component nodes # nx presumably takes equidistant routes into account, in which case only the fraction is aggregated assert np.allclose(nx_betw[:52], node_betweenness[4][:52], atol=0.001, rtol=0) # do the comparisons array-wise so that betweenness can be aggregated d_n = len(distances) betw = np.full((d_n, primal_graph.number_of_nodes()), 0.0) betw_wt = np.full((d_n, primal_graph.number_of_nodes()), 0.0) dens = np.full((d_n, primal_graph.number_of_nodes()), 0.0) far_short_dist = np.full((d_n, primal_graph.number_of_nodes()), 0.0) far_simpl_dist = np.full((d_n, primal_graph.number_of_nodes()), 0.0) harmonic_cl = np.full((d_n, primal_graph.number_of_nodes()), 0.0) grav = np.full((d_n, primal_graph.number_of_nodes()), 0.0) cyc = np.full((d_n, primal_graph.number_of_nodes()), 0.0) for src_idx in range(len(primal_graph)): # get shortest path maps tree_map, tree_edges = centrality.shortest_path_tree(edge_data, node_edge_map, src_idx, max(distances), angular=False) tree_nodes = np.where(tree_map[:, 0])[0] tree_preds = tree_map[:, 1] tree_short_dist = tree_map[:, 2] tree_simpl_dist = tree_map[:, 3] tree_cycles = tree_map[:, 4] for to_idx in tree_nodes: # skip self nodes if to_idx == src_idx: continue # get shortest / simplest distances to_short_dist = tree_short_dist[to_idx] to_simpl_dist = tree_simpl_dist[to_idx] cycles = tree_cycles[to_idx] # continue if exceeds max if np.isinf(to_short_dist): continue for d_idx in range(len(distances)): dist_cutoff = distances[d_idx] beta = betas[d_idx] if to_short_dist <= dist_cutoff: # don't exceed threshold # if to_dist <= dist_cutoff: # aggregate values dens[d_idx][src_idx] += 1 far_short_dist[d_idx][src_idx] += to_short_dist far_simpl_dist[d_idx][src_idx] += to_simpl_dist harmonic_cl[d_idx][src_idx] += 1 / to_short_dist grav[d_idx][src_idx] += np.exp(-beta * to_short_dist) # cycles cyc[d_idx][src_idx] += cycles # only process betweenness in one direction if to_idx < src_idx: continue # betweenness - only counting truly between vertices, not starting and ending verts inter_idx = tree_preds[to_idx] # isolated nodes will have no predecessors if np.isnan(inter_idx): continue inter_idx = np.int(inter_idx) while True: # break out of while loop if the intermediary has reached the source node if inter_idx == src_idx: break betw[d_idx][inter_idx] += 1 betw_wt[d_idx][inter_idx] += np.exp(-beta * to_short_dist) # follow inter_idx = np.int(tree_preds[inter_idx]) improved_cl = dens / far_short_dist / dens assert np.allclose(node_density, dens, atol=0.001, rtol=0) assert np.allclose(node_farness, far_short_dist, atol=0.01, rtol=0) # relax precision assert np.allclose(node_cycles, cyc, atol=0.001, rtol=0) assert np.allclose(node_harmonic, harmonic_cl, atol=0.001, rtol=0) assert np.allclose(node_beta, grav, atol=0.001, rtol=0) assert np.allclose(improved_closness, improved_cl, equal_nan=True, atol=0.001, rtol=0) assert np.allclose(node_betweenness, betw, atol=0.001, rtol=0) assert np.allclose(node_betweenness_beta, betw_wt, atol=0.001, rtol=0) # catch typos with pytest.raises(ValueError): centrality.local_node_centrality(node_data, edge_data, node_edge_map, distances, betas, ('typo_key', ))
def aggregate_to_src_idx(netw_src_idx: int, node_data: np.ndarray, edge_data: np.ndarray, node_edge_map: Dict, data_map: np.ndarray, max_dist: float, jitter_scale: float = 0.0, angular: bool = False): # this function is typically called iteratively, so do type checks from parent methods netw_x_arr = node_data[:, 0] netw_y_arr = node_data[:, 1] netw_src_x = netw_x_arr[netw_src_idx] netw_src_y = netw_y_arr[netw_src_idx] d_x_arr = data_map[:, 0] d_y_arr = data_map[:, 1] d_assign_nearest = data_map[:, 2] d_assign_next_nearest = data_map[:, 3] # run the shortest tree dijkstra # keep in mind that predecessor map is based on impedance heuristic - which can be different from metres # NOTE -> use np.inf for max distance so as to explore all paths # In some cases the predecessor nodes will be within reach even if the closest node is not # Total distance is checked later tree_map, tree_edges = centrality.shortest_path_tree( edge_data, node_edge_map, netw_src_idx, max_dist=max_dist, jitter_scale=jitter_scale, angular=angular) ''' Shortest tree dijkstra Predecessor map is based on impedance heuristic - i.e. angular vs not Shortest path distances in metres used for defining max distances regardless RETURNS A SHORTEST PATH TREE MAP: 0 - processed nodes 1 - predecessors 2 - shortest path distance 3 - simplest path angular distance 4 - cycles 5 - origin segments 6 - last segments ''' tree_preds = tree_map[:, 1] tree_short_dists = tree_map[:, 2] # arrays for writing the reachable data points and their distances reachable_data = np.full(len(data_map), False) reachable_data_dist = np.full(len(data_map), np.inf) # iterate the data points for data_idx in range(len(data_map)): # find the primary assigned network index for the data point if np.isfinite(d_assign_nearest[data_idx]): netw_idx = int(d_assign_nearest[data_idx]) # if the assigned network node is within the threshold if tree_short_dists[netw_idx] < max_dist: # get the distance from the data point to the network node d_d = np.hypot(d_x_arr[data_idx] - netw_x_arr[netw_idx], d_y_arr[data_idx] - netw_y_arr[netw_idx]) # add to the distance assigned for the network node dist = tree_short_dists[netw_idx] + d_d # only assign distance if within max distance if dist <= max_dist: reachable_data[data_idx] = True reachable_data_dist[data_idx] = dist # the next-nearest may offer a closer route depending on the direction the shortest path approaches from if np.isfinite(d_assign_next_nearest[data_idx]): netw_idx = int(d_assign_next_nearest[data_idx]) # if the assigned network node is within the threshold if tree_short_dists[netw_idx] < max_dist: # get the distance from the data point to the network node d_d = np.hypot(d_x_arr[data_idx] - netw_x_arr[netw_idx], d_y_arr[data_idx] - netw_y_arr[netw_idx]) # add to the distance assigned for the network node dist = tree_short_dists[netw_idx] + d_d # only assign distance if within max distance # AND only if closer than other direction if dist <= max_dist and dist < reachable_data_dist[data_idx]: reachable_data[data_idx] = True reachable_data_dist[data_idx] = dist # note that some entries will be nan values if the max distance was exceeded return reachable_data, reachable_data_dist, tree_preds
def test_local_centrality(): ''' Also tested indirectly via test_networks.test_compute_centrality Test centrality methods where possible against NetworkX - i.e. harmonic closeness and betweenness Note that NetworkX improved closeness is not the same as derivation used in this package NetworkX doesn't have a maximum distance cutoff, so run on the whole graph (low beta / high distance) ''' # load the test graph G = mock.mock_graph() G = graphs.nX_simple_geoms(G) node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX(G) # generate node and edge maps G_round_trip = graphs.nX_from_graph_maps(node_uids, node_data, edge_data, node_edge_map) # plots for debugging # needs a large enough beta so that distance thresholds aren't encountered betas = np.array([-0.02, -0.01, -0.005, -0.0008, -0.0]) distances = networks.distance_from_beta(betas) # set the keys - add shuffling to be sure various orders work measure_keys = [ 'node_density', 'node_farness', 'node_cycles', 'node_harmonic', 'node_beta', 'segment_density', 'segment_harmonic', 'segment_beta', 'node_betweenness', 'node_betweenness_beta', 'segment_betweenness' ] np.random.shuffle(measure_keys) # in place measure_keys = tuple(measure_keys) # generate the measures measures_data = centrality.local_centrality(node_data, edge_data, node_edge_map, distances, betas, measure_keys, angular=False) node_density = measures_data[measure_keys.index('node_density')] node_farness = measures_data[measure_keys.index('node_farness')] node_cycles = measures_data[measure_keys.index('node_cycles')] node_harmonic = measures_data[measure_keys.index('node_harmonic')] node_beta = measures_data[measure_keys.index('node_beta')] segment_density = measures_data[measure_keys.index('segment_density')] segment_harmonic = measures_data[measure_keys.index('segment_harmonic')] segment_beta = measures_data[measure_keys.index('segment_beta')] node_betweenness = measures_data[measure_keys.index('node_betweenness')] node_betweenness_beta = measures_data[measure_keys.index('node_betweenness_beta')] segment_betweenness = measures_data[measure_keys.index('segment_betweenness')] # post compute improved improved_closness = node_density / node_farness / node_density # angular keys measure_keys_angular = [ 'node_harmonic_angular', 'segment_harmonic_hybrid', 'node_betweenness_angular', 'segment_betweeness_hybrid' ] np.random.shuffle(measure_keys_angular) # in place measure_keys_angular = tuple(measure_keys_angular) # generate the angular measures measures_data_angular = centrality.local_centrality(node_data, edge_data, node_edge_map, distances, betas, measure_keys_angular, angular=True) node_harmonic_angular = measures_data_angular[measure_keys_angular.index('node_harmonic_angular')] segment_harmonic_hybrid = measures_data_angular[measure_keys_angular.index('segment_harmonic_hybrid')] node_betweenness_angular = measures_data_angular[measure_keys_angular.index('node_betweenness_angular')] segment_betweeness_hybrid = measures_data_angular[measure_keys_angular.index('segment_betweeness_hybrid')] # test node density # node density count doesn't include self-node # connected component == 48 == len(G) - 4 # isolated looping component == 3 # isolated edge == 1 # isolated node == 0 for n in node_density[4]: # infinite distance - exceeds cutoff clashes assert n in [48, 3, 1, 0] # test harmonic closeness vs NetworkX nx_harm_cl = nx.harmonic_centrality(G_round_trip, distance='length') nx_harm_cl = np.array([v for v in nx_harm_cl.values()]) assert np.allclose(nx_harm_cl, node_harmonic[4], atol=0.001, rtol=0) # test betweenness vs NetworkX # set endpoint counting to false and do not normalise nx_betw = nx.betweenness_centrality(G_round_trip, weight='length', endpoints=False, normalized=False) nx_betw = np.array([v for v in nx_betw.values()]) # for some reason nx betweenness gives 0.5 instead of 1 for disconnected looping component (should be 1) # maybe two equidistant routes being divided through 2 # nx betweenness gives 0.5 instead of 1 for all disconnected looping component nodes # nx presumably takes equidistant routes into account, in which case only the fraction is aggregated assert np.allclose(nx_betw[:52], node_betweenness[4][:52], atol=0.001, rtol=0) # test against various distances for d_idx in range(len(distances)): dist_cutoff = distances[d_idx] beta = betas[d_idx] # do the comparisons array-wise so that betweenness can be aggregated betw = np.full(G.number_of_nodes(), 0.0) betw_wt = np.full(G.number_of_nodes(), 0.0) dens = np.full(G.number_of_nodes(), 0.0) far_imp = np.full(G.number_of_nodes(), 0.0) far_dist = np.full(G.number_of_nodes(), 0.0) harmonic_cl = np.full(G.number_of_nodes(), 0.0) grav = np.full(G.number_of_nodes(), 0.0) cyc = np.full(G.number_of_nodes(), 0.0) for src_idx in range(len(G)): # get shortest path maps tree_map, tree_edges = centrality.shortest_path_tree(edge_data, node_edge_map, src_idx, dist_cutoff, angular=False) tree_preds = tree_map[:, 1] tree_dists = tree_map[:, 2] tree_imps = tree_map[:, 3] tree_cycles = tree_map[:, 4] for n_idx in G.nodes(): # skip self nodes if n_idx == src_idx: continue # get distance and impedance dist = tree_dists[n_idx] imp = tree_imps[n_idx] # continue if exceeds max if np.isinf(dist) or dist > dist_cutoff: continue # aggregate values dens[src_idx] += 1 far_imp[src_idx] += imp far_dist[src_idx] += dist harmonic_cl[src_idx] += 1 / imp grav[src_idx] += np.exp(beta * dist) # cycles if tree_cycles[n_idx]: cyc[src_idx] += 1 # BETWEENNESS # only process betweenness in one direction if n_idx < src_idx: continue # betweenness - only counting truly between vertices, not starting and ending verts inter_idx = tree_preds[n_idx] # isolated nodes will have no predecessors if np.isnan(inter_idx): continue inter_idx = np.int(inter_idx) while True: # break out of while loop if the intermediary has reached the source node if inter_idx == src_idx: break betw[inter_idx] += 1 betw_wt[inter_idx] += np.exp(beta * dist) # follow inter_idx = np.int(tree_preds[inter_idx]) improved_cl = dens / far_dist / dens assert np.allclose(node_density[d_idx], dens, atol=0.001, rtol=0) assert np.allclose(node_farness[d_idx], far_dist, atol=0.01, rtol=0) # relax precision assert np.allclose(node_cycles[d_idx], cyc, atol=0.001, rtol=0) assert np.allclose(node_harmonic[d_idx], harmonic_cl, atol=0.001, rtol=0) assert np.allclose(node_beta[d_idx], grav, atol=0.001, rtol=0) assert np.allclose(improved_closness[d_idx], improved_cl, equal_nan=True, atol=0.001, rtol=0) assert np.allclose(node_betweenness[d_idx], betw, atol=0.001, rtol=0) assert np.allclose(node_betweenness_beta[d_idx], betw_wt, atol=0.001, rtol=0) # TODO: are there possibly ways to test segment_density, harmonic_segment, segment_beta, segment_betweenness # for infinite distance, the segment density should match the sum of reachable segments length_sum = 0 for s, e, d in G_round_trip.edges(data=True): length_sum += d['length'] reachable_length_sum = length_sum - \ (G_round_trip[50][51]['length'] + G_round_trip[52][53]['length'] + G_round_trip[53][54]['length'] + G_round_trip[54][55]['length'] + G_round_trip[52][55]['length']) assert np.allclose(segment_density[-1][:49], reachable_length_sum, atol=0.01, rtol=0) # relax precision # check that problematic keys are caught for angular, k in zip([False, True], ['node_harmonic', 'node_harmonic_angular']): # catch typos with pytest.raises(ValueError): centrality.local_centrality(node_data, edge_data, node_edge_map, distances, betas, ('typo_key',), angular=False) # catch duplicates with pytest.raises(ValueError): centrality.local_centrality(node_data, edge_data, node_edge_map, distances, betas, (k, k), angular=False) # catch mixed angular and non-angular keys with pytest.raises(ValueError): centrality.local_centrality(node_data, edge_data, node_edge_map, distances, betas, ('node_density', 'node_harmonic_angular'), angular=False)