voxcity 1.0.2__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/downloader/ocean.py +559 -0
- voxcity/generator/api.py +6 -0
- voxcity/generator/grids.py +45 -32
- voxcity/generator/pipeline.py +327 -27
- voxcity/geoprocessor/draw.py +14 -8
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/landcover.py +173 -49
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/models.py +2 -0
- 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/visualizer/renderer.py +2 -1
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/METADATA +16 -53
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/RECORD +50 -15
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.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
|