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,561 @@
|
|
|
1
|
+
"""Domain definition for palm-solar.
|
|
2
|
+
|
|
3
|
+
Represents the 3D computational domain with:
|
|
4
|
+
- Grid cells (dx, dy, dz spacing)
|
|
5
|
+
- Topography (terrain height)
|
|
6
|
+
- Building geometry (3D obstacles)
|
|
7
|
+
- Plant canopy (Leaf Area Density - LAD)
|
|
8
|
+
- Surface properties (albedo)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import taichi as ti
|
|
12
|
+
import numpy as np
|
|
13
|
+
from typing import Tuple, Optional, Union
|
|
14
|
+
from .core import Vector3, Point3, EXT_COEF
|
|
15
|
+
from ..init_taichi import ensure_initialized
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Surface direction indices (matching PALM convention)
|
|
19
|
+
IUP = 0 # Upward facing (horizontal roof/ground)
|
|
20
|
+
IDOWN = 1 # Downward facing
|
|
21
|
+
INORTH = 2 # North facing (positive y)
|
|
22
|
+
ISOUTH = 3 # South facing (negative y)
|
|
23
|
+
IEAST = 4 # East facing (positive x)
|
|
24
|
+
IWEST = 5 # West facing (negative x)
|
|
25
|
+
|
|
26
|
+
# Direction normal vectors (x, y, z)
|
|
27
|
+
DIR_NORMALS = {
|
|
28
|
+
IUP: (0.0, 0.0, 1.0),
|
|
29
|
+
IDOWN: (0.0, 0.0, -1.0),
|
|
30
|
+
INORTH: (0.0, 1.0, 0.0),
|
|
31
|
+
ISOUTH: (0.0, -1.0, 0.0),
|
|
32
|
+
IEAST: (1.0, 0.0, 0.0),
|
|
33
|
+
IWEST: (-1.0, 0.0, 0.0),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@ti.data_oriented
|
|
38
|
+
class Domain:
|
|
39
|
+
"""
|
|
40
|
+
3D computational domain for solar radiation simulation.
|
|
41
|
+
|
|
42
|
+
The domain uses a regular grid with:
|
|
43
|
+
- x: West to East
|
|
44
|
+
- y: South to North
|
|
45
|
+
- z: Ground to Sky
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
nx, ny, nz: Number of grid cells in each direction
|
|
49
|
+
dx, dy, dz: Grid spacing in meters
|
|
50
|
+
origin: (x, y, z) coordinates of domain origin
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
nx: int,
|
|
56
|
+
ny: int,
|
|
57
|
+
nz: int,
|
|
58
|
+
dx: float = 1.0,
|
|
59
|
+
dy: float = 1.0,
|
|
60
|
+
dz: float = 1.0,
|
|
61
|
+
origin: Tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
62
|
+
origin_lat: Optional[float] = None,
|
|
63
|
+
origin_lon: Optional[float] = None
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Initialize the domain.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
nx, ny, nz: Grid dimensions
|
|
70
|
+
dx, dy, dz: Grid spacing (m)
|
|
71
|
+
origin: Domain origin coordinates
|
|
72
|
+
origin_lat: Latitude for solar calculations (degrees)
|
|
73
|
+
origin_lon: Longitude for solar calculations (degrees)
|
|
74
|
+
"""
|
|
75
|
+
# Ensure Taichi is initialized before creating any fields
|
|
76
|
+
ensure_initialized()
|
|
77
|
+
|
|
78
|
+
self.nx = nx
|
|
79
|
+
self.ny = ny
|
|
80
|
+
self.nz = nz
|
|
81
|
+
self.dx = dx
|
|
82
|
+
self.dy = dy
|
|
83
|
+
self.dz = dz
|
|
84
|
+
self.origin = origin
|
|
85
|
+
self.origin_lat = origin_lat if origin_lat is not None else 0.0
|
|
86
|
+
self.origin_lon = origin_lon if origin_lon is not None else 0.0
|
|
87
|
+
|
|
88
|
+
# Domain bounds
|
|
89
|
+
self.x_min = origin[0]
|
|
90
|
+
self.x_max = origin[0] + nx * dx
|
|
91
|
+
self.y_min = origin[1]
|
|
92
|
+
self.y_max = origin[1] + ny * dy
|
|
93
|
+
self.z_min = origin[2]
|
|
94
|
+
self.z_max = origin[2] + nz * dz
|
|
95
|
+
|
|
96
|
+
# Grid cell volume
|
|
97
|
+
self.cell_volume = dx * dy * dz
|
|
98
|
+
|
|
99
|
+
# Topography: terrain height at each (i, j) column
|
|
100
|
+
# Value is the k-index of the topmost solid cell
|
|
101
|
+
self.topo_top = ti.field(dtype=ti.i32, shape=(nx, ny))
|
|
102
|
+
|
|
103
|
+
# Building mask: 1 if cell is solid (building), 0 if air
|
|
104
|
+
self.is_solid = ti.field(dtype=ti.i32, shape=(nx, ny, nz))
|
|
105
|
+
|
|
106
|
+
# Tree mask: 1 if cell is tree canopy, 0 otherwise (for view calculations)
|
|
107
|
+
self.is_tree = ti.field(dtype=ti.i32, shape=(nx, ny, nz))
|
|
108
|
+
|
|
109
|
+
# Leaf Area Density (m^2/m^3) for plant canopy
|
|
110
|
+
self.lad = ti.field(dtype=ti.f32, shape=(nx, ny, nz))
|
|
111
|
+
|
|
112
|
+
# Plant canopy top index for each column
|
|
113
|
+
self.plant_top = ti.field(dtype=ti.i32, shape=(nx, ny))
|
|
114
|
+
|
|
115
|
+
# Surface count
|
|
116
|
+
self.n_surfaces = ti.field(dtype=ti.i32, shape=())
|
|
117
|
+
|
|
118
|
+
# Initialize arrays
|
|
119
|
+
self._init_arrays()
|
|
120
|
+
|
|
121
|
+
@ti.kernel
|
|
122
|
+
def _init_arrays(self):
|
|
123
|
+
"""Initialize all arrays to default values."""
|
|
124
|
+
for i, j in self.topo_top:
|
|
125
|
+
self.topo_top[i, j] = 0
|
|
126
|
+
self.plant_top[i, j] = 0
|
|
127
|
+
|
|
128
|
+
for i, j, k in self.is_solid:
|
|
129
|
+
self.is_solid[i, j, k] = 0
|
|
130
|
+
self.is_tree[i, j, k] = 0
|
|
131
|
+
self.lad[i, j, k] = 0.0
|
|
132
|
+
|
|
133
|
+
def set_flat_terrain(self, height: float = 0.0):
|
|
134
|
+
"""Set flat terrain at given height."""
|
|
135
|
+
k_top = int(height / self.dz)
|
|
136
|
+
self._set_flat_terrain_kernel(k_top)
|
|
137
|
+
|
|
138
|
+
# Alias for backwards compatibility
|
|
139
|
+
def initialize_terrain(self, height: float = 0.0):
|
|
140
|
+
"""Alias for set_flat_terrain."""
|
|
141
|
+
self.set_flat_terrain(height)
|
|
142
|
+
|
|
143
|
+
@ti.kernel
|
|
144
|
+
def _set_flat_terrain_kernel(self, k_top: ti.i32):
|
|
145
|
+
for i, j in self.topo_top:
|
|
146
|
+
self.topo_top[i, j] = k_top
|
|
147
|
+
for k in range(k_top + 1):
|
|
148
|
+
self.is_solid[i, j, k] = 1
|
|
149
|
+
|
|
150
|
+
def set_terrain_from_array(self, terrain_height: np.ndarray):
|
|
151
|
+
"""
|
|
152
|
+
Set terrain from 2D numpy array of heights.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
terrain_height: 2D array (nx, ny) of terrain heights in meters
|
|
156
|
+
"""
|
|
157
|
+
terrain_k = (terrain_height / self.dz).astype(np.int32)
|
|
158
|
+
self._set_terrain_kernel(terrain_k)
|
|
159
|
+
|
|
160
|
+
@ti.kernel
|
|
161
|
+
def _set_terrain_kernel(self, terrain_k: ti.types.ndarray()):
|
|
162
|
+
for i, j in self.topo_top:
|
|
163
|
+
k_top = terrain_k[i, j]
|
|
164
|
+
self.topo_top[i, j] = k_top
|
|
165
|
+
for k in range(self.nz):
|
|
166
|
+
if k <= k_top:
|
|
167
|
+
self.is_solid[i, j, k] = 1
|
|
168
|
+
else:
|
|
169
|
+
self.is_solid[i, j, k] = 0
|
|
170
|
+
|
|
171
|
+
def add_building(
|
|
172
|
+
self,
|
|
173
|
+
x_range: Optional[Tuple[int, int]] = None,
|
|
174
|
+
y_range: Optional[Tuple[int, int]] = None,
|
|
175
|
+
z_range: Optional[Tuple[int, int]] = None,
|
|
176
|
+
*,
|
|
177
|
+
x_start: Optional[int] = None,
|
|
178
|
+
x_end: Optional[int] = None,
|
|
179
|
+
y_start: Optional[int] = None,
|
|
180
|
+
y_end: Optional[int] = None,
|
|
181
|
+
height: Optional[float] = None
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
Add a rectangular building to the domain.
|
|
185
|
+
|
|
186
|
+
Can be called with either range tuples or individual parameters:
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
x_range: (i_start, i_end) grid indices
|
|
190
|
+
y_range: (j_start, j_end) grid indices
|
|
191
|
+
z_range: (k_start, k_end) grid indices
|
|
192
|
+
|
|
193
|
+
Or with keyword arguments:
|
|
194
|
+
x_start, x_end: X grid indices
|
|
195
|
+
y_start, y_end: Y grid indices
|
|
196
|
+
height: Building height in meters (z_range computed from this)
|
|
197
|
+
"""
|
|
198
|
+
# Handle convenience parameters
|
|
199
|
+
if x_start is not None and x_end is not None:
|
|
200
|
+
x_range = (x_start, x_end)
|
|
201
|
+
if y_start is not None and y_end is not None:
|
|
202
|
+
y_range = (y_start, y_end)
|
|
203
|
+
if height is not None and z_range is None:
|
|
204
|
+
k_top = int(height / self.dz) + 1
|
|
205
|
+
z_range = (0, k_top)
|
|
206
|
+
|
|
207
|
+
if x_range is None or y_range is None or z_range is None:
|
|
208
|
+
raise ValueError("Must provide either range tuples or individual parameters")
|
|
209
|
+
|
|
210
|
+
self._add_building_kernel(
|
|
211
|
+
x_range[0], x_range[1],
|
|
212
|
+
y_range[0], y_range[1],
|
|
213
|
+
z_range[0], z_range[1]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@ti.kernel
|
|
217
|
+
def _add_building_kernel(
|
|
218
|
+
self,
|
|
219
|
+
i0: ti.i32, i1: ti.i32,
|
|
220
|
+
j0: ti.i32, j1: ti.i32,
|
|
221
|
+
k0: ti.i32, k1: ti.i32
|
|
222
|
+
):
|
|
223
|
+
for i in range(i0, i1):
|
|
224
|
+
for j in range(j0, j1):
|
|
225
|
+
for k in range(k0, k1):
|
|
226
|
+
if 0 <= i < self.nx and 0 <= j < self.ny and 0 <= k < self.nz:
|
|
227
|
+
self.is_solid[i, j, k] = 1
|
|
228
|
+
if k > self.topo_top[i, j]:
|
|
229
|
+
self.topo_top[i, j] = k
|
|
230
|
+
|
|
231
|
+
def set_lad_from_array(self, lad_array: np.ndarray):
|
|
232
|
+
"""
|
|
233
|
+
Set Leaf Area Density from 3D numpy array.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
lad_array: 3D array (nx, ny, nz) of LAD values (m^2/m^3)
|
|
237
|
+
"""
|
|
238
|
+
self._set_lad_kernel(lad_array)
|
|
239
|
+
self._update_plant_top()
|
|
240
|
+
|
|
241
|
+
@ti.kernel
|
|
242
|
+
def _set_lad_kernel(self, lad_array: ti.types.ndarray()):
|
|
243
|
+
for i, j, k in self.lad:
|
|
244
|
+
self.lad[i, j, k] = lad_array[i, j, k]
|
|
245
|
+
|
|
246
|
+
@ti.kernel
|
|
247
|
+
def _update_plant_top(self):
|
|
248
|
+
"""Update plant canopy top index for each column."""
|
|
249
|
+
for i, j in self.plant_top:
|
|
250
|
+
max_k = 0
|
|
251
|
+
# Taichi doesn't support 3-arg range, so iterate forward and track highest
|
|
252
|
+
for k in range(self.nz):
|
|
253
|
+
if self.lad[i, j, k] > 0.0:
|
|
254
|
+
max_k = k
|
|
255
|
+
self.plant_top[i, j] = max_k
|
|
256
|
+
|
|
257
|
+
def add_tree(
|
|
258
|
+
self,
|
|
259
|
+
center: Optional[Tuple[float, float]] = None,
|
|
260
|
+
height: Optional[float] = None,
|
|
261
|
+
crown_radius: Optional[float] = None,
|
|
262
|
+
crown_height: Optional[float] = None,
|
|
263
|
+
trunk_height: Optional[float] = None,
|
|
264
|
+
max_lad: float = 1.0,
|
|
265
|
+
*,
|
|
266
|
+
center_x: Optional[float] = None,
|
|
267
|
+
center_y: Optional[float] = None,
|
|
268
|
+
lad: Optional[float] = None
|
|
269
|
+
):
|
|
270
|
+
"""
|
|
271
|
+
Add a simple tree with cylindrical trunk and spherical crown.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
center: (x, y) position in meters
|
|
275
|
+
height: Total tree height in meters (optional, computed from crown+trunk)
|
|
276
|
+
crown_radius: Radius of crown in meters
|
|
277
|
+
crown_height: Height of crown sphere in meters
|
|
278
|
+
trunk_height: Height of trunk (no leaves) in meters
|
|
279
|
+
max_lad: Maximum LAD at crown center
|
|
280
|
+
|
|
281
|
+
Or with keyword arguments:
|
|
282
|
+
center_x, center_y: Position in meters
|
|
283
|
+
lad: Alias for max_lad
|
|
284
|
+
"""
|
|
285
|
+
# Handle convenience parameters
|
|
286
|
+
if center_x is not None and center_y is not None:
|
|
287
|
+
center = (center_x, center_y)
|
|
288
|
+
if lad is not None:
|
|
289
|
+
max_lad = lad
|
|
290
|
+
|
|
291
|
+
if center is None or crown_radius is None or crown_height is None or trunk_height is None:
|
|
292
|
+
raise ValueError("Must provide center, crown_radius, crown_height, and trunk_height")
|
|
293
|
+
# Convert to grid indices
|
|
294
|
+
ci = int((center[0] - self.origin[0]) / self.dx)
|
|
295
|
+
cj = int((center[1] - self.origin[1]) / self.dy)
|
|
296
|
+
crown_center_k = int((trunk_height + crown_height / 2) / self.dz)
|
|
297
|
+
|
|
298
|
+
ri = int(crown_radius / self.dx) + 1
|
|
299
|
+
rj = int(crown_radius / self.dy) + 1
|
|
300
|
+
rk = int(crown_height / 2 / self.dz) + 1
|
|
301
|
+
|
|
302
|
+
self._add_tree_kernel(ci, cj, crown_center_k, ri, rj, rk,
|
|
303
|
+
crown_radius, crown_height / 2, max_lad)
|
|
304
|
+
self._update_plant_top()
|
|
305
|
+
|
|
306
|
+
@ti.kernel
|
|
307
|
+
def _add_tree_kernel(
|
|
308
|
+
self,
|
|
309
|
+
ci: ti.i32, cj: ti.i32, ck: ti.i32,
|
|
310
|
+
ri: ti.i32, rj: ti.i32, rk: ti.i32,
|
|
311
|
+
rx: ti.f32, rz: ti.f32, max_lad: ti.f32
|
|
312
|
+
):
|
|
313
|
+
for di in range(-ri, ri + 1):
|
|
314
|
+
for dj in range(-rj, rj + 1):
|
|
315
|
+
for dk in range(-rk, rk + 1):
|
|
316
|
+
i = ci + di
|
|
317
|
+
j = cj + dj
|
|
318
|
+
k = ck + dk
|
|
319
|
+
|
|
320
|
+
if 0 <= i < self.nx and 0 <= j < self.ny and 0 <= k < self.nz:
|
|
321
|
+
# Ellipsoid distance
|
|
322
|
+
dx_norm = (di * self.dx) / rx
|
|
323
|
+
dy_norm = (dj * self.dy) / rx
|
|
324
|
+
dz_norm = (dk * self.dz) / rz
|
|
325
|
+
dist = ti.sqrt(dx_norm**2 + dy_norm**2 + dz_norm**2)
|
|
326
|
+
|
|
327
|
+
if dist <= 1.0:
|
|
328
|
+
# LAD decreases from center
|
|
329
|
+
lad_val = max_lad * (1.0 - dist**2)
|
|
330
|
+
if lad_val > self.lad[i, j, k]:
|
|
331
|
+
self.lad[i, j, k] = lad_val
|
|
332
|
+
# Mark as tree voxel
|
|
333
|
+
self.is_tree[i, j, k] = 1
|
|
334
|
+
|
|
335
|
+
@ti.func
|
|
336
|
+
def get_cell_indices(self, point: Point3) -> ti.math.ivec3:
|
|
337
|
+
"""Get grid cell indices for a point."""
|
|
338
|
+
i = ti.cast((point.x - self.origin[0]) / self.dx, ti.i32)
|
|
339
|
+
j = ti.cast((point.y - self.origin[1]) / self.dy, ti.i32)
|
|
340
|
+
k = ti.cast((point.z - self.origin[2]) / self.dz, ti.i32)
|
|
341
|
+
return ti.math.ivec3(i, j, k)
|
|
342
|
+
|
|
343
|
+
@ti.func
|
|
344
|
+
def get_cell_center(self, i: ti.i32, j: ti.i32, k: ti.i32) -> Point3:
|
|
345
|
+
"""Get center coordinates of grid cell."""
|
|
346
|
+
x = self.origin[0] + (i + 0.5) * self.dx
|
|
347
|
+
y = self.origin[1] + (j + 0.5) * self.dy
|
|
348
|
+
z = self.origin[2] + (k + 0.5) * self.dz
|
|
349
|
+
return Point3(x, y, z)
|
|
350
|
+
|
|
351
|
+
@ti.func
|
|
352
|
+
def is_inside(self, point: Point3) -> ti.i32:
|
|
353
|
+
"""Check if point is inside domain."""
|
|
354
|
+
inside = 1
|
|
355
|
+
if point.x < self.x_min or point.x > self.x_max:
|
|
356
|
+
inside = 0
|
|
357
|
+
if point.y < self.y_min or point.y > self.y_max:
|
|
358
|
+
inside = 0
|
|
359
|
+
if point.z < self.z_min or point.z > self.z_max:
|
|
360
|
+
inside = 0
|
|
361
|
+
return inside
|
|
362
|
+
|
|
363
|
+
@ti.func
|
|
364
|
+
def is_cell_solid(self, i: ti.i32, j: ti.i32, k: ti.i32) -> ti.i32:
|
|
365
|
+
"""Check if cell is solid (building or terrain)."""
|
|
366
|
+
solid = 0
|
|
367
|
+
if 0 <= i < self.nx and 0 <= j < self.ny and 0 <= k < self.nz:
|
|
368
|
+
solid = self.is_solid[i, j, k]
|
|
369
|
+
return solid
|
|
370
|
+
|
|
371
|
+
def get_max_dist(self) -> float:
|
|
372
|
+
"""Get maximum ray distance (domain diagonal)."""
|
|
373
|
+
import math
|
|
374
|
+
return math.sqrt(
|
|
375
|
+
(self.nx * self.dx)**2 +
|
|
376
|
+
(self.ny * self.dy)**2 +
|
|
377
|
+
(self.nz * self.dz)**2
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@ti.func
|
|
381
|
+
def get_lad(self, i: ti.i32, j: ti.i32, k: ti.i32) -> ti.f32:
|
|
382
|
+
"""Get LAD value at cell."""
|
|
383
|
+
lad_val = 0.0
|
|
384
|
+
if 0 <= i < self.nx and 0 <= j < self.ny and 0 <= k < self.nz:
|
|
385
|
+
lad_val = self.lad[i, j, k]
|
|
386
|
+
return lad_val
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@ti.data_oriented
|
|
390
|
+
class Surfaces:
|
|
391
|
+
"""
|
|
392
|
+
Collection of surface elements for radiation calculations.
|
|
393
|
+
|
|
394
|
+
Each surface has:
|
|
395
|
+
- Position (grid indices i, j, k)
|
|
396
|
+
- Direction (normal direction index)
|
|
397
|
+
- Area
|
|
398
|
+
- Albedo (reflectivity)
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
def __init__(self, max_surfaces: int):
|
|
402
|
+
"""
|
|
403
|
+
Initialize surface storage.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
max_surfaces: Maximum number of surfaces to allocate
|
|
407
|
+
"""
|
|
408
|
+
self.max_surfaces = max_surfaces
|
|
409
|
+
self.n_surfaces = ti.field(dtype=ti.i32, shape=())
|
|
410
|
+
|
|
411
|
+
# Surface geometry
|
|
412
|
+
self.position = ti.Vector.field(3, dtype=ti.i32, shape=(max_surfaces,)) # i, j, k
|
|
413
|
+
self.direction = ti.field(dtype=ti.i32, shape=(max_surfaces,)) # direction index
|
|
414
|
+
self.center = ti.Vector.field(3, dtype=ti.f32, shape=(max_surfaces,)) # world coords
|
|
415
|
+
self.normal = ti.Vector.field(3, dtype=ti.f32, shape=(max_surfaces,))
|
|
416
|
+
self.area = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
417
|
+
|
|
418
|
+
# Surface properties
|
|
419
|
+
self.albedo = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
420
|
+
|
|
421
|
+
# Radiation fluxes (shortwave only)
|
|
422
|
+
self.sw_in_direct = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
423
|
+
self.sw_in_diffuse = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
424
|
+
self.sw_out = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
425
|
+
|
|
426
|
+
# Sky view factor (total SVF and SVF from urban surfaces only)
|
|
427
|
+
self.svf = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
428
|
+
self.svf_urban = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
429
|
+
|
|
430
|
+
# Shadow factor (0 = fully shadowed, 1 = fully lit)
|
|
431
|
+
self.shadow = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
432
|
+
self.shadow_factor = ti.field(dtype=ti.f32, shape=(max_surfaces,)) # same as shadow, for compatibility
|
|
433
|
+
|
|
434
|
+
# Canopy transmissivity (for direct solar through vegetation)
|
|
435
|
+
self.canopy_transmissivity = ti.field(dtype=ti.f32, shape=(max_surfaces,))
|
|
436
|
+
|
|
437
|
+
self.n_surfaces[None] = 0
|
|
438
|
+
|
|
439
|
+
@ti.func
|
|
440
|
+
def add_surface(
|
|
441
|
+
self,
|
|
442
|
+
i: ti.i32, j: ti.i32, k: ti.i32,
|
|
443
|
+
direction: ti.i32,
|
|
444
|
+
center: Point3,
|
|
445
|
+
normal: Vector3,
|
|
446
|
+
area: ti.f32,
|
|
447
|
+
albedo: ti.f32 = 0.2
|
|
448
|
+
) -> ti.i32:
|
|
449
|
+
"""Add a surface and return its index."""
|
|
450
|
+
idx = ti.atomic_add(self.n_surfaces[None], 1)
|
|
451
|
+
if idx < self.max_surfaces:
|
|
452
|
+
self.position[idx] = ti.math.ivec3(i, j, k)
|
|
453
|
+
self.direction[idx] = direction
|
|
454
|
+
self.center[idx] = center
|
|
455
|
+
self.normal[idx] = normal
|
|
456
|
+
self.area[idx] = area
|
|
457
|
+
self.albedo[idx] = albedo
|
|
458
|
+
self.svf[idx] = 1.0
|
|
459
|
+
self.shadow[idx] = 1.0
|
|
460
|
+
return idx
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def count(self) -> int:
|
|
464
|
+
"""Get current number of surfaces."""
|
|
465
|
+
return self.n_surfaces[None]
|
|
466
|
+
|
|
467
|
+
def get_count(self) -> int:
|
|
468
|
+
"""Get current number of surfaces (alias for count property)."""
|
|
469
|
+
return self.n_surfaces[None]
|
|
470
|
+
|
|
471
|
+
@ti.kernel
|
|
472
|
+
def reset_fluxes(self):
|
|
473
|
+
"""Reset all radiation fluxes to zero."""
|
|
474
|
+
for idx in range(self.n_surfaces[None]):
|
|
475
|
+
self.sw_in_direct[idx] = 0.0
|
|
476
|
+
self.sw_in_diffuse[idx] = 0.0
|
|
477
|
+
self.sw_out[idx] = 0.0
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def extract_surfaces_from_domain(domain: Domain,
|
|
481
|
+
default_albedo: float = 0.2) -> Surfaces:
|
|
482
|
+
"""
|
|
483
|
+
Extract all surface elements from domain geometry.
|
|
484
|
+
|
|
485
|
+
Creates surface elements at all interfaces between solid and air cells.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
domain: The computational domain
|
|
489
|
+
default_albedo: Default surface albedo
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Surfaces object containing all extracted surfaces
|
|
493
|
+
"""
|
|
494
|
+
# Estimate max surfaces (6 faces per building cell, 1 top face per ground cell)
|
|
495
|
+
max_surfaces = domain.nx * domain.ny * 6 # Conservative estimate
|
|
496
|
+
surfaces = Surfaces(max_surfaces)
|
|
497
|
+
|
|
498
|
+
_extract_surfaces_kernel(domain, surfaces, default_albedo)
|
|
499
|
+
|
|
500
|
+
return surfaces
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@ti.kernel
|
|
504
|
+
def _extract_surfaces_kernel(
|
|
505
|
+
domain: ti.template(),
|
|
506
|
+
surfaces: ti.template(),
|
|
507
|
+
default_albedo: ti.f32
|
|
508
|
+
):
|
|
509
|
+
"""Kernel to extract surfaces from domain."""
|
|
510
|
+
dx = domain.dx
|
|
511
|
+
dy = domain.dy
|
|
512
|
+
dz = domain.dz
|
|
513
|
+
|
|
514
|
+
for i, j, k in domain.is_solid:
|
|
515
|
+
if domain.is_solid[i, j, k] == 1:
|
|
516
|
+
# Check each neighbor direction for air interface
|
|
517
|
+
|
|
518
|
+
# Up (z+)
|
|
519
|
+
if k + 1 >= domain.nz or domain.is_solid[i, j, k + 1] == 0:
|
|
520
|
+
center = domain.get_cell_center(i, j, k)
|
|
521
|
+
center.z += dz / 2
|
|
522
|
+
normal = Vector3(0.0, 0.0, 1.0)
|
|
523
|
+
area = dx * dy
|
|
524
|
+
surfaces.add_surface(i, j, k, IUP, center, normal, area,
|
|
525
|
+
default_albedo)
|
|
526
|
+
|
|
527
|
+
# North (y+)
|
|
528
|
+
if j + 1 >= domain.ny or domain.is_solid[i, j + 1, k] == 0:
|
|
529
|
+
center = domain.get_cell_center(i, j, k)
|
|
530
|
+
center.y += dy / 2
|
|
531
|
+
normal = Vector3(0.0, 1.0, 0.0)
|
|
532
|
+
area = dx * dz
|
|
533
|
+
surfaces.add_surface(i, j, k, INORTH, center, normal, area,
|
|
534
|
+
default_albedo)
|
|
535
|
+
|
|
536
|
+
# South (y-)
|
|
537
|
+
if j - 1 < 0 or domain.is_solid[i, j - 1, k] == 0:
|
|
538
|
+
center = domain.get_cell_center(i, j, k)
|
|
539
|
+
center.y -= dy / 2
|
|
540
|
+
normal = Vector3(0.0, -1.0, 0.0)
|
|
541
|
+
area = dx * dz
|
|
542
|
+
surfaces.add_surface(i, j, k, ISOUTH, center, normal, area,
|
|
543
|
+
default_albedo)
|
|
544
|
+
|
|
545
|
+
# East (x+)
|
|
546
|
+
if i + 1 >= domain.nx or domain.is_solid[i + 1, j, k] == 0:
|
|
547
|
+
center = domain.get_cell_center(i, j, k)
|
|
548
|
+
center.x += dx / 2
|
|
549
|
+
normal = Vector3(1.0, 0.0, 0.0)
|
|
550
|
+
area = dy * dz
|
|
551
|
+
surfaces.add_surface(i, j, k, IEAST, center, normal, area,
|
|
552
|
+
default_albedo)
|
|
553
|
+
|
|
554
|
+
# West (x-)
|
|
555
|
+
if i - 1 < 0 or domain.is_solid[i - 1, j, k] == 0:
|
|
556
|
+
center = domain.get_cell_center(i, j, k)
|
|
557
|
+
center.x -= dx / 2
|
|
558
|
+
normal = Vector3(-1.0, 0.0, 0.0)
|
|
559
|
+
area = dy * dz
|
|
560
|
+
surfaces.add_surface(i, j, k, IWEST, center, normal, area,
|
|
561
|
+
default_albedo)
|