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.
Files changed (50) 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_gpu/__init__.py +115 -0
  12. voxcity/simulator_gpu/common/__init__.py +9 -0
  13. voxcity/simulator_gpu/common/geometry.py +11 -0
  14. voxcity/simulator_gpu/core.py +322 -0
  15. voxcity/simulator_gpu/domain.py +262 -0
  16. voxcity/simulator_gpu/environment.yml +11 -0
  17. voxcity/simulator_gpu/init_taichi.py +154 -0
  18. voxcity/simulator_gpu/integration.py +15 -0
  19. voxcity/simulator_gpu/kernels.py +56 -0
  20. voxcity/simulator_gpu/radiation.py +28 -0
  21. voxcity/simulator_gpu/raytracing.py +623 -0
  22. voxcity/simulator_gpu/sky.py +9 -0
  23. voxcity/simulator_gpu/solar/__init__.py +178 -0
  24. voxcity/simulator_gpu/solar/core.py +66 -0
  25. voxcity/simulator_gpu/solar/csf.py +1249 -0
  26. voxcity/simulator_gpu/solar/domain.py +561 -0
  27. voxcity/simulator_gpu/solar/epw.py +421 -0
  28. voxcity/simulator_gpu/solar/integration.py +2953 -0
  29. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  30. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  31. voxcity/simulator_gpu/solar/reflection.py +533 -0
  32. voxcity/simulator_gpu/solar/sky.py +907 -0
  33. voxcity/simulator_gpu/solar/solar.py +337 -0
  34. voxcity/simulator_gpu/solar/svf.py +446 -0
  35. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  36. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  37. voxcity/simulator_gpu/temporal.py +13 -0
  38. voxcity/simulator_gpu/utils.py +25 -0
  39. voxcity/simulator_gpu/view.py +32 -0
  40. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  41. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  42. voxcity/simulator_gpu/visibility/integration.py +808 -0
  43. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  44. voxcity/simulator_gpu/visibility/view.py +944 -0
  45. voxcity/visualizer/renderer.py +2 -1
  46. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/METADATA +16 -53
  47. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/RECORD +50 -15
  48. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  49. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  50. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,322 @@
