Esempio n. 1
0
def aggregate_to_src_idx(netw_src_idx: int,
                         node_data: np.ndarray,
                         edge_data: np.ndarray,
                         node_edge_map: Dict,
                         data_map: np.ndarray,
                         max_dist: float,
                         angular: bool = False):
    # this function is typically called iteratively, so do type checks from parent methods
    netw_x_arr = node_data[:, 0]
    netw_y_arr = node_data[:, 1]
    netw_src_x = netw_x_arr[netw_src_idx]
    netw_src_y = netw_y_arr[netw_src_idx]
    d_x_arr = data_map[:, 0]
    d_y_arr = data_map[:, 1]
    d_assign_nearest = data_map[:, 2]
    d_assign_next_nearest = data_map[:, 3]
    # run the shortest tree dijkstra
    # keep in mind that predecessor map is based on impedance heuristic - which can be different from metres
    # NOTE -> use np.inf for max distance so as to explore all paths
    # In some cases the predecessor nodes will be within reach even if the closest node is not
    # Total distance is checked later
    tree_map, tree_edges = centrality.shortest_path_tree(
        edge_data,
        node_edge_map,
        netw_src_idx,
        max_dist=max_dist,
        angular=angular)  # turn off checks! This is called iteratively...
    tree_preds = tree_map[:, 1]
    tree_dists = tree_map[:, 2]
    # filter the data by distance
    # in this case, the source x, y is the same as for the networks
    filtered_data = radial_filter(netw_src_x, netw_src_y, d_x_arr, d_y_arr,
                                  max_dist)
    # arrays for writing the reachable data points and their distances
    reachable_data = np.full(len(data_map), False)
    reachable_data_dist = np.full(len(data_map), np.inf)
    # iterate the distance trimmed data points
    reachable_idx = np.where(filtered_data)[0]
    for data_idx in reachable_idx:
        # find the primary assigned network index for the data point
        if np.isfinite(d_assign_nearest[data_idx]):
            netw_idx = int(d_assign_nearest[data_idx])
            # if the assigned network node is within the threshold
            if tree_dists[netw_idx] < max_dist:
                # get the distance from the data point to the network node
                d_d = np.hypot(d_x_arr[data_idx] - netw_x_arr[netw_idx],
                               d_y_arr[data_idx] - netw_y_arr[netw_idx])
                # add to the distance assigned for the network node
                dist = tree_dists[netw_idx] + d_d
                # only assign distance if within max distance
                if dist <= max_dist:
                    reachable_data[data_idx] = True
                    reachable_data_dist[data_idx] = dist
        # the next-nearest may offer a closer route depending on the direction the shortest path approaches from
        if np.isfinite(d_assign_next_nearest[data_idx]):
            netw_idx = int(d_assign_next_nearest[data_idx])
            # if the assigned network node is within the threshold
            if tree_dists[netw_idx] < max_dist:
                # get the distance from the data point to the network node
                d_d = np.hypot(d_x_arr[data_idx] - netw_x_arr[netw_idx],
                               d_y_arr[data_idx] - netw_y_arr[netw_idx])
                # add to the distance assigned for the network node
                dist = tree_dists[netw_idx] + d_d
                # only assign distance if within max distance
                # AND only if closer than other direction
                if dist <= max_dist and dist < reachable_data_dist[data_idx]:
                    reachable_data[data_idx] = True
                    reachable_data_dist[data_idx] = dist

    # note that some entries will be nan values if the max distance was exceeded
    return reachable_data, reachable_data_dist, tree_preds
