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.
Files changed (81) hide show
  1. voxcity/__init__.py +14 -14
  2. voxcity/downloader/ocean.py +559 -0
  3. voxcity/exporter/__init__.py +12 -12
  4. voxcity/exporter/cityles.py +633 -633
  5. voxcity/exporter/envimet.py +733 -728
  6. voxcity/exporter/magicavoxel.py +333 -333
  7. voxcity/exporter/netcdf.py +238 -238
  8. voxcity/exporter/obj.py +1480 -1480
  9. voxcity/generator/__init__.py +47 -44
  10. voxcity/generator/api.py +727 -675
  11. voxcity/generator/grids.py +394 -379
  12. voxcity/generator/io.py +94 -94
  13. voxcity/generator/pipeline.py +582 -282
  14. voxcity/generator/update.py +429 -0
  15. voxcity/generator/voxelizer.py +18 -6
  16. voxcity/geoprocessor/__init__.py +75 -75
  17. voxcity/geoprocessor/draw.py +1494 -1219
  18. voxcity/geoprocessor/merge_utils.py +91 -91
  19. voxcity/geoprocessor/mesh.py +806 -806
  20. voxcity/geoprocessor/network.py +708 -708
  21. voxcity/geoprocessor/raster/__init__.py +2 -0
  22. voxcity/geoprocessor/raster/buildings.py +435 -428
  23. voxcity/geoprocessor/raster/core.py +31 -0
  24. voxcity/geoprocessor/raster/export.py +93 -93
  25. voxcity/geoprocessor/raster/landcover.py +178 -51
  26. voxcity/geoprocessor/raster/raster.py +1 -1
  27. voxcity/geoprocessor/utils.py +824 -824
  28. voxcity/models.py +115 -113
  29. voxcity/simulator/solar/__init__.py +66 -43
  30. voxcity/simulator/solar/integration.py +336 -336
  31. voxcity/simulator/solar/sky.py +668 -0
  32. voxcity/simulator/solar/temporal.py +792 -434
  33. voxcity/simulator_gpu/__init__.py +115 -0
  34. voxcity/simulator_gpu/common/__init__.py +9 -0
  35. voxcity/simulator_gpu/common/geometry.py +11 -0
  36. voxcity/simulator_gpu/core.py +322 -0
  37. voxcity/simulator_gpu/domain.py +262 -0
  38. voxcity/simulator_gpu/environment.yml +11 -0
  39. voxcity/simulator_gpu/init_taichi.py +154 -0
  40. voxcity/simulator_gpu/integration.py +15 -0
  41. voxcity/simulator_gpu/kernels.py +56 -0
  42. voxcity/simulator_gpu/radiation.py +28 -0
  43. voxcity/simulator_gpu/raytracing.py +623 -0
  44. voxcity/simulator_gpu/sky.py +9 -0
  45. voxcity/simulator_gpu/solar/__init__.py +178 -0
  46. voxcity/simulator_gpu/solar/core.py +66 -0
  47. voxcity/simulator_gpu/solar/csf.py +1249 -0
  48. voxcity/simulator_gpu/solar/domain.py +561 -0
  49. voxcity/simulator_gpu/solar/epw.py +421 -0
  50. voxcity/simulator_gpu/solar/integration.py +2953 -0
  51. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  52. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  53. voxcity/simulator_gpu/solar/reflection.py +533 -0
  54. voxcity/simulator_gpu/solar/sky.py +907 -0
  55. voxcity/simulator_gpu/solar/solar.py +337 -0
  56. voxcity/simulator_gpu/solar/svf.py +446 -0
  57. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  58. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  59. voxcity/simulator_gpu/temporal.py +13 -0
  60. voxcity/simulator_gpu/utils.py +25 -0
  61. voxcity/simulator_gpu/view.py +32 -0
  62. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  63. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  64. voxcity/simulator_gpu/visibility/integration.py +808 -0
  65. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  66. voxcity/simulator_gpu/visibility/view.py +944 -0
  67. voxcity/utils/__init__.py +11 -0
  68. voxcity/utils/classes.py +194 -0
  69. voxcity/utils/lc.py +80 -39
  70. voxcity/utils/shape.py +230 -0
  71. voxcity/visualizer/__init__.py +24 -24
  72. voxcity/visualizer/builder.py +43 -43
  73. voxcity/visualizer/grids.py +141 -141
  74. voxcity/visualizer/maps.py +187 -187
  75. voxcity/visualizer/renderer.py +1146 -928
  76. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
  77. voxcity-1.0.13.dist-info/RECORD +116 -0
  78. voxcity-0.7.0.dist-info/RECORD +0 -77
  79. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  80. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,623 @@
