Ejemplo n.º 1
0
def test_check_data_map():
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    N = networks.Network_Layer_From_nX(G, distances=[500])
    data_dict = mock.mock_data_dict(G)
    data_uids, data_map = layers.data_map_from_dict(data_dict)

    # should throw error if not assigned
    with pytest.raises(ValueError):
        checks.check_data_map(data_map)

    # should work if flag set to False
    checks.check_data_map(data_map, check_assigned=False)

    # assign then check that it runs as intended
    data_map = data.assign_to_network(data_map,
                                      N._node_data,
                                      N._edge_data,
                                      N._node_edge_map,
                                      max_dist=400)
    checks.check_data_map(data_map)

    # catch zero length data arrays
    empty_2d_arr = np.full((0, 4), np.nan)
    with pytest.raises(ValueError):
        checks.check_data_map(empty_2d_arr)

    # catch invalid dimensionality
    with pytest.raises(ValueError):
        checks.check_data_map(data_map[:, :-1])
Ejemplo n.º 2
0
def test_metrics_to_dict():
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    # create a network layer and run some metrics
    N = networks.Network_Layer_From_nX(G, distances=[500, 1000])

    # check with no metrics
    metrics_dict = N.metrics_to_dict()
    dict_check(metrics_dict, N)

    # check with centrality metrics
    N.compute_centrality(measures=['node_harmonic'])
    metrics_dict = N.metrics_to_dict()
    dict_check(metrics_dict, N)

    # check with data metrics
    data_dict = mock.mock_data_dict(G)
    landuse_labels = mock.mock_categorical_data(len(data_dict))
    numerical_data = mock.mock_numerical_data(len(data_dict))
    # TODO:
    '''
    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],
                         stats_keys=['boo'],
                         stats_data_arrs=numerical_data)
    '''
    metrics_dict = N.metrics_to_dict()
    dict_check(metrics_dict, N)
Ejemplo n.º 3
0
def network_generator():
    for betas in [[-0.008], [-0.008, -0.002, -0.0]]:
        distances = networks.distance_from_beta(betas)
        for angular in [False, True]:
            G = mock.mock_graph()
            G = graphs.nX_simple_geoms(G)
            yield G, distances, betas, angular
Ejemplo n.º 4
0
def test_nX_remove_dangling_nodes():
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    G_messy = make_messy_graph(G)

    # no despining or disconnected components removal
    G_post = graphs.nX_remove_dangling_nodes(G_messy, despine=0, remove_disconnected=False)
    assert G_post.nodes == G_messy.nodes
    assert G_post.edges == G_messy.edges

    # check that all single neighbour nodes have been removed if geom less than despine distance
    G_post = graphs.nX_remove_dangling_nodes(G_messy, despine=100, remove_disconnected=False)
    for n in G_messy.nodes():
        if nx.degree(G_messy, n) == 1:
            nb = list(nx.neighbors(G_messy, n))[0]
            if G_messy[n][nb]['geom'].length <= 100:
                assert (n, nb) not in G_post.edges
            else:
                assert (n, nb) in G_post.edges

    # check that disconnected components are removed
    # this behaviour changed in networkx 2.4
    G_post = graphs.nX_remove_dangling_nodes(G_messy, despine=0, remove_disconnected=True)
    pre_components = list(nx.algorithms.components.connected_components(G_messy))
    post_components = list(nx.algorithms.components.connected_components(G_post))
    assert len(pre_components) != 1
    assert len(post_components) == 1
    # check that components match
    biggest_component = sorted(pre_components, key=len, reverse=True)[0]
    # index to 0 because post_components is still in list form
    assert biggest_component == post_components[0]
    # check that actual graphs are equivalent
    G_biggest_component = nx.Graph(G_messy.subgraph(biggest_component))
    assert G_biggest_component.nodes == G_post.nodes
    assert G_biggest_component.edges == G_post.edges
Ejemplo n.º 5
0
def test_dict_wgs_to_utm():
    # check that node coordinates are correctly converted
    G_utm = mock.mock_graph()
    data_dict_utm = mock.mock_data_dict(G_utm)

    # create a test dictionary
    test_dict = copy.deepcopy(data_dict_utm)
    # cast to lat, lon
    for k, v in test_dict.items():
        easting = v['x']
        northing = v['y']
        # be cognisant of parameter and return order
        # returns in lat lng order
        lat, lng = utm.to_latlon(easting, northing, 30, 'U')
        test_dict[k]['x'] = lng
        test_dict[k]['y'] = lat

    # convert back
    dict_converted = layers.dict_wgs_to_utm(test_dict)

    # check that round-trip converted match with reasonable proximity given rounding errors
    for k in data_dict_utm.keys():
        # rounding can be tricky
        assert np.allclose(data_dict_utm[k]['x'],
                           dict_converted[k]['x'],
                           atol=0.1,
                           rtol=0)  # relax precision
        assert np.allclose(data_dict_utm[k]['y'],
                           dict_converted[k]['y'],
                           atol=0.1,
                           rtol=0)  # relax precision

    # check that missing node attributes throw an error
    for attr in ['x', 'y']:
        G_wgs = mock.mock_graph(wgs84_coords=True)
        data_dict_wgs = mock.mock_data_dict(G_wgs)
        for k in data_dict_wgs.keys():
            del data_dict_wgs[k][attr]
            break
        # check that missing attribute throws an error
        with pytest.raises(AttributeError):
            layers.dict_wgs_to_utm(data_dict_wgs)

    # check that non WGS coordinates throw error
    with pytest.raises(AttributeError):
        layers.dict_wgs_to_utm(data_dict_utm)
