Exemple #1
0
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)
Exemple #2
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
Exemple #3
0
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
Exemple #4
0
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):
Exemple #5
0
def test_nX_wgs_to_utm():
    # check that node coordinates are correctly converted
    G_utm = mock.mock_graph()
    G_wgs = mock.mock_graph(wgs84_coords=True)
    G_converted = graphs.nX_wgs_to_utm(G_wgs)
    for n, d in G_utm.nodes(data=True):
        # rounding can be tricky
        assert np.allclose(d['x'], G_converted.nodes[n]['x'], atol=0.1, rtol=0)
        assert np.allclose(d['y'], G_converted.nodes[n]['y'], atol=0.1, rtol=0)

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

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

    G_converted = graphs.nX_wgs_to_utm(G_wgs)
    for s, e, 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']
Exemple #6
0
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]
Exemple #7
0
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)