voxcity 1.0.2__py3-none-any.whl → 1.0.15__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 (41) hide show
  1. voxcity/downloader/ocean.py +559 -0
  2. voxcity/generator/api.py +6 -0
  3. voxcity/generator/grids.py +45 -32
  4. voxcity/generator/pipeline.py +327 -27
  5. voxcity/geoprocessor/draw.py +14 -8
  6. voxcity/geoprocessor/raster/__init__.py +2 -0
  7. voxcity/geoprocessor/raster/core.py +31 -0
  8. voxcity/geoprocessor/raster/landcover.py +173 -49
  9. voxcity/geoprocessor/raster/raster.py +1 -1
  10. voxcity/models.py +2 -0
  11. voxcity/simulator/solar/__init__.py +13 -0
  12. voxcity/simulator_gpu/__init__.py +90 -0
  13. voxcity/simulator_gpu/core.py +322 -0
  14. voxcity/simulator_gpu/domain.py +36 -0
  15. voxcity/simulator_gpu/init_taichi.py +154 -0
  16. voxcity/simulator_gpu/raytracing.py +776 -0
  17. voxcity/simulator_gpu/solar/__init__.py +222 -0
  18. voxcity/simulator_gpu/solar/core.py +66 -0
  19. voxcity/simulator_gpu/solar/csf.py +1249 -0
  20. voxcity/simulator_gpu/solar/domain.py +618 -0
  21. voxcity/simulator_gpu/solar/epw.py +421 -0
  22. voxcity/simulator_gpu/solar/integration.py +4322 -0
  23. voxcity/simulator_gpu/solar/mask.py +459 -0
  24. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  25. voxcity/simulator_gpu/solar/raytracing.py +182 -0
  26. voxcity/simulator_gpu/solar/reflection.py +533 -0
  27. voxcity/simulator_gpu/solar/sky.py +907 -0
  28. voxcity/simulator_gpu/solar/solar.py +337 -0
  29. voxcity/simulator_gpu/solar/svf.py +446 -0
  30. voxcity/simulator_gpu/solar/volumetric.py +2099 -0
  31. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  32. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  33. voxcity/simulator_gpu/visibility/integration.py +808 -0
  34. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  35. voxcity/simulator_gpu/visibility/view.py +944 -0
  36. voxcity/visualizer/renderer.py +2 -1
  37. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/METADATA +16 -53
  38. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/RECORD +41 -16
  39. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/WHEEL +0 -0
  40. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/AUTHORS.rst +0 -0
  41. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,808 @@
