spa-l3-paris-emergency-routing

Status: done
Score: 0.000
Duration: 17:33
Cost: 3.92¢
Model: google/gemma-4-26b-a4b-it

Map (reference ← swipe → agent)

0:00
Need the dispatch coverage model rebuilt before Thursday's briefing. The eight historical emergency-call locations are in `incidents.csv` (columns: incident_id, latitude, longitude, label). Pull the driveable road network and all hospitals from OpenStreetMap via Overpass for the area bbox 48.83,2.30 to 48.88,2.38. I need three deliverables: the shortest driving route from each incident to its closest hospital, a distance matrix of each incident against its three nearest hospitals, and a 15-minute drive-time isochrone around every hospital to visualise coverage gaps. Assume 30 km/h for edges without a maxspeed tag; use posted maxspeed otherwise. Everything goes into a single GPKG called `emergency_routing.gpkg` with all layers in the official metric coordinate system for Paris and exactly these four layer names. The `incidents` layer has Point (or MultiPoint) geometry and an `incident_id` column. The `closest_hospital` layer has LineString (or MultiLineString) geometry representing the route, with columns `incident_id`, `hospital_name`, and `network_distance_m` (numeric, metres). One row per incident. The `distance_matrix` layer is tabular (geometry may be empty or null) with columns `incident_id`, `hospital_name`, `rank` (integer 1 through 3, ascending by `network_distance_m` within each incident), and `network_distance_m`. The `isochrones_15min` layer has MultiPolygon geometry (Polygon also accepted) with columns `hospital_name` and `travel_time_min`. One row per hospital.
0:00
System info
{
  "adapter": "openrouter",
  "model": "google/gemma-4-26b-a4b-it",
  "base_url": "https://openrouter.ai/api/v1"
}
0:02
Bash ls -F
incidents.csv
0:04
Read incidents.csv
     1	incident_id,latitude,longitude,label
     2	INC_001,48.8566,2.3522,near Notre-Dame
     3	INC_002,48.8620,2.3360,near Louvre
     4	INC_003,48.8462,2.3464,Quartier Latin
     5	INC_004,48.8700,2.3431,near Gare du Nord area
     6	INC_005,48.8530,2.3325,Luxembourg Garden area
     7	INC_006,48.8600,2.3700,Bastille area
     8	INC_007,48.8450,2.3100,near Montparnasse
     9	INC_008,48.8750,2.3600,near Republique
1:48
Write solve.py
content · 132 lines · py
import osmnx as ox
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, LineString, MultiLineString, Polygon, MultiPolygon
import networkx as nx
import numpy as np

# 1. Load Incidents
incidents_df = pd.read_csv('incidents.csv')
incidents_gdf = gpd.GeoDataFrame(
    incidents_df, 
    geometry=gpd.points_from_xy(incidents_df.longitude, incidents_df.latitude),
    crs="EPSG:4326"
)

# Target CRS: Paris metric (EPSG:2154 - Lambert-93)
target_crs = "EPSG:2154"
incidents_gdf = incidents_gdf.to_crs(target_crs)

# Bounding box for OSM (from prompt)
bbox = (48.83, 2.30, 48.88, 2.38) # minlat, minlon, maxlat, maxlon

# 2. Fetch OSM Data
print("Fetching road network and hospitals...")
# Fetch graph
G = ox.graph_from_bbox(bbox[0], bbox[2], bbox[1], bbox[3], network_type='drive')

# Fetch hospitals
# Overpass query for hospitals (amenity=hospital)
# Note: ox.geometries (deprecated) or ox.features_from_bbox (new)
hospitals = ox.features_from_bbox(bbox[0], bbox[2], bbox[1], bbox[3], tags={'amenity': 'hospital'})

# Pre-process graph for speed and speed assumptions
G = ox.add_edge_speeds(G)
G = ox.add_edge_travel_times(G)

# Ensure all edges have a travel time based on 30km/h if missing
# (ox.add_edge_speeds handles maxspeed, but if still nan, we set to 30km/h)
for u, v, k, data in G.edges(data=True, keys=True):
    if 'travel_time' not in data or np.isnan(data['travel_time']):
        # 30 km/h = 30000 m / 3600 s = 8.333 m/s
        # length in meters
        length = data.get('length', 1.0) 
        data['travel_time'] = length / 8.333

# Prepare hospital list
# We need the nearest node for each hospital to use in network calculations
# and for distances.
hospitals = hospitals.to_crs(target_crs)
# We'll use the centroid of the hospital if it's a polygon, or the point itself
hospitals['geometry'] = hospitals.geometry.centroid

# 3. Analysis Functions
def get_nearest_node(G, point):
    return ox.nearest_nodes(G, point.x, point.y)

# For distances, we will find the closest hospital to each incident and the top 3.
# First, let's find the nearest nodes for each hospital
hospitals['nearest_node'] = hospitals.geometry.apply(lambda p: get_nearest_node(G, p))

# Distance Matrix & Closest Hospital
results_closest = []
results_matrix = []

for _, incident in incidents_gdf.iterrows():
    inc_id = incident['incident_id']
    inc_node = get_nearest_node(G, incident.geometry)
    
    # Calculate distances to all hospitals via network
    # To be efficient, we find shortest path distances from inc_node to all hospital nodes
    # However, we only have a few hospitals, so it's fine to loop.
    
    h_distances = []
    
    for _, hosp in hospitals.iterrows():
        h_node = hosp['nearest_node']
        h_name = hosp.get('name', 'Unnamed Hospital')
        
        try:
            # Shortest path distance (length)
            dist = nx.shortest_path_length(G, inc_node, h_node, weight='length')
            # Shortest path time
            time = nx.shortest_path_length(G, inc_node, h_node, weight='travel_time')
            
            # Path geometry for the closest one
            path_nodes = nx.shortest_path(G, inc_node, h_node, weight='length')
            path_geom = ox.utils_graph.get_route_edge_attributes(G, path_nodes, 'geometry')
            # This is complex, simpler way to get geometry:
            # nx.shortest_path returns nodes, we need to collect edges.
            
            h_distances.append({
                'hospital_name': h_name,
                'network_distance_m': dist,
                'travel_time_s': time,
                'path_nodes': path_nodes
            })
        except nx.NetworkXNoPath:
            continue
            
    # Sort distances for this incident
    h_distances.sort(key=lambda x: x['network_distance_m'])
    
    # Closest hospital (rank 1)
    if h_distances:
        best = h_distances[0]
        # Create LineString for the route
        # We'll use ox.routing.route_to_gdf or manual reconstruction
        # Manually:
        edges = []
        for i in range(len(best['path_nodes']) - 1):
            u, v = best['path_nodes'][i], best['path_nodes'][i+1]
            # Handle multi-edges (simplification: take first)
            edge_data = list(G.get_edge_data(u, v).values())[0]
            if 'geometry' in edge_data:
                edges.append(edge_data['geometry'])
            else:
                # If no geometry (straight line)
                edges.append(LineString([G.nodes[u]['x'], G.nodes[u]['y'], G.nodes[v]['x'], G.nodes[v]['y']])) # This is wrong, LineString needs 2 points
                # Correct fallback:
                p1 = Point(G.nodes[u]['x'], G.nodes[u]['y'])
                p2 = Point(G.nodes[v]['x'], G.nodes[v]['y'])
                edges.append(LineString([p1, p2]))
        
        # Combine edges into a single LineString or MultiLineString
        # Since we are in target_crs, the x,y from G.nodes are in target_crs? 
        # No, ox.graph_from_bbox uses lat/lon. We need to ensure graph is in target_crs.
        
        # Wait, I should project the graph first.
        pass

