voxcity 0.3.9__py3-none-any.whl → 0.3.11__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.

voxcity/file/geojson.py CHANGED
@@ -187,118 +187,325 @@ def extract_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_da
187
187
 
188
188
  return updated_geojson_data_0
189
189
 
190
- from typing import List, Dict
191
- from shapely.geometry import shape
192
- from shapely.errors import GEOSException
193
- import numpy as np
194
-
195
- def complement_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_data_1: List[Dict]) -> List[Dict]:
196
- """
197
- Complement building heights in one GeoJSON dataset with data from another and add non-intersecting buildings.
190
+ # from typing import List, Dict
191
+ # from shapely.geometry import shape
192
+ # from shapely.errors import GEOSException
193
+ # import numpy as np
194
+
195
+ # def complement_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_data_1: List[Dict]) -> List[Dict]:
196
+ # """
197
+ # Complement building heights in one GeoJSON dataset with data from another and add non-intersecting buildings.
198
198
 
199
- Args:
200
- geojson_data_0 (List[Dict]): Primary GeoJSON features to update with heights
201
- geojson_data_1 (List[Dict]): Reference GeoJSON features containing height data
199
+ # Args:
200
+ # geojson_data_0 (List[Dict]): Primary GeoJSON features to update with heights
201
+ # geojson_data_1 (List[Dict]): Reference GeoJSON features containing height data
202
202
 
203
- Returns:
204
- List[Dict]: Updated GeoJSON features with complemented heights and additional buildings
205
- """
206
- # Convert primary dataset to Shapely polygons for intersection checking
207
- existing_buildings = []
208
- for feature in geojson_data_0:
209
- geom = shape(feature['geometry'])
210
- existing_buildings.append(geom)
203
+ # Returns:
204
+ # List[Dict]: Updated GeoJSON features with complemented heights and additional buildings
205
+ # """
206
+ # # Convert primary dataset to Shapely polygons for intersection checking
207
+ # existing_buildings = []
208
+ # for feature in geojson_data_0:
209
+ # geom = shape(feature['geometry'])
210
+ # existing_buildings.append(geom)
211
211
 
212
- # Convert reference dataset to Shapely polygons with height info
213
- reference_buildings = []
214
- for feature in geojson_data_1:
215
- geom = shape(feature['geometry'])
216
- height = feature['properties']['height']
217
- reference_buildings.append((geom, height, feature))
212
+ # # Convert reference dataset to Shapely polygons with height info
213
+ # reference_buildings = []
214
+ # for feature in geojson_data_1:
215
+ # geom = shape(feature['geometry'])
216
+ # height = feature['properties']['height']
217
+ # reference_buildings.append((geom, height, feature))
218
218
 
219
- # Initialize counters for statistics
220
- count_0 = 0 # Buildings without height
221
- count_1 = 0 # Buildings updated with height
222
- count_2 = 0 # Buildings with no height data found
223
- count_3 = 0 # New non-intersecting buildings added
219
+ # # Initialize counters for statistics
220
+ # count_0 = 0 # Buildings without height
221
+ # count_1 = 0 # Buildings updated with height
222
+ # count_2 = 0 # Buildings with no height data found
223
+ # count_3 = 0 # New non-intersecting buildings added
224
224
 
225
- # Process primary dataset and update heights where needed
226
- updated_geojson_data_0 = []
227
- for feature in geojson_data_0:
228
- geom = shape(feature['geometry'])
229
- height = feature['properties']['height']
230
- if height == 0:
231
- count_0 += 1
232
- # Calculate weighted average height based on overlapping areas
233
- overlapping_height_area = 0
234
- overlapping_area = 0
235
- for ref_geom, ref_height, _ in reference_buildings:
236
- try:
237
- if geom.intersects(ref_geom):
238
- overlap_area = geom.intersection(ref_geom).area
239
- overlapping_height_area += ref_height * overlap_area
240
- overlapping_area += overlap_area
241
- except GEOSException as e:
242
- # Try to fix invalid geometries
243
- try:
244
- fixed_ref_geom = ref_geom.buffer(0)
245
- if geom.intersects(fixed_ref_geom):
246
- overlap_area = geom.intersection(ref_geom).area
247
- overlapping_height_area += ref_height * overlap_area
248
- overlapping_area += overlap_area
249
- except Exception as fix_error:
250
- print(f"Failed to fix polygon")
251
- continue
225
+ # # Process primary dataset and update heights where needed
226
+ # updated_geojson_data_0 = []
227
+ # for feature in geojson_data_0:
228
+ # geom = shape(feature['geometry'])
229
+ # height = feature['properties']['height']
230
+ # if height == 0:
231
+ # count_0 += 1
232
+ # # Calculate weighted average height based on overlapping areas
233
+ # overlapping_height_area = 0
234
+ # overlapping_area = 0
235
+ # for ref_geom, ref_height, _ in reference_buildings:
236
+ # try:
237
+ # if geom.intersects(ref_geom):
238
+ # overlap_area = geom.intersection(ref_geom).area
239
+ # overlapping_height_area += ref_height * overlap_area
240
+ # overlapping_area += overlap_area
241
+ # except GEOSException as e:
242
+ # # Try to fix invalid geometries
243
+ # try:
244
+ # fixed_ref_geom = ref_geom.buffer(0)
245
+ # if geom.intersects(fixed_ref_geom):
246
+ # overlap_area = geom.intersection(ref_geom).area
247
+ # overlapping_height_area += ref_height * overlap_area
248
+ # overlapping_area += overlap_area
249
+ # except Exception as fix_error:
250
+ # print(f"Failed to fix polygon")
251
+ # continue
252
252
 
253
- # Update height if overlapping buildings found
254
- if overlapping_height_area > 0:
255
- count_1 += 1
256
- new_height = overlapping_height_area / overlapping_area
257
- feature['properties']['height'] = new_height
258
- else:
259
- count_2 += 1
260
- feature['properties']['height'] = np.nan
253
+ # # Update height if overlapping buildings found
254
+ # if overlapping_height_area > 0:
255
+ # count_1 += 1
256
+ # new_height = overlapping_height_area / overlapping_area
257
+ # feature['properties']['height'] = new_height
258
+ # else:
259
+ # count_2 += 1
260
+ # feature['properties']['height'] = np.nan
261
261
 
