voxcity 0.5.31__py3-none-any.whl → 0.6.1__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/generator.py +74 -51
- voxcity/geoprocessor/grid.py +305 -139
- voxcity/geoprocessor/mesh.py +790 -758
- voxcity/geoprocessor/polygon.py +1343 -1343
- voxcity/simulator/solar.py +2252 -1820
- voxcity/simulator/view.py +2238 -1722
- voxcity/utils/lc.py +21 -100
- voxcity/utils/visualization.py +90 -44
- {voxcity-0.5.31.dist-info → voxcity-0.6.1.dist-info}/METADATA +1 -1
- {voxcity-0.5.31.dist-info → voxcity-0.6.1.dist-info}/RECORD +14 -14
- {voxcity-0.5.31.dist-info → voxcity-0.6.1.dist-info}/WHEEL +0 -0
- {voxcity-0.5.31.dist-info → voxcity-0.6.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.5.31.dist-info → voxcity-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {voxcity-0.5.31.dist-info → voxcity-0.6.1.dist-info}/top_level.txt +0 -0
voxcity/generator.py
CHANGED
|
@@ -24,6 +24,19 @@ Key Features:
|
|
|
24
24
|
# Standard library imports
|
|
25
25
|
import numpy as np
|
|
26
26
|
import os
|
|
27
|
+
try:
|
|
28
|
+
from numba import jit, prange
|
|
29
|
+
import numba
|
|
30
|
+
NUMBA_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
NUMBA_AVAILABLE = False
|
|
33
|
+
print("Numba not available. Using optimized version without JIT compilation.")
|
|
34
|
+
# Define dummy decorators
|
|
35
|
+
def jit(*args, **kwargs):
|
|
36
|
+
def decorator(func):
|
|
37
|
+
return func
|
|
38
|
+
return decorator
|
|
39
|
+
prange = range
|
|
27
40
|
|
|
28
41
|
# Local application/library specific imports
|
|
29
42
|
|
|
@@ -244,11 +257,12 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
|
|
|
244
257
|
# This allows combining multiple sources for better coverage or accuracy
|
|
245
258
|
building_complementary_source = kwargs.get("building_complementary_source")
|
|
246
259
|
building_complement_height = kwargs.get("building_complement_height")
|
|
260
|
+
overlapping_footprint = kwargs.get("overlapping_footprint")
|
|
247
261
|
|
|
248
262
|
if (building_complementary_source is None) or (building_complementary_source=='None'):
|
|
249
263
|
# Use only the primary data source
|
|
250
264
|
if source != "Open Building 2.5D Temporal":
|
|
251
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, complement_height=building_complement_height)
|
|
265
|
+
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, complement_height=building_complement_height, overlapping_footprint=overlapping_footprint)
|
|
252
266
|
else:
|
|
253
267
|
# Combine primary source with complementary data
|
|
254
268
|
if building_complementary_source == "Open Building 2.5D Temporal":
|
|
@@ -257,14 +271,14 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
|
|
|
257
271
|
os.makedirs(output_dir, exist_ok=True)
|
|
258
272
|
geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
|
|
259
273
|
save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
|
|
260
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height)
|
|
274
|
+
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height, overlapping_footprint=overlapping_footprint)
|
|
261
275
|
elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
|
|
262
276
|
# Use digital surface model minus digital terrain model for height estimation
|
|
263
277
|
roi = get_roi(rectangle_vertices)
|
|
264
278
|
os.makedirs(output_dir, exist_ok=True)
|
|
265
279
|
geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
|
|
266
280
|
save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
|
|
267
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height)
|
|
281
|
+
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height, overlapping_footprint=overlapping_footprint)
|
|
268
282
|
else:
|
|
269
283
|
# Fetch complementary data from another vector source
|
|
270
284
|
if building_complementary_source == 'Microsoft Building Footprints':
|
|
@@ -285,7 +299,7 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
|
|
|
285
299
|
# Configure how to combine the complementary data
|
|
286
300
|
# Can complement footprints only or both footprints and heights
|
|
287
301
|
complement_building_footprints = kwargs.get("complement_building_footprints")
|
|
288
|
-
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, gdf_comp=gdf_comp, complement_building_footprints=complement_building_footprints, complement_height=building_complement_height)
|
|
302
|
+
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, gdf_comp=gdf_comp, complement_building_footprints=complement_building_footprints, complement_height=building_complement_height, overlapping_footprint=overlapping_footprint)
|
|
289
303
|
|
|
290
304
|
# Generate visualization if requested
|
|
291
305
|
grid_vis = kwargs.get("gridvis", True)
|
|
@@ -415,9 +429,10 @@ def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
|
415
429
|
|
|
416
430
|
return dem_grid
|
|
417
431
|
|
|
418
|
-
def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori,
|
|
432
|
+
def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori,
|
|
433
|
+
building_id_grid_ori, land_cover_grid_ori, dem_grid_ori,
|
|
434
|
+
tree_grid_ori, voxel_size, land_cover_source, **kwargs):
|
|
419
435
|
"""Creates a 3D voxel representation combining all input grids.
|
|
420
|
-
|
|
421
436
|
Args:
|
|
422
437
|
building_height_grid_ori: Grid of building heights
|
|
423
438
|
building_min_height_grid_ori: Grid of building minimum heights
|
|
@@ -427,22 +442,19 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
|
|
|
427
442
|
tree_grid_ori: Grid of tree heights
|
|
428
443
|
voxel_size: Size of each voxel in meters
|
|
429
444
|
land_cover_source: Source of land cover data
|
|
430
|
-
|
|
445
|
+
kwargs: Additional arguments including:
|
|
431
446
|
- trunk_height_ratio: Ratio of trunk height to total tree height
|
|
432
|
-
|
|
433
447
|
Returns:
|
|
434
448
|
numpy.ndarray: 3D voxel grid with encoded values for different features
|
|
435
449
|
"""
|
|
436
|
-
|
|
437
450
|
print("Generating 3D voxel data")
|
|
438
|
-
|
|
451
|
+
|
|
439
452
|
# Convert land cover values to standardized format if needed
|
|
440
453
|
# OpenStreetMap data is already in the correct format
|
|
441
454
|
if (land_cover_source == 'OpenStreetMap'):
|
|
442
455
|
land_cover_grid_converted = land_cover_grid_ori
|
|
443
456
|
else:
|
|
444
457
|
land_cover_grid_converted = convert_land_cover(land_cover_grid_ori, land_cover_source=land_cover_source)
|
|
445
|
-
|
|
446
458
|
# Prepare all input grids for 3D processing
|
|
447
459
|
# Flip vertically to align with standard geographic orientation (north-up)
|
|
448
460
|
# Handle missing data appropriately for each grid type
|
|
@@ -453,43 +465,47 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
|
|
|
453
465
|
dem_grid = np.flipud(dem_grid_ori.copy()) - np.min(dem_grid_ori) # Normalize DEM to start at 0
|
|
454
466
|
dem_grid = process_grid(building_id_grid, dem_grid) # Process DEM based on building footprints
|
|
455
467
|
tree_grid = np.flipud(tree_grid_ori.copy())
|
|
456
|
-
|
|
457
468
|
# Validate that all input grids have consistent dimensions
|
|
458
469
|
assert building_height_grid.shape == land_cover_grid.shape == dem_grid.shape == tree_grid.shape, "Input grids must have the same shape"
|
|
459
|
-
|
|
460
470
|
rows, cols = building_height_grid.shape
|
|
461
|
-
|
|
462
471
|
# Calculate the required height for the 3D voxel grid
|
|
463
472
|
# Add 1 voxel layer to ensure sufficient vertical space
|
|
464
473
|
max_height = int(np.ceil(np.max(building_height_grid + dem_grid + tree_grid) / voxel_size))+1
|
|
465
|
-
|
|
466
474
|
# Initialize the 3D voxel grid with zeros
|
|
467
|
-
#
|
|
468
|
-
|
|
469
|
-
|
|
475
|
+
# Use int8 by default to reduce memory (values range from about -99 to small positives)
|
|
476
|
+
# Allow override via kwarg 'voxel_dtype'
|
|
477
|
+
voxel_dtype = kwargs.get("voxel_dtype", np.int8)
|
|
478
|
+
|
|
479
|
+
# Optional: estimate memory and allow a soft limit before allocating
|
|
480
|
+
try:
|
|
481
|
+
bytes_per_elem = np.dtype(voxel_dtype).itemsize
|
|
482
|
+
est_mb = rows * cols * max_height * bytes_per_elem / (1024 ** 2)
|
|
483
|
+
print(f"Voxel grid shape: ({rows}, {cols}, {max_height}), dtype: {voxel_dtype}, ~{est_mb:.1f} MB")
|
|
484
|
+
max_ram_mb = kwargs.get("max_voxel_ram_mb")
|
|
485
|
+
if (max_ram_mb is not None) and (est_mb > max_ram_mb):
|
|
486
|
+
raise MemoryError(f"Estimated voxel grid memory {est_mb:.1f} MB exceeds limit {max_ram_mb} MB. Increase mesh size or restrict ROI.")
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
voxel_grid = np.zeros((rows, cols, max_height), dtype=voxel_dtype)
|
|
470
491
|
# Configure tree trunk-to-crown ratio
|
|
471
492
|
# This determines how much of the tree is trunk vs canopy
|
|
472
493
|
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
473
494
|
if trunk_height_ratio is None:
|
|
474
495
|
trunk_height_ratio = 11.76 / 19.98 # Default ratio based on typical tree proportions
|
|
475
|
-
|
|
476
496
|
# Process each grid cell to build the 3D voxel representation
|
|
477
497
|
for i in range(rows):
|
|
478
498
|
for j in range(cols):
|
|
479
499
|
# Calculate ground level in voxel units
|
|
480
500
|
# Add 1 to ensure space for surface features
|
|
481
501
|
ground_level = int(dem_grid[i, j] / voxel_size + 0.5) + 1
|
|
482
|
-
|
|
483
502
|
# Extract current cell values
|
|
484
503
|
tree_height = tree_grid[i, j]
|
|
485
504
|
land_cover = land_cover_grid[i, j]
|
|
486
|
-
|
|
487
505
|
# Fill underground voxels with -1 (represents subsurface)
|
|
488
506
|
voxel_grid[i, j, :ground_level] = -1
|
|
489
|
-
|
|
490
507
|
# Set the ground surface to the land cover type
|
|
491
508
|
voxel_grid[i, j, ground_level-1] = land_cover
|
|
492
|
-
|
|
493
509
|
# Process tree canopy if trees are present
|
|
494
510
|
if tree_height > 0:
|
|
495
511
|
# Calculate tree structure: trunk base to crown base to crown top
|
|
@@ -497,19 +513,18 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
|
|
|
497
513
|
crown_base_height_level = int(crown_base_height / voxel_size + 0.5)
|
|
498
514
|
crown_top_height = tree_height
|
|
499
515
|
crown_top_height_level = int(crown_top_height / voxel_size + 0.5)
|
|
500
|
-
|
|
516
|
+
|
|
501
517
|
# Ensure minimum crown height of 1 voxel
|
|
502
518
|
# Prevent crown base and top from being at the same level
|
|
503
519
|
if (crown_top_height_level == crown_base_height_level) and (crown_base_height_level>0):
|
|
504
520
|
crown_base_height_level -= 1
|
|
505
|
-
|
|
521
|
+
|
|
506
522
|
# Calculate absolute positions relative to ground level
|
|
507
523
|
tree_start = ground_level + crown_base_height_level
|
|
508
524
|
tree_end = ground_level + crown_top_height_level
|
|
509
|
-
|
|
525
|
+
|
|
510
526
|
# Fill tree crown voxels with -2 (represents vegetation canopy)
|
|
511
527
|
voxel_grid[i, j, tree_start:tree_end] = -2
|
|
512
|
-
|
|
513
528
|
# Process buildings - handle multiple height segments per building
|
|
514
529
|
# Some buildings may have multiple levels or complex height profiles
|
|
515
530
|
for k in building_min_height_grid[i, j]:
|
|
@@ -517,7 +532,6 @@ def create_3d_voxel(building_height_grid_ori, building_min_height_grid_ori, buil
|
|
|
517
532
|
building_height = int(k[1] / voxel_size + 0.5) # Upper height of building segment
|
|
518
533
|
# Fill building voxels with -3 (represents built structures)
|
|
519
534
|
voxel_grid[i, j, ground_level+building_min_height:ground_level+building_height] = -3
|
|
520
|
-
|
|
521
535
|
return voxel_grid
|
|
522
536
|
|
|
523
537
|
def create_3d_voxel_individuals(building_height_grid_ori, land_cover_grid_ori, dem_grid_ori, tree_grid_ori, voxel_size, land_cover_source, layered_interval=None):
|
|
@@ -935,32 +949,41 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
|
|
|
935
949
|
return voxcity_grid, building_height_grid, building_min_height_grid, building_id_grid, canopy_height_grid, land_cover_grid, dem_grid, filtered_buildings
|
|
936
950
|
|
|
937
951
|
def replace_nan_in_nested(arr, replace_value=10.0):
|
|
938
|
-
"""Replace NaN values in a nested array structure with a specified value.
|
|
939
|
-
|
|
940
|
-
Args:
|
|
941
|
-
arr: Numpy array containing nested lists and potentially NaN values
|
|
942
|
-
replace_value: Value to replace NaN with (default: 10.0)
|
|
943
|
-
|
|
944
|
-
Returns:
|
|
945
|
-
Numpy array with NaN values replaced
|
|
946
952
|
"""
|
|
947
|
-
|
|
948
|
-
|
|
953
|
+
Optimized version that avoids converting to Python lists.
|
|
954
|
+
Works directly with numpy arrays.
|
|
955
|
+
"""
|
|
956
|
+
if not isinstance(arr, np.ndarray):
|
|
957
|
+
return arr
|
|
958
|
+
|
|
959
|
+
# Create output array
|
|
960
|
+
result = np.empty_like(arr, dtype=object)
|
|
949
961
|
|
|
950
|
-
#
|
|
951
|
-
for i in range(
|
|
952
|
-
for j in range(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
+
# Vectorized operation for empty cells
|
|
963
|
+
for i in range(arr.shape[0]):
|
|
964
|
+
for j in range(arr.shape[1]):
|
|
965
|
+
cell = arr[i, j]
|
|
966
|
+
|
|
967
|
+
if cell is None or (isinstance(cell, list) and len(cell) == 0):
|
|
968
|
+
result[i, j] = []
|
|
969
|
+
elif isinstance(cell, list):
|
|
970
|
+
# Process list without converting entire array
|
|
971
|
+
new_cell = []
|
|
972
|
+
for segment in cell:
|
|
973
|
+
if isinstance(segment, (list, np.ndarray)):
|
|
974
|
+
# Use numpy operations where possible
|
|
975
|
+
if isinstance(segment, np.ndarray):
|
|
976
|
+
new_segment = np.where(np.isnan(segment), replace_value, segment).tolist()
|
|
977
|
+
else:
|
|
978
|
+
new_segment = [replace_value if (isinstance(v, float) and np.isnan(v)) else v for v in segment]
|
|
979
|
+
new_cell.append(new_segment)
|
|
980
|
+
else:
|
|
981
|
+
new_cell.append(segment)
|
|
982
|
+
result[i, j] = new_cell
|
|
983
|
+
else:
|
|
984
|
+
result[i, j] = cell
|
|
962
985
|
|
|
963
|
-
return
|
|
986
|
+
return result
|
|
964
987
|
|
|
965
988
|
def save_voxcity_data(output_path, voxcity_grid, building_height_grid, building_min_height_grid,
|
|
966
989
|
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid,
|