print("Wait, I need to re-structure the script to project the graph properly.")
Wrote solve.py (5231 chars).
2:22
Write solve.py
content · 248 lines · py
import osmnx as ox
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, LineString, MultiLineString, Polygon, MultiPolygon
import networkx as nx
import numpy as np

# 1. Load Incidents
incidents_df = pd.read_csv('incidents.csv')
incidents_gdf = gpd.GeoDataFrame(
    incidents_df, 
    geometry=gpd.points_from_xy(incidents_df.longitude, incidents_df.latitude),
    crs="EPSG:4326"
)

# Target CRS: Paris metric (EPSG:2154 - Lambert-93)
target_crs = "EPSG:2154"
incidents_gdf = incidents_gdf.to_crs(target_crs)

# Bounding box for OSM (from prompt) - note: lat/lon
bbox = (48.83, 2.30, 48.88, 2.38) # minlat, minlon, maxlat, maxlon

# 2. Fetch OSM Data
print("Fetching road network and hospitals...")
G = ox.graph_from_bbox(bbox[0], bbox[2], bbox[1], bbox[3], network_type='drive')

# Fetch hospitals
hospitals = ox.features_from_bbox(bbox[0], bbox[2], bbox[1], bbox[3], tags={'amenity': 'hospital'})

# Pre-process graph
G = ox.add_edge_speeds(G)
G = ox.add_edge_travel_times(G)

# Apply default 30 km/h if speed/travel_time is missing
# 30 km/h = 8.333 m/s
for u, v, k, data in G.edges(data=True, keys=True):
    if 'travel_time' not in data or np.isnan(data['travel_time']):
        length = data.get('length', 1.0)
        data['travel_time'] = length / 8.333333333333333

# Project the graph to target CRS
# This makes all node geometries and edge geometries metric
G_proj = ox.project_graph(G, to_crs=target_crs)

# Prepare hospital list
hospitals = hospitals.to_crs(target_crs)
# Use centroid for hospitals
hospitals['geometry'] = hospitals.geometry.centroid
# Ensure hospital names are unique and clean
hospitals['hospital_name'] = hospitals['name'].fillna('Unnamed Hospital')
# If duplicates exist in name, append index
hospitals['hospital_name'] = hospitals['hospital_name'].astype(str) + "_" + hospitals.index.astype(str)

# For distance, we need to find nearest nodes in projected graph
# Get node coords in target CRS
nodes_proj = ox.graph_to_gdfs(G_proj, nodes=True, edges=False)

def get_nearest_node_proj(graph_nodes, point):
    # Using the nodes dataframe to find nearest node
    # A simple approach with scipy or ox.nearest_nodes
    # Since graph_nodes is a GeoDataFrame, we can use it
    return ox.nearest_nodes(G_proj, point.x, point.y)

# 3. Analysis
print("Calculating routes and distances...")

hospitals['nearest_node'] = hospitals.geometry.apply(lambda p: get_nearest_node_proj(nodes_proj, p))

closest_hospital_rows = []
distance_matrix_rows = []

for _, incident in incidents_gdf.iterrows():
    inc_id = incident['incident_id']
    inc_node = get_nearest_node_proj(nodes_proj, incident.geometry)
    
    h_stats = []
    
    for _, hosp in hospitals.iterrows():
        h_node = hosp['nearest_node']
        h_name = hosp['hospital_name']
        
        try:
            # Network distance (length)
            dist = nx.shortest_path_length(G_proj, inc_node, h_node, weight='length')
            # Network time (seconds)
            time = nx.shortest_path_length(G_proj, inc_node, h_node, weight='travel_time')
            # Path for geometry
            path = nx.shortest_path(G_proj, inc_node, h_node, weight='length')
            
            h_stats.append({
                'hospital_name': h_name,
                'network_distance_m': dist,
                'travel_time_s': time,
                'path': path
            })
        except nx.NetworkXNoPath:
            continue
            
    # Sort by distance
    h_stats.sort(key=lambda x: x['network_distance_m'])
    
    # 1. Distance Matrix (up to 3)
    for rank, stat in enumerate(h_stats[:3], 1):
        distance_matrix_rows.append({
            'incident_id': inc_id,
            'hospital_name': stat['hospital_name'],
            'rank': rank,
            'network_distance_m': stat['network_distance_m']
        })
        
    # 2. Closest Hospital
    if h_stats:
        best = h_stats[0]
        # Create route geometry
        path_nodes = best['path']
        
        # Build route from edges
        route_edges = []
        for i in range(len(path_nodes) - 1):
            u, v = path_nodes[i], path_nodes[i+1]
            # Get edge geometry
            edge_data = G_proj.get_edge_data(u, v)
            if edge_data:
                # Pick the first edge if there are multiple (usually 1)
                # edge_data is a dict of dicts (k)
                k = list(edge_data.keys())[0]
                if 'geometry' in edge_data[k]:
                    route_edges.append(edge_data[k]['geometry'])
                else:
                    # Fallback to straight line
                    p1 = Point(G_proj.nodes[u]['x'], G_proj.nodes[u]['y'])
                    p2 = Point(G_proj.nodes[v]['x'], G_proj.nodes[v]['y'])
                    route_edges.append(LineString([p1, p2]))
        
        if route_edges:
            from shapely.ops import linemerge
            merged_route = linemerge(route_edges)
            closest_hospital_rows.append({
                'incident_id': inc_id,
                'hospital_name': best['hospital_name'],
                'network_distance_m': best['network_distance_m'],
                'geometry': merged_route
            })

# 4. Isochrones (15 min)
print("Calculating 15-min isochrones...")
isochrones_rows = []
# 15 minutes = 900 seconds
limit_s = 15 * 60