262
- updated_geojson_data_0.append(feature)
262
+ # updated_geojson_data_0.append(feature)
263
263
 
264
- # Add non-intersecting buildings from reference dataset
265
- for ref_geom, ref_height, ref_feature in reference_buildings:
266
- has_intersection = False
267
- try:
268
- # Check if reference building intersects with any existing building
269
- for existing_geom in existing_buildings:
270
- if ref_geom.intersects(existing_geom):
271
- has_intersection = True
272
- break
264
+ # # Add non-intersecting buildings from reference dataset
265
+ # for ref_geom, ref_height, ref_feature in reference_buildings:
266
+ # has_intersection = False
267
+ # try:
268
+ # # Check if reference building intersects with any existing building
269
+ # for existing_geom in existing_buildings:
270
+ # if ref_geom.intersects(existing_geom):
271
+ # has_intersection = True
272
+ # break
273
273
 
274
- # Add building if it doesn't intersect with any existing ones
275
- if not has_intersection:
276
- updated_geojson_data_0.append(ref_feature)
277
- count_3 += 1
274
+ # # Add building if it doesn't intersect with any existing ones
275
+ # if not has_intersection:
276
+ # updated_geojson_data_0.append(ref_feature)
277
+ # count_3 += 1
278
278
 
279
- except GEOSException as e:
280
- # Try to fix invalid geometries
281
- try:
282
- fixed_ref_geom = ref_geom.buffer(0)
283
- for existing_geom in existing_buildings:
284
- if fixed_ref_geom.intersects(existing_geom):
285
- has_intersection = True
286
- break
279
+ # except GEOSException as e:
280
+ # # Try to fix invalid geometries
281
+ # try:
282
+ # fixed_ref_geom = ref_geom.buffer(0)
283
+ # for existing_geom in existing_buildings:
284
+ # if fixed_ref_geom.intersects(existing_geom):
285
+ # has_intersection = True
286
+ # break
287
287
 
288
- if not has_intersection:
289
- updated_geojson_data_0.append(ref_feature)
290
- count_3 += 1
291
- except Exception as fix_error:
292
- print(f"Failed to process non-intersecting building")
293
- continue
288
+ # if not has_intersection:
289
+ # updated_geojson_data_0.append(ref_feature)
290
+ # count_3 += 1
291
+ # except Exception as fix_error:
292
+ # print(f"Failed to process non-intersecting building")
293
+ # continue
294
294
 
295
- # Print statistics about updates
296
- if count_0 > 0:
297
- print(f"{count_0} of the total {len(geojson_data_0)} building footprint from base source did not have height data.")
298
- print(f"For {count_1} of these building footprints without height, values from complement source were assigned.")
299
- print(f"{count_3} non-intersecting buildings from Microsoft Building Footprints were added to the output.")
295
+ # # Print statistics about updates
296
+ # if count_0 > 0:
297
+ # print(f"{count_0} of the total {len(geojson_data_0)} building footprint from base source did not have height data.")
298
+ # print(f"For {count_1} of these building footprints without height, values from complement source were assigned.")
299
+ # print(f"{count_3} non-intersecting buildings from Microsoft Building Footprints were added to the output.")
300
300
 