Ejemplo n.º 6
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
Ejemplo n.º 7
0
def test_check_network_maps():
    # network maps
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    N = networks.Network_Layer_From_nX(G, distances=[500])
    # from cityseer.util import plot
    # plot.plot_networkX_primal_or_dual(primal=G)
    # plot.plot_graph_maps(N.uids, N._node_data, N._edge_data)
    # catch zero length node and edge arrays
    empty_node_arr = np.full((0, 5), np.nan)
    with pytest.raises(ValueError):
        checks.check_network_maps(empty_node_arr, N._edge_data,
                                  N._node_edge_map)
    empty_edge_arr = np.full((0, 4), np.nan)
    with pytest.raises(ValueError):
        checks.check_network_maps(N._node_data, empty_edge_arr,
                                  N._node_edge_map)
    # check that malformed node and data maps throw errors
    with pytest.raises(ValueError):
        checks.check_network_maps(N._node_data[:, :-1], N._edge_data,
                                  N._node_edge_map)
    with pytest.raises(ValueError):
        checks.check_network_maps(N._node_data, N._edge_data[:, :-1],
                                  N._node_edge_map)
    # catch problematic edge map values
    for x in [np.nan, -1]:
        # missing start node
        corrupted_edges = N._edge_data.copy()
        corrupted_edges[0, 0] = x
        with pytest.raises(AssertionError):
            checks.check_network_maps(N._node_data, corrupted_edges,
                                      N._node_edge_map)
        # missing end node
        corrupted_edges = N._edge_data.copy()
        corrupted_edges[0, 1] = x
        with pytest.raises(KeyError):
            checks.check_network_maps(N._node_data, corrupted_edges,
                                      N._node_edge_map)
        # invalid length
        corrupted_edges = N._edge_data.copy()
        corrupted_edges[0, 2] = x
        with pytest.raises(ValueError):
            checks.check_network_maps(N._node_data, corrupted_edges,
                                      N._node_edge_map)
        # invalid angle_sum
        corrupted_edges = N._edge_data.copy()
        corrupted_edges[0, 3] = x
        with pytest.raises(ValueError):
            checks.check_network_maps(N._node_data, corrupted_edges,
                                      N._node_edge_map)
        # invalid imp_factor
        corrupted_edges = N._edge_data.copy()
        corrupted_edges[0, 4] = x
        with pytest.raises(ValueError):
            checks.check_network_maps(N._node_data, corrupted_edges,
                                      N._node_edge_map)
Ejemplo n.º 8
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
Ejemplo n.º 9
0
def test_Data_Layer():
    G = mock.mock_graph()
    data_dict = mock.mock_data_dict(G)
    data_uids, data_map = layers.data_map_from_dict(data_dict)
    x_arr = data_map[:, 0]
    y_arr = data_map[:, 1]

    # test against Data_Layer internal process
    D = layers.Data_Layer(data_uids, data_map)
    assert D.uids == data_uids
    assert np.allclose(D._data, data_map, equal_nan=True, atol=0.001, rtol=0)
    assert np.allclose(D.x_arr, x_arr, atol=0.001, rtol=0)
    assert np.allclose(D.y_arr, y_arr, atol=0.001, rtol=0)
Ejemplo n.º 10
0
def test_to_networkX():
    # also see test_graphs.test_networkX_from_graph_maps for underlying graph maps version

    # 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 and weight params for equality checks
    # graph_maps_from_networkX generates these implicitly if missing
    G = graphs.nX_decompose(G, decompose_max=20)
    for n in G.nodes():
        G.nodes[n]['live'] = bool(np.random.randint(0, 1))
    for s, e in G.edges():
        G[s][e]['imp_factor'] = np.random.randint(0, 2)

    # add random data to check persistence at other end
    baa_node = None
    for n in G.nodes():
        baa_node = n
        G.nodes[n]['boo'] = 'baa'
        break
    boo_edge = None
    for s, e in G.edges():
        boo_edge = (s, e)
        G[s][e]['baa'] = 'boo'

    # test with metrics
    N = networks.Network_Layer_From_nX(G, distances=[500])
    N.compute_centrality(measures=['node_harmonic'])
    metrics_dict = N.metrics_to_dict()
    G_round_trip = N.to_networkX()
    for n, d in G.nodes(data=True):
        assert G_round_trip.nodes[n]['x'] == d['x']
        assert G_round_trip.nodes[n]['y'] == d['y']
        assert G_round_trip.nodes[n]['live'] == d['live']
    for s, e, d in G.edges(data=True):
        assert G_round_trip[s][e]['geom'] == d['geom']
        assert G_round_trip[s][e]['imp_factor'] == d['imp_factor']
    # check that metrics came through
    for uid, metrics in metrics_dict.items():
        assert G_round_trip.nodes[uid]['metrics'] == metrics
    # check data persistence
    assert G_round_trip.nodes[baa_node]['boo'] == 'baa'
    assert G_round_trip[boo_edge[0]][boo_edge[1]]['baa'] == 'boo'