for _, hosp in hospitals.iterrows():
    h_node = hosp['nearest_node']
    h_name = hosp['hospital_name']
    
    # Get all nodes reachable within limit_s
    subgraph = nx.ego_graph(G_proj, h_node, radius=limit_s, distance='travel_time')
    
    if len(subgraph.nodes) > 0:
        # Get all edges in the subgraph
        sub_edges = subgraph.edges(data=True)
        edge_geoms = []
        for u, v, data in sub_edges:
            if 'geometry' in data:
                edge_geoms.append(data['geometry'])
            else:
                p1 = Point(G_proj.nodes[u]['x'], G_proj.nodes[u]['y'])
                p2 = Point(G_proj.nodes[v]['x'], G_proj.nodes[v]['y'])
                edge_geoms.append(LineString([p1, p2]))
        
        if edge_geoms:
            from shapely.ops import linemerge, unary_union
            merged_lines = linemerge(edge_geoms)
            # To create a polygon, we need to buffer the lines. 
            # A standard way to do isochrones is to buffer the edges, 
            # but that's not quite a "coverage area". 
            # However, for a "travel-time isochrone" in a network context, 
            # a common approach is the union of buffers of the reachable edges.
            # Let's buffer edges by something reasonable or just find the boundary.
            # Wait, a more accurate way is the concave hull or alpha shape of the nodes.
            # But buffering edges is a good approximation for "area reached".
            # Let's buffer edges by a small amount to ensure they connect, then unary_union.
            # Actually, let's buffer edges by say 20m.
            buffered_edges = unary_union([geom.buffer(20) for geom in edge_geoms])
            
            # For a cleaner isochrone, we can take the convex hull or a concave hull.
            # But the prompt asks for a "15-minute drive-time isochrone".
            # Let's use the union of buffered edges.
            isochrones_rows.append({
                'hospital_name': h_name,
                'travel_time_min': 15.0,
                'geometry': buffered_edges
            })
        else:
            # No edges? Just buffer the node
            p = Point(G_proj.nodes[h_node]['x'], G_proj.nodes[h_node]['y'])
            isochrones_rows.append({
                'hospital_name': h_name,
                'travel_time_min': 15.0,
                'geometry': p.buffer(50) # Small default
            })
    else:
        # No nodes reachable?
        p = Point(G_proj.nodes[h_node]['x'], G_proj.nodes[h_node]['y'])
        isochrones_rows.append({
            'hospital_name': h_name,
            'travel_time_min': 15.0,
            'geometry': p.buffer(50)
        })

# 5. Prepare Outputs
print("Saving outputs to GPKG...")

# incidents layer
closest_hospital_gdf = gpd.GeoDataFrame(closest_hospital_rows, crs=target_crs)
# Note: closest_hospital_gdf might have MultiLineString or LineString
# If merged_route is a LineString, it's fine.

# distance_matrix layer
dist_matrix_gdf = gpd.GeoDataFrame(distance_matrix_rows, crs=target_crs)
# The prompt says: distance_matrix layer is tabular (geometry may be empty or null)
# So we set geometry to None.
dist_matrix_gdf.set_geometry(None, inplace=True)

# isochrones layer
isochrones_gdf = gpd.GeoDataFrame(isochrones_rows, crs=target_crs)
# Ensure MultiPolygon/Polygon
isochrones_gdf['geometry'] = isochrones_gdf['geometry'].apply(lambda x: x if x.geom_type in ['Polygon', 'MultiPolygon'] else x.convex_hull)

# Final save
with pd.io.formats.format.DataFrameFormatter.precision_context(6):
    with gpd.GeoDataFrame(incidents_gdf, crs=target_crs).to_crs(target_crs).explode(index_parts=False) as inc_exploded:
        # Actually the prompt says incidents layer has Point (or MultiPoint)
        # We'll just use incidents_gdf directly.
        pass

# Let's do a more robust way to ensure geometry type for isochrones
# Some union results might be weird.
isochrones_gdf['geometry'] = isochrones_gdf['geometry'].buffer(0) # Clean up

# Create the GPKG
# Layers: incidents, closest_hospital, distance_matrix, isochrones_15min
incidents_gdf.to_file('emergency_routing.gpkg', layer='incidents', driver='GPKG')
closest_hospital_gdf.to_file('emergency_routing.gpkg', layer='closest_hospital', driver='GPKG')
dist_matrix_gdf.to_file('emergency_routing.gpkg', layer='distance_matrix', driver='GPKG')
isochrones_gdf.to_file('emergency_routing.gpkg', layer='isochrones_15min', driver='GPKG')

print("Done!")
Wrote solve.py (9826 chars).
2:24
Bash python solve.py
Fetching road network and hospitals...
Traceback (most recent call last):
  File "/work/solve.py", line 25, in <module>
    G = ox.graph_from_bbox(bbox[0], bbox[2], bbox[1], bbox[3], network_type='drive')
TypeError: graph_from_bbox() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given
2:33
Edit solve.py
Edited solve.py.
2:41
Edit solve.py
Edited solve.py.
2:42
Bash python solve.py
Fetching road network and hospitals...
Traceback (most recent call last):
  File "/work/solve.py", line 25, in <module>
    G = ox.graph_from_bbox(north=bbox[0], south=bbox[2], east=bbox[3], west=bbox[1], network_type='drive')
