diff --git a/examples/development.py b/examples/development.py index 4db5508..9fb75cb 100644 --- a/examples/development.py +++ b/examples/development.py @@ -2,6 +2,14 @@ import growbikenet as gbn -edges_ranked = gbn.growbikenet("Oelde", - seed_point_type='file', - import_files={'street_network':"./tests/test_data/oelde_street_network.gpkg", 'seed_points':"./tests/test_data/oelde_seed_points.gpkg"}) \ No newline at end of file +gbn.settings.import_data_impact = 9 +gbn.settings.import_data_trip_point_balance = 0.4 # alpha +edges_ranked = gbn.growbikenet("Turin", + export_data = True, + import_files={ + 'street_network':"./tests/test_data/turin_street_network.gpkg", + 'point_data':"./tests/test_data/turin_crashes.gpkg", + 'trip_data':"./tests/test_data/turin_trips.csv", + }, + ) +print(len(edges_ranked)) \ No newline at end of file diff --git a/growbikenet/constants.py b/growbikenet/constants.py index d8a97f0..6a346ec 100644 --- a/growbikenet/constants.py +++ b/growbikenet/constants.py @@ -2,24 +2,25 @@ PBI_CUSTOM_FILTER : list[str] Custom filter for protected bicycle infrastructure (pbi) +EXISTING_NETWORK_MINIMUM_COMPONENT_LENGTH : int + Minimum length a bike network component needs to have for seed points to snap, in meters +GRID_SPACING_TRIANGULATE : int + Grid spacing in meters for grid triangulation that ensures that any point in the city is always within buffer distance b=500m of the network (if seed points snap perfectly). +GRID_SPACING_QUADRANGULATE : int + Grid spacing in meters for quadrangulation that ensures that any point in the city is always within buffer distance b=500m of the network (if seed points snap perfectly). +GRID_SPACING_TRIANGLE : int + Grid spacing in meters for triangle grid that ensures that any point in the city is always within buffer distance b=500m of the network (if seed points snap perfectly). + _PRESET_TAGS : dict Pre-defined tags to select tags as seed points _PHI_LIMITS : list[float] Two orientation order limits between street networks with: 1) negligible grid elements, 2) some grid elements, 3) grid. We aimed to use the tercile limits from the paper [3]_ (Fig 2), but the values here are lower for unknown reasons, also with the unweighted version. Also, it was aimed to have Barcelona in the grid category. For these reasons, the limits were lowered. -EXISTING_NETWORK_MINIMUM_COMPONENT_LENGTH : int - Minimum length a bike network component needs to have for seed points to snap, in meters _SEED_POINT_SNAP_DISTANCE_FACTOR : float Factor to multiply seed_point_grid_spacing with, to determine auto value of seed_point_snap_distance _EXISTING_NETWORK_SPACING_FACTOR : float Factor to multiply seed_point_grid_spacing with, to determine auto value of existing_network_spacing -GRID_SPACING_TRIANGULATE : int - Grid spacing in meters for grid triangulation that ensures that any point in the city is always within buffer distance b=500m of the network (if seed points snap perfectly). -GRID_SPACING_QUADRANGULATE : int - Grid spacing in meters for quadrangulation that ensures that any point in the city is always within buffer distance b=500m of the network (if seed points snap perfectly). -GRID_SPACING_TRIANGLE : int - Grid spacing in meters for triangle grid that ensures that any point in the city is always within buffer distance b=500m of the network (if seed points snap perfectly). _BUFFER_SEED_POINTS_EXNW_FACTOR : float Factor to multiply existing_network_spacing with, to determine which previously determined seed points (grid or rail) to drop that are too close to the extra existing network points _BEARING_BINS : int @@ -40,10 +41,16 @@ ] # Populate ox.settings.useful_tags_way to make application of custom filter possible import osmnx as ox -for custom_tag in ["cycleway", "bicycle", "cycleway:right", "cycleway:left", "cycleway:both", "cyclestreet"]: +for custom_tag in ["highway", "cycleway", "bicycle", "cycleway:right", "cycleway:left", "cycleway:both", "cyclestreet"]: # This list should contain all tags used in any custom filters if custom_tag not in ox.settings.useful_tags_way: ox.settings.useful_tags_way.extend(custom_tag) +EXISTING_NETWORK_MINIMUM_COMPONENT_LENGTH = 100 + +GRID_SPACING_TRIANGULATE = 1707 # a=2b/(2-sqrt(2)) +GRID_SPACING_QUADRANGULATE = 1000 # a=2b +GRID_SPACING_TRIANGLE = 1154 # h/2=b=a*sqrt(3)/4 -> a=4b/sqrt(3) + _PRESET_TAGS = { "rail": {"railway": ["station", "halt"]}, "school": {"amenity": ["kindergarten", "school", "college", "university"]}, @@ -52,16 +59,10 @@ _PHI_LIMITS = [0.02, 0.08] # Tercile limits in the paper: 0.033, 0.161 -EXISTING_NETWORK_MINIMUM_COMPONENT_LENGTH = 100 - _SEED_POINT_SNAP_DISTANCE_FACTOR = 0.25 _EXISTING_NETWORK_SPACING_FACTOR = 0.5 -GRID_SPACING_TRIANGULATE = 1707 # a=2b/(2-sqrt(2)) -GRID_SPACING_QUADRANGULATE = 1000 # a=2b -GRID_SPACING_TRIANGLE = 1154 # h/2=b=a*sqrt(3)/4 -> a=4b/sqrt(3) - _BUFFER_SEED_POINTS_EXNW_FACTOR = 0.5 -_BEARING_BINS = 72 +_BEARING_BINS = 72 \ No newline at end of file diff --git a/growbikenet/functions.py b/growbikenet/functions.py index 32d5567..c0de229 100644 --- a/growbikenet/functions.py +++ b/growbikenet/functions.py @@ -163,7 +163,7 @@ def _resolve_auto_parameters( phi, import_files, ): - """Resolve auto parameters and parameter inconsistencies + """Resolve auto parameters, their inconsistencies, and settings Parameters ---------- @@ -232,10 +232,16 @@ def _resolve_auto_parameters( if existing_network_spacing == 'auto': existing_network_spacing = int(np.ceil(seed_point_grid_spacing*constants._EXISTING_NETWORK_SPACING_FACTOR)) + # Set import data balance to 0 or 1 if only one data set is imported + if import_files['point_data'] is not None and import_files['trip_data'] is None: + settings.import_data_trip_point_balance = 0 + elif import_files['trip_data'] is not None and import_files['point_data'] is None: + settings.import_data_trip_point_balance = 1 + return seed_point_type, seed_point_grid_spacing, seed_point_linking, existing_network_spacing -def add_trip_data_to_net(trips, A, crs_projected, matching_distance=500): +def add_trip_data_to_net(trips, A, crs_projected=settings.crs_projected, matching_distance=settings.import_trip_data_snap_distance): """Match trip data to network edges First, match origin and destination points given in trips to the nodes. Only consider trips where both origins and nodes are matched within matching_distance. Then, for each trip, find the shortest path over the edges from matched origin node to matched destination node, and add 1 (or optionally "num" if column provided in trips) to the affected edges. @@ -243,18 +249,18 @@ def add_trip_data_to_net(trips, A, crs_projected, matching_distance=500): Parameters ---------- trips : pandas DataFrame - A df of unprojected origin-destination coordinates (columns: o_lat, o_lon, d_lat, d_lon), with each row encoding a trip. Optional with a column "num" containing an integer. This could be (number of) trip events. If "num" column is not provided, assumes 1 per trip. + A df of unprojected origin-destination coordinates (columns: o_lat, o_lon, d_lat, d_lon), with each row encoding a trip, in unprojected crs EPSG:4326. Optional with a column "num" containing an integer. This could be (number of) trip events. If "num" column is not provided, assumes 1 per trip. A: networkx.graph Graph created from triangulation edge list crs_projected : str Coordinate reference system that is used to project spatial data. matching_distance : int - Matching distance in meters + Matching distance in meters. Set via settings.import_trip_data_snap_distance. Returns ------- graph_with_data : networkx.graph - The same graph created from triagulation edges list, but with a new edge attribute "num_trips" populated with the summed up "num" values of all trips where both origins and destinations could be matched to the closest network nodes within matching_distance. + The same graph created from triangulation edges list, but with a new edge attribute "num_trips" populated with the summed up "num" values of all trips where both origins and destinations could be matched to the closest network nodes within matching_distance. """ graph_with_data = A.copy() @@ -291,7 +297,6 @@ def add_trip_data_to_net(trips, A, crs_projected, matching_distance=500): if o_node == d_node or o_distance > matching_distance or d_distance > matching_distance: continue - # Get shortest path between the snapped orgin and destination nodes path = nx.shortest_path(graph_with_data, source=o_node, target=d_node, weight="distance") path_edges = list(zip(path[:-1], path[1:])) @@ -312,7 +317,7 @@ def add_trip_data_to_net(trips, A, crs_projected, matching_distance=500): return graph_with_data -def add_point_data_to_net(points, edges, crs_projected, matching_distance=500): +def add_point_data_to_net(points, edges, crs_projected=settings.crs_projected, matching_distance=settings.import_point_data_snap_distance): """Match point data to network edges Parameters @@ -321,10 +326,10 @@ def add_point_data_to_net(points, edges, crs_projected, matching_distance=500): A gdf of unprojected point geometries, optional having a column "num" containing an integer. This could be (number of) point events like crashes or citizen feedback to improve bike infrastructure. If "num" column is not provided, assumes 1 per point. edges : geopandas.geodataframe.GeoDataFrame A gdf of projected spatial network edges. This is the routed network of seed points. - matching_distance : int - Matching distance in meters crs_projected : str Coordinate reference system that is used to project osm data. + matching_distance : int + Matching distance in meters. Set via settings.import_point_data_snap_distance. Returns ------- @@ -1329,4 +1334,40 @@ def orientation_order(g_undir): Hg = 1.386 Hmax = 3.584 phi = 1 - ((Hw-Hg)/(Hmax-Hg))**2 - return phi \ No newline at end of file + return phi + +def _get_weighted_distances(B, num_types): + """Get weighted distances by edge attribute num_types + + The calculation follows [1]_, only without +1 in the numerator and with a small epsilon to prevent division by zero. + + Parameters + ---------- + B : networkx.classes.multigraph.MultiGraph + The routed, grown bicycle network graph, where edges have the attribute num_types. The numerical attribute "distance" must exist for all edges. + num_types : str + Name of the attribute to weight the distances + + Returns + ------- + dist_weighted_by_types_dict : dict + Dictionary where keys are the edges (tuples of node ids), and values are the weighted distances following [1]_ + + References + ---------- + .. [1] P. Folco, L. Gauvin, M. Tizzoni, M. Szell, "Data-driven micromobility network planning for demand and safety", Environment and planning B: Urban analytics and city science 50(8), 2087-2102 (2023) + """ + + num_types_dict = nx.get_edge_attributes(B, num_types) + dist_dict = nx.get_edge_attributes(B, "distance") + num_types_per_km_dict = {} + + for k,d in dist_dict.items(): + num_types_per_km_dict[k] = 1000*num_types_dict[k]/d + max_n = max(num_types_per_km_dict.values())+1e-10 + + dist_weighted_by_types_dict = {} + for k,d in dist_dict.items(): + dist_weighted_by_types_dict[k] = d/(1+settings.import_data_impact*(num_types_per_km_dict[k]/max_n)) + + return dist_weighted_by_types_dict \ No newline at end of file diff --git a/growbikenet/growbikenet.py b/growbikenet/growbikenet.py index 9aa71e5..f7e7373 100644 --- a/growbikenet/growbikenet.py +++ b/growbikenet/growbikenet.py @@ -35,6 +35,7 @@ add_point_data_to_net, add_trip_data_to_net, slugify, + _get_weighted_distances, ) from growbikenet.visualization import create_plots @@ -56,7 +57,7 @@ def growbikenet( ): """Creates a list of urban street network edges ordered by a ranking method. - The edges form a subnetwork of a city's street network, interpreted as a growing bicycle network following [1]_. By default, growth is from scratch, but the existing bicycle network can also be used as a starting point[2]_. The original paper [1]_ uses minimum weight triangulation, but Delaunay triangulation is implemented much faster and in practice gives identical results. Triangulation and metrics (betweenness, closeness) are calculated for the unrouted, abstract network for which egde lengths are taken from the routed network. + The edges form a subnetwork of a city's street network, interpreted as a growing bicycle network following [1]_. By default, growth is from scratch, but the existing bicycle network can also be used as a starting point [2]_. The original paper [1]_ uses minimum weight triangulation, but Delaunay triangulation is implemented much faster and in practice gives identical results. Triangulation and metrics (betweenness, closeness) are calculated for the unrouted, abstract network for which egde lengths are taken from the routed network. Parameters ---------- @@ -109,16 +110,16 @@ def growbikenet( "seed_points" : str | None, default None If not set to None, the seed points is loaded from this file. Must be a gpkg file in unprojected crs EPSG:4326 containing only point objects. For example, "./tests/test_data/oelde_seed_points.shp". seed_point_type must be set to 'file'. "point_data" : str | None, default None - If not set to None, an additional data set of points will be loaded from this file, representing point events like crashes or citizen feedback to improve bike infrastructure. Must be a gpkg file in unprojected crs EPSG:4326 containing only point objects, optionally with an int "num" column that encodes the number of point events. To be used in future versions of growbikenet. + If not set to None, an additional data set of points will be loaded from this file, representing point events like traffic crashes or citizen feedback to improve bike infrastructure. Must be a gpkg file in unprojected crs EPSG:4326 containing only point objects, optionally with an int "num" column that encodes the number of point events. The data set is used to re-prioritize the ranking of the network links, controlled with settings.import_data_impact and settings.import_data_trip_point_balance, following [2]_. "trip_data" : str | None, default None - If not set to None, an additional data set of trips will be loaded from this file, representing trip events for prioritizing bike infrastructure growth. Must be a csv file in unprojected crs EPSG:4326 containing the following fields: o_lat, o_lon, d_lat, d_lon. Optionally there can be an int "num" field that encodes the number of trips between each origin and destination. To be used in future versions of growbikenet. + If not set to None, an additional data set of trips will be loaded from this file, representing trip events for prioritizing bike infrastructure growth. Must be a csv file in unprojected crs EPSG:4326 containing the following fields: o_lat, o_lon, d_lat, d_lon. Optionally there can be an int "num" field that encodes the number of trips between each origin and destination. The data set is used to re-prioritize the ranking of the network links, controlled with settings.import_data_impact and settings.import_data_trip_point_balance, following [2]_. seed_point_tags : None | dict[str, bool | str | list[str]], default None If not None, must be a geocodable seed_point_tags, see [4]_, and seed_point_type must be set to 'tags'. For example, seed_point_tags={"railway": ["station", "halt"]} retrieves exactly the same as seed_point_type='rail'. Returns ------- edges_ranked : geopandas.geodataframe.GeoDataFrame - ordered geodataframe of all edges in street network + Geodataframe of all edges in street network ordered by the ranking method. Examples -------- @@ -171,6 +172,26 @@ def growbikenet( print(ranking + " | " + seed_point_type + " | " + ("from existing bike network " if existing_network_spacing else "from scratch")) print("----------------------------------------------╮") + + ### Import data files + num_data_files = int(bool(import_files['point_data'])) + int(bool(import_files['trip_data'])) + point_data = None + trip_data = None + if num_data_files: + progress_bar = tqdm( + desc="{:<23}".format("Importing data files"), + total=num_data_files, + unit="file", + bar_format='{l_bar}{bar:16}{r_bar}', + ) + if import_files['point_data']: + point_data = gpd.read_file(settings.import_path+import_files['point_data']) + progress_bar.update(1) + if import_files['trip_data']: + trip_data = pd.read_csv(settings.import_path+import_files['trip_data']) + progress_bar.update(1) + progress_bar.close() + if import_files['street_network'] is not None: ### Import and preprocess data from file city_boundary_exists = True @@ -209,7 +230,6 @@ def growbikenet( progress_bar.update(1) progress_bar.close() - # Now that the graph is ready, decide auto values ox.bearing.add_edge_bearings(g_undir) phi = orientation_order(g_undir) @@ -310,12 +330,9 @@ def growbikenet( # Map each unrouted edge to a merged geometry of corresponding osmnx edges (routed on g_undir) grown_bikenet_edges_abstract = add_path_to_df(grown_bikenet_edges_abstract, edges, g_undir) progress_bar.update(1) - grown_bikenet_edges = create_gdf_with_geoms(grown_bikenet_edges_abstract, edges) progress_bar.update(1) - # Apply add_point_data_to_net() from here - # Add distances between source and target from geometry grown_bikenet_edges["dist"] = grown_bikenet_edges["geometry"].length @@ -325,6 +342,11 @@ def growbikenet( dist_list = grown_bikenet_edges["dist"] dist_dict = dict(zip(edge_list, dist_list)) geom_dict = dict(zip(edge_list, grown_bikenet_edges["geometry"].tolist())) + # Add point data to edges + if point_data is not None: + grown_bikenet_edges = add_point_data_to_net(point_data, grown_bikenet_edges) + num_points_list = grown_bikenet_edges["num_points"] + num_points_dict = dict(zip(edge_list, num_points_list)) # Make graph object from edge list B = nx.Graph() # B like bike network @@ -334,10 +356,35 @@ def growbikenet( B.add_edges_from(edge_list) nx.set_edge_attributes(B, dist_dict, "distance") nx.set_edge_attributes(B, geom_dict, "geometry") + if point_data is not None: + nx.set_edge_attributes(B, num_points_dict, "num_points") B.graph["crs"] = settings.crs_projected # Needed for add_trip_data_to_net() - # Add add_trip_data_to_net() from here - + if num_data_files: + # Add trip data to edges + if trip_data is not None: + B = add_trip_data_to_net(trip_data, B) + + # Compute weighted distances + if trip_data is not None: + dist_weighted_by_trips_dict = _get_weighted_distances(B, "num_trips") # d_trip in [2]_ + if point_data is not None: + dist_weighted_by_points_dict = _get_weighted_distances(B, "num_points") # d_crash in [2]_ + + # Combine them + dist_weighted_dict = {} + if trip_data is not None and point_data is not None: + for k in dist_weighted_by_trips_dict: + dist_weighted_dict[k] = settings.import_data_trip_point_balance*dist_weighted_by_trips_dict[k] + (1-settings.import_data_trip_point_balance)*dist_weighted_by_points_dict[k] # d_{W} in [2]_ + elif trip_data is not None and point_data is None: + dist_weighted_dict = dist_weighted_by_trips_dict + elif trip_data is None and point_data is not None: + dist_weighted_dict = dist_weighted_by_points_dict + + metric_weight = "distance_weighted" + nx.set_edge_attributes(B, dist_weighted_dict, metric_weight) + else: + metric_weight = "distance" progress_bar.update(1) progress_bar.close() @@ -354,12 +401,12 @@ def growbikenet( if ranking == "betweenness_centrality": # Add betweenness attributes to edges bc_values = nx.edge_betweenness_centrality( - B, weight="distance", normalized=True + B, weight=metric_weight, normalized=True ) nx.set_edge_attributes(B, bc_values, name="betweenness_centrality") elif ranking == "closeness_centrality": # Add closeness attributes to nodes and edges - cc_values_nodes = nx.closeness_centrality(B, distance="distance") + cc_values_nodes = nx.closeness_centrality(B, distance=metric_weight) nx.set_node_attributes(B, cc_values_nodes, name="closeness_centrality") cc_values = node_to_edge_attributes(cc_values_nodes, B.edges) nx.set_edge_attributes(B, cc_values, name="closeness_centrality") diff --git a/growbikenet/settings.py b/growbikenet/settings.py index c6ba5f2..a9b95f4 100644 --- a/growbikenet/settings.py +++ b/growbikenet/settings.py @@ -1,32 +1,43 @@ """Global settings for growbikenet that can be configured by the user. -export_path : dict(str | Path) - Paths to results, plots, and video folders to save data, plots, and videos. -import_path : str | Path - Path to import files (as defined in growbikenet's import_files parameter). crs_projected : str, default '3857' EPSG code of the coordinate reference system that is used to project osm data. Default is '3857' (WGS 84 / Pseudo-Mercator). If this web mercator projection is not needed, then for Europe '3035' (LAEA) and globally '54035' (Equal Earth) or '54030' (Robinson) is better. +export_path : dict(str) + Paths to results and plots folders to save data and plots. export_file_format : str ('geojson' | 'gpkg'), default 'gpkg' File format for the data export, relevant if export_data set to True. If exporting as geojson, generates extra files for seed points and city boundary. If exporting as gkpg, these are added all in one file as extra layers. +import_data_impact : float, default 9 + Impact of imported trip or point data on results. Must be non-negative. +import_data_trip_point_balance : float, default 0.5 + Impact of imported trip data versus point data on results. Must be between 0 and 1, where 0 means no trip impact and full point impact, 1 means full trip impact and no point impact, and 0.5 means balanced impact of both. If only the trip data is imported, this variable is treated as 1; if only the point data is imported, this variable is treated as 0 - meaning in such a case the data impact is controlled only by settings.import_data_impact. +import_path : str + Path to import files (as defined in growbikenet's import_files parameter). +import_point_data_snap_distance : int, default 500 + Maximum distance between point data and network links for snapping, in meters. +import_trip_data_snap_distance : int, default 500 + Maximum distance between trip data and network links for snapping, in meters. +random_seed : int + Random number generator seed for reproducibility seed_point_snap_distance : 'auto' | int, default 'auto' Maximum distance between raw seed points and osm nodes for snapping, in meters. Auto-value is ceil(seed_point_grid_spacing*constants._SEED_POINT_SNAP_DISTANCE_FACTOR). If integer, must be positive. -random_seed : int - Random number generator seed for reproducibility viz : dict Dictionary of visualization settings """ +crs_projected = '3857' export_path = { "results":"./results/", "plots":"./results/plots/", - "videos":"./results/videos/", } -import_path = "./" -crs_projected = '3857' export_file_format = 'gpkg' -seed_point_snap_distance = 'auto' +import_data_impact = 9 +import_data_trip_point_balance = 0.5 +import_path = "./" +import_point_data_snap_distance = 500 +import_trip_data_snap_distance = 500 random_seed = 42 +seed_point_snap_distance = 'auto' # Viz/plot settings viz = { @@ -48,4 +59,4 @@ "markersize": 60, }, "dpi": 150, -} +} \ No newline at end of file diff --git a/tests/test_data/turin_B.pickle b/tests/test_data/turin_B.pickle new file mode 100644 index 0000000..c9fb976 Binary files /dev/null and b/tests/test_data/turin_B.pickle differ diff --git a/tests/test_data/turin_alpha0_4.gpkg b/tests/test_data/turin_alpha0_4.gpkg new file mode 100644 index 0000000..e1f6e36 Binary files /dev/null and b/tests/test_data/turin_alpha0_4.gpkg differ diff --git a/tests/test_data/turin_alpha1.gpkg b/tests/test_data/turin_alpha1.gpkg new file mode 100644 index 0000000..5cbde23 Binary files /dev/null and b/tests/test_data/turin_alpha1.gpkg differ diff --git a/tests/test_data/turin_d_points.pickle b/tests/test_data/turin_d_points.pickle new file mode 100644 index 0000000..d159c85 Binary files /dev/null and b/tests/test_data/turin_d_points.pickle differ diff --git a/tests/test_data/turin_d_trips.pickle b/tests/test_data/turin_d_trips.pickle new file mode 100644 index 0000000..a5ba11a Binary files /dev/null and b/tests/test_data/turin_d_trips.pickle differ diff --git a/tests/test_data/turin_street_network.gpkg b/tests/test_data/turin_street_network.gpkg new file mode 100644 index 0000000..6026b3f Binary files /dev/null and b/tests/test_data/turin_street_network.gpkg differ diff --git a/tests/test_functions.py b/tests/test_functions.py index 1c965ca..33b4fc8 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -9,6 +9,7 @@ import ast from pandas.testing import assert_frame_equal import shapely +import pickle from growbikenet.functions import ( get_principal_bearing, _get_grid_seed_points, @@ -19,6 +20,7 @@ add_point_data_to_net, add_trip_data_to_net, create_gdf_with_geoms, + _get_weighted_distances, ) @@ -198,7 +200,6 @@ def routed_edges_crashes(): ] } graph = gpd.GeoDataFrame(g, geometry="geometry", crs = "EPSG:3857") - return graph @pytest.fixture @@ -225,7 +226,6 @@ def crashes_data(): 'num': [1,1,2,3,1,1,1,3,5,1,1,1,1,2,1] } crashes = gpd.GeoDataFrame(c, geometry="geometry", crs = "EPSG:4326") - return crashes @pytest.fixture @@ -272,7 +272,6 @@ def routed_edges_with_crashes_data(): ] } graph = gpd.GeoDataFrame(g, geometry="geometry", crs = "EPSG:3857") - return graph def test_add_point_data_to_net_case_success_simple(routed_edges_crashes, crashes_data, routed_edges_with_crashes_data): @@ -316,7 +315,6 @@ def test_add_point_data_to_net_case_success_turin(turin_accidents_data, turin_ro Z = (1150,1100) - @pytest.fixture def triangulation_graph_trips(): # Nodes of the graph @@ -377,7 +375,6 @@ def triangulation_graph_trips(): nx.set_edge_attributes(graph, edges) graph.graph["crs"] = '3857' - return graph @pytest.fixture @@ -409,7 +406,6 @@ def trips_data(): } trips = pd.DataFrame(t) - return trips @pytest.fixture @@ -472,7 +468,6 @@ def triangulation_graph_with_trips_data(): nx.set_edge_attributes(graph, edges) graph.graph["crs"] = '3857' - return graph def test_add_trip_data_to_net_case_success_simple(trips_data, triangulation_graph_trips, triangulation_graph_with_trips_data): @@ -482,7 +477,6 @@ def test_add_trip_data_to_net_case_success_simple(trips_data, triangulation_grap '3857' ) expected = triangulation_graph_with_trips_data - assert result.nodes == expected.nodes, "Nodes are not the same" assert result.adj == expected.adj, "Edges are not the same" assert result.graph == expected.graph, "Graphs are not the same" @@ -506,7 +500,6 @@ def turin_triangulation_graph_trips(): nx.set_edge_attributes(graph, edges) graph.graph = graph_att - return graph @pytest.fixture @@ -532,7 +525,6 @@ def turin_triangulation_graph_with_trips_data(): nx.set_edge_attributes(graph, edges) graph.graph = graph_att - return graph def test_add_trip_data_to_net_case_success_turin(turin_trips_data, turin_triangulation_graph_trips, turin_triangulation_graph_with_trips_data): @@ -542,13 +534,11 @@ def test_add_trip_data_to_net_case_success_turin(turin_trips_data, turin_triangu '3857' ) expected = turin_triangulation_graph_with_trips_data - assert result.nodes == expected.nodes, "Nodes are not the same" assert result.adj == expected.adj, "Edges are not the same" assert result.graph == expected.graph, "Graphs are not the same" - @pytest.fixture def routed_triangulation(): df = pd.DataFrame( @@ -565,7 +555,6 @@ def routed_triangulation(): ] } ) - return df @pytest.fixture @@ -601,9 +590,7 @@ def network_edges(): crs = '3857', geometry = [AB, BC, CD, DE, EF, FG, BH, HF, FI, IJ, GJ] ) - gdf.set_index(['u','v'], inplace = True) - return gdf @pytest.fixture @@ -641,7 +628,6 @@ def routed_triangulation_with_geoms(): crs = '3857', geometry = 'geometry' ) - return gdf def test_create_gdf_with_geoms_case_success_simple(routed_triangulation, network_edges, routed_triangulation_with_geoms): @@ -662,7 +648,6 @@ def turin_routed_triangulation(): df['pair'] = df['pair'].apply(ast.literal_eval) df['path_nodes'] = df['path_nodes'].apply(ast.literal_eval) df['path_edges'] = df['path_edges'].apply(ast.literal_eval) - return df @pytest.fixture @@ -671,7 +656,6 @@ def turin_network_edges(): "./tests/test_data/turin_edges.gpkg" ) gdf.set_index(['u','v'], inplace=True) - return gdf @pytest.fixture @@ -682,7 +666,6 @@ def turin_routed_triangulation_with_geoms(): gdf['pair'] = gdf['pair'].apply(ast.literal_eval) gdf['path_nodes'] = gdf['path_nodes'].apply(ast.literal_eval) gdf['path_edges'] = gdf['path_edges'].apply(ast.literal_eval) - return gdf def test_create_gdf_with_geoms_case_success_turin(turin_routed_triangulation, turin_network_edges, turin_routed_triangulation_with_geoms): @@ -693,3 +676,22 @@ def test_create_gdf_with_geoms_case_success_turin(turin_routed_triangulation, tu ), turin_routed_triangulation_with_geoms ) + +@pytest.fixture +def turin_B(): + B = pickle.load(open('./tests/test_data/turin_B.pickle', 'rb')) + return B + +@pytest.fixture +def turin_d_points(): + d = pickle.load(open('./tests/test_data/turin_d_points.pickle', 'rb')) + return d + +@pytest.fixture +def turin_d_trips(): + d = pickle.load(open('./tests/test_data/turin_d_trips.pickle', 'rb')) + return d + +def test__get_weighted_distances(turin_B, turin_d_points, turin_d_trips): + assert turin_d_points == _get_weighted_distances(turin_B, "num_points") + assert turin_d_trips == _get_weighted_distances(turin_B, "num_trips") diff --git a/tests/test_main.py b/tests/test_main.py index 32f75c1..4e1450c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,7 +1,8 @@ import pytest import geopandas as gpd import osmnx as ox -from growbikenet.growbikenet import growbikenet +import growbikenet as gbn +from pandas.testing import assert_frame_equal @pytest.fixture def validation_gdf_oelde(): @@ -18,23 +19,23 @@ def validation_gdf_asti(): gdf = gpd.read_file("./tests/test_data/asti_growbikenet.gpkg", layer='Grown bike network') return gdf -def test_growbikenet_case_success_online(validation_gdf_oelde): - """Verify that the online version of growbikenet works as intended. - This test might brake whenever Oelde is changed too much on OSM! - """ - validation_gdf_oelde.equals( - growbikenet( - city_query="Oelde", - ranking="betweenness_centrality", - export_data=False, - ) - ) +# def test_growbikenet_case_success_online(validation_gdf_oelde): +# """Verify that the online version of growbikenet works as intended. +# This test might brake whenever Oelde is changed too much on OSM! +# """ +# validation_gdf_oelde.equals( +# gbn.growbikenet( +# city_query="Oelde", +# ranking="betweenness_centrality", +# export_data=False, +# ) +# ) def test_growbikenet_case_success_offline1(validation_gdf_oelde): """Verify that the offline version of growbikenet works as intended. """ validation_gdf_oelde.equals( - growbikenet( + gbn.growbikenet( city_query="Oelde", ranking="betweenness_centrality", export_data=False, @@ -46,7 +47,7 @@ def test_growbikenet_case_success_offline2(validation_gdf_athens): """Verify that the offline version of growbikenet works as intended, with existing bike network. """ validation_gdf_athens.equals( - growbikenet( + gbn.growbikenet( city_query="Municipality of Athens", ranking="betweenness_centrality", export_data=False, @@ -70,7 +71,7 @@ def test_growbikenet_case_success_offline3(validation_gdf_asti): >>> ox.io.save_graph_geopackage(g, "asti_street_network.gpkg") """ validation_gdf_asti.equals( - growbikenet( + gbn.growbikenet( city_query="Asti 16", # The 16 is needed to select the city, not the municipality (it doesn't matter here as the network is imported anyway, but it is important to know for exporting the street network asti_street_network.gpkg in the first place) ranking="betweenness_centrality", export_data=False, @@ -78,32 +79,97 @@ def test_growbikenet_case_success_offline3(validation_gdf_asti): ) ) -def test_growbikenet_case_fail_rail1(): - """Verify that when there are too few rail stations (Oelde), a - RunTimeError is thrown. +# def test_growbikenet_case_fail_rail1(): +# """Verify that when there are too few rail stations (Oelde), a +# RunTimeError is thrown. +# """ +# with pytest.raises(Exception): +# gbn.growbikenet( +# "Oelde", +# seed_point_type='rail', +# ) + +# def test_growbikenet_case_fail_rail2(): +# """Verify that in the absence of rail stations (Andorra), an +# osmnx._errors.InsufficientResponseError is thrown. +# """ +# with pytest.raises(ox._errors.InsufficientResponseError): +# gbn.growbikenet( +# "Andorra", +# seed_point_type='rail', +# ) + +# def test_growbikenet_case_fail_existing_network(): +# """Verify that in the absence of an existing bike network (Hallettsville), an +# osmnx._errors.InsufficientResponseError is thrown. +# """ +# with pytest.raises(ox._errors.InsufficientResponseError): +# gbn.growbikenet( +# "Hallettsville", +# existing_network_spacing=500, +# ) + + + +# Tests with settings + + +@pytest.fixture +def validation_gdf_turin_alpha1(): + gdf = gpd.read_file("./tests/test_data/turin_alpha1.gpkg", layer='Grown bike network') + return gdf + +@pytest.fixture +def validation_gdf_turin_alpha0_4(): + gdf = gpd.read_file("./tests/test_data/turin_alpha0_4.gpkg", layer='Grown bike network') + return gdf + +def test_growbikenet_case_success_offline_import_data1(validation_gdf_turin_alpha1): + """Verify that the offline version of growbikenet works as intended with import data sets. """ - with pytest.raises(Exception): - growbikenet( - "Oelde", - seed_point_type='rail', + gbn.settings.import_data_impact = 9 + gbn.settings.import_data_trip_point_balance = 1 # alpha + + validation_gdf_turin_alpha1.equals( + gbn.growbikenet( + "Turin", + import_files={ + 'street_network':"./tests/test_data/turin_street_network.gpkg", + 'point_data':"./tests/test_data/turin_crashes.gpkg", + 'trip_data':"./tests/test_data/turin_trips.csv", + }, ) + ) -def test_growbikenet_case_fail_rail2(): - """Verify that in the absence of rail stations (Andorra), an - osmnx._errors.InsufficientResponseError is thrown. +def test_growbikenet_case_success_offline_import_data2(validation_gdf_turin_alpha1): + """Verify that the offline version of growbikenet works as intended with import data sets. """ - with pytest.raises(ox._errors.InsufficientResponseError): - growbikenet( - "Andorra", - seed_point_type='rail', + gbn.settings.import_data_impact = 9 + gbn.settings.import_data_trip_point_balance = 1 # alpha + + validation_gdf_turin_alpha1.equals( + gbn.growbikenet( + "Turin", + import_files={ + 'street_network':"./tests/test_data/turin_street_network.gpkg", + 'trip_data':"./tests/test_data/turin_trips.csv", + }, ) + ) -def test_growbikenet_case_fail_existing_network(): - """Verify that in the absence of an existing bike network (Hallettsville), an - osmnx._errors.InsufficientResponseError is thrown. +def test_growbikenet_case_success_offline_import_data3(validation_gdf_turin_alpha0_4): + """Verify that the offline version of growbikenet works as intended with import data sets. """ - with pytest.raises(ox._errors.InsufficientResponseError): - growbikenet( - "Hallettsville", - existing_network_spacing=500, + gbn.settings.import_data_impact = 9 + gbn.settings.import_data_trip_point_balance = 0.4 # alpha + + validation_gdf_turin_alpha0_4.equals( + gbn.growbikenet( + "Turin", + import_files={ + 'street_network':"./tests/test_data/turin_street_network.gpkg", + 'point_data':"./tests/test_data/turin_crashes.gpkg", + 'trip_data':"./tests/test_data/turin_trips.csv", + }, ) + ) \ No newline at end of file