voxcity 0.5.13__py3-none-any.whl → 0.5.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of voxcity might be problematic. Click here for more details.

@@ -19,17 +19,42 @@ from .grid import grid_to_geodataframe
19
19
 
20
20
  def vectorized_edge_values(G, polygons_gdf, value_col='value'):
21
21
  """
22
- Compute average polygon values along each edge by:
23
- 1) Building an Edge GeoDataFrame in linestring form
24
- 2) Using gpd.overlay or sjoin to get their intersection with polygons
25
- 3) Computing length-weighted average
22
+ Compute average polygon values along each edge in a network graph using vectorized operations.
23
+
24
+ This function performs efficient computation of average values from polygons that intersect
25
+ with network edges. It uses GeoDataFrames for vectorized spatial operations instead of
26
+ iterating over individual edges.
27
+
28
+ Parameters
29
+ ----------
30
+ G : networkx.MultiDiGraph
31
+ OSMnx graph with edges containing either geometry attributes or node coordinates.
32
+ polygons_gdf : geopandas.GeoDataFrame
33
+ GeoDataFrame containing polygons with values to be averaged along edges.
34
+ value_col : str, default='value'
35
+ Name of the column in polygons_gdf containing the values to average.
36
+
37
+ Returns
38
+ -------
39
+ dict
40
+ Dictionary mapping edge tuples (u, v, k) to their computed average values.
41
+ Values are length-weighted averages of intersecting polygon values.
42
+
43
+ Notes
44
+ -----
45
+ The process involves:
46
+ 1. Converting edges to a GeoDataFrame with LineString geometries
47
+ 2. Projecting geometries to a metric CRS (EPSG:3857) for accurate length calculations
48
+ 3. Computing intersections between edges and polygons
49
+ 4. Calculating length-weighted averages of polygon values for each edge
26
50
  """
27
- # 1) Build edge GeoDataFrame (EPSG:4326)
51
+ # Build edge GeoDataFrame in WGS84 (EPSG:4326)
28
52
  records = []
29
53
  for i, (u, v, k, data) in enumerate(G.edges(keys=True, data=True)):
30
54
  if 'geometry' in data:
31
55
  edge_geom = data['geometry']
32
56
  else:
57
+ # Create LineString from node coordinates if no geometry exists
33
58
  start_node = G.nodes[u]
34
59
  end_node = G.nodes[v]