TypeError: graph_from_bbox() got an unexpected keyword argument 'north'
2:46
Read solve.py
     1	import osmnx as ox
     2	import pandas as pd
     3	import geopandas as gpd
     4	from shapely.geometry import Point, LineString, MultiLineString, Polygon, MultiPolygon
     5	import networkx as nx
     6	import numpy as np
     7	
     8	# 1. Load Incidents
     9	incidents_df = pd.read_csv('incidents.csv')
    10	incidents_gdf = gpd.GeoDataFrame(
    11	    incidents_df, 
    12	    geometry=gpd.points_from_xy(incidents_df.longitude, incidents_df.latitude),
    13	    crs="EPSG:4326"
    14	)
    15	
    16	# Target CRS: Paris metric (EPSG:2154 - Lambert-93)
    17	target_crs = "EPSG:2154"
    18	incidents_gdf = incidents_gdf.to_crs(target_crs)
    19	
    20	# Bounding box for OSM (from prompt) - note: lat/lon
    21	bbox = (48.83, 2.30, 48.88, 2.38) # minlat, minlon, maxlat, maxlon
    22	
    23	# 2. Fetch OSM Data
    24	print("Fetching road network and hospitals...")
    25	G = ox.graph_from_bbox(north=bbox[0], south=bbox[2], east=bbox[3], west=bbox[1], network_type='drive')
    26	
    27	# Fetch hospitals
    28	hospitals = ox.features_from_bbox(north=bbox[0], south=bbox[2], east=bbox[3], west=bbox[1], tags={'amenity': 'hospital'})
    29	
    30	# Pre-process graph
    31	G = ox.add_edge_speeds(G)
    32	G = ox.add_edge_travel_times(G)
    33	
    34	# Apply default 30 km/h if speed/travel_time is missing
    35	# 30 km/h = 8.333 m/s
    36	for u, v, k, data in G.edges(data=True, keys=True):
    37	    if 'travel_time' not in data or np.isnan(data['travel_time']):
    38	        length = data.get('length', 1.0)
    39	        data['travel_time'] = length / 8.333333333333333
    40	
    41	# Project the graph to target CRS
    42	# This makes all node geometries and edge geometries metric
    43	G_proj = ox.project_graph(G, to_crs=target_crs)
    44	
    45	# Prepare hospital list
    46	hospitals = hospitals.to_crs(target_crs)
    47	# Use centroid for hospitals
    48	hospitals['geometry'] = hospitals.geometry.centroid
    49	# Ensure hospital names are unique and clean
    50	hospitals['hospital_name'] = hospitals['name'].fillna('Unnamed Hospital')
    51	# If duplicates exist in name, append index
    52	hospitals['hospital_name'] = hospitals['hospital_name'].astype(str) + "_" + hospitals.index.astype(str)
    53	
    54	# For distance, we need to find nearest nodes in projected graph
    55	# Get node coords in target CRS
    56	nodes_proj = ox.graph_to_gdfs(G_proj, nodes=True, edges=False)
    57	
    58	def get_nearest_node_proj(graph_nodes, point):
    59	    # Using the nodes dataframe to find nearest node
    60	    # A simple approach with scipy or ox.nearest_nodes
    61	    # Since graph_nodes is a GeoDataFrame, we can use it
    62	    return ox.nearest_nodes(G_proj, point.x, point.y)
    63	
    64	# 3. Analysis
    65	print("Calculating routes and distances...")
    66	
    67	hospitals['nearest_node'] = hospitals.geometry.apply(lambda p: get_nearest_node_proj(nodes_proj, p))
    68	
    69	closest_hospital_rows = []
    70	distance_matrix_rows = []
    71	
    72	for _, incident in incidents_gdf.iterrows():
    73	    inc_id = incident['incident_id']
    74	    inc_node = get_nearest_node_proj(nodes_proj, incident.geometry)
    75	    
    76	    h_stats = []
    77	    
    78	    for _, hosp in hospitals.iterrows():
    79	        h_node = hosp['nearest_node']
    80	        h_name = hosp['hospital_name']
    81	        
    82	        try:
    83	            # Network distance (length)
    84	            dist = nx.shortest_path_length(G_proj, inc_node, h_node, weight='length')
    85	            # Network time (seconds)
    86	            time = nx.shortest_path_length(G_proj, inc_node, h_node, weight='travel_time')
    87	            # Path for geometry
    88	            path = nx.shortest_path(G_proj, inc_node, h_node, weight='length')
    89	            
    90	            h_stats.append({
    91	                'hospital_name': h_name,
    92	                'network_distance_m': dist,
    93	                'travel_time_s': time,
    94	                'path': path
    95	            })
    96	        except nx.NetworkXNoPath:
    97	            continue
    98	            
    99	    # Sort by distance
   100	    h_stats.sort(key=lambda x: x['network_distance_m'])
   101	    
   102	    # 1. Distance Matrix (up to 3)
   103	    for rank, stat in enumerate(h_stats[:3], 1):
   104	        distance_matrix_rows.append({
   105	            'incident_id': inc_id,
   106	            'hospital_name': stat['hospital_name'],
   107	            'rank': rank,
   108	            'network_distance_m': stat['network_distance_m']
   109	        })
   110	        
   111	    # 2. Closest Hospital
   112	    if h_stats:
   113	        best = h_stats[0]
   114	        # Create route geometry
   115	        path_nodes = best['path']
   116	        
   117	        # Build route from edges
   118	        route_edges = []
   119	        for i in range(len(path_nodes) - 1):
   120	            u, v = path_nodes[i], path_nodes[i+1]
   121	            # Get edge geometry
   122	            edge_data = G_proj.get_edge_data(u, v)
   123	            if edge_data:
   124	                # Pick the first edge if there are multiple (usually 1)
   125	                # edge_data is a dict of dicts (k)
   126	                k = list(edge_data.keys())[0]
   127	                if 'geometry' in edge_data[k]:
   128	                    route_edges.append(edge_data[k]['geometry'])
   129	                else:
   130	                    # Fallback to straight line
   131	                    p1 = Point(G_proj.nodes[u]['x'], G_proj.nodes[u]['y'])
   132	                    p2 = Point(G_proj.nodes[v]['x'], G_proj.nodes[v]['y'])
   133	                    route_edges.append(LineString([p1, p2]))
   134	        
   135	        if route_edges:
   136	            from shapely.ops import linemerge
   137	            merged_route = linemerge(route_edges)
   138	            closest_hospital_rows.append({
   139	                'incident_id': inc_id,
   140	                'hospital_name': best['hospital_name'],
   141	                'network_distance_m': best['network_distance_m'],
   142	                'geometry': merged_route
   143	            })
   144	
   145	# 4. Isochrones (15 min)
   146	print("Calculating 15-min isochrones...")
   147	isochrones_rows = []
   148	# 15 minutes = 900 seconds
   149	limit_s = 15 * 60
   150	
   151	for _, hosp in hospitals.iterrows():
   152	    h_node = hosp['nearest_node']
   153	    h_name = hosp['hospital_name']
   154	    
   155	    # Get all nodes reachable within limit_s
   156	    subgraph = nx.ego_graph(G_proj, h_node, radius=limit_s, distance='travel_time')
   157	    
   158	    if len(subgraph.nodes) > 0:
   159	        # Get all edges in the subgraph
   160	        sub_edges = subgraph.edges(data=True)
   161	        edge_geoms = []
   162	        for u, v, data in sub_edges:
   163	            if 'geometry' in data:
   164	                edge_geoms.append(data['geometry'])
   165	            else:
   166	                p1 = Point(G_proj.nodes[u]['x'], G_proj.nodes[u]['y'])
   167	                p2 = Point(G_proj.nodes[v]['x'], G_proj.nodes[v]['y'])
   168	                edge_geoms.append(LineString([p1, p2]))
   169	        
   170	        if edge_geoms:
   171	            from shapely.ops import linemerge, unary_union
   172	            merged_lines = linemerge(edge_geoms)
   173	            # To create a polygon, we need to buffer the lines. 
   174	            # A standard way to do isochrones is to buffer the edges, 
   175	            # but that's not quite a "coverage area". 
   176	            # However, for a "travel-time isochrone" in a network context, 
   177	            # a common approach is the union of buffers of the reachable edges.
   178	            # Let's buffer edges by something reasonable or just find the boundary.
   179	            # Wait, a more accurate way is the concave hull or alpha shape of the nodes.
   180	            # But buffering edges is a good approximation for "area reached".
   181	            # Let's buffer edges by a small amount to ensure they connect, then unary_union.
   182	            # Actually, let's buffer edges by say 20m.
   183	            buffered_edges = unary_union([geom.buffer(20) for geom in edge_geoms])
   184	            
   185	            # For a cleaner isochrone, we can take the convex hull or a concave hull.
   186	            # But the prompt asks for a "15-minute drive-time isochrone".
   187	            # Let's use the union of buffered edges.
   188	            isochrones_rows.append({
   189	                'hospital_name': h_name,
   190	                'travel_time_min': 15.0,
   191	                'geometry': buffered_edges
   192	            })
   193	        else:
   194	            # No edges? Just buffer the node
   195	            p = Point(G_proj.nodes[h_node]['x'], G_proj.nodes[h_node]['y'])
   196	            isochrones_rows.append({
   197	                'hospital_name': h_name,
   198	                'travel_time_min': 15.0,
   199	                'geometry': p.buffer(50) # Small default
   200	            })
   201	    else:
   202	        # No nodes reachable?
   203	        p = Point(G_proj.nodes[h_node]['x'], G_proj.nodes[h_node]['y'])
   204	        isochrones_rows.append({
   205	            'hospital_name': h_name,
   206	            'travel_time_min': 15.0,
   207	            'geometry': p.buffer(50)
   208	        })
   209	
   210	# 5. Prepare Outputs
   211	print("Saving outputs to GPKG...")
   212	
   213	# incidents layer
   214	closest_hospital_gdf = gpd.GeoDataFrame(closest_hospital_rows, crs=target_crs)
   215	# Note: closest_hospital_gdf might have MultiLineString or LineString
   216	# If merged_route is a LineString, it's fine.
   217	
   218	# distance_matrix layer
   219	dist_matrix_gdf = gpd.GeoDataFrame(distance_matrix_rows, crs=target_crs)
   220	# The prompt says: distance_matrix layer is tabular (geometry may be empty or null)
   221	# So we set geometry to None.
   222	dist_matrix_gdf.set_geometry(None, inplace=True)
   223	
   224	# isochrones layer
   225	isochrones_gdf = gpd.GeoDataFrame(isochrones_rows, crs=target_crs)
   226	# Ensure MultiPolygon/Polygon
   227	isochrones_gdf['geometry'] = isochrones_gdf['geometry'].apply(lambda x: x if x.geom_type in ['Polygon', 'MultiPolygon'] else x.convex_hull)
   228	
   229	# Final save
   230	with pd.io.formats.format.DataFrameFormatter.precision_context(6):
   231	    with gpd.GeoDataFrame(incidents_gdf, crs=target_crs).to_crs(target_crs).explode(index_parts=False) as inc_exploded:
   232	        # Actually the prompt says incidents layer has Point (or MultiPoint)
   233	        # We'll just use incidents_gdf directly.
   234	        pass
   235	
   236	# Let's do a more robust way to ensure geometry type for isochrones
   237	# Some union results might be weird.
   238	isochrones_gdf['geometry'] = isochrones_gdf['geometry'].buffer(0) # Clean up
   239	
   240	# Create the GPKG
   241	# Layers: incidents, closest_hospital, distance_matrix, isochrones_15min
   242	incidents_gdf.to_file('emergency_routing.gpkg', layer='incidents', driver='GPKG')
   243	closest_hospital_gdf.to_file('emergency_routing.gpkg', layer='closest_hospital', driver='GPKG')
   244	dist_matrix_gdf.to_file('emergency_routing.gpkg', layer='distance_matrix', driver='GPKG')
   245	isochrones_gdf.to_file('emergency_routing.gpkg', layer='isochrones_15min', driver='GPKG')
   246	
   247	print("Done!")