1
+ """
2
+ VoxCity Integration Module for simulator_gpu.visibility
3
+
4
+ This module provides utilities for loading VoxCity models and using them
5
+ with the GPU-accelerated visibility analysis tools.
6
+
7
+ This module emulates the voxcity.simulator.visibility API using Taichi GPU acceleration.
8
+
9
+ VoxCity models contain:
10
+ - 3D voxel grids with building, tree, and ground information
11
+ - Land cover classification codes
12
+ - Building heights and IDs
13
+ - Tree canopy data
14
+
15
+ API Compatibility:
16
+ The functions in this module match the voxcity.simulator.visibility API:
17
+ - get_view_index() - same signature as voxcity.simulator.visibility.get_view_index
18
+ - get_sky_view_factor_map() - same signature as voxcity.simulator.visibility.get_sky_view_factor_map
19
+ - get_surface_view_factor() - same signature as voxcity.simulator.visibility.get_surface_view_factor
20
+ - get_landmark_visibility_map() - same signature as voxcity.simulator.visibility.get_landmark_visibility_map
21
+ - get_surface_landmark_visibility() - same signature as voxcity.simulator.visibility.get_surface_landmark_visibility
22
+ - mark_building_by_id() - same as voxcity.simulator.visibility.mark_building_by_id
23
+ """
24
+
25
+ import numpy as np
26
+ from typing import Dict, Optional, Tuple, Union, List
27
+ from dataclasses import dataclass
28
+ import math
29
+
30
+ from ..domain import Domain
31
+ from .view import ViewCalculator, SurfaceViewFactorCalculator
32
+ from .landmark import LandmarkVisibilityCalculator, SurfaceLandmarkVisibilityCalculator
33
+ from .landmark import mark_building_by_id
34
+
35
+
36
+ # VoxCity voxel class codes
37
+ VOXCITY_GROUND_CODE = -1
38
+ VOXCITY_TREE_CODE = -2
39
+ VOXCITY_BUILDING_CODE = -3
40
+
41
+ # Green view target codes
42
+ GREEN_VIEW_CODES = (-2, 2, 5, 6, 7, 8) # Trees and vegetation classes
43
+
44
+
45
+ def create_domain_from_voxcity(voxcity) -> Domain:
46
+ """
47
+ Create a Domain object from a VoxCity model.
48
+
49
+ Args:
50
+ voxcity: VoxCity object with voxels attribute
51
+
52
+ Returns:
53
+ Domain object configured for view analysis
54
+ """
55
+ voxel_data = voxcity.voxels.classes
56
+ nx, ny, nz = voxel_data.shape
57
+ meshsize = voxcity.voxels.meta.meshsize
58
+
59
+ domain = Domain(
60
+ nx=nx, ny=ny, nz=nz,
61
+ dx=meshsize, dy=meshsize, dz=meshsize
62
+ )
63
+
64
+ # Set domain from voxel data
65
+ domain.set_from_voxel_data(voxel_data, tree_code=VOXCITY_TREE_CODE)
66
+
67
+ return domain
68
+
69
+
70
+ def get_view_index_gpu(
71
+ voxcity,
72
+ mode: str = None,
73
+ hit_values: Tuple[int, ...] = None,
74
+ inclusion_mode: bool = True,
75
+ view_point_height: float = 1.5,
76
+ n_azimuth: int = 120,
77
+ n_elevation: int = 20,
78
+ elevation_min_degrees: float = -30.0,
79
+ elevation_max_degrees: float = 30.0,
80
+ ray_sampling: str = "grid",
81
+ n_rays: int = None,
82
+ tree_k: float = 0.5,
83
+ tree_lad: float = 1.0,
84
+ show_plot: bool = False,
85
+ **kwargs
86
+ ) -> np.ndarray:
87
+ """
88
+ GPU-accelerated View Index calculation for VoxCity.
89
+
90
+ This function emulates voxcity.simulator.visibility.view.get_view_index
91
+ using Taichi GPU acceleration.
92
+
93
+ Args:
94
+ voxcity: VoxCity object
95
+ mode: Predefined mode ('green', 'sky', or None for custom)
96
+ hit_values: Target voxel values to count as visible
97
+ inclusion_mode: If True, count hits on targets; if False, count non-blocked rays
98
+ view_point_height: Observer height above ground (meters)
99
+ n_azimuth: Number of azimuthal divisions
100
+ n_elevation: Number of elevation divisions
101
+ elevation_min_degrees: Minimum viewing angle
102
+ elevation_max_degrees: Maximum viewing angle
103
+ ray_sampling: 'grid' or 'fibonacci'
104
+ n_rays: Total rays for fibonacci sampling
105
+ tree_k: Tree extinction coefficient
106
+ tree_lad: Leaf area density
107
+ show_plot: Whether to display a matplotlib plot
108
+ **kwargs: Additional parameters
109
+
110
+ Returns:
111
+ 2D array of view index values
112
+ """
113
+ voxel_data = voxcity.voxels.classes
114
+ meshsize = voxcity.voxels.meta.meshsize
115
+ nx, ny, nz = voxel_data.shape
116
+
117
+ # Create domain
118
+ domain = Domain(
119
+ nx=nx, ny=ny, nz=nz,
120
+ dx=meshsize, dy=meshsize, dz=meshsize
121
+ )
122
+
123
+ # Create calculator
124
+ calc = ViewCalculator(
125
+ domain,
126
+ n_azimuth=n_azimuth,
127
+ n_elevation=n_elevation,
128
+ ray_sampling=ray_sampling,
129
+ n_rays=n_rays
130
+ )
131
+
132
+ # Compute view index
133
+ vi_map = calc.compute_view_index(
134
+ voxel_data=voxel_data,
135
+ mode=mode,
136
+ hit_values=hit_values,
137
+ inclusion_mode=inclusion_mode,
138
+ view_point_height=view_point_height,
139
+ elevation_min_degrees=elevation_min_degrees,
140
+ elevation_max_degrees=elevation_max_degrees,
141
+ tree_k=tree_k,
142
+ tree_lad=tree_lad
143
+ )
144
+
145
+ # Note: ViewCalculator.compute_view_index already flips to match VoxCity coordinate system
146
+
147
+ # Plot if requested
148
+ if show_plot:
149
+ try:
150
+ import matplotlib.pyplot as plt
151
+ colormap = kwargs.get('colormap', 'viridis')
152
+ vmin = kwargs.get('vmin', 0.0)
153
+ vmax = kwargs.get('vmax', 1.0)
154
+
155
+ cmap = plt.cm.get_cmap(colormap).copy()
156
+ cmap.set_bad(color='lightgray')
157
+ plt.figure(figsize=(10, 8))
158
+ plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
159
+ plt.colorbar(label='View Index')
160
+ plt.axis('off')
161
+ plt.show()
162
+ except ImportError:
163
+ pass
164
+
165
+ return vi_map
166
+
167
+
168
+ # VoxCity API-compatible function names (recommended interface)
169
+ def get_view_index(voxcity, mode=None, hit_values=None, inclusion_mode=True, fast_path=True, **kwargs):
170
+ """
171
+ GPU-accelerated View Index calculation for VoxCity.
172
+
173
+ This function matches the signature of voxcity.simulator.visibility.get_view_index
174
+ using Taichi GPU acceleration.
175
+
176
+ Args:
177
+ voxcity: VoxCity object
178
+ mode: Predefined mode ('green', 'sky', or None for custom)
179
+ hit_values: Target voxel values to count as visible
180
+ inclusion_mode: If True, count hits on targets; if False, count non-blocked rays
181
+ **kwargs: Additional parameters including:
182
+ - view_point_height (float): Observer height above ground (default: 1.5)
183
+ - N_azimuth (int): Number of azimuthal divisions (default: 120)
184
+ - N_elevation (int): Number of elevation divisions (default: 20)
185
+ - elevation_min_degrees (float): Minimum viewing angle (default: -30)
186
+ - elevation_max_degrees (float): Maximum viewing angle (default: 30)
187
+ - ray_sampling (str): 'grid' or 'fibonacci' (default: 'grid')
188
+ - N_rays (int): Total rays for fibonacci sampling
189
+ - tree_k (float): Tree extinction coefficient (default: 0.5)
190
+ - tree_lad (float): Leaf area density (default: 1.0)
191
+ - colormap (str): Matplotlib colormap name (default: 'viridis')
192
+ - vmin, vmax (float): Colormap limits (default: 0.0, 1.0)
193
+ - obj_export (bool): Whether to export OBJ file (default: False)
194
+
195
+ Returns:
196
+ 2D array of view index values
197
+ """
198
+ # Map VoxCity-style kwargs to our internal parameter names
199
+ n_azimuth = kwargs.pop('N_azimuth', kwargs.pop('n_azimuth', 120))
200
+ n_elevation = kwargs.pop('N_elevation', kwargs.pop('n_elevation', 20))
201
+ n_rays = kwargs.pop('N_rays', kwargs.pop('n_rays', None))
202
+ view_point_height = kwargs.pop('view_point_height', 1.5)
203
+ elevation_min_degrees = kwargs.pop('elevation_min_degrees', -30.0)
204
+ elevation_max_degrees = kwargs.pop('elevation_max_degrees', 30.0)
205
+ ray_sampling = kwargs.pop('ray_sampling', 'grid')
206
+ tree_k = kwargs.pop('tree_k', 0.5)
207
+ tree_lad = kwargs.pop('tree_lad', 1.0)
208
+
209
+ return get_view_index_gpu(
210
+ voxcity,
211
+ mode=mode,
212
+ hit_values=hit_values,
213
+ inclusion_mode=inclusion_mode,
214
+ view_point_height=view_point_height,
215
+ n_azimuth=n_azimuth,
216
+ n_elevation=n_elevation,
217
+ elevation_min_degrees=elevation_min_degrees,
218
+ elevation_max_degrees=elevation_max_degrees,
219
+ ray_sampling=ray_sampling,
220
+ n_rays=n_rays,
221
+ tree_k=tree_k,
222
+ tree_lad=tree_lad,
223
+ show_plot=True, # VoxCity default shows plot
224
+ **kwargs
225
+ )
226
+
227
+
228
+ def get_sky_view_factor_map(voxcity, show_plot=False, **kwargs):
229
+ """
230
+ GPU-accelerated Sky View Factor calculation for VoxCity.
231
+
232
+ This function matches the signature of voxcity.simulator.visibility.get_sky_view_factor_map
233
+ using Taichi GPU acceleration.
234
+
235
+ Args:
236
+ voxcity: VoxCity object
237
+ show_plot: Whether to display a matplotlib plot
238
+ **kwargs: Additional parameters including:
239
+ - view_point_height (float): Observer height above ground (default: 1.5)
240
+ - N_azimuth (int): Number of azimuthal divisions (default: 120)
241
+ - N_elevation (int): Number of elevation divisions (default: 20)
242
+ - tree_k (float): Tree extinction coefficient (default: 0.6)
243
+ - tree_lad (float): Leaf area density (default: 1.0)
244
+ - colormap (str): Matplotlib colormap name (default: 'BuPu_r')
245
+
246
+ Returns:
247
+ 2D array of SVF values
248
+ """
249
+ n_azimuth = kwargs.pop('N_azimuth', kwargs.pop('n_azimuth', 120))
250
+ n_elevation = kwargs.pop('N_elevation', kwargs.pop('n_elevation', 20))
251
+ view_point_height = kwargs.pop('view_point_height', 1.5)
252
+ tree_k = kwargs.pop('tree_k', 0.6)
253
+ tree_lad = kwargs.pop('tree_lad', 1.0)
254
+ colormap = kwargs.pop('colormap', 'BuPu_r')
255
+
256
+ return get_view_index_gpu(
257
+ voxcity,
258
+ mode='sky',
259
+ view_point_height=view_point_height,
260
+ n_azimuth=n_azimuth,
261
+ n_elevation=n_elevation,
262
+ elevation_min_degrees=0.0,
263
+ elevation_max_degrees=90.0,
264
+ tree_k=tree_k,
265
+ tree_lad=tree_lad,
266
+ show_plot=show_plot,
267
+ colormap=colormap,
268
+ **kwargs
269
+ )
270
+
271
+
272
+ def get_surface_view_factor(voxcity, mode=None, **kwargs):
273
+ """
274
+ GPU-accelerated Surface View Factor calculation for VoxCity.
275
+
276
+ This function matches the signature of voxcity.simulator.visibility.get_surface_view_factor
277
+ using Taichi GPU acceleration.
278
+
279
+ Computes view factors for building surface faces by tracing rays from
280
+ face centers through the voxel domain.
281
+
282
+ Args:
283
+ voxcity: VoxCity object
284
+ **kwargs: Additional parameters including:
285
+ - value_name (str): Name for the metadata field (default: 'view_factor_values')
286
+ - colormap (str): Matplotlib colormap name (default: 'BuPu_r')
287
+ - vmin, vmax (float): Colormap limits (default: 0.0, 1.0)
288
+ - N_azimuth (int): Number of azimuthal divisions (default: 120)
289
+ - N_elevation (int): Number of elevation divisions (default: 20)
290
+ - ray_sampling (str): 'grid' or 'fibonacci' (default: 'grid')
291
+ - tree_k (float): Tree extinction coefficient (default: 0.6)
292
+ - tree_lad (float): Leaf area density (default: 1.0)
293
+ - target_values (tuple): Target voxel values (default: (0,))
294
+ - inclusion_mode (bool): Inclusion vs exclusion mode (default: False)
295
+ - building_class_id (int): Building class ID for mesh extraction (default: -3)
296
+ - progress_report (bool): Show progress (default: False)
297
+ - obj_export (bool): Export mesh to OBJ (default: False)
298
+
299
+ Returns:
300
+ Trimesh object with view factor values in metadata
301
+ """
302
+ voxel_data = voxcity.voxels.classes
303
+ meshsize = voxcity.voxels.meta.meshsize
304
+ building_id_grid = voxcity.buildings.ids
305
+ nx, ny, nz = voxel_data.shape
306
+
307
+ value_name = kwargs.get('value_name', 'view_factor_values')
308
+ n_azimuth = kwargs.get('N_azimuth', kwargs.get('n_azimuth', 120))
309
+ n_elevation = kwargs.get('N_elevation', kwargs.get('n_elevation', 20))
310
+ ray_sampling = kwargs.get('ray_sampling', 'grid')
311
+ n_rays = kwargs.get('N_rays', kwargs.get('n_rays', None))
312
+ tree_k = kwargs.get('tree_k', 0.6)
313
+ tree_lad = kwargs.get('tree_lad', 1.0)
314
+ building_class_id = kwargs.get('building_class_id', -3)
315
+ progress_report = kwargs.get('progress_report', False)
316
+
317
+ # Handle mode parameter
318
+ if mode == 'sky':
319
+ target_values = (0,)
320
+ inclusion_mode = False
321
+ elif mode == 'green':
322
+ target_values = (-2, 2, 5, 6, 7, 8)
323
+ inclusion_mode = True
324
+ else:
325
+ target_values = kwargs.get('target_values', (0,))
326
+ inclusion_mode = kwargs.get('inclusion_mode', False)
327
+
328
+ # Try to import mesh creation utility
329
+ try:
330
+ from voxcity.geoprocessor.mesh import create_voxel_mesh
331
+ except ImportError:
332
+ raise ImportError("VoxCity geoprocessor.mesh module required for surface view factor calculation")
333
+
334
+ # Create mesh from building voxels
335
+ try:
336
+ building_mesh = create_voxel_mesh(
337
+ voxel_data,
338
+ building_class_id,
339
+ meshsize,
340
+ building_id_grid=building_id_grid,
341
+ mesh_type='open_air'
342
+ )
343
+ if building_mesh is None or len(building_mesh.faces) == 0:
344
+ print("No surfaces found in voxel data for the specified class.")
345
+ return None
346
+ except Exception as e:
347
+ print(f"Error during mesh extraction: {e}")
348
+ return None
349
+
350
+ if progress_report:
351
+ print(f"Processing view factor for {len(building_mesh.faces)} faces...")
352
+
353
+ face_centers = building_mesh.triangles_center.astype(np.float32)
354
+ face_normals = building_mesh.face_normals.astype(np.float32)
355
+
356
+ # Create domain and calculator
357
+ domain = Domain(
358
+ nx=nx, ny=ny, nz=nz,
359
+ dx=meshsize, dy=meshsize, dz=meshsize
360
+ )
361
+
362
+ calc = SurfaceViewFactorCalculator(
363
+ domain,
364
+ n_azimuth=n_azimuth,
365
+ n_elevation=n_elevation,
366
+ ray_sampling=ray_sampling,
367
+ n_rays=n_rays
368
+ )
369
+
370
+ # Compute surface view factors
371
+ face_vf_values = calc.compute_surface_view_factor(
372
+ face_centers=face_centers,
373
+ face_normals=face_normals,
374
+ voxel_data=voxel_data,
375
+ target_values=target_values,
376
+ inclusion_mode=inclusion_mode,
377
+ tree_k=tree_k,
378
+ tree_lad=tree_lad
379
+ )
380
+
381
+ # Add values to mesh metadata
382
+ if not hasattr(building_mesh, 'metadata'):
383
+ building_mesh.metadata = {}
384
+ building_mesh.metadata[value_name] = face_vf_values
385
+
386
+ # Export if requested
387
+ obj_export = kwargs.get('obj_export', False)
388
+ if obj_export:
389
+ import os
390
+ output_dir = kwargs.get('output_directory', 'output')
391
+ output_file_name = kwargs.get('output_file_name', 'surface_view_factor')
392
+ os.makedirs(output_dir, exist_ok=True)
393
+ try:
394
+ building_mesh.export(f"{output_dir}/{output_file_name}.obj")
395
+ if progress_report:
396
+ print(f"Exported surface mesh to {output_dir}/{output_file_name}.obj")
397
+ except Exception as e:
398
+ print(f"Error exporting mesh: {e}")
399
+
400
+ return building_mesh
401
+
402
+
403
+ def get_landmark_visibility_map(voxcity, building_gdf=None, **kwargs):
404
+ """
405
+ GPU-accelerated Landmark Visibility Map calculation for VoxCity.
406
+
407
+ This function matches the signature of voxcity.simulator.visibility.get_landmark_visibility_map
408
+ using Taichi GPU acceleration.
409
+
410
+ Args:
411
+ voxcity: VoxCity object
412
+ building_gdf: GeoDataFrame of buildings (optional, will use voxcity.extras['building_gdf'])
413
+ **kwargs: Additional parameters including:
414
+ - view_point_height (float): Observer height above ground (default: 1.5)
415
+ - colormap (str): Matplotlib colormap name (default: 'viridis')
416
+ - landmark_building_ids (list): List of building IDs to mark as landmarks
417
+ - landmark_polygon: Polygon to select landmark buildings
418
+ - tree_k (float): Tree extinction coefficient (default: 0.6)
419
+ - tree_lad (float): Leaf area density (default: 1.0)
420
+ - obj_export (bool): Export results to OBJ (default: False)
421
+
422
+ Returns:
423
+ Tuple of (visibility_map, modified_voxel_data)
424
+ """
425
+ landmark_building_ids = kwargs.pop('landmark_building_ids', None)
426
+ view_point_height = kwargs.pop('view_point_height', 1.5)
427
+ tree_k = kwargs.pop('tree_k', 0.6)
428
+ tree_lad = kwargs.pop('tree_lad', 1.0)
429
+ colormap = kwargs.pop('colormap', 'viridis')
430
+
431
+ # Get landmark IDs from various sources if not provided
432
+ if landmark_building_ids is None:
433
+ landmark_polygon = kwargs.get('landmark_polygon', None)
434
+ if landmark_polygon is not None and building_gdf is not None:
435
+ try:
436
+ from voxcity.geoprocessor.selection import get_buildings_in_drawn_polygon
437
+ # Convert landmark_polygon to VoxCity expected format
438
+ # VoxCity expects: [{'vertices': [(x1,y1), (x2,y2), ...]}]
439
+ if hasattr(landmark_polygon, 'exterior'):
440
+ # Single shapely Polygon - convert to VoxCity format
441
+ polygons = [{'vertices': list(landmark_polygon.exterior.coords)}]
442
+ elif isinstance(landmark_polygon, list) and len(landmark_polygon) > 0:
443
+ if isinstance(landmark_polygon[0], dict) and 'vertices' in landmark_polygon[0]:
444
+ # Already in VoxCity format
445
+ polygons = landmark_polygon
446
+ elif hasattr(landmark_polygon[0], 'exterior'):
447
+ # List of shapely Polygons
448
+ polygons = [{'vertices': list(p.exterior.coords)} for p in landmark_polygon]
449
+ else:
450
+ # Assume list of coordinate tuples - wrap as single polygon
451
+ polygons = [{'vertices': landmark_polygon}]
452
+ else:
453
+ polygons = landmark_polygon
454
+ # Use 'intersect' to find buildings that touch/overlap the polygon
455
+ landmark_building_ids = get_buildings_in_drawn_polygon(building_gdf, polygons, operation='intersect')
456
+ except ImportError:
457
+ pass
458
+
459
+ if landmark_building_ids is None:
460
+ rectangle_vertices = kwargs.get('rectangle_vertices', None)
461
+ if rectangle_vertices is None:
462
+ rectangle_vertices = voxcity.extras.get('rectangle_vertices', None)
463
+ if rectangle_vertices is not None and building_gdf is not None:
464
+ try:
465
+ from voxcity.geoprocessor.selection import find_building_containing_point
466
+ lons = [coord[0] for coord in rectangle_vertices]
467
+ lats = [coord[1] for coord in rectangle_vertices]
468
+ center_lon = (min(lons) + max(lons)) / 2
469
+ center_lat = (min(lats) + max(lats)) / 2
470
+ target_point = (center_lon, center_lat)
471
+ landmark_building_ids = find_building_containing_point(building_gdf, target_point)
472
+ except ImportError:
473
+ pass
474
+
475
+ if landmark_building_ids is None:
476
+ print("Cannot set landmark buildings. You need to input either of rectangle_vertices or landmark_building_ids.")
477
+ return None
478
+
479
+ return get_landmark_visibility_map_gpu(
480
+ voxcity,
481
+ building_gdf=building_gdf,
482
+ landmark_building_ids=landmark_building_ids,
483
+ view_point_height=view_point_height,
484
+ tree_k=tree_k,
485
+ tree_lad=tree_lad,
486
+ show_plot=True, # VoxCity default shows plot
487
+ colormap=colormap,
488
+ **kwargs
489
+ )
490
+
491
+
492
+ def get_surface_landmark_visibility(voxcity, building_gdf=None, **kwargs):
493
+ """
494
+ GPU-accelerated Surface Landmark Visibility calculation for VoxCity.
495
+
496
+ This function matches the signature of voxcity.simulator.visibility.get_surface_landmark_visibility
497
+ using Taichi GPU acceleration.
498
+
499
+ Computes landmark visibility for building surface faces.
500
+
501
+ Args:
502
+ voxcity: VoxCity object
503
+ building_gdf: GeoDataFrame of buildings
504
+ **kwargs: Additional parameters including:
505
+ - landmark_building_ids (list): List of building IDs to mark as landmarks
506
+ - landmark_polygon: Polygon to select landmark buildings
507
+ - tree_k (float): Tree extinction coefficient (default: 0.6)
508
+ - tree_lad (float): Leaf area density (default: 1.0)
509
+ - building_class_id (int): Building class ID (default: -3)
510
+ - progress_report (bool): Show progress (default: False)
511
+ - colormap (str): Matplotlib colormap name (default: 'RdYlGn')
512
+ - obj_export (bool): Export mesh to OBJ (default: False)
513
+
514
+ Returns:
515
+ Tuple of (building_mesh with visibility, modified_voxel_data)
516
+ """
517
+ if building_gdf is None:
518
+ building_gdf = voxcity.extras.get('building_gdf', None)
519
+ if building_gdf is None:
520
+ raise ValueError("building_gdf not provided and not found in voxcity.extras['building_gdf']")
521
+
522
+ voxel_data = voxcity.voxels.classes
523
+ building_id_grid = voxcity.buildings.ids
524
+ meshsize = voxcity.voxels.meta.meshsize
525
+ nx, ny, nz = voxel_data.shape
526
+
527
+ progress_report = kwargs.get('progress_report', False)
528
+ landmark_building_ids = kwargs.get('landmark_building_ids', None)
529
+ landmark_polygon = kwargs.get('landmark_polygon', None)
530
+ tree_k = kwargs.get('tree_k', 0.6)
531
+ tree_lad = kwargs.get('tree_lad', 1.0)
532
+ building_class_id = kwargs.get('building_class_id', -3)
533
+ colormap = kwargs.get('colormap', 'RdYlGn')
534
+ landmark_value = -30
535
+
536
+ # Get landmark IDs
537
+ if landmark_building_ids is None:
538
+ if landmark_polygon is not None:
539
+ try:
540
+ from voxcity.geoprocessor.selection import get_buildings_in_drawn_polygon
541
+ # Convert landmark_polygon to VoxCity expected format
542
+ # VoxCity expects: [{'vertices': [(x1,y1), (x2,y2), ...]}]
543
+ if hasattr(landmark_polygon, 'exterior'):
544
+ # Single shapely Polygon - convert to VoxCity format
545
+ polygons = [{'vertices': list(landmark_polygon.exterior.coords)}]
546
+ elif isinstance(landmark_polygon, list) and len(landmark_polygon) > 0:
547
+ if isinstance(landmark_polygon[0], dict) and 'vertices' in landmark_polygon[0]:
548
+ # Already in VoxCity format
549
+ polygons = landmark_polygon
550
+ elif hasattr(landmark_polygon[0], 'exterior'):
551
+ # List of shapely Polygons
552
+ polygons = [{'vertices': list(p.exterior.coords)} for p in landmark_polygon]
553
+ else:
554
+ # Assume list of coordinate tuples - wrap as single polygon
555
+ polygons = [{'vertices': landmark_polygon}]
556
+ else:
557
+ polygons = landmark_polygon
558
+ # Use 'intersect' to find buildings that touch/overlap the polygon
559
+ landmark_building_ids = get_buildings_in_drawn_polygon(building_gdf, polygons, operation='intersect')
560
+ except ImportError:
561
+ pass
562
+ else:
563
+ rectangle_vertices = kwargs.get('rectangle_vertices', None)
564
+ if rectangle_vertices is None:
565
+ rectangle_vertices = voxcity.extras.get('rectangle_vertices', None)
566
+ if rectangle_vertices is None:
567
+ print("Cannot set landmark buildings. You need to input either rectangle_vertices or landmark_building_ids.")
568
+ return None, None
569
+ try:
570
+ from voxcity.geoprocessor.selection import find_building_containing_point
571
+ lons = [coord[0] for coord in rectangle_vertices]
572
+ lats = [coord[1] for coord in rectangle_vertices]
573
+ center_lon = (min(lons) + max(lons)) / 2
574
+ center_lat = (min(lats) + max(lats)) / 2
575
+ target_point = (center_lon, center_lat)
576
+ landmark_building_ids = find_building_containing_point(building_gdf, target_point)
577
+ except ImportError:
578
+ pass
579
+
580
+ if landmark_building_ids is None:
581
+ print("Cannot set landmark buildings. No landmark_building_ids found.")
582
+ return None, None
583
+
584
+ # Prepare voxel data
585
+ voxel_data_for_mesh = voxel_data.copy()
586
+ voxel_data_modified = voxel_data.copy()
587
+ voxel_data_modified = mark_building_by_id(voxel_data_modified, building_id_grid, landmark_building_ids, landmark_value)
588
+ voxel_data_for_mesh = mark_building_by_id(voxel_data_for_mesh, building_id_grid, landmark_building_ids, 0)
589
+
590
+ landmark_positions = np.argwhere(voxel_data_modified == landmark_value).astype(np.float32)
591
+ if landmark_positions.shape[0] == 0:
592
+ print(f"No landmarks found after marking buildings with IDs: {landmark_building_ids}")
593
+ return None, None
594
+
595
+ if progress_report:
596
+ print(f"Found {landmark_positions.shape[0]} landmark voxels")
597
+ print(f"Landmark building IDs: {landmark_building_ids}")
598
+
599
+ # Create mesh
600
+ try:
601
+ from voxcity.geoprocessor.mesh import create_voxel_mesh
602
+ building_mesh = create_voxel_mesh(
603
+ voxel_data_for_mesh,
604
+ building_class_id,
605
+ meshsize,
606
+ building_id_grid=building_id_grid,
607
+ mesh_type='open_air'
608
+ )
609
+ if building_mesh is None or len(building_mesh.faces) == 0:
610
+ print("No non-landmark building surfaces found in voxel data.")
611
+ return None, None
612
+ except ImportError:
613
+ raise ImportError("VoxCity geoprocessor.mesh module required for surface landmark visibility")
614
+ except Exception as e:
615
+ print(f"Error during mesh extraction: {e}")
616
+ return None, None
617
+
618
+ if progress_report:
619
+ print(f"Processing landmark visibility for {len(building_mesh.faces)} faces...")
620
+
621
+ face_centers = building_mesh.triangles_center.astype(np.float32)
622
+ face_normals = building_mesh.face_normals.astype(np.float32)
623
+
624
+ # Create domain and calculator
625
+ domain = Domain(
626
+ nx=nx, ny=ny, nz=nz,
627
+ dx=meshsize, dy=meshsize, dz=meshsize
628
+ )
629
+
630
+ calc = SurfaceLandmarkVisibilityCalculator(domain)
631
+ calc.set_landmarks_from_positions(landmark_positions)
632
+
633
+ # Compute surface landmark visibility
634
+ visibility_values = calc.compute_surface_landmark_visibility(
635
+ face_centers=face_centers,
636
+ face_normals=face_normals,
637
+ voxel_data=voxel_data_modified,
638
+ landmark_value=landmark_value,
639
+ tree_k=tree_k,
640
+ tree_lad=tree_lad
641
+ )
642
+
643
+ # Add to mesh metadata
644
+ building_mesh.metadata = getattr(building_mesh, 'metadata', {})
645
+ building_mesh.metadata['landmark_visibility'] = visibility_values
646
+
647
+ if progress_report:
648
+ valid_mask = ~np.isnan(visibility_values)
649
+ n_valid = np.sum(valid_mask)
650
+ n_visible = np.sum(visibility_values[valid_mask] > 0.5)
651
+ print(f"Landmark visibility statistics:")
652
+ print(f" Total faces: {len(visibility_values)}")
653
+ print(f" Valid faces: {n_valid}")
654
+ print(f" Faces with landmark visibility: {n_visible} ({n_visible/n_valid*100:.1f}%)")
655
+
656
+ # Export if requested
657
+ obj_export = kwargs.get('obj_export', False)
658
+ if obj_export:
659
+ import os
660
+ try:
661
+ import matplotlib.pyplot as plt
662
+ output_dir = kwargs.get('output_directory', 'output')
663
+ output_file_name = kwargs.get('output_file_name', 'surface_landmark_visibility')
664
+ os.makedirs(output_dir, exist_ok=True)
665
+
666
+ cmap = plt.cm.get_cmap(colormap)
667
+ face_colors = np.zeros((len(visibility_values), 4))
668
+ for i, val in enumerate(visibility_values):
669
+ if np.isnan(val):
670
+ face_colors[i] = [0.7, 0.7, 0.7, 1.0]
671
+ else:
672
+ face_colors[i] = cmap(val)
673
+ building_mesh.visual.face_colors = face_colors
674
+ building_mesh.export(f"{output_dir}/{output_file_name}.obj")
675
+ if progress_report:
676
+ print(f"Exported surface mesh to {output_dir}/{output_file_name}.obj")
677
+ except Exception as e:
678
+ print(f"Error exporting mesh: {e}")
679
+
680
+ return building_mesh, voxel_data_modified
681
+
682
+
683
+ def get_sky_view_factor_map_gpu(
684
+ voxcity,
685
+ view_point_height: float = 1.5,
686
+ n_azimuth: int = 120,
687
+ n_elevation: int = 20,
688
+ tree_k: float = 0.6,
689
+ tree_lad: float = 1.0,
690
+ show_plot: bool = False,
691
+ **kwargs
692
+ ) -> np.ndarray:
693
+ """
694
+ GPU-accelerated Sky View Factor calculation for VoxCity.
695
+
696
+ Legacy function - use get_sky_view_factor_map() for VoxCity API compatibility.
697
+
698
+ Args:
699
+ voxcity: VoxCity object
700
+ view_point_height: Observer height above ground (meters)
701
+ n_azimuth: Number of azimuthal divisions
702
+ n_elevation: Number of elevation divisions
703
+ tree_k: Tree extinction coefficient
704
+ tree_lad: Leaf area density
705
+ show_plot: Whether to display a matplotlib plot
706
+ **kwargs: Additional parameters
707
+
708
+ Returns:
709
+ 2D array of SVF values
710
+ """
711
+ return get_view_index_gpu(
712
+ voxcity,
713
+ mode='sky',
714
+ view_point_height=view_point_height,
715
+ n_azimuth=n_azimuth,
716
+ n_elevation=n_elevation,
717
+ elevation_min_degrees=0.0,
718
+ elevation_max_degrees=90.0,
719
+ tree_k=tree_k,
720
+ tree_lad=tree_lad,
721
+ show_plot=show_plot,
722
+ **kwargs
723
+ )
724
+
725
+
726
+ def get_landmark_visibility_map_gpu(
727
+ voxcity,
728
+ building_gdf=None,
729
+ landmark_building_ids: List[int] = None,
730
+ view_point_height: float = 1.5,
731
+ tree_k: float = 0.6,
732
+ tree_lad: float = 1.0,
733
+ show_plot: bool = False,
734
+ **kwargs
735
+ ) -> Tuple[np.ndarray, np.ndarray]:
736
+ """
737
+ GPU-accelerated Landmark Visibility calculation for VoxCity.
738
+
739
+ Legacy function - use get_landmark_visibility_map() for VoxCity API compatibility.
740
+
741
+ Args:
742
+ voxcity: VoxCity object
743
+ building_gdf: GeoDataFrame of buildings (optional)
744
+ landmark_building_ids: List of building IDs to mark as landmarks
745
+ view_point_height: Observer height above ground (meters)
746
+ tree_k: Tree extinction coefficient
747
+ tree_lad: Leaf area density
748
+ show_plot: Whether to display a matplotlib plot
749
+ **kwargs: Additional parameters
750
+
751
+ Returns:
752
+ Tuple of (visibility_map, modified_voxel_data)
753
+ """
754
+ if landmark_building_ids is None:
755
+ raise ValueError("landmark_building_ids must be provided")
756
+
757
+ voxel_data = voxcity.voxels.classes
758
+ building_id_grid = voxcity.buildings.ids
759
+ meshsize = voxcity.voxels.meta.meshsize
760
+ nx, ny, nz = voxel_data.shape
761
+
762
+ view_height_voxel = int(view_point_height / meshsize)
763
+
764
+ # Mark landmark buildings
765
+ target_value = -30
766
+ voxel_data_modified = mark_building_by_id(
767
+ voxel_data, building_id_grid, landmark_building_ids, target_value
768
+ )
769
+
770
+ # Create domain
771
+ domain = Domain(
772
+ nx=nx, ny=ny, nz=nz,
773
+ dx=meshsize, dy=meshsize, dz=meshsize
774
+ )
775
+
776
+ # Create calculator
777
+ calc = LandmarkVisibilityCalculator(domain)
778
+ calc.set_landmarks_from_voxel_value(voxel_data_modified, target_value)
779
+
780
+ # Compute visibility
781
+ visibility_map = calc.compute_visibility_map(
782
+ voxel_data=voxel_data_modified,
783
+ view_height_voxel=view_height_voxel,
784
+ tree_k=tree_k,
785
+ tree_lad=tree_lad
786
+ )
787
+
788
+ # Plot if requested
789
+ if show_plot:
790
+ try:
791
+ import matplotlib.pyplot as plt
792
+ import matplotlib.patches as mpatches
793
+ colormap = kwargs.get('colormap', 'viridis')
794
+
795
+ cmap = plt.cm.get_cmap(colormap, 2).copy()
796
+ cmap.set_bad(color='lightgray')
797
+ plt.figure(figsize=(10, 8))
798
+ plt.imshow(visibility_map, origin='lower', cmap=cmap, vmin=0, vmax=1)
799
+ visible_patch = mpatches.Patch(color=cmap(1.0), label='Visible (1)')
800
+ not_visible_patch = mpatches.Patch(color=cmap(0.0), label='Not Visible (0)')
801
+ plt.legend(handles=[visible_patch, not_visible_patch],
802
+ loc='center left', bbox_to_anchor=(1.0, 0.5))
803
+ plt.axis('off')
804
+ plt.show()
805
+ except ImportError:
806
+ pass
807
+
808
+ return visibility_map, voxel_data_modified