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,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,36 @@
1
+ """
2
+ Shared domain definition for simulator_gpu.
3
+
4
+ This module re-exports the Domain class from solar.domain for backward compatibility.
5
+ The main implementation is in simulator_gpu.solar.domain which includes:
6
+ - Domain class with full grid, terrain, building, and vegetation support
7
+ - Surfaces class for radiation calculations
8
+ - Surface extraction utilities
9
+ """
10
+
11
+ # Re-export from solar.domain (the main implementation)
12
+ from .solar.domain import (
13
+ Domain,
14
+ Surfaces,
15
+ extract_surfaces_from_domain,
16
+ IUP,
17
+ IDOWN,
18
+ INORTH,
19
+ ISOUTH,
20
+ IEAST,
21
+ IWEST,
22
+ DIR_NORMALS,
23
+ )
24
+
25
+ __all__ = [
26
+ 'Domain',
27
+ 'Surfaces',
28
+ 'extract_surfaces_from_domain',
29
+ 'IUP',
30
+ 'IDOWN',
31
+ 'INORTH',
32
+ 'ISOUTH',
33
+ 'IEAST',
34
+ 'IWEST',
35
+ 'DIR_NORMALS',
36
+ ]
@@ -0,0 +1,154 @@
1
+ """
2
+ Taichi initialization for simulator_gpu.
3
+
4
+ This module provides centralized Taichi initialization to ensure ti.init()
5
+ is called before any Taichi fields or kernels are used.
6
+ """
7
+
8
+ import taichi as ti
9
+ import os
10
+ import warnings
11
+ from typing import Optional
12
+
13
+ # Track initialization state
14
+ _TAICHI_INITIALIZED = False
15
+
16
+
17
+ def init_taichi(
18
+ arch: Optional[str] = None,
19
+ default_fp: type = ti.f32,
20
+ default_ip: type = ti.i32,
21
+ debug: bool = False,
22
+ suppress_fp16_warnings: bool = True,
23
+ **kwargs
24
+ ) -> bool:
25
+ """
26
+ Initialize Taichi runtime if not already initialized.
27
+
28
+ This function is idempotent - calling it multiple times is safe.
29
+ The first call will initialize Taichi, subsequent calls will be no-ops.
30
+
31
+ Args:
32
+ arch: Architecture to use. Options:
33
+ - None (default): Auto-detect best available (GPU preferred)
34
+ - 'gpu': Use GPU (CUDA, Vulkan, Metal, etc.)
35
+ - 'cuda': Use CUDA specifically
36
+ - 'vulkan': Use Vulkan
37
+ - 'metal': Use Metal (macOS)
38
+ - 'cpu': Use CPU
39
+ default_fp: Default floating point type (ti.f32 or ti.f64)
40
+ default_ip: Default integer type (ti.i32 or ti.i64)
41
+ debug: Enable debug mode for better error messages
42
+ suppress_fp16_warnings: Suppress Taichi's fp16 precision loss warnings
43
+ (default: True). These warnings occur when using fp16 intermediate
44
+ buffers for memory bandwidth optimization.
45
+ **kwargs: Additional arguments passed to ti.init()
46
+
47
+ Returns:
48
+ True if initialization was performed, False if already initialized.
49
+ """
50
+ global _TAICHI_INITIALIZED
51
+
52
+ if _TAICHI_INITIALIZED:
53
+ return False
54
+
55
+ # Suppress fp16 precision warnings if requested
56
+ # These occur when assigning f32 values to f16 fields, which is intentional
57
+ # for memory bandwidth optimization in intermediate buffers
58
+ if suppress_fp16_warnings:
59
+ # Filter Taichi's precision loss warnings via Python warnings module
60
+ warnings.filterwarnings(
61
+ 'ignore',
62
+ message='.*Assign a value with precision.*',
63
+ category=UserWarning
64
+ )
65
+ warnings.filterwarnings(
66
+ 'ignore',
67
+ message='.*Atomic add may lose precision.*',
68
+ category=UserWarning
69
+ )
70
+ # Also set Taichi log level to ERROR to suppress warnings from Taichi's internal logging
71
+ # This is needed because Taichi uses its own logging system for some warnings
72
+ if 'log_level' not in kwargs:
73
+ kwargs['log_level'] = ti.ERROR
74
+
75
+ # Determine architecture
76
+ if arch is None:
77
+ # Auto-detect: prefer GPU, fall back to CPU
78
+ ti_arch = ti.gpu
79
+ elif arch == 'gpu':
80
+ ti_arch = ti.gpu
81
+ elif arch == 'cuda':
82
+ ti_arch = ti.cuda
83
+ elif arch == 'vulkan':
84
+ ti_arch = ti.vulkan
85
+ elif arch == 'metal':
86
+ ti_arch = ti.metal
87
+ elif arch == 'cpu':
88
+ ti_arch = ti.cpu
89
+ else:
90
+ raise ValueError(f"Unknown architecture: {arch}")
91
+
92
+ # Check environment variable for override
93
+ env_arch = os.environ.get('TAICHI_ARCH', '').lower()
94
+ if env_arch == 'cpu':
95
+ ti_arch = ti.cpu
96
+ elif env_arch == 'cuda':
97
+ ti_arch = ti.cuda
98
+ elif env_arch == 'vulkan':
99
+ ti_arch = ti.vulkan
100
+ elif env_arch == 'gpu':
101
+ ti_arch = ti.gpu
102
+
103
+ # Initialize Taichi
104
+ ti.init(
105
+ arch=ti_arch,
106
+ default_fp=default_fp,
107
+ default_ip=default_ip,
108
+ debug=debug,
109
+ **kwargs
110
+ )
111
+
112
+ _TAICHI_INITIALIZED = True
113
+ return True
114
+
115
+
116
+ def ensure_initialized():
117
+ """
118
+ Ensure Taichi is initialized with default settings.
119
+
120
+ This is a convenience function for lazy initialization.
121
+ Call this before any Taichi operations if you're not sure
122
+ whether init_taichi() has been called.
123
+ """
124
+ if not _TAICHI_INITIALIZED:
125
+ init_taichi()
126
+
127
+
128
+ def is_initialized() -> bool:
129
+ """Check if Taichi has been initialized."""
130
+ return _TAICHI_INITIALIZED
131
+
132
+
133
+ def reset():
134
+ """
135
+ Reset initialization state.
136
+
137
+ Note: This does NOT reset Taichi itself (which cannot be reset once initialized).
138
+ This only resets the tracking flag for testing purposes.
139
+ """
140
+ global _TAICHI_INITIALIZED
141
+ _TAICHI_INITIALIZED = False
142
+
143
+
144
+ # Convenience: expose ti for direct access
145
+ taichi = ti
146
+
147
+ __all__ = [
148
+ 'init_taichi',
149
+ 'ensure_initialized',
150
+ 'is_initialized',
151
+ 'reset',
152
+ 'taichi',
153
+ 'ti',
154
+ ]