Esempio n. 2
0
def test_aggregate_to_src_idx(primal_graph):
    node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX(primal_graph)
    # generate data
    data_dict = mock.mock_data_dict(primal_graph, random_seed=13)
    data_uids, data_map = layers.data_map_from_dict(data_dict)
    for max_dist in [400, 750]:
        # in this case, use same assignment max dist as search max dist
        data_map_temp = data_map.copy()
        data_map_temp = data.assign_to_network(data_map_temp,
                                               node_data,
                                               edge_data,
                                               node_edge_map,
                                               max_dist=max_dist)
        for angular in [True, False]:
            for netw_src_idx in range(len(node_data)):
                # aggregate to src...
                reachable_data, reachable_data_dist, tree_preds = data.aggregate_to_src_idx(netw_src_idx,
                                                                                            node_data,
                                                                                            edge_data,
                                                                                            node_edge_map,
                                                                                            data_map_temp,
                                                                                            max_dist,
                                                                                            angular=angular)
                # for debugging
                # from cityseer.tools import plot
                # plot.plot_graph_maps(node_uids, node_data, edge_data, data_map)
                # compare to manual checks on distances:
                netw_x_arr = node_data[:, 0]
                netw_y_arr = node_data[:, 1]
                data_x_arr = data_map_temp[:, 0]
                data_y_arr = data_map_temp[:, 1]
                # get the network distances
                tree_map, tree_edges = centrality.shortest_path_tree(edge_data,
                                                                     node_edge_map,
                                                                     netw_src_idx,
                                                                     max_dist=max_dist,
                                                                     angular=angular)
                tree_dists = tree_map[:, 2]
                # verify distances vs. the max
                for d_idx in range(len(data_map_temp)):
                    # check the integrity of the distances and classes
                    reachable = reachable_data[d_idx]
                    reachable_dist = reachable_data_dist[d_idx]
                    # get the distance via the nearest assigned index
                    nearest_dist = np.inf
                    # if a nearest node has been assigned
                    if np.isfinite(data_map_temp[d_idx, 2]):
                        # get the index for the assigned network node
                        netw_idx = int(data_map_temp[d_idx, 2])
                        # if this node is within the cutoff distance:
                        if tree_dists[netw_idx] < max_dist:
                            # get the distances from the data point to the assigned network node
                            d_d = np.hypot(data_x_arr[d_idx] - netw_x_arr[netw_idx],
                                           data_y_arr[d_idx] - netw_y_arr[netw_idx])
                            # and add it to the network distance path from the source to the assigned node
                            n_d = tree_dists[netw_idx]
                            nearest_dist = d_d + n_d
                    # also get the distance via the next nearest assigned index
                    next_nearest_dist = np.inf
                    # if a nearest node has been assigned
                    if np.isfinite(data_map_temp[d_idx, 3]):
                        # get the index for the assigned network node
                        netw_idx = int(data_map_temp[d_idx, 3])
                        # if this node is within the radial cutoff distance:
                        if tree_dists[netw_idx] < max_dist:
                            # get the distances from the data point to the assigned network node
                            d_d = np.hypot(data_x_arr[d_idx] - netw_x_arr[netw_idx],
                                           data_y_arr[d_idx] - netw_y_arr[netw_idx])
                            # and add it to the network distance path from the source to the assigned node
                            n_d = tree_dists[netw_idx]
                            next_nearest_dist = d_d + n_d
                    # now check distance integrity
                    if np.isinf(reachable_dist):
                        assert not reachable
                        assert nearest_dist > max_dist and next_nearest_dist > max_dist
                    else:
                        assert reachable
                        assert reachable_dist <= max_dist
                        if nearest_dist < next_nearest_dist:
                            assert reachable_dist == nearest_dist
                        else:
                            assert reachable_dist == next_nearest_dist
