diff --git a/LinkBikeNet_MVP.ipynb b/LinkBikeNet_MVP.ipynb index 41175b3..6395046 100644 --- a/LinkBikeNet_MVP.ipynb +++ b/LinkBikeNet_MVP.ipynb @@ -141,7 +141,7 @@ "closest_pairs = []\n", "for i in range(to_iterate):\n", " wcc = [H.subgraph(c).copy() for c in sorted(nx.connected_components(H), key=lambda c: sum([l[-1] for l in H.subgraph(c).copy().edges.data('length')]), reverse=True)]\n", - " pair = pair_between_nearest_components(wcc)\n", + " pair = pair_between_closest_components(wcc)\n", " closest_pairs.append(pair)\n", " H.add_edge(pair[0], pair[1], length=0)" ], diff --git a/linkbikenet/functions.py b/linkbikenet/functions.py index 79540ed..ec685fd 100644 --- a/linkbikenet/functions.py +++ b/linkbikenet/functions.py @@ -146,7 +146,7 @@ def pair_between_largest_components(wcc): return closest_pair -def pair_between_nearest_components(wcc): +def pair_between_largest_and_closest_components(wcc): """ Find the pair of nodes connecting the largest component to the geographically nearest remaining component. @@ -161,7 +161,6 @@ def pair_between_nearest_components(wcc): closest_pair : tuple The two nodes that should be connected """ - largest = wcc[0] # Build KD-tree for the largest component @@ -191,6 +190,52 @@ def pair_between_nearest_components(wcc): ) return closest_pair +def pair_between_closest_components(wcc): + """ + Find the closest pair of nodes belonging to two different connected + components. + + Parameters + ---------- + wcc : list of nx.Graph + Connected components sorted with the largest first. + + Returns + ------- + closest_pair : tuple + The two nodes that should be connected + """ + closest_pair = None + best_distance = np.inf + + for i in range(len(wcc) - 1): + G1 = wcc[i] + nodes1 = list(G1.nodes()) + coords1 = np.array([ + (G1.nodes[n]["x"], G1.nodes[n]["y"]) + for n in nodes1 + ]) + tree = cKDTree(coords1) + + for j in range(i + 1, len(wcc)): + G2 = wcc[j] + nodes2 = list(G2.nodes()) + coords2 = np.array([ + (G2.nodes[n]["x"], G2.nodes[n]["y"]) + for n in nodes2 + ]) + + distances, indices = tree.query(coords2) + k = np.argmin(distances) + if distances[k] < best_distance: + best_distance = distances[k] + closest_pair = ( + nodes1[indices[k]], + nodes2[k] + ) + + return closest_pair + def get_correct_edgetuples(edge_gdf, nodelist): """ helper function that maps a node list (output of nx.shortest_paths) diff --git a/linkbikenet/linkbikenet.py b/linkbikenet/linkbikenet.py index 2b673ba..4579c6a 100644 --- a/linkbikenet/linkbikenet.py +++ b/linkbikenet/linkbikenet.py @@ -20,7 +20,7 @@ def linkbikenet( city_name : str name of the city that the analysis should be performed on connection_strategy : str, default="largest - strategy to use for connecting between components. Default is "largest", second option is "closest" + strategy to use for connecting between components. Default is "largest", other options are "largest-closest" and "closest" proj_crs : str, default '3857' coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator) export_data : bool, optional, default True @@ -37,8 +37,8 @@ def linkbikenet( raise TypeError("city_name must be a string") if type(proj_crs) != str: raise TypeError("proj_crs must be a string") - if connection_strategy != "largest" and connection_strategy != "closest": - raise TypeError("connection_strategy must be 'largest' or 'closest'") + if connection_strategy != "largest" and connection_strategy != "largest-closest" and connection_strategy != "closest": + raise TypeError("connection_strategy must be 'largest', 'largest-closest' or 'closest'") if type(export_data) is not bool: raise TypeError("export_data must be a boolean") if export_file_format != "geojson" and export_file_format != "gpkg": @@ -99,11 +99,19 @@ def linkbikenet( closest_pairs.append(pair) H.add_edge(pair[0], pair[1], length=0) + elif connection_strategy == "largest-closest": + for i in range(to_iterate): + wcc = [H.subgraph(c).copy() for c in sorted(nx.connected_components(H), key=lambda c: sum( + [l[-1] for l in H.subgraph(c).copy().edges.data('length')]), reverse=True)] + pair = pair_between_largest_and_closest_components(wcc) + closest_pairs.append(pair) + H.add_edge(pair[0], pair[1], length=0) + elif connection_strategy == "closest": for i in range(to_iterate): wcc = [H.subgraph(c).copy() for c in sorted(nx.connected_components(H), key=lambda c: sum( [l[-1] for l in H.subgraph(c).copy().edges.data('length')]), reverse=True)] - pair = pair_between_nearest_components(wcc) + pair = pair_between_closest_components(wcc) closest_pairs.append(pair) H.add_edge(pair[0], pair[1], length=0) diff --git a/tests/test_functions.py b/tests/test_functions.py index db5fb08..74f6ce8 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -35,4 +35,21 @@ def create_validation_pair_nearest(): return pair def test_pair_between_nearest_components(create_test_components_nearest, create_validation_pair_nearest): - assert pair_between_nearest_components(create_test_components_nearest) == create_validation_pair_nearest + assert pair_between_largest_and_closest_components(create_test_components_nearest) == create_validation_pair_nearest + +@pytest.fixture +def create_test_components_closest(): + G = nx.Graph() + G.add_nodes_from([(1, {"x": 1, "y": 1}), (2, {"x": 2, "y": 2}), (3, {"x": 1, "y": 2}), (4, {"x": 30, "y": 30}), (5, {"x": 31, "y": 31}), (6, {"x": 10, "y": 10})]) + G.add_edges_from([(1, 2, {'length': 5}), (2, 3, {'length': 2}), (1, 3, {'length': 7}), (4, 5, {'length': 1})]) + wcc = [G.subgraph(c).copy() for c in sorted(nx.connected_components(G), key=lambda c: sum( + [l[-1] for l in G.subgraph(c).copy().edges.data('length')]), reverse=True)] + return wcc + +@pytest.fixture +def create_validation_pair_closest(): + pair = 2,6 + return pair + +def test_pair_between_closest_components(create_test_components_closest, create_validation_pair_closest): + assert pair_between_closest_components(create_test_components_closest) == create_validation_pair_closest