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