voxcity 0.6.26__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.
- voxcity/__init__.py +14 -8
- 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 +13 -5
- voxcity/exporter/cityles.py +633 -538
- voxcity/exporter/envimet.py +728 -708
- voxcity/exporter/magicavoxel.py +334 -297
- voxcity/exporter/netcdf.py +238 -211
- voxcity/exporter/obj.py +1481 -1406
- voxcity/generator/__init__.py +44 -0
- voxcity/generator/api.py +675 -0
- voxcity/generator/grids.py +379 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/voxelizer.py +380 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +62 -12
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +806 -790
- voxcity/geoprocessor/network.py +708 -679
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +428 -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 +156 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +18 -14
- 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 +43 -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/temporal.py +434 -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/logging.py +61 -0
- voxcity/utils/orientation.py +51 -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 +928 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/METADATA +107 -34
- voxcity-0.7.0.dist-info/RECORD +77 -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-0.7.0.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.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
|
+
|
voxcity/utils/logging.py
ADDED
|
@@ -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
|
+
|