voxcity 0.6.15__py3-none-any.whl → 0.7.0__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 (78) hide show
  1. voxcity/__init__.py +14 -8
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/citygml.py +32 -18
  4. voxcity/downloader/gba.py +210 -0
  5. voxcity/downloader/gee.py +5 -1
  6. voxcity/downloader/mbfp.py +1 -1
  7. voxcity/downloader/oemj.py +80 -8
  8. voxcity/downloader/osm.py +23 -7
  9. voxcity/downloader/overture.py +26 -1
  10. voxcity/downloader/utils.py +73 -73
  11. voxcity/errors.py +30 -0
  12. voxcity/exporter/__init__.py +13 -4
  13. voxcity/exporter/cityles.py +633 -535
  14. voxcity/exporter/envimet.py +728 -708
  15. voxcity/exporter/magicavoxel.py +334 -297
  16. voxcity/exporter/netcdf.py +238 -0
  17. voxcity/exporter/obj.py +1481 -655
  18. voxcity/generator/__init__.py +44 -0
  19. voxcity/generator/api.py +675 -0
  20. voxcity/generator/grids.py +379 -0
  21. voxcity/generator/io.py +94 -0
  22. voxcity/generator/pipeline.py +282 -0
  23. voxcity/generator/voxelizer.py +380 -0
  24. voxcity/geoprocessor/__init__.py +75 -6
  25. voxcity/geoprocessor/conversion.py +153 -0
  26. voxcity/geoprocessor/draw.py +62 -12
  27. voxcity/geoprocessor/heights.py +199 -0
  28. voxcity/geoprocessor/io.py +101 -0
  29. voxcity/geoprocessor/merge_utils.py +91 -0
  30. voxcity/geoprocessor/mesh.py +806 -790
  31. voxcity/geoprocessor/network.py +708 -679
  32. voxcity/geoprocessor/overlap.py +84 -0
  33. voxcity/geoprocessor/raster/__init__.py +82 -0
  34. voxcity/geoprocessor/raster/buildings.py +428 -0
  35. voxcity/geoprocessor/raster/canopy.py +258 -0
  36. voxcity/geoprocessor/raster/core.py +150 -0
  37. voxcity/geoprocessor/raster/export.py +93 -0
  38. voxcity/geoprocessor/raster/landcover.py +156 -0
  39. voxcity/geoprocessor/raster/raster.py +110 -0
  40. voxcity/geoprocessor/selection.py +85 -0
  41. voxcity/geoprocessor/utils.py +18 -14
  42. voxcity/models.py +113 -0
  43. voxcity/simulator/common/__init__.py +22 -0
  44. voxcity/simulator/common/geometry.py +98 -0
  45. voxcity/simulator/common/raytracing.py +450 -0
  46. voxcity/simulator/solar/__init__.py +43 -0
  47. voxcity/simulator/solar/integration.py +336 -0
  48. voxcity/simulator/solar/kernels.py +62 -0
  49. voxcity/simulator/solar/radiation.py +648 -0
  50. voxcity/simulator/solar/temporal.py +434 -0
  51. voxcity/simulator/view.py +36 -2286
  52. voxcity/simulator/visibility/__init__.py +29 -0
  53. voxcity/simulator/visibility/landmark.py +392 -0
  54. voxcity/simulator/visibility/view.py +508 -0
  55. voxcity/utils/logging.py +61 -0
  56. voxcity/utils/orientation.py +51 -0
  57. voxcity/utils/weather/__init__.py +26 -0
  58. voxcity/utils/weather/epw.py +146 -0
  59. voxcity/utils/weather/files.py +36 -0
  60. voxcity/utils/weather/onebuilding.py +486 -0
  61. voxcity/visualizer/__init__.py +24 -0
  62. voxcity/visualizer/builder.py +43 -0
  63. voxcity/visualizer/grids.py +141 -0
  64. voxcity/visualizer/maps.py +187 -0
  65. voxcity/visualizer/palette.py +228 -0
  66. voxcity/visualizer/renderer.py +928 -0
  67. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/METADATA +113 -36
  68. voxcity-0.7.0.dist-info/RECORD +77 -0
  69. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/WHEEL +1 -1
  70. voxcity/generator.py +0 -1137
  71. voxcity/geoprocessor/grid.py +0 -1568
  72. voxcity/geoprocessor/polygon.py +0 -1344
  73. voxcity/simulator/solar.py +0 -2329
  74. voxcity/utils/visualization.py +0 -2660
  75. voxcity/utils/weather.py +0 -817
  76. voxcity-0.6.15.dist-info/RECORD +0 -37
  77. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/AUTHORS.rst +0 -0
  78. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,508 @@