def test_shortest_path_tree(primal_graph, dual_graph):
    node_uids_p, node_data_p, edge_data_p, node_edge_map_p = graphs.graph_maps_from_nX(
        primal_graph)
    # prepare round-trip graph for checks
    G_round_trip = graphs.nX_from_graph_maps(node_uids_p, node_data_p,
                                             edge_data_p, node_edge_map_p)
    # prepare dual graph
    node_uids_d, node_data_d, edge_data_d, node_edge_map_d = graphs.graph_maps_from_nX(
        dual_graph)
    assert len(node_uids_d) > len(node_uids_p)
    # test all shortest paths against networkX version of dijkstra
    for max_dist in [0, 500, 2000, np.inf]:
        for src_idx in range(len(primal_graph)):
            # check shortest path maps
            tree_map, tree_edges = centrality.shortest_path_tree(
                edge_data_p,
                node_edge_map_p,
                src_idx,
                max_dist=max_dist,
                angular=False)
            tree_preds_p = tree_map[:, 1]
            tree_short_dists_p = tree_map[:, 2]
            # compare against networkx dijkstra
            nx_dist, nx_path = nx.single_source_dijkstra(G_round_trip,
                                                         src_idx,
                                                         weight='length',
                                                         cutoff=max_dist)
            for j in range(len(primal_graph)):
                if j in nx_path:
                    assert find_path(j, src_idx, tree_preds_p) == nx_path[j]
                    assert np.allclose(tree_short_dists_p[j],
                                       nx_dist[j],
                                       atol=0.001,
                                       rtol=0)
    # compare angular simplest paths for a selection of targets on primal vs. dual
    # remember, this is angular change not distance travelled
    # can be compared from primal to dual in this instance because edge segments are straight
    # i.e. same amount of angular change whether primal or dual graph
    # plot.plot_nX_primal_or_dual(primal=primal_graph, dual=dual_graph, labels=True, node_size=80)
    p_source_idx = node_uids_p.index(0)
    primal_targets = (15, 20, 37)
    dual_sources = ('0_1', '0_16', '0_31')
    dual_targets = ('13_15', '17_20', '36_37')
    for p_target, d_source, d_target in zip(primal_targets, dual_sources,
                                            dual_targets):
        p_target_idx = node_uids_p.index(p_target)
        d_source_idx = node_uids_d.index(
            d_source)  # dual source index changes depending on direction
        d_target_idx = node_uids_d.index(d_target)
        tree_map_p, tree_edges_p = centrality.shortest_path_tree(
            edge_data_p,
            node_edge_map_p,
            p_source_idx,
            max_dist=max_dist,
            angular=True)
        tree_simpl_dists_p = tree_map_p[:, 3]
        tree_map_d, tree_edges_d = centrality.shortest_path_tree(
            edge_data_d,
            node_edge_map_d,
            d_source_idx,
            max_dist=max_dist,
            angular=True)
        tree_simpl_dists_d = tree_map_d[:, 3]
        assert np.allclose(tree_simpl_dists_p[p_target_idx],
                           tree_simpl_dists_d[d_target_idx],
                           atol=0.001,
                           rtol=0)
    # angular impedance should take a simpler but longer path - test basic case on dual
    # source and target are the same for either
    src_idx = node_uids_d.index('11_6')
    target = node_uids_d.index('39_40')
    # SIMPLEST PATH: get simplest path tree using angular impedance
    tree_map, tree_edges = centrality.shortest_path_tree(
        edge_data_d, node_edge_map_d, src_idx, max_dist=np.inf,
        angular=True)  # ANGULAR = TRUE
    # find path
    tree_preds = tree_map[:, 1]
    path = find_path(target, src_idx, tree_preds)
    path_transpose = [node_uids_d[n] for n in path]
    # takes 1597m route via long outside segment
    # tree_dists[int(full_to_trim_idx_map[node_labels.index('39_40')])]
    assert path_transpose == [
        '11_6', '11_14', '10_14', '10_43', '43_44', '40_44', '39_40'
    ]
    # SHORTEST PATH:
    # get shortest path tree using non angular impedance
    tree_map, tree_edges = centrality.shortest_path_tree(
        edge_data_d, node_edge_map_d, src_idx, max_dist=np.inf,
        angular=False)  # ANGULAR = FALSE
    # find path
    tree_preds = tree_map[:, 1]
    path = find_path(target, src_idx, tree_preds)
    path_transpose = [node_uids_d[n] for n in path]
    # takes 1345m shorter route
    # tree_dists[int(full_to_trim_idx_map[node_labels.index('39_40')])]
    assert path_transpose == [
        '11_6', '6_7', '3_7', '3_4', '1_4', '0_1', '0_31', '31_32', '32_34',
        '34_37', '37_39', '39_40'
    ]
    # NO SIDESTEPS - explicit check that sidesteps are prevented
    src_idx = node_uids_d.index('10_43')
    target = node_uids_d.index('10_5')
    tree_map, tree_edges = centrality.shortest_path_tree(edge_data_d,
                                                         node_edge_map_d,
                                                         src_idx,
                                                         max_dist=np.inf,
                                                         angular=True)
    # find path
    tree_preds = tree_map[:, 1]
    path = find_path(target, src_idx, tree_preds)
    path_transpose = [node_uids_d[n] for n in path]
    # print(path_transpose)
    assert path_transpose == ['10_43', '10_5']
    # WITH SIDESTEPS - set angular flag to False
    # manually overwrite distance impedances with angular for this test
    # (angular has to be false otherwise shortest-path sidestepping avoided)
    edge_data_d_temp = edge_data_d.copy()
    # angular impedances at index 3 copied to distance impedances at distance 2
    edge_data_d_temp[:, 2] = edge_data_d_temp[:, 3]
    tree_map, tree_edges = centrality.shortest_path_tree(edge_data_d_temp,
                                                         node_edge_map_d,
                                                         src_idx,
                                                         max_dist=np.inf,
                                                         angular=False)
    # find path
    tree_preds = tree_map[:, 1]
    path = find_path(target, src_idx, tree_preds)
    path_transpose = [node_uids_d[n] for n in path]
    assert path_transpose == ['10_43', '10_14', '10_5']
