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.
- voxcity/__init__.py +10 -4
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +9 -1
- voxcity/exporter/cityles.py +129 -34
- voxcity/exporter/envimet.py +51 -26
- voxcity/exporter/magicavoxel.py +42 -5
- voxcity/exporter/netcdf.py +27 -0
- voxcity/exporter/obj.py +103 -28
- voxcity/generator/__init__.py +47 -0
- voxcity/generator/api.py +721 -0
- voxcity/generator/grids.py +381 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +392 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +1488 -1169
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +26 -10
- voxcity/geoprocessor/network.py +35 -6
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +435 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +159 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +824 -820
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +66 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/shape.py +230 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +1145 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
- voxcity-1.0.2.dist-info/RECORD +81 -0
- voxcity/generator.py +0 -1302
- voxcity/geoprocessor/grid.py +0 -1739
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2339
- voxcity/utils/visualization.py +0 -2849
- voxcity/utils/weather.py +0 -1038
- voxcity-0.6.26.dist-info/RECORD +0 -38
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""Functions for updating VoxCity objects with new grid data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ..models import (
|
|
9
|
+
GridMetadata,
|
|
10
|
+
BuildingGrid,
|
|
11
|
+
LandCoverGrid,
|
|
12
|
+
DemGrid,
|
|
13
|
+
VoxelGrid,
|
|
14
|
+
CanopyGrid,
|
|
15
|
+
VoxCity,
|
|
16
|
+
)
|
|
17
|
+
from .voxelizer import Voxelizer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def update_voxcity(
|
|
21
|
+
city: VoxCity,
|
|
22
|
+
*,
|
|
23
|
+
buildings: Optional[BuildingGrid] = None,
|
|
24
|
+
building_heights: Optional[np.ndarray] = None,
|
|
25
|
+
building_min_heights: Optional[np.ndarray] = None,
|
|
26
|
+
building_ids: Optional[np.ndarray] = None,
|
|
27
|
+
land_cover: Optional[Union[LandCoverGrid, np.ndarray]] = None,
|
|
28
|
+
dem: Optional[Union[DemGrid, np.ndarray]] = None,
|
|
29
|
+
tree_canopy: Optional[Union[CanopyGrid, np.ndarray]] = None,
|
|
30
|
+
canopy_top: Optional[np.ndarray] = None,
|
|
31
|
+
canopy_bottom: Optional[np.ndarray] = None,
|
|
32
|
+
building_gdf=None,
|
|
33
|
+
tree_gdf=None,
|
|
34
|
+
tree_gdf_mode: str = "replace",
|
|
35
|
+
land_cover_source: Optional[str] = None,
|
|
36
|
+
trunk_height_ratio: Optional[float] = None,
|
|
37
|
+
voxel_dtype=None,
|
|
38
|
+
max_voxel_ram_mb: Optional[float] = None,
|
|
39
|
+
inplace: bool = False,
|
|
40
|
+
) -> VoxCity:
|
|
41
|
+
"""
|
|
42
|
+
Update a VoxCity object with new grid data and regenerate the VoxelGrid.
|
|
43
|
+
|
|
44
|
+
This function allows partial updates - only the grids you provide will be
|
|
45
|
+
updated, while the rest will be taken from the existing VoxCity object.
|
|
46
|
+
The VoxelGrid is always regenerated from the (updated) component grids.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
city : VoxCity
|
|
51
|
+
The existing VoxCity object to update.
|
|
52
|
+
|
|
53
|
+
buildings : BuildingGrid, optional
|
|
54
|
+
Complete replacement for the building grid. If provided, takes precedence
|
|
55
|
+
over individual building_heights/building_min_heights/building_ids.
|
|
56
|
+
|
|
57
|
+
building_heights : np.ndarray, optional
|
|
58
|
+
2D array of building heights. If buildings is not provided, this updates
|
|
59
|
+
only the heights while keeping existing min_heights and ids.
|
|
60
|
+
|
|
61
|
+
building_min_heights : np.ndarray, optional
|
|
62
|
+
2D object-dtype array of lists containing [min_height, max_height] pairs
|
|
63
|
+
for each building segment per cell.
|
|
64
|
+
|
|
65
|
+
building_ids : np.ndarray, optional
|
|
66
|
+
2D array of building IDs per cell.
|
|
67
|
+
|
|
68
|
+
land_cover : LandCoverGrid or np.ndarray, optional
|
|
69
|
+
New land cover data. Can be a LandCoverGrid or a raw numpy array.
|
|
70
|
+
|
|
71
|
+
dem : DemGrid or np.ndarray, optional
|
|
72
|
+
New DEM/elevation data. Can be a DemGrid or a raw numpy array.
|
|
73
|
+
|
|
74
|
+
tree_canopy : CanopyGrid or np.ndarray, optional
|
|
75
|
+
New tree canopy data. Can be a CanopyGrid (with top/bottom) or a raw
|
|
76
|
+
numpy array (interpreted as canopy top heights).
|
|
77
|
+
|
|
78
|
+
canopy_top : np.ndarray, optional
|
|
79
|
+
2D array of tree canopy top heights. Takes precedence over tree_canopy
|
|
80
|
+
for top heights if both are provided.
|
|
81
|
+
|
|
82
|
+
canopy_bottom : np.ndarray, optional
|
|
83
|
+
2D array of tree canopy bottom heights (crown base).
|
|
84
|
+
|
|
85
|
+
building_gdf : GeoDataFrame, optional
|
|
86
|
+
Updated building GeoDataFrame. If provided without building grids
|
|
87
|
+
(building_heights, building_min_heights, building_ids), the function
|
|
88
|
+
will automatically generate the building grids from the GeoDataFrame
|
|
89
|
+
using create_building_height_grid_from_gdf_polygon. The GeoDataFrame
|
|
90
|
+
is also stored in city.extras['building_gdf'].
|
|
91
|
+
|
|
92
|
+
tree_gdf : GeoDataFrame, optional
|
|
93
|
+
Updated tree GeoDataFrame. If provided without tree canopy data
|
|
94
|
+
(tree_canopy, canopy_top, canopy_bottom), the function will
|
|
95
|
+
automatically generate the canopy grids from the GeoDataFrame
|
|
96
|
+
using create_canopy_grids_from_tree_gdf. The GeoDataFrame must
|
|
97
|
+
contain 'top_height', 'bottom_height', 'crown_diameter', and
|
|
98
|
+
'geometry' columns. The GeoDataFrame is stored in city.extras['tree_gdf'].
|
|
99
|
+
|
|
100
|
+
tree_gdf_mode : str, default "replace"
|
|
101
|
+
How to combine tree_gdf with existing canopy data. Options:
|
|
102
|
+
- "replace": Replace the existing canopy grids with new ones from tree_gdf.
|
|
103
|
+
- "add": Merge the tree_gdf grids with existing canopy grids by taking
|
|
104
|
+
the maximum height at each cell (preserves existing trees).
|
|
105
|
+
|
|
106
|
+
land_cover_source : str, optional
|
|
107
|
+
The land cover source name for proper voxelization. If not provided,
|
|
108
|
+
attempts to use the source from city.extras or defaults to 'OpenStreetMap'.
|
|
109
|
+
|
|
110
|
+
trunk_height_ratio : float, optional
|
|
111
|
+
Ratio of trunk height to total tree height for canopy bottom calculation.
|
|
112
|
+
Default is approximately 0.588 (11.76/19.98).
|
|
113
|
+
|
|
114
|
+
voxel_dtype : dtype, optional
|
|
115
|
+
NumPy dtype for the voxel grid. Defaults to np.int8.
|
|
116
|
+
|
|
117
|
+
max_voxel_ram_mb : float, optional
|
|
118
|
+
Maximum RAM in MB for voxel grid allocation. Raises MemoryError if exceeded.
|
|
119
|
+
|
|
120
|
+
inplace : bool, default False
|
|
121
|
+
If True, modifies the input city object directly and returns it.
|
|
122
|
+
If False, creates and returns a new VoxCity object.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
VoxCity
|
|
127
|
+
The updated VoxCity object with regenerated VoxelGrid.
|
|
128
|
+
|
|
129
|
+
Examples
|
|
130
|
+
--------
|
|
131
|
+
Update building heights and regenerate voxels:
|
|
132
|
+
|
|
133
|
+
>>> import numpy as np
|
|
134
|
+
>>> new_heights = city.buildings.heights.copy()
|
|
135
|
+
>>> new_heights[10:20, 10:20] = 50.0 # Increase height in a region
|
|
136
|
+
>>> updated = update_voxcity(city, building_heights=new_heights)
|
|
137
|
+
|
|
138
|
+
Update with a complete new BuildingGrid:
|
|
139
|
+
|
|
140
|
+
>>> from voxcity.models import BuildingGrid
|
|
141
|
+
>>> new_buildings = BuildingGrid(heights=..., min_heights=..., ids=..., meta=city.buildings.meta)
|
|
142
|
+
>>> updated = update_voxcity(city, buildings=new_buildings)
|
|
143
|
+
|
|
144
|
+
Update land cover and DEM together:
|
|
145
|
+
|
|
146
|
+
>>> updated = update_voxcity(city, land_cover=new_lc_array, dem=new_dem_array)
|
|
147
|
+
|
|
148
|
+
Update buildings from GeoDataFrame (automatic grid generation):
|
|
149
|
+
|
|
150
|
+
>>> updated = update_voxcity(city, building_gdf=updated_building_gdf)
|
|
151
|
+
|
|
152
|
+
Update trees from GeoDataFrame (replace existing canopy):
|
|
153
|
+
|
|
154
|
+
>>> updated = update_voxcity(city, tree_gdf=updated_tree_gdf)
|
|
155
|
+
|
|
156
|
+
Add trees from GeoDataFrame to existing canopy:
|
|
157
|
+
|
|
158
|
+
>>> updated = update_voxcity(city, tree_gdf=new_tree_gdf, tree_gdf_mode="add")
|
|
159
|
+
"""
|
|
160
|
+
# Resolve metadata from existing city
|
|
161
|
+
meta = city.buildings.meta
|
|
162
|
+
meshsize = meta.meshsize
|
|
163
|
+
|
|
164
|
+
# --- Auto-generate building grids from GeoDataFrame if provided ---
|
|
165
|
+
if building_gdf is not None and buildings is None and building_heights is None:
|
|
166
|
+
# Auto-generate building grids from the GeoDataFrame
|
|
167
|
+
from ..geoprocessor.raster import create_building_height_grid_from_gdf_polygon
|
|
168
|
+
|
|
169
|
+
rectangle_vertices = city.extras.get("rectangle_vertices")
|
|
170
|
+
if rectangle_vertices is None:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"Cannot auto-generate building grids: 'rectangle_vertices' not found in city.extras. "
|
|
173
|
+
"Provide building_heights, building_min_heights, and building_ids explicitly."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
building_heights, building_min_heights, building_ids, _ = (
|
|
177
|
+
create_building_height_grid_from_gdf_polygon(
|
|
178
|
+
building_gdf,
|
|
179
|
+
meshsize,
|
|
180
|
+
rectangle_vertices,
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# --- Auto-generate canopy grids from tree GeoDataFrame if provided ---
|
|
185
|
+
if tree_gdf is not None and tree_canopy is None and canopy_top is None:
|
|
186
|
+
# Validate tree_gdf_mode
|
|
187
|
+
if tree_gdf_mode not in ("replace", "add"):
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"Invalid tree_gdf_mode '{tree_gdf_mode}'. Must be 'replace' or 'add'."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Auto-generate canopy grids from the tree GeoDataFrame
|
|
193
|
+
from ..geoprocessor.raster import create_canopy_grids_from_tree_gdf
|
|
194
|
+
|
|
195
|
+
rectangle_vertices = city.extras.get("rectangle_vertices")
|
|
196
|
+
if rectangle_vertices is None:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
"Cannot auto-generate canopy grids: 'rectangle_vertices' not found in city.extras. "
|
|
199
|
+
"Provide canopy_top and canopy_bottom explicitly."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
new_canopy_top, new_canopy_bottom = create_canopy_grids_from_tree_gdf(
|
|
203
|
+
tree_gdf,
|
|
204
|
+
meshsize,
|
|
205
|
+
rectangle_vertices,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if tree_gdf_mode == "add":
|
|
209
|
+
# Merge with existing canopy by taking maximum values
|
|
210
|
+
existing_top = city.tree_canopy.top
|
|
211
|
+
existing_bottom = city.tree_canopy.bottom
|
|
212
|
+
if existing_top is not None:
|
|
213
|
+
canopy_top = np.maximum(existing_top, new_canopy_top)
|
|
214
|
+
else:
|
|
215
|
+
canopy_top = new_canopy_top
|
|
216
|
+
if existing_bottom is not None:
|
|
217
|
+
canopy_bottom = np.maximum(existing_bottom, new_canopy_bottom)
|
|
218
|
+
else:
|
|
219
|
+
canopy_bottom = new_canopy_bottom
|
|
220
|
+
else:
|
|
221
|
+
# Replace mode: use new canopy grids directly
|
|
222
|
+
canopy_top = new_canopy_top
|
|
223
|
+
canopy_bottom = new_canopy_bottom
|
|
224
|
+
|
|
225
|
+
# --- Resolve building data ---
|
|
226
|
+
if buildings is not None:
|
|
227
|
+
# Use provided BuildingGrid directly
|
|
228
|
+
final_building_heights = buildings.heights
|
|
229
|
+
final_building_min_heights = buildings.min_heights
|
|
230
|
+
final_building_ids = buildings.ids
|
|
231
|
+
final_building_meta = buildings.meta
|
|
232
|
+
else:
|
|
233
|
+
# Use individual arrays or fall back to existing
|
|
234
|
+
final_building_heights = (
|
|
235
|
+
building_heights if building_heights is not None else city.buildings.heights
|
|
236
|
+
)
|
|
237
|
+
final_building_min_heights = (
|
|
238
|
+
building_min_heights
|
|
239
|
+
if building_min_heights is not None
|
|
240
|
+
else city.buildings.min_heights
|
|
241
|
+
)
|
|
242
|
+
final_building_ids = (
|
|
243
|
+
building_ids if building_ids is not None else city.buildings.ids
|
|
244
|
+
)
|
|
245
|
+
final_building_meta = meta
|
|
246
|
+
|
|
247
|
+
# --- Resolve land cover data ---
|
|
248
|
+
if land_cover is not None:
|
|
249
|
+
if isinstance(land_cover, LandCoverGrid):
|
|
250
|
+
final_land_cover = land_cover.classes
|
|
251
|
+
else:
|
|
252
|
+
final_land_cover = land_cover
|
|
253
|
+
else:
|
|
254
|
+
final_land_cover = city.land_cover.classes
|
|
255
|
+
|
|
256
|
+
# --- Resolve DEM data ---
|
|
257
|
+
if dem is not None:
|
|
258
|
+
if isinstance(dem, DemGrid):
|
|
259
|
+
final_dem = dem.elevation
|
|
260
|
+
else:
|
|
261
|
+
final_dem = dem
|
|
262
|
+
else:
|
|
263
|
+
final_dem = city.dem.elevation
|
|
264
|
+
|
|
265
|
+
# --- Resolve canopy data ---
|
|
266
|
+
# Priority: canopy_top/canopy_bottom > tree_canopy > existing
|
|
267
|
+
if canopy_top is not None:
|
|
268
|
+
final_canopy_top = canopy_top
|
|
269
|
+
elif tree_canopy is not None:
|
|
270
|
+
if isinstance(tree_canopy, CanopyGrid):
|
|
271
|
+
final_canopy_top = tree_canopy.top
|
|
272
|
+
else:
|
|
273
|
+
final_canopy_top = tree_canopy
|
|
274
|
+
else:
|
|
275
|
+
final_canopy_top = city.tree_canopy.top
|
|
276
|
+
|
|
277
|
+
if canopy_bottom is not None:
|
|
278
|
+
final_canopy_bottom = canopy_bottom
|
|
279
|
+
elif tree_canopy is not None and isinstance(tree_canopy, CanopyGrid):
|
|
280
|
+
final_canopy_bottom = tree_canopy.bottom
|
|
281
|
+
else:
|
|
282
|
+
final_canopy_bottom = city.tree_canopy.bottom
|
|
283
|
+
|
|
284
|
+
# --- Determine land cover source ---
|
|
285
|
+
if land_cover_source is None:
|
|
286
|
+
# Try to get from extras
|
|
287
|
+
land_cover_source = city.extras.get("land_cover_source")
|
|
288
|
+
if land_cover_source is None:
|
|
289
|
+
selected = city.extras.get("selected_sources", {})
|
|
290
|
+
land_cover_source = selected.get("land_cover_source", "OpenStreetMap")
|
|
291
|
+
|
|
292
|
+
# --- Build updated extras ---
|
|
293
|
+
new_extras = dict(city.extras)
|
|
294
|
+
if building_gdf is not None:
|
|
295
|
+
new_extras["building_gdf"] = building_gdf
|
|
296
|
+
if tree_gdf is not None:
|
|
297
|
+
new_extras["tree_gdf"] = tree_gdf
|
|
298
|
+
new_extras["canopy_top"] = final_canopy_top
|
|
299
|
+
new_extras["canopy_bottom"] = final_canopy_bottom
|
|
300
|
+
|
|
301
|
+
# --- Shape validation ---
|
|
302
|
+
expected_shape = final_land_cover.shape
|
|
303
|
+
shapes = {
|
|
304
|
+
"building_heights": final_building_heights.shape,
|
|
305
|
+
"building_min_heights": final_building_min_heights.shape,
|
|
306
|
+
"building_ids": final_building_ids.shape,
|
|
307
|
+
"land_cover": final_land_cover.shape,
|
|
308
|
+
"dem": final_dem.shape,
|
|
309
|
+
"canopy_top": final_canopy_top.shape if final_canopy_top is not None else None,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
mismatched = {k: v for k, v in shapes.items() if v is not None and v != expected_shape}
|
|
313
|
+
if mismatched:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"Grid shape mismatch! Expected {expected_shape}, but got: {mismatched}. "
|
|
316
|
+
f"All grids must have the same shape."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# --- Create Voxelizer and regenerate voxel grid ---
|
|
320
|
+
_voxel_dtype = voxel_dtype if voxel_dtype is not None else np.int8
|
|
321
|
+
voxelizer = Voxelizer(
|
|
322
|
+
voxel_size=meshsize,
|
|
323
|
+
land_cover_source=land_cover_source,
|
|
324
|
+
trunk_height_ratio=trunk_height_ratio,
|
|
325
|
+
voxel_dtype=_voxel_dtype,
|
|
326
|
+
max_voxel_ram_mb=max_voxel_ram_mb,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
new_voxel_classes = voxelizer.generate_combined(
|
|
330
|
+
building_height_grid_ori=final_building_heights,
|
|
331
|
+
building_min_height_grid_ori=final_building_min_heights,
|
|
332
|
+
building_id_grid_ori=final_building_ids,
|
|
333
|
+
land_cover_grid_ori=final_land_cover,
|
|
334
|
+
dem_grid_ori=final_dem,
|
|
335
|
+
tree_grid_ori=final_canopy_top,
|
|
336
|
+
canopy_bottom_height_grid_ori=final_canopy_bottom,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# --- Assemble result ---
|
|
340
|
+
new_voxels = VoxelGrid(classes=new_voxel_classes, meta=meta)
|
|
341
|
+
new_buildings = BuildingGrid(
|
|
342
|
+
heights=final_building_heights,
|
|
343
|
+
min_heights=final_building_min_heights,
|
|
344
|
+
ids=final_building_ids,
|
|
345
|
+
meta=final_building_meta,
|
|
346
|
+
)
|
|
347
|
+
new_land_cover = LandCoverGrid(classes=final_land_cover, meta=meta)
|
|
348
|
+
new_dem = DemGrid(elevation=final_dem, meta=meta)
|
|
349
|
+
new_canopy = CanopyGrid(
|
|
350
|
+
top=final_canopy_top,
|
|
351
|
+
bottom=final_canopy_bottom,
|
|
352
|
+
meta=meta,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if inplace:
|
|
356
|
+
city.voxels = new_voxels
|
|
357
|
+
city.buildings = new_buildings
|
|
358
|
+
city.land_cover = new_land_cover
|
|
359
|
+
city.dem = new_dem
|
|
360
|
+
city.tree_canopy = new_canopy
|
|
361
|
+
city.extras = new_extras
|
|
362
|
+
return city
|
|
363
|
+
else:
|
|
364
|
+
return VoxCity(
|
|
365
|
+
voxels=new_voxels,
|
|
366
|
+
buildings=new_buildings,
|
|
367
|
+
land_cover=new_land_cover,
|
|
368
|
+
dem=new_dem,
|
|
369
|
+
tree_canopy=new_canopy,
|
|
370
|
+
extras=new_extras,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def regenerate_voxels(
|
|
375
|
+
city: VoxCity,
|
|
376
|
+
*,
|
|
377
|
+
land_cover_source: Optional[str] = None,
|
|
378
|
+
trunk_height_ratio: Optional[float] = None,
|
|
379
|
+
voxel_dtype=None,
|
|
380
|
+
max_voxel_ram_mb: Optional[float] = None,
|
|
381
|
+
inplace: bool = False,
|
|
382
|
+
) -> VoxCity:
|
|
383
|
+
"""
|
|
384
|
+
Regenerate only the VoxelGrid from existing component grids.
|
|
385
|
+
|
|
386
|
+
This is a convenience function for when you've modified the grids in-place
|
|
387
|
+
and need to regenerate the voxels without passing all parameters.
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
city : VoxCity
|
|
392
|
+
The VoxCity object whose voxels should be regenerated.
|
|
393
|
+
|
|
394
|
+
land_cover_source : str, optional
|
|
395
|
+
Land cover source for voxelization. Defaults to source from extras.
|
|
396
|
+
|
|
397
|
+
trunk_height_ratio : float, optional
|
|
398
|
+
Trunk height ratio for tree canopy calculation.
|
|
399
|
+
|
|
400
|
+
voxel_dtype : dtype, optional
|
|
401
|
+
NumPy dtype for voxel grid.
|
|
402
|
+
|
|
403
|
+
max_voxel_ram_mb : float, optional
|
|
404
|
+
Maximum RAM in MB for voxel allocation.
|
|
405
|
+
|
|
406
|
+
inplace : bool, default False
|
|
407
|
+
If True, modifies city directly; otherwise returns a new object.
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
VoxCity
|
|
412
|
+
The VoxCity object with regenerated VoxelGrid.
|
|
413
|
+
|
|
414
|
+
Examples
|
|
415
|
+
--------
|
|
416
|
+
>>> # Modify building heights in place
|
|
417
|
+
>>> city.buildings.heights[50:60, 50:60] = 100.0
|
|
418
|
+
>>> # Regenerate voxels to reflect the change
|
|
419
|
+
>>> city = regenerate_voxels(city, inplace=True)
|
|
420
|
+
"""
|
|
421
|
+
return update_voxcity(
|
|
422
|
+
city,
|
|
423
|
+
land_cover_source=land_cover_source,
|
|
424
|
+
trunk_height_ratio=trunk_height_ratio,
|
|
425
|
+
voxel_dtype=voxel_dtype,
|
|
426
|
+
max_voxel_ram_mb=max_voxel_ram_mb,
|
|
427
|
+
inplace=inplace,
|
|
428
|
+
)
|
|
429
|
+
|