voxcity 0.6.26__py3-none-any.whl → 1.0.2__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.
Files changed (81) hide show
  1. voxcity/__init__.py +10 -4
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/gba.py +210 -0
  4. voxcity/downloader/gee.py +5 -1
  5. voxcity/downloader/mbfp.py +1 -1
  6. voxcity/downloader/oemj.py +80 -8
  7. voxcity/downloader/utils.py +73 -73
  8. voxcity/errors.py +30 -0
  9. voxcity/exporter/__init__.py +9 -1
  10. voxcity/exporter/cityles.py +129 -34
  11. voxcity/exporter/envimet.py +51 -26
  12. voxcity/exporter/magicavoxel.py +42 -5
  13. voxcity/exporter/netcdf.py +27 -0
  14. voxcity/exporter/obj.py +103 -28
  15. voxcity/generator/__init__.py +47 -0
  16. voxcity/generator/api.py +721 -0
  17. voxcity/generator/grids.py +381 -0
  18. voxcity/generator/io.py +94 -0
  19. voxcity/generator/pipeline.py +282 -0
  20. voxcity/generator/update.py +429 -0
  21. voxcity/generator/voxelizer.py +392 -0
  22. voxcity/geoprocessor/__init__.py +75 -6
  23. voxcity/geoprocessor/conversion.py +153 -0
  24. voxcity/geoprocessor/draw.py +1488 -1169
  25. voxcity/geoprocessor/heights.py +199 -0
  26. voxcity/geoprocessor/io.py +101 -0
  27. voxcity/geoprocessor/merge_utils.py +91 -0
  28. voxcity/geoprocessor/mesh.py +26 -10
  29. voxcity/geoprocessor/network.py +35 -6
  30. voxcity/geoprocessor/overlap.py +84 -0
  31. voxcity/geoprocessor/raster/__init__.py +82 -0
  32. voxcity/geoprocessor/raster/buildings.py +435 -0
  33. voxcity/geoprocessor/raster/canopy.py +258 -0
  34. voxcity/geoprocessor/raster/core.py +150 -0
  35. voxcity/geoprocessor/raster/export.py +93 -0
  36. voxcity/geoprocessor/raster/landcover.py +159 -0
  37. voxcity/geoprocessor/raster/raster.py +110 -0
  38. voxcity/geoprocessor/selection.py +85 -0
  39. voxcity/geoprocessor/utils.py +824 -820
  40. voxcity/models.py +113 -0
  41. voxcity/simulator/common/__init__.py +22 -0
  42. voxcity/simulator/common/geometry.py +98 -0
  43. voxcity/simulator/common/raytracing.py +450 -0
  44. voxcity/simulator/solar/__init__.py +66 -0
  45. voxcity/simulator/solar/integration.py +336 -0
  46. voxcity/simulator/solar/kernels.py +62 -0
  47. voxcity/simulator/solar/radiation.py +648 -0
  48. voxcity/simulator/solar/sky.py +668 -0
  49. voxcity/simulator/solar/temporal.py +792 -0
  50. voxcity/simulator/view.py +36 -2286
  51. voxcity/simulator/visibility/__init__.py +29 -0
  52. voxcity/simulator/visibility/landmark.py +392 -0
  53. voxcity/simulator/visibility/view.py +508 -0
  54. voxcity/utils/__init__.py +11 -0
  55. voxcity/utils/classes.py +194 -0
  56. voxcity/utils/lc.py +80 -39
  57. voxcity/utils/logging.py +61 -0
  58. voxcity/utils/orientation.py +51 -0
  59. voxcity/utils/shape.py +230 -0
  60. voxcity/utils/weather/__init__.py +26 -0
  61. voxcity/utils/weather/epw.py +146 -0
  62. voxcity/utils/weather/files.py +36 -0
  63. voxcity/utils/weather/onebuilding.py +486 -0
  64. voxcity/visualizer/__init__.py +24 -0
  65. voxcity/visualizer/builder.py +43 -0
  66. voxcity/visualizer/grids.py +141 -0
  67. voxcity/visualizer/maps.py +187 -0
  68. voxcity/visualizer/palette.py +228 -0
  69. voxcity/visualizer/renderer.py +1145 -0
  70. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
  71. voxcity-1.0.2.dist-info/RECORD +81 -0
  72. voxcity/generator.py +0 -1302
  73. voxcity/geoprocessor/grid.py +0 -1739
  74. voxcity/geoprocessor/polygon.py +0 -1344
  75. voxcity/simulator/solar.py +0 -2339
  76. voxcity/utils/visualization.py +0 -2849
  77. voxcity/utils/weather.py +0 -1038
  78. voxcity-0.6.26.dist-info/RECORD +0 -38
  79. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
  80. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
