def test_decomposed_local_centrality(): # centralities on the original nodes within the decomposed network should equal non-decomposed workflow betas = np.array([-0.02, -0.01, -0.005, -0.0008, -0.0]) distances = networks.distance_from_beta(betas) 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') # test a decomposed 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 measures_data = centrality.local_centrality(node_data, edge_data, node_edge_map, distances, betas, measure_keys, angular=False) G_decomposed = graphs.nX_decompose(G, 20) # generate node and edge maps node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX(G_decomposed) checks.check_network_maps(node_data, edge_data, node_edge_map) measures_data_decomposed = centrality.local_centrality(node_data, edge_data, node_edge_map, distances, betas, measure_keys, angular=False) # test harmonic closeness on original nodes for non-decomposed vs decomposed d_range = len(distances) m_range = len(measure_keys) assert measures_data.shape == (m_range, d_range, len(G)) assert measures_data_decomposed.shape == (m_range, d_range, len(G_decomposed)) original_node_idx = np.where(node_data[:, 3] == 0) # with increasing decomposition: # - node based measures will not match # - node based segment measures will match - these measure to the cut endpoints per thresholds # - betweenness based segment won't match - doesn't measure to cut endpoints for m_idx in range(m_range): print(m_idx) for d_idx in range(d_range): match = np.allclose(measures_data[m_idx][d_idx], measures_data_decomposed[m_idx][d_idx][original_node_idx], atol=0.1, rtol=0) # relax precision if not match: print('key', measure_keys[m_idx], 'dist:', distances[d_idx], 'match:', match) if m_idx in [5, 6, 7]: assert match
def __init__(self, networkX_graph: nx.Graph, distances: Union[list, tuple, np.ndarray] = None, betas: Union[list, tuple, np.ndarray] = None, min_threshold_wt: float = checks.def_min_thresh_wt): node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX( networkX_graph) super().__init__(node_uids, node_data, edge_data, node_edge_map, distances, betas, min_threshold_wt) # keep reference to networkX graph self.networkX = networkX_graph
def test_local_centrality_time(): ''' originally based on node_harmonic and node_betweenness: OLD VERSION with trim maps: Timing: 10.490865555 for 10000 iterations NEW VERSION with numba typed list - faster and removes arcane full vs. trim maps workflow 8.242256040000001 for 10000 iterations VERSION with node_edge_map Dict - tad slower but worthwhile for cleaner and more intuitive code 8.882408618 for 10000 iterations float64 - 17.881911942000002 float32 - 13.612861239 segments of unreachable code add to timing regardless... possibly because of high number of iters vs. function prep and teardown...? 14.4 -> 14.293403884 for simpler ghost node workflow ''' # 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 # needs a large enough beta so that distance thresholds aren't encountered distances = np.array([np.inf]) betas = networks.beta_from_distance(distances) # setup timing wrapper def wrapper_func(): ''' node density invokes aggregative workflow betweenness node invokes betweenness workflow segment density invokes segments workflow ''' return centrality.local_centrality(node_data, edge_data, node_edge_map, distances, betas, ('node_density', # 7.16s 'node_betweenness', # 8.08s - adds around 1s 'segment_density', # 11.2s - adds around 3s 'segment_betweenness' ), angular=False, suppress_progress=True) # prime the function wrapper_func() iters = 10000 # time and report func_time = timeit.timeit(wrapper_func, number=iters) print(f'Timing: {func_time} for {iters} iterations') if 'GITHUB_ACTIONS' not in os.environ: assert func_time < 20
def test_Network_Layer(): G = mock.mock_graph() G = graphs.nX_simple_geoms(G) # manual graph maps for comparison node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX( G) x_arr = node_data[:, 0] y_arr = node_data[:, 1] betas = [-0.02, -0.005] distances = networks.distance_from_beta(betas) # test Network_Layer's class for d, b in zip([distances, None], [None, betas]): for angular in [True, False]: N = networks.Network_Layer(node_uids, node_data, edge_data, node_edge_map, distances=d, betas=b) assert np.allclose(N.uids, node_uids, atol=0.001, rtol=0) assert np.allclose(N._node_data, node_data, atol=0.001, rtol=0) assert np.allclose(N._edge_data, edge_data, atol=0.001, rtol=0) assert np.allclose( N.distances, distances, atol=0.001, rtol=0) # inferred automatically when only betas provided assert np.allclose( N.betas, betas, atol=0.001, rtol=0) # inferred automatically when only distances provided assert N.min_threshold_wt == checks.def_min_thresh_wt assert np.allclose(N.x_arr, x_arr, atol=0.001, rtol=0) assert np.allclose(N.y_arr, y_arr, atol=0.001, rtol=0) assert np.allclose(N.live, node_data[:, 2], atol=0.001, rtol=0) assert np.allclose(N.ghosted, node_data[:, 3], atol=0.001, rtol=0) assert np.allclose(N.edge_lengths, edge_data[:, 2], atol=0.001, rtol=0) assert np.allclose(N.edge_angles, edge_data[:, 3], atol=0.001, rtol=0) assert np.allclose(N.edge_impedance_factor, edge_data[:, 4], atol=0.001, rtol=0) assert np.allclose(N.edge_in_bearing, edge_data[:, 5], atol=0.001, rtol=0) assert np.allclose(N.edge_out_bearing, edge_data[:, 6], atol=0.001, rtol=0) # test round-trip graph to and from Network_Layer N = networks.Network_Layer(node_uids, node_data, edge_data, node_edge_map, distances=distances) G_round_trip = N.to_networkX() # graph_maps_from_networkX generates implicit live (all True) and weight (all 1) attributes if missing # i.e. can't simply check that all nodes equal, so check properties manually for n, d in G.nodes(data=True): assert n in G_round_trip assert G_round_trip.nodes[n]['x'] == d['x'] assert G_round_trip.nodes[n]['y'] == d['y'] # edges can be checked en masse assert G_round_trip.edges == G.edges # check alternate min_threshold_wt gets passed through successfully alt_min = 0.02 alt_distances = networks.distance_from_beta(betas, min_threshold_wt=alt_min) N = networks.Network_Layer(node_uids, node_data, edge_data, node_edge_map, betas=betas, min_threshold_wt=alt_min) assert np.allclose(N.distances, alt_distances, atol=0.001, rtol=0) # check for malformed signatures with pytest.raises(ValueError): networks.Network_Layer(node_uids[:-1], node_data, edge_data, node_edge_map, distances) with pytest.raises(ValueError): networks.Network_Layer(node_uids, node_data[:, :-1], edge_data, node_edge_map, distances) with pytest.raises(ValueError): networks.Network_Layer(node_uids, node_data, edge_data[:, :-1], node_edge_map, distances) with pytest.raises(ValueError): networks.Network_Layer(node_uids, node_data, edge_data[:, :-1], node_edge_map, distances) with pytest.raises(ValueError): networks.Network_Layer(node_uids, node_data, edge_data, node_edge_map) # no betas or distances with pytest.raises(ValueError): networks.Network_Layer(node_uids, node_data, edge_data, node_edge_map, distances=None, betas=None) with pytest.raises(ValueError): networks.Network_Layer(node_uids, node_data, edge_data, node_edge_map, distances=[]) with pytest.raises(ValueError): networks.Network_Layer(node_uids, node_data, edge_data, node_edge_map, betas=[])
def test_Network_Layer_From_nX(): 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) x_arr = node_data[:, 0] y_arr = node_data[:, 1] betas = np.array([-0.04, -0.02]) distances = networks.distance_from_beta(betas) # test Network_Layer_From_NetworkX's class for d, b in zip([distances, None], [None, betas]): for angular in [True, False]: N = networks.Network_Layer_From_nX(G, distances=d, betas=b) assert np.allclose(N.uids, node_uids, atol=0.001, rtol=0) assert np.allclose(N._node_data, node_data, atol=0.001, rtol=0) assert np.allclose(N._edge_data, edge_data, atol=0.001, rtol=0) assert np.allclose( N.distances, distances, atol=0.001, rtol=0) # inferred automatically when only betas provided assert np.allclose( N.betas, betas, atol=0.001, rtol=0) # inferred automatically when only distances provided assert N.min_threshold_wt == checks.def_min_thresh_wt assert np.allclose(N.x_arr, x_arr, atol=0.001, rtol=0) assert np.allclose(N.y_arr, y_arr, atol=0.001, rtol=0) assert np.allclose(N.live, node_data[:, 2], atol=0.001, rtol=0) assert np.allclose(N.edge_lengths, edge_data[:, 2], atol=0.001, rtol=0) assert np.allclose(N.edge_angles, edge_data[:, 3], atol=0.001, rtol=0) assert np.allclose(N.edge_impedance_factor, edge_data[:, 4], atol=0.001, rtol=0) assert np.allclose(N.edge_in_bearing, edge_data[:, 5], atol=0.001, rtol=0) assert np.allclose(N.edge_out_bearing, edge_data[:, 6], atol=0.001, rtol=0) # check alternate min_threshold_wt gets passed through successfully alt_min = 0.02 alt_distances = networks.distance_from_beta(betas, min_threshold_wt=alt_min) N = networks.Network_Layer_From_nX(G, betas=betas, min_threshold_wt=alt_min) assert np.allclose(N.distances, alt_distances, atol=0.001, rtol=0) # check for malformed signatures with pytest.raises(TypeError): networks.Network_Layer_From_nX('boo', distances=distances) with pytest.raises(ValueError): networks.Network_Layer_From_nX(G) # no betas or distances with pytest.raises(ValueError): networks.Network_Layer_From_nX(G, distances=None, betas=None) with pytest.raises(ValueError): networks.Network_Layer_From_nX(G, distances=[]) with pytest.raises(ValueError): networks.Network_Layer_From_nX(G, betas=[])
def test_nX_from_graph_maps(): # also see test_networks.test_to_networkX for tests on implementation via Network layer # check round trip to and from graph maps results in same graph G = mock.mock_graph() G = graphs.nX_simple_geoms(G) # explicitly set live params for equality checks # graph_maps_from_networkX generates these implicitly if missing for n in G.nodes(): G.nodes[n]['live'] = bool(np.random.randint(0, 1)) # test directly from and to graph maps node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX(G) G_round_trip = graphs.nX_from_graph_maps(node_uids, node_data, edge_data, node_edge_map) assert list(G_round_trip.nodes) == list(G.nodes) assert list(G_round_trip.edges) == list(G.edges) # check with metrics dictionary N = networks.Network_Layer_From_nX(G, distances=[500, 1000]) N.compute_centrality(measures=['node_harmonic']) data_dict = mock.mock_data_dict(G) landuse_labels = mock.mock_categorical_data(len(data_dict)) D = layers.Data_Layer_From_Dict(data_dict) D.assign_to_network(N, max_dist=400) D.compute_aggregated(landuse_labels, mixed_use_keys=['hill', 'shannon'], accessibility_keys=['a', 'c'], qs=[0, 1]) metrics_dict = N.metrics_to_dict() # without backbone G_round_trip_data = graphs.nX_from_graph_maps(node_uids, node_data, edge_data, node_edge_map, metrics_dict=metrics_dict) for uid, metrics in metrics_dict.items(): assert G_round_trip_data.nodes[uid]['metrics'] == metrics # with backbone G_round_trip_data = graphs.nX_from_graph_maps(node_uids, node_data, edge_data, node_edge_map, networkX_graph=G, metrics_dict=metrics_dict) for uid, metrics in metrics_dict.items(): assert G_round_trip_data.nodes[uid]['metrics'] == metrics # test with decomposed G_decomposed = graphs.nX_decompose(G, decompose_max=20) # set live explicitly for n in G_decomposed.nodes(): G_decomposed.nodes[n]['live'] = bool(np.random.randint(0, 1)) node_uids_d, node_data_d, edge_data_d, node_edge_map_d = graphs.graph_maps_from_nX(G_decomposed) G_round_trip_d = graphs.nX_from_graph_maps(node_uids_d, node_data_d, edge_data_d, node_edge_map_d) assert list(G_round_trip_d.nodes) == list(G_decomposed.nodes) for n, node_data in G_round_trip.nodes(data=True): assert n in G_decomposed assert node_data['live'] == G_decomposed.nodes[n]['live'] assert node_data['x'] == G_decomposed.nodes[n]['x'] assert node_data['y'] == G_decomposed.nodes[n]['y'] assert G_round_trip_d.edges == G_decomposed.edges # error checks for when using backbone graph: # mismatching numbers of nodes corrupt_G = G.copy() corrupt_G.remove_node(0) with pytest.raises(ValueError): graphs.nX_from_graph_maps(node_uids, node_data, edge_data, node_edge_map, networkX_graph=corrupt_G) # mismatching node uid with pytest.raises(ValueError): corrupt_node_uids = list(node_uids) corrupt_node_uids[0] = 'boo' graphs.nX_from_graph_maps(corrupt_node_uids, node_data, edge_data, node_edge_map, networkX_graph=G)
def test_graph_maps_from_nX(): # template graph G_template = mock.mock_graph() G_template = graphs.nX_simple_geoms(G_template) # test maps vs. networkX G_test = G_template.copy() # set some random 'live' statuses for n in G_test.nodes(): G_test.nodes[n]['live'] = bool(np.random.randint(0, 1)) # randomise the imp_factors for s, e in G_test.edges(): G_test[s][e]['imp_factor'] = np.random.random() * 2 # generate geom with angular change for edge 50-51 - should sum to 360 angle_geom = geometry.LineString([ [700700, 5719900], [700700, 5720000], [700750, 5720050], [700700, 5720050], [700700, 5720100] ]) G_test[50][51]['geom'] = angle_geom # generate test maps node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX(G_test) # debug plot # plot.plot_graphs(primal=G_test) # plot.plot_graph_maps(node_uids, node_data, edge_data) # run check checks.check_network_maps(node_data, edge_data, node_edge_map) # check lengths assert len(node_uids) == len(node_data) == G_test.number_of_nodes() # no ghosted edges, so edges = x2 assert len(edge_data) == G_test.number_of_edges() * 2 # check node maps (idx and label match in this case...) for n_label in node_uids: assert node_data[n_label][0] == G_test.nodes[n_label]['x'] assert node_data[n_label][1] == G_test.nodes[n_label]['y'] assert node_data[n_label][2] == G_test.nodes[n_label]['live'] assert node_data[n_label][3] == 0 # ghosted is False by default # check edge maps (idx and label match in this case...) for start, end, length, angle_sum, imp_factor, start_bearing, end_bearing in edge_data: assert np.allclose(length, G_test[start][end]['geom'].length, atol=0.001, rtol=0) if (start == 50 and end == 51) or (start == 51 and end == 50): # check that the angle is measured along the line of change # i.e. 45 + 135 + 90 (not 45 + 45 + 90) # angles are transformed per: 1 + (angle_sum / 180) assert angle_sum == 270 else: assert angle_sum == 0 assert np.allclose(imp_factor, G_test[start][end]['imp_factor'], atol=0.001, rtol=0) s_x, s_y = node_data[int(start)][:2] e_x, e_y = node_data[int(end)][:2] assert np.allclose(start_bearing, np.rad2deg(np.arctan2(e_y - s_y, e_x - s_x)), atol=0.001, rtol=0) assert np.allclose(end_bearing, np.rad2deg(np.arctan2(e_y - s_y, e_x - s_x)), atol=0.001, rtol=0) # check that missing geoms throw an error G_test = G_template.copy() for s, e in G_test.edges(): # delete key from first node and break del G_test[s][e]['geom'] break with pytest.raises(KeyError): graphs.graph_maps_from_nX(G_test) # check that non-LineString geoms throw an error G_test = G_template.copy() for s, e in G_test.edges(): G_test[s][e]['geom'] = geometry.Point([G_test.nodes[s]['x'], G_test.nodes[s]['y']]) with pytest.raises(TypeError): graphs.graph_maps_from_nX(G_test) # check that missing node keys throw an error G_test = G_template.copy() for k in ['x', 'y']: for n in G_test.nodes(): # delete key from first node and break del G_test.nodes[n][k] break with pytest.raises(KeyError): graphs.graph_maps_from_nX(G_test) # check that invalid imp_factors are caught G_test = G_template.copy() # corrupt imp_factor value and break for corrupt_val in [-1, -np.inf, np.nan]: for s, e in G_test.edges(): G_test[s][e]['imp_factor'] = corrupt_val break with pytest.raises(ValueError): graphs.graph_maps_from_nX(G_test)
def test_shortest_path_tree(): # prepare primal graph G_primal = mock.mock_graph() G_primal = graphs.nX_simple_geoms(G_primal) node_uids_p, node_data_p, edge_data_p, node_edge_map_p = graphs.graph_maps_from_nX(G_primal) # 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 G_dual = mock.mock_graph() G_dual = graphs.nX_simple_geoms(G_dual) G_dual = graphs.nX_to_dual(G_dual) node_uids_d, node_data_d, edge_data_d, node_edge_map_d = graphs.graph_maps_from_nX(G_dual) 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(G_primal)): # 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_dists_p = tree_map[:, 2] tree_imps_p = tree_map[:, 3] # 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(G_primal)): if j in nx_path: assert _find_path(j, src_idx, tree_preds_p) == nx_path[j] assert np.allclose(tree_imps_p[j], tree_dists_p[j], atol=0.001, rtol=0) assert np.allclose(tree_imps_p[j], nx_dist[j], atol=0.001, rtol=0) # compare angular impedances and paths for a selection of targets on primal vs. dual # this works for this graph because edge segments are straight 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_imps_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_imps_d = tree_map_d[:, 3] assert np.allclose(tree_imps_p[p_target_idx], tree_imps_d[d_target_idx], atol=0.001, rtol=0) # angular impedance should take a simpler but longer path - test basic case on dual # for debugging # from cityseer.util import plot # plot.plot_nX_primal_or_dual(primal=G_primal, dual=G_dual, labels=True) # 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) # 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) # 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] 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_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)