Ejemplo n.º 11
0
def test_nX_simple_geoms():
    G = mock.mock_graph()
    G_geoms = graphs.nX_simple_geoms(G)

    for s, e in G.edges():
        line_geom = geometry.LineString([
            [G.nodes[s]['x'], G.nodes[s]['y']],
            [G.nodes[e]['x'], G.nodes[e]['y']]
        ])
        assert line_geom == G_geoms[s][e]['geom']

    # check that missing node keys throw an error
    for k in ['x', 'y']:
        for n in G.nodes():
            # delete key from first node and break
            del G.nodes[n][k]
            break
        # check that missing key throws an error
        with pytest.raises(KeyError):
            graphs.nX_simple_geoms(G)
Ejemplo n.º 12
0
def test_data_map_from_dict():
    # generate mock data
    G = mock.mock_graph()
    data_dict = mock.mock_data_dict(G)
    data_uids, data_map = layers.data_map_from_dict(data_dict)

    assert len(data_uids) == len(data_map) == len(data_dict)

    for d_label, d in zip(data_uids, data_map):
        assert d[0] == data_dict[d_label]['x']
        assert d[1] == data_dict[d_label]['y']
        assert np.isnan(d[2])
        assert np.isnan(d[3])

    # check that missing attributes throw errors
    for attr in ['x', 'y']:
        for k in data_dict.keys():
            del data_dict[k][attr]
        with pytest.raises(AttributeError):
            layers.data_map_from_dict(data_dict)
Ejemplo n.º 13
0
def test_compute_centrality():
    '''
    Underlying method also tested via test_networks.test_network_centralities
    '''
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    betas = np.array([-0.01, -0.005])
    distances = networks.distance_from_beta(betas)
    # generate data structures
    N = networks.Network_Layer_From_nX(G, distances)
    node_data = N._node_data
    edge_data = N._edge_data
    node_edge_map = N._node_edge_map
    # check measures against underlying method
    N = networks.Network_Layer_From_nX(G, distances)
    N.compute_centrality(measures=['node_density'])
    # test against underlying method
    measures_data = centrality.local_centrality(
        node_data,
        edge_data,
        node_edge_map,
        distances,
        betas,
        measure_keys=('node_density', ))
    for d_idx, d_key in enumerate(distances):
        assert np.allclose(N.metrics['centrality']['node_density'][d_key],
                           measures_data[0][d_idx])
    # also check the number of returned types for a few assortments of metrics
    measures = [
        'node_density', 'node_farness', 'node_cycles', 'node_harmonic',
        'segment_density', 'node_betweenness', 'segment_betweenness'
    ]
    np.random.shuffle(measures)  # in place
    # not necessary to do all labels, first few should do
    for min_idx in range(3):
        measure_keys = np.array(measures[min_idx:])
        N = networks.Network_Layer_From_nX(G, distances)
        N.compute_centrality(measures=measures)
        # test against underlying method
        measures_data = centrality.local_centrality(
            node_data,
            edge_data,
            node_edge_map,
            distances,
            betas,
            measure_keys=tuple(measure_keys))
        for m_idx, measure_name in enumerate(measure_keys):
            for d_idx, d_key in enumerate(distances):
                assert np.allclose(
                    N.metrics['centrality'][measure_name][d_key],
                    measures_data[m_idx][d_idx],
                    atol=0.001,
                    rtol=0)
    # check that angular gets passed through
    N_ang = networks.Network_Layer_From_nX(G, distances=[2000])
    N_ang.compute_centrality(measures=['node_harmonic_angular'], angular=True)
    N = networks.Network_Layer_From_nX(G, distances=[2000])
    N.compute_centrality(measures=['node_harmonic'], angular=False)
    assert not np.allclose(
        N_ang.metrics['centrality']['node_harmonic_angular'][2000],
        N.metrics['centrality']['node_harmonic'][2000],
        atol=0.001,
        rtol=0)
    assert not np.allclose(
        N_ang.metrics['centrality']['node_harmonic_angular'][2000],
        N.metrics['centrality']['node_harmonic'][2000],
        atol=0.001,
        rtol=0)
    # check that typos, duplicates, and mixed angular / non-angular are caught
    with pytest.raises(ValueError):
        N.compute_centrality(measures=['spelling_typo'])
    with pytest.raises(ValueError):
        N.compute_centrality(measures=['node_density', 'node_density'])
    with pytest.raises(ValueError):
        N.compute_centrality(
            measures=['harmonic_angle', 'node_harmonic_angular'])
