voxcity 1.0.2__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.
Files changed (41) hide show
  1. voxcity/downloader/ocean.py +559 -0
  2. voxcity/generator/api.py +6 -0
  3. voxcity/generator/grids.py +45 -32
  4. voxcity/generator/pipeline.py +327 -27
  5. voxcity/geoprocessor/draw.py +14 -8
  6. voxcity/geoprocessor/raster/__init__.py +2 -0
  7. voxcity/geoprocessor/raster/core.py +31 -0
  8. voxcity/geoprocessor/raster/landcover.py +173 -49
  9. voxcity/geoprocessor/raster/raster.py +1 -1
  10. voxcity/models.py +2 -0
  11. voxcity/simulator/solar/__init__.py +13 -0
  12. voxcity/simulator_gpu/__init__.py +90 -0
  13. voxcity/simulator_gpu/core.py +322 -0
  14. voxcity/simulator_gpu/domain.py +36 -0
  15. voxcity/simulator_gpu/init_taichi.py +154 -0
  16. voxcity/simulator_gpu/raytracing.py +776 -0
  17. voxcity/simulator_gpu/solar/__init__.py +222 -0
  18. voxcity/simulator_gpu/solar/core.py +66 -0
  19. voxcity/simulator_gpu/solar/csf.py +1249 -0
  20. voxcity/simulator_gpu/solar/domain.py +618 -0
  21. voxcity/simulator_gpu/solar/epw.py +421 -0
  22. voxcity/simulator_gpu/solar/integration.py +4322 -0
  23. voxcity/simulator_gpu/solar/mask.py +459 -0
  24. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  25. voxcity/simulator_gpu/solar/raytracing.py +182 -0
  26. voxcity/simulator_gpu/solar/reflection.py +533 -0
  27. voxcity/simulator_gpu/solar/sky.py +907 -0
  28. voxcity/simulator_gpu/solar/solar.py +337 -0
  29. voxcity/simulator_gpu/solar/svf.py +446 -0
  30. voxcity/simulator_gpu/solar/volumetric.py +2099 -0
  31. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  32. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  33. voxcity/simulator_gpu/visibility/integration.py +808 -0
  34. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  35. voxcity/simulator_gpu/visibility/view.py +944 -0
  36. voxcity/visualizer/renderer.py +2 -1
  37. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/METADATA +16 -53
  38. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/RECORD +41 -16
  39. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/WHEEL +0 -0
  40. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/AUTHORS.rst +0 -0
  41. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,446 @@