2:48
Bash python solve.py
Fetching road network and hospitals...
Traceback (most recent call last):
  File "/work/solve.py", line 25, in <module>
    G = ox.graph_from_bbox(north=bbox[0], south=bbox[2], east=bbox[3], west=bbox[1], network_type='drive')
TypeError: graph_from_bbox() got an unexpected keyword argument 'north'
2:53
Bash python -c "import osmnx; print(help(osmnx.graph_from_bbox))"
Help on function graph_from_bbox in module osmnx.graph:

graph_from_bbox(
    bbox: tuple[float, float, float, float],
    *,
    network_type: str = 'all',
    simplify: bool = True,
    retain_all: bool = False,
    truncate_by_edge: bool = False,
    custom_filter: str | list[str] | None = None
) -> nx.MultiDiGraph
    Download and create a graph within a lat-lon bounding box.

    This function uses filters to query the Overpass API: you can either
    specify a pre-defined `network_type` or provide your own `custom_filter`
    with Overpass QL.

    Use the `settings` module's `useful_tags_node` and `useful_tags_way`
    settings to configure which OSM node/way tags are added as graph node/edge
    attributes. If you want a fully bidirectional network, ensure your
    `network_type` is in `settings.bidirectional_network_types` before
    creating your graph. You can also use the `settings` module to retrieve a
    snapshot of historical OSM data as of a certain date, or to configure the
    Overpass server timeout, memory allocation, and other customizations.

    Parameters
    ----------
    bbox
        Bounding box as `(left, bottom, right, top)`. Coordinates should be in
        unprojected latitude-longitude degrees (EPSG:4326).
    network_type
        {"all", "all_public", "bike", "drive", "drive_service", "walk"}
        What type of street network to retrieve if `custom_filter` is None.
    simplify
        If True, simplify graph topology via the `simplify_graph` function.
    retain_all
        If True, return the entire graph even if it is not connected. If
        False, retain only the largest weakly connected component.
    truncate_by_edge
        If True, retain nodes the outside bounding box if at least one of
        the node's neighbors lies within the bounding box.
    custom_filter
        A custom ways filter to be used instead of the `network_type` presets,
        e.g. `'["power"~"line"]' or '["highway"~"motorway|trunk"]'`. If `str`,
        the intersection of keys/values will be used, e.g., `'[maxspeed=50][lanes=2]'`
        will return all ways having both maxspeed of 50 and two lanes. If
        `list`, the union of the `list` items will be used, e.g.,
        `['[maxspeed=50]', '[lanes=2]']` will return all ways having either
        maximum speed of 50 or two lanes. Also pass in a `network_type` that
        is in `settings.bidirectional_network_types` if you want the graph to
        be fully bidirectional.

    Returns
    -------
    G
        The resulting MultiDiGraph.

    Notes
    -----
    Very large query areas use the `utils_geo._consolidate_subdivide_geometry`
    function to automatically make multiple requests: see that function's
    documentation for caveats.

None
2:59
Edit solve.py
Edited solve.py.
3:02
Edit solve.py
Edited solve.py.
3:03
Bash python -c "import osmnx; print(help(osmnx.features_from_bbox))"
Help on function features_from_bbox in module osmnx.features:

features_from_bbox(
    bbox: tuple[float, float, float, float],
    tags: dict[str, bool | str | list[str]]
) -> gpd.GeoDataFrame
    Download OSM features within a lat-lon bounding box.

    You can use the `settings` module to retrieve a snapshot of historical OSM
    data as of a certain date, or to configure the Overpass server timeout,
    memory allocation, and other custom settings. This function searches for
    features using tags. For more details, see:
    https://wiki.openstreetmap.org/wiki/Map_features

    Parameters
    ----------
    bbox
        Bounding box as `(left, bottom, right, top)`. Coordinates should be in
        unprojected latitude-longitude degrees (EPSG:4326).
    tags
        Tags for finding elements in the selected area. Results are the union,
        not intersection of the tags and each result matches at least one tag.
        The keys are OSM tags (e.g. `building`, `landuse`, `highway`, etc) and
        the values can be either `True` to retrieve all elements matching the
        tag, or a string to retrieve a single `tag:value` combination, or a
        list of strings to retrieve multiple values for the tag. For example,
        `tags = {'building': True}` would return all buildings in the area.
        Or, `tags = {'amenity':True, 'landuse':['retail','commercial'],
        'highway':'bus_stop'}` would return all amenities, any landuse=retail,
        any landuse=commercial, and any highway=bus_stop.

    Returns
    -------
    gdf
        The features, multi-indexed by element type and OSM ID.

None
3:07
Edit solve.py
Edited solve.py.
3:09
Bash python solve.py
<exit_code>-1</exit_code>
<error>Command timed out after 120000ms</error>
5:33
Write solve.py
content · 157 lines · py
import osmnx as ox
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, LineString, MultiLineString, Polygon, MultiPolygon
import networkx as nx
import numpy as np
from shapely.ops import linemerge, unary_union