def test_local_node_centrality(primal_graph):
    """
    Also tested indirectly via test_networks.test_compute_centrality

    Test centrality methods where possible against NetworkX - i.e. harmonic closeness and betweenness
    Note that NetworkX improved closeness is not the same as derivation used in this package
    NetworkX doesn't have a maximum distance cutoff, so run on the whole graph (low beta / high distance)
    """
    # generate node and edge maps
    node_uids, node_data, edge_data, node_edge_map = graphs.graph_maps_from_nX(
        primal_graph)
    G_round_trip = graphs.nX_from_graph_maps(node_uids, node_data, edge_data,
                                             node_edge_map)
    # needs a large enough beta so that distance thresholds aren't encountered
    betas = np.array([0.02, 0.01, 0.005, 0.0008, 0.0])
    distances = networks.distance_from_beta(betas)
    # set the keys - add shuffling to be sure various orders work
    measure_keys = [
        'node_density', 'node_farness', 'node_cycles', 'node_harmonic',
        'node_beta', 'node_betweenness', 'node_betweenness_beta'
    ]
    np.random.shuffle(measure_keys)  # in place
    measure_keys = tuple(measure_keys)
    # generate the measures
    measures_data = centrality.local_node_centrality(node_data, edge_data,
                                                     node_edge_map, distances,
                                                     betas, measure_keys)
    node_density = measures_data[measure_keys.index('node_density')]
    node_farness = measures_data[measure_keys.index('node_farness')]
    node_cycles = measures_data[measure_keys.index('node_cycles')]
    node_harmonic = measures_data[measure_keys.index('node_harmonic')]
    node_beta = measures_data[measure_keys.index('node_beta')]
    node_betweenness = measures_data[measure_keys.index('node_betweenness')]
    node_betweenness_beta = measures_data[measure_keys.index(
        'node_betweenness_beta')]
    # improved closeness is derived after the fact
    improved_closness = node_density / node_farness / node_density

    # test node density
    # node density count doesn't include self-node
    # connected component == 49 == len(G) - 1
    # isolated looping component == 3
    # isolated edge == 1
    # isolated node == 0
    for n in node_density[4]:  # infinite distance - exceeds cutoff clashes
        assert n in [49, 3, 1, 0]

    # test harmonic closeness vs NetworkX
    nx_harm_cl = nx.harmonic_centrality(G_round_trip, distance='length')
    nx_harm_cl = np.array([v for v in nx_harm_cl.values()])
    assert np.allclose(nx_harm_cl, node_harmonic[4], atol=0.001, rtol=0)

    # test betweenness vs NetworkX
    # set endpoint counting to false and do not normalise
    # nx node centrality NOT implemented for MultiGraph
    G_non_multi = nx.Graph()  # don't change to MultiGraph!!!
    G_non_multi.add_nodes_from(G_round_trip.nodes())
    for s, e, k, d in G_round_trip.edges(keys=True, data=True):
        assert k == 0
        G_non_multi.add_edge(s, e, **d)
    nx_betw = nx.betweenness_centrality(G_non_multi,
                                        weight='length',
                                        endpoints=False,
                                        normalized=False)
    nx_betw = np.array([v for v in nx_betw.values()])
    # nx betweenness gives 0.5 instead of 1 for all disconnected looping component nodes
    # nx presumably takes equidistant routes into account, in which case only the fraction is aggregated
    assert np.allclose(nx_betw[:52],
                       node_betweenness[4][:52],
                       atol=0.001,
                       rtol=0)

    # do the comparisons array-wise so that betweenness can be aggregated
    d_n = len(distances)
    betw = np.full((d_n, primal_graph.number_of_nodes()), 0.0)
    betw_wt = np.full((d_n, primal_graph.number_of_nodes()), 0.0)
    dens = np.full((d_n, primal_graph.number_of_nodes()), 0.0)
    far_short_dist = np.full((d_n, primal_graph.number_of_nodes()), 0.0)
    far_simpl_dist = np.full((d_n, primal_graph.number_of_nodes()), 0.0)
    harmonic_cl = np.full((d_n, primal_graph.number_of_nodes()), 0.0)
    grav = np.full((d_n, primal_graph.number_of_nodes()), 0.0)
    cyc = np.full((d_n, primal_graph.number_of_nodes()), 0.0)

    for src_idx in range(len(primal_graph)):
        # get shortest path maps
        tree_map, tree_edges = centrality.shortest_path_tree(edge_data,
                                                             node_edge_map,
                                                             src_idx,
                                                             max(distances),
                                                             angular=False)
        tree_nodes = np.where(tree_map[:, 0])[0]
        tree_preds = tree_map[:, 1]
        tree_short_dist = tree_map[:, 2]
        tree_simpl_dist = tree_map[:, 3]
        tree_cycles = tree_map[:, 4]
        for to_idx in tree_nodes:
            # skip self nodes
            if to_idx == src_idx:
                continue
            # get shortest / simplest distances
            to_short_dist = tree_short_dist[to_idx]
            to_simpl_dist = tree_simpl_dist[to_idx]
            cycles = tree_cycles[to_idx]
            # continue if exceeds max
            if np.isinf(to_short_dist):
                continue
            for d_idx in range(len(distances)):
                dist_cutoff = distances[d_idx]
                beta = betas[d_idx]
                if to_short_dist <= dist_cutoff:
                    # don't exceed threshold
                    # if to_dist <= dist_cutoff:
                    # aggregate values
                    dens[d_idx][src_idx] += 1
                    far_short_dist[d_idx][src_idx] += to_short_dist
                    far_simpl_dist[d_idx][src_idx] += to_simpl_dist
                    harmonic_cl[d_idx][src_idx] += 1 / to_short_dist
                    grav[d_idx][src_idx] += np.exp(-beta * to_short_dist)
                    # cycles
                    cyc[d_idx][src_idx] += cycles
                    # only process betweenness in one direction
                    if to_idx < src_idx:
                        continue
                    # betweenness - only counting truly between vertices, not starting and ending verts
                    inter_idx = tree_preds[to_idx]
                    # isolated nodes will have no predecessors
                    if np.isnan(inter_idx):
                        continue
                    inter_idx = np.int(inter_idx)
                    while True:
                        # break out of while loop if the intermediary has reached the source node
                        if inter_idx == src_idx:
                            break
                        betw[d_idx][inter_idx] += 1
                        betw_wt[d_idx][inter_idx] += np.exp(-beta *
                                                            to_short_dist)
                        # follow
                        inter_idx = np.int(tree_preds[inter_idx])
    improved_cl = dens / far_short_dist / dens

    assert np.allclose(node_density, dens, atol=0.001, rtol=0)
    assert np.allclose(node_farness, far_short_dist, atol=0.01,
                       rtol=0)  # relax precision
    assert np.allclose(node_cycles, cyc, atol=0.001, rtol=0)
    assert np.allclose(node_harmonic, harmonic_cl, atol=0.001, rtol=0)
    assert np.allclose(node_beta, grav, atol=0.001, rtol=0)
    assert np.allclose(improved_closness,
                       improved_cl,
                       equal_nan=True,
                       atol=0.001,
                       rtol=0)
    assert np.allclose(node_betweenness, betw, atol=0.001, rtol=0)
    assert np.allclose(node_betweenness_beta, betw_wt, atol=0.001, rtol=0)

    # catch typos
    with pytest.raises(ValueError):
        centrality.local_node_centrality(node_data, edge_data, node_edge_map,
                                         distances, betas, ('typo_key', ))