1
+ """
2
+ Shared core utilities for simulator_gpu.
3
+
4
+ Vector and ray utilities using Taichi for GPU acceleration.
5
+ Based on ray-tracing-one-weekend-taichi patterns.
6
+
7
+ GPU Optimization Notes:
8
+ - All functions use @ti.func for GPU inlining
9
+ - Branchless operations preferred where possible
10
+ - Memory coalescing friendly access patterns
11
+ - done-flag pattern for early termination (reduces warp divergence)
12
+ """
13
+
14
+ import taichi as ti
15
+ import math
16
+
17
+ # Type aliases for clarity
18
+ Vector3 = ti.math.vec3
19
+ Point3 = ti.math.vec3
20
+ Color3 = ti.math.vec3
21
+
22
+ # Constants - using Taichi static for compile-time optimization
23
+ PI = math.pi
24
+ TWO_PI = 2.0 * math.pi
25
+ HALF_PI = math.pi / 2.0
26
+ DEG_TO_RAD = math.pi / 180.0
27
+ RAD_TO_DEG = 180.0 / math.pi
28
+
29
+ # Solar constant (W/m^2) - matching PALM's solar_constant
30
+ SOLAR_CONSTANT = 1361.0
31
+
32
+ # Default extinction coefficient for vegetation
33
+ # PALM: ext_coef = 0.6_wp (radiation_model_mod.f90 line ~890)
34
+ EXT_COEF = 0.6
35
+
36
+ # Minimum stable cosine of zenith angle
37
+ MIN_STABLE_COSZEN = 0.0262
38
+
39
+ # GPU block size hint for optimal thread occupancy
40
+ GPU_BLOCK_SIZE = 256
41
+
42
+
43
+ @ti.func
44
+ def normalize(v: Vector3) -> Vector3:
45
+ """Normalize a vector."""
46
+ return v / v.norm()
47
+
48
+
49
+ @ti.func
50
+ def normalize_safe(v: Vector3) -> Vector3:
51
+ """Normalize a vector with safety check for zero-length."""
52
+ len_sq = v.dot(v)
53
+ if len_sq > 1e-10:
54
+ return v / ti.sqrt(len_sq)
55
+ return Vector3(0.0, 0.0, 1.0)
56
+
57
+
58
+ @ti.func
59
+ def dot(v1: Vector3, v2: Vector3) -> ti.f32:
60
+ """Dot product of two vectors."""
61
+ return v1.dot(v2)
62
+
63
+
64
+ @ti.func
65
+ def cross(v1: Vector3, v2: Vector3) -> Vector3:
66
+ """Cross product of two vectors."""
67
+ return v1.cross(v2)
68
+
69
+
70
+ @ti.func
71
+ def reflect(v: Vector3, n: Vector3) -> Vector3:
72
+ """Reflect vector v around normal n."""
73
+ return v - 2.0 * v.dot(n) * n
74
+
75
+
76
+ @ti.func
77
+ def ray_at(origin: Point3, direction: Vector3, t: ti.f32) -> Point3:
78
+ """Get point along ray at parameter t."""
79
+ return origin + t * direction
80
+
81
+
82
+ @ti.func
83
+ def length_squared(v: Vector3) -> ti.f32:
84
+ """Compute squared length of vector (avoids sqrt)."""
85
+ return v.dot(v)
86
+
87
+
88
+ @ti.func
89
+ def distance_squared(p1: Point3, p2: Point3) -> ti.f32:
90
+ """Compute squared distance between two points (avoids sqrt)."""
91
+ diff = p2 - p1
92
+ return diff.dot(diff)
93
+
94
+
95
+ @ti.func
96
+ def min3(a: ti.f32, b: ti.f32, c: ti.f32) -> ti.f32:
97
+ """Branchless minimum of three values."""
98
+ return ti.min(a, ti.min(b, c))
99
+
100
+
101
+ @ti.func
102
+ def max3(a: ti.f32, b: ti.f32, c: ti.f32) -> ti.f32:
103
+ """Branchless maximum of three values."""
104
+ return ti.max(a, ti.max(b, c))
105
+
106
+
107
+ @ti.func
108
+ def clamp(x: ti.f32, lo: ti.f32, hi: ti.f32) -> ti.f32:
109
+ """Clamp value to range [lo, hi]."""
110
+ return ti.max(lo, ti.min(hi, x))
111
+
112
+
113
+ @ti.func
114
+ def random_in_unit_sphere() -> Vector3:
115
+ """Generate random point in unit sphere."""
116
+ theta = ti.random() * TWO_PI
117
+ v = ti.random()
118
+ phi = ti.acos(2.0 * v - 1.0)
119
+ r = ti.random() ** (1.0 / 3.0)
120
+ return Vector3(
121
+ r * ti.sin(phi) * ti.cos(theta),
122
+ r * ti.sin(phi) * ti.sin(theta),
123
+ r * ti.cos(phi)
124
+ )
125
+
126
+
127
+ @ti.func
128
+ def random_in_hemisphere(normal: Vector3) -> Vector3:
129
+ """Generate random vector in hemisphere around normal."""
130
+ vec = random_in_unit_sphere()
131
+ if vec.dot(normal) < 0.0:
132
+ vec = -vec
133
+ return vec
134
+
135
+
136
+ @ti.func
137
+ def random_cosine_hemisphere(normal: Vector3) -> Vector3:
138
+ """
139
+ Generate random vector with cosine-weighted distribution in hemisphere.
140
+ Used for diffuse radiation sampling.
141
+ """
142
+ u1 = ti.random()
143
+ u2 = ti.random()
144
+ r = ti.sqrt(u1)
145
+ theta = TWO_PI * u2
146
+
147
+ x = r * ti.cos(theta)
148
+ y = r * ti.sin(theta)
149
+ z = ti.sqrt(1.0 - u1)
150
+
151
+ # Create orthonormal basis around normal
152
+ up = Vector3(0.0, 1.0, 0.0)
153
+ if ti.abs(normal.y) > 0.999:
154
+ up = Vector3(1.0, 0.0, 0.0)
155
+
156
+ tangent = normalize(cross(up, normal))
157
+ bitangent = cross(normal, tangent)
158
+
159
+ return normalize(x * tangent + y * bitangent + z * normal)
160
+
161
+
162
+ @ti.func
163
+ def spherical_to_cartesian(azimuth: ti.f32, elevation: ti.f32) -> Vector3:
164
+ """
165
+ Convert spherical coordinates to Cartesian unit vector.
166
+
167
+ Args:
168
+ azimuth: Angle from north (y-axis), clockwise, in radians
169
+ elevation: Angle from horizontal, in radians (0 = horizontal, pi/2 = zenith)
170
+
171
+ Returns:
172
+ Unit vector (x, y, z) where z is vertical (up)
173
+ """
174
+ cos_elev = ti.cos(elevation)
175
+ sin_elev = ti.sin(elevation)
176
+ cos_azim = ti.cos(azimuth)
177
+ sin_azim = ti.sin(azimuth)
178
+
179
+ # x = east, y = north, z = up
180
+ x = cos_elev * sin_azim
181
+ y = cos_elev * cos_azim
182
+ z = sin_elev
183
+
184
+ return Vector3(x, y, z)
185
+
186
+
187
+ @ti.func
188
+ def cartesian_to_spherical(v: Vector3) -> ti.math.vec2:
189
+ """
190
+ Convert Cartesian unit vector to spherical coordinates.
191
+
192
+ Returns:
193
+ vec2(azimuth, elevation) in radians
194
+ """
195
+ elevation = ti.asin(ti.math.clamp(v.z, -1.0, 1.0))
196
+ azimuth = ti.atan2(v.x, v.y)
197
+ if azimuth < 0.0:
198
+ azimuth += TWO_PI
199
+ return ti.math.vec2(azimuth, elevation)
200
+
201
+
202
+ @ti.func
203
+ def rotate_vector_axis_angle(vec: Vector3, axis: Vector3, angle: ti.f32) -> Vector3:
204
+ """
205
+ Rotate vector around an axis by a given angle using Rodrigues' rotation formula.
206
+
207
+ Args:
208
+ vec: Vector to rotate
209
+ axis: Rotation axis (will be normalized)
210
+ angle: Rotation angle in radians
211
+
212
+ Returns:
213
+ Rotated vector
214
+ """
215
+ axis_len = axis.norm()
216
+ if axis_len < 1e-12:
217
+ return vec
218
+
219
+ k = axis / axis_len
220
+ c = ti.cos(angle)
221
+ s = ti.sin(angle)
222
+
223
+ # Rodrigues' rotation formula: v_rot = v*cos(θ) + (k×v)*sin(θ) + k*(k·v)*(1-cos(θ))
224
+ v_rot = vec * c + cross(k, vec) * s + k * dot(k, vec) * (1.0 - c)
225
+
226
+ return v_rot
227
+
228
+
229
+ @ti.func
230
+ def build_face_basis(normal: Vector3):
231
+ """
232
+ Build orthonormal basis for a face with given normal.
233
+
234
+ Returns:
235
+ Tuple of (tangent, bitangent, normal) vectors
236
+ """
237
+ n_len = normal.norm()
238
+ if n_len < 1e-12:
239
+ return Vector3(1.0, 0.0, 0.0), Vector3(0.0, 1.0, 0.0), Vector3(0.0, 0.0, 1.0)
240
+
241
+ n = normal / n_len
242
+
243
+ # Choose helper vector not parallel to normal
244
+ helper = Vector3(0.0, 0.0, 1.0)
245
+ if ti.abs(n.z) > 0.999:
246
+ helper = Vector3(1.0, 0.0, 0.0)
247
+
248
+ # Compute tangent via cross product
249
+ u = cross(helper, n)
250
+ u_len = u.norm()
251
+ if u_len < 1e-12:
252
+ u = Vector3(1.0, 0.0, 0.0)
253
+ else:
254
+ u = u / u_len
255
+
256
+ # Bitangent
257
+ v = cross(n, u)
258
+
259
+ return u, v, n
260
+
261
+
262
+ @ti.data_oriented
263
+ class Rays:
264
+ """
265
+ Array of rays for batch processing.
266
+ Similar to ray-tracing-one-weekend-taichi but adapted for view/solar tracing.
267
+ """
268
+
269
+ def __init__(self, n_rays: int):
270
+ self.n_rays = n_rays
271
+ self.origin = ti.Vector.field(3, dtype=ti.f32, shape=(n_rays,))
272
+ self.direction = ti.Vector.field(3, dtype=ti.f32, shape=(n_rays,))
273
+ self.transparency = ti.field(dtype=ti.f32, shape=(n_rays,))
274
+ self.active = ti.field(dtype=ti.i32, shape=(n_rays,))
275
+
276
+ @ti.func
277
+ def set(self, idx: ti.i32, origin: Point3, direction: Vector3, transp: ti.f32):
278
+ self.origin[idx] = origin
279
+ self.direction[idx] = direction
280
+ self.transparency[idx] = transp
281
+ self.active[idx] = 1
282
+
283
+ @ti.func
284
+ def get(self, idx: ti.i32):
285
+ return self.origin[idx], self.direction[idx], self.transparency[idx]
286
+
287
+ @ti.func
288
+ def deactivate(self, idx: ti.i32):
289
+ self.active[idx] = 0
290
+
291
+ @ti.func
292
+ def is_active(self, idx: ti.i32) -> ti.i32:
293
+ return self.active[idx]
294
+
295
+
296
+ @ti.data_oriented
297
+ class HitRecord:
298
+ """
299
+ Store ray-surface intersection results.
300
+ """
301
+
302
+ def __init__(self, n_rays: int):
303
+ self.n_rays = n_rays
304
+ self.hit = ti.field(dtype=ti.i32, shape=(n_rays,))
305
+ self.t = ti.field(dtype=ti.f32, shape=(n_rays,))
306
+ self.point = ti.Vector.field(3, dtype=ti.f32, shape=(n_rays,))
307
+ self.normal = ti.Vector.field(3, dtype=ti.f32, shape=(n_rays,))
308
+ self.surface_id = ti.field(dtype=ti.i32, shape=(n_rays,))
309
+
310
+ @ti.func
311
+ def set(self, idx: ti.i32, hit: ti.i32, t: ti.f32, point: Point3,
312
+ normal: Vector3, surface_id: ti.i32):
313
+ self.hit[idx] = hit
314
+ self.t[idx] = t
315
+ self.point[idx] = point
316
+ self.normal[idx] = normal
317
+ self.surface_id[idx] = surface_id
318
+
319
+ @ti.func
320
+ def get(self, idx: ti.i32):
321
+ return (self.hit[idx], self.t[idx], self.point[idx],
322
+ self.normal[idx], self.surface_id[idx])
@@ -0,0 +1,262 @@
1
+ """
2
+ Shared domain definition for simulator_gpu.
3
+
4
+ Represents the 3D computational domain with:
5
+ - Grid cells (dx, dy, dz spacing)
6
+ - Topography (terrain height)
7
+ - Building geometry (3D obstacles)
8
+ - Plant canopy (Leaf Area Density - LAD)
9
+ - Tree mask for view analysis
10
+ """
11
+
12
+ import taichi as ti
13
+ import numpy as np
14
+ from typing import Tuple, Optional, Union
15
+ from .core import Vector3, Point3, EXT_COEF
16
+ from .init_taichi import ensure_initialized
17
+
18
+
19
+ # Surface direction indices (matching PALM convention)
20
+ IUP = 0 # Upward facing (horizontal roof/ground)
21
+ IDOWN = 1 # Downward facing
22
+ INORTH = 2 # North facing (positive y)
23
+ ISOUTH = 3 # South facing (negative y)
24
+ IEAST = 4 # East facing (positive x)
25
+ IWEST = 5 # West facing (negative x)
26
+
27
+ # Direction normal vectors (x, y, z)
28
+ DIR_NORMALS = {
29
+ IUP: (0.0, 0.0, 1.0),
30
+ IDOWN: (0.0, 0.0, -1.0),
31
+ INORTH: (0.0, 1.0, 0.0),
32
+ ISOUTH: (0.0, -1.0, 0.0),
33
+ IEAST: (1.0, 0.0, 0.0),
34
+ IWEST: (-1.0, 0.0, 0.0),
35
+ }
36
+
37
+
38
+ @ti.data_oriented
39
+ class Domain:
40
+ """
41
+ 3D computational domain for simulation.
42
+
43
+ The domain uses a regular grid with:
44
+ - x: West to East
45
+ - y: South to North
46
+ - z: Ground to Sky
47
+
48
+ Attributes:
49
+ nx, ny, nz: Number of grid cells in each direction
50
+ dx, dy, dz: Grid spacing in meters
51
+ origin: (x, y, z) coordinates of domain origin
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ nx: int,
57
+ ny: int,
58
+ nz: int,
59
+ dx: float = 1.0,
60
+ dy: float = 1.0,
61
+ dz: float = 1.0,
62
+ origin: Tuple[float, float, float] = (0.0, 0.0, 0.0),
63
+ origin_lat: Optional[float] = None,
64
+ origin_lon: Optional[float] = None
65
+ ):
66
+ """
67
+ Initialize the domain.
68
+
69
+ Args:
70
+ nx, ny, nz: Grid dimensions
71
+ dx, dy, dz: Grid spacing (m)
72
+ origin: Domain origin coordinates
73
+ origin_lat: Latitude for solar calculations (degrees)
74
+ origin_lon: Longitude for solar calculations (degrees)
75
+ """
76
+ # Ensure Taichi is initialized before creating any fields
77
+ ensure_initialized()
78
+
79
+ self.nx = nx
80
+ self.ny = ny
81
+ self.nz = nz
82
+ self.dx = dx
83
+ self.dy = dy
84
+ self.dz = dz
85
+ self.origin = origin
86
+ self.origin_lat = origin_lat if origin_lat is not None else 0.0
87
+ self.origin_lon = origin_lon if origin_lon is not None else 0.0
88
+
89
+ # Domain bounds
90
+ self.x_min = origin[0]
91
+ self.x_max = origin[0] + nx * dx
92
+ self.y_min = origin[1]
93
+ self.y_max = origin[1] + ny * dy
94
+ self.z_min = origin[2]
95
+ self.z_max = origin[2] + nz * dz
96
+
97
+ # Grid cell volume
98
+ self.cell_volume = dx * dy * dz
99
+
100
+ # Topography: terrain height at each (i, j) column
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
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
+ def initialize_terrain(self, height: float = 0.0):
139
+ """Alias for set_flat_terrain."""
140
+ self.set_flat_terrain(height)
141
+
142
+ @ti.kernel
143
+ def _set_flat_terrain_kernel(self, k_top: ti.i32):
144
+ for i, j in self.topo_top:
145
+ self.topo_top[i, j] = k_top
146
+ for k in range(k_top + 1):
147
+ self.is_solid[i, j, k] = 1
148
+
149
+ def set_terrain_from_array(self, terrain_height: np.ndarray):
150
+ """
151
+ Set terrain from 2D numpy array of heights.
152
+
153
+ Args:
154
+ terrain_height: 2D array (nx, ny) of terrain heights in meters
155
+ """
156
+ terrain_k = (terrain_height / self.dz).astype(np.int32)
157
+ self._set_terrain_kernel(terrain_k)
158
+
159
+ @ti.kernel
160
+ def _set_terrain_kernel(self, terrain_k: ti.types.ndarray()):
161
+ for i, j in self.topo_top:
162
+ k_top = terrain_k[i, j]
163
+ self.topo_top[i, j] = k_top
164
+ for k in range(self.nz):
165
+ if k <= k_top:
166
+ self.is_solid[i, j, k] = 1
167
+ else:
168
+ self.is_solid[i, j, k] = 0
169
+
170
+ def add_building(
171
+ self,
172
+ x_range: Optional[Tuple[int, int]] = None,
173
+ y_range: Optional[Tuple[int, int]] = None,
174
+ z_range: Optional[Tuple[int, int]] = None,
175
+ *,
176
+ x_start: Optional[int] = None,
177
+ x_end: Optional[int] = None,
178
+ y_start: Optional[int] = None,
179
+ y_end: Optional[int] = None,
180
+ height: Optional[float] = None
181
+ ):
182
+ """
183
+ Add a rectangular building to the domain.
184
+ """
185
+ # Handle convenience parameters
186
+ if x_start is not None and x_end is not None:
187
+ x_range = (x_start, x_end)
188
+ if y_start is not None and y_end is not None:
189
+ y_range = (y_start, y_end)
190
+ if height is not None and z_range is None:
191
+ k_top = int(height / self.dz) + 1
192
+ z_range = (0, k_top)
193
+
194
+ if x_range is None or y_range is None or z_range is None:
195
+ raise ValueError("Must provide either range tuples or individual parameters")
196
+
197
+ self._add_building_kernel(x_range[0], x_range[1], y_range[0], y_range[1], z_range[0], z_range[1])
198
+
199
+ @ti.kernel
200
+ def _add_building_kernel(self, i_min: ti.i32, i_max: ti.i32, j_min: ti.i32, j_max: ti.i32, k_min: ti.i32, k_max: ti.i32):
201
+ for i, j, k in ti.ndrange((i_min, i_max), (j_min, j_max), (k_min, k_max)):
202
+ self.is_solid[i, j, k] = 1
203
+
204
+ def add_tree(
205
+ self,
206
+ x_range: Tuple[int, int],
207
+ y_range: Tuple[int, int],
208
+ z_range: Tuple[int, int],
209
+ lad_value: float = 1.0
210
+ ):
211
+ """
212
+ Add a tree canopy region to the domain.
213
+
214
+ Args:
215
+ x_range, y_range, z_range: Grid index ranges
216
+ lad_value: Leaf Area Density value
217
+ """
218
+ self._add_tree_kernel(x_range[0], x_range[1], y_range[0], y_range[1], z_range[0], z_range[1], lad_value)
219
+
220
+ @ti.kernel
221
+ def _add_tree_kernel(self, i_min: ti.i32, i_max: ti.i32, j_min: ti.i32, j_max: ti.i32, k_min: ti.i32, k_max: ti.i32, lad: ti.f32):
222
+ for i, j, k in ti.ndrange((i_min, i_max), (j_min, j_max), (k_min, k_max)):
223
+ self.is_tree[i, j, k] = 1
224
+ self.lad[i, j, k] = lad
225
+
226
+ def set_from_voxel_data(self, voxel_data: np.ndarray, tree_code: int = -2, solid_codes: Optional[list] = None):
227
+ """
228
+ Set domain from a 3D voxel data array.
229
+
230
+ Args:
231
+ voxel_data: 3D numpy array with voxel class codes
232
+ tree_code: Class code for trees (default -2)
233
+ solid_codes: List of codes that are solid (default: all non-zero except tree_code)
234
+ """
235
+ if solid_codes is None:
236
+ # All non-zero codes except tree are solid
237
+ solid_codes = []
238
+
239
+ self._set_from_voxel_data_kernel(voxel_data, tree_code)
240
+
241
+ @ti.kernel
242
+ def _set_from_voxel_data_kernel(self, voxel_data: ti.types.ndarray(), tree_code: ti.i32):
243
+ for i, j, k in ti.ndrange(self.nx, self.ny, self.nz):
244
+ val = voxel_data[i, j, k]
245
+ if val == tree_code:
246
+ self.is_tree[i, j, k] = 1
247
+ self.is_solid[i, j, k] = 0
248
+ elif val != 0:
249
+ self.is_solid[i, j, k] = 1
250
+ self.is_tree[i, j, k] = 0
251
+ else:
252
+ self.is_solid[i, j, k] = 0
253
+ self.is_tree[i, j, k] = 0
254
+
255
+ def get_max_dist(self) -> float:
256
+ """Get maximum ray distance (domain diagonal)."""
257
+ import math
258
+ return math.sqrt(
259
+ (self.nx * self.dx)**2 +
260
+ (self.ny * self.dy)**2 +
261
+ (self.nz * self.dz)**2
262
+ )
@@ -0,0 +1,11 @@
1
+ name: voxcity_gpu
2
+ channels:
3
+ - conda-forge
4
+ - defaults
5
+ dependencies:
6
+ - python=3.12
7
+ - gdal
8
+ - timezonefinder
9
+ - pip
10
+ - pip:
11
+ - voxcity[gpu]