# 1. Load Incidents
incidents_df = pd.read_csv('incidents.csv')
incidents_gdf = gpd.GeoDataFrame(
    incidents_df, 
    geometry=gpd.points_from_xy(incidents_df.longitude, incidents_df.latitude),
    crs="EPSG:4326"
)

# Target CRS: Paris metric (EPSG:2154 - Lambert-93)
target_crs = "EPSG:2154"
incidents_gdf = incidents_gdf.to_crs(target_crs)

# Bounding box for OSM (left, bottom, right, top)
bbox = (2.30, 48.83, 2.38, 48.88) 

# 2. Fetch OSM Data
print("Fetching road network and hospitals...")
G = ox.graph_from_bbox(bbox, network_type='drive')
hospitals = ox.features_from_bbox(bbox, tags={'amenity': 'hospital'})

# Pre-process graph
G = ox.add_edge_speeds(G)
G = ox.add_edge_travel_times(G)

# Apply default 30 km/h if speed/travel_time is missing
for u, v, k, data in G.edges(data=True, keys=True):
    if 'travel_time' not in data or np.isnan(data['travel_time']):
        length = data.get('length', 1.0)
        data['travel_time'] = length / 8.333333333333333

# Project the graph to target CRS
G_proj = ox.project_graph(G, to_crs=target_crs)

# Prepare hospital list
hospitals = hospitals.to_crs(target_crs)
hospitals['geometry'] = hospitals.geometry.centroid
hospitals['hospital_name'] = hospitals['name'].fillna('Unnamed Hospital').astype(str) + "_" + hospitals.index.astype(str)

# Get node coords in target CRS
nodes_proj = ox.graph_to_gdfs(G_proj, nodes=True, edges=False)

def get_nearest_node_proj(graph_nodes, point):
    return ox.nearest_nodes(G_proj, point.x, point.y)

# 3. Analysis
print("Calculating routes and distances...")
hospitals['nearest_node'] = hospitals.geometry.apply(lambda p: get_nearest_node_proj(nodes_proj, p))

closest_hospital_rows = []
distance_matrix_rows = []

for _, incident in incidents_gdf.iterrows():
    inc_id = incident['incident_id']
    inc_node = get_nearest_node_proj(nodes_proj, incident.geometry)
    
    h_stats = []
    for _, hosp in hospitals.iterrows():
        h_node = hosp['nearest_node']
        h_name = hosp['hospital_name']
        try:
            dist = nx.shortest_path_length(G_proj, inc_node, h_node, weight='length')
            time = nx.shortest_path_length(G_proj, inc_node, h_node, weight='travel_time')
            path = nx.shortest_path(G_proj, inc_node, h_node, weight='length')
            h_stats.append({'hospital_name': h_name, 'network_distance_m': dist, 'travel_time_s': time, 'path': path})
        except nx.NetworkXNoPath:
            continue
            
    h_stats.sort(key=lambda x: x['network_distance_m'])
    
    # 1. Distance Matrix (up to 3)
    for rank, stat in enumerate(h_stats[:3], 1):
        distance_matrix_rows.append({
            'incident_id': inc_id,
            'hospital_name': stat['hospital_name'],
            'rank': rank,
            'network_distance_m': stat['network_distance_m']
        })
        
    # 2. Closest Hospital
    if h_stats:
        best = h_stats[0]
        path_nodes = best['path']
        route_edges = []
        for i in range(len(path_nodes) - 1):
            u, v = path_nodes[i], path_nodes[i+1]
            edge_data = G_proj.get_edge_data(u, v)
            if edge_data:
                k = list(edge_data.keys())[0]
                if 'geometry' in edge_data[k]:
                    route_edges.append(edge_data[k]['geometry'])
                else:
                    p1 = Point(G_proj.nodes[u]['x'], G_proj.nodes[u]['y'])
                    p2 = Point(G_proj.nodes[v]['x'], G_proj.nodes[v]['y'])
                    route_edges.append(LineString([p1, p2]))
        if route_edges:
            merged_route = linemerge(route_edges)
            closest_hospital_rows.append({
                'incident_id': inc_id,
                'hospital_name': best['hospital_name'],
                'network_distance_m': best['network_distance_m'],
                'geometry': merged_route
            })

# 4. Isochrones (15 min)
print("Calculating 15-min isochrones...")
isochrones_rows = []
limit_s = 15 * 60

for _, hosp in hospitals.iterrows():
    h_node = hosp['nearest_node']
    h_name = hosp['hospital_name']
    subgraph = nx.ego_graph(G_proj, h_node, radius=limit_s, distance='travel_time')
    
    if len(subgraph.nodes) > 0:
        sub_edges = subgraph.edges(data=True)
        edge_geoms = []
        for u, v, data in sub_edges:
            if 'geometry' in data:
                edge_geoms.append(data['geometry'])
            else:
                p1 = Point(G_proj.nodes[u]['x'], G_proj.nodes[u]['y'])
                p2 = Point(G_proj.nodes[v]['x'], G_proj.nodes[v]['y'])
                edge_geoms.append(LineString([p1, p2]))
        if edge_geoms:
            buffered_edges = unary_union([geom.buffer(20) for geom in edge_geoms])
            isochrones_rows.append({'hospital_name': h_name, 'travel_time_min': 15.0, 'geometry': buffered_edges})
        else:
            p = Point(G_proj.nodes[h_node]['x'], G_proj.nodes[h_node]['y'])
            isochrones_rows.append({'hospital_name': h_name, 'travel_time_min': 15.0, 'geometry': p.buffer(50)})
    else:
        p = Point(G_proj.nodes[h_node]['x'], G_proj.nodes[h_node]['y'])
        isochrones_rows.append({'hospital_name': h_name, 'travel_time_min': 15.0, 'geometry': p.buffer(50)})

# 5. Prepare Outputs
print("Saving outputs to GPKG...")
closest_hospital_gdf = gpd.GeoDataFrame(closest_hospital_rows, crs=target_crs)
dist_matrix_gdf = gpd.GeoDataFrame(distance_matrix_rows, crs=target_crs)
dist_matrix_gdf.set_geometry(None, inplace=True)
isochrones_gdf = gpd.GeoDataFrame(isochrones_rows, crs=target_crs)
isochrones_gdf['geometry'] = isochrones_gdf['geometry'].apply(lambda x: x if x.geom_type in ['Polygon', 'MultiPolygon'] else x.convex_hull)
isochrones_gdf['geometry'] = isochrones_gdf['geometry'].buffer(0)

incidents_gdf.to_file('emergency_routing.gpkg', layer='incidents', driver='GPKG')
closest_hospital_gdf.to_file('emergency_routing.gpkg', layer='closest_hospital', driver='GPKG')
dist_matrix_gdf.to_file('emergency_routing.gpkg', layer='distance_matrix', driver='GPKG')
isochrones_gdf.to_file('emergency_routing.gpkg', layer='isochrones_15min', driver='GPKG')

