def test_nX_simple_geoms(): # generate a mock graph g_raw = mock_graph() g_copy = graphs.nX_simple_geoms(g_raw) # test that geoms have been inferred correctly for s, e, k in g_copy.edges(keys=True): line_geom = geometry.LineString([ [g_raw.nodes[s]['x'], g_raw.nodes[s]['y']], [g_raw.nodes[e]['x'], g_raw.nodes[e]['y']] ]) assert line_geom == g_copy[s][e][k]['geom'] # check that missing node keys throw an error g_copy = g_raw.copy() for k in ['x', 'y']: for n in g_copy.nodes(): # delete key from first node and break del g_copy.nodes[n][k] break # check that missing key throws an error with pytest.raises(KeyError): graphs.nX_simple_geoms(g_copy) # check that zero length self-loops are caught and removed g_copy = g_raw.copy() g_copy.add_edge(0, 0) # simple geom from self edge = length of zero g_simple = graphs.nX_simple_geoms(g_copy) assert not g_simple.has_edge(0, 0)
def primal_graph() -> nx.MultiGraph: """ Returns ------- nx.MultiGraph A primal `NetworkX` `MultiGraph` for `pytest` tests. """ G_primal = mock_graph() G_primal = graphs.nX_simple_geoms(G_primal) return G_primal
def diamond_graph() -> nx.MultiGraph: """ Generates a diamond shaped `NetworkX` `MultiGraph` for testing or experimentation purposes. For manual checks of all node and segmentised methods. Returns ------- nx.MultiGraph A `NetworkX` `MultiGraph` with `x` and `y` node attributes. Notes ----- ```python # 3 # / \ # / \ # / a \ # 1-------2 # \ | / # \ |b/ c # \|/ # 0 # a = 100m = 2 * 50m # b = 86.60254m # c = 100m # all inner angles = 60º ``` """ G_diamond = nx.MultiGraph() G_diamond.add_nodes_from([(0, { 'x': 50, 'y': 0 }), (1, { 'x': 0, 'y': 86.60254 }), (2, { 'x': 100, 'y': 86.60254 }), (3, { 'x': 50, 'y': 86.60254 * 2 })]) G_diamond.add_edges_from([(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]) G_diamond = graphs.nX_simple_geoms(G_diamond) return G_diamond
from matplotlib import colors from shapely import geometry from cityseer.metrics import networks, layers from cityseer.tools import mock, graphs, plot base_path = os.getcwd() plt.style.use('matplotlibrc') ### # INTRO PLOT G = mock.mock_graph() plot.plot_nX(G, labels=True, node_size=80, path='images/graph.png', dpi=150) # INTRO EXAMPLE PLOTS G = graphs.nX_simple_geoms(G) G = graphs.nX_decompose(G, 20) N = networks.NetworkLayerFromNX(G, distances=[400, 800]) N.segment_centrality(measures=['segment_harmonic']) data_dict = mock.mock_data_dict(G, random_seed=25) D = layers.DataLayerFromDict(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() segment_harmonic_vals = [] mixed_uses_vals = [] for node, data in G_metrics.nodes(data=True):
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, k, d in G_utm.edges(data=True, keys=True): assert round(d['geom'].length, 1) == round(G_converted[s][e][k]['geom'].length, 1) # check that non-LineString geoms throw an error G_wgs = mock.mock_graph(wgs84_coords=True) for s, e, k in G_wgs.edges(keys=True): G_wgs[s][e][k]['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.MultiGraph() 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_consolidate(): # create a test graph G = nx.MultiGraph() nodes = [ (0, {'x': 620, 'y': 720}), (1, {'x': 620, 'y': 700}), (2, {'x': 660, 'y': 700}), (3, {'x': 660, 'y': 660}), (4, {'x': 700, 'y': 800}), (5, {'x': 720, 'y': 800}), (6, {'x': 700, 'y': 720}), (7, {'x': 720, 'y': 720}), (8, {'x': 700, 'y': 700}), (9, {'x': 700, 'y': 620}), (10, {'x': 720, 'y': 620}), (11, {'x': 760, 'y': 760}), (12, {'x': 800, 'y': 760}), (13, {'x': 780, 'y': 720}), (14, {'x': 840, 'y': 720}), (15, {'x': 840, 'y': 700})] edges = [ (0, 6), (1, 2), (2, 3), (2, 8), (4, 6), (5, 7), (6, 7), (6, 8), (7, 10), (7, 13), (8, 9), (8, 15), (11, 12), (11, 13), (12, 13), (13, 14) ] G.add_nodes_from(nodes) G.add_edges_from(edges) G = graphs.nX_simple_geoms(G) # behaviour confirmed visually # from cityseer.tools import plot # plot.plot_nX(G, labels=True, node_size=80, plot_geoms=True) G_merged_spatial = graphs.nX_consolidate_nodes(G, buffer_dist=25, crawl=True, merge_edges_by_midline=True) # plot.plot_nX(G_merged_spatial, labels=True, node_size=80, plot_geoms=True) # simplify first to test lollipop self-loop from node 15 G_split_opps = graphs.nX_split_opposing_geoms(G, buffer_dist=25, merge_edges_by_midline=True) # plot.plot_nX(G_split_opps, labels=True, node_size=80, plot_geoms=True) G_merged_spatial = graphs.nX_consolidate_nodes(G_split_opps, buffer_dist=25, merge_edges_by_midline=True, cent_min_degree=2) # plot.plot_nX(G_merged_spatial, labels=True, node_size=80, plot_geoms=True) assert G_merged_spatial.number_of_nodes() == 8 assert G_merged_spatial.number_of_edges() == 8 node_coords = [] for n, d in G_merged_spatial.nodes(data=True): node_coords.append((d['x'], d['y'])) assert node_coords == [(660, 660), (620.0, 710.0), (660.0, 710.0), (710.0, 800.0), (710.0, 710.0), (710.0, 620.0), (780.0, 710.0), (840.0, 710.0)] edge_lens = [] for s, e, d in G_merged_spatial.edges(data=True): edge_lens.append(d['geom'].length) assert edge_lens == [50.0, 40.0, 50.0, 90.0, 90.0, 70.0, 60.0, 147.70329614269008]
def test_nX_remove_filler_nodes(primal_graph): # test that redundant intersections are removed, i.e. where degree == 2 G_messy = make_messy_graph(primal_graph) # from cityseer.tools import plot # plot.plot_nX(G_messy, labels=True, node_size=80) # simplify and test G_simplified = graphs.nX_remove_filler_nodes(G_messy) # plot.plot_nX(G_simplified, labels=True, node_size=80) # 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(primal_graph.nodes) g_nodes = g_nodes.difference([53, 54, 55]) assert list(g_nodes).sort() == list(G_simplified.nodes).sort() g_edges = set(primal_graph.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() # plot.plot_nX(G_simplified, labels=True, node_size=80, plot_geoms=True) # check the integrity of the edges for s, e, k, d in G_simplified.edges(data=True, keys=True): # ignore the new self-looping disconnected edge if s == 52 and e == 52: continue # and the parallel edge if s in [45, 30] and e in [45, 30]: continue assert G_simplified[s][e][k]['geom'].length == primal_graph[s][e][k]['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 += primal_graph[s][e][0]['geom'].length assert l == G_simplified[52][52][0]['geom'].length # and that the new parallel edge is correct l = 0 for s, e in [(45, 56), (56, 30)]: l += primal_graph[s][e][0]['geom'].length assert l == G_simplified[45][30][0]['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 a looping component (all nodes == degree 2) suspends off a node with degree > 2 G_lollipop = nx.MultiGraph() nodes = [ (1, {'x': 400, 'y': 750}), (2, {'x': 400, 'y': 650}), (3, {'x': 500, 'y': 550}), (4, {'x': 400, 'y': 450}), (5, {'x': 300, 'y': 550}) ] 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][0]['geom'] = geometry.LineString(G_lollipop[2][5][0]['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][0]['geom'].coords[-1] == G_lollipop_simpl[2][2][0]['geom'].coords[0] # start and end point of lollipop should match assert G_lollipop_simpl[2][2][0]['geom'].coords[0] == G_lollipop_simpl[2][2][0]['geom'].coords[-1] # manually check welded geom assert G_lollipop_simpl[2][2][0]['geom'].wkt == 'LINESTRING (400 650, 500 550, 400 450, 300 550, 400 650)' # stairway test - where overlapping edges (all nodes == degree 2) have overlapping coordinates in 2D space G_stairway = nx.MultiGraph() nodes = [ ('1-down', {'x': 400, 'y': 750}), ('2-down', {'x': 400, 'y': 650}), ('3-down', {'x': 500, 'y': 550}), ('4-down', {'x': 400, 'y': 450}), ('5-down', {'x': 300, 'y': 550}), ('2-mid', {'x': 400, 'y': 650}), ('3-mid', {'x': 500, 'y': 550}), ('4-mid', {'x': 400, 'y': 450}), ('5-mid', {'x': 300, 'y': 550}), ('2-up', {'x': 400, 'y': 650}), ('1-up', {'x': 400, 'y': 750}) ] G_stairway.add_nodes_from(nodes) G_stairway.add_nodes_from(nodes) edges = [ ('1-down', '2-down'), ('2-down', '3-down'), ('3-down', '4-down'), ('4-down', '5-down'), ('5-down', '2-mid'), ('2-mid', '3-mid'), ('3-mid', '4-mid'), ('4-mid', '5-mid'), ('5-mid', '2-up'), ('2-up', '1-up') ] G_stairway.add_edges_from(edges) # add edge geoms G_stairway = graphs.nX_simple_geoms(G_stairway) # flip some geometry G_stairway['5-down']['2-mid'][0]['geom'] = geometry.LineString( G_stairway['5-down']['2-mid'][0]['geom'].coords[::-1]) # simplify G_stairway_simpl = graphs.nX_remove_filler_nodes(G_stairway) # check integrity of graph assert nx.number_of_nodes(G_stairway_simpl) == 2 assert nx.number_of_edges(G_stairway_simpl) == 1 # geoms should still be same cumulative length before_len = 0 for s, e, d in G_stairway.edges(data=True): before_len += d['geom'].length after_len = 0 for s, e, d in G_stairway_simpl.edges(data=True): after_len += d['geom'].length assert before_len == after_len assert G_stairway_simpl['1-down']['1-up'][0]['geom'].wkt == \ 'LINESTRING (400 750, 400 650, 500 550, 400 450, 300 550, 400 650, 500 550, 400 450, 300 550, 400 650, 400 750)' # check that missing geoms throw an error G_k = G_messy.copy() for i, (s, e, k) in enumerate(G_k.edges(keys=True)): if i % 2 == 0: del G_k[s][e][k]['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, k in G_k.edges(keys=True): G_k[s][e][k]['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, k in G_corr.edges(keys=True): geom = G_corr[s][e][k]['geom'] start = list(geom.coords[0]) end = list(geom.coords[1]) # corrupt a point start[0] = start[0] - 1 G_corr[s][e][k]['geom'] = geometry.LineString([start, end]) with pytest.raises(ValueError): graphs.nX_remove_filler_nodes(G_corr)