1
+ import numpy as np
2
+
3
+ from ..common.geometry import (
4
+ _generate_ray_directions_grid,
5
+ _generate_ray_directions_fibonacci,
6
+ )
7
+ from ..common.raytracing import (
8
+ compute_vi_map_generic,
9
+ _prepare_masks_for_vi,
10
+ _compute_vi_map_generic_fast,
11
+ )
12
+
13
+ from ...exporter.obj import grid_to_obj
14
+ import matplotlib.pyplot as plt
15
+
16
+
17
+ def get_view_index(voxcity, mode=None, hit_values=None, inclusion_mode=True, fast_path=True, **kwargs):
18
+ voxel_data = voxcity.voxels.classes
19
+ meshsize = voxcity.voxels.meta.meshsize
20
+
21
+ if mode == 'green':
22
+ hit_values = (-2, 2, 5, 6, 7, 8)
23
+ inclusion_mode = True
24
+ elif mode == 'sky':
25
+ hit_values = (0,)
26
+ inclusion_mode = False
27
+ else:
28
+ if hit_values is None:
29
+ raise ValueError("For custom mode, you must provide hit_values.")
30
+
31
+ view_point_height = kwargs.get("view_point_height", 1.5)
32
+ view_height_voxel = int(view_point_height / meshsize)
33
+ colormap = kwargs.get("colormap", 'viridis')
34
+ vmin = kwargs.get("vmin", 0.0)
35
+ vmax = kwargs.get("vmax", 1.0)
36
+
37
+ N_azimuth = kwargs.get("N_azimuth", 120)
38
+ N_elevation = kwargs.get("N_elevation", 20)
39
+ elevation_min_degrees = kwargs.get("elevation_min_degrees", -30)
40
+ elevation_max_degrees = kwargs.get("elevation_max_degrees", 30)
41
+ ray_sampling = kwargs.get("ray_sampling", "grid")
42
+ N_rays = kwargs.get("N_rays", N_azimuth * N_elevation)
43
+
44
+ tree_k = kwargs.get("tree_k", 0.5)
45
+ tree_lad = kwargs.get("tree_lad", 1.0)
46
+
47
+ if str(ray_sampling).lower() == "fibonacci":
48
+ ray_directions = _generate_ray_directions_fibonacci(int(N_rays), elevation_min_degrees, elevation_max_degrees)
49
+ else:
50
+ ray_directions = _generate_ray_directions_grid(int(N_azimuth), int(N_elevation), elevation_min_degrees, elevation_max_degrees)
51
+
52
+ num_threads = kwargs.get("num_threads", None)
53
+ if num_threads is not None:
54
+ try:
55
+ from numba import set_num_threads
56
+ set_num_threads(int(num_threads))
57
+ except Exception:
58
+ pass
59
+
60
+ if fast_path:
61
+ try:
62
+ is_tree, is_target, is_allowed, is_blocker_inc = _prepare_masks_for_vi(voxel_data, hit_values, inclusion_mode)
63
+ trees_in_targets = bool(inclusion_mode and (-2 in hit_values))
64
+ vi_map = _compute_vi_map_generic_fast(
65
+ voxel_data, ray_directions, view_height_voxel,
66
+ meshsize, tree_k, tree_lad,
67
+ is_tree, is_target if is_target is not None else np.zeros(1, dtype=np.bool_),
68
+ is_allowed if is_allowed is not None else np.zeros(1, dtype=np.bool_),
69
+ is_blocker_inc if is_blocker_inc is not None else np.zeros(1, dtype=np.bool_),
70
+ inclusion_mode, trees_in_targets
71
+ )
72
+ except Exception:
73
+ vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
74
+ else:
75
+ vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
76
+
77
+ cmap = plt.cm.get_cmap(colormap).copy()
78
+ cmap.set_bad(color='lightgray')
79
+ plt.figure(figsize=(10, 8))
80
+ plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
81
+ plt.colorbar(label='View Index')
82
+ plt.axis('off')
83
+ plt.show()
84
+
85
+ obj_export = kwargs.get("obj_export", False)
86
+ if obj_export:
87
+ dem_grid = kwargs.get("dem_grid", voxcity.dem.elevation if voxcity.dem else np.zeros_like(vi_map))
88
+ output_dir = kwargs.get("output_directory", "output")
89
+ output_file_name = kwargs.get("output_file_name", "view_index")
90
+ num_colors = kwargs.get("num_colors", 10)
91
+ alpha = kwargs.get("alpha", 1.0)
92
+ grid_to_obj(
93
+ vi_map,
94
+ dem_grid,
95
+ output_dir,
96
+ output_file_name,
97
+ meshsize,
98
+ view_point_height,
99
+ colormap_name=colormap,
100
+ num_colors=num_colors,
101
+ alpha=alpha,
102
+ vmin=vmin,
103
+ vmax=vmax
104
+ )
105
+ return vi_map
106
+
107
+
108
+ def get_sky_view_factor_map(voxcity, show_plot=False, **kwargs):
109
+ voxel_data = voxcity.voxels.classes
110
+ meshsize = voxcity.voxels.meta.meshsize
111
+ view_point_height = kwargs.get("view_point_height", 1.5)
112
+ view_height_voxel = int(view_point_height / meshsize)
113
+ colormap = kwargs.get("colormap", 'BuPu_r')
114
+ vmin = kwargs.get("vmin", 0.0)
115
+ vmax = kwargs.get("vmax", 1.0)
116
+ N_azimuth = kwargs.get("N_azimuth", 120)
117
+ N_elevation = kwargs.get("N_elevation", 20)
118
+ elevation_min_degrees = kwargs.get("elevation_min_degrees", 0)
119
+ elevation_max_degrees = kwargs.get("elevation_max_degrees", 90)
120
+ ray_sampling = kwargs.get("ray_sampling", "grid")
121
+ N_rays = kwargs.get("N_rays", N_azimuth * N_elevation)
122
+ tree_k = kwargs.get("tree_k", 0.6)
123
+ tree_lad = kwargs.get("tree_lad", 1.0)
124
+ hit_values = (0,)
125
+ inclusion_mode = False
126
+ if str(ray_sampling).lower() == "fibonacci":
127
+ ray_directions = _generate_ray_directions_fibonacci(int(N_rays), elevation_min_degrees, elevation_max_degrees)
128
+ else:
129
+ ray_directions = _generate_ray_directions_grid(int(N_azimuth), int(N_elevation), elevation_min_degrees, elevation_max_degrees)
130
+ vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
131
+ if show_plot:
132
+ cmap = plt.cm.get_cmap(colormap).copy()
133
+ cmap.set_bad(color='lightgray')
134
+ plt.figure(figsize=(10, 8))
135
+ plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
136
+ plt.colorbar(label='Sky View Factor')
137
+ plt.axis('off')
138
+ plt.show()
139
+ obj_export = kwargs.get("obj_export", False)
140
+ if obj_export:
141
+ dem_grid = kwargs.get("dem_grid", voxcity.dem.elevation if voxcity.dem else np.zeros_like(vi_map))
142
+ output_dir = kwargs.get("output_directory", "output")
143
+ output_file_name = kwargs.get("output_file_name", "sky_view_factor")
144
+ num_colors = kwargs.get("num_colors", 10)
145
+ alpha = kwargs.get("alpha", 1.0)
146
+ grid_to_obj(
147
+ vi_map,
148
+ dem_grid,
149
+ output_dir,
150
+ output_file_name,
151
+ meshsize,
152
+ view_point_height,
153
+ colormap_name=colormap,
154
+ num_colors=num_colors,
155
+ alpha=alpha,
156
+ vmin=vmin,
157
+ vmax=vmax
158
+ )
159
+ return vi_map
160
+
161
+
162
+ # Surface view-factor (kept here for API; implementation uses local fast path if available)
163
+ import math
164
+ from ..common.geometry import _build_face_basis, rotate_vector_axis_angle
165
+ from numba import njit, prange
166
+
167
+
168
+ def _prepare_masks_for_view(voxel_data, target_values, inclusion_mode):
169
+ is_tree = (voxel_data == -2)
170
+ target_mask = np.zeros(voxel_data.shape, dtype=np.bool_)
171
+ for tv in target_values:
172
+ target_mask |= (voxel_data == tv)
173
+ if inclusion_mode:
174
+ is_opaque = (voxel_data != 0) & (~is_tree) & (~target_mask)
175
+ is_allowed = target_mask.copy()
176
+ else:
177
+ is_allowed = target_mask
178
+ is_opaque = (~is_tree) & (~is_allowed)
179
+ return is_tree, target_mask, is_allowed, is_opaque
180
+
181
+
182
+ @njit(cache=True, fastmath=True, nogil=True)
183
+ def _ray_visibility_contrib(origin, direction, vox_is_tree, vox_is_target, vox_is_allowed, vox_is_opaque, att, att_cutoff, inclusion_mode, trees_are_targets):
184
+ nx, ny, nz = vox_is_opaque.shape
185
+ x0 = origin[0]; y0 = origin[1]; z0 = origin[2]
186
+ dx = direction[0]; dy = direction[1]; dz = direction[2]
187
+ L = (dx*dx + dy*dy + dz*dz) ** 0.5
188
+ if L == 0.0:
189
+ return 0.0
190
+ invL = 1.0 / L
191
+ dx *= invL; dy *= invL; dz *= invL
192
+ x = x0 + 0.5
193
+ y = y0 + 0.5
194
+ z = z0 + 0.5
195
+ i = int(x0); j = int(y0); k = int(z0)
196
+ step_x = 1 if dx >= 0.0 else -1
197
+ step_y = 1 if dy >= 0.0 else -1
198
+ step_z = 1 if dz >= 0.0 else -1
199
+ BIG = 1e30
200
+ if dx != 0.0:
201
+ t_max_x = (((i + (1 if step_x > 0 else 0)) - x) / dx)
202
+ t_delta_x = abs(1.0 / dx)
203
+ else:
204
+ t_max_x = BIG; t_delta_x = BIG
205
+ if dy != 0.0:
206
+ t_max_y = (((j + (1 if step_y > 0 else 0)) - y) / dy)
207
+ t_delta_y = abs(1.0 / dy)
208
+ else:
209
+ t_max_y = BIG; t_delta_y = BIG
210
+ if dz != 0.0:
211
+ t_max_z = (((k + (1 if step_z > 0 else 0)) - z) / dz)
212
+ t_delta_z = abs(1.0 / dz)
213
+ else:
214
+ t_max_z = BIG; t_delta_z = BIG
215
+ T = 1.0
216
+ while True:
217
+ if (i < 0) or (i >= nx) or (j < 0) or (j >= ny) or (k < 0) or (k >= nz):
218
+ if inclusion_mode:
219
+ return 0.0
220
+ else:
221
+ return T
222
+ if vox_is_opaque[i, j, k]:
223
+ return 0.0
224
+ if vox_is_tree[i, j, k]:
225
+ T *= att
226
+ if T < att_cutoff:
227
+ return 0.0
228
+ if inclusion_mode and trees_are_targets:
229
+ return 1.0 - (T if T < 1.0 else 1.0)
230
+ if inclusion_mode:
231
+ if (not vox_is_tree[i, j, k]) and vox_is_target[i, j, k]:
232
+ return 1.0
233
+ else:
234
+ if (not vox_is_tree[i, j, k]) and (not vox_is_allowed[i, j, k]):
235
+ return 0.0
236
+ if t_max_x < t_max_y:
237
+ if t_max_x < t_max_z:
238
+ t_max_x += t_delta_x; i += step_x
239
+ else:
240
+ t_max_z += t_delta_z; k += step_z
241
+ else:
242
+ if t_max_y < t_max_z:
243
+ t_max_y += t_delta_y; j += step_y
244
+ else:
245
+ t_max_z += t_delta_z; k += step_z
246
+
247
+
248
+ @njit(parallel=True, cache=True, fastmath=True, nogil=True)
249
+ def _compute_view_factor_faces_chunk(face_centers, face_normals, hemisphere_dirs, vox_is_tree, vox_is_target, vox_is_allowed, vox_is_opaque, meshsize, att, att_cutoff, grid_bounds_real, boundary_epsilon, inclusion_mode, trees_are_targets):
250
+ n_faces = face_centers.shape[0]
251
+ out = np.empty(n_faces, dtype=np.float64)
252
+ for f in prange(n_faces):
253
+ center = face_centers[f]
254
+ normal = face_normals[f]
255
+ is_vertical = (abs(normal[2]) < 0.01)
256
+ on_x_min = (abs(center[0] - grid_bounds_real[0,0]) < boundary_epsilon)
257
+ on_y_min = (abs(center[1] - grid_bounds_real[0,1]) < boundary_epsilon)
258
+ on_x_max = (abs(center[0] - grid_bounds_real[1,0]) < boundary_epsilon)
259
+ on_y_max = (abs(center[1] - grid_bounds_real[1,1]) < boundary_epsilon)
260
+ if is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max):
261
+ out[f] = np.nan
262
+ continue
263
+ u, v, n = _build_face_basis(normal)
264
+ ox = center[0] / meshsize + n[0] * 0.51
265
+ oy = center[1] / meshsize + n[1] * 0.51
266
+ oz = center[2] / meshsize + n[2] * 0.51
267
+ origin = np.array((ox, oy, oz))
268
+ vis_sum = 0.0
269
+ valid = 0
270
+ for i in range(hemisphere_dirs.shape[0]):
271
+ lx = hemisphere_dirs[i,0]; ly = hemisphere_dirs[i,1]; lz = hemisphere_dirs[i,2]
272
+ dx = u[0]*lx + v[0]*ly + n[0]*lz
273
+ dy = u[1]*lx + v[1]*ly + n[1]*lz
274
+ dz = u[2]*lx + v[2]*ly + n[2]*lz
275
+ if (dx*n[0] + dy*n[1] + dz*n[2]) <= 0.0:
276
+ continue
277
+ contrib = _ray_visibility_contrib(origin, np.array((dx, dy, dz)), vox_is_tree, vox_is_target, vox_is_allowed, vox_is_opaque, att, att_cutoff, inclusion_mode, trees_are_targets)
278
+ vis_sum += contrib
279
+ valid += 1
280
+ out[f] = 0.0 if valid == 0 else (vis_sum / valid)
281
+ return out
282
+
283
+
284
+ def _compute_view_factor_faces_progress(face_centers, face_normals, hemisphere_dirs, vox_is_tree, vox_is_target, vox_is_allowed, vox_is_opaque, meshsize, att, att_cutoff, grid_bounds_real, boundary_epsilon, inclusion_mode, trees_are_targets, progress_report=False, chunks=10):
285
+ n_faces = face_centers.shape[0]
286
+ results = np.empty(n_faces, dtype=np.float64)
287
+ step = math.ceil(n_faces / chunks) if n_faces > 0 else 1
288
+ for start in range(0, n_faces, step):
289
+ end = min(start + step, n_faces)
290
+ results[start:end] = _compute_view_factor_faces_chunk(
291
+ face_centers[start:end], face_normals[start:end], hemisphere_dirs,
292
+ vox_is_tree, vox_is_target, vox_is_allowed, vox_is_opaque,
293
+ float(meshsize), float(att), float(att_cutoff),
294
+ grid_bounds_real, float(boundary_epsilon),
295
+ inclusion_mode, trees_are_targets
296
+ )
297
+ if progress_report:
298
+ pct = (end / n_faces) * 100 if n_faces > 0 else 100.0
299
+ print(f" Processed {end}/{n_faces} faces ({pct:.1f}%)")
300
+ return results
301
+
302
+
303
+ def compute_view_factor_for_all_faces(face_centers, face_normals, hemisphere_dirs, voxel_data, meshsize, tree_k, tree_lad, target_values, inclusion_mode, grid_bounds_real, boundary_epsilon, offset_vox=0.51):
304
+ n_faces = face_centers.shape[0]
305
+ face_vf_values = np.zeros(n_faces, dtype=np.float64)
306
+ z_axis = np.array([0.0, 0.0, 1.0])
307
+ for fidx in range(n_faces):
308
+ center = face_centers[fidx]
309
+ normal = face_normals[fidx]
310
+ is_vertical = (abs(normal[2]) < 0.01)
311
+ on_x_min = (abs(center[0] - grid_bounds_real[0,0]) < boundary_epsilon)
312
+ on_y_min = (abs(center[1] - grid_bounds_real[0,1]) < boundary_epsilon)
313
+ on_x_max = (abs(center[0] - grid_bounds_real[1,0]) < boundary_epsilon)
314
+ on_y_max = (abs(center[1] - grid_bounds_real[1,1]) < boundary_epsilon)
315
+ is_boundary_vertical = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
316
+ if is_boundary_vertical:
317
+ face_vf_values[fidx] = np.nan
318
+ continue
319
+ norm_n = np.sqrt(normal[0]**2 + normal[1]**2 + normal[2]**2)
320
+ if norm_n < 1e-12:
321
+ face_vf_values[fidx] = 0.0
322
+ continue
323
+ dot_zn = z_axis[0]*normal[0] + z_axis[1]*normal[1] + z_axis[2]*normal[2]
324
+ cos_angle = dot_zn / (norm_n)
325
+ if cos_angle > 1.0: cos_angle = 1.0
326
+ if cos_angle < -1.0: cos_angle = -1.0
327
+ angle = np.arccos(cos_angle)
328
+ if abs(cos_angle - 1.0) < 1e-9:
329
+ local_dirs = hemisphere_dirs
330
+ elif abs(cos_angle + 1.0) < 1e-9:
331
+ axis_180 = np.array([1.0, 0.0, 0.0])
332
+ local_dirs = np.empty_like(hemisphere_dirs)
333
+ for i in range(hemisphere_dirs.shape[0]):
334
+ local_dirs[i] = rotate_vector_axis_angle(hemisphere_dirs[i], axis_180, np.pi)
335
+ else:
336
+ axis_x = z_axis[1]*normal[2] - z_axis[2]*normal[1]
337
+ axis_y = z_axis[2]*normal[0] - z_axis[0]*normal[2]
338
+ axis_z = z_axis[0]*normal[1] - z_axis[1]*normal[0]
339
+ rot_axis = np.array([axis_x, axis_y, axis_z], dtype=np.float64)
340
+ local_dirs = np.empty_like(hemisphere_dirs)
341
+ for i in range(hemisphere_dirs.shape[0]):
342
+ local_dirs[i] = rotate_vector_axis_angle(hemisphere_dirs[i], rot_axis, angle)
343
+ total_outward = 0
344
+ num_valid = 0
345
+ for i in range(local_dirs.shape[0]):
346
+ dvec = local_dirs[i]
347
+ dp = dvec[0]*normal[0] + dvec[1]*normal[1] + dvec[2]*normal[2]
348
+ if dp > 0.0:
349
+ total_outward += 1
350
+ num_valid += 1
351
+ if total_outward == 0:
352
+ face_vf_values[fidx] = 0.0
353
+ continue
354
+ if num_valid == 0:
355
+ face_vf_values[fidx] = 0.0
356
+ continue
357
+ valid_dirs_arr = np.empty((num_valid, 3), dtype=np.float64)
358
+ out_idx = 0
359
+ for i in range(local_dirs.shape[0]):
360
+ dvec = local_dirs[i]
361
+ dp = dvec[0]*normal[0] + dvec[1]*normal[1] + dvec[2]*normal[2]
362
+ if dp > 0.0:
363
+ valid_dirs_arr[out_idx, 0] = dvec[0]
364
+ valid_dirs_arr[out_idx, 1] = dvec[1]
365
+ valid_dirs_arr[out_idx, 2] = dvec[2]
366
+ out_idx += 1
367
+ ray_origin = (center / meshsize) + (normal / norm_n) * offset_vox
368
+ from ..common.raytracing import compute_vi_generic # local import for numba friendliness
369
+ vf = compute_vi_generic(
370
+ ray_origin,
371
+ voxel_data,
372
+ valid_dirs_arr,
373
+ target_values,
374
+ meshsize,
375
+ tree_k,
376
+ tree_lad,
377
+ inclusion_mode
378
+ )
379
+ fraction_valid = num_valid / total_outward
380
+ face_vf_values[fidx] = vf * fraction_valid
381
+ return face_vf_values
382
+
383
+
384
+ def get_surface_view_factor(voxcity, **kwargs):
385
+ import matplotlib.cm as cm
386
+ import matplotlib.colors as mcolors
387
+ import os
388
+ from ...geoprocessor.mesh import create_voxel_mesh
389
+ voxel_data = voxcity.voxels.classes
390
+ meshsize = voxcity.voxels.meta.meshsize
391
+ building_id_grid = voxcity.buildings.ids
392
+ value_name = kwargs.get("value_name", 'view_factor_values')
393
+ colormap = kwargs.get("colormap", 'BuPu_r')
394
+ vmin = kwargs.get("vmin", 0.0)
395
+ vmax = kwargs.get("vmax", 1.0)
396
+ N_azimuth = kwargs.get("N_azimuth", 120)
397
+ N_elevation = kwargs.get("N_elevation", 20)
398
+ ray_sampling = kwargs.get("ray_sampling", "grid")
399
+ N_rays = kwargs.get("N_rays", N_azimuth * N_elevation)
400
+ debug = kwargs.get("debug", False)
401
+ progress_report = kwargs.get("progress_report", False)
402
+ tree_k = kwargs.get("tree_k", 0.6)
403
+ tree_lad = kwargs.get("tree_lad", 1.0)
404
+ target_values = kwargs.get("target_values", (0,))
405
+ inclusion_mode = kwargs.get("inclusion_mode", False)
406
+ building_class_id = kwargs.get("building_class_id", -3)
407
+ try:
408
+ building_mesh = create_voxel_mesh(
409
+ voxel_data,
410
+ building_class_id,
411
+ meshsize,
412
+ building_id_grid=building_id_grid,
413
+ mesh_type='open_air'
414
+ )
415
+ if building_mesh is None or len(building_mesh.faces) == 0:
416
+ print("No surfaces found in voxel data for the specified class.")
417
+ return None
418
+ except Exception as e:
419
+ print(f"Error during mesh extraction: {e}")
420
+ return None
421
+ if progress_report:
422
+ print(f"Processing view factor for {len(building_mesh.faces)} faces...")
423
+ face_centers = building_mesh.triangles_center
424
+ face_normals = building_mesh.face_normals
425
+ if str(ray_sampling).lower() == "fibonacci":
426
+ hemisphere_dirs = _generate_ray_directions_fibonacci(int(N_rays), 0.0, 90.0)
427
+ else:
428
+ hemisphere_dirs = _generate_ray_directions_grid(int(N_azimuth), int(N_elevation), 0.0, 90.0)
429
+ nx, ny, nz = voxel_data.shape
430
+ grid_bounds_voxel = np.array([[0,0,0],[nx, ny, nz]], dtype=np.float64)
431
+ grid_bounds_real = grid_bounds_voxel * meshsize
432
+ boundary_epsilon = meshsize * 0.05
433
+ fast_path = kwargs.get("fast_path", True)
434
+ face_vf_values = None
435
+ if fast_path:
436
+ try:
437
+ vox_is_tree, vox_is_target, vox_is_allowed, vox_is_opaque = _prepare_masks_for_view(voxel_data, target_values, inclusion_mode)
438
+ att = float(np.exp(-tree_k * tree_lad * meshsize))
439
+ att_cutoff = 0.01
440
+ trees_are_targets = bool((-2 in target_values) and inclusion_mode)
441
+ face_vf_values = _compute_view_factor_faces_progress(
442
+ face_centers.astype(np.float64),
443
+ face_normals.astype(np.float64),
444
+ hemisphere_dirs.astype(np.float64),
445
+ vox_is_tree, vox_is_target, vox_is_allowed, vox_is_opaque,
446
+ float(meshsize), float(att), float(att_cutoff),
447
+ grid_bounds_real.astype(np.float64), float(boundary_epsilon),
448
+ inclusion_mode, trees_are_targets,
449
+ progress_report=progress_report
450
+ )
451
+ except Exception as e:
452
+ if debug:
453
+ print(f"Fast view-factor path failed: {e}. Falling back to standard path.")
454
+ face_vf_values = None
455
+ if face_vf_values is None:
456
+ face_vf_values = compute_view_factor_for_all_faces(
457
+ face_centers,
458
+ face_normals,
459
+ hemisphere_dirs,
460
+ voxel_data,
461
+ meshsize,
462
+ tree_k,
463
+ tree_lad,
464
+ target_values,
465
+ inclusion_mode,
466
+ grid_bounds_real,
467
+ boundary_epsilon
468
+ )
469
+ if not hasattr(building_mesh, 'metadata'):
470
+ building_mesh.metadata = {}
471
+ building_mesh.metadata[value_name] = face_vf_values
472
+ obj_export = kwargs.get("obj_export", False)
473
+ if obj_export:
474
+ output_dir = kwargs.get("output_directory", "output")
475
+ output_file_name = kwargs.get("output_file_name", "surface_view_factor")
476
+ import os
477
+ os.makedirs(output_dir, exist_ok=True)
478
+ try:
479
+ building_mesh.export(f"{output_dir}/{output_file_name}.obj")
480
+ print(f"Exported surface mesh to {output_dir}/{output_file_name}.obj")
481
+ except Exception as e:
482
+ print(f"Error exporting mesh: {e}")
483
+ return building_mesh
484
+ """Visibility API aggregator.
485
+
486
+ This module re-exports selected public APIs:
487
+ - raytracing: low-level VI computation helpers
488
+ - landmark: landmark visibility utilities
489
+ """
490
+
491
+ from ..common.raytracing import (
492
+ compute_vi_generic,
493
+ compute_vi_map_generic,
494
+ )
495
+
496
+ # get_view_index, get_sky_view_factor_map, get_surface_view_factor, and
497
+ # compute_view_factor_for_all_faces are defined in this module above.
498
+
499
+ __all__ = [
500
+ 'get_view_index',
501
+ 'get_surface_view_factor',
502
+ 'get_sky_view_factor_map',
503
+ 'compute_view_factor_for_all_faces',
504
+ 'compute_vi_generic',
505
+ 'compute_vi_map_generic',
506
+ ]
507
+
508
+
@@ -0,0 +1,61 @@
1
+ """
2
+ Lightweight, centralized logging utilities for the voxcity package.
3
+
4
+ Usage:
5
+ from voxcity.utils.logging import get_logger
6
+ logger = get_logger(__name__)
7
+
8
+ Environment variables:
9
+ VOXCITY_LOG_LEVEL: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ from typing import Optional
17
+
18
+
19
+ _LEVEL_NAMES = {
20
+ "CRITICAL": logging.CRITICAL,
21
+ "ERROR": logging.ERROR,
22
+ "WARNING": logging.WARNING,
23
+ "INFO": logging.INFO,
24
+ "DEBUG": logging.DEBUG,
25
+ }
26
+
27
+
28
+ def _resolve_level(env_value: Optional[str]) -> int:
29
+ if not env_value:
30
+ return logging.INFO
31
+ return _LEVEL_NAMES.get(env_value.strip().upper(), logging.INFO)
32
+
33
+
34
+ def _configure_root_once() -> None:
35
+ root = logging.getLogger("voxcity")
36
+ if root.handlers:
37
+ return
38
+ level = _resolve_level(os.getenv("VOXCITY_LOG_LEVEL"))
39
+ root.setLevel(level)
40
+ handler = logging.StreamHandler()
41
+ handler.setLevel(level)
42
+ formatter = logging.Formatter(
43
+ fmt="%(levelname)s | %(name)s | %(message)s",
44
+ )
45
+ handler.setFormatter(formatter)
46
+ root.addHandler(handler)
47
+ # Prevent duplicate messages from propagating to the global root logger
48
+ root.propagate = False
49
+
50
+
51
+ def get_logger(name: Optional[str] = None) -> logging.Logger:
52
+ """Return a child logger under the package root logger.
53
+
54
+ - Ensures a single configuration for the package
55
+ - Respects VOXCITY_LOG_LEVEL if set
56
+ """
57
+ _configure_root_once()
58
+ pkg_logger = logging.getLogger("voxcity")
59
+ return pkg_logger.getChild(name) if name else pkg_logger
60
+
61
+
@@ -0,0 +1,51 @@
1
+ """Grid orientation helpers.
2
+
3
+ Contract:
4
+ - Canonical internal orientation is "north_up": row 0 is the northern/top row,
5
+ increasing row index moves south/down. Columns increase eastward: column 0 is
6
+ west/left and indices increase toward the east/right. All processing functions
7
+ accept and return 2D grids in this orientation unless explicitly documented
8
+ otherwise.
9
+ - Visualization utilities may flip vertically for display purposes only.
10
+ - 3D indexing follows (row, col, z) = (north→south, west→east, ground→up).
11
+
12
+ Utilities here are intentionally minimal to avoid introducing hidden behavior.
13
+ They can be used at I/O boundaries (e.g., when reading rasters with south_up
14
+ conventions) to normalize to the internal orientation.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Literal
20
+ import numpy as np
21
+
22
+ # Public constants to reference orientation in docs and code
23
+ ORIENTATION_NORTH_UP: Literal["north_up"] = "north_up"
24
+ ORIENTATION_SOUTH_UP: Literal["south_up"] = "south_up"
25
+
26
+
27
+ def ensure_orientation(
28
+ grid: np.ndarray,
29
+ orientation_in: Literal["north_up", "south_up"],
30
+ orientation_out: Literal["north_up", "south_up"] = ORIENTATION_NORTH_UP,
31
+ ) -> np.ndarray:
32
+ """Return ``grid`` converted from ``orientation_in`` to ``orientation_out``.
33
+
34
+ Both orientations are defined for 2D arrays as:
35
+ - north_up: row 0 = north/top, last row = south/bottom
36
+ - south_up: row 0 = south/bottom, last row = north/top
37
+
38
+ If orientations match, the input array is returned unchanged. When converting
39
+ between north_up and south_up, a vertical flip is applied using ``np.flipud``.
40
+
41
+ Notes
42
+ -----
43
+ - This function does not copy when no conversion is needed.
44
+ - Use at data boundaries (read/write, interop) rather than deep in processing code.
45
+ """
46
+ if orientation_in == orientation_out:
47
+ return grid
48
+ # Only two orientations supported; converting between them is a vertical flip
49
+ return np.flipud(grid)
50
+
51
+
@@ -0,0 +1,26 @@
1
+ """
2
+ Weather utilities subpackage.
3
+
4
+ Public API:
5
+ - safe_rename, safe_extract
6
+ - process_epw, read_epw_for_solar_simulation
7
+ - get_nearest_epw_from_climate_onebuilding
8
+
9
+ This package was introduced to split a previously monolithic module into
10
+ cohesive submodules. Backwards-compatible imports are preserved: importing
11
+ from `voxcity.utils.weather` continues to work.
12
+ """
13
+
14
+ from .files import safe_rename, safe_extract
15
+ from .epw import process_epw, read_epw_for_solar_simulation
16
+ from .onebuilding import get_nearest_epw_from_climate_onebuilding
17
+
18
+ __all__ = [
19
+ "safe_rename",
20
+ "safe_extract",
21
+ "process_epw",
22
+ "read_epw_for_solar_simulation",
23
+ "get_nearest_epw_from_climate_onebuilding",
24
+ ]
25
+
26
+