예제 #1
0
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
예제 #2
0
 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
예제 #3
0
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
예제 #4
0
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=[])
예제 #5
0
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=[])
예제 #6
0
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)
예제 #7
0
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)
예제 #8
0
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']
예제 #9
0
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)