voxcity 0.7.0__py3-none-any.whl → 1.0.13__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 -14
- voxcity/downloader/ocean.py +559 -0
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +727 -675
- voxcity/generator/grids.py +394 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +582 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1494 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +178 -51
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +115 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/simulator_gpu/__init__.py +115 -0
- voxcity/simulator_gpu/common/__init__.py +9 -0
- voxcity/simulator_gpu/common/geometry.py +11 -0
- voxcity/simulator_gpu/core.py +322 -0
- voxcity/simulator_gpu/domain.py +262 -0
- voxcity/simulator_gpu/environment.yml +11 -0
- voxcity/simulator_gpu/init_taichi.py +154 -0
- voxcity/simulator_gpu/integration.py +15 -0
- voxcity/simulator_gpu/kernels.py +56 -0
- voxcity/simulator_gpu/radiation.py +28 -0
- voxcity/simulator_gpu/raytracing.py +623 -0
- voxcity/simulator_gpu/sky.py +9 -0
- voxcity/simulator_gpu/solar/__init__.py +178 -0
- voxcity/simulator_gpu/solar/core.py +66 -0
- voxcity/simulator_gpu/solar/csf.py +1249 -0
- voxcity/simulator_gpu/solar/domain.py +561 -0
- voxcity/simulator_gpu/solar/epw.py +421 -0
- voxcity/simulator_gpu/solar/integration.py +2953 -0
- voxcity/simulator_gpu/solar/radiation.py +3019 -0
- voxcity/simulator_gpu/solar/raytracing.py +686 -0
- voxcity/simulator_gpu/solar/reflection.py +533 -0
- voxcity/simulator_gpu/solar/sky.py +907 -0
- voxcity/simulator_gpu/solar/solar.py +337 -0
- voxcity/simulator_gpu/solar/svf.py +446 -0
- voxcity/simulator_gpu/solar/volumetric.py +1151 -0
- voxcity/simulator_gpu/solar/voxcity.py +2953 -0
- voxcity/simulator_gpu/temporal.py +13 -0
- voxcity/simulator_gpu/utils.py +25 -0
- voxcity/simulator_gpu/view.py +32 -0
- voxcity/simulator_gpu/visibility/__init__.py +109 -0
- voxcity/simulator_gpu/visibility/geometry.py +278 -0
- voxcity/simulator_gpu/visibility/integration.py +808 -0
- voxcity/simulator_gpu/visibility/landmark.py +753 -0
- voxcity/simulator_gpu/visibility/view.py +944 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1146 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
- voxcity-1.0.13.dist-info/RECORD +116 -0
- voxcity-0.7.0.dist-info/RECORD +0 -77
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Landmark visibility calculation using Taichi GPU acceleration.
|
|
3
|
+
|
|
4
|
+
This module emulates the functionality of voxcity.simulator.visibility.landmark
|
|
5
|
+
with GPU-accelerated ray tracing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import taichi as ti
|
|
9
|
+
import numpy as np
|
|
10
|
+
import math
|
|
11
|
+
from typing import Tuple, Optional, List
|
|
12
|
+
|
|
13
|
+
from ..core import Vector3, Point3
|
|
14
|
+
from ..init_taichi import ensure_initialized
|
|
15
|
+
from ..raytracing import ray_trace_to_target
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@ti.data_oriented
|
|
19
|
+
class LandmarkVisibilityCalculator:
|
|
20
|
+
"""
|
|
21
|
+
GPU-accelerated Landmark Visibility calculator.
|
|
22
|
+
|
|
23
|
+
Computes visibility of landmark buildings from observation points
|
|
24
|
+
throughout the domain.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, domain):
|
|
28
|
+
"""
|
|
29
|
+
Initialize Landmark Visibility Calculator.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
domain: Domain object with grid geometry
|
|
33
|
+
"""
|
|
34
|
+
# Ensure Taichi is initialized before creating any fields
|
|
35
|
+
ensure_initialized()
|
|
36
|
+
|
|
37
|
+
self.domain = domain
|
|
38
|
+
self.nx = domain.nx
|
|
39
|
+
self.ny = domain.ny
|
|
40
|
+
self.nz = domain.nz
|
|
41
|
+
self.dx = domain.dx
|
|
42
|
+
self.dy = domain.dy
|
|
43
|
+
self.dz = domain.dz
|
|
44
|
+
|
|
45
|
+
# Landmark positions (will be set later)
|
|
46
|
+
self._landmark_positions = None
|
|
47
|
+
self._n_landmarks = 0
|
|
48
|
+
|
|
49
|
+
def set_landmarks_from_positions(self, positions: np.ndarray):
|
|
50
|
+
"""
|
|
51
|
+
Set landmark positions directly.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
positions: Array of shape (n_landmarks, 3) with (x, y, z) coordinates
|
|
55
|
+
"""
|
|
56
|
+
self._n_landmarks = positions.shape[0]
|
|
57
|
+
self._landmark_positions = ti.Vector.field(3, dtype=ti.f32, shape=(self._n_landmarks,))
|
|
58
|
+
self._landmark_positions.from_numpy(positions.astype(np.float32))
|
|
59
|
+
|
|
60
|
+
def set_landmarks_from_voxel_value(self, voxel_data: np.ndarray, landmark_value: int = -30):
|
|
61
|
+
"""
|
|
62
|
+
Set landmark positions from voxel data based on a marker value.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
voxel_data: 3D voxel class array
|
|
66
|
+
landmark_value: Voxel value marking landmarks
|
|
67
|
+
"""
|
|
68
|
+
positions = np.argwhere(voxel_data == landmark_value).astype(np.float32)
|
|
69
|
+
if positions.shape[0] == 0:
|
|
70
|
+
raise ValueError(f"No landmark with value {landmark_value} found in voxel data.")
|
|
71
|
+
self.set_landmarks_from_positions(positions)
|
|
72
|
+
|
|
73
|
+
def compute_visibility_map(
|
|
74
|
+
self,
|
|
75
|
+
voxel_data: np.ndarray = None,
|
|
76
|
+
view_height_voxel: int = 0,
|
|
77
|
+
tree_k: float = 0.6,
|
|
78
|
+
tree_lad: float = 1.0
|
|
79
|
+
) -> np.ndarray:
|
|
80
|
+
"""
|
|
81
|
+
Compute landmark visibility map.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
voxel_data: 3D voxel class array (optional if domain has masks)
|
|
85
|
+
view_height_voxel: Observer height in voxels above ground
|
|
86
|
+
tree_k: Tree extinction coefficient
|
|
87
|
+
tree_lad: Leaf area density
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
2D array with 1 where landmark is visible, 0 otherwise, nan for invalid
|
|
91
|
+
"""
|
|
92
|
+
if self._landmark_positions is None or self._n_landmarks == 0:
|
|
93
|
+
raise ValueError("No landmarks set. Call set_landmarks_* first.")
|
|
94
|
+
|
|
95
|
+
# Prepare output
|
|
96
|
+
visibility_map = ti.field(dtype=ti.f32, shape=(self.nx, self.ny))
|
|
97
|
+
|
|
98
|
+
# Prepare masks
|
|
99
|
+
if voxel_data is not None:
|
|
100
|
+
is_tree = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
101
|
+
is_solid = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
102
|
+
is_walkable = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
103
|
+
self._setup_masks_from_voxel(voxel_data, is_tree, is_solid, is_walkable)
|
|
104
|
+
else:
|
|
105
|
+
is_tree = self.domain.is_tree
|
|
106
|
+
is_solid = self.domain.is_solid
|
|
107
|
+
# Create walkable mask - assume all non-solid, non-tree surfaces are walkable
|
|
108
|
+
is_walkable = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
109
|
+
self._init_walkable_from_domain(is_tree, is_solid, is_walkable)
|
|
110
|
+
|
|
111
|
+
# Tree attenuation
|
|
112
|
+
tree_att = float(math.exp(-tree_k * tree_lad * self.dz))
|
|
113
|
+
att_cutoff = 0.01
|
|
114
|
+
|
|
115
|
+
# Run GPU computation
|
|
116
|
+
self._compute_visibility_map_kernel(
|
|
117
|
+
visibility_map, view_height_voxel,
|
|
118
|
+
is_tree, is_solid, is_walkable, tree_att, att_cutoff
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Return flipped result
|
|
122
|
+
result = visibility_map.to_numpy()
|
|
123
|
+
return np.flipud(result)
|
|
124
|
+
|
|
125
|
+
def _setup_masks_from_voxel(
|
|
126
|
+
self,
|
|
127
|
+
voxel_data: np.ndarray,
|
|
128
|
+
is_tree: ti.template(),
|
|
129
|
+
is_solid: ti.template(),
|
|
130
|
+
is_walkable: ti.template()
|
|
131
|
+
):
|
|
132
|
+
"""Setup tree, solid, and walkable masks from voxel data."""
|
|
133
|
+
self._setup_masks_kernel(voxel_data, is_tree, is_solid, is_walkable)
|
|
134
|
+
|
|
135
|
+
@ti.kernel
|
|
136
|
+
def _init_walkable_from_domain(
|
|
137
|
+
self,
|
|
138
|
+
is_tree: ti.template(),
|
|
139
|
+
is_solid: ti.template(),
|
|
140
|
+
is_walkable: ti.template()
|
|
141
|
+
):
|
|
142
|
+
"""Initialize walkable mask from domain masks (assume all walkable)."""
|
|
143
|
+
for i, j, k in is_walkable:
|
|
144
|
+
# Without voxel_data, assume surfaces are walkable if not tree/solid
|
|
145
|
+
is_walkable[i, j, k] = 1
|
|
146
|
+
|
|
147
|
+
@ti.kernel
|
|
148
|
+
def _setup_masks_kernel(
|
|
149
|
+
self,
|
|
150
|
+
voxel_data: ti.types.ndarray(),
|
|
151
|
+
is_tree: ti.template(),
|
|
152
|
+
is_solid: ti.template(),
|
|
153
|
+
is_walkable: ti.template()
|
|
154
|
+
):
|
|
155
|
+
for i, j, k in is_tree:
|
|
156
|
+
val = voxel_data[i, j, k]
|
|
157
|
+
|
|
158
|
+
tree = 0
|
|
159
|
+
if val == -2:
|
|
160
|
+
tree = 1
|
|
161
|
+
is_tree[i, j, k] = tree
|
|
162
|
+
|
|
163
|
+
# Solid blocks rays, but NOT landmark voxels (val == -30)
|
|
164
|
+
solid = 0
|
|
165
|
+
if val != 0 and val != -2 and val != -30:
|
|
166
|
+
solid = 1
|
|
167
|
+
is_solid[i, j, k] = solid
|
|
168
|
+
|
|
169
|
+
# Walkable: surfaces that are valid observer positions
|
|
170
|
+
# Exclude: water (7, 8, 9) and negative values (ground -1, tree -2, building -3, etc.)
|
|
171
|
+
# A surface is walkable if the voxel value is positive and not water
|
|
172
|
+
walkable = 1
|
|
173
|
+
if val == 7 or val == 8 or val == 9: # Water
|
|
174
|
+
walkable = 0
|
|
175
|
+
elif val < 0: # Ground, trees, buildings, landmarks, etc.
|
|
176
|
+
walkable = 0
|
|
177
|
+
is_walkable[i, j, k] = walkable
|
|
178
|
+
|
|
179
|
+
@ti.kernel
|
|
180
|
+
def _compute_visibility_map_kernel(
|
|
181
|
+
self,
|
|
182
|
+
visibility_map: ti.template(),
|
|
183
|
+
view_height_voxel: ti.i32,
|
|
184
|
+
is_tree: ti.template(),
|
|
185
|
+
is_solid: ti.template(),
|
|
186
|
+
is_walkable: ti.template(),
|
|
187
|
+
tree_att: ti.f32,
|
|
188
|
+
att_cutoff: ti.f32
|
|
189
|
+
):
|
|
190
|
+
"""Compute landmark visibility map using GPU parallel processing."""
|
|
191
|
+
for x, y in visibility_map:
|
|
192
|
+
# Find observer position (first air voxel above a solid surface)
|
|
193
|
+
observer_z = -1
|
|
194
|
+
surface_walkable = 0
|
|
195
|
+
for z in range(1, self.nz):
|
|
196
|
+
val_above = is_solid[x, y, z] + is_tree[x, y, z]
|
|
197
|
+
val_below = is_solid[x, y, z-1] + is_tree[x, y, z-1]
|
|
198
|
+
|
|
199
|
+
if val_above == 0 and val_below > 0:
|
|
200
|
+
# Found ground level - check if walkable
|
|
201
|
+
surface_walkable = is_walkable[x, y, z-1]
|
|
202
|
+
observer_z = z + view_height_voxel
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
# Mark as invalid if no observer position found or surface not walkable
|
|
206
|
+
# (water, building tops, etc. are not walkable)
|
|
207
|
+
if observer_z < 0 or observer_z >= self.nz or surface_walkable == 0:
|
|
208
|
+
visibility_map[x, y] = ti.cast(float('nan'), ti.f32)
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
# Check visibility to any landmark
|
|
212
|
+
visible = 0
|
|
213
|
+
origin = Vector3(ti.cast(x, ti.f32), ti.cast(y, ti.f32), ti.cast(observer_z, ti.f32))
|
|
214
|
+
|
|
215
|
+
for lm in range(self._n_landmarks):
|
|
216
|
+
if visible == 0:
|
|
217
|
+
target = self._landmark_positions[lm]
|
|
218
|
+
|
|
219
|
+
vis = self._trace_to_landmark(
|
|
220
|
+
origin, target,
|
|
221
|
+
is_tree, is_solid,
|
|
222
|
+
tree_att, att_cutoff
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if vis == 1:
|
|
226
|
+
visible = 1
|
|
227
|
+
|
|
228
|
+
visibility_map[x, y] = ti.cast(visible, ti.f32)
|
|
229
|
+
|
|
230
|
+
@ti.func
|
|
231
|
+
def _trace_to_landmark(
|
|
232
|
+
self,
|
|
233
|
+
origin: Vector3,
|
|
234
|
+
target: Vector3,
|
|
235
|
+
is_tree: ti.template(),
|
|
236
|
+
is_solid: ti.template(),
|
|
237
|
+
tree_att: ti.f32,
|
|
238
|
+
att_cutoff: ti.f32
|
|
239
|
+
) -> ti.i32:
|
|
240
|
+
"""Trace ray from origin to target landmark."""
|
|
241
|
+
diff = target - origin
|
|
242
|
+
dist = diff.norm()
|
|
243
|
+
|
|
244
|
+
visible = 1
|
|
245
|
+
|
|
246
|
+
if dist < 0.01:
|
|
247
|
+
visible = 1
|
|
248
|
+
else:
|
|
249
|
+
ray_dir = diff / dist
|
|
250
|
+
|
|
251
|
+
ox, oy, oz = origin[0], origin[1], origin[2]
|
|
252
|
+
x = ox + 0.5
|
|
253
|
+
y = oy + 0.5
|
|
254
|
+
z = oz + 0.5
|
|
255
|
+
|
|
256
|
+
i = ti.cast(ti.floor(ox), ti.i32)
|
|
257
|
+
j = ti.cast(ti.floor(oy), ti.i32)
|
|
258
|
+
k = ti.cast(ti.floor(oz), ti.i32)
|
|
259
|
+
|
|
260
|
+
ti_x = ti.cast(ti.floor(target[0]), ti.i32)
|
|
261
|
+
tj_y = ti.cast(ti.floor(target[1]), ti.i32)
|
|
262
|
+
tk_z = ti.cast(ti.floor(target[2]), ti.i32)
|
|
263
|
+
|
|
264
|
+
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
265
|
+
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
266
|
+
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
267
|
+
|
|
268
|
+
BIG = 1e30
|
|
269
|
+
t_max_x, t_max_y, t_max_z = BIG, BIG, BIG
|
|
270
|
+
t_delta_x, t_delta_y, t_delta_z = BIG, BIG, BIG
|
|
271
|
+
|
|
272
|
+
if ray_dir[0] != 0.0:
|
|
273
|
+
t_max_x = ((i + (1 if step_x > 0 else 0)) - x) / ray_dir[0]
|
|
274
|
+
t_delta_x = ti.abs(1.0 / ray_dir[0])
|
|
275
|
+
if ray_dir[1] != 0.0:
|
|
276
|
+
t_max_y = ((j + (1 if step_y > 0 else 0)) - y) / ray_dir[1]
|
|
277
|
+
t_delta_y = ti.abs(1.0 / ray_dir[1])
|
|
278
|
+
if ray_dir[2] != 0.0:
|
|
279
|
+
t_max_z = ((k + (1 if step_z > 0 else 0)) - z) / ray_dir[2]
|
|
280
|
+
t_delta_z = ti.abs(1.0 / ray_dir[2])
|
|
281
|
+
|
|
282
|
+
T = 1.0
|
|
283
|
+
max_steps = self.nx + self.ny + self.nz
|
|
284
|
+
done = 0
|
|
285
|
+
|
|
286
|
+
for _ in range(max_steps):
|
|
287
|
+
if done == 0:
|
|
288
|
+
# Check bounds
|
|
289
|
+
if i < 0 or i >= self.nx or j < 0 or j >= self.ny or k < 0 or k >= self.nz:
|
|
290
|
+
visible = 0
|
|
291
|
+
done = 1
|
|
292
|
+
# Check if reached target
|
|
293
|
+
elif i == ti_x and j == tj_y and k == tk_z:
|
|
294
|
+
visible = 1
|
|
295
|
+
done = 1
|
|
296
|
+
# Check for solid blocker (not the target)
|
|
297
|
+
elif is_solid[i, j, k] == 1:
|
|
298
|
+
visible = 0
|
|
299
|
+
done = 1
|
|
300
|
+
# Check for tree attenuation
|
|
301
|
+
elif is_tree[i, j, k] == 1:
|
|
302
|
+
T *= tree_att
|
|
303
|
+
if T < att_cutoff:
|
|
304
|
+
visible = 0
|
|
305
|
+
done = 1
|
|
306
|
+
|
|
307
|
+
# Move to next voxel using 3D DDA
|
|
308
|
+
if done == 0:
|
|
309
|
+
if t_max_x < t_max_y:
|
|
310
|
+
if t_max_x < t_max_z:
|
|
311
|
+
t_max_x += t_delta_x
|
|
312
|
+
i += step_x
|
|
313
|
+
else:
|
|
314
|
+
t_max_z += t_delta_z
|
|
315
|
+
k += step_z
|
|
316
|
+
else:
|
|
317
|
+
if t_max_y < t_max_z:
|
|
318
|
+
t_max_y += t_delta_y
|
|
319
|
+
j += step_y
|
|
320
|
+
else:
|
|
321
|
+
t_max_z += t_delta_z
|
|
322
|
+
k += step_z
|
|
323
|
+
|
|
324
|
+
return visible
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def mark_building_by_id(
|
|
328
|
+
voxcity_grid_ori: np.ndarray,
|
|
329
|
+
building_id_grid_ori: np.ndarray,
|
|
330
|
+
ids: List[int],
|
|
331
|
+
mark: int = -30
|
|
332
|
+
) -> np.ndarray:
|
|
333
|
+
"""
|
|
334
|
+
Mark specific buildings in voxel data with a marker value.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
voxcity_grid_ori: 3D voxel class array
|
|
338
|
+
building_id_grid_ori: 2D array of building IDs (VoxCity format - needs flipud to match voxel_data)
|
|
339
|
+
ids: List of building IDs to mark
|
|
340
|
+
mark: Marker value to use
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Modified voxel_data copy
|
|
344
|
+
"""
|
|
345
|
+
voxel_data = voxcity_grid_ori.copy()
|
|
346
|
+
|
|
347
|
+
# VoxCity building_id_grid is flipped relative to voxel_data coordinate system
|
|
348
|
+
# We need to flip it to align with voxel_data
|
|
349
|
+
building_id_grid_aligned = np.flipud(building_id_grid_ori)
|
|
350
|
+
|
|
351
|
+
# Find positions where building IDs match
|
|
352
|
+
positions = np.where(np.isin(building_id_grid_aligned, ids))
|
|
353
|
+
for i in range(len(positions[0])):
|
|
354
|
+
x, y = positions[0][i], positions[1][i]
|
|
355
|
+
z_mask = voxel_data[x, y, :] == -3 # Building class
|
|
356
|
+
voxel_data[x, y, z_mask] = mark
|
|
357
|
+
|
|
358
|
+
return voxel_data
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def compute_landmark_visibility(
|
|
362
|
+
voxel_data: np.ndarray,
|
|
363
|
+
target_value: int = -30,
|
|
364
|
+
view_height_voxel: int = 0,
|
|
365
|
+
colormap: str = 'viridis'
|
|
366
|
+
) -> np.ndarray:
|
|
367
|
+
"""VoxCity-compatible landmark visibility on raw voxel data.
|
|
368
|
+
|
|
369
|
+
Matches `voxcity.simulator.visibility.landmark.compute_landmark_visibility`.
|
|
370
|
+
|
|
371
|
+
Notes:
|
|
372
|
+
- Uses Taichi GPU ray tracing underneath.
|
|
373
|
+
- Returns a 2D map flipped with `np.flipud`, consistent with VoxCity.
|
|
374
|
+
"""
|
|
375
|
+
from ..domain import Domain
|
|
376
|
+
|
|
377
|
+
if voxel_data.ndim != 3:
|
|
378
|
+
raise ValueError("voxel_data must be a 3D array")
|
|
379
|
+
|
|
380
|
+
nx, ny, nz = voxel_data.shape
|
|
381
|
+
domain = Domain(nx=nx, ny=ny, nz=nz, dx=1.0, dy=1.0, dz=1.0)
|
|
382
|
+
calc = LandmarkVisibilityCalculator(domain)
|
|
383
|
+
calc.set_landmarks_from_voxel_value(voxel_data, landmark_value=int(target_value))
|
|
384
|
+
visibility_map = calc.compute_visibility_map(
|
|
385
|
+
voxel_data=voxel_data,
|
|
386
|
+
view_height_voxel=int(view_height_voxel),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Plot (VoxCity function always plots)
|
|
390
|
+
try:
|
|
391
|
+
import matplotlib.pyplot as plt
|
|
392
|
+
import matplotlib.patches as mpatches
|
|
393
|
+
|
|
394
|
+
cmap = plt.cm.get_cmap(colormap, 2).copy()
|
|
395
|
+
cmap.set_bad(color='lightgray')
|
|
396
|
+
plt.figure(figsize=(10, 8))
|
|
397
|
+
plt.imshow(visibility_map, origin='lower', cmap=cmap, vmin=0, vmax=1)
|
|
398
|
+
visible_patch = mpatches.Patch(color=cmap(1.0), label='Visible (1)')
|
|
399
|
+
not_visible_patch = mpatches.Patch(color=cmap(0.0), label='Not Visible (0)')
|
|
400
|
+
plt.legend(handles=[visible_patch, not_visible_patch], loc='center left', bbox_to_anchor=(1.0, 0.5))
|
|
401
|
+
plt.axis('off')
|
|
402
|
+
plt.show()
|
|
403
|
+
except Exception:
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
return visibility_map
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@ti.data_oriented
|
|
410
|
+
class SurfaceLandmarkVisibilityCalculator:
|
|
411
|
+
"""
|
|
412
|
+
GPU-accelerated Surface Landmark Visibility calculator.
|
|
413
|
+
|
|
414
|
+
Computes visibility of landmarks from building surface faces
|
|
415
|
+
using Taichi GPU acceleration.
|
|
416
|
+
|
|
417
|
+
This emulates voxcity.simulator.visibility.landmark.get_surface_landmark_visibility.
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
def __init__(self, domain):
|
|
421
|
+
"""
|
|
422
|
+
Initialize Surface Landmark Visibility Calculator.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
domain: Domain object with grid geometry
|
|
426
|
+
"""
|
|
427
|
+
self.domain = domain
|
|
428
|
+
self.nx = domain.nx
|
|
429
|
+
self.ny = domain.ny
|
|
430
|
+
self.nz = domain.nz
|
|
431
|
+
self.dx = domain.dx
|
|
432
|
+
self.dy = domain.dy
|
|
433
|
+
self.dz = domain.dz
|
|
434
|
+
self.meshsize = domain.dx
|
|
435
|
+
|
|
436
|
+
# Landmark positions
|
|
437
|
+
self._landmark_positions = None
|
|
438
|
+
self._n_landmarks = 0
|
|
439
|
+
|
|
440
|
+
def set_landmarks_from_positions(self, positions: np.ndarray):
|
|
441
|
+
"""
|
|
442
|
+
Set landmark positions directly.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
positions: Array of shape (n_landmarks, 3) with (x, y, z) coordinates in voxels
|
|
446
|
+
"""
|
|
447
|
+
self._n_landmarks = positions.shape[0]
|
|
448
|
+
self._landmark_positions = ti.Vector.field(3, dtype=ti.f32, shape=(self._n_landmarks,))
|
|
449
|
+
self._landmark_positions.from_numpy(positions.astype(np.float32))
|
|
450
|
+
|
|
451
|
+
def set_landmarks_from_voxel_value(self, voxel_data: np.ndarray, landmark_value: int = -30):
|
|
452
|
+
"""
|
|
453
|
+
Set landmark positions from voxel data based on a marker value.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
voxel_data: 3D voxel class array
|
|
457
|
+
landmark_value: Voxel value marking landmarks
|
|
458
|
+
"""
|
|
459
|
+
positions = np.argwhere(voxel_data == landmark_value).astype(np.float32)
|
|
460
|
+
if positions.shape[0] == 0:
|
|
461
|
+
raise ValueError(f"No landmark with value {landmark_value} found in voxel data.")
|
|
462
|
+
self.set_landmarks_from_positions(positions)
|
|
463
|
+
|
|
464
|
+
def compute_surface_landmark_visibility(
|
|
465
|
+
self,
|
|
466
|
+
face_centers: np.ndarray,
|
|
467
|
+
face_normals: np.ndarray,
|
|
468
|
+
voxel_data: np.ndarray = None,
|
|
469
|
+
landmark_value: int = -30,
|
|
470
|
+
tree_k: float = 0.6,
|
|
471
|
+
tree_lad: float = 1.0,
|
|
472
|
+
boundary_epsilon: float = None
|
|
473
|
+
) -> np.ndarray:
|
|
474
|
+
"""
|
|
475
|
+
Compute landmark visibility for building surface faces.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
face_centers: Array of face center positions (n_faces, 3) in world coords
|
|
479
|
+
face_normals: Array of face normal vectors (n_faces, 3)
|
|
480
|
+
voxel_data: 3D voxel class array with landmarks marked
|
|
481
|
+
landmark_value: Voxel value marking landmarks
|
|
482
|
+
tree_k: Tree extinction coefficient
|
|
483
|
+
tree_lad: Leaf area density
|
|
484
|
+
boundary_epsilon: Epsilon for boundary detection
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
1D array with 1.0 where any landmark is visible, 0.0 otherwise, nan for boundary
|
|
488
|
+
"""
|
|
489
|
+
if self._landmark_positions is None or self._n_landmarks == 0:
|
|
490
|
+
raise ValueError("No landmarks set. Call set_landmarks_* first.")
|
|
491
|
+
|
|
492
|
+
n_faces = face_centers.shape[0]
|
|
493
|
+
|
|
494
|
+
if boundary_epsilon is None:
|
|
495
|
+
boundary_epsilon = self.meshsize * 0.05
|
|
496
|
+
|
|
497
|
+
# Grid bounds in world coordinates
|
|
498
|
+
grid_bounds_real = np.array([
|
|
499
|
+
[0.0, 0.0, 0.0],
|
|
500
|
+
[self.nx * self.meshsize, self.ny * self.meshsize, self.nz * self.meshsize]
|
|
501
|
+
], dtype=np.float32)
|
|
502
|
+
|
|
503
|
+
# Prepare Taichi fields
|
|
504
|
+
face_centers_ti = ti.Vector.field(3, dtype=ti.f32, shape=(n_faces,))
|
|
505
|
+
face_normals_ti = ti.Vector.field(3, dtype=ti.f32, shape=(n_faces,))
|
|
506
|
+
visibility_values = ti.field(dtype=ti.f32, shape=(n_faces,))
|
|
507
|
+
|
|
508
|
+
face_centers_ti.from_numpy(face_centers.astype(np.float32))
|
|
509
|
+
face_normals_ti.from_numpy(face_normals.astype(np.float32))
|
|
510
|
+
|
|
511
|
+
# Prepare masks
|
|
512
|
+
if voxel_data is not None:
|
|
513
|
+
is_tree = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
514
|
+
is_opaque = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
515
|
+
self._setup_surface_masks(voxel_data, landmark_value, is_tree, is_opaque)
|
|
516
|
+
else:
|
|
517
|
+
is_tree = self.domain.is_tree
|
|
518
|
+
is_opaque = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
519
|
+
|
|
520
|
+
# Tree attenuation
|
|
521
|
+
tree_att = float(math.exp(-tree_k * tree_lad * self.meshsize))
|
|
522
|
+
att_cutoff = 0.01
|
|
523
|
+
|
|
524
|
+
# Run GPU computation
|
|
525
|
+
self._compute_surface_landmark_kernel(
|
|
526
|
+
face_centers_ti, face_normals_ti, visibility_values,
|
|
527
|
+
is_tree, is_opaque, tree_att, att_cutoff,
|
|
528
|
+
grid_bounds_real, boundary_epsilon
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return visibility_values.to_numpy()
|
|
532
|
+
|
|
533
|
+
@ti.kernel
|
|
534
|
+
def _setup_surface_masks(
|
|
535
|
+
self,
|
|
536
|
+
voxel_data: ti.types.ndarray(),
|
|
537
|
+
landmark_value: ti.i32,
|
|
538
|
+
is_tree: ti.template(),
|
|
539
|
+
is_opaque: ti.template()
|
|
540
|
+
):
|
|
541
|
+
for i, j, k in is_tree:
|
|
542
|
+
val = voxel_data[i, j, k]
|
|
543
|
+
|
|
544
|
+
# Tree check
|
|
545
|
+
tree = 0
|
|
546
|
+
if val == -2:
|
|
547
|
+
tree = 1
|
|
548
|
+
is_tree[i, j, k] = tree
|
|
549
|
+
|
|
550
|
+
# Opaque: non-zero, non-tree, non-landmark
|
|
551
|
+
opaque = 0
|
|
552
|
+
if val != 0 and val != -2 and val != landmark_value:
|
|
553
|
+
opaque = 1
|
|
554
|
+
is_opaque[i, j, k] = opaque
|
|
555
|
+
|
|
556
|
+
@ti.kernel
|
|
557
|
+
def _compute_surface_landmark_kernel(
|
|
558
|
+
self,
|
|
559
|
+
face_centers: ti.template(),
|
|
560
|
+
face_normals: ti.template(),
|
|
561
|
+
visibility_values: ti.template(),
|
|
562
|
+
is_tree: ti.template(),
|
|
563
|
+
is_opaque: ti.template(),
|
|
564
|
+
tree_att: ti.f32,
|
|
565
|
+
att_cutoff: ti.f32,
|
|
566
|
+
grid_bounds: ti.types.ndarray(),
|
|
567
|
+
boundary_epsilon: ti.f32
|
|
568
|
+
):
|
|
569
|
+
"""Compute surface landmark visibility using GPU parallel processing."""
|
|
570
|
+
for f in visibility_values:
|
|
571
|
+
center = face_centers[f]
|
|
572
|
+
normal = face_normals[f]
|
|
573
|
+
|
|
574
|
+
# Check if face is on domain boundary
|
|
575
|
+
is_vertical = ti.abs(normal[2]) < 0.01
|
|
576
|
+
on_x_min = ti.abs(center[0] - grid_bounds[0, 0]) < boundary_epsilon
|
|
577
|
+
on_y_min = ti.abs(center[1] - grid_bounds[0, 1]) < boundary_epsilon
|
|
578
|
+
on_x_max = ti.abs(center[0] - grid_bounds[1, 0]) < boundary_epsilon
|
|
579
|
+
on_y_max = ti.abs(center[1] - grid_bounds[1, 1]) < boundary_epsilon
|
|
580
|
+
|
|
581
|
+
is_boundary = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
|
|
582
|
+
|
|
583
|
+
if is_boundary:
|
|
584
|
+
visibility_values[f] = ti.cast(float('nan'), ti.f32)
|
|
585
|
+
else:
|
|
586
|
+
# Normalize normal
|
|
587
|
+
nrm = normal.norm()
|
|
588
|
+
n = normal
|
|
589
|
+
if nrm > 1e-12:
|
|
590
|
+
n = normal / nrm
|
|
591
|
+
|
|
592
|
+
# Origin: face center offset by normal (in voxel coordinates)
|
|
593
|
+
meshsize = self.dx
|
|
594
|
+
ox = center[0] / meshsize + n[0] * 0.1
|
|
595
|
+
oy = center[1] / meshsize + n[1] * 0.1
|
|
596
|
+
oz = center[2] / meshsize + n[2] * 0.1
|
|
597
|
+
|
|
598
|
+
visible = 0
|
|
599
|
+
|
|
600
|
+
# Check visibility to each landmark
|
|
601
|
+
for lm in range(self._n_landmarks):
|
|
602
|
+
if visible == 0:
|
|
603
|
+
target = self._landmark_positions[lm]
|
|
604
|
+
|
|
605
|
+
# Direction to landmark
|
|
606
|
+
rx = target[0] - ox
|
|
607
|
+
ry = target[1] - oy
|
|
608
|
+
rz = target[2] - oz
|
|
609
|
+
rlen = ti.sqrt(rx*rx + ry*ry + rz*rz)
|
|
610
|
+
|
|
611
|
+
if rlen > 0.0:
|
|
612
|
+
# Check if landmark is in front of face
|
|
613
|
+
rdx = rx / rlen
|
|
614
|
+
rdy = ry / rlen
|
|
615
|
+
rdz = rz / rlen
|
|
616
|
+
|
|
617
|
+
dot = rdx*n[0] + rdy*n[1] + rdz*n[2]
|
|
618
|
+
if dot > 0.0:
|
|
619
|
+
# Trace ray to landmark
|
|
620
|
+
vis = self._trace_to_landmark(
|
|
621
|
+
ox, oy, oz, target,
|
|
622
|
+
is_tree, is_opaque,
|
|
623
|
+
tree_att, att_cutoff
|
|
624
|
+
)
|
|
625
|
+
if vis == 1:
|
|
626
|
+
visible = 1
|
|
627
|
+
|
|
628
|
+
visibility_values[f] = ti.cast(visible, ti.f32)
|
|
629
|
+
|
|
630
|
+
@ti.func
|
|
631
|
+
def _trace_to_landmark(
|
|
632
|
+
self,
|
|
633
|
+
ox: ti.f32,
|
|
634
|
+
oy: ti.f32,
|
|
635
|
+
oz: ti.f32,
|
|
636
|
+
target: ti.template(),
|
|
637
|
+
is_tree: ti.template(),
|
|
638
|
+
is_opaque: ti.template(),
|
|
639
|
+
tree_att: ti.f32,
|
|
640
|
+
att_cutoff: ti.f32
|
|
641
|
+
) -> ti.i32:
|
|
642
|
+
"""Trace ray from surface to landmark."""
|
|
643
|
+
diff_x = target[0] - ox
|
|
644
|
+
diff_y = target[1] - oy
|
|
645
|
+
diff_z = target[2] - oz
|
|
646
|
+
dist = ti.sqrt(diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)
|
|
647
|
+
|
|
648
|
+
visible = 1
|
|
649
|
+
|
|
650
|
+
if dist < 0.01:
|
|
651
|
+
visible = 1
|
|
652
|
+
else:
|
|
653
|
+
ray_dir = ti.Vector([diff_x/dist, diff_y/dist, diff_z/dist])
|
|
654
|
+
|
|
655
|
+
x = ox + 0.5
|
|
656
|
+
y = oy + 0.5
|
|
657
|
+
z = oz + 0.5
|
|
658
|
+
|
|
659
|
+
i = ti.cast(ti.floor(ox), ti.i32)
|
|
660
|
+
j = ti.cast(ti.floor(oy), ti.i32)
|
|
661
|
+
k = ti.cast(ti.floor(oz), ti.i32)
|
|
662
|
+
|
|
663
|
+
ti_x = ti.cast(ti.floor(target[0]), ti.i32)
|
|
664
|
+
tj_y = ti.cast(ti.floor(target[1]), ti.i32)
|
|
665
|
+
tk_z = ti.cast(ti.floor(target[2]), ti.i32)
|
|
666
|
+
|
|
667
|
+
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
668
|
+
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
669
|
+
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
670
|
+
|
|
671
|
+
BIG = 1e30
|
|
672
|
+
t_max_x, t_max_y, t_max_z = BIG, BIG, BIG
|
|
673
|
+
t_delta_x, t_delta_y, t_delta_z = BIG, BIG, BIG
|
|
674
|
+
|
|
675
|
+
if ray_dir[0] != 0.0:
|
|
676
|
+
t_max_x = ((i + (1 if step_x > 0 else 0)) - x) / ray_dir[0]
|
|
677
|
+
t_delta_x = ti.abs(1.0 / ray_dir[0])
|
|
678
|
+
if ray_dir[1] != 0.0:
|
|
679
|
+
t_max_y = ((j + (1 if step_y > 0 else 0)) - y) / ray_dir[1]
|
|
680
|
+
t_delta_y = ti.abs(1.0 / ray_dir[1])
|
|
681
|
+
if ray_dir[2] != 0.0:
|
|
682
|
+
t_max_z = ((k + (1 if step_z > 0 else 0)) - z) / ray_dir[2]
|
|
683
|
+
t_delta_z = ti.abs(1.0 / ray_dir[2])
|
|
684
|
+
|
|
685
|
+
T = 1.0
|
|
686
|
+
max_steps = self.nx + self.ny + self.nz
|
|
687
|
+
done = 0
|
|
688
|
+
|
|
689
|
+
for _ in range(max_steps):
|
|
690
|
+
if done == 0:
|
|
691
|
+
if i < 0 or i >= self.nx or j < 0 or j >= self.ny or k < 0 or k >= self.nz:
|
|
692
|
+
visible = 0
|
|
693
|
+
done = 1
|
|
694
|
+
elif is_opaque[i, j, k] == 1:
|
|
695
|
+
# Check if we're at the target
|
|
696
|
+
if not (i == ti_x and j == tj_y and k == tk_z):
|
|
697
|
+
visible = 0
|
|
698
|
+
done = 1
|
|
699
|
+
elif is_tree[i, j, k] == 1:
|
|
700
|
+
T *= tree_att
|
|
701
|
+
if T < att_cutoff:
|
|
702
|
+
visible = 0
|
|
703
|
+
done = 1
|
|
704
|
+
|
|
705
|
+
if done == 0:
|
|
706
|
+
if i == ti_x and j == tj_y and k == tk_z:
|
|
707
|
+
done = 1
|
|
708
|
+
else:
|
|
709
|
+
if t_max_x < t_max_y:
|
|
710
|
+
if t_max_x < t_max_z:
|
|
711
|
+
t_max_x += t_delta_x
|
|
712
|
+
i += step_x
|
|
713
|
+
else:
|
|
714
|
+
t_max_z += t_delta_z
|
|
715
|
+
k += step_z
|
|
716
|
+
else:
|
|
717
|
+
if t_max_y < t_max_z:
|
|
718
|
+
t_max_y += t_delta_y
|
|
719
|
+
j += step_y
|
|
720
|
+
else:
|
|
721
|
+
t_max_z += t_delta_z
|
|
722
|
+
k += step_z
|
|
723
|
+
|
|
724
|
+
return visible
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def compute_landmark_visibility_map(
|
|
728
|
+
domain,
|
|
729
|
+
voxel_data: np.ndarray,
|
|
730
|
+
landmark_value: int = -30,
|
|
731
|
+
view_height_voxel: int = 0,
|
|
732
|
+
**kwargs
|
|
733
|
+
) -> np.ndarray:
|
|
734
|
+
"""
|
|
735
|
+
Compute landmark visibility map.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
domain: Domain object
|
|
739
|
+
voxel_data: 3D voxel class array with landmarks marked
|
|
740
|
+
landmark_value: Voxel value marking landmarks
|
|
741
|
+
view_height_voxel: Observer height in voxels
|
|
742
|
+
**kwargs: Additional parameters
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
2D visibility map
|
|
746
|
+
"""
|
|
747
|
+
calc = LandmarkVisibilityCalculator(domain)
|
|
748
|
+
calc.set_landmarks_from_voxel_value(voxel_data, landmark_value)
|
|
749
|
+
return calc.compute_visibility_map(
|
|
750
|
+
voxel_data=voxel_data,
|
|
751
|
+
view_height_voxel=view_height_voxel,
|
|
752
|
+
**kwargs
|
|
753
|
+
)
|