Esempio n. 5
0
def aggregate_to_src_idx(netw_src_idx: int,
                         node_data: np.ndarray,
                         edge_data: np.ndarray,
                         node_edge_map: Dict,
                         data_map: np.ndarray,
                         max_dist: float,
                         jitter_scale: float = 0.0,
                         angular: bool = False):
    # this function is typically called iteratively, so do type checks from parent methods
    netw_x_arr = node_data[:, 0]
    netw_y_arr = node_data[:, 1]
    netw_src_x = netw_x_arr[netw_src_idx]
    netw_src_y = netw_y_arr[netw_src_idx]
    d_x_arr = data_map[:, 0]
    d_y_arr = data_map[:, 1]
    d_assign_nearest = data_map[:, 2]
    d_assign_next_nearest = data_map[:, 3]
    # run the shortest tree dijkstra
    # keep in mind that predecessor map is based on impedance heuristic - which can be different from metres
    # NOTE -> use np.inf for max distance so as to explore all paths
    # In some cases the predecessor nodes will be within reach even if the closest node is not
    # Total distance is checked later
    tree_map, tree_edges = centrality.shortest_path_tree(
        edge_data,
        node_edge_map,
        netw_src_idx,
        max_dist=max_dist,
        jitter_scale=jitter_scale,
        angular=angular)
    '''
    Shortest tree dijkstra        
    Predecessor map is based on impedance heuristic - i.e. angular vs not
    Shortest path distances in metres used for defining max distances regardless
    RETURNS A SHORTEST PATH TREE MAP:
    0 - processed nodes
    1 - predecessors
    2 - shortest path distance
    3 - simplest path angular distance
    4 - cycles
    5 - origin segments
    6 - last segments
    '''
    tree_preds = tree_map[:, 1]
    tree_short_dists = tree_map[:, 2]
    # arrays for writing the reachable data points and their distances
    reachable_data = np.full(len(data_map), False)
    reachable_data_dist = np.full(len(data_map), np.inf)
    # iterate the data points
    for data_idx in range(len(data_map)):
        # find the primary assigned network index for the data point
        if np.isfinite(d_assign_nearest[data_idx]):
            netw_idx = int(d_assign_nearest[data_idx])
            # if the assigned network node is within the threshold
            if tree_short_dists[netw_idx] < max_dist:
                # get the distance from the data point to the network node
                d_d = np.hypot(d_x_arr[data_idx] - netw_x_arr[netw_idx],
                               d_y_arr[data_idx] - netw_y_arr[netw_idx])
                # add to the distance assigned for the network node
                dist = tree_short_dists[netw_idx] + d_d
                # only assign distance if within max distance
                if dist <= max_dist:
                    reachable_data[data_idx] = True
                    reachable_data_dist[data_idx] = dist
        # the next-nearest may offer a closer route depending on the direction the shortest path approaches from
        if np.isfinite(d_assign_next_nearest[data_idx]):
            netw_idx = int(d_assign_next_nearest[data_idx])
            # if the assigned network node is within the threshold
            if tree_short_dists[netw_idx] < max_dist:
                # get the distance from the data point to the network node
                d_d = np.hypot(d_x_arr[data_idx] - netw_x_arr[netw_idx],
                               d_y_arr[data_idx] - netw_y_arr[netw_idx])
                # add to the distance assigned for the network node
                dist = tree_short_dists[netw_idx] + d_d
                # only assign distance if within max distance
                # AND only if closer than other direction
                if dist <= max_dist and dist < reachable_data_dist[data_idx]:
                    reachable_data[data_idx] = True
                    reachable_data_dist[data_idx] = dist

    # note that some entries will be nan values if the max distance was exceeded
    return reachable_data, reachable_data_dist, tree_preds
Esempio n. 6
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)