Ejemplo n.º 14
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=[])
Ejemplo n.º 15
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)
Ejemplo n.º 16
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)
Ejemplo n.º 17
0
def test_nX_to_dual():
    # check that missing geoms throw an error
    G = mock.mock_graph()
    with pytest.raises(KeyError):
        graphs.nX_to_dual(G)

    # check that non-LineString geoms throw an error
    G = mock.mock_graph()
    for s, e in G.edges():
        G[s][e]['geom'] = geometry.Point([G.nodes[s]['x'], G.nodes[s]['y']])
    with pytest.raises(TypeError):
        graphs.nX_to_dual(G)

    # check that missing node keys throw an error
    for k in ['x', 'y']:
        G = mock.mock_graph()
        for n in G.nodes():
            # delete key from first node and break
            del G.nodes[n][k]
            break
        # check that missing key throws an error
        with pytest.raises(KeyError):
            graphs.nX_to_dual(G)

    # test dual
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)

    # complexify the geoms to check with and without kinks, and in mixed forward and reverse directions
    for i, (s, e, d) in enumerate(G.edges(data=True)):
        # add a kink to each second geom
        if i % 2 == 0:
            geom = d['geom']
            start = geom.coords[0]
            end = geom.coords[-1]
            # bump the new midpoint coordinates
            mid = list(geom.centroid.coords[0])
            mid[0] += 10
            mid[1] -= 10
            # append 3d coord to check behaviour on 3d data
            for n in [start, mid, end]:
                n = list(n)
                n.append(10)
            G[s][e]['geom'] = geometry.LineString([start, mid, end])
        # flip each third geom
        if i % 3 == 0:
            flipped_coords = np.fliplr(d['geom'].coords.xy)
            G[s][e]['geom'] = geometry.LineString([[x, y] for x, y in zip(flipped_coords[0], flipped_coords[1])])
    G_dual = graphs.nX_to_dual(G)

    # from cityseer.util import plot
    # plot.plot_nX_primal_or_dual(primal=G, dual=G_dual)

    # dual nodes should equal primal edges
    assert G_dual.number_of_nodes() == G.number_of_edges()
    # all new nodes should have in-out-degrees of 4 except for following conditions:
    for n in G_dual.nodes():
        if n in ['50_51']:
            assert nx.degree(G_dual, n) == 0
        elif n in ['46_47', '46_48', '52_55', '52_53', '53_54', '54_55']:
            assert nx.degree(G_dual, n) == 2
        elif n in ['19_22', '22_23', '22_27', '22_46']:
            assert nx.degree(G_dual, n) == 5
        else:
            assert nx.degree(G_dual, n) == 4