print("Done!")
Wrote solve.py (6364 chars).
5:35
Bash python solve.py
<exit_code>-1</exit_code>
<error>Command timed out after 120000ms</error>
7:42
Bash timeout 10s python -c "import osmnx as ox; G = ox.graph_from_bbox(2.30, 48.83, 2...
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import osmnx as ox; G = ox.graph_from_bbox(2.30, 48.83, 2.38, 48.88, network_type='drive'); print(len(G))
                            ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: graph_from_bbox() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given
7:48
Bash timeout 10s python -c "import osmnx as ox; G = ox.graph_from_bbox(bbox=(2.30, 48...
3459
7:57
Bash timeout 10s python -c "import osmnx as ox; G = ox.graph_from_bbox(bbox=(2.30, 48...
Success
14:07
Write solve.py
content · 158 lines · py
import osmnx as ox
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, LineString, MultiLineString, Polygon, MultiPolygon
import networkx as nx
import numpy as np
from shapely.ops import linemerge, unary_union

# 1. Load Incidents
incidents_df = pd.read_csv('incidents.csv')
incidents_gdf = gpd.GeoDataFrame(
    incidents_df, 
    geometry=gpd.points_from_xy(incidents_df.longitude, incidents_df.latitude),
    crs="EPSG:4326"
)

# Target CRS: Paris metric (EPSG:2154 - Lambert-93)
target_crs = "EPSG:2154"
incidents_gdf = incidents_gdf.to_crs(target_crs)

# Bounding box for OSM (left, bottom, right, top)
bbox = (2.30, 48.83, 2.38, 48.88) 

# 2. Fetch OSM Data
print("Fetching road network and hospitals...")
G = ox.graph_from_bbox(bbox=bbox, network_type='drive')
hospitals = ox.features_from_bbox(bbox=bbox, tags={'amenity': 'hospital'})

# Pre-process graph
G = ox.add_edge_speeds(G)
G = ox.add_edge_travel_times(G)

# Apply default 30 km/h if speed/travel_time is missing
for u, v, k, data in G.edges(data=True, keys=True):
    if 'travel_time' not in data or np.isnan(data['travel_time']):
        length = data.get('length', 1.0)
        data['travel_time'] = length / 8.333333333333333

# Project the graph to target CRS
G_proj = ox.project_graph(G, to_crs=target_crs)

# Prepare hospital list
hospitals = hospitals.to_crs(target_crs)
hospitals['geometry'] = hospitals.geometry.centroid
# Ensure hospital names are unique and clean
hospitals['hospital_name'] = hospitals['name'].fillna('Unnamed Hospital').astype(str) + "_" + hospitals.index.astype(str)

# Get node coords in target CRS
nodes_proj = ox.graph_to_gdfs(G_proj, nodes=True, edges=False)

def get_nearest_node_proj(graph_nodes, point):
    return ox.nearest_nodes(G_proj, point.x, point.y)

# 3. Analysis
print("Calculating routes and distances...")
hospitals['nearest_node'] = hospitals.geometry.apply(lambda p: get_nearest_node_proj(nodes_proj, p))

closest_hospital_rows = []
distance_matrix_rows = []

for _, incident in incidents_gdf.iterrows():
    inc_id = incident['incident_id']
    inc_node = get_nearest_node_proj(nodes_proj, incident.geometry)
    
    h_stats = []
    for _, hosp in hospitals.iterrows():
        h_node = hosp['nearest_node']
        h_name = hosp['hospital_name']
        try:
            dist = nx.shortest_path_length(G_proj, inc_node, h_node, weight='length')
            time = nx.shortest_path_length(G_proj, inc_node, h_node, weight='travel_time')
            path = nx.shortest_path(G_proj, inc_node, h_node, weight='length')
            h_stats.append({'hospital_name': h_name, 'network_distance_m': dist, 'travel_time_s': time, 'path': path})
        except nx.NetworkXNoPath:
            continue
            
    h_stats.sort(key=lambda x: x['network_distance_m'])
    
    # 1. Distance Matrix (up to 3)
    for rank, stat in enumerate(h_stats[:3], 1):
        distance_matrix_rows.append({
            'incident_id': inc_id,
            'hospital_name': stat['hospital_name'],
            'rank': rank,
            'network_distance_m': stat['network_distance_m']
        })
        
    # 2. Closest Hospital
    if h_stats:
        best = h_stats[0]
        path_nodes = best['path']
        route_edges = []
        for i in range(len(path_nodes) - 1):
            u, v = path_nodes[i], path_nodes[i+1]
            edge_data = G_proj.get_edge_data(u, v)
            if edge_data:
                k = list(edge_data.keys())[0]
                if 'geometry' in edge_data[k]:
                    route_edges.append(edge_data[k]['geometry'])
                else:
                    p1 = Point(G_proj.nodes[u]['x'], G_proj.nodes[u]['y'])
                    p2 = Point(G_proj.nodes[v]['x'], G_proj.nodes[v]['y'])
                    route_edges.append(LineString([p1, p2]))
        if route_edges:
            merged_route = linemerge(route_edges)
            closest_hospital_rows.append({
                'incident_id': inc_id,
                'hospital_name': best['hospital_name'],
                'network_distance_m': best['network_distance_m'],
                'geometry': merged_route
            })

# 4. Isochrones (15 min)
print("Calculating 15-min isochrones...")
isochrones_rows = []
limit_s = 15 * 60

for _, hosp in hospitals.iterrows():
    h_node = hosp['nearest_node']
    h_name = hosp['hospital_name']
    subgraph = nx.ego_graph(G_proj, h_node, radius=limit_s, distance='travel_time')
    
    if len(subgraph.nodes) > 0:
        sub_edges = subgraph.edges(data=True)
        edge_geoms = []
        for u, v, data in sub_edges:
            if 'geometry' in data:
                edge_geoms.append(data['geometry'])
            else:
                p1 = Point(G_proj.nodes[u]['x'], G_proj.nodes[u]['y'])
                p2 = Point(G_proj.nodes[v]['x'], G_proj.nodes[v]['y'])
                edge_geoms.append(LineString([p1, p2]))
        if edge_geoms:
            buffered_edges = unary_union([geom.buffer(20) for geom in edge_geoms])
            isochrones_rows.append({'hospital_name': h_name, 'travel_time_min': 15.0, 'geometry': buffered_edges})
        else:
            p = Point(G_proj.nodes[h_node]['x'], G_proj.nodes[h_node]['y'])
            isochrones_rows.append({'hospital_name': h_name, 'travel_time_min': 15.0, 'geometry': p.buffer(50)})
    else:
        p = Point(G_proj.nodes[h_node]['x'], G_proj.nodes[h_node]['y'])
        isochrones_rows.append({'hospital_name': h_name, 'travel_time_min': 15.0, 'geometry': p.buffer(50)})

# 5. Prepare Outputs
print("Saving outputs to GPKG...")
closest_hospital_gdf = gpd.GeoDataFrame(closest_hospital_rows, crs=target_crs)
dist_matrix_gdf = gpd.GeoDataFrame(distance_matrix_rows, crs=target_crs)
dist_matrix_gdf.set_geometry(None, inplace=True)
isochrones_gdf = gpd.GeoDataFrame(isochrones_rows, crs=target_crs)
isochrones_gdf['geometry'] = isochrones_gdf['geometry'].apply(lambda x: x if x.geom_type in ['Polygon', 'MultiPolygon'] else x.convex_hull)
isochrones_gdf['geometry'] = isochrones_gdf['geometry'].buffer(0)

