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
@@ -0,0 +1,282 @@
1
+ import os
2
+ from ..utils.logging import get_logger
3
+ from typing import Optional
4
+ import numpy as np
5
+
6
+ from ..models import (
7
+ GridMetadata,
8
+ BuildingGrid,
9
+ LandCoverGrid,
10
+ DemGrid,
11
+ VoxelGrid,
12
+ CanopyGrid,
13
+ VoxCity,
14
+ PipelineConfig,
15
+ )
16
+
17
+ from .grids import (
18
+ get_land_cover_grid,
19
+ get_building_height_grid,
20
+ get_canopy_height_grid,
21
+ get_dem_grid,
22
+ )
23
+ from .voxelizer import Voxelizer
24
+
25
+
26
+ class VoxCityPipeline:
27
+ def __init__(self, meshsize: float, rectangle_vertices, crs: str = "EPSG:4326") -> None:
28
+ self.meshsize = float(meshsize)
29
+ self.rectangle_vertices = rectangle_vertices
30
+ self.crs = crs
31
+
32
+ def _bounds(self):
33
+ xs = [p[0] for p in self.rectangle_vertices]
34
+ ys = [p[1] for p in self.rectangle_vertices]
35
+ return (min(xs), min(ys), max(xs), max(ys))
36
+
37
+ def _meta(self) -> GridMetadata:
38
+ return GridMetadata(crs=self.crs, bounds=self._bounds(), meshsize=self.meshsize)
39
+
40
+ def assemble_voxcity(
41
+ self,
42
+ voxcity_grid: np.ndarray,
43
+ building_height_grid: np.ndarray,
44
+ building_min_height_grid: np.ndarray,
45
+ building_id_grid: np.ndarray,
46
+ land_cover_grid: np.ndarray,
47
+ dem_grid: np.ndarray,
48
+ canopy_height_top: Optional[np.ndarray] = None,
49
+ canopy_height_bottom: Optional[np.ndarray] = None,
50
+ extras: Optional[dict] = None,
51
+ ) -> VoxCity:
52
+ meta = self._meta()
53
+ buildings = BuildingGrid(
54
+ heights=building_height_grid,
55
+ min_heights=building_min_height_grid,
56
+ ids=building_id_grid,
57
+ meta=meta,
58
+ )
59
+ land = LandCoverGrid(classes=land_cover_grid, meta=meta)
60
+ dem = DemGrid(elevation=dem_grid, meta=meta)
61
+ voxels = VoxelGrid(classes=voxcity_grid, meta=meta)
62
+ canopy = CanopyGrid(top=canopy_height_top if canopy_height_top is not None else np.zeros_like(land_cover_grid, dtype=float),
63
+ bottom=canopy_height_bottom,
64
+ meta=meta)
65
+ _extras = {
66
+ "rectangle_vertices": self.rectangle_vertices,
67
+ "canopy_top": canopy.top,
68
+ "canopy_bottom": canopy.bottom,
69
+ }
70
+ if extras:
71
+ _extras.update(extras)
72
+ return VoxCity(voxels=voxels, buildings=buildings, land_cover=land, dem=dem, tree_canopy=canopy, extras=_extras)
73
+
74
+ def run(self, cfg: PipelineConfig, building_gdf=None, terrain_gdf=None, **kwargs) -> VoxCity:
75
+ os.makedirs(cfg.output_dir, exist_ok=True)
76
+ land_strategy = LandCoverSourceFactory.create(cfg.land_cover_source)
77
+ build_strategy = BuildingSourceFactory.create(cfg.building_source)
78
+ canopy_strategy = CanopySourceFactory.create(cfg.canopy_height_source, cfg)
79
+ dem_strategy = DemSourceFactory.create(cfg.dem_source)
80
+
81
+ # Prefer structured options from cfg; allow legacy kwargs for back-compat
82
+ land_cover_grid = land_strategy.build_grid(
83
+ cfg.rectangle_vertices, cfg.meshsize, cfg.output_dir,
84
+ **{**cfg.land_cover_options, **kwargs}
85
+ )
86
+ # Detect effective land cover source (e.g., Urbanwatch -> OpenStreetMap fallback)
87
+ try:
88
+ from .grids import get_last_effective_land_cover_source
89
+ lc_src_effective = get_last_effective_land_cover_source() or cfg.land_cover_source
90
+ except Exception:
91
+ lc_src_effective = cfg.land_cover_source
92
+ bh, bmin, bid, building_gdf_out = build_strategy.build_grids(
93
+ cfg.rectangle_vertices, cfg.meshsize, cfg.output_dir,
94
+ building_gdf=building_gdf,
95
+ **{**cfg.building_options, **kwargs}
96
+ )
97
+ canopy_top, canopy_bottom = canopy_strategy.build_grids(
98
+ cfg.rectangle_vertices, cfg.meshsize, land_cover_grid, cfg.output_dir,
99
+ land_cover_source=lc_src_effective,
100
+ **{**cfg.canopy_options, **kwargs}
101
+ )
102
+ dem = dem_strategy.build_grid(
103
+ cfg.rectangle_vertices, cfg.meshsize, land_cover_grid, cfg.output_dir,
104
+ terrain_gdf=terrain_gdf,
105
+ land_cover_like=land_cover_grid,
106
+ **{**cfg.dem_options, **kwargs}
107
+ )
108
+
109
+ ro = cfg.remove_perimeter_object
110
+ if (ro is not None) and (ro > 0):
111
+ w_peri = int(ro * bh.shape[0] + 0.5)
112
+ h_peri = int(ro * bh.shape[1] + 0.5)
113
+ canopy_top[:w_peri, :] = canopy_top[-w_peri:, :] = canopy_top[:, :h_peri] = canopy_top[:, -h_peri:] = 0
114
+ canopy_bottom[:w_peri, :] = canopy_bottom[-w_peri:, :] = canopy_bottom[:, :h_peri] = canopy_bottom[:, -h_peri:] = 0
115
+ ids1 = np.unique(bid[:w_peri, :][bid[:w_peri, :] > 0]); ids2 = np.unique(bid[-w_peri:, :][bid[-w_peri:, :] > 0])
116
+ ids3 = np.unique(bid[:, :h_peri][bid[:, :h_peri] > 0]); ids4 = np.unique(bid[:, -h_peri:][bid[:, -h_peri:] > 0])
117
+ for rid in np.concatenate((ids1, ids2, ids3, ids4)):
118
+ pos = np.where(bid == rid)
119
+ bh[pos] = 0
120
+ bmin[pos] = [[] for _ in range(len(bmin[pos]))]
121
+
122
+ voxelizer = Voxelizer(
123
+ voxel_size=cfg.meshsize,
124
+ land_cover_source=lc_src_effective,
125
+ trunk_height_ratio=cfg.trunk_height_ratio,
126
+ voxel_dtype=kwargs.get("voxel_dtype", np.int8),
127
+ max_voxel_ram_mb=kwargs.get("max_voxel_ram_mb"),
128
+ )
129
+ vox = voxelizer.generate_combined(
130
+ building_height_grid_ori=bh,
131
+ building_min_height_grid_ori=bmin,
132
+ building_id_grid_ori=bid,
133
+ land_cover_grid_ori=land_cover_grid,
134
+ dem_grid_ori=dem,
135
+ tree_grid_ori=canopy_top,
136
+ canopy_bottom_height_grid_ori=canopy_bottom,
137
+ )
138
+ return self.assemble_voxcity(
139
+ voxcity_grid=vox,
140
+ building_height_grid=bh,
141
+ building_min_height_grid=bmin,
142
+ building_id_grid=bid,
143
+ land_cover_grid=land_cover_grid,
144
+ dem_grid=dem,
145
+ canopy_height_top=canopy_top,
146
+ canopy_height_bottom=canopy_bottom,
147
+ extras={
148
+ "building_gdf": building_gdf_out,
149
+ "land_cover_source": lc_src_effective,
150
+ "building_source": cfg.building_source,
151
+ "dem_source": cfg.dem_source,
152
+ },
153
+ )
154
+
155
+
156
+ class LandCoverSourceStrategy: # ABC simplified to avoid dependency in split
157
+ def build_grid(self, rectangle_vertices, meshsize: float, output_dir: str, **kwargs): # pragma: no cover - interface
158
+ raise NotImplementedError
159
+
160
+
161
+ class DefaultLandCoverStrategy(LandCoverSourceStrategy):
162
+ def __init__(self, source: str) -> None:
163
+ self.source = source
164
+
165
+ def build_grid(self, rectangle_vertices, meshsize: float, output_dir: str, **kwargs):
166
+ return get_land_cover_grid(rectangle_vertices, meshsize, self.source, output_dir, **kwargs)
167
+
168
+
169
+ class LandCoverSourceFactory:
170
+ @staticmethod
171
+ def create(source: str) -> LandCoverSourceStrategy:
172
+ return DefaultLandCoverStrategy(source)
173
+
174
+
175
+ class BuildingSourceStrategy: # ABC simplified
176
+ def build_grids(self, rectangle_vertices, meshsize: float, output_dir: str, **kwargs): # pragma: no cover - interface
177
+ raise NotImplementedError
178
+
179
+
180
+ class DefaultBuildingSourceStrategy(BuildingSourceStrategy):
181
+ def __init__(self, source: str) -> None:
182
+ self.source = source
183
+
184
+ def build_grids(self, rectangle_vertices, meshsize: float, output_dir: str, **kwargs):
185
+ return get_building_height_grid(rectangle_vertices, meshsize, self.source, output_dir, **kwargs)
186
+
187
+
188
+ class BuildingSourceFactory:
189
+ @staticmethod
190
+ def create(source: str) -> BuildingSourceStrategy:
191
+ return DefaultBuildingSourceStrategy(source)
192
+
193
+
194
+ class CanopySourceStrategy: # ABC simplified
195
+ def build_grids(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs): # pragma: no cover
196
+ raise NotImplementedError
197
+
198
+
199
+ class StaticCanopyStrategy(CanopySourceStrategy):
200
+ def __init__(self, cfg: PipelineConfig) -> None:
201
+ self.cfg = cfg
202
+
203
+ def build_grids(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs):
204
+ canopy_top = np.zeros_like(land_cover_grid, dtype=float)
205
+ static_h = self.cfg.static_tree_height if self.cfg.static_tree_height is not None else kwargs.get("static_tree_height", 10.0)
206
+ from ..utils.lc import get_land_cover_classes
207
+ _classes = get_land_cover_classes(self.cfg.land_cover_source)
208
+ _class_to_int = {name: i for i, name in enumerate(_classes.values())}
209
+ _tree_labels = ["Tree", "Trees", "Tree Canopy"]
210
+ _tree_idx = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
211
+ tree_mask = np.isin(land_cover_grid, _tree_idx) if _tree_idx else np.zeros_like(land_cover_grid, dtype=bool)
212
+ canopy_top[tree_mask] = static_h
213
+ tr = self.cfg.trunk_height_ratio if self.cfg.trunk_height_ratio is not None else (11.76 / 19.98)
214
+ canopy_bottom = canopy_top * float(tr)
215
+ return canopy_top, canopy_bottom
216
+
217
+
218
+ class SourceCanopyStrategy(CanopySourceStrategy):
219
+ def __init__(self, source: str) -> None:
220
+ self.source = source
221
+
222
+ def build_grids(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs):
223
+ # Provide land_cover_like for graceful fallback sizing without EE
224
+ return get_canopy_height_grid(
225
+ rectangle_vertices,
226
+ meshsize,
227
+ self.source,
228
+ output_dir,
229
+ land_cover_like=land_cover_grid,
230
+ **kwargs,
231
+ )
232
+
233
+
234
+ class CanopySourceFactory:
235
+ @staticmethod
236
+ def create(source: str, cfg: PipelineConfig) -> CanopySourceStrategy:
237
+ if source == "Static":
238
+ return StaticCanopyStrategy(cfg)
239
+ return SourceCanopyStrategy(source)
240
+
241
+
242
+ class DemSourceStrategy: # ABC simplified
243
+ def build_grid(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs): # pragma: no cover
244
+ raise NotImplementedError
245
+
246
+
247
+ class FlatDemStrategy(DemSourceStrategy):
248
+ def build_grid(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs):
249
+ return np.zeros_like(land_cover_grid)
250
+
251
+
252
+ class SourceDemStrategy(DemSourceStrategy):
253
+ def __init__(self, source: str) -> None:
254
+ self.source = source
255
+
256
+ def build_grid(self, rectangle_vertices, meshsize: float, land_cover_grid: np.ndarray, output_dir: str, **kwargs):
257
+ terrain_gdf = kwargs.get("terrain_gdf")
258
+ if terrain_gdf is not None:
259
+ from ..geoprocessor.raster import create_dem_grid_from_gdf_polygon
260
+ return create_dem_grid_from_gdf_polygon(terrain_gdf, meshsize, rectangle_vertices)
261
+ try:
262
+ return get_dem_grid(rectangle_vertices, meshsize, self.source, output_dir, **kwargs)
263
+ except Exception as e:
264
+ # Fallback to flat DEM if source fails or unsupported
265
+ logger = get_logger(__name__)
266
+ logger.warning("DEM source '%s' failed (%s). Falling back to flat DEM.", self.source, e)
267
+ return np.zeros_like(land_cover_grid)
268
+
269
+
270
+ class DemSourceFactory:
271
+ @staticmethod
272
+ def create(source: str) -> DemSourceStrategy:
273
+ # Normalize and auto-fallback: None/"none" -> Flat
274
+ try:
275
+ src_norm = (source or "").strip().lower()
276
+ except Exception:
277
+ src_norm = ""
278
+ if (not source) or (src_norm in {"flat", "none", "null"}):
279
+ return FlatDemStrategy()
280
+ return SourceDemStrategy(source)
281
+
282
+