Ejemplo n.º 18
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']
Ejemplo n.º 19
0
def test_compute_aggregated_A():
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    betas = np.array([-0.01, -0.005])
    distances = networks.distance_from_beta(betas)
    # network layer
    N = networks.Network_Layer_From_nX(G, distances)
    node_map = N._node_data
    edge_map = N._edge_data
    node_edge_map = N._node_edge_map
    # data layer
    data_dict = mock.mock_data_dict(G)
    qs = np.array([0, 1, 2])
    D = layers.Data_Layer_From_Dict(data_dict)
    # check single metrics independently against underlying for some use-cases, e.g. hill, non-hill, accessibility...
    D.assign_to_network(N, max_dist=500)
    # generate some mock landuse data
    landuse_labels = mock.mock_categorical_data(len(data_dict))
    landuse_classes, landuse_encodings = layers.encode_categorical(
        landuse_labels)
    # compute hill mixed uses
    D.compute_aggregated(landuse_labels,
                         mixed_use_keys=['hill_branch_wt'],
                         qs=qs)
    # test against underlying method
    data_map = D._data
    mu_data_hill, mu_data_other, ac_data, ac_data_wt, \
    stats_sum, stats_sum_wt, stats_mean, stats_mean_wt, stats_variance, stats_variance_wt, stats_max, stats_min = \
        data.local_aggregator(node_map,
                              edge_map,
                              node_edge_map,
                              data_map,
                              distances,
                              betas,
                              landuse_encodings,
                              qs=qs,
                              mixed_use_hill_keys=np.array([1]))
    for q_idx, q_key in enumerate(qs):
        for d_idx, d_key in enumerate(distances):
            assert np.allclose(
                N.metrics['mixed_uses']['hill_branch_wt'][q_key][d_key],
                mu_data_hill[0][q_idx][d_idx],
                atol=0.001,
                rtol=0)
    # gini simpson
    D.compute_aggregated(landuse_labels, mixed_use_keys=['gini_simpson'])
    # test against underlying method
    data_map = D._data
    mu_data_hill, mu_data_other, ac_data, ac_data_wt, \
    stats_sum, stats_sum_wt, stats_mean, stats_mean_wt, stats_variance, stats_variance_wt, stats_max, stats_min = \
        data.local_aggregator(node_map,
                              edge_map,
                              node_edge_map,
                              data_map,
                              distances,
                              betas,
                              landuse_encodings,
                              mixed_use_other_keys=np.array([1]))
    for d_idx, d_key in enumerate(distances):
        assert np.allclose(N.metrics['mixed_uses']['gini_simpson'][d_key],
                           mu_data_other[0][d_idx],
                           atol=0.001,
                           rtol=0)
    # accessibilities
    D.compute_aggregated(landuse_labels, accessibility_keys=['c'])
    # test against underlying method
    data_map = D._data
    mu_data_hill, mu_data_other, ac_data, ac_data_wt, \
    stats_sum, stats_sum_wt, stats_mean, stats_mean_wt, stats_variance, stats_variance_wt, stats_max, stats_min = \
        data.local_aggregator(node_map,
                              edge_map,
                              node_edge_map,
                              data_map,
                              distances,
                              betas,
                              landuse_encodings,
                              accessibility_keys=np.array([landuse_classes.index('c')]))
    for d_idx, d_key in enumerate(distances):
        assert np.allclose(
            N.metrics['accessibility']['non_weighted']['c'][d_key],
            ac_data[0][d_idx],
            atol=0.001,
            rtol=0)
        assert np.allclose(N.metrics['accessibility']['weighted']['c'][d_key],
                           ac_data_wt[0][d_idx],
                           atol=0.001,
                           rtol=0)
    # also check the number of returned types for a few assortments of metrics
    mixed_uses_hill_types = np.array([
        'hill', 'hill_branch_wt', 'hill_pairwise_wt', 'hill_pairwise_disparity'
    ])
    mixed_use_other_types = np.array(
        ['shannon', 'gini_simpson', 'raos_pairwise_disparity'])
    ac_codes = np.array(landuse_classes)

    mu_hill_random = np.arange(len(mixed_uses_hill_types))
    np.random.shuffle(mu_hill_random)

    mu_other_random = np.arange(len(mixed_use_other_types))
    np.random.shuffle(mu_other_random)

    ac_random = np.arange(len(landuse_classes))
    np.random.shuffle(ac_random)

    # mock disparity matrix
    mock_disparity_wt_matrix = np.full(
        (len(landuse_classes), len(landuse_classes)), 1)

    # not necessary to do all labels, first few should do
    for mu_h_min in range(3):
        mu_h_keys = np.array(mu_hill_random[mu_h_min:])

        for mu_o_min in range(3):
            mu_o_keys = np.array(mu_other_random[mu_o_min:])

            for ac_min in range(3):
                ac_keys = np.array(ac_random[ac_min:])

                # in the final case, set accessibility to a single code otherwise an error would be raised
                if len(mu_h_keys) == 0 and len(mu_o_keys) == 0 and len(
                        ac_keys) == 0:
                    ac_keys = np.array([0])

                # randomise order of keys and metrics
                mu_h_metrics = mixed_uses_hill_types[mu_h_keys]
                mu_o_metrics = mixed_use_other_types[mu_o_keys]
                ac_metrics = ac_codes[ac_keys]

                N_temp = networks.Network_Layer_From_nX(G, distances)
                D_temp = layers.Data_Layer_From_Dict(data_dict)
                D_temp.assign_to_network(N_temp, max_dist=500)
                D_temp.compute_aggregated(
                    landuse_labels,
                    mixed_use_keys=list(mu_h_metrics) + list(mu_o_metrics),
                    accessibility_keys=ac_metrics,
                    cl_disparity_wt_matrix=mock_disparity_wt_matrix,
                    qs=qs)

                # test against underlying method
                mu_data_hill, mu_data_other, ac_data, ac_data_wt, stats_sum, stats_sum_wt, \
                stats_mean, stats_mean_wt, stats_variance, stats_variance_wt, stats_max, stats_min = \
                    data.local_aggregator(node_map,
                                          edge_map,
                                          node_edge_map,
                                          data_map,
                                          distances,
                                          betas,
                                          landuse_encodings,
                                          qs=qs,
                                          mixed_use_hill_keys=mu_h_keys,
                                          mixed_use_other_keys=mu_o_keys,
                                          accessibility_keys=ac_keys,
                                          cl_disparity_wt_matrix=mock_disparity_wt_matrix)

                for mu_h_idx, mu_h_met in enumerate(mu_h_metrics):
                    for q_idx, q_key in enumerate(qs):
                        for d_idx, d_key in enumerate(distances):
                            assert np.allclose(
                                N_temp.metrics['mixed_uses'][mu_h_met][q_key]
                                [d_key],
                                mu_data_hill[mu_h_idx][q_idx][d_idx],
                                atol=0.001,
                                rtol=0)

                for mu_o_idx, mu_o_met in enumerate(mu_o_metrics):
                    for d_idx, d_key in enumerate(distances):
                        assert np.allclose(
                            N_temp.metrics['mixed_uses'][mu_o_met][d_key],
                            mu_data_other[mu_o_idx][d_idx],
                            atol=0.001,
                            rtol=0)

                for ac_idx, ac_met in enumerate(ac_metrics):
                    for d_idx, d_key in enumerate(distances):
                        assert np.allclose(N_temp.metrics['accessibility']
                                           ['non_weighted'][ac_met][d_key],
                                           ac_data[ac_idx][d_idx],
                                           atol=0.001,
                                           rtol=0)
                        assert np.allclose(N_temp.metrics['accessibility']
                                           ['weighted'][ac_met][d_key],
                                           ac_data_wt[ac_idx][d_idx],
                                           atol=0.001,
                                           rtol=0)

    # most integrity checks happen in underlying method, though check here for mismatching labels length and typos
    with pytest.raises(ValueError):
        D.compute_aggregated(landuse_labels[-1], mixed_use_keys=['shannon'])
    with pytest.raises(ValueError):
        D.compute_aggregated(landuse_labels, mixed_use_keys=['spelling_typo'])
    # don't check accessibility_labels for typos - because only warning is triggered (not all labels will be in all data)
    # check that unassigned data layer flags
    with pytest.raises(ValueError):
        D_new = layers.Data_Layer_From_Dict(data_dict)
        D_new.compute_aggregated(landuse_labels, mixed_use_keys=['shannon'])
Ejemplo n.º 20
0
# %%
# test maps version
import os

os.environ['CITYSEER_QUIET_MODE'] = '1'