1
+ """
2
+ Sky View Factor (SVF) calculation for palm-solar.
3
+
4
+ Computes the fraction of sky hemisphere visible from each surface element.
5
+ Uses GPU-accelerated ray tracing to sample the hemisphere.
6
+
7
+ PALM Alignment:
8
+ - Uses PALM's vffrac_up formula: (cos(2*elev_low) - cos(2*elev_high)) / (2*n_azim)
9
+ - Default discretization: n_azimuth=80, n_elevation=40 (PALM defaults)
10
+ - svf output is equivalent to PALM's skyvft (transmissivity-weighted sky view factor)
11
+ - svf_urban output is equivalent to PALM's skyvf (urban-only, no canopy)
12
+ - Ray accumulation: SUM(vffrac * trans) matching PALM's methodology
13
+ PALM: skyvft = SUM(ztransp * vffrac, MASK=(itarget < 0))
14
+ """
15
+
16
+ import taichi as ti
17
+ import math
18
+ from typing import Tuple, Optional
19
+
20
+ from .core import Vector3, Point3, PI, TWO_PI
21
+ from .raytracing import ray_voxel_first_hit, ray_canopy_absorption, sample_hemisphere_direction, hemisphere_solid_angle
22
+
23
+
24
+ @ti.data_oriented
25
+ class SVFCalculator:
26
+ """
27
+ GPU-accelerated Sky View Factor calculator.
28
+
29
+ Computes SVF by tracing rays from each surface to the hemisphere.
30
+ SVF = fraction of hemisphere visible from surface.
31
+ """
32
+
33
+ def __init__(self, domain, n_azimuth: int = 80, n_elevation: int = 40):
34
+ """
35
+ Initialize SVF calculator.
36
+
37
+ Args:
38
+ domain: Domain object with grid geometry
39
+ n_azimuth: Number of azimuthal divisions (default 80)
40
+ n_elevation: Number of elevation divisions (default 40)
41
+ """
42
+ self.domain = domain
43
+ self.nx = domain.nx
44
+ self.ny = domain.ny
45
+ self.nz = domain.nz
46
+ self.dx = domain.dx
47
+ self.dy = domain.dy
48
+ self.dz = domain.dz
49
+
50
+ self.n_azimuth = n_azimuth
51
+ self.n_elevation = n_elevation
52
+ self.n_directions = n_azimuth * n_elevation
53
+
54
+ # Maximum ray distance
55
+ self.max_dist = math.sqrt(
56
+ (self.nx * self.dx)**2 +
57
+ (self.ny * self.dy)**2 +
58
+ (self.nz * self.dz)**2
59
+ )
60
+
61
+ # Pre-compute directions and view factor fractions
62
+ # PALM uses separate vffrac_up (for upward surfaces) and vffrac_vert (for vertical surfaces)
63
+ # See radiation_model_mod.f90 lines 12093-12105
64
+ # vffrac_up is pre-computed; vffrac_vert is computed on-the-fly since it depends on surface orientation
65
+ self.directions = ti.Vector.field(3, dtype=ti.f32, shape=(n_azimuth, n_elevation))
66
+ self.solid_angles = ti.field(dtype=ti.f32, shape=(n_azimuth, n_elevation)) # vffrac_up for upward surfaces
67
+ self.total_solid_angle = ti.field(dtype=ti.f32, shape=())
68
+
69
+ self._init_directions()
70
+
71
+ @ti.kernel
72
+ def _init_directions(self):
73
+ """
74
+ Pre-compute hemisphere directions and view factor fractions for upward surfaces.
75
+
76
+ Uses PALM's formulas for view factor fractions (radiation_model_mod.f90 lines 12093-12105):
77
+
78
+ 1. For upward surfaces (vffrac_up, pre-computed here):
79
+ vffrac_up = (cos(2*elev_low) - cos(2*elev_high)) / (2*n_azim)
80
+ This accounts for cosine-weighted projected area on horizontal surface.
81
+
82
+ 2. For vertical surfaces (vffrac_vert, computed on-the-fly in compute_svf):
83
+ vffrac_vert = (sin(az2) - sin(az1)) * elev_terms / (2*pi)
84
+ where elev_terms = elev_high - elev_low + sin(elev_low)*cos(elev_low) - sin(elev_high)*cos(elev_high)
85
+ Azimuth is measured relative to surface normal: az = azim_angle_relative - pi/2
86
+
87
+ Elevation angle is measured from zenith (0 = up, π/2 = horizon).
88
+ The sum of vffrac_up over all rays in the upper hemisphere equals 1.0.
89
+ """
90
+ total_omega = 0.0
91
+ n_azim_f = ti.cast(self.n_azimuth, ti.f32)
92
+ n_elev_f = ti.cast(self.n_elevation, ti.f32)
93
+
94
+ d_azim = TWO_PI / n_azim_f # Azimuth step size
95
+
96
+ for i_azim, i_elev in ti.ndrange(self.n_azimuth, self.n_elevation):
97
+ # Elevation boundaries (from zenith)
98
+ elev_low = ti.cast(i_elev, ti.f32) * (PI / 2.0) / n_elev_f
99
+ elev_high = ti.cast(i_elev + 1, ti.f32) * (PI / 2.0) / n_elev_f
100
+ elev_center = (elev_low + elev_high) / 2.0
101
+
102
+ # Azimuth center
103
+ azim_center = (ti.cast(i_azim, ti.f32) + 0.5) * d_azim
104
+
105
+ # Direction vector (x=East, y=North, z=Up)
106
+ sin_elev = ti.sin(elev_center)
107
+ cos_elev = ti.cos(elev_center)
108
+
109
+ x = sin_elev * ti.sin(azim_center)
110
+ y = sin_elev * ti.cos(azim_center)
111
+ z = cos_elev
112
+
113
+ self.directions[i_azim, i_elev] = Vector3(x, y, z)
114
+
115
+ # View factor fraction for upward-facing surfaces (PALM formula)
116
+ # vffrac_up = (cos(2*elev_low) - cos(2*elev_high)) / (2*n_azim)
117
+ vf_up = (ti.cos(2.0 * elev_low) - ti.cos(2.0 * elev_high)) / (2.0 * n_azim_f)
118
+ self.solid_angles[i_azim, i_elev] = vf_up
119
+ total_omega += vf_up
120
+
121
+ self.total_solid_angle[None] = total_omega
122
+
123
+ @ti.kernel
124
+ def compute_svf(
125
+ self,
126
+ surf_pos: ti.template(),
127
+ surf_dir: ti.template(),
128
+ is_solid: ti.template(),
129
+ n_surf: ti.i32,
130
+ svf: ti.template()
131
+ ):
132
+ """
133
+ Compute Sky View Factor for all surfaces.
134
+
135
+ Uses PALM's methodology (radiation_model_mod.f90):
136
+ - For upward surfaces: vffrac_up = (cos(2*elev_low) - cos(2*elev_high)) / (2*n_azim)
137
+ - For vertical surfaces: vffrac_vert = (sin(az2) - sin(az1)) * elev_terms / (2*pi)
138
+ where az is measured relative to surface normal and elev_terms accounts for
139
+ the elevation integration.
140
+
141
+ Args:
142
+ surf_pos: Surface positions (n_surf, 3)
143
+ surf_dir: Surface directions (n_surf,)
144
+ is_solid: 3D field of solid cells
145
+ n_surf: Number of surfaces
146
+ svf: Output SVF values (n_surf,)
147
+ """
148
+ n_azim_f = ti.cast(self.n_azimuth, ti.f32)
149
+ n_elev_f = ti.cast(self.n_elevation, ti.f32)
150
+ d_azim = TWO_PI / n_azim_f # Azimuth step size
151
+
152
+ for i in range(n_surf):
153
+ pos = Vector3(surf_pos[i][0], surf_pos[i][1], surf_pos[i][2])
154
+ direction = surf_dir[i]
155
+
156
+ # Get surface normal and azimuth of normal (for vertical surfaces)
157
+ normal = Vector3(0.0, 0.0, 0.0)
158
+ normal_azim = 0.0 # Azimuth angle of surface normal (for vertical surfaces)
159
+
160
+ if direction == 0: # Up
161
+ normal = Vector3(0.0, 0.0, 1.0)
162
+ elif direction == 1: # Down
163
+ normal = Vector3(0.0, 0.0, -1.0)
164
+ elif direction == 2: # North (normal points +y)
165
+ normal = Vector3(0.0, 1.0, 0.0)
166
+ normal_azim = 0.0 # North is azimuth 0
167
+ elif direction == 3: # South (normal points -y)
168
+ normal = Vector3(0.0, -1.0, 0.0)
169
+ normal_azim = PI # South is azimuth π
170
+ elif direction == 4: # East (normal points +x)
171
+ normal = Vector3(1.0, 0.0, 0.0)
172
+ normal_azim = PI / 2.0 # East is azimuth π/2
173
+ elif direction == 5: # West (normal points -x)
174
+ normal = Vector3(-1.0, 0.0, 0.0)
175
+ normal_azim = 3.0 * PI / 2.0 # West is azimuth 3π/2
176
+
177
+ visible_vf = 0.0
178
+ total_vf = 0.0
179
+
180
+ # Trace rays to all hemisphere directions
181
+ for i_azim, i_elev in ti.ndrange(self.n_azimuth, self.n_elevation):
182
+ ray_dir = self.directions[i_azim, i_elev]
183
+
184
+ # Check if direction is above surface (dot product with normal > 0)
185
+ cos_angle = ray_dir[0] * normal[0] + ray_dir[1] * normal[1] + ray_dir[2] * normal[2]
186
+
187
+ if cos_angle > 0.001: # Small threshold to avoid numerical issues
188
+ # Compute view factor fraction based on surface orientation
189
+ elev_low = ti.cast(i_elev, ti.f32) * (PI / 2.0) / n_elev_f
190
+ elev_high = ti.cast(i_elev + 1, ti.f32) * (PI / 2.0) / n_elev_f
191
+
192
+ vf_frac = 0.0
193
+ if direction == 0: # Upward surface
194
+ # PALM: vffrac_up = (cos(2*elev_low) - cos(2*elev_high)) / (2*n_azim)
195
+ vf_frac = (ti.cos(2.0 * elev_low) - ti.cos(2.0 * elev_high)) / (2.0 * n_azim_f)
196
+ elif direction == 1: # Downward surface
197
+ # Use same formula as upward (symmetric)
198
+ vf_frac = (ti.cos(2.0 * elev_low) - ti.cos(2.0 * elev_high)) / (2.0 * n_azim_f)
199
+ else:
200
+ # Vertical surfaces: use PALM's vffrac_vert formula
201
+ # Compute azimuth relative to surface normal
202
+ azim_low = ti.cast(i_azim, ti.f32) * d_azim
203
+ azim_high = ti.cast(i_azim + 1, ti.f32) * d_azim
204
+
205
+ # Relative azimuth (measured from surface normal)
206
+ # PALM shifts by -π/2 so that az=0 is at surface normal
207
+ az1_rel = azim_low - normal_azim - PI / 2.0
208
+ az2_rel = azim_high - normal_azim - PI / 2.0
209
+
210
+ # Elevation terms for vertical surface
211
+ # elev_terms = elev_high - elev_low + sin(elev_low)*cos(elev_low) - sin(elev_high)*cos(elev_high)
212
+ elev_terms = (elev_high - elev_low
213
+ + ti.sin(elev_low) * ti.cos(elev_low)
214
+ - ti.sin(elev_high) * ti.cos(elev_high))
215
+
216
+ # vffrac_vert = (sin(az2) - sin(az1)) * elev_terms / (2*π)
217
+ vf_frac = (ti.sin(az2_rel) - ti.sin(az1_rel)) * elev_terms / TWO_PI
218
+
219
+ # Only positive contributions (ray in front of surface)
220
+ if vf_frac < 0.0:
221
+ vf_frac = 0.0
222
+
223
+ total_vf += vf_frac
224
+
225
+ # Trace ray
226
+ hit, _, _, _, _ = ray_voxel_first_hit(
227
+ pos, ray_dir,
228
+ is_solid,
229
+ self.nx, self.ny, self.nz,
230
+ self.dx, self.dy, self.dz,
231
+ self.max_dist
232
+ )
233
+
234
+ if hit == 0:
235
+ visible_vf += vf_frac
236
+
237
+ # For vertical surfaces (direction 2-5), account for ground blocking
238
+ # Directions with z < 0 see ground, not sky
239
+ # We sample hemisphere with z > 0, but for vertical walls the mirrored rays (z < 0)
240
+ # would have same view factor contribution and are ALL blocked by ground
241
+ # This effectively doubles the total and leaves visible unchanged
242
+ if direction >= 2: # Vertical surfaces
243
+ # The lower hemisphere (z < 0) contribution equals upper hemisphere (z > 0) by symmetry
244
+ # All lower hemisphere rays are blocked by ground
245
+ total_vf = total_vf * 2.0
246
+
247
+ # Normalize SVF so that unobstructed surface = 1.0
248
+ if total_vf > 0.001:
249
+ svf[i] = visible_vf / total_vf
250
+ else:
251
+ svf[i] = 1.0
252
+
253
+ @ti.kernel
254
+ def compute_svf_with_canopy(
255
+ self,
256
+ surf_pos: ti.template(),
257
+ surf_dir: ti.template(),
258
+ is_solid: ti.template(),
259
+ lad: ti.template(),
260
+ n_surf: ti.i32,
261
+ ext_coef: ti.f32,
262
+ svf: ti.template(),
263
+ svf_urban: ti.template()
264
+ ):
265
+ """
266
+ Compute SVF including canopy absorption.
267
+
268
+ Uses PALM's methodology (radiation_model_mod.f90):
269
+ - For upward surfaces: vffrac_up = (cos(2*elev_low) - cos(2*elev_high)) / (2*n_azim)
270
+ - For vertical surfaces: vffrac_vert = (sin(az2) - sin(az1)) * elev_terms / (2*pi)
271
+
272
+ Args:
273
+ surf_pos: Surface positions
274
+ surf_dir: Surface directions
275
+ is_solid: 3D solid field
276
+ lad: 3D Leaf Area Density field
277
+ n_surf: Number of surfaces
278
+ ext_coef: Extinction coefficient
279
+ svf: Output SVF with canopy (n_surf,)
280
+ svf_urban: Output SVF without canopy (n_surf,)
281
+ """
282
+ n_azim_f = ti.cast(self.n_azimuth, ti.f32)
283
+ n_elev_f = ti.cast(self.n_elevation, ti.f32)
284
+ d_azim = TWO_PI / n_azim_f
285
+
286
+ for i in range(n_surf):
287
+ pos = Vector3(surf_pos[i][0], surf_pos[i][1], surf_pos[i][2])
288
+ direction = surf_dir[i]
289
+
290
+ # Get surface normal and azimuth of normal (for vertical surfaces)
291
+ normal = Vector3(0.0, 0.0, 0.0)
292
+ normal_azim = 0.0
293
+
294
+ if direction == 0: # Up
295
+ normal = Vector3(0.0, 0.0, 1.0)
296
+ elif direction == 1: # Down
297
+ normal = Vector3(0.0, 0.0, -1.0)
298
+ elif direction == 2: # North
299
+ normal = Vector3(0.0, 1.0, 0.0)
300
+ normal_azim = 0.0
301
+ elif direction == 3: # South
302
+ normal = Vector3(0.0, -1.0, 0.0)
303
+ normal_azim = PI
304
+ elif direction == 4: # East
305
+ normal = Vector3(1.0, 0.0, 0.0)
306
+ normal_azim = PI / 2.0
307
+ elif direction == 5: # West
308
+ normal = Vector3(-1.0, 0.0, 0.0)
309
+ normal_azim = 3.0 * PI / 2.0
310
+
311
+ visible_omega = 0.0
312
+ visible_omega_urban = 0.0
313
+ total_omega = 0.0
314
+
315
+ for i_azim, i_elev in ti.ndrange(self.n_azimuth, self.n_elevation):
316
+ ray_dir = self.directions[i_azim, i_elev]
317
+
318
+ cos_angle = ray_dir[0] * normal[0] + ray_dir[1] * normal[1] + ray_dir[2] * normal[2]
319
+
320
+ if cos_angle > 0.001:
321
+ # Compute view factor fraction based on surface orientation
322
+ elev_low = ti.cast(i_elev, ti.f32) * (PI / 2.0) / n_elev_f
323
+ elev_high = ti.cast(i_elev + 1, ti.f32) * (PI / 2.0) / n_elev_f
324
+
325
+ vf_frac = 0.0
326
+ if direction == 0 or direction == 1: # Upward or Downward surface
327
+ # PALM: vffrac_up
328
+ vf_frac = (ti.cos(2.0 * elev_low) - ti.cos(2.0 * elev_high)) / (2.0 * n_azim_f)
329
+ else:
330
+ # Vertical surfaces: use PALM's vffrac_vert formula
331
+ azim_low = ti.cast(i_azim, ti.f32) * d_azim
332
+ azim_high = ti.cast(i_azim + 1, ti.f32) * d_azim
333
+
334
+ # Relative azimuth (measured from surface normal)
335
+ az1_rel = azim_low - normal_azim - PI / 2.0
336
+ az2_rel = azim_high - normal_azim - PI / 2.0
337
+
338
+ # Elevation terms
339
+ elev_terms = (elev_high - elev_low
340
+ + ti.sin(elev_low) * ti.cos(elev_low)
341
+ - ti.sin(elev_high) * ti.cos(elev_high))
342
+
343
+ # vffrac_vert = (sin(az2) - sin(az1)) * elev_terms / (2*π)
344
+ vf_frac = (ti.sin(az2_rel) - ti.sin(az1_rel)) * elev_terms / TWO_PI
345
+
346
+ if vf_frac < 0.0:
347
+ vf_frac = 0.0
348
+
349
+ total_omega += vf_frac
350
+
351
+ # Trace with canopy absorption
352
+ trans, _ = ray_canopy_absorption(
353
+ pos, ray_dir,
354
+ lad, is_solid,
355
+ self.nx, self.ny, self.nz,
356
+ self.dx, self.dy, self.dz,
357
+ self.max_dist,
358
+ ext_coef
359
+ )
360
+
361
+ # SVF with canopy considers transparency (PALM's skyvft)
362
+ visible_omega += vf_frac * trans
363
+
364
+ # SVF urban (only solid obstacles, PALM's skyvf)
365
+ hit, _, _, _, _ = ray_voxel_first_hit(
366
+ pos, ray_dir,
367
+ is_solid,
368
+ self.nx, self.ny, self.nz,
369
+ self.dx, self.dy, self.dz,
370
+ self.max_dist
371
+ )
372
+ if hit == 0:
373
+ visible_omega_urban += vf_frac
374
+
375
+ # For vertical surfaces (direction 2-5), account for ground blocking
376
+ # Directions with z < 0 see ground, not sky
377
+ # The lower hemisphere has equal total_omega contribution, all blocked by ground
378
+ if direction >= 2: # Vertical surfaces
379
+ total_omega = total_omega * 2.0
380
+
381
+ if total_omega > 1e-10:
382
+ svf[i] = visible_omega / total_omega
383
+ svf_urban[i] = visible_omega_urban / total_omega
384
+ else:
385
+ svf[i] = 0.0
386
+ svf_urban[i] = 0.0
387
+
388
+
389
+ @ti.kernel
390
+ def compute_svf_grid_kernel(
391
+ topo_top: ti.template(),
392
+ is_solid: ti.template(),
393
+ directions: ti.template(),
394
+ solid_angles: ti.template(),
395
+ nx: ti.i32,
396
+ ny: ti.i32,
397
+ nz: ti.i32,
398
+ dx: ti.f32,
399
+ dy: ti.f32,
400
+ dz: ti.f32,
401
+ n_azim: ti.i32,
402
+ n_elev: ti.i32,
403
+ max_dist: ti.f32,
404
+ svf_grid: ti.template()
405
+ ):
406
+ """
407
+ Compute SVF for a 2D grid at terrain level.
408
+
409
+ svf_grid[i, j] = SVF at terrain surface (i, j)
410
+ """
411
+ for i, j in ti.ndrange(nx, ny):
412
+ k = topo_top[i, j]
413
+
414
+ if k < nz:
415
+ pos = Vector3((i + 0.5) * dx, (j + 0.5) * dy, (k + 0.5) * dz)
416
+ normal = Vector3(0.0, 0.0, 1.0)
417
+
418
+ visible_omega = 0.0
419
+ total_omega = 0.0
420
+
421
+ for i_azim, i_elev in ti.ndrange(n_azim, n_elev):
422
+ ray_dir = directions[i_azim, i_elev]
423
+ d_omega = solid_angles[i_azim, i_elev]
424
+
425
+ cos_angle = ray_dir[2] # Normal is (0, 0, 1)
426
+
427
+ if cos_angle > 0:
428
+ total_omega += d_omega * cos_angle
429
+
430
+ hit, _, _, _, _ = ray_voxel_first_hit(
431
+ pos, ray_dir,
432
+ is_solid,
433
+ nx, ny, nz,
434
+ dx, dy, dz,
435
+ max_dist
436
+ )
437
+
438
+ if hit == 0:
439
+ visible_omega += d_omega * cos_angle
440
+
441
+ if total_omega > 1e-10:
442
+ svf_grid[i, j] = visible_omega / total_omega
443
+ else:
444
+ svf_grid[i, j] = 1.0
445
+ else:
446
+ svf_grid[i, j] = 0.0 # Inside terrain