voxcity 1.0.13__py3-none-any.whl → 1.0.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voxcity/simulator/solar/__init__.py +13 -0
- voxcity/simulator_gpu/__init__.py +73 -98
- voxcity/simulator_gpu/domain.py +30 -256
- voxcity/simulator_gpu/raytracing.py +153 -0
- voxcity/simulator_gpu/solar/__init__.py +45 -1
- voxcity/simulator_gpu/solar/domain.py +57 -0
- voxcity/simulator_gpu/solar/integration.py +1622 -253
- voxcity/simulator_gpu/solar/mask.py +459 -0
- voxcity/simulator_gpu/solar/raytracing.py +28 -532
- voxcity/simulator_gpu/solar/volumetric.py +962 -14
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/METADATA +1 -1
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/RECORD +15 -25
- voxcity/simulator_gpu/common/__init__.py +0 -9
- voxcity/simulator_gpu/common/geometry.py +0 -11
- voxcity/simulator_gpu/environment.yml +0 -11
- voxcity/simulator_gpu/integration.py +0 -15
- voxcity/simulator_gpu/kernels.py +0 -56
- voxcity/simulator_gpu/radiation.py +0 -28
- voxcity/simulator_gpu/sky.py +0 -9
- voxcity/simulator_gpu/solar/voxcity.py +0 -2953
- voxcity/simulator_gpu/temporal.py +0 -13
- voxcity/simulator_gpu/utils.py +0 -25
- voxcity/simulator_gpu/view.py +0 -32
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/WHEEL +0 -0
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Ray tracing module for
|
|
2
|
+
Ray tracing module for solar simulation.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Uses 3D-DDA (Digital Differential Analyzer) for voxel traversal.
|
|
4
|
+
This module provides the RayTracer class for GPU-accelerated radiation calculations.
|
|
5
|
+
Shared ray tracing functions are imported from simulator_gpu.raytracing.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- Solid obstacles block rays completely (trans = 0)
|
|
11
|
-
- Ray tracing replaces PALM's raytrace_2d subroutine with GPU-parallel version
|
|
12
|
-
|
|
13
|
-
Difference from PALM:
|
|
14
|
-
- PALM pre-computes dsitrans for discrete solar directions
|
|
15
|
-
- palm_solar traces rays dynamically for exact sun position
|
|
16
|
-
- This gives identical physics with slightly different numerical approach
|
|
17
|
-
|
|
18
|
-
Note: This module contains solar-specific ray tracing implementations that use
|
|
19
|
-
LAD (Leaf Area Density) fields. For simpler view-based ray tracing using
|
|
20
|
-
is_tree masks, see simulator_gpu.raytracing.
|
|
7
|
+
Usage:
|
|
8
|
+
from .raytracing import RayTracer, ray_voxel_first_hit, ray_canopy_absorption
|
|
21
9
|
"""
|
|
22
10
|
|
|
23
11
|
import taichi as ti
|
|
@@ -26,475 +14,17 @@ from typing import Tuple, Optional
|
|
|
26
14
|
|
|
27
15
|
from .core import Vector3, Point3, EXT_COEF
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Ray-AABB intersection using slab method.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
ray_origin: Ray origin point
|
|
44
|
-
ray_dir: Ray direction (normalized)
|
|
45
|
-
box_min: AABB minimum corner
|
|
46
|
-
box_max: AABB maximum corner
|
|
47
|
-
t_min: Minimum t value
|
|
48
|
-
t_max: Maximum t value
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Tuple of (hit, t_enter, t_exit)
|
|
52
|
-
"""
|
|
53
|
-
t_enter = t_min
|
|
54
|
-
t_exit = t_max
|
|
55
|
-
hit = 1
|
|
56
|
-
|
|
57
|
-
for i in ti.static(range(3)):
|
|
58
|
-
if ti.abs(ray_dir[i]) < 1e-10:
|
|
59
|
-
# Ray parallel to slab
|
|
60
|
-
if ray_origin[i] < box_min[i] or ray_origin[i] > box_max[i]:
|
|
61
|
-
hit = 0
|
|
62
|
-
else:
|
|
63
|
-
inv_d = 1.0 / ray_dir[i]
|
|
64
|
-
t1 = (box_min[i] - ray_origin[i]) * inv_d
|
|
65
|
-
t2 = (box_max[i] - ray_origin[i]) * inv_d
|
|
66
|
-
|
|
67
|
-
if t1 > t2:
|
|
68
|
-
t1, t2 = t2, t1
|
|
69
|
-
|
|
70
|
-
t_enter = ti.max(t_enter, t1)
|
|
71
|
-
t_exit = ti.min(t_exit, t2)
|
|
72
|
-
|
|
73
|
-
if t_enter > t_exit:
|
|
74
|
-
hit = 0
|
|
75
|
-
|
|
76
|
-
return hit, t_enter, t_exit
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@ti.func
|
|
80
|
-
def ray_voxel_first_hit(
|
|
81
|
-
ray_origin: Vector3,
|
|
82
|
-
ray_dir: Vector3,
|
|
83
|
-
is_solid: ti.template(),
|
|
84
|
-
nx: ti.i32,
|
|
85
|
-
ny: ti.i32,
|
|
86
|
-
nz: ti.i32,
|
|
87
|
-
dx: ti.f32,
|
|
88
|
-
dy: ti.f32,
|
|
89
|
-
dz: ti.f32,
|
|
90
|
-
max_dist: ti.f32
|
|
91
|
-
):
|
|
92
|
-
"""
|
|
93
|
-
3D-DDA ray marching to find first solid voxel hit.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
ray_origin: Ray origin
|
|
97
|
-
ray_dir: Ray direction (normalized)
|
|
98
|
-
is_solid: 3D field of solid cells
|
|
99
|
-
nx, ny, nz: Grid dimensions
|
|
100
|
-
dx, dy, dz: Cell sizes
|
|
101
|
-
max_dist: Maximum ray distance
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
Tuple of (hit, t_hit, ix, iy, iz)
|
|
105
|
-
"""
|
|
106
|
-
hit = 0
|
|
107
|
-
t_hit = max_dist
|
|
108
|
-
hit_ix, hit_iy, hit_iz = 0, 0, 0
|
|
109
|
-
|
|
110
|
-
# Find entry into domain
|
|
111
|
-
domain_min = Vector3(0.0, 0.0, 0.0)
|
|
112
|
-
domain_max = Vector3(nx * dx, ny * dy, nz * dz)
|
|
113
|
-
|
|
114
|
-
in_domain, t_enter, t_exit = ray_aabb_intersect(
|
|
115
|
-
ray_origin, ray_dir, domain_min, domain_max, 0.0, max_dist
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
if in_domain == 1:
|
|
119
|
-
# Start position (slightly inside domain)
|
|
120
|
-
t = t_enter + 1e-5
|
|
121
|
-
pos = ray_origin + ray_dir * t
|
|
122
|
-
|
|
123
|
-
# Current voxel indices
|
|
124
|
-
ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
|
|
125
|
-
iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
|
|
126
|
-
iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
|
|
127
|
-
|
|
128
|
-
# Clamp to valid range
|
|
129
|
-
ix = ti.max(0, ti.min(nx - 1, ix))
|
|
130
|
-
iy = ti.max(0, ti.min(ny - 1, iy))
|
|
131
|
-
iz = ti.max(0, ti.min(nz - 1, iz))
|
|
132
|
-
|
|
133
|
-
# Step directions
|
|
134
|
-
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
135
|
-
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
136
|
-
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
137
|
-
|
|
138
|
-
# Initialize DDA variables
|
|
139
|
-
t_max_x = 1e30
|
|
140
|
-
t_max_y = 1e30
|
|
141
|
-
t_max_z = 1e30
|
|
142
|
-
t_delta_x = 1e30
|
|
143
|
-
t_delta_y = 1e30
|
|
144
|
-
t_delta_z = 1e30
|
|
145
|
-
|
|
146
|
-
# t values for next boundary crossing
|
|
147
|
-
if ti.abs(ray_dir[0]) > 1e-10:
|
|
148
|
-
if step_x > 0:
|
|
149
|
-
t_max_x = ((ix + 1) * dx - pos[0]) / ray_dir[0] + t
|
|
150
|
-
else:
|
|
151
|
-
t_max_x = (ix * dx - pos[0]) / ray_dir[0] + t
|
|
152
|
-
t_delta_x = ti.abs(dx / ray_dir[0])
|
|
153
|
-
|
|
154
|
-
if ti.abs(ray_dir[1]) > 1e-10:
|
|
155
|
-
if step_y > 0:
|
|
156
|
-
t_max_y = ((iy + 1) * dy - pos[1]) / ray_dir[1] + t
|
|
157
|
-
else:
|
|
158
|
-
t_max_y = (iy * dy - pos[1]) / ray_dir[1] + t
|
|
159
|
-
t_delta_y = ti.abs(dy / ray_dir[1])
|
|
160
|
-
|
|
161
|
-
if ti.abs(ray_dir[2]) > 1e-10:
|
|
162
|
-
if step_z > 0:
|
|
163
|
-
t_max_z = ((iz + 1) * dz - pos[2]) / ray_dir[2] + t
|
|
164
|
-
else:
|
|
165
|
-
t_max_z = (iz * dz - pos[2]) / ray_dir[2] + t
|
|
166
|
-
t_delta_z = ti.abs(dz / ray_dir[2])
|
|
167
|
-
|
|
168
|
-
# 3D-DDA traversal - optimized with done flag to reduce branch divergence
|
|
169
|
-
# Using done flag pattern is more GPU-friendly than break statements
|
|
170
|
-
max_steps = nx + ny + nz
|
|
171
|
-
done = 0
|
|
172
|
-
|
|
173
|
-
for _ in range(max_steps):
|
|
174
|
-
# Use done flag pattern for GPU-friendly early termination
|
|
175
|
-
# This reduces warp divergence compared to break statements
|
|
176
|
-
if done == 0:
|
|
177
|
-
# Bounds check - exit if outside domain
|
|
178
|
-
if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
|
|
179
|
-
done = 1
|
|
180
|
-
elif t > t_exit:
|
|
181
|
-
done = 1
|
|
182
|
-
# Check current voxel for solid hit
|
|
183
|
-
elif is_solid[ix, iy, iz] == 1:
|
|
184
|
-
hit = 1
|
|
185
|
-
t_hit = t
|
|
186
|
-
hit_ix = ix
|
|
187
|
-
hit_iy = iy
|
|
188
|
-
hit_iz = iz
|
|
189
|
-
done = 1
|
|
190
|
-
else:
|
|
191
|
-
# Step to next voxel using branchless min selection
|
|
192
|
-
# This is more GPU-efficient than nested if-else
|
|
193
|
-
if t_max_x < t_max_y and t_max_x < t_max_z:
|
|
194
|
-
t = t_max_x
|
|
195
|
-
ix += step_x
|
|
196
|
-
t_max_x += t_delta_x
|
|
197
|
-
elif t_max_y < t_max_z:
|
|
198
|
-
t = t_max_y
|
|
199
|
-
iy += step_y
|
|
200
|
-
t_max_y += t_delta_y
|
|
201
|
-
else:
|
|
202
|
-
t = t_max_z
|
|
203
|
-
iz += step_z
|
|
204
|
-
t_max_z += t_delta_z
|
|
205
|
-
|
|
206
|
-
return hit, t_hit, hit_ix, hit_iy, hit_iz
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
@ti.func
|
|
210
|
-
def ray_canopy_absorption(
|
|
211
|
-
ray_origin: Vector3,
|
|
212
|
-
ray_dir: Vector3,
|
|
213
|
-
lad: ti.template(),
|
|
214
|
-
is_solid: ti.template(),
|
|
215
|
-
nx: ti.i32,
|
|
216
|
-
ny: ti.i32,
|
|
217
|
-
nz: ti.i32,
|
|
218
|
-
dx: ti.f32,
|
|
219
|
-
dy: ti.f32,
|
|
220
|
-
dz: ti.f32,
|
|
221
|
-
max_dist: ti.f32,
|
|
222
|
-
ext_coef: ti.f32
|
|
223
|
-
):
|
|
224
|
-
"""
|
|
225
|
-
Trace ray through canopy computing Beer-Lambert absorption.
|
|
226
|
-
|
|
227
|
-
Args:
|
|
228
|
-
ray_origin: Ray origin
|
|
229
|
-
ray_dir: Ray direction (normalized)
|
|
230
|
-
lad: 3D field of Leaf Area Density
|
|
231
|
-
is_solid: 3D field of solid cells (buildings/terrain)
|
|
232
|
-
nx, ny, nz: Grid dimensions
|
|
233
|
-
dx, dy, dz: Cell sizes
|
|
234
|
-
max_dist: Maximum ray distance
|
|
235
|
-
ext_coef: Extinction coefficient
|
|
236
|
-
|
|
237
|
-
Returns:
|
|
238
|
-
Tuple of (transmissivity, path_length_through_canopy)
|
|
239
|
-
"""
|
|
240
|
-
transmissivity = 1.0
|
|
241
|
-
total_lad_path = 0.0
|
|
242
|
-
|
|
243
|
-
# Find entry into domain
|
|
244
|
-
domain_min = Vector3(0.0, 0.0, 0.0)
|
|
245
|
-
domain_max = Vector3(nx * dx, ny * dy, nz * dz)
|
|
246
|
-
|
|
247
|
-
in_domain, t_enter, t_exit = ray_aabb_intersect(
|
|
248
|
-
ray_origin, ray_dir, domain_min, domain_max, 0.0, max_dist
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
if in_domain == 1:
|
|
252
|
-
t = t_enter + 1e-5
|
|
253
|
-
pos = ray_origin + ray_dir * t
|
|
254
|
-
|
|
255
|
-
ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
|
|
256
|
-
iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
|
|
257
|
-
iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
|
|
258
|
-
|
|
259
|
-
ix = ti.max(0, ti.min(nx - 1, ix))
|
|
260
|
-
iy = ti.max(0, ti.min(ny - 1, iy))
|
|
261
|
-
iz = ti.max(0, ti.min(nz - 1, iz))
|
|
262
|
-
|
|
263
|
-
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
264
|
-
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
265
|
-
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
266
|
-
|
|
267
|
-
# Initialize all DDA variables
|
|
268
|
-
t_max_x = 1e30
|
|
269
|
-
t_max_y = 1e30
|
|
270
|
-
t_max_z = 1e30
|
|
271
|
-
t_delta_x = 1e30
|
|
272
|
-
t_delta_y = 1e30
|
|
273
|
-
t_delta_z = 1e30
|
|
274
|
-
|
|
275
|
-
if ti.abs(ray_dir[0]) > 1e-10:
|
|
276
|
-
if step_x > 0:
|
|
277
|
-
t_max_x = ((ix + 1) * dx - pos[0]) / ray_dir[0] + t
|
|
278
|
-
else:
|
|
279
|
-
t_max_x = (ix * dx - pos[0]) / ray_dir[0] + t
|
|
280
|
-
t_delta_x = ti.abs(dx / ray_dir[0])
|
|
281
|
-
|
|
282
|
-
if ti.abs(ray_dir[1]) > 1e-10:
|
|
283
|
-
if step_y > 0:
|
|
284
|
-
t_max_y = ((iy + 1) * dy - pos[1]) / ray_dir[1] + t
|
|
285
|
-
else:
|
|
286
|
-
t_max_y = (iy * dy - pos[1]) / ray_dir[1] + t
|
|
287
|
-
t_delta_y = ti.abs(dy / ray_dir[1])
|
|
288
|
-
|
|
289
|
-
if ti.abs(ray_dir[2]) > 1e-10:
|
|
290
|
-
if step_z > 0:
|
|
291
|
-
t_max_z = ((iz + 1) * dz - pos[2]) / ray_dir[2] + t
|
|
292
|
-
else:
|
|
293
|
-
t_max_z = (iz * dz - pos[2]) / ray_dir[2] + t
|
|
294
|
-
t_delta_z = ti.abs(dz / ray_dir[2])
|
|
295
|
-
|
|
296
|
-
t_prev = t
|
|
297
|
-
max_steps = nx + ny + nz
|
|
298
|
-
done = 0
|
|
299
|
-
|
|
300
|
-
for _ in range(max_steps):
|
|
301
|
-
# GPU-friendly done flag pattern reduces warp divergence
|
|
302
|
-
if done == 0:
|
|
303
|
-
# Bounds and exit checks
|
|
304
|
-
if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
|
|
305
|
-
done = 1
|
|
306
|
-
elif t > t_exit:
|
|
307
|
-
done = 1
|
|
308
|
-
# Hit solid -> ray blocked
|
|
309
|
-
elif is_solid[ix, iy, iz] == 1:
|
|
310
|
-
transmissivity = 0.0
|
|
311
|
-
done = 1
|
|
312
|
-
else:
|
|
313
|
-
# Get step distance using branchless min
|
|
314
|
-
t_next = ti.min(t_max_x, ti.min(t_max_y, t_max_z))
|
|
315
|
-
|
|
316
|
-
# Path length through this cell
|
|
317
|
-
path_len = t_next - t_prev
|
|
318
|
-
|
|
319
|
-
# Accumulate absorption from LAD
|
|
320
|
-
# Using fused multiply-add for efficiency
|
|
321
|
-
cell_lad = lad[ix, iy, iz]
|
|
322
|
-
if cell_lad > 0.0:
|
|
323
|
-
lad_path = cell_lad * path_len
|
|
324
|
-
total_lad_path += lad_path
|
|
325
|
-
# Beer-Lambert: T = exp(-ext_coef * LAD * path)
|
|
326
|
-
transmissivity *= ti.exp(-ext_coef * lad_path)
|
|
327
|
-
|
|
328
|
-
t_prev = t_next
|
|
329
|
-
|
|
330
|
-
# Step to next voxel
|
|
331
|
-
if t_max_x < t_max_y and t_max_x < t_max_z:
|
|
332
|
-
t = t_max_x
|
|
333
|
-
ix += step_x
|
|
334
|
-
t_max_x += t_delta_x
|
|
335
|
-
elif t_max_y < t_max_z:
|
|
336
|
-
t = t_max_y
|
|
337
|
-
iy += step_y
|
|
338
|
-
t_max_y += t_delta_y
|
|
339
|
-
else:
|
|
340
|
-
t = t_max_z
|
|
341
|
-
iz += step_z
|
|
342
|
-
t_max_z += t_delta_z
|
|
343
|
-
|
|
344
|
-
return transmissivity, total_lad_path
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
@ti.func
|
|
348
|
-
def ray_point_to_point_transmissivity(
|
|
349
|
-
pos_from: Vector3,
|
|
350
|
-
pos_to: Vector3,
|
|
351
|
-
lad: ti.template(),
|
|
352
|
-
is_solid: ti.template(),
|
|
353
|
-
nx: ti.i32,
|
|
354
|
-
ny: ti.i32,
|
|
355
|
-
nz: ti.i32,
|
|
356
|
-
dx: ti.f32,
|
|
357
|
-
dy: ti.f32,
|
|
358
|
-
dz: ti.f32,
|
|
359
|
-
ext_coef: ti.f32
|
|
360
|
-
):
|
|
361
|
-
"""
|
|
362
|
-
Compute transmissivity of radiation between two points through canopy.
|
|
363
|
-
|
|
364
|
-
This is used for surface-to-surface reflections where reflected radiation
|
|
365
|
-
must pass through any intervening vegetation.
|
|
366
|
-
|
|
367
|
-
Args:
|
|
368
|
-
pos_from: Start position (emitting surface center)
|
|
369
|
-
pos_to: End position (receiving surface center)
|
|
370
|
-
lad: 3D field of Leaf Area Density
|
|
371
|
-
is_solid: 3D field of solid cells (buildings/terrain)
|
|
372
|
-
nx, ny, nz: Grid dimensions
|
|
373
|
-
dx, dy, dz: Cell sizes
|
|
374
|
-
ext_coef: Extinction coefficient
|
|
375
|
-
|
|
376
|
-
Returns:
|
|
377
|
-
Tuple of (transmissivity, blocked_by_solid)
|
|
378
|
-
- transmissivity: 0-1 fraction of radiation that gets through
|
|
379
|
-
- blocked_by_solid: 1 if ray hits a solid cell, 0 otherwise
|
|
380
|
-
"""
|
|
381
|
-
# Compute ray direction and distance
|
|
382
|
-
diff = pos_to - pos_from
|
|
383
|
-
dist = diff.norm()
|
|
384
|
-
|
|
385
|
-
transmissivity = 1.0
|
|
386
|
-
blocked = 0
|
|
387
|
-
|
|
388
|
-
# Only trace if distance is significant
|
|
389
|
-
if dist >= 0.01:
|
|
390
|
-
ray_dir = diff / dist
|
|
391
|
-
|
|
392
|
-
# Starting voxel
|
|
393
|
-
pos = pos_from + ray_dir * 0.01 # Slight offset to avoid self-intersection
|
|
394
|
-
|
|
395
|
-
ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
|
|
396
|
-
iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
|
|
397
|
-
iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
|
|
398
|
-
|
|
399
|
-
# Clamp to valid range
|
|
400
|
-
ix = ti.max(0, ti.min(nx - 1, ix))
|
|
401
|
-
iy = ti.max(0, ti.min(ny - 1, iy))
|
|
402
|
-
iz = ti.max(0, ti.min(nz - 1, iz))
|
|
403
|
-
|
|
404
|
-
# Step directions
|
|
405
|
-
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
406
|
-
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
407
|
-
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
408
|
-
|
|
409
|
-
# Initialize DDA variables
|
|
410
|
-
t_max_x = 1e30
|
|
411
|
-
t_max_y = 1e30
|
|
412
|
-
t_max_z = 1e30
|
|
413
|
-
t_delta_x = 1e30
|
|
414
|
-
t_delta_y = 1e30
|
|
415
|
-
t_delta_z = 1e30
|
|
416
|
-
|
|
417
|
-
t = 0.01 # Start offset
|
|
418
|
-
|
|
419
|
-
if ti.abs(ray_dir[0]) > 1e-10:
|
|
420
|
-
if step_x > 0:
|
|
421
|
-
t_max_x = ((ix + 1) * dx - pos_from[0]) / ray_dir[0]
|
|
422
|
-
else:
|
|
423
|
-
t_max_x = (ix * dx - pos_from[0]) / ray_dir[0]
|
|
424
|
-
t_delta_x = ti.abs(dx / ray_dir[0])
|
|
425
|
-
|
|
426
|
-
if ti.abs(ray_dir[1]) > 1e-10:
|
|
427
|
-
if step_y > 0:
|
|
428
|
-
t_max_y = ((iy + 1) * dy - pos_from[1]) / ray_dir[1]
|
|
429
|
-
else:
|
|
430
|
-
t_max_y = (iy * dy - pos_from[1]) / ray_dir[1]
|
|
431
|
-
t_delta_y = ti.abs(dy / ray_dir[1])
|
|
432
|
-
|
|
433
|
-
if ti.abs(ray_dir[2]) > 1e-10:
|
|
434
|
-
if step_z > 0:
|
|
435
|
-
t_max_z = ((iz + 1) * dz - pos_from[2]) / ray_dir[2]
|
|
436
|
-
else:
|
|
437
|
-
t_max_z = (iz * dz - pos_from[2]) / ray_dir[2]
|
|
438
|
-
t_delta_z = ti.abs(dz / ray_dir[2])
|
|
439
|
-
|
|
440
|
-
t_prev = t
|
|
441
|
-
max_steps = nx + ny + nz
|
|
442
|
-
done = 0
|
|
443
|
-
|
|
444
|
-
for _ in range(max_steps):
|
|
445
|
-
if done == 1:
|
|
446
|
-
continue # Skip remaining iterations
|
|
447
|
-
|
|
448
|
-
if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
|
|
449
|
-
done = 1
|
|
450
|
-
continue
|
|
451
|
-
if t > dist: # Reached target
|
|
452
|
-
done = 1
|
|
453
|
-
continue
|
|
454
|
-
|
|
455
|
-
# Check for solid obstruction (but skip first and last cell as they're the surfaces)
|
|
456
|
-
if is_solid[ix, iy, iz] == 1 and t > 0.1 and t < dist - 0.1:
|
|
457
|
-
blocked = 1
|
|
458
|
-
transmissivity = 0.0
|
|
459
|
-
done = 1
|
|
460
|
-
continue
|
|
461
|
-
|
|
462
|
-
# Get step distance
|
|
463
|
-
t_next = t_max_x
|
|
464
|
-
if t_max_y < t_next:
|
|
465
|
-
t_next = t_max_y
|
|
466
|
-
if t_max_z < t_next:
|
|
467
|
-
t_next = t_max_z
|
|
468
|
-
|
|
469
|
-
# Limit to target distance
|
|
470
|
-
t_next = ti.min(t_next, dist)
|
|
471
|
-
|
|
472
|
-
# Path length through this cell
|
|
473
|
-
path_len = t_next - t_prev
|
|
474
|
-
|
|
475
|
-
# Accumulate absorption from LAD
|
|
476
|
-
cell_lad = lad[ix, iy, iz]
|
|
477
|
-
if cell_lad > 0.0:
|
|
478
|
-
# Beer-Lambert: T = exp(-ext_coef * LAD * path)
|
|
479
|
-
transmissivity *= ti.exp(-ext_coef * cell_lad * path_len)
|
|
480
|
-
|
|
481
|
-
t_prev = t_next
|
|
482
|
-
|
|
483
|
-
# Step to next voxel
|
|
484
|
-
if t_max_x < t_max_y and t_max_x < t_max_z:
|
|
485
|
-
t = t_max_x
|
|
486
|
-
ix += step_x
|
|
487
|
-
t_max_x += t_delta_x
|
|
488
|
-
elif t_max_y < t_max_z:
|
|
489
|
-
t = t_max_y
|
|
490
|
-
iy += step_y
|
|
491
|
-
t_max_y += t_delta_y
|
|
492
|
-
else:
|
|
493
|
-
t = t_max_z
|
|
494
|
-
iz += step_z
|
|
495
|
-
t_max_z += t_delta_z
|
|
496
|
-
|
|
497
|
-
return transmissivity, blocked
|
|
17
|
+
# Import shared ray tracing functions from parent module
|
|
18
|
+
from ..raytracing import (
|
|
19
|
+
ray_aabb_intersect,
|
|
20
|
+
ray_voxel_first_hit,
|
|
21
|
+
ray_canopy_absorption,
|
|
22
|
+
ray_voxel_transmissivity,
|
|
23
|
+
ray_trace_to_target,
|
|
24
|
+
ray_point_to_point_transmissivity,
|
|
25
|
+
sample_hemisphere_direction,
|
|
26
|
+
hemisphere_solid_angle,
|
|
27
|
+
)
|
|
498
28
|
|
|
499
29
|
|
|
500
30
|
@ti.data_oriented
|
|
@@ -638,49 +168,15 @@ class RayTracer:
|
|
|
638
168
|
shadow_factor[i] = 1.0 - trans
|
|
639
169
|
|
|
640
170
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
Unit direction vector
|
|
654
|
-
"""
|
|
655
|
-
PI = 3.14159265359
|
|
656
|
-
|
|
657
|
-
# Elevation angle (from zenith)
|
|
658
|
-
elev = (i_elev + 0.5) * (PI / 2.0) / n_elev
|
|
659
|
-
|
|
660
|
-
# Azimuth angle
|
|
661
|
-
azim = (i_azim + 0.5) * (2.0 * PI) / n_azim
|
|
662
|
-
|
|
663
|
-
# Convert to Cartesian (z up)
|
|
664
|
-
sin_elev = ti.sin(elev)
|
|
665
|
-
cos_elev = ti.cos(elev)
|
|
666
|
-
|
|
667
|
-
x = sin_elev * ti.sin(azim)
|
|
668
|
-
y = sin_elev * ti.cos(azim)
|
|
669
|
-
z = cos_elev
|
|
670
|
-
|
|
671
|
-
return Vector3(x, y, z)
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
@ti.func
|
|
675
|
-
def hemisphere_solid_angle(i_elev: ti.i32, n_azim: ti.i32, n_elev: ti.i32) -> ti.f32:
|
|
676
|
-
"""
|
|
677
|
-
Calculate solid angle for a hemisphere segment.
|
|
678
|
-
"""
|
|
679
|
-
PI = 3.14159265359
|
|
680
|
-
|
|
681
|
-
elev_low = i_elev * (PI / 2.0) / n_elev
|
|
682
|
-
elev_high = (i_elev + 1) * (PI / 2.0) / n_elev
|
|
683
|
-
|
|
684
|
-
d_omega = (2.0 * PI / n_azim) * (ti.cos(elev_low) - ti.cos(elev_high))
|
|
685
|
-
|
|
686
|
-
return d_omega
|
|
171
|
+
# Re-export all symbols for backward compatibility
|
|
172
|
+
__all__ = [
|
|
173
|
+
'RayTracer',
|
|
174
|
+
'ray_aabb_intersect',
|
|
175
|
+
'ray_voxel_first_hit',
|
|
176
|
+
'ray_canopy_absorption',
|
|
177
|
+
'ray_voxel_transmissivity',
|
|
178
|
+
'ray_trace_to_target',
|
|
179
|
+
'ray_point_to_point_transmissivity',
|
|
180
|
+
'sample_hemisphere_direction',
|
|
181
|
+
'hemisphere_solid_angle',
|
|
182
|
+
]
|