from cityseer.util import mock, graphs
from src.explore.toy_models import mmm_layercake_b

iters = 200
graph = mock.mock_graph()
graph = graphs.nX_simple_geoms(graph)
graph = graphs.nX_decompose(graph, 20)

layer_specs = {
    'cap_step': 0.5,
    'dist_threshold': 600,
    'pop_threshold': 30,
    'kill_threshold': 0.5,
    'explore_rate': 0.5
}

pop_map, landuse_maps, capacitance_maps = mmm_layercake_b(graph,
                                                          iters,
                                                          _layer_specs=layer_specs,
                                                          seed=False,
                                                          random_seed=0)

# %%
import networkx as nx
from src import util_funcs
Ejemplo n.º 21
0
def test_compute_aggregated_B():
    '''
    Test stats component
    '''
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    betas = np.array([-0.01, -0.005])
    distances = networks.distance_from_beta(betas)
    # network layer
    N = networks.Network_Layer_From_nX(G, distances)
    node_map = N._node_data
    edge_map = N._edge_data
    node_edge_map = N._node_edge_map
    # data layer
    data_dict = mock.mock_data_dict(G)
    qs = np.array([0, 1, 2])
    D = layers.Data_Layer_From_Dict(data_dict)
    # check single metrics independently against underlying for some use-cases, e.g. hill, non-hill, accessibility...
    D.assign_to_network(N, max_dist=500)

    # generate some mock landuse data
    mock_numeric = mock.mock_numerical_data(len(data_dict), num_arrs=2)

    # generate stats
    D.compute_aggregated(stats_keys=['boo', 'baa'],
                         stats_data_arrs=mock_numeric)

    # test against underlying method
    data_map = D._data
    mu_data_hill, mu_data_other, ac_data, ac_data_wt, \
    stats_sum, stats_sum_wt, stats_mean, stats_mean_wt, stats_variance, stats_variance_wt, stats_max, stats_min = \
        data.local_aggregator(node_map,
                              edge_map,
                              node_edge_map,
                              data_map,
                              distances,
                              betas,
                              numerical_arrays=mock_numeric)

    stats_keys = [
        'max', 'min', 'sum', 'sum_weighted', 'mean', 'mean_weighted',
        'variance', 'variance_weighted'
    ]
    stats_data = [
        stats_max, stats_min, stats_sum, stats_sum_wt, stats_mean,
        stats_mean_wt, stats_variance, stats_variance_wt
    ]

    for num_idx, num_label in enumerate(['boo', 'baa']):
        for s_key, stats in zip(stats_keys, stats_data):
            for d_idx, d_key in enumerate(distances):
                assert np.allclose(N.metrics['stats'][num_label][s_key][d_key],
                                   stats[num_idx][d_idx],
                                   atol=0.001,
                                   rtol=0)

    # check that mismatching label and array lengths are caught
    for labels, arrs in (
        (['a'], mock_numeric),  # mismatching lengths
        (['a', 'b'], None),  # missing arrays
        (None, mock_numeric)):  # missing labels
        with pytest.raises(ValueError):
            D.compute_aggregated(stats_keys=labels, stats_data_arrs=arrs)
Ejemplo n.º 22
0
def network_generator():
    for betas in [[-0.008], [-0.008, -0.002]]:
        distances = networks.distance_from_beta(betas)
        G = mock.mock_graph()
        G = graphs.nX_simple_geoms(G)
        yield G, distances, betas