1
+ """
2
+ Shared ray tracing module for simulator_gpu.
3
+
4
+ Implements GPU-accelerated ray tracing through a 3D voxel grid
5
+ for both solar radiation and view analysis.
6
+ Uses 3D-DDA (Digital Differential Analyzer) for voxel traversal.
7
+
8
+ Key features:
9
+ - Beer-Lambert law for canopy: trans = exp(-ext_coef * LAD * path_length)
10
+ - Solid obstacles block rays completely (trans = 0)
11
+ - Tree canopy attenuates rays based on LAD and path length
12
+ """
13
+
14
+ import taichi as ti
15
+ import math
16
+ from typing import Tuple, Optional
17
+
18
+ from .core import Vector3, Point3, EXT_COEF
19
+
20
+
21
+ @ti.func
22
+ def ray_aabb_intersect(
23
+ ray_origin: Vector3,
24
+ ray_dir: Vector3,
25
+ box_min: Vector3,
26
+ box_max: Vector3,
27
+ t_min: ti.f32,
28
+ t_max: ti.f32
29
+ ):
30
+ """
31
+ Ray-AABB intersection using slab method.
32
+
33
+ Args:
34
+ ray_origin: Ray origin point
35
+ ray_dir: Ray direction (normalized)
36
+ box_min: AABB minimum corner
37
+ box_max: AABB maximum corner
38
+ t_min: Minimum t value
39
+ t_max: Maximum t value
40
+
41
+ Returns:
42
+ Tuple of (hit, t_enter, t_exit)
43
+ """
44
+ t_enter = t_min
45
+ t_exit = t_max
46
+ hit = 1
47
+
48
+ for i in ti.static(range(3)):
49
+ if ti.abs(ray_dir[i]) < 1e-10:
50
+ # Ray parallel to slab
51
+ if ray_origin[i] < box_min[i] or ray_origin[i] > box_max[i]:
52
+ hit = 0
53
+ else:
54
+ inv_d = 1.0 / ray_dir[i]
55
+ t1 = (box_min[i] - ray_origin[i]) * inv_d
56
+ t2 = (box_max[i] - ray_origin[i]) * inv_d
57
+
58
+ if t1 > t2:
59
+ t1, t2 = t2, t1
60
+
61
+ t_enter = ti.max(t_enter, t1)
62
+ t_exit = ti.min(t_exit, t2)
63
+
64
+ if t_enter > t_exit:
65
+ hit = 0
66
+
67
+ return hit, t_enter, t_exit
68
+
69
+
70
+ @ti.func
71
+ def ray_voxel_first_hit(
72
+ ray_origin: Vector3,
73
+ ray_dir: Vector3,
74
+ is_solid: ti.template(),
75
+ nx: ti.i32,
76
+ ny: ti.i32,
77
+ nz: ti.i32,
78
+ dx: ti.f32,
79
+ dy: ti.f32,
80
+ dz: ti.f32,
81
+ max_dist: ti.f32
82
+ ):
83
+ """
84
+ 3D-DDA ray marching to find first solid voxel hit.
85
+
86
+ Args:
87
+ ray_origin: Ray origin
88
+ ray_dir: Ray direction (normalized)
89
+ is_solid: 3D field of solid cells
90
+ nx, ny, nz: Grid dimensions
91
+ dx, dy, dz: Cell sizes
92
+ max_dist: Maximum ray distance
93
+
94
+ Returns:
95
+ Tuple of (hit, t_hit, ix, iy, iz)
96
+ """
97
+ hit = 0
98
+ t_hit = max_dist
99
+ hit_ix, hit_iy, hit_iz = 0, 0, 0
100
+
101
+ # Find entry into domain
102
+ domain_min = Vector3(0.0, 0.0, 0.0)
103
+ domain_max = Vector3(nx * dx, ny * dy, nz * dz)
104
+
105
+ in_domain, t_enter, t_exit = ray_aabb_intersect(
106
+ ray_origin, ray_dir, domain_min, domain_max, 0.0, max_dist
107
+ )
108
+
109
+ if in_domain == 1:
110
+ # Start position (slightly inside domain)
111
+ t = t_enter + 1e-5
112
+ pos = ray_origin + ray_dir * t
113
+
114
+ # Current voxel indices
115
+ ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
116
+ iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
117
+ iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
118
+
119
+ # Clamp to valid range
120
+ ix = ti.max(0, ti.min(nx - 1, ix))
121
+ iy = ti.max(0, ti.min(ny - 1, iy))
122
+ iz = ti.max(0, ti.min(nz - 1, iz))
123
+
124
+ # Step directions
125
+ step_x = 1 if ray_dir[0] >= 0 else -1
126
+ step_y = 1 if ray_dir[1] >= 0 else -1
127
+ step_z = 1 if ray_dir[2] >= 0 else -1
128
+
129
+ # Initialize DDA variables
130
+ t_max_x = 1e30
131
+ t_max_y = 1e30
132
+ t_max_z = 1e30
133
+ t_delta_x = 1e30
134
+ t_delta_y = 1e30
135
+ t_delta_z = 1e30
136
+
137
+ # t values for next boundary crossing
138
+ if ti.abs(ray_dir[0]) > 1e-10:
139
+ if step_x > 0:
140
+ t_max_x = ((ix + 1) * dx - pos[0]) / ray_dir[0] + t
141
+ else:
142
+ t_max_x = (ix * dx - pos[0]) / ray_dir[0] + t
143
+ t_delta_x = ti.abs(dx / ray_dir[0])
144
+
145
+ if ti.abs(ray_dir[1]) > 1e-10:
146
+ if step_y > 0:
147
+ t_max_y = ((iy + 1) * dy - pos[1]) / ray_dir[1] + t
148
+ else:
149
+ t_max_y = (iy * dy - pos[1]) / ray_dir[1] + t
150
+ t_delta_y = ti.abs(dy / ray_dir[1])
151
+
152
+ if ti.abs(ray_dir[2]) > 1e-10:
153
+ if step_z > 0:
154
+ t_max_z = ((iz + 1) * dz - pos[2]) / ray_dir[2] + t
155
+ else:
156
+ t_max_z = (iz * dz - pos[2]) / ray_dir[2] + t
157
+ t_delta_z = ti.abs(dz / ray_dir[2])
158
+
159
+ # 3D-DDA traversal - optimized with done flag to reduce branch divergence
160
+ max_steps = nx + ny + nz
161
+ done = 0
162
+
163
+ for _ in range(max_steps):
164
+ if done == 0:
165
+ # Bounds check - exit if outside domain
166
+ if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
167
+ done = 1
168
+ elif t > t_exit:
169
+ done = 1
170
+ # Check current voxel for solid hit
171
+ elif is_solid[ix, iy, iz] == 1:
172
+ hit = 1
173
+ t_hit = t
174
+ hit_ix = ix
175
+ hit_iy = iy
176
+ hit_iz = iz
177
+ done = 1
178
+ else:
179
+ # Step to next voxel using branchless min selection
180
+ if t_max_x < t_max_y and t_max_x < t_max_z:
181
+ t = t_max_x
182
+ ix += step_x
183
+ t_max_x += t_delta_x
184
+ elif t_max_y < t_max_z:
185
+ t = t_max_y
186
+ iy += step_y
187
+ t_max_y += t_delta_y
188
+ else:
189
+ t = t_max_z
190
+ iz += step_z
191
+ t_max_z += t_delta_z
192
+
193
+ return hit, t_hit, hit_ix, hit_iy, hit_iz
194
+
195
+
196
+ @ti.func
197
+ def ray_voxel_transmissivity(
198
+ ray_origin: Vector3,
199
+ ray_dir: Vector3,
200
+ is_solid: ti.template(),
201
+ is_tree: ti.template(),
202
+ nx: ti.i32,
203
+ ny: ti.i32,
204
+ nz: ti.i32,
205
+ dx: ti.f32,
206
+ dy: ti.f32,
207
+ dz: ti.f32,
208
+ max_dist: ti.f32,
209
+ tree_k: ti.f32,
210
+ tree_lad: ti.f32
211
+ ):
212
+ """
213
+ 3D-DDA ray marching with tree canopy transmissivity calculation.
214
+
215
+ Args:
216
+ ray_origin: Ray origin
217
+ ray_dir: Ray direction (normalized)
218
+ is_solid: 3D field of solid cells (buildings, ground)
219
+ is_tree: 3D field of tree cells
220
+ nx, ny, nz: Grid dimensions
221
+ dx, dy, dz: Cell sizes
222
+ max_dist: Maximum ray distance
223
+ tree_k: Tree extinction coefficient
224
+ tree_lad: Leaf area density
225
+
226
+ Returns:
227
+ Tuple of (blocked_by_solid, transmissivity)
228
+ - blocked_by_solid: 1 if ray hit solid, 0 otherwise
229
+ - transmissivity: 0-1 fraction of light that gets through trees
230
+ """
231
+ blocked = 0
232
+ transmissivity = 1.0
233
+
234
+ # Find entry into domain
235
+ domain_min = Vector3(0.0, 0.0, 0.0)
236
+ domain_max = Vector3(nx * dx, ny * dy, nz * dz)
237
+
238
+ in_domain, t_enter, t_exit = ray_aabb_intersect(
239
+ ray_origin, ray_dir, domain_min, domain_max, 0.0, max_dist
240
+ )
241
+
242
+ if in_domain == 1:
243
+ t = t_enter + 1e-5
244
+ pos = ray_origin + ray_dir * t
245
+
246
+ ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
247
+ iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
248
+ iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
249
+
250
+ ix = ti.max(0, ti.min(nx - 1, ix))
251
+ iy = ti.max(0, ti.min(ny - 1, iy))
252
+ iz = ti.max(0, ti.min(nz - 1, iz))
253
+
254
+ step_x = 1 if ray_dir[0] >= 0 else -1
255
+ step_y = 1 if ray_dir[1] >= 0 else -1
256
+ step_z = 1 if ray_dir[2] >= 0 else -1
257
+
258
+ t_max_x = 1e30
259
+ t_max_y = 1e30
260
+ t_max_z = 1e30
261
+ t_delta_x = 1e30
262
+ t_delta_y = 1e30
263
+ t_delta_z = 1e30
264
+
265
+ if ti.abs(ray_dir[0]) > 1e-10:
266
+ if step_x > 0:
267
+ t_max_x = ((ix + 1) * dx - pos[0]) / ray_dir[0] + t
268
+ else:
269
+ t_max_x = (ix * dx - pos[0]) / ray_dir[0] + t
270
+ t_delta_x = ti.abs(dx / ray_dir[0])
271
+
272
+ if ti.abs(ray_dir[1]) > 1e-10:
273
+ if step_y > 0:
274
+ t_max_y = ((iy + 1) * dy - pos[1]) / ray_dir[1] + t
275
+ else:
276
+ t_max_y = (iy * dy - pos[1]) / ray_dir[1] + t
277
+ t_delta_y = ti.abs(dy / ray_dir[1])
278
+
279
+ if ti.abs(ray_dir[2]) > 1e-10:
280
+ if step_z > 0:
281
+ t_max_z = ((iz + 1) * dz - pos[2]) / ray_dir[2] + t
282
+ else:
283
+ t_max_z = (iz * dz - pos[2]) / ray_dir[2] + t
284
+ t_delta_z = ti.abs(dz / ray_dir[2])
285
+
286
+ t_prev = t
287
+ max_steps = nx + ny + nz
288
+ done = 0
289
+
290
+ for _ in range(max_steps):
291
+ if done == 0:
292
+ if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
293
+ done = 1
294
+ elif t > t_exit:
295
+ done = 1
296
+ elif is_solid[ix, iy, iz] == 1:
297
+ blocked = 1
298
+ transmissivity = 0.0
299
+ done = 1
300
+ else:
301
+ # Get step distance
302
+ t_next = ti.min(t_max_x, ti.min(t_max_y, t_max_z))
303
+
304
+ # Path length through this cell
305
+ path_len = t_next - t_prev
306
+
307
+ # Accumulate absorption from tree canopy
308
+ if is_tree[ix, iy, iz] == 1:
309
+ # Beer-Lambert: T = exp(-k * LAD * path)
310
+ segment_trans = ti.exp(-tree_k * tree_lad * path_len)
311
+ transmissivity *= segment_trans
312
+
313
+ # Early termination if transmissivity is negligible
314
+ if transmissivity < 0.01:
315
+ done = 1
316
+
317
+ t_prev = t_next
318
+
319
+ # Step to next voxel
320
+ if t_max_x < t_max_y and t_max_x < t_max_z:
321
+ t = t_max_x
322
+ ix += step_x
323
+ t_max_x += t_delta_x
324
+ elif t_max_y < t_max_z:
325
+ t = t_max_y
326
+ iy += step_y
327
+ t_max_y += t_delta_y
328
+ else:
329
+ t = t_max_z
330
+ iz += step_z
331
+ t_max_z += t_delta_z
332
+
333
+ return blocked, transmissivity
334
+
335
+
336
+ @ti.func
337
+ def ray_canopy_absorption(
338
+ ray_origin: Vector3,
339
+ ray_dir: Vector3,
340
+ lad: ti.template(),
341
+ is_solid: ti.template(),
342
+ nx: ti.i32,
343
+ ny: ti.i32,
344
+ nz: ti.i32,
345
+ dx: ti.f32,
346
+ dy: ti.f32,
347
+ dz: ti.f32,
348
+ max_dist: ti.f32,
349
+ ext_coef: ti.f32
350
+ ):
351
+ """
352
+ Trace ray through canopy computing Beer-Lambert absorption.
353
+
354
+ Args:
355
+ ray_origin: Ray origin
356
+ ray_dir: Ray direction (normalized)
357
+ lad: 3D field of Leaf Area Density
358
+ is_solid: 3D field of solid cells (buildings/terrain)
359
+ nx, ny, nz: Grid dimensions
360
+ dx, dy, dz: Cell sizes
361
+ max_dist: Maximum ray distance
362
+ ext_coef: Extinction coefficient
363
+
364
+ Returns:
365
+ Tuple of (transmissivity, path_length_through_canopy)
366
+ """
367
+ transmissivity = 1.0
368
+ total_lad_path = 0.0
369
+
370
+ # Find entry into domain
371
+ domain_min = Vector3(0.0, 0.0, 0.0)
372
+ domain_max = Vector3(nx * dx, ny * dy, nz * dz)
373
+
374
+ in_domain, t_enter, t_exit = ray_aabb_intersect(
375
+ ray_origin, ray_dir, domain_min, domain_max, 0.0, max_dist
376
+ )
377
+
378
+ if in_domain == 1:
379
+ t = t_enter + 1e-5
380
+ pos = ray_origin + ray_dir * t
381
+
382
+ ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
383
+ iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
384
+ iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
385
+
386
+ ix = ti.max(0, ti.min(nx - 1, ix))
387
+ iy = ti.max(0, ti.min(ny - 1, iy))
388
+ iz = ti.max(0, ti.min(nz - 1, iz))
389
+
390
+ step_x = 1 if ray_dir[0] >= 0 else -1
391
+ step_y = 1 if ray_dir[1] >= 0 else -1
392
+ step_z = 1 if ray_dir[2] >= 0 else -1
393
+
394
+ t_max_x = 1e30
395
+ t_max_y = 1e30
396
+ t_max_z = 1e30
397
+ t_delta_x = 1e30
398
+ t_delta_y = 1e30
399
+ t_delta_z = 1e30
400
+
401
+ if ti.abs(ray_dir[0]) > 1e-10:
402
+ if step_x > 0:
403
+ t_max_x = ((ix + 1) * dx - pos[0]) / ray_dir[0] + t
404
+ else:
405
+ t_max_x = (ix * dx - pos[0]) / ray_dir[0] + t
406
+ t_delta_x = ti.abs(dx / ray_dir[0])
407
+
408
+ if ti.abs(ray_dir[1]) > 1e-10:
409
+ if step_y > 0:
410
+ t_max_y = ((iy + 1) * dy - pos[1]) / ray_dir[1] + t
411
+ else:
412
+ t_max_y = (iy * dy - pos[1]) / ray_dir[1] + t
413
+ t_delta_y = ti.abs(dy / ray_dir[1])
414
+
415
+ if ti.abs(ray_dir[2]) > 1e-10:
416
+ if step_z > 0:
417
+ t_max_z = ((iz + 1) * dz - pos[2]) / ray_dir[2] + t
418
+ else:
419
+ t_max_z = (iz * dz - pos[2]) / ray_dir[2] + t
420
+ t_delta_z = ti.abs(dz / ray_dir[2])
421
+
422
+ t_prev = t
423
+ max_steps = nx + ny + nz
424
+ done = 0
425
+
426
+ for _ in range(max_steps):
427
+ if done == 0:
428
+ if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
429
+ done = 1
430
+ elif t > t_exit:
431
+ done = 1
432
+ elif is_solid[ix, iy, iz] == 1:
433
+ transmissivity = 0.0
434
+ done = 1
435
+ else:
436
+ # Get step distance
437
+ t_next = ti.min(t_max_x, ti.min(t_max_y, t_max_z))
438
+
439
+ # Path length through this cell
440
+ path_len = t_next - t_prev
441
+
442
+ # Accumulate absorption from LAD
443
+ cell_lad = lad[ix, iy, iz]
444
+ if cell_lad > 0.0:
445
+ lad_path = cell_lad * path_len
446
+ total_lad_path += lad_path
447
+ # Beer-Lambert: T = exp(-ext_coef * LAD * path)
448
+ transmissivity *= ti.exp(-ext_coef * lad_path)
449
+
450
+ t_prev = t_next
451
+
452
+ # Step to next voxel
453
+ if t_max_x < t_max_y and t_max_x < t_max_z:
454
+ t = t_max_x
455
+ ix += step_x
456
+ t_max_x += t_delta_x
457
+ elif t_max_y < t_max_z:
458
+ t = t_max_y
459
+ iy += step_y
460
+ t_max_y += t_delta_y
461
+ else:
462
+ t = t_max_z
463
+ iz += step_z
464
+ t_max_z += t_delta_z
465
+
466
+ return transmissivity, total_lad_path
467
+
468
+
469
+ @ti.func
470
+ def ray_trace_to_target(
471
+ origin: Vector3,
472
+ target: Vector3,
473
+ is_solid: ti.template(),
474
+ is_tree: ti.template(),
475
+ nx: ti.i32,
476
+ ny: ti.i32,
477
+ nz: ti.i32,
478
+ dx: ti.f32,
479
+ dy: ti.f32,
480
+ dz: ti.f32,
481
+ tree_att: ti.f32,
482
+ att_cutoff: ti.f32
483
+ ):
484
+ """
485
+ Trace ray from origin to target, checking for visibility.
486
+
487
+ Args:
488
+ origin: Start position (in voxel coordinates)
489
+ target: End position (in voxel coordinates)
490
+ is_solid: 3D field of solid cells
491
+ is_tree: 3D field of tree cells
492
+ nx, ny, nz: Grid dimensions
493
+ dx, dy, dz: Cell sizes (typically all 1.0 for voxel coords)
494
+ tree_att: Attenuation factor per voxel for trees
495
+ att_cutoff: Minimum transmissivity before considering blocked
496
+
497
+ Returns:
498
+ 1 if target is visible, 0 otherwise
499
+ """
500
+ diff = target - origin
501
+ dist = diff.norm()
502
+
503
+ if dist < 0.01:
504
+ return 1
505
+
506
+ ray_dir = diff / dist
507
+
508
+ x, y, z = origin[0] + 0.5, origin[1] + 0.5, origin[2] + 0.5
509
+ i = ti.cast(ti.floor(origin[0]), ti.i32)
510
+ j = ti.cast(ti.floor(origin[1]), ti.i32)
511
+ k = ti.cast(ti.floor(origin[2]), ti.i32)
512
+
513
+ ti_x = ti.cast(ti.floor(target[0]), ti.i32)
514
+ tj_y = ti.cast(ti.floor(target[1]), ti.i32)
515
+ tk_z = ti.cast(ti.floor(target[2]), ti.i32)
516
+
517
+ step_x = 1 if ray_dir[0] >= 0 else -1
518
+ step_y = 1 if ray_dir[1] >= 0 else -1
519
+ step_z = 1 if ray_dir[2] >= 0 else -1
520
+
521
+ BIG = 1e30
522
+ t_max_x, t_max_y, t_max_z = BIG, BIG, BIG
523
+ t_delta_x, t_delta_y, t_delta_z = BIG, BIG, BIG
524
+
525
+ if ray_dir[0] != 0.0:
526
+ t_max_x = (((i + (1 if step_x > 0 else 0)) - x) / ray_dir[0])
527
+ t_delta_x = ti.abs(1.0 / ray_dir[0])
528
+ if ray_dir[1] != 0.0:
529
+ t_max_y = (((j + (1 if step_y > 0 else 0)) - y) / ray_dir[1])
530
+ t_delta_y = ti.abs(1.0 / ray_dir[1])
531
+ if ray_dir[2] != 0.0:
532
+ t_max_z = (((k + (1 if step_z > 0 else 0)) - z) / ray_dir[2])
533
+ t_delta_z = ti.abs(1.0 / ray_dir[2])
534
+
535
+ T = 1.0
536
+ visible = 1
537
+ max_steps = nx + ny + nz
538
+ done = 0
539
+
540
+ for _ in range(max_steps):
541
+ if done == 0:
542
+ if i < 0 or i >= nx or j < 0 or j >= ny or k < 0 or k >= nz:
543
+ visible = 0
544
+ done = 1
545
+ elif is_solid[i, j, k] == 1:
546
+ visible = 0
547
+ done = 1
548
+ elif is_tree[i, j, k] == 1:
549
+ T *= tree_att
550
+ if T < att_cutoff:
551
+ visible = 0
552
+ done = 1
553
+
554
+ if done == 0:
555
+ # Check if we've reached the target
556
+ if i == ti_x and j == tj_y and k == tk_z:
557
+ done = 1
558
+ else:
559
+ # Step to next voxel
560
+ if t_max_x < t_max_y:
561
+ if t_max_x < t_max_z:
562
+ t_max_x += t_delta_x
563
+ i += step_x
564
+ else:
565
+ t_max_z += t_delta_z
566
+ k += step_z
567
+ else:
568
+ if t_max_y < t_max_z:
569
+ t_max_y += t_delta_y
570
+ j += step_y
571
+ else:
572
+ t_max_z += t_delta_z
573
+ k += step_z
574
+
575
+ return visible
576
+
577
+
578
+ @ti.func
579
+ def sample_hemisphere_direction(i_azim: ti.i32, i_elev: ti.i32, n_azim: ti.i32, n_elev: ti.i32) -> Vector3:
580
+ """
581
+ Generate a direction on the upper hemisphere.
582
+
583
+ Args:
584
+ i_azim: Azimuthal index (0 to n_azim-1)
585
+ i_elev: Elevation index (0 to n_elev-1)
586
+ n_azim: Number of azimuthal divisions
587
+ n_elev: Number of elevation divisions
588
+
589
+ Returns:
590
+ Unit direction vector
591
+ """
592
+ PI = 3.14159265359
593
+
594
+ # Elevation angle (from zenith)
595
+ elev = (i_elev + 0.5) * (PI / 2.0) / n_elev
596
+
597
+ # Azimuth angle
598
+ azim = (i_azim + 0.5) * (2.0 * PI) / n_azim
599
+
600
+ # Convert to Cartesian (z up)
601
+ sin_elev = ti.sin(elev)
602
+ cos_elev = ti.cos(elev)
603
+
604
+ x = sin_elev * ti.sin(azim)
605
+ y = sin_elev * ti.cos(azim)
606
+ z = cos_elev
607
+
608
+ return Vector3(x, y, z)
609
+
610
+
611
+ @ti.func
612
+ def hemisphere_solid_angle(i_elev: ti.i32, n_azim: ti.i32, n_elev: ti.i32) -> ti.f32:
613
+ """
614
+ Calculate solid angle for a hemisphere segment.
615
+ """
616
+ PI = 3.14159265359
617
+
618
+ elev_low = i_elev * (PI / 2.0) / n_elev
619
+ elev_high = (i_elev + 1) * (PI / 2.0) / n_elev
620
+
621
+ d_omega = (2.0 * PI / n_azim) * (ti.cos(elev_low) - ti.cos(elev_high))
622
+
623
+ return d_omega
@@ -0,0 +1,9 @@
1
+ """VoxCity-style `sky` module (toplevel) for compatibility.
2
+
3
+ VoxCity exposes sky patch utilities under `voxcity.simulator.solar.sky`, but the
4
+ flattened `voxcity.simulator` namespace often ends up with a `sky` attribute.
5
+
6
+ This module forwards to `simulator_gpu.solar.sky`.
7
+ """
8
+
9
+ from .solar.sky import * # noqa: F401,F403