voxcity/exporter/obj.py CHANGED
@@ -23,6 +23,13 @@ Dependencies:
23
23
  - numpy: For array operations
24
24
  - matplotlib: For colormap handling
25
25
  - trimesh: For mesh operations
26
+
27
+ Orientation contract:
28
+ - Export functions assume input 2D grids are north_up (row 0 = north/top) with
29
+ columns increasing eastward (col 0 = west/left), and voxel arrays use
30
+ (row, col, z) = (north→south, west→east, ground→up).
31
+ - Internal flips may be applied to match OBJ coordinate conventions; these do
32
+ not change the semantic orientation of the data.
26
33
  """
27
34
 
28
35
  import numpy as np
@@ -31,7 +38,7 @@ from numba import njit, prange
31
38
  import matplotlib.pyplot as plt
32
39
  import trimesh
33
40
  import numpy as np
34
- from ..utils.visualization import get_voxel_color_map
41
+ from ..visualizer import get_voxel_color_map
35
42
 
36
43
  def convert_colormap_indices(original_map):
37
44
  """
@@ -266,7 +273,7 @@ def mesh_faces(mask, layer_index, axis, positive_direction, normal_idx, voxel_si
266
273
 
267
274
  v += width
268
275
 
269
- def export_obj(array, output_dir, file_name, voxel_size, voxel_color_map=None):
276
+ def export_obj(array, output_dir, file_name, voxel_size=None, voxel_color_map=None):
270
277
  """
271
278
  Export a voxel array to OBJ format with materials and proper face orientations.
272
279
 
@@ -275,14 +282,14 @@ def export_obj(array, output_dir, file_name, voxel_size, voxel_color_map=None):
275
282
  both OBJ and MTL files with all necessary components for rendering.
276
283
 
277
284
  Args:
278
- array (ndarray): 3D numpy array containing voxel values.
285
+ array (ndarray | VoxCity): 3D numpy array of voxel values or a VoxCity instance.
279
286
  Non-zero values indicate voxel presence and material type.
280
287
  output_dir (str): Directory to save the OBJ and MTL files.
281
288
  Will be created if it doesn't exist.
282
289
  file_name (str): Base name for the output files.
283
290
  Will be used for both .obj and .mtl files.
284
- voxel_size (float): Size of each voxel in meters.
285
- Used to scale the model to real-world units.
291
+ voxel_size (float | None): Size of each voxel in meters. If a VoxCity is provided,
292
+ this is inferred from the object and this parameter is ignored.
286
293
  voxel_color_map (dict, optional): Dictionary mapping voxel values to RGB colors.
287
294
  If None, uses default color map. Colors should be RGB lists (0-255).
288
295
 
@@ -307,6 +314,15 @@ def export_obj(array, output_dir, file_name, voxel_size, voxel_color_map=None):
307
314
  - Transparency settings
308
315
  - Illumination model definitions
309
316
  """
317
+ # Accept VoxCity instance as first argument
318
+ try:
319
+ from ..models import VoxCity as _VoxCity
320
+ if isinstance(array, _VoxCity):
321
+ voxel_size = float(array.voxels.meta.meshsize)
322
+ array = array.voxels.classes
323
+ except Exception:
324
+ pass
325
+
310
326
  if voxel_color_map is None:
311
327
  voxel_color_map = get_voxel_color_map()
312
328
 
@@ -681,6 +697,7 @@ def export_netcdf_to_obj(
681
697
  classes_to_show=None,
682
698
  voxel_color_scheme="default",
683
699
  max_faces_warn=1_000_000,
700
+ export_vox_base=True,
684
701
  ):
685
702
  """
686
703
  Export two OBJ/MTL files using the same local meter frame:
@@ -718,6 +735,8 @@ def export_netcdf_to_obj(
718
735
  classes_to_show (set[int]|None): Optional subset of voxel classes to export; None -> all present (except 0).
719
736
  voxel_color_scheme (str): Color scheme name passed to get_voxel_color_map.
720
737
  max_faces_warn (int): Warn if a single class exceeds this many faces.
738
+ export_vox_base (bool): If False, skip exporting VoxCity OBJ/MTL; VoxCity input
739
+ is still used to define the shared coordinate system for scalar OBJ.
721
740
 
722
741
  Returns:
723
742
  dict: Paths of written files: keys 'vox_obj','vox_mtl','tm_obj','tm_mtl' (values may be None).
@@ -1345,27 +1364,28 @@ def export_netcdf_to_obj(
1345
1364
  vox_meshes = {}
1346
1365
  tm_meshes = {}
1347
1366
 
1348
- present = set(np.unique(Av_kji))
1349
- present.discard(0)
1350
- if classes_to_show is not None:
1351
- present &= set(classes_to_show)
1352
- present = sorted(present)
1353
-
1354
- faces_total = 0
1355
- voxel_color_map = get_voxel_color_map(color_scheme=voxel_color_scheme)
1356
- for cls in present:
1357
- mask = Av_kji == cls
1358
- if not np.any(mask):
1359
- continue
1360
- rgb = voxel_color_map.get(int(cls), [200, 200, 200])
1361
- if greedy_vox:
1362
- m_cls, faces = make_voxel_mesh_uniform_color_greedy(mask, Xv_s, Yv_s, Zv_s, rgb=rgb, name=f"class_{int(cls)}")
1363
- else:
1364
- m_cls, faces = make_voxel_mesh_uniform_color(mask, Xv_s, Yv_s, Zv_s, rgb=rgb, name=f"class_{int(cls)}")
1365
- if m_cls is not None:
1366
- vox_meshes[f"voxclass_{int(cls)}"] = m_cls
1367
- faces_total += faces
1368
- print(f"[VoxCity] total voxel faces: {faces_total:,}")
1367
+ if export_vox_base:
1368
+ present = set(np.unique(Av_kji))
1369
+ present.discard(0)
1370
+ if classes_to_show is not None:
1371
+ present &= set(classes_to_show)
1372
+ present = sorted(present)
1373
+
1374
+ faces_total = 0
1375
+ voxel_color_map = get_voxel_color_map(color_scheme=voxel_color_scheme)
1376
+ for cls in present:
1377
+ mask = Av_kji == cls
1378
+ if not np.any(mask):
1379
+ continue
1380
+ rgb = voxel_color_map.get(int(cls), [200, 200, 200])
1381
+ if greedy_vox:
1382
+ m_cls, faces = make_voxel_mesh_uniform_color_greedy(mask, Xv_s, Yv_s, Zv_s, rgb=rgb, name=f"class_{int(cls)}")
1383
+ else:
1384
+ m_cls, faces = make_voxel_mesh_uniform_color(mask, Xv_s, Yv_s, Zv_s, rgb=rgb, name=f"class_{int(cls)}")
1385
+ if m_cls is not None:
1386
+ vox_meshes[f"voxclass_{int(cls)}"] = m_cls
1387
+ faces_total += faces
1388
+ print(f"[VoxCity] total voxel faces: {faces_total:,}")
1369
1389
 
1370
1390
  iso_meshes = build_tm_isosurfaces_regular_grid(
1371
1391
  A_scalar=A_s,
@@ -1390,7 +1410,7 @@ def export_netcdf_to_obj(
1390
1410
 
1391
1411
  os.makedirs(output_dir, exist_ok=True)
1392
1412
  obj_vox = mtl_vox = obj_tm = mtl_tm = None
1393
- if vox_meshes:
1413
+ if export_vox_base and vox_meshes:
1394
1414
  obj_vox, mtl_vox = save_obj_with_mtl_and_normals(vox_meshes, output_dir, vox_base_filename)
1395
1415
  if tm_meshes:
1396
1416
  obj_tm, mtl_tm = save_obj_with_mtl_and_normals(tm_meshes, output_dir, tm_base_filename)
@@ -1403,4 +1423,59 @@ def export_netcdf_to_obj(
1403
1423
  print(f"Scalar Iso OBJ: {obj_tm}")
1404
1424
  print(f"Scalar Iso MTL: {mtl_tm}")
1405
1425
 
1406
- return {"vox_obj": obj_vox, "vox_mtl": mtl_vox, "tm_obj": obj_tm, "tm_mtl": mtl_tm}
1426
+ return {"vox_obj": obj_vox, "vox_mtl": mtl_vox, "tm_obj": obj_tm, "tm_mtl": mtl_tm}
1427
+
1428
+
1429
+ class OBJExporter:
1430
+ """Exporter that writes mesh collections or trimesh dicts to OBJ/MTL.
1431
+
1432
+ Accepts either a MeshCollection (voxcity.models) or dict[str, trimesh.Trimesh].
1433
+ """
1434
+
1435
+ def export(self, obj, output_directory: str, base_filename: str, **kwargs):
1436
+ os.makedirs(output_directory, exist_ok=True)
1437
+ # VoxCity or MeshCollection path
1438
+ try:
1439
+ from ..models import MeshCollection, VoxCity
1440
+ if isinstance(obj, VoxCity):
1441
+ # Delegate to file-writing path using voxels
1442
+ export_obj(
1443
+ array=obj.voxels.classes,
1444
+ output_dir=output_directory,
1445
+ file_name=base_filename,
1446
+ voxel_size=float(obj.voxels.meta.meshsize),
1447
+ voxel_color_map=kwargs.get("voxel_color_map"),
1448
+ )
1449
+ return os.path.join(output_directory, f"{base_filename}.obj")
1450
+ is_collection = isinstance(obj, MeshCollection)
1451
+ except Exception:
1452
+ is_collection = False
1453
+
1454
+ if is_collection:
1455
+ tm = {}
1456
+ for key, mm in obj.items.items():
1457
+ if getattr(mm, "vertices", None) is None or getattr(mm, "faces", None) is None:
1458
+ continue
1459
+ if mm.vertices.size == 0 or mm.faces.size == 0:
1460
+ continue
1461
+ tri = trimesh.Trimesh(vertices=mm.vertices, faces=mm.faces, process=False)
1462
+ if getattr(mm, "colors", None) is not None:
1463
+ tri.visual.face_colors = mm.colors
1464
+ tm[key] = tri
1465
+ if not tm:
1466
+ return None
1467
+ combined = trimesh.util.concatenate(list(tm.values()))
1468
+ out = os.path.join(output_directory, f"{base_filename}.obj")
1469
+ combined.export(out)
1470
+ return out
1471
+
1472
+ # Dict[str, trimesh.Trimesh] path
1473
+ if isinstance(obj, dict) and all(hasattr(m, "vertices") for m in obj.values()):
1474
+ if not obj:
1475
+ return None
1476
+ combined = trimesh.util.concatenate(list(obj.values()))
1477
+ out = os.path.join(output_directory, f"{base_filename}.obj")
1478
+ combined.export(out)
1479
+ return out
1480
+
1481
+ raise TypeError("OBJExporter.export expects MeshCollection or dict[str, trimesh.Trimesh]")
@@ -0,0 +1,47 @@
1
+ """VoxCity generator subpackage.
2
+
3
+ This package organizes the voxel city generation pipeline into focused modules
4
+ while preserving the original public API under `voxcity.generator`.
5
+
6
+ Orientation contract:
7
+ - All 2D grids use north_up orientation (row 0 = north/top; columns increase eastward).
8
+ - 3D indexing follows (row, col, z) = (north→south, west→east, ground→up).
9
+ """
10
+
11
+ from .api import get_voxcity, get_voxcity_CityGML, auto_select_data_sources
12
+ from .grids import (
13
+ get_land_cover_grid,
14
+ get_building_height_grid,
15
+ get_canopy_height_grid,
16
+ get_dem_grid,
17
+ )
18
+ from .voxelizer import (
19
+ Voxelizer,
20
+ GROUND_CODE,
21
+ TREE_CODE,
22
+ BUILDING_CODE,
23
+ )
24
+ from .pipeline import VoxCityPipeline
25
+ from .io import save_voxcity, load_voxcity
26
+ from .update import update_voxcity, regenerate_voxels
27
+
28
+ __all__ = [
29
+ "get_voxcity",
30
+ "auto_select_data_sources",
31
+ "get_voxcity_CityGML",
32
+ "get_land_cover_grid",
33
+ "get_building_height_grid",
34
+ "get_canopy_height_grid",
35
+ "get_dem_grid",
36
+ "Voxelizer",
37
+ "GROUND_CODE",
38
+ "TREE_CODE",
39
+ "BUILDING_CODE",
40
+ "VoxCityPipeline",
41
+ "save_voxcity",
42
+ "load_voxcity",
43
+ "update_voxcity",
44
+ "regenerate_voxels",
45
+ ]
46
+
47
+