Ejemplo n.º 23
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=[])
Ejemplo n.º 24
0
def test_nX_remove_filler_nodes():
    # test that redundant intersections are removed, i.e. where degree == 2
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    G_messy = make_messy_graph(G)

    # from cityseer.util import plot
    # plot.plot_nX(G_messy, labels=True)

    # simplify and test
    G_simplified = graphs.nX_remove_filler_nodes(G_messy)
    # plot.plot_nX(G_simplified, labels=True)

    # check that the simplified version matches the original un-messified version
    # but note the simplified version will have the disconnected loop of 52-53-54-55 now condensed to only #52
    g_nodes = set(G.nodes)
    g_nodes = g_nodes.difference([53, 54, 55])
    assert list(g_nodes).sort() == list(G_simplified.nodes).sort()
    g_edges = set(G.edges)
    g_edges = g_edges.difference([(52, 53), (53, 54), (54, 55), (52, 55)])  # condensed edges
    g_edges = g_edges.union([(52, 52)])  # the new self loop
    assert list(g_edges).sort() == list(G_simplified.edges).sort()

    # check the integrity of the edges
    for s, e, d in G_simplified.edges(data=True):
        # ignore the new self-looping disconnected edge
        if s == 52 and e == 52:
            continue
        assert G_simplified[s][e]['geom'].length == G[s][e]['geom'].length
    # manually check that the new self-looping edge is equal in length to its original segments
    l = 0
    for s, e in [(52, 53), (53, 54), (54, 55), (52, 55)]:
        l += G[s][e]['geom'].length
    assert l == G_simplified[52][52]['geom'].length

    # check that all nodes still have 'x' and 'y' keys
    for n, d in G_simplified.nodes(data=True):
        assert 'x' in d
        assert 'y' in d

    # lollipop test - where looping component (all nodes == degree 2) suspend off a node with degree > 2
    # lollipops are handled slightly differently from isolated looping components (all nodes == degree 2)
    # there are no lollipops in the mock graph, so create one here

    # generate graph
    G_lollipop = nx.Graph()
    nodes = [
        (1, {'x': 700400, 'y': 5719750}),
        (2, {'x': 700400, 'y': 5719650}),
        (3, {'x': 700500, 'y': 5719550}),
        (4, {'x': 700400, 'y': 5719450}),
        (5, {'x': 700300, 'y': 5719550})
    ]
    G_lollipop.add_nodes_from(nodes)
    edges = [
        (1, 2),
        (2, 3),
        (3, 4),
        (4, 5),
        (5, 2)
    ]
    G_lollipop.add_edges_from(edges)

    # add edge geoms
    G_lollipop = graphs.nX_simple_geoms(G_lollipop)

    # flip some geometry
    G_lollipop[2][5]['geom'] = geometry.LineString(G_lollipop[2][5]['geom'].coords[::-1])
    # simplify
    G_lollipop_simpl = graphs.nX_remove_filler_nodes(G_lollipop)

    # check integrity of graph
    assert nx.number_of_nodes(G_lollipop_simpl) == 2
    assert nx.number_of_edges(G_lollipop_simpl) == 2

    # geoms should still be same cumulative length
    before_len = 0
    for s, e, d in G_lollipop.edges(data=True):
        before_len += d['geom'].length
    after_len = 0
    for s, e, d in G_lollipop_simpl.edges(data=True):
        after_len += d['geom'].length
    assert before_len == after_len
    # end point of stick should match start / end point of lollipop
    assert G_lollipop_simpl[1][2]['geom'].coords[-1] == G_lollipop_simpl[2][2]['geom'].coords[0]
    # start and end point of lollipop should match
    assert G_lollipop_simpl[2][2]['geom'].coords[0] == G_lollipop_simpl[2][2]['geom'].coords[-1]

    # check that missing geoms throw an error
    G_k = G_messy.copy()
    for i, (s, e) in enumerate(G_k.edges()):
        if i % 2 == 0:
            del G_k[s][e]['geom']
    with pytest.raises(KeyError):
        graphs.nX_remove_filler_nodes(G_k)

    # check that non-LineString geoms throw an error
    G_k = G_messy.copy()
    for s, e in G_k.edges():
        G_k[s][e]['geom'] = geometry.Point([G_k.nodes[s]['x'], G_k.nodes[s]['y']])
    with pytest.raises(TypeError):
        graphs.nX_remove_filler_nodes(G_k)

    # catch non-touching Linestrings
    G_corr = G_messy.copy()
    for s, e in G_corr.edges():
        geom = G_corr[s][e]['geom']
        start = list(geom.coords[0])
        end = list(geom.coords[1])
        # corrupt a point
        start[0] = start[0] - 1
        G_corr[s][e]['geom'] = geometry.LineString([start, end])
    with pytest.raises(TypeError):
        graphs.nX_remove_filler_nodes(G_corr)
Ejemplo n.º 25
0
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import colors

from cityseer.metrics import networks, layers
from cityseer.util import mock, graphs, plot

plt.style.use('./matplotlibrc')

base_path = path.dirname(__file__)

#
#
# INTRO PLOT
G = mock.mock_graph()
plot.plot_nX(G, path='graph.png', labels=True, dpi=150)

# INTRO EXAMPLE PLOTS
G = graphs.nX_simple_geoms(G)
G = graphs.nX_decompose(G, 20)

N = networks.Network_Layer_From_nX(G, distances=[400, 800])
N.compute_centrality(measures=['segment_harmonic'])