301
- return updated_geojson_data_0
301
+ # return updated_geojson_data_0
302
+
303
+ import numpy as np
304
+ import geopandas as gpd
305
+ import pandas as pd
306
+ from shapely.geometry import shape
307
+ from shapely.errors import GEOSException
308
+
309
+ def geojson_to_gdf(geojson_data, id_col='id'):
310
+ """
311
+ Convert a list of GeoJSON-like dict features into a GeoDataFrame.
312
+
313
+ Args:
314
+ geojson_data (List[Dict]): A list of feature dicts (Fiona-like).
315
+ id_col (str): Name of property to use as an identifier. If not found,
316
+ we'll try to create a unique ID.
317
+
318
+ Returns:
319
+ gpd.GeoDataFrame: GeoDataFrame with geometry and property columns.
320
+ """
321
+ # Build lists for geometry and properties
322
+ geometries = []
323
+ all_props = []
324
+
325
+ for i, feature in enumerate(geojson_data):
326
+ # Extract geometry
327
+ geom = feature.get('geometry')
328
+ shapely_geom = shape(geom) if geom else None
329
+
330
+ # Extract properties
331
+ props = feature.get('properties', {})
332
+
333
+ # If an ID column is missing, create one
334
+ if id_col not in props:
335
+ props[id_col] = i # fallback ID
336
+
337
+ # Capture geometry and all props
338
+ geometries.append(shapely_geom)
339
+ all_props.append(props)
340
+
341
+ gdf = gpd.GeoDataFrame(all_props, geometry=geometries, crs="EPSG:4326")
342
+ return gdf
343
+
344
+
345
+ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
346
+ primary_id='id', ref_id='id'):
347
+ """
348
+ Use a vectorized approach with GeoPandas to:
349
+ 1) Convert both datasets to GeoDataFrames
350
+ 2) Find intersections and compute weighted average heights
351
+ 3) Update heights in the primary dataset
352
+ 4) Add non-intersecting buildings from the reference dataset
353
+
354
+ Args:
355
+ geojson_data_0 (List[Dict]): Primary GeoJSON-like features
356
+ geojson_data_1 (List[Dict]): Reference GeoJSON-like features
357
+ primary_id (str): Name of the unique identifier in primary dataset's properties
358
+ ref_id (str): Name of the unique identifier in reference dataset's properties
359
+
360
+ Returns:
361
+ gpd.GeoDataFrame: Updated GeoDataFrame (including new buildings).
362
+ You can convert it back to a list of dict features if needed.
363
+ """
364
+ # ----------------------------------------------------------------
365
+ # 1) Convert primary and reference data to GeoDataFrames
366
+ # ----------------------------------------------------------------
367
+ gdf_primary = geojson_to_gdf(geojson_data_0, id_col=primary_id)
368
+ gdf_ref = geojson_to_gdf(geojson_data_1, id_col=ref_id)
369
+
370
+ # Ensure both are in the same CRS, e.g. EPSG:4326 or some projected CRS
371
+ # If needed, do something like:
372
+ # gdf_primary = gdf_primary.to_crs("EPSG:xxxx")
373
+ # gdf_ref = gdf_ref.to_crs("EPSG:xxxx")
374
+
375
+ # Make sure height columns exist
376
+ if 'height' not in gdf_primary.columns:
377
+ gdf_primary['height'] = 0.0
378
+ if 'height' not in gdf_ref.columns:
379
+ gdf_ref['height'] = 0.0
380
+
381
+ # ----------------------------------------------------------------
382
+ # 2) Intersection to compute areas for overlapping buildings
383
+ # ----------------------------------------------------------------
384
+ # We'll rename columns to avoid collision after overlay
385
+ gdf_primary = gdf_primary.rename(columns={'height': 'height_primary'})
386
+ gdf_ref = gdf_ref.rename(columns={'height': 'height_ref'})
387
+
388
+ # We perform an 'intersection' overlay to get the overlapping polygons
389
+ intersect_gdf = gpd.overlay(gdf_primary, gdf_ref, how='intersection')
390
+
391
+ # Compute intersection area
392
+ intersect_gdf['intersect_area'] = intersect_gdf.area
393
+ # Weighted area (height_ref * intersect_area)
394
+ intersect_gdf['height_area'] = intersect_gdf['height_ref'] * intersect_gdf['intersect_area']
395
+
396
+ # ----------------------------------------------------------------
397
+ # 3) Aggregate to get weighted average height for each primary building
398
+ # ----------------------------------------------------------------
399
+ # We group by the primary building ID, summing up the area and the 'height_area'
400
+ group_cols = {
401
+ 'height_area': 'sum',
402
+ 'intersect_area': 'sum'
403
+ }
404
+ grouped = intersect_gdf.groupby(gdf_primary[primary_id].name).agg(group_cols)
405
+
406
+ # Weighted average
407
+ grouped['weighted_height'] = grouped['height_area'] / grouped['intersect_area']
408
+
409
+ # ----------------------------------------------------------------
410
+ # 4) Merge aggregated results back to the primary GDF
411
+ # ----------------------------------------------------------------
412
+ # After merging, the primary GDF will have a column 'weighted_height'
413
+ gdf_primary = gdf_primary.merge(grouped['weighted_height'],
414
+ left_on=primary_id,
415
+ right_index=True,
416
+ how='left')
417
+
418
+ # Where primary had zero or missing height, we assign the new weighted height
419
+ zero_or_nan_mask = (gdf_primary['height_primary'] == 0) | (gdf_primary['height_primary'].isna())
420
+ gdf_primary.loc[zero_or_nan_mask, 'height_primary'] = gdf_primary.loc[zero_or_nan_mask, 'weighted_height']
421
+
422
+ # For any building that had no overlap, 'weighted_height' might be NaN.
423
+ # Keep it as NaN or set to 0 if you prefer:
424
+ gdf_primary['height_primary'] = gdf_primary['height_primary'].fillna(np.nan)
425
+
426
+ # ----------------------------------------------------------------
427
+ # 5) Identify reference buildings that do not intersect any primary building
428
+ # ----------------------------------------------------------------
429
+ # Another overlay or spatial join can do this:
430
+ # Option A: use 'difference' on reference to get non-overlapping parts, but that can chop polygons.
431
+ # Option B: check building-level intersection. We'll do a bounding test with sjoin.
432
+
433
+ # For building-level intersection, do a left join of ref onto primary.
434
+ # Then we'll identify which reference IDs are missing from the intersection result.
435
+ sjoin_gdf = gpd.sjoin(gdf_ref, gdf_primary, how='left', op='intersects')
436
+
437
+ # All reference buildings that did not intersect any primary building
438
+ non_intersect_ids = sjoin_gdf.loc[sjoin_gdf[primary_id].isna(), ref_id].unique()
439
+
440
+ # Extract them from the original reference GDF
441
+ gdf_ref_non_intersect = gdf_ref[gdf_ref[ref_id].isin(non_intersect_ids)]
442
+
443
+ # We'll rename columns back to 'height' to be consistent
444
+ gdf_ref_non_intersect = gdf_ref_non_intersect.rename(columns={'height_ref': 'height'})
445
+
446
+ # Also rename any other properties you prefer. For clarity, keep an ID so you know they came from reference.
447
+
448
+ # ----------------------------------------------------------------
449
+ # 6) Combine the updated primary GDF with the new reference buildings
450
+ # ----------------------------------------------------------------
451
+ # First, rename columns in updated primary GDF
452
+ gdf_primary = gdf_primary.rename(columns={'height_primary': 'height'})
453
+ # Drop the 'weighted_height' column to clean up
454
+ if 'weighted_height' in gdf_primary.columns:
455
+ gdf_primary.drop(columns='weighted_height', inplace=True)
456
+
457
+ # Concatenate
458
+ final_gdf = pd.concat([gdf_primary, gdf_ref_non_intersect], ignore_index=True)
459
+
460
+ # ----------------------------------------------------------------
461
+ # Return the combined GeoDataFrame
462
+ # (You can convert it back to a list of GeoJSON-like dictionaries)
463
+ # ----------------------------------------------------------------
464
+ return final_gdf
465
+
466
+
467
+ def gdf_to_geojson_dicts(gdf, id_col='id'):
468
+ """
469
+ Convert a GeoDataFrame to a list of dicts similar to GeoJSON features.
470
+ """
471
+ records = gdf.to_dict(orient='records')
472
+ features = []
473
+ for rec in records:
474
+ # geometry is separate
475
+ geom = rec.pop('geometry', None)
476
+ if geom is not None:
477
+ geom = geom.__geo_interface__
478
+ # use or set ID
479
+ feature_id = rec.get(id_col, None)
480
+ props = {k: v for k, v in rec.items() if k != id_col}
481
+ # build GeoJSON-like feature dict
482
+ feature = {
483
+ 'type': 'Feature',
484
+ 'properties': props,
485
+ 'geometry': geom
486
+ }
487
+ features.append(feature)
488
+
489
+ return features
490
+
491
+
492
+ def complement_building_heights_from_geojson(geojson_data_0, geojson_data_1,
493
+ primary_id='id', ref_id='id'):
494
+ """
495
+ High-level function that wraps the GeoPandas approach end-to-end.
496
+ Returns a list of GeoJSON-like feature dicts.
497
+ """
498
+ # 1) Complement building heights using the GeoDataFrame approach
499
+ final_gdf = complement_building_heights_gdf(
500
+ geojson_data_0,
501
+ geojson_data_1,
502
+ primary_id=primary_id,
503
+ ref_id=ref_id
504
+ )
505
+
506
+ # 2) Convert back to geojson-like dict format
507
+ updated_features = gdf_to_geojson_dicts(final_gdf, id_col=primary_id)
508
+ return updated_features
302
509
 