35
60
  edge_geom = LineString([(start_node['x'], start_node['y']),
@@ -46,32 +71,25 @@ def vectorized_edge_values(G, polygons_gdf, value_col='value'):
46
71
  if polygons_gdf.crs != edges_gdf.crs:
47
72
  polygons_gdf = polygons_gdf.to_crs(edges_gdf.crs)
48
73
 
49
- # 2) Use a projected CRS for length calculations
74
+ # Project to Web Mercator for accurate length calculations
50
75
  edges_3857 = edges_gdf.to_crs(epsg=3857)
51
76
  polys_3857 = polygons_gdf.to_crs(epsg=3857)
52
77
 
53
- # 3) Intersection: lines vs polygons -> lines clipped to polygons
54
- # gpd.overlay with how='intersection' can yield partial lines
78
+ # Compute intersections between edges and polygons
55
79
  intersected = gpd.overlay(edges_3857, polys_3857, how='intersection')
56
80
 
57
- # Now each row is a geometry representing the intersection segment,
58
- # with columns from edges + polygons.
59
- # For lines, 'intersection' yields the line portion inside each polygon.
60
- # We'll compute the length, then do a length-weighted average of value_col.
61
-
81
+ # Calculate length-weighted averages
62
82
  intersected['seg_length'] = intersected.geometry.length
63
- # Weighted contribution = seg_length * polygon_value
64
83
  intersected['weighted_val'] = intersected['seg_length'] * intersected[value_col]
65
84
 
66
- # 4) Group by edge_id
85
+ # Group by edge and compute weighted averages
67
86
  grouped = intersected.groupby('edge_id')
68
87
  results = grouped.apply(
69
88
  lambda df: df['weighted_val'].sum() / df['seg_length'].sum()
70
89
  if df['seg_length'].sum() > 0 else np.nan
71
90
  )
72
- # results is a Series with index=edge_id
73
91
 
74
- # 5) Map results back to edges
92
+ # Map results back to edge tuples
75
93
  edge_values = {}
76
94
  for edge_id, val in results.items():
77
95
  rec = edges_gdf.iloc[edge_id]
@@ -86,6 +104,50 @@ def get_network_values(
86
104
  value_name='value',
87
105
  **kwargs
88
106
  ):
107
+ """
108
+ Extract and visualize values from a grid along a street network.
109
+
110
+ This function downloads a street network from OpenStreetMap for a given area,
111
+ computes average grid values along network edges, and optionally visualizes
112
+ the results on an interactive map.
113
+
114
+ Parameters
115
+ ----------
116
+ grid : array-like or geopandas.GeoDataFrame
117
+ Either a grid array of values or a pre-built GeoDataFrame with polygons and values.
118
+ rectangle_vertices : list of tuples
119
+ List of (lon, lat) coordinates defining the bounding rectangle in EPSG:4326.
120
+ meshsize : float
121
+ Size of each grid cell (used only if grid is array-like).
122
+ value_name : str, default='value'
123
+ Name to use for the edge attribute storing computed values.
124
+ **kwargs : dict
125
+ Additional visualization and processing parameters:
126
+ - network_type : str, default='walk'
127
+ Type of street network to download ('walk', 'drive', etc.)
128
+ - vis_graph : bool, default=True
129
+ Whether to display the visualization
130
+ - colormap : str, default='viridis'
131
+ Matplotlib colormap for edge colors
132
+ - vmin, vmax : float, optional
133
+ Value range for color mapping
134
+ - edge_width : float, default=1
135
+ Width of edge lines in visualization
136
+ - fig_size : tuple, default=(15,15)
137
+ Figure size in inches
138
+ - zoom : int, default=16
139
+ Zoom level for basemap
140
+ - basemap_style : ctx.providers, default=CartoDB.Positron
141
+ Contextily basemap provider
142
+ - save_path : str, optional
143
+ Path to save the edge GeoDataFrame as a GeoPackage
144
+
145
+ Returns
146
+ -------
147
+ tuple
148
+ (networkx.MultiDiGraph, geopandas.GeoDataFrame)
149
+ The network graph with computed edge values and edge geometries as a GeoDataFrame.
150
+ """
89
151
  defaults = {
90
152
  'network_type': 'walk',
91
153
  'vis_graph': True,
@@ -174,6 +236,10 @@ def interpolate_points_along_line(line, interval):
174
236
  Interpolate points along a single LineString at a given interval (in meters).
175
237
  If the line is shorter than `interval`, only start/end points are returned.
176
238
 
239
+ This function handles coordinate system transformations to ensure accurate
240
+ distance measurements, working in Web Mercator (EPSG:3857) for distance
241
+ calculations while maintaining WGS84 (EPSG:4326) for input/output.
242
+
177
243
  Parameters
178
244
  ----------
179
245
  line : shapely.geometry.LineString
@@ -184,7 +250,9 @@ def interpolate_points_along_line(line, interval):
184
250
  Returns
185
251
  -------
186
252
  list of shapely.geometry.Point
187
- Points in EPSG:4326 along the line.
253
+ Points in EPSG:4326 along the line, spaced approximately `interval` meters apart.
254
+ For lines shorter than interval, only start and end points are returned.
255
+ For empty lines, an empty list is returned.
188
256
  """
189
257
  if line.is_empty:
190
258
  return []
@@ -219,7 +287,11 @@ def interpolate_points_along_line(line, interval):
219
287
  def gather_interpolation_points(G, interval=10.0, n_jobs=1):
220
288
  """
221
289
  Gather all interpolation points for each edge in the graph into a single GeoDataFrame.
222
- Can be parallelized with `n_jobs`.
290
+ Supports parallel processing for improved performance on large networks.
291
+
292
+ This function processes each edge in the graph, either using its geometry attribute
293
+ or creating a LineString from node coordinates, then interpolates points along it
294
+ at the specified interval.
223
295
 
224
296
  Parameters
225
297
  ----------
@@ -228,12 +300,16 @@ def gather_interpolation_points(G, interval=10.0, n_jobs=1):
228
300
  interval : float, default=10.0
229
301
  Interpolation distance interval in meters.
230
302
  n_jobs : int, default=1
231
- Number of parallel jobs (1 => no parallelization).
303
+ Number of parallel jobs for processing edges. Set to 1 for sequential processing,
304
+ or -1 to use all available CPU cores.
232
305
 
233
306
  Returns
234
307
  -------
235
308
  gpd.GeoDataFrame
236
- Columns: edge_id, index_in_edge, geometry (EPSG:4326).
309
+ GeoDataFrame in EPSG:4326 with columns:
310
+ - edge_id: Index of the edge in the graph
311
+ - index_in_edge: Position of the point along its edge
312
+ - geometry: Point geometry
237
313
  """
238
314
  edges = list(G.edges(keys=True, data=True))
239
315
 
@@ -268,21 +344,26 @@ def gather_interpolation_points(G, interval=10.0, n_jobs=1):
268
344
 
269
345
  def fetch_elevations_for_points(points_gdf_3857, dem_gdf_3857, elevation_col='value'):
270
346
  """
271
- Do a spatial join (nearest) in a projected CRS (EPSG:3857) to fetch DEM elevations.
347
+ Perform a spatial join to fetch DEM elevations for interpolated points.
348
+
349
+ Uses nearest neighbor matching in projected coordinates (EPSG:3857) to ensure
350
+ accurate distance calculations when finding the closest DEM cell for each point.
272
351
 
273
352
  Parameters
274
353
  ----------
275
354
  points_gdf_3857 : gpd.GeoDataFrame
276
- Interpolation points in EPSG:3857.
355
+ Interpolation points in EPSG:3857 projection.
277
356
  dem_gdf_3857 : gpd.GeoDataFrame
278
- DEM polygons in EPSG:3857, must have `elevation_col`.
357
+ DEM polygons in EPSG:3857 projection, containing elevation values.
279
358
  elevation_col : str, default='value'
280
- Column with elevation values in dem_gdf_3857.
359
+ Name of the column containing elevation values in dem_gdf_3857.
281
360
 
282
361
  Returns
283
362
  -------
284
363
  gpd.GeoDataFrame
285
- A copy of points_gdf_3857 with new column 'elevation'.
364
+ Copy of points_gdf_3857 with additional columns:
365
+ - elevation: Elevation value from nearest DEM cell
366
+ - dist_to_poly: Distance to nearest DEM cell
286
367
  """
287
368
  joined = gpd.sjoin_nearest(
288
369
  points_gdf_3857,
@@ -296,10 +377,25 @@ def fetch_elevations_for_points(points_gdf_3857, dem_gdf_3857, elevation_col='va
296
377
 
297
378
  def compute_slope_for_group(df):
298
379
  """
299
- Given a subset of points for a single edge, compute average slope between
300
- consecutive points, using columns: geometry, elevation, index_in_edge.
380
+ Compute average slope between consecutive points along a single edge.
381
+
382
+ Slopes are calculated as absolute percentage grade (rise/run * 100) between
383
+ consecutive points, then averaged for the entire edge. Points must be in
384
+ EPSG:3857 projection for accurate horizontal distance calculations.
385
+
386
+ Parameters
387
+ ----------
388
+ df : pd.DataFrame
389
+ DataFrame containing points for a single edge with columns:
390
+ - geometry: Point geometries in EPSG:3857
391
+ - elevation: Elevation values in meters
392
+ - index_in_edge: Position along the edge for sorting
301
393
 
302
- Note: We assume df is already in EPSG:3857 for direct distance calculations.
394
+ Returns
395
+ -------
396
+ float
397
+ Average slope as a percentage, or np.nan if no valid slopes can be computed
398
+ (e.g., when points are coincident or no elevation change).
303
399
  """
304
400
  # Sort by position along the edge
305
401
  df = df.sort_values("index_in_edge")
@@ -326,19 +422,28 @@ def compute_slope_for_group(df):
326
422
 
327
423
  def calculate_edge_slopes_from_join(joined_points_gdf, n_edges):
328
424
  """
329
- Calculate average slopes for each edge by grouping joined points.
425
+ Calculate average slopes for all edges in the network from interpolated points.
426
+
427
+ This function groups points by edge_id and computes the average slope for each edge
428
+ using the compute_slope_for_group function. It ensures all edges in the original
429
+ graph have a slope value, even if no valid slope could be computed.
330
430
 
331
431
  Parameters
332
432
  ----------
333
433
  joined_points_gdf : gpd.GeoDataFrame
334
- Must have columns: edge_id, index_in_edge, elevation, geometry (EPSG:3857).
434
+ Points with elevations in EPSG:3857, must have columns:
435
+ - edge_id: Index of the edge in the graph
436
+ - index_in_edge: Position along the edge
437
+ - elevation: Elevation value
438
+ - geometry: Point geometry
335
439
  n_edges : int
336
- Number of edges from the graph.
440
+ Total number of edges in the original graph.
337
441
 
338
442
  Returns
339
443
  -------
340
444
  dict
341
- edge_id -> average slope (in %).
445
+ Dictionary mapping edge_id to average slope (in %). Edges with no valid
446
+ slope calculation are assigned np.nan.
342
447
  """
343
448
  # We'll group by edge_id, ignoring the group columns in apply (pandas >= 2.1).
344
449
  # If your pandas version < 2.1, just do a column subset after groupby.
@@ -369,27 +474,72 @@ def analyze_network_slopes(
369
474
  **kwargs
370
475
  ):
371
476
  """
372
- Analyze and visualize network slopes based on DEM data, using vectorized + parallel methods.
477
+ Analyze and visualize street network slopes using Digital Elevation Model (DEM) data.
478
+
479
+ This function performs a comprehensive analysis of street network slopes by:
480
+ 1. Converting DEM data to a GeoDataFrame of elevation polygons
481
+ 2. Downloading the street network from OpenStreetMap
482
+ 3. Interpolating points along network edges
483
+ 4. Matching points to DEM elevations
484
+ 5. Computing slopes between consecutive points
485
+ 6. Aggregating slopes per edge
486
+ 7. Optionally visualizing results on an interactive map
487
+
488
+ The analysis uses appropriate coordinate transformations between WGS84 (EPSG:4326)
489
+ for geographic operations and Web Mercator (EPSG:3857) for distance calculations.
373
490
 
374
491
  Parameters
375
492
  ----------
376
493
  dem_grid : array-like
377
- DEM grid data.
494
+ Digital Elevation Model grid data containing elevation values.
378
495
  meshsize : float
379
- Mesh grid size.
496
+ Size of each DEM grid cell.
380
497
  value_name : str, default='slope'
381
- Column name for slopes assigned to each edge.
498
+ Name to use for the slope attribute in output data.
382
499
  interval : float, default=10.0
383
- Interpolation distance in meters.
500
+ Distance in meters between interpolated points along edges.
384
501
  n_jobs : int, default=1
385
- Parallelization for edge interpolation (1 => sequential).
502
+ Number of parallel jobs for edge processing.
386
503
  **kwargs : dict
387
- Additional parameters:
388
- - rectangle_vertices : list of (x, y) in EPSG:4326
389
- - network_type : str, default='walk'
390
- - vis_graph : bool, default=True
391
- - colormap, vmin, vmax, edge_width, fig_size, zoom, basemap_style, alpha
392
- - output_directory, output_file_name
504
+ Additional configuration parameters:
505
+ - rectangle_vertices : list of (lon, lat), required
506
+ Coordinates defining the analysis area in EPSG:4326
507
+ - network_type : str, default='walk'
508
+ Type of street network to download
509
+ - vis_graph : bool, default=True
510
+ Whether to create visualization
511
+ - colormap : str, default='viridis'
512
+ Matplotlib colormap for slope visualization
513
+ - vmin, vmax : float, optional
514
+ Value range for slope coloring
515
+ - edge_width : float, default=1
516
+ Width of edge lines in plot
517
+ - fig_size : tuple, default=(15,15)
518
+ Figure size in inches
519
+ - zoom : int, default=16
520
+ Zoom level for basemap
521
+ - basemap_style : ctx.providers, default=CartoDB.Positron
522
+ Contextily basemap provider
523
+ - output_directory : str, optional
524
+ Directory to save results
525
+ - output_file_name : str, default='network_slopes'
526
+ Base name for output files
527
+ - alpha : float, default=1.0
528
+ Transparency of edge lines in visualization
529
+
530
+ Returns
531
+ -------
532
+ tuple
533
+ (networkx.MultiDiGraph, geopandas.GeoDataFrame)
534
+ - Graph with slope values as edge attributes
535
+ - GeoDataFrame of edges with geometries and slope values
536
+
537
+ Notes
538
+ -----
539
+ - Slopes are calculated as absolute percentage grades (rise/run * 100)
540
+ - Edge slopes are length-weighted averages of point-to-point slopes
541
+ - The visualization includes a basemap and legend showing slope percentages
542
+ - If output_directory is specified, results are saved as a GeoPackage
393
543
  """
394
544
  defaults = {
395
545
  'rectangle_vertices': None,
@@ -482,7 +632,7 @@ def analyze_network_slopes(
482
632
  # 10) Visualization
483
633
  if settings['vis_graph']:
484
634
  # Create a Polygon from the rectangle vertices
485
- rectangle_polygon = Polygon(rectangle_vertices)
635
+ rectangle_polygon = Polygon(settings['rectangle_vertices'])
486
636
 
487
637
  # Convert the rectangle polygon to the same CRS as edge_gdf_web
488
638
  rectangle_gdf = gpd.GeoDataFrame(index=[0], crs='epsg:4326', geometry=[rectangle_polygon])
@@ -526,22 +676,5 @@ def analyze_network_slopes(
526
676
 
527
677
  # Show the plot
528
678
  plt.show()
529
- # edge_gdf_web = edge_gdf.to_crs(epsg=3857)
530
- # fig, ax = plt.subplots(figsize=settings['fig_size'])
531
- # edge_gdf_web.plot(
532
- # column=value_name,
533
- # ax=ax,
534
- # cmap=settings['colormap'],
535
- # legend=True,
536
- # vmin=settings['vmin'],
537
- # vmax=settings['vmax'],
538
- # linewidth=settings['edge_width'],
539
- # alpha=settings['alpha'],
540
- # legend_kwds={'label': f"{value_name} (%)"}
541
- # )
542
- # ctx.add_basemap(ax, source=settings['basemap_style'], zoom=settings['zoom'])
543
- # ax.set_axis_off()
544
- # plt.title(f'Network {value_name} Analysis', pad=20)
545
- # plt.show()
546
679
 
547
680
  return G, edge_gdf