data_dict = mock.mock_data_dict(G, random_seed=25)
D = layers.Data_Layer_From_Dict(data_dict)
D.assign_to_network(N, max_dist=400)
landuse_labels = mock.mock_categorical_data(len(data_dict), random_seed=25)
D.hill_branch_wt_diversity(landuse_labels, qs=[0])
G_metrics = N.to_networkX()
Ejemplo n.º 26
0
def test_nX_wgs_to_utm():
    # check that node coordinates are correctly converted
    G_utm = mock.mock_graph()
    G_wgs = mock.mock_graph(wgs84_coords=True)
    G_converted = graphs.nX_wgs_to_utm(G_wgs)
    for n, d in G_utm.nodes(data=True):
        # rounding can be tricky
        assert np.allclose(d['x'], G_converted.nodes[n]['x'], atol=0.1, rtol=0)
        assert np.allclose(d['y'], G_converted.nodes[n]['y'], atol=0.1, rtol=0)

    # check that edge coordinates are correctly converted
    G_utm = mock.mock_graph()
    G_utm = graphs.nX_simple_geoms(G_utm)

    G_wgs = mock.mock_graph(wgs84_coords=True)
    G_wgs = graphs.nX_simple_geoms(G_wgs)

    G_converted = graphs.nX_wgs_to_utm(G_wgs)
    for s, e, d in G_utm.edges(data=True):
        assert round(d['geom'].length, 1) == round(G_converted[s][e]['geom'].length, 1)

    # check that non-LineString geoms throw an error
    G_wgs = mock.mock_graph(wgs84_coords=True)
    for s, e in G_wgs.edges():
        G_wgs[s][e]['geom'] = geometry.Point([G_wgs.nodes[s]['x'], G_wgs.nodes[s]['y']])
    with pytest.raises(TypeError):
        graphs.nX_wgs_to_utm(G_wgs)

    # check that missing node keys throw an error
    for k in ['x', 'y']:
        G_wgs = mock.mock_graph(wgs84_coords=True)
        for n in G_wgs.nodes():
            # delete key from first node and break
            del G_wgs.nodes[n][k]
            break
        # check that missing key throws an error
        with pytest.raises(KeyError):
            graphs.nX_wgs_to_utm(G_wgs)

    # check that non WGS coordinates throw error
    G_utm = mock.mock_graph()
    with pytest.raises(ValueError):
        graphs.nX_wgs_to_utm(G_utm)

    # check that non-matching UTM zones are coerced to the same zone
    # this scenario spans two UTM zones
    G_wgs_b = nx.Graph()
    nodes = [
        (1, {'x': -0.0005, 'y': 51.572}),
        (2, {'x': -0.0005, 'y': 51.571}),
        (3, {'x': 0.0005, 'y': 51.570}),
        (4, {'x': -0.0005, 'y': 51.569}),
        (5, {'x': -0.0015, 'y': 51.570})
    ]
    G_wgs_b.add_nodes_from(nodes)
    edges = [
        (1, 2),
        (2, 3),
        (3, 4),
        (4, 5),
        (5, 2)
    ]
    G_wgs_b.add_edges_from(edges)
    G_utm_30 = graphs.nX_wgs_to_utm(G_wgs_b)
    G_utm_30 = graphs.nX_simple_geoms(G_utm_30)

    # if not consistently coerced to UTM zone, the distances from 2-3 and 3-4 will be over 400km
    for s, e, d in G_utm_30.edges(data=True):
        assert d['geom'].length < 200

    # check that explicit zones are respectively coerced
    G_utm_31 = graphs.nX_wgs_to_utm(G_wgs_b, force_zone_number=31)
    G_utm_31 = graphs.nX_simple_geoms(G_utm_31)
    for n, d in G_utm_31.nodes(data=True):
        assert d['x'] != G_utm_30.nodes[n]['x']
Ejemplo n.º 27
0
def test_nX_decompose():
    # check that missing geoms throw an error
    G = mock.mock_graph()
    with pytest.raises(KeyError):
        graphs.nX_decompose(G, 20)

    # check that non-LineString geoms throw an error
    G = mock.mock_graph()
    for s, e in G.edges():
        G[s][e]['geom'] = geometry.Point([G.nodes[s]['x'], G.nodes[s]['y']])
    with pytest.raises(TypeError):
        graphs.nX_decompose(G, 20)

    # test decomposition
    G = mock.mock_graph()
    G = graphs.nX_simple_geoms(G)
    # first clean the graph to strip disconnected looping component
    # this gives a start == end node situation for testing
    G_simple = graphs.nX_remove_filler_nodes(G)
    G_decompose = graphs.nX_decompose(G_simple, 20)

    # from cityseer.util import plot
    # plot.plot_nX(G_simple, labels=True)
    # plot.plot_nX(G_decompose)
    assert nx.number_of_nodes(G_decompose) == 661
    assert nx.number_of_edges(G_decompose) == 682

    # check that total lengths are the same
    G_lens = 0
    for s, e, e_data in G_simple.edges(data=True):
        G_lens += e_data['geom'].length
    G_d_lens = 0
    for s, e, e_data in G_decompose.edges(data=True):
        G_d_lens += e_data['geom'].length
    assert np.allclose(G_lens, G_d_lens, atol=0.001, rtol=0)

    # check that all ghosted edges have one or two edges
    for n, n_data in G_decompose.nodes(data=True):
        if 'ghosted' in n_data and n_data['ghosted']:
            nbs = list(G_decompose.neighbors(n))
            assert len(nbs) == 1 or len(nbs) == 2

    # check that all new nodes are ghosted
    for n, n_data in G_decompose.nodes(data=True):
        if not G_simple.has_node(n):
            assert n_data['ghosted']

    # check that geoms are correctly flipped
    G_forward = mock.mock_graph()
    G_forward = graphs.nX_simple_geoms(G_forward)
    G_forward_decompose = graphs.nX_decompose(G_forward, 20)

    G_backward = mock.mock_graph()
    G_backward = graphs.nX_simple_geoms(G_backward)
    for i, (s, e, d) in enumerate(G_backward.edges(data=True)):
        # flip each third geom
        if i % 3 == 0:
            flipped_coords = np.fliplr(d['geom'].coords.xy)
            G[s][e]['geom'] = geometry.LineString([[x, y] for x, y in zip(flipped_coords[0], flipped_coords[1])])
    G_backward_decompose = graphs.nX_decompose(G_backward, 20)

    for n, d in G_forward_decompose.nodes(data=True):
        assert d['x'] == G_backward_decompose.nodes[n]['x']
        assert d['y'] == G_backward_decompose.nodes[n]['y']

    # test that geom coordinate mismatch throws an error
    G = mock.mock_graph()
    for k in ['x', 'y']:
        for n in G.nodes():
            G.nodes[n][k] = G.nodes[n][k] + 1
            break
        with pytest.raises(KeyError):
            graphs.nX_decompose(G, 20)
Ejemplo n.º 28
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)