incidents_gdf.to_file('emergency_routing.gpkg', layer='incidents', driver='GPKG')
closest_hospital_gdf.to_file('emergency_routing.gpkg', layer='closest_hospital', driver='GPKG')
dist_matrix_gdf.to_file('emergency_routing.gpkg', layer='distance_matrix', driver='GPKG')
isochrones_gdf.to_file('emergency_routing.gpkg', layer='isochrones_15min', driver='GPKG')

print("Done!")
Wrote solve.py (6419 chars).
14:09
Bash python solve.py
Fetching road network and hospitals...
Traceback (most recent call last):
  File "/work/solve.py", line 46, in <module>
    hospitals['hospital_name'] = hospitals['name'].fillna('Unnamed Hospital').astype(str) + "_" + hospitals.index.astype(str)
                                                                                                  ~~~~~~~~~~~~~~~~~~~~~~^^^^^
  File "/usr/local/lib/python3.14/site-packages/pandas/core/indexes/multi.py", line 4401, in astype
    raise TypeError(
    ...<2 lines>...
    )
TypeError: Setting a MultiIndex dtype to anything other than object is not supported
14:23
Edit solve.py
Edited solve.py.
14:26
Bash python solve.py
Fetching road network and hospitals...
Calculating routes and distances...
Traceback (most recent call last):
  File "/work/solve.py", line 56, in <module>
    hospitals['nearest_node'] = hospitals.geometry.apply(lambda p: get_nearest_node_proj(nodes_proj, p))
                                ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/site-packages/geopandas/geoseries.py", line 800, in apply
    result = super().apply(func, args=args, **kwargs)
  File "/usr/local/lib/python3.14/site-packages/pandas/core/series.py", line 5084, in apply
    ).apply()
      ~~~~~^^
  File "/usr/local/lib/python3.14/site-packages/pandas/core/apply.py", line 1520, in apply
    return self.apply_standard()
           ~~~~~~~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.14/site-packages/pandas/core/apply.py", line 1578, in apply_standard
    mapped = obj._map_values(mapper=curried)
  File "/usr/local/lib/python3.14/site-packages/pandas/core/base.py", line 1020, in _map_values
    return arr.map(mapper, na_action=na_action)
           ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/site-packages/pandas/core/arrays/base.py", line 2745, in map
    return map_array(self, mapper, na_action=na_action)
  File "/usr/local/lib/python3.14/site-packages/pandas/core/algorithms.py", line 1715, in map_array
    return lib.map_infer(values, mapper)
           ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "pandas/_libs/lib.pyx", line 3071, in pandas._libs.lib.map_infer
  File "/work/solve.py", line 56, in <lambda>
    hospitals['nearest_node'] = hospitals.geometry.apply(lambda p: get_nearest_node_proj(nodes_proj, p))
                                                                   ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
  File "/work/solve.py", line 52, in get_nearest_node_proj
    return ox.nearest_nodes(G_proj, point.x, point.y)
           ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/site-packages/osmnx/distance.py", line 368, in nearest_nodes
    raise ImportError(msg)
ImportError: scipy must be installed as an optional dependency to search a projected graph.
14:33
Bash pip install --user scipy
Collecting scipy
  Downloading scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (62 kB)
Requirement already satisfied: numpy<2.7,>=1.26.4 in /usr/local/lib/python3.14/site-packages (from scipy) (2.4.6)
Downloading scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (35.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35.2/35.2 MB 67.6 MB/s  0:00:00
Installing collected packages: scipy
Successfully installed scipy-1.17.1
14:42
Bash python solve.py
Fetching road network and hospitals...
Calculating routes and distances...
Calculating 15-min isochrones...
Saving outputs to GPKG...
Traceback (most recent call last):
  File "/work/solve.py", line 146, in <module>
    dist_matrix_gdf = gpd.GeoDataFrame(distance_matrix_rows, crs=target_crs)
  File "/usr/local/lib/python3.14/site-packages/geopandas/geodataframe.py", line 246, in __init__
    raise ValueError(
    ...<3 lines>...
    )
ValueError: Assigning CRS to a GeoDataFrame without a geometry column is not supported. Supply geometry using the 'geometry=' keyword argument, or by providing a DataFrame with column name 'geometry'
15:30
Edit solve.py
Edited solve.py.
15:33
Bash python solve.py
Fetching road network and hospitals...
Calculating routes and distances...
Calculating 15-min isochrones...
Saving outputs to GPKG...
Traceback (most recent call last):
  File "/work/solve.py", line 146, in <module>
    dist_matrix_gdf = gpd.GeoDataFrame(distance_matrix_rows, geometry=None, crs=target_crs)
  File "/usr/local/lib/python3.14/site-packages/geopandas/geodataframe.py", line 246, in __init__
    raise ValueError(
    ...<3 lines>...
    )
ValueError: Assigning CRS to a GeoDataFrame without a geometry column is not supported. Supply geometry using the 'geometry=' keyword argument, or by providing a DataFrame with column name 'geometry'
16:19
Edit solve.py
Edited solve.py.
16:22
Bash python solve.py
Fetching road network and hospitals...
Calculating routes and distances...
Calculating 15-min isochrones...
Saving outputs to GPKG...
Traceback (most recent call last):
  File "/usr/local/lib/python3.14/site-packages/pandas/core/indexes/base.py", line 3641, in get_loc
    return self._engine.get_loc(casted_key)
           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "pandas/_libs/index.pyx", line 168, in pandas._libs.index.IndexEngine.get_loc
  File "pandas/_libs/index.pyx", line 197, in pandas._libs.index.IndexEngine.get_loc
  File "pandas/_libs/hashtable_class_helper.pxi", line 7668, in pandas._libs.hashtable.PyObjectHashTable.get_item
  File "pandas/_libs/hashtable_class_helper.pxi", line 7676, in pandas._libs.hashtable.PyObjectHashTable.get_item
KeyError: nan

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.14/site-packages/geopandas/geodataframe.py", line 420, in set_geometry
    level = frame[col]
            ~~~~~^^^^^
  File "/usr/local/lib/python3.14/site-packages/geopandas/geodataframe.py", line 1891, in __getitem__
    result = super().__getitem__(key)
  File "/usr/local/lib/python3.14/site-packages/pandas/core/frame.py", line 4378, in __getitem__
    indexer = self.columns.get_loc(key)
  File "/usr/local/lib/python3.14/site-packages/pandas/core/indexes/base.py", line 3648, in get_loc
    raise KeyError(key) from err
KeyError: None

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/work/solve.py", line 150, in <module>
    dist_matrix_gdf.set_geometry(None, inplace=True)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/site-packages/geopandas/geodataframe.py", line 422, in set_geometry
    raise ValueError(f"Unknown column {col}")
ValueError: Unknown column None
17:27
Edit
Invalid JSON arguments: Expecting ',' delimiter: line 1 column 2395 (char 2394)