303
510
  def load_geojsons_from_multiple_gz(file_paths):
304
511
  """
voxcity/geo/network.py CHANGED
@@ -15,103 +15,75 @@ from joblib import Parallel, delayed
15
15
 
16
16
  from .grid import grid_to_geodataframe
17
17
 
18
- def calculate_edge_values(G, gdf, value_col='value'):
18
+ def vectorized_edge_values(G, polygons_gdf, value_col='value'):
19
19
  """
20
- Calculate average values for graph edges based on intersection with polygons.
21
-
22
- Parameters:
23
- -----------
24
- G : NetworkX Graph
25
- Input graph with edges to analyze
26
- gdf : GeoDataFrame
27
- Grid containing polygons with values
28
- value_col : str, default 'value'
29
- Name of the column containing values in the grid
30
-
31
- Returns:
32
- --------
33
- dict
34
- Dictionary with edge identifiers (u,v,k) as keys and average values as values
20
+ Compute average polygon values along each edge by:
21
+ 1) Building an Edge GeoDataFrame in linestring form
22
+ 2) Using gpd.overlay or sjoin to get their intersection with polygons
23
+ 3) Computing length-weighted average
35
24
  """
36
- edge_values = {}
37
- for u, v, k, data in G.edges(data=True, keys=True):
25
+ # 1) Build edge GeoDataFrame (EPSG:4326)
26
+ records = []
27
+ for i, (u, v, k, data) in enumerate(G.edges(keys=True, data=True)):
38
28
  if 'geometry' in data:
39
- edge_line = data['geometry']
29
+ edge_geom = data['geometry']
40
30
  else:
41
31
  start_node = G.nodes[u]
42
32
  end_node = G.nodes[v]
43
- edge_line = LineString([(start_node['x'], start_node['y']),
44
- (end_node['x'], end_node['y'])])
45
-
46
- intersecting_polys = gdf[gdf.geometry.intersects(edge_line)]
47
-
48
- if len(intersecting_polys) > 0:
49
- total_length = 0
50
- weighted_sum = 0
51
-
52
- for idx, poly in intersecting_polys.iterrows():
53
- if pd.isna(poly[value_col]):
54
- continue
55
-
56
- intersection = edge_line.intersection(poly.geometry)
57
- if not intersection.is_empty:
58
- length = intersection.length
59
- total_length += length
60
- weighted_sum += length * poly[value_col]
61
-
62
- if total_length > 0:
63
- avg_value = weighted_sum / total_length
64
- edge_values[(u, v, k)] = avg_value
65
- else:
66
- edge_values[(u, v, k)] = np.nan
67
- else:
68
- edge_values[(u, v, k)] = np.nan
69
-
33
+ edge_geom = LineString([(start_node['x'], start_node['y']),
34
+ (end_node['x'], end_node['y'])])
35
+ records.append({
36
+ 'edge_id': i, # unique ID for grouping
37
+ 'u': u,
38
+ 'v': v,
39
+ 'k': k,
40
+ 'geometry': edge_geom
41
+ })
42
+
43
+ edges_gdf = gpd.GeoDataFrame(records, crs="EPSG:4326")
44
+ if polygons_gdf.crs != edges_gdf.crs:
45
+ polygons_gdf = polygons_gdf.to_crs(edges_gdf.crs)
46
+
47
+ # 2) Use a projected CRS for length calculations
48
+ edges_3857 = edges_gdf.to_crs(epsg=3857)
49
+ polys_3857 = polygons_gdf.to_crs(epsg=3857)
50
+
51
+ # 3) Intersection: lines vs polygons -> lines clipped to polygons
52
+ # gpd.overlay with how='intersection' can yield partial lines
53
+ intersected = gpd.overlay(edges_3857, polys_3857, how='intersection')
54
+
55
+ # Now each row is a geometry representing the intersection segment,
56
+ # with columns from edges + polygons.
57
+ # For lines, 'intersection' yields the line portion inside each polygon.
58
+ # We'll compute the length, then do a length-weighted average of value_col.
59
+
60
+ intersected['seg_length'] = intersected.geometry.length
61
+ # Weighted contribution = seg_length * polygon_value
62
+ intersected['weighted_val'] = intersected['seg_length'] * intersected[value_col]
63
+
64
+ # 4) Group by edge_id
65
+ grouped = intersected.groupby('edge_id')
66
+ results = grouped.apply(
67
+ lambda df: df['weighted_val'].sum() / df['seg_length'].sum()
68
+ if df['seg_length'].sum() > 0 else np.nan
69
+ )
70
+ # results is a Series with index=edge_id
71
+
72
+ # 5) Map results back to edges
73
+ edge_values = {}
74
+ for edge_id, val in results.items():
75
+ rec = edges_gdf.iloc[edge_id]
76
+ edge_values[(rec['u'], rec['v'], rec['k'])] = val
77
+
70
78
  return edge_values
71
79
 
72
- def get_network_values(grid, rectangle_vertices, meshsize, value_name='value', **kwargs):
73
- """
74
- Analyze and visualize network values based on grid intersections.
75
-
76
- Parameters:
77
- -----------
78
- grid : GeoDataFrame
79
- Input grid with geometries and values
80
- rectangle_vertices : list
81
- List of coordinates defining the bounding box vertices
82
- meshsize : float
83
- Size of the mesh grid
84
- value_name : str, default 'value'
85
- Name of the column containing values in the grid
86
- **kwargs : dict
87
- Optional arguments including:
88
- - network_type : str, default 'walk'
89
- Type of network to download ('walk', 'drive', 'all', etc.)
90
- - vis_graph : bool, default True
91
- Whether to visualize the graph
92
- - colormap : str, default 'viridis'
93
- Matplotlib colormap name for visualization
94
- - vmin : float, optional
95
- Minimum value for color scaling
96
- - vmax : float, optional
97
- Maximum value for color scaling
98
- - edge_width : float, default 1
99
- Width of the edges in visualization
100
- - fig_size : tuple, default (15,15)
101
- Figure size for visualization
102
- - zoom : int, default 16
103
- Zoom level for the basemap
104
- - basemap_style : ctx.providers, default CartoDB.Positron
105
- Contextily basemap provider
106
- - save_path : str, optional
107
- Path to save the output GeoPackage
108
-
109
- Returns:
110
- --------
111
- tuple : (NetworkX Graph, GeoDataFrame)
112
- Returns the processed graph and edge GeoDataFrame
113
- """
114
- # Set default values for optional arguments
80
+ def get_network_values(
81
+ grid,
82
+ rectangle_vertices,
83
+ meshsize,
84
+ value_name='value',
85
+ **kwargs
86
+ ):
115
87
  defaults = {
116
88
  'network_type': 'walk',
117
89
  'vis_graph': True,
@@ -124,79 +96,71 @@ def get_network_values(grid, rectangle_vertices, meshsize, value_name='value', *
124
96
  'basemap_style': ctx.providers.CartoDB.Positron,
125
97
  'save_path': None
126
98
  }
127
-
128
- # Update defaults with provided kwargs
129
- settings = defaults.copy()
130
- settings.update(kwargs)
99
+ settings = {**defaults, **kwargs}
131
100
 
132
- grid_gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
133
-
134
- # Extract bounding box coordinates
101
+ # Build polygons GDF if needed
102
+ polygons_gdf = (grid if isinstance(grid, gpd.GeoDataFrame)
103
+ else grid_to_geodataframe(grid, rectangle_vertices, meshsize))
104
+ if polygons_gdf.crs is None:
105
+ polygons_gdf.set_crs(epsg=4326, inplace=True)
106
+
107
+ # BBox
135
108
  north, south = rectangle_vertices[1][1], rectangle_vertices[0][1]
136
- east, west = rectangle_vertices[2][0], rectangle_vertices[0][0]
109
+ east, west = rectangle_vertices[2][0], rectangle_vertices[0][0]
137
110
  bbox = (west, south, east, north)
138
-
139
- # Download the road network
140
- G = ox.graph.graph_from_bbox(bbox=bbox, network_type=settings['network_type'], simplify=True)
141
-
142
- # Calculate edge values using the separate function
143
- edge_values = calculate_edge_values(G, grid_gdf, "value")
144
-
145
- # Add values to the graph
146
- nx.set_edge_attributes(G, edge_values, value_name)
147
-
148
- # Create GeoDataFrame from edges
111
+
112
+ # Download OSMnx network
113
+ G = ox.graph.graph_from_bbox(
114
+ bbox=bbox,
115
+ network_type=settings['network_type'],
116
+ simplify=True
117
+ )
118
+
119
+ # Compute edge values with the vectorized function
120
+ edge_values = vectorized_edge_values(G, polygons_gdf, value_col="value")
121
+ nx.set_edge_attributes(G, edge_values, name=value_name)
122
+
123
+ # Build edge GDF
149
124
  edges_with_values = []
150
125
  for u, v, k, data in G.edges(data=True, keys=True):
151
126
  if 'geometry' in data:
152
- edge_line = data['geometry']
127
+ geom = data['geometry']
153
128
  else:
154
129
  start_node = G.nodes[u]
155
130
  end_node = G.nodes[v]
156
- edge_line = LineString([(start_node['x'], start_node['y']),
157
- (end_node['x'], end_node['y'])])
158
-
131
+ geom = LineString([(start_node['x'], start_node['y']),
132
+ (end_node['x'], end_node['y'])])
133
+
134
+ val = data.get(value_name, np.nan)
159
135
  edges_with_values.append({
160
- 'geometry': edge_line,
161
- value_name: data.get(value_name, np.nan),
162
- 'u': u,
163
- 'v': v,
164
- 'key': k
136
+ 'u': u, 'v': v, 'key': k,
137
+ 'geometry': geom,
138
+ value_name: val
165
139
  })
166
-
167
- edge_gdf = gpd.GeoDataFrame(edges_with_values)
168
-
169
- # Set CRS and save if requested
170
- if edge_gdf.crs is None:
171
- edge_gdf.set_crs(epsg=4326, inplace=True)
172
-
140
+
141
+ edge_gdf = gpd.GeoDataFrame(edges_with_values, crs="EPSG:4326")
142
+
143
+ # Save
173
144
  if settings['save_path']:
174
145
  edge_gdf.to_file(settings['save_path'], driver="GPKG")
175
-
176
- # Visualize if requested
146
+
177
147
  if settings['vis_graph']:
178
148
  edge_gdf_web = edge_gdf.to_crs(epsg=3857)
179
-
180
149
  fig, ax = plt.subplots(figsize=settings['fig_size'])
181
-
182
- plot = edge_gdf_web.plot(column=value_name,
183
- ax=ax,
184
- cmap=settings['colormap'],
185
- legend=True,
186
- vmin=settings['vmin'],
187
- vmax=settings['vmax'],
188
- linewidth=settings['edge_width'],
189
- legend_kwds={'label': value_name,
190
- 'shrink': 0.5}) # Make colorbar 50% smaller
191
-
192
- ctx.add_basemap(ax,
193
- source=settings['basemap_style'],
194
- zoom=settings['zoom'])
195
-
150
+ edge_gdf_web.plot(
151
+ column=value_name,
152
+ ax=ax,
153
+ cmap=settings['colormap'],
154
+ legend=True,
155
+ vmin=settings['vmin'],
156
+ vmax=settings['vmax'],
157
+ linewidth=settings['edge_width'],
158
+ legend_kwds={'label': value_name, 'shrink': 0.5}
159
+ )
160
+ ctx.add_basemap(ax, source=settings['basemap_style'], zoom=settings['zoom'])
196
161
  ax.set_axis_off()
197
- # plt.title(f'Network {value_name} Analysis', pad=20)
198
162
  plt.show()
199
-
163
+
200
164
  return G, edge_gdf
201
165
 
202
166
  # -------------------------------------------------------------------
voxcity/sim/solar.py CHANGED
@@ -57,7 +57,7 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
57
57
  # Check if current voxel is empty/tree and voxel below is solid
58
58
  if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
59
59
  # Skip if standing on building/vegetation/water
60
- if voxel_data[x, y, z - 1] in (-30, -3, -2):
60
+ if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
61
61
  irradiance_map[x, y] = np.nan
62
62
  found_observer = True
63
63
  break
@@ -148,9 +148,10 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
148
148
  cmap = plt.cm.get_cmap(colormap).copy()
149
149
  cmap.set_bad(color='lightgray')
150
150
  plt.figure(figsize=(10, 8))
151
- plt.title("Horizontal Direct Solar Irradiance Map (0° = North)")
151
+ # plt.title("Horizontal Direct Solar Irradiance Map (0° = North)")
152
152
  plt.imshow(direct_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
153
153
  plt.colorbar(label='Direct Solar Irradiance (W/m²)')
154
+ plt.axis('off')
154
155
  plt.show()
155
156
 
156
157
  # Optional OBJ export
@@ -231,9 +232,10 @@ def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.
231
232
  cmap = plt.cm.get_cmap(colormap).copy()
232
233
  cmap.set_bad(color='lightgray')
233
234
  plt.figure(figsize=(10, 8))
234
- plt.title("Diffuse Solar Irradiance Map")
235
+ # plt.title("Diffuse Solar Irradiance Map")
235
236
  plt.imshow(diffuse_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
236
237
  plt.colorbar(label='Diffuse Solar Irradiance (W/m²)')
238
+ plt.axis('off')
237
239
  plt.show()
238
240
 
239
241
  # Optional OBJ export
@@ -310,7 +312,7 @@ def get_global_solar_irradiance_map(
310
312
  # Create kwargs for diffuse calculation
311
313
  direct_diffuse_kwargs = kwargs.copy()
312
314
  direct_diffuse_kwargs.update({
313
- 'show_plot': False,
315
+ 'show_plot': True,
314
316
  'obj_export': False
315
317
  })
316
318
 
@@ -343,9 +345,10 @@ def get_global_solar_irradiance_map(
343
345
  cmap = plt.cm.get_cmap(colormap).copy()
344
346
  cmap.set_bad(color='lightgray')
345
347
  plt.figure(figsize=(10, 8))
346
- plt.title("Global Solar Irradiance Map")
348
+ # plt.title("Global Solar Irradiance Map")
347
349
  plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
348
350
  plt.colorbar(label='Global Solar Irradiance (W/m²)')
351
+ plt.axis('off')
349
352
  plt.show()
350
353
 
351
354
  # Optional OBJ export
@@ -570,9 +573,10 @@ def get_cumulative_global_solar_irradiance(
570
573
  vmax = kwargs.get("vmax", max(direct_normal_irradiance_scaling, diffuse_irradiance_scaling) * 1000)
571
574
  cmap = plt.cm.get_cmap(colormap).copy()
572
575
  cmap.set_bad(color='lightgray')
573
- plt.figure(figsize=(8, 6))
574
- plt.title(f"Global Solar Irradiance at {time_local.strftime('%Y-%m-%d %H:%M:%S')}")
576
+ plt.figure(figsize=(10, 8))
577
+ # plt.title(f"Global Solar Irradiance at {time_local.strftime('%Y-%m-%d %H:%M:%S')}")
575
578
  plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
579
+ plt.axis('off')
576
580
  plt.colorbar(label='Global Solar Irradiance (W/m²)')
577
581
  plt.show()
578
582
 
@@ -587,10 +591,11 @@ def get_cumulative_global_solar_irradiance(
587
591
  vmax = kwargs.get("vmax", np.nanmax(cumulative_map))
588
592
  cmap = plt.cm.get_cmap(colormap).copy()
589
593
  cmap.set_bad(color='lightgray')
590
- plt.figure(figsize=(8, 6))
591
- plt.title("Cumulative Global Solar Irradiance Map")
594
+ plt.figure(figsize=(10, 8))
595
+ # plt.title("Cumulative Global Solar Irradiance Map")
592
596
  plt.imshow(cumulative_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
593
597
  plt.colorbar(label='Cumulative Global Solar Irradiance (W/m²·hour)')
598
+ plt.axis('off')
594
599
  plt.show()
595
600
 
596
601
  # Optional OBJ export
voxcity/sim/view.py CHANGED
@@ -413,6 +413,7 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
413
413
  plt.figure(figsize=(10, 8))
414
414
  plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
415
415
  plt.colorbar(label='View Index')
416
+ plt.axis('off')
416
417
  plt.show()
417
418
 
418
419
  # Optional OBJ export
@@ -626,7 +627,7 @@ def compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_h
626
627
  for z in range(1, nz):
627
628
  if voxel_data[x, y, z] == 0 and voxel_data[x, y, z - 1] != 0:
628
629
  # Skip if standing on building or vegetation
629
- if voxel_data[x, y, z - 1] in (-3, -2, 7, 8, 9):
630
+ if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
630
631
  visibility_map[x, y] = np.nan
631
632
  found_observer = True
632
633
  break
@@ -696,7 +697,7 @@ def compute_landmark_visibility(voxel_data, target_value=-30, view_height_voxel=
696
697
  plt.legend(handles=[visible_patch, not_visible_patch],
697
698
  loc='center left',
698
699
  bbox_to_anchor=(1.0, 0.5))
699
-
700
+ plt.axis('off')
700
701
  plt.show()
701
702
 
702
703
  return np.flipud(visibility_map)
@@ -852,9 +853,10 @@ def get_sky_view_factor_map(voxel_data, meshsize, show_plot=False, **kwargs):
852
853
  cmap = plt.cm.get_cmap(colormap).copy()
853
854
  cmap.set_bad(color='lightgray')
854
855
  plt.figure(figsize=(10, 8))
855
- plt.title("Sky View Factor Map")
856
+ # plt.title("Sky View Factor Map")
856
857
  plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
857
858
  plt.colorbar(label='Sky View Factor')
859
+ plt.axis('off')
858
860
  plt.show()
859
861
 
860
862
  # Optional OBJ export
voxcity/voxcity.py CHANGED
@@ -294,28 +294,31 @@ def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
294
294
  print("Creating Digital Elevation Model (DEM) grid\n ")
295
295
  print(f"Data source: {source}")
296
296
 
297
- # Initialize Earth Engine for accessing elevation data
298
- initialize_earth_engine()
297
+ if source == "Local file":
298
+ geotiff_path = kwargs["dem_path"]
299
+ else:
300
+ # Initialize Earth Engine for accessing elevation data
301
+ initialize_earth_engine()
299
302
 
300
- geotiff_path = os.path.join(output_dir, "dem.tif")
303
+ geotiff_path = os.path.join(output_dir, "dem.tif")
301
304
 
302
- # Add buffer around ROI to ensure smooth interpolation at edges
303
- buffer_distance = 100
304
- roi = get_roi(rectangle_vertices)
305
- roi_buffered = roi.buffer(buffer_distance)
306
-
307
- # Get DEM data
308
- image = get_dem_image(roi_buffered, source)
309
-
310
- # Save DEM data with appropriate resolution based on source
311
- if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM']:
312
- save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
313
- elif source == 'USGS 3DEP 1m':
314
- scale = max(meshsize, 1.25)
315
- save_geotiff(image, geotiff_path, scale=scale, region=roi_buffered, crs='EPSG:4326')
316
- else:
317
- # Default to 30m resolution for other sources
318
- save_geotiff(image, geotiff_path, scale=30, region=roi_buffered)
305
+ # Add buffer around ROI to ensure smooth interpolation at edges
306
+ buffer_distance = 100
307
+ roi = get_roi(rectangle_vertices)
308
+ roi_buffered = roi.buffer(buffer_distance)
309
+
310
+ # Get DEM data
311
+ image = get_dem_image(roi_buffered, source)
312
+
313
+ # Save DEM data with appropriate resolution based on source
314
+ if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM']:
315
+ save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
316
+ elif source == 'USGS 3DEP 1m':
317
+ scale = max(meshsize, 1.25)
318
+ save_geotiff(image, geotiff_path, scale=scale, region=roi_buffered, crs='EPSG:4326')
319
+ else:
320
+ # Default to 30m resolution for other sources
321
+ save_geotiff(image, geotiff_path, scale=30, region=roi_buffered)
319
322
 
320
323
  # Create DEM grid with optional interpolation method
321
324
  dem_interpolation = kwargs.get("dem_interpolation")
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: voxcity
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
5
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
@@ -66,7 +66,7 @@ Requires-Dist: ruff; extra == "dev"
66
66
 
67
67
  # VoxCity
68
68
 
69
- **VoxCity** is a Python package that facilitates the creation of voxel-based 3D urban environments and related geospatial analyses. It integrates various geospatial datasets—such as building footprints, land cover, canopy height, and digital elevation models (DEMs)—to generate 2D and 3D representations of urban areas. It can export data in formats compatible with popular simulation tools like ENVI-MET, as well as visualization tools like MagicaVoxel, and supports simulations such as sky view index and green view index calculations.
69
+ **VoxCity** is a Python package that provides a one-stop solution for grid-based 3D city model generation and urban simulation for cities worldwide. It integrates various geospatial datasets—such as building footprints, land cover, canopy height, and digital elevation models (DEMs)—to generate 2D and 3D representations of urban areas. It can export data in formats compatible with popular simulation tools like ENVI-MET, as well as visualization tools like MagicaVoxel, and supports simulations such as sky view index and green view index calculations.
70
70
 
71
71
  <!-- <p align="center">
72
72
  <picture>
@@ -284,7 +284,7 @@ The generated OBJ files can be opened and rendered in the following 3D visualiza
284
284
  <img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/obj.png" alt="OBJ 3D City Model Rendered in Rhino" width="600">
285
285
  </p>
286
286
  <p align="center">
287
- <em>Example Output Exported in OBJ and Rendered in MagicaVoxel</em>
287
+ <em>Example Output Exported in OBJ and Rendered in Rhino</em>
288
288
  </p>
289
289
 
290
290
  #### MagicaVoxel VOX Files:
@@ -1,5 +1,5 @@
1
1
  voxcity/__init__.py,sha256=HJM0D2Mv9qpk4JdVzt2SRAAk-hA1D_pCO0ezZH9F7KA,248
2
- voxcity/voxcity.py,sha256=aeM1OzW7nmbW4h4SFHceugvb0NhKdXlQz_QcVQrZrIk,33498
2
+ voxcity/voxcity.py,sha256=tZOgRnyvw4ZHuuJr_QP3qeen5DxhYOkDglNDtpB0hzY,33664
3
3
  voxcity/download/__init__.py,sha256=OgGcGxOXF4tjcEL6DhOnt13DYPTvOigUelp5xIpTqM0,171
4
4
  voxcity/download/eubucco.py,sha256=e1JXBuUfBptSDvNznSGckRs5Xgrj_SAFxk445J_o4KY,14854
5
5
  voxcity/download/gee.py,sha256=j7jmzp44T3M6j_4DwhU9Y8Y6gqbZo1zFIlduQPc0jvk,14339
@@ -11,26 +11,26 @@ voxcity/download/overture.py,sha256=daOvsySC2KIcTcMJUSA7XdbMELJuyLAIM2vr1DRLGp0,
11
11
  voxcity/download/utils.py,sha256=z6MdPxM96FWQVqvZW2Eg5pMewVHVysUP7F6ueeCwMfI,1375
12
12
  voxcity/file/__init_.py,sha256=cVyNyE6axEpSd3CT5hGuMOAlOyU1p8lVP4jkF1-0Ad8,94
13
13
  voxcity/file/envimet.py,sha256=SPVoSyYTMNyDRDFWsI0YAsIsb6yt_SXZeDUlhyqlEqY,24282
14
- voxcity/file/geojson.py,sha256=G8jG5Ffh86uhNZBLmr_hgyU9FwGab_tJBePET5DUQYk,24188
14
+ voxcity/file/geojson.py,sha256=h6WYr1bdvzC46w6CbjKzO4wMtVrKTSF7SGaOUuUMkCM,33412
15
15
  voxcity/file/magicavoxel.py,sha256=Fsv7yGRXeKmp82xcG3rOb0t_HtoqltNq2tHl08xVlqY,7500
16
16
  voxcity/file/obj.py,sha256=oW-kPoZj53nfmO9tXP3Wvizq6Kkjh-QQR8UBexRuMiI,21609
17
17
  voxcity/geo/__init_.py,sha256=AZYQxK1zY1M_mDT1HmgcdVI86OAtwK7CNo3AOScLHco,88
18
18
  voxcity/geo/draw.py,sha256=roljWXyqYdsWYkmb-5_WNxrJrfV5lnAt8uZblCCo_3Q,13555
19
19
  voxcity/geo/grid.py,sha256=_MzO-Cu2GhlP9nuCql6f1pfbU2_OAL27aQ_zCj1u_zk,36288
20
- voxcity/geo/network.py,sha256=lcDLgsmPb9MyFeQlJwscXl7_9JCG7TIlbAu19MPf2m8,18846
20
+ voxcity/geo/network.py,sha256=P1oaosieFPuWj9QbU9VmZHpoXV3HR61VssZMcUZLYIw,17403
21
21
  voxcity/geo/utils.py,sha256=1BRHp-DDeOA8HG8jplY7Eo75G3oXkVGL6DGONL4BA8A,19815
22
22
  voxcity/sim/__init_.py,sha256=APdkcdaovj0v_RPOaA4SBvFUKT2RM7Hxuuz3Sux4gCo,65
23
- voxcity/sim/solar.py,sha256=f9GLANRnEVj7NseSETVRDvTD_t_Bn9hC6dJUV5Ak_cU,31799
23
+ voxcity/sim/solar.py,sha256=ddzEjN9s9zEABMPu1S0k-NXlNtIYDl12EAwiaaASJs0,31970
24
24
  voxcity/sim/utils.py,sha256=sEYBB2-hLJxTiXQps1_-Fi7t1HN3-1OPOvBCWtgIisA,130
25
- voxcity/sim/view.py,sha256=oq6G-f0Tn-KT0vjYNJfucmOIrv1GNjljhA-zvU4nNoA,36668
25
+ voxcity/sim/view.py,sha256=FoXovh406hmvL1obaIXi2MyiRnPdXHY9SWxqfqmcJnc,36758
26
26
  voxcity/utils/__init_.py,sha256=nLYrj2huBbDBNMqfchCwexGP8Tlt9O_XluVDG7MoFkw,98
27
27
  voxcity/utils/lc.py,sha256=RwPd-VY3POV3gTrBhM7TubgGb9MCd3nVah_G8iUEF7k,11562
28
28
  voxcity/utils/material.py,sha256=Vt3IID5Ft54HNJcEC4zi31BCPqi_687X3CSp7rXaRVY,5907
29
29
  voxcity/utils/visualization.py,sha256=FNBMN0V5IPuAdqvLHnqSGYqNS7jWesg0ZADEtsUtl0A,31925
30
30
  voxcity/utils/weather.py,sha256=P6s1y_EstBL1OGP_MR_6u3vr-t6Uawg8uDckJnoI7FI,21482
31
- voxcity-0.3.9.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
32
- voxcity-0.3.9.dist-info/LICENSE,sha256=-hGliOFiwUrUSoZiB5WF90xXGqinKyqiDI2t6hrnam8,1087
33
- voxcity-0.3.9.dist-info/METADATA,sha256=wbktfEaoqiafDk3sX8gZxM2m6-pYxXDkGI5daZbU-S4,25110
34
- voxcity-0.3.9.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
35
- voxcity-0.3.9.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
36
- voxcity-0.3.9.dist-info/RECORD,,
31
+ voxcity-0.3.11.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
32
+ voxcity-0.3.11.dist-info/LICENSE,sha256=-hGliOFiwUrUSoZiB5WF90xXGqinKyqiDI2t6hrnam8,1087
33
+ voxcity-0.3.11.dist-info/METADATA,sha256=hwpRtrSTbEGMwUCtt4q8cxXKn23nfvxj2AWAZIbZmHw,25122
34
+ voxcity-0.3.11.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
35
+ voxcity-0.3.11.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
36
+ voxcity-0.3.11.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.7.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5