raystrack 1.0.1__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.
raystrack/main.py ADDED
@@ -0,0 +1,2194 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+ import numpy as np
11
+ from numba import cuda
12
+
13
+ from .params import MatrixParams, SkyParams
14
+ from .utils.cpu_trace import (
15
+ bin_tregenza_cpu,
16
+ count_upward_misses_cpu,
17
+ reduce_first_hits,
18
+ trace_cpu_bvh_combined,
19
+ trace_cpu_bvh_firsthit,
20
+ trace_cpu_bvh_hitmask,
21
+ trace_cpu_combined,
22
+ trace_cpu_firsthit,
23
+ trace_cpu_hitmask,
24
+ )
25
+ from .utils.cuda_trace import (
26
+ kernel_accumulate_hits,
27
+ kernel_accumulate_hits_stats,
28
+ kernel_bin_tregenza,
29
+ kernel_build_rays,
30
+ kernel_count_upward_misses,
31
+ kernel_reduce_hits,
32
+ kernel_trace_bvh_combined,
33
+ kernel_trace_bvh_count_upward,
34
+ kernel_trace_bvh_firsthit,
35
+ kernel_trace_bvh_tregenza,
36
+ kernel_trace_combined,
37
+ kernel_trace_count_upward,
38
+ kernel_trace_firsthit,
39
+ kernel_trace_tregenza,
40
+ kernel_zero_i32,
41
+ kernel_zero_i64,
42
+ )
43
+ from .utils.helpers import enforce_reciprocity_and_rowsum as _enforce_reciprocity_and_rowsum
44
+ from .utils.prepared import PreparedEmitter, PreparedSolver
45
+ from .utils.ray_builder import build_rays
46
+
47
+ _LOG_PROC = None
48
+ _BVH_AUTO_THRESHOLD = 512
49
+
50
+
51
+ def _env_int(name: str, default: int, *, minimum: int = 1) -> int:
52
+ raw = os.environ.get(name)
53
+ if raw is None:
54
+ return max(minimum, int(default))
55
+ try:
56
+ return max(minimum, int(raw))
57
+ except Exception:
58
+ return max(minimum, int(default))
59
+
60
+
61
+ _GPU_BATCH_TARGET_ACTIVE_RAYS = _env_int("RAYSTRACK_GPU_BATCH_TARGET_ACTIVE_RAYS", 524288)
62
+ _GPU_BATCH_STREAMS_MAX = _env_int("RAYSTRACK_GPU_BATCH_STREAMS_MAX", 32)
63
+ _GPU_BATCH_MEMORY_BUDGET_MB = _env_int("RAYSTRACK_GPU_BATCH_MEMORY_BUDGET_MB", 512)
64
+ _GPU_SMALL_EMITTER_RAY_CAP = _env_int("RAYSTRACK_GPU_SMALL_EMITTER_RAY_CAP", 1048576)
65
+
66
+
67
+ def _open_log_console() -> None:
68
+ global _LOG_PROC
69
+ if _LOG_PROC is not None:
70
+ return
71
+ try:
72
+ helper = [
73
+ sys.executable,
74
+ "-u",
75
+ "-c",
76
+ "import sys; [sys.stdout.write(l) for l in iter(sys.stdin.readline, '')]",
77
+ ]
78
+ if os.name == "nt":
79
+ _LOG_PROC = subprocess.Popen(
80
+ ["cmd.exe", "/k", *helper],
81
+ stdin=subprocess.PIPE,
82
+ creationflags=subprocess.CREATE_NEW_CONSOLE,
83
+ text=True,
84
+ )
85
+ else:
86
+ term = os.environ.get("TERM_WINDOW", "xterm")
87
+ _LOG_PROC = subprocess.Popen([term, "-hold", "-e", *helper], stdin=subprocess.PIPE, text=True)
88
+ except Exception:
89
+ _LOG_PROC = None
90
+
91
+
92
+ def _log(msg: str) -> None:
93
+ if _LOG_PROC is None:
94
+ _open_log_console()
95
+ if _LOG_PROC and _LOG_PROC.stdin:
96
+ try:
97
+ _LOG_PROC.stdin.write(msg + "\n")
98
+ _LOG_PROC.stdin.flush()
99
+ return
100
+ except Exception:
101
+ pass
102
+ print(msg)
103
+
104
+
105
+ def _compute_cuda_launch(n_items: int, preferred_threads: Optional[int], min_blocks: int = 4) -> Tuple[int, int]:
106
+ n = int(max(0, n_items))
107
+ if n <= 0:
108
+ return 1, 1
109
+ device = cuda.get_current_device()
110
+ warp = 32
111
+ max_threads = int(device.MAX_THREADS_PER_BLOCK)
112
+ threads = int(max(1, min(preferred_threads or 256, max_threads)))
113
+ if threads >= warp:
114
+ threads = ((threads + warp - 1) // warp) * warp
115
+ if n < threads:
116
+ if n >= warp:
117
+ target = max(warp, (n + min_blocks - 1) // min_blocks)
118
+ threads = min(threads, ((target + warp - 1) // warp) * warp)
119
+ else:
120
+ threads = min(threads, max(1, min(n, (n + min_blocks - 1) // min_blocks)))
121
+ threads = max(1, min(threads, max_threads))
122
+ return max(1, (n + threads - 1) // threads), threads
123
+
124
+
125
+ def _select_bvh(bvh: str | None, total_faces: int) -> bool:
126
+ mode = (bvh or "auto").lower()
127
+ if mode not in ("auto", "off", "builtin"):
128
+ raise ValueError(f"bvh must be 'auto', 'off', or 'builtin' (got {bvh!r})")
129
+ if mode == "builtin":
130
+ return True
131
+ if mode == "off":
132
+ return False
133
+ return total_faces >= _BVH_AUTO_THRESHOLD
134
+
135
+
136
+ def _resolve_device(device: str | None) -> bool:
137
+ have_cuda = cuda.is_available()
138
+ dev = (device or "auto").lower()
139
+ if dev not in ("auto", "gpu", "cpu"):
140
+ raise ValueError(f"device must be 'auto', 'gpu', or 'cpu' (got {device!r})")
141
+ if dev == "auto":
142
+ return have_cuda
143
+ if dev == "gpu":
144
+ if not have_cuda:
145
+ raise RuntimeError("device='gpu' requested but CUDA is not available")
146
+ return True
147
+ return False
148
+
149
+
150
+ def _ensure_prepared(
151
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
152
+ prepared: PreparedSolver | None,
153
+ ) -> PreparedSolver:
154
+ if prepared is None:
155
+ return PreparedSolver(meshes)
156
+ if not isinstance(prepared, PreparedSolver):
157
+ raise TypeError("prepared must be a PreparedSolver instance")
158
+ return prepared
159
+
160
+
161
+ def _matrix_receivers(idx_emit: int, n_surf: int, reciprocity: bool) -> List[int]:
162
+ if reciprocity:
163
+ return list(range(idx_emit + 1, n_surf))
164
+ return [j for j in range(n_surf) if j != idx_emit]
165
+
166
+
167
+ def _build_emitter_surface_mask(
168
+ idx_emit: int,
169
+ emitter: PreparedEmitter,
170
+ bounds_center: np.ndarray,
171
+ bounds_extent: np.ndarray,
172
+ ) -> np.ndarray:
173
+ n_surf = int(bounds_center.shape[0])
174
+ surf_active = np.ones(n_surf, dtype=np.uint8)
175
+ if 0 <= idx_emit < n_surf:
176
+ surf_active[idx_emit] = 0
177
+
178
+ if not emitter.plane_is_planar:
179
+ return surf_active
180
+
181
+ plane_origin = emitter.plane_origin
182
+ plane_normal = emitter.plane_normal
183
+ plane_tol = float(emitter.plane_tol)
184
+ abs_n = np.abs(plane_normal)
185
+
186
+ for j in range(n_surf):
187
+ if j == idx_emit:
188
+ continue
189
+ dx = bounds_center[j, 0] - plane_origin[0]
190
+ dy = bounds_center[j, 1] - plane_origin[1]
191
+ dz = bounds_center[j, 2] - plane_origin[2]
192
+ signed = (
193
+ dx * plane_normal[0]
194
+ + dy * plane_normal[1]
195
+ + dz * plane_normal[2]
196
+ )
197
+ radius = (
198
+ abs_n[0] * bounds_extent[j, 0]
199
+ + abs_n[1] * bounds_extent[j, 1]
200
+ + abs_n[2] * bounds_extent[j, 2]
201
+ )
202
+ if signed + radius <= plane_tol:
203
+ surf_active[j] = 0
204
+ return surf_active
205
+
206
+
207
+ def _matrix_active_receivers(
208
+ idx_emit: int,
209
+ n_surf: int,
210
+ reciprocity: bool,
211
+ surf_active: np.ndarray,
212
+ ) -> Tuple[List[int], np.ndarray]:
213
+ receivers = [j for j in _matrix_receivers(idx_emit, n_surf, reciprocity) if surf_active[j] != 0]
214
+ return receivers, np.asarray(receivers, dtype=np.int32)
215
+
216
+
217
+ def _convergence_checkpoint(iters_done: int, *, min_iters: int, interval: int, max_iters: int, needs_variance: bool = False) -> bool:
218
+ if iters_done < max(1, int(min_iters)):
219
+ return False
220
+ if needs_variance and iters_done <= 1:
221
+ return False
222
+ if iters_done >= int(max_iters):
223
+ return True
224
+ span = max(1, int(interval))
225
+ if span <= 1:
226
+ return True
227
+ start = max(1, int(min_iters))
228
+ return ((iters_done - start) % span) == 0
229
+
230
+
231
+ def _host_ray_buffers(n_rays: int) -> Tuple[np.ndarray, np.ndarray]:
232
+ orig = np.empty((n_rays, 3), dtype=np.float32)
233
+ return orig, np.empty_like(orig)
234
+
235
+
236
+ def _build_rays_host(emitter: PreparedEmitter, rays: int, orig, dire, cp_grid, cp_dims) -> None:
237
+ build_rays(
238
+ emitter.u_grid,
239
+ emitter.v_grid,
240
+ emitter.halton_tri,
241
+ emitter.halton_u,
242
+ emitter.halton_v,
243
+ emitter.halton_r1,
244
+ emitter.halton_r2,
245
+ emitter.cdf,
246
+ emitter.tri_a,
247
+ emitter.tri_e1,
248
+ emitter.tri_e2,
249
+ emitter.tri_u,
250
+ emitter.tri_v,
251
+ emitter.tri_n,
252
+ emitter.tri_origin_eps,
253
+ rays,
254
+ orig,
255
+ dire,
256
+ cp_grid,
257
+ cp_dims,
258
+ )
259
+
260
+
261
+ @dataclass
262
+ class _MatrixGpuWorkspace:
263
+ stream: Any
264
+ ray_cap: int
265
+ n_surf: int
266
+ track_stats: bool
267
+ d_orig: Any
268
+ d_dirs: Any
269
+ d_hit_sid: Any
270
+ d_hit_front: Any
271
+ d_surf_active: Any
272
+ d_hf_iter: Any
273
+ d_hb_iter: Any
274
+ d_hf_total: Any
275
+ d_hb_total: Any
276
+ d_sum_f: Any
277
+ d_sumsq_f: Any
278
+ d_sum_b: Any
279
+ d_sumsq_b: Any
280
+ h_hf_total: np.ndarray
281
+ h_hb_total: np.ndarray
282
+ h_sum_f: Optional[np.ndarray]
283
+ h_sumsq_f: Optional[np.ndarray]
284
+ h_sum_b: Optional[np.ndarray]
285
+ h_sumsq_b: Optional[np.ndarray]
286
+ h_orig: Optional[np.ndarray]
287
+ h_dirs: Optional[np.ndarray]
288
+ surf_blocks: int
289
+ surf_threads: int
290
+
291
+
292
+ @dataclass
293
+ class _MatrixGpuEmitterState:
294
+ idx_emit: int
295
+ name_e: str
296
+ receivers: List[int]
297
+ recv_idx: np.ndarray
298
+ surf_active: np.ndarray
299
+ emit_sid: int
300
+ min_sid: int
301
+ emitter: PreparedEmitter
302
+ emitter_dev: Any
303
+ n_rays_once: int
304
+ blocks: int
305
+ threads: int
306
+ total_rays: int = 0
307
+ iters_done: int = 0
308
+ pending_chunk: int = 0
309
+ prev_f: Optional[np.ndarray] = None
310
+ prev_b: Optional[np.ndarray] = None
311
+ started_at: float = 0.0
312
+
313
+
314
+ def _matrix_stats_required(tol_mode: str, return_stats: bool) -> bool:
315
+ return bool(return_stats or tol_mode == "stderr")
316
+
317
+
318
+ def _matrix_can_batch_emitters(*, cuda_async: bool, gpu_raygen: bool) -> bool:
319
+ return bool(cuda_async and gpu_raygen and _GPU_BATCH_STREAMS_MAX > 1)
320
+
321
+
322
+ def _matrix_can_chunk_iterations(*, cuda_async: bool, gpu_raygen: bool) -> bool:
323
+ return not (cuda_async and not gpu_raygen)
324
+
325
+
326
+ def _estimate_matrix_gpu_workspace_bytes(
327
+ ray_cap: int,
328
+ n_surf: int,
329
+ *,
330
+ track_stats: bool,
331
+ gpu_raygen: bool,
332
+ cuda_async: bool,
333
+ ) -> int:
334
+ f32 = np.dtype(np.float32).itemsize
335
+ f64 = np.dtype(np.float64).itemsize
336
+ i32 = np.dtype(np.int32).itemsize
337
+ i64 = np.dtype(np.int64).itemsize
338
+ u8 = np.dtype(np.uint8).itemsize
339
+
340
+ ray_bytes = int(ray_cap) * ((2 * 3 * f32) + i32 + u8)
341
+ surf_device_bytes = int(n_surf) * (4 * i64) + int(n_surf) * u8
342
+ surf_host_bytes = int(n_surf) * (2 * i64)
343
+ if track_stats:
344
+ surf_device_bytes += int(n_surf) * (4 * f64)
345
+ surf_host_bytes += int(n_surf) * (4 * f64)
346
+
347
+ if cuda_async and not gpu_raygen:
348
+ ray_bytes += int(ray_cap) * (2 * 3 * f32)
349
+
350
+ return ray_bytes + surf_device_bytes + surf_host_bytes
351
+
352
+
353
+ def _matrix_batch_slot_count(
354
+ ray_cap: int,
355
+ group_size: int,
356
+ n_surf: int,
357
+ *,
358
+ track_stats: bool,
359
+ gpu_raygen: bool,
360
+ cuda_async: bool,
361
+ ) -> int:
362
+ if group_size <= 1:
363
+ return 1
364
+
365
+ ray_cap_i = max(1, int(ray_cap))
366
+ desired = max(2, (_GPU_BATCH_TARGET_ACTIVE_RAYS + ray_cap_i - 1) // ray_cap_i)
367
+ device = cuda.get_current_device()
368
+ sm_count = int(getattr(device, "MULTIPROCESSOR_COUNT", _GPU_BATCH_STREAMS_MAX))
369
+ if ray_cap_i < 65536:
370
+ workload_cap = min(_GPU_BATCH_STREAMS_MAX, 4)
371
+ elif ray_cap_i < 262144:
372
+ workload_cap = min(_GPU_BATCH_STREAMS_MAX, 8)
373
+ else:
374
+ workload_cap = _GPU_BATCH_STREAMS_MAX
375
+ hard_cap = min(int(group_size), max(2, sm_count), workload_cap)
376
+
377
+ ws_bytes = max(
378
+ 1,
379
+ _estimate_matrix_gpu_workspace_bytes(
380
+ ray_cap_i,
381
+ n_surf,
382
+ track_stats=track_stats,
383
+ gpu_raygen=gpu_raygen,
384
+ cuda_async=cuda_async,
385
+ ),
386
+ )
387
+ budget_bytes = _GPU_BATCH_MEMORY_BUDGET_MB * 1024 * 1024
388
+ budget_cap = max(1, budget_bytes // ws_bytes)
389
+ return max(1, min(hard_cap, desired, int(budget_cap)))
390
+
391
+
392
+ def _iterations_to_next_checkpoint(
393
+ iters_done: int,
394
+ *,
395
+ min_iters: int,
396
+ interval: int,
397
+ max_iters: int,
398
+ needs_variance: bool,
399
+ ) -> int:
400
+ remaining = int(max_iters) - int(iters_done)
401
+ if remaining <= 0:
402
+ return 0
403
+
404
+ start = max(1, int(min_iters))
405
+ if needs_variance:
406
+ start = max(start, 2)
407
+
408
+ if iters_done < start:
409
+ return min(start - iters_done, remaining)
410
+
411
+ span = max(1, int(interval))
412
+ if span <= 1:
413
+ return 1
414
+
415
+ rem = (iters_done - start) % span
416
+ return min(span if rem == 0 else span - rem, remaining)
417
+
418
+
419
+ def _create_matrix_gpu_workspace(
420
+ ray_cap: int,
421
+ n_surf: int,
422
+ *,
423
+ cuda_async: bool,
424
+ gpu_raygen: bool,
425
+ track_stats: bool,
426
+ ) -> _MatrixGpuWorkspace:
427
+ stream = cuda.stream() if cuda_async else None
428
+ d_orig = cuda.device_array((ray_cap, 3), dtype=np.float32)
429
+ d_dirs = cuda.device_array((ray_cap, 3), dtype=np.float32)
430
+ d_hit_sid = cuda.device_array(ray_cap, dtype=np.int32)
431
+ d_hit_front = cuda.device_array(ray_cap, dtype=np.uint8)
432
+ d_surf_active = cuda.device_array(n_surf, dtype=np.uint8)
433
+ d_hf_iter = cuda.device_array(n_surf, dtype=np.int64)
434
+ d_hb_iter = cuda.device_array(n_surf, dtype=np.int64)
435
+ d_hf_total = cuda.device_array(n_surf, dtype=np.int64)
436
+ d_hb_total = cuda.device_array(n_surf, dtype=np.int64)
437
+ d_sum_f = cuda.device_array(n_surf, dtype=np.float64) if track_stats else None
438
+ d_sumsq_f = cuda.device_array(n_surf, dtype=np.float64) if track_stats else None
439
+ d_sum_b = cuda.device_array(n_surf, dtype=np.float64) if track_stats else None
440
+ d_sumsq_b = cuda.device_array(n_surf, dtype=np.float64) if track_stats else None
441
+
442
+ if cuda_async:
443
+ h_hf_total = cuda.pinned_array(n_surf, dtype=np.int64)
444
+ h_hb_total = cuda.pinned_array(n_surf, dtype=np.int64)
445
+ h_sum_f = cuda.pinned_array(n_surf, dtype=np.float64) if track_stats else None
446
+ h_sumsq_f = cuda.pinned_array(n_surf, dtype=np.float64) if track_stats else None
447
+ h_sum_b = cuda.pinned_array(n_surf, dtype=np.float64) if track_stats else None
448
+ h_sumsq_b = cuda.pinned_array(n_surf, dtype=np.float64) if track_stats else None
449
+ if gpu_raygen:
450
+ h_orig = h_dirs = None
451
+ else:
452
+ h_orig = cuda.pinned_array((ray_cap, 3), dtype=np.float32)
453
+ h_dirs = cuda.pinned_array((ray_cap, 3), dtype=np.float32)
454
+ else:
455
+ h_hf_total = np.empty(n_surf, dtype=np.int64)
456
+ h_hb_total = np.empty(n_surf, dtype=np.int64)
457
+ h_sum_f = np.empty(n_surf, dtype=np.float64) if track_stats else None
458
+ h_sumsq_f = np.empty(n_surf, dtype=np.float64) if track_stats else None
459
+ h_sum_b = np.empty(n_surf, dtype=np.float64) if track_stats else None
460
+ h_sumsq_b = np.empty(n_surf, dtype=np.float64) if track_stats else None
461
+ if gpu_raygen:
462
+ h_orig = h_dirs = None
463
+ else:
464
+ h_orig, h_dirs = _host_ray_buffers(ray_cap)
465
+
466
+ surf_blocks, surf_threads = _compute_cuda_launch(n_surf, None)
467
+ return _MatrixGpuWorkspace(
468
+ stream=stream,
469
+ ray_cap=ray_cap,
470
+ n_surf=n_surf,
471
+ track_stats=track_stats,
472
+ d_orig=d_orig,
473
+ d_dirs=d_dirs,
474
+ d_hit_sid=d_hit_sid,
475
+ d_hit_front=d_hit_front,
476
+ d_surf_active=d_surf_active,
477
+ d_hf_iter=d_hf_iter,
478
+ d_hb_iter=d_hb_iter,
479
+ d_hf_total=d_hf_total,
480
+ d_hb_total=d_hb_total,
481
+ d_sum_f=d_sum_f,
482
+ d_sumsq_f=d_sumsq_f,
483
+ d_sum_b=d_sum_b,
484
+ d_sumsq_b=d_sumsq_b,
485
+ h_hf_total=h_hf_total,
486
+ h_hb_total=h_hb_total,
487
+ h_sum_f=h_sum_f,
488
+ h_sumsq_f=h_sumsq_f,
489
+ h_sum_b=h_sum_b,
490
+ h_sumsq_b=h_sumsq_b,
491
+ h_orig=h_orig,
492
+ h_dirs=h_dirs,
493
+ surf_blocks=surf_blocks,
494
+ surf_threads=surf_threads,
495
+ )
496
+
497
+
498
+ def _reset_matrix_gpu_workspace(ws: _MatrixGpuWorkspace) -> None:
499
+ zero = kernel_zero_i64[ws.surf_blocks, ws.surf_threads, ws.stream] if ws.stream is not None else kernel_zero_i64[ws.surf_blocks, ws.surf_threads]
500
+ zero(ws.d_hf_total)
501
+ zero(ws.d_hb_total)
502
+ if ws.track_stats:
503
+ zero(ws.d_sum_f)
504
+ zero(ws.d_sumsq_f)
505
+ zero(ws.d_sum_b)
506
+ zero(ws.d_sumsq_b)
507
+
508
+
509
+ def _load_matrix_gpu_active_mask(ws: _MatrixGpuWorkspace, surf_active: np.ndarray) -> None:
510
+ if ws.stream is not None:
511
+ ws.d_surf_active.copy_to_device(surf_active, stream=ws.stream)
512
+ else:
513
+ ws.d_surf_active.copy_to_device(surf_active)
514
+
515
+
516
+ def _enqueue_matrix_gpu_summary_copy(ws: _MatrixGpuWorkspace) -> None:
517
+ if ws.stream is not None:
518
+ ws.d_hf_total.copy_to_host(ws.h_hf_total, stream=ws.stream)
519
+ ws.d_hb_total.copy_to_host(ws.h_hb_total, stream=ws.stream)
520
+ if ws.track_stats:
521
+ ws.d_sum_f.copy_to_host(ws.h_sum_f, stream=ws.stream)
522
+ ws.d_sumsq_f.copy_to_host(ws.h_sumsq_f, stream=ws.stream)
523
+ ws.d_sum_b.copy_to_host(ws.h_sum_b, stream=ws.stream)
524
+ ws.d_sumsq_b.copy_to_host(ws.h_sumsq_b, stream=ws.stream)
525
+
526
+
527
+ def _read_matrix_gpu_summary(ws: _MatrixGpuWorkspace) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]]:
528
+ if ws.stream is not None:
529
+ ws.stream.synchronize()
530
+ hits_f_total = np.asarray(ws.h_hf_total)
531
+ hits_b_total = np.asarray(ws.h_hb_total)
532
+ if ws.track_stats:
533
+ return (
534
+ hits_f_total,
535
+ hits_b_total,
536
+ np.asarray(ws.h_sum_f),
537
+ np.asarray(ws.h_sumsq_f),
538
+ np.asarray(ws.h_sum_b),
539
+ np.asarray(ws.h_sumsq_b),
540
+ )
541
+ return hits_f_total, hits_b_total, None, None, None, None
542
+
543
+ cuda.synchronize()
544
+ hits_f_total = ws.d_hf_total.copy_to_host()
545
+ hits_b_total = ws.d_hb_total.copy_to_host()
546
+ if ws.track_stats:
547
+ return (
548
+ hits_f_total,
549
+ hits_b_total,
550
+ ws.d_sum_f.copy_to_host(),
551
+ ws.d_sumsq_f.copy_to_host(),
552
+ ws.d_sum_b.copy_to_host(),
553
+ ws.d_sumsq_b.copy_to_host(),
554
+ )
555
+ return hits_f_total, hits_b_total, None, None, None, None
556
+
557
+
558
+ def _summary_to_stderr(sum_arr: np.ndarray, sumsq_arr: np.ndarray, iters_done: int) -> np.ndarray:
559
+ if iters_done <= 1:
560
+ return np.full(sum_arr.shape, np.inf, dtype=np.float64)
561
+ n = float(iters_done)
562
+ mean = sum_arr / n
563
+ m2 = np.maximum(sumsq_arr - n * mean * mean, 0.0)
564
+ return np.sqrt(m2 / float(iters_done - 1) / n)
565
+
566
+
567
+ def _build_matrix_gpu_state(
568
+ idx_emit: int,
569
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
570
+ emitters: List[PreparedEmitter],
571
+ prepared_solver: PreparedSolver,
572
+ *,
573
+ samples: int,
574
+ rays: int,
575
+ flip_faces: bool,
576
+ reciprocity: bool,
577
+ ) -> Optional[_MatrixGpuEmitterState]:
578
+ name_e, _, _ = meshes[idx_emit]
579
+ emitter = emitters[idx_emit]
580
+ bounds_center, bounds_extent = prepared_solver.get_mesh_bounds()
581
+ surf_active = _build_emitter_surface_mask(idx_emit, emitter, bounds_center, bounds_extent)
582
+ receivers, recv_idx = _matrix_active_receivers(idx_emit, len(meshes), reciprocity, surf_active)
583
+ if not receivers:
584
+ return None
585
+
586
+ emit_sid, min_sid = _matrix_skip(idx_emit, reciprocity)
587
+ n_rays_once = emitter.n_cells * rays
588
+ blocks, threads = _compute_cuda_launch(n_rays_once, None)
589
+ emitter_dev = prepared_solver.get_device_emitter(idx_emit, samples=samples, rays=rays, flip_faces=flip_faces)
590
+ return _MatrixGpuEmitterState(
591
+ idx_emit=idx_emit,
592
+ name_e=name_e,
593
+ receivers=receivers,
594
+ recv_idx=recv_idx,
595
+ surf_active=surf_active,
596
+ emit_sid=emit_sid,
597
+ min_sid=min_sid,
598
+ emitter=emitter,
599
+ emitter_dev=emitter_dev,
600
+ n_rays_once=n_rays_once,
601
+ blocks=blocks,
602
+ threads=threads,
603
+ started_at=time.time(),
604
+ )
605
+
606
+
607
+ def _enqueue_matrix_gpu_chunk(
608
+ ws: _MatrixGpuWorkspace,
609
+ state: _MatrixGpuEmitterState,
610
+ scene,
611
+ d_scene,
612
+ *,
613
+ rays: int,
614
+ seed: int,
615
+ gpu_raygen: bool,
616
+ cuda_async: bool,
617
+ ) -> None:
618
+ d_orig = ws.d_orig[: state.n_rays_once]
619
+ d_dirs = ws.d_dirs[: state.n_rays_once]
620
+ d_hit_sid = ws.d_hit_sid[: state.n_rays_once]
621
+ d_hit_front = ws.d_hit_front[: state.n_rays_once]
622
+ stream = ws.stream
623
+ zero = kernel_zero_i64[ws.surf_blocks, ws.surf_threads, stream] if stream is not None else kernel_zero_i64[ws.surf_blocks, ws.surf_threads]
624
+
625
+ offset = 0
626
+ while offset < state.pending_chunk: # type: ignore[attr-defined]
627
+ itr = state.iters_done + offset
628
+ rng = np.random.default_rng(seed + state.idx_emit + itr)
629
+ cp_grid = rng.random(2, dtype=np.float32)
630
+ cp_dims = rng.random(5, dtype=np.float32)
631
+
632
+ if gpu_raygen:
633
+ launch = kernel_build_rays[state.blocks, state.threads, stream] if stream is not None else kernel_build_rays[state.blocks, state.threads]
634
+ launch(
635
+ state.emitter_dev.u_grid, state.emitter_dev.v_grid,
636
+ state.emitter_dev.halton_tri, state.emitter_dev.halton_u, state.emitter_dev.halton_v,
637
+ state.emitter_dev.halton_r1, state.emitter_dev.halton_r2, state.emitter_dev.cdf,
638
+ state.emitter_dev.tri_a, state.emitter_dev.tri_e1, state.emitter_dev.tri_e2,
639
+ state.emitter_dev.tri_u, state.emitter_dev.tri_v, state.emitter_dev.tri_n, state.emitter_dev.tri_origin_eps,
640
+ rays, d_orig, d_dirs, cp_grid, cp_dims,
641
+ )
642
+ else:
643
+ h_orig = ws.h_orig[: state.n_rays_once]
644
+ h_dirs = ws.h_dirs[: state.n_rays_once]
645
+ _build_rays_host(state.emitter, rays, h_orig, h_dirs, cp_grid, cp_dims)
646
+ if stream is not None:
647
+ d_orig.copy_to_device(h_orig, stream=stream)
648
+ d_dirs.copy_to_device(h_dirs, stream=stream)
649
+ else:
650
+ d_orig.copy_to_device(h_orig)
651
+ d_dirs.copy_to_device(h_dirs)
652
+
653
+ zero(ws.d_hf_iter)
654
+ zero(ws.d_hb_iter)
655
+
656
+ if scene.use_bvh:
657
+ trace = kernel_trace_bvh_firsthit[state.blocks, state.threads, stream] if stream is not None else kernel_trace_bvh_firsthit[state.blocks, state.threads]
658
+ trace(
659
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.normals, d_scene.sid, ws.d_surf_active,
660
+ d_scene.bb_min, d_scene.bb_max, d_scene.left, d_scene.right, d_scene.start, d_scene.count,
661
+ state.emit_sid, state.min_sid, d_hit_sid, d_hit_front,
662
+ )
663
+ else:
664
+ trace = kernel_trace_firsthit[state.blocks, state.threads, stream] if stream is not None else kernel_trace_firsthit[state.blocks, state.threads]
665
+ trace(
666
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.normals, d_scene.sid, ws.d_surf_active,
667
+ state.emit_sid, state.min_sid, d_hit_sid, d_hit_front,
668
+ )
669
+
670
+ reduce_kernel = kernel_reduce_hits[state.blocks, state.threads, stream] if stream is not None else kernel_reduce_hits[state.blocks, state.threads]
671
+ reduce_kernel(d_hit_sid, d_hit_front, ws.d_hf_iter, ws.d_hb_iter, ws.n_surf)
672
+
673
+ if ws.track_stats:
674
+ acc = kernel_accumulate_hits_stats[ws.surf_blocks, ws.surf_threads, stream] if stream is not None else kernel_accumulate_hits_stats[ws.surf_blocks, ws.surf_threads]
675
+ acc(
676
+ ws.d_hf_iter,
677
+ ws.d_hb_iter,
678
+ ws.d_hf_total,
679
+ ws.d_hb_total,
680
+ ws.d_sum_f,
681
+ ws.d_sumsq_f,
682
+ ws.d_sum_b,
683
+ ws.d_sumsq_b,
684
+ 1.0 / float(state.n_rays_once),
685
+ )
686
+ else:
687
+ acc = kernel_accumulate_hits[ws.surf_blocks, ws.surf_threads, stream] if stream is not None else kernel_accumulate_hits[ws.surf_blocks, ws.surf_threads]
688
+ acc(ws.d_hf_iter, ws.d_hb_iter, ws.d_hf_total, ws.d_hb_total)
689
+
690
+ offset += 1
691
+
692
+
693
+ def _finalize_matrix_gpu_state(
694
+ state: _MatrixGpuEmitterState,
695
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
696
+ result: Dict[str, Dict[str, float]],
697
+ stats_result: Optional[Dict[str, Dict[str, float]]],
698
+ *,
699
+ hits_f_total: np.ndarray,
700
+ hits_b_total: np.ndarray,
701
+ sum_f: Optional[np.ndarray],
702
+ sumsq_f: Optional[np.ndarray],
703
+ sum_b: Optional[np.ndarray],
704
+ sumsq_b: Optional[np.ndarray],
705
+ reciprocity: bool,
706
+ areas: Optional[List[float]],
707
+ return_stats: bool,
708
+ use_bvh: bool,
709
+ ) -> None:
710
+ if state.iters_done > 1 and sum_f is not None and sumsq_f is not None and sum_b is not None and sumsq_b is not None:
711
+ se_f = _summary_to_stderr(sum_f, sumsq_f, state.iters_done)
712
+ se_b = _summary_to_stderr(sum_b, sumsq_b, state.iters_done)
713
+ else:
714
+ se_f = np.full((len(meshes),), np.inf, dtype=np.float64)
715
+ se_b = np.full((len(meshes),), np.inf, dtype=np.float64)
716
+
717
+ row: Dict[str, float] = {}
718
+ stats_row: Dict[str, float] = {}
719
+ for j in state.receivers:
720
+ name_r, _, _ = meshes[j]
721
+ f = hits_f_total[j] / float(state.total_rays)
722
+ b = hits_b_total[j] / float(state.total_rays)
723
+ if f > 0.0:
724
+ row[f"{name_r}_front"] = f
725
+ if reciprocity and areas is not None and areas[j] > 0.0:
726
+ result[name_r][f"{state.name_e}_front"] = f * (areas[state.idx_emit] / areas[j])
727
+ if return_stats:
728
+ stats_row[f"{name_r}_front"] = float(se_f[j])
729
+ if b > 0.0:
730
+ row[f"{name_r}_back"] = b
731
+ if return_stats:
732
+ stats_row[f"{name_r}_back"] = float(se_b[j])
733
+ result[state.name_e].update(row)
734
+ if return_stats and stats_result is not None:
735
+ stats_result[state.name_e] = stats_row
736
+
737
+ msg = (
738
+ f"({state.idx_emit+1}/{len(meshes)}) [{state.name_e}] {state.iters_done} iter, "
739
+ f"{state.total_rays:,} rays -> {time.time() - state.started_at:0.3f}s "
740
+ f"(BVH={'builtin' if use_bvh else 'off'}, device=gpu)"
741
+ )
742
+ _log(msg)
743
+
744
+
745
+ def _evaluate_matrix_gpu_state(
746
+ state: _MatrixGpuEmitterState,
747
+ *,
748
+ hits_f_total: np.ndarray,
749
+ hits_b_total: np.ndarray,
750
+ sum_f: Optional[np.ndarray],
751
+ sumsq_f: Optional[np.ndarray],
752
+ sum_b: Optional[np.ndarray],
753
+ sumsq_b: Optional[np.ndarray],
754
+ tol: float,
755
+ tol_mode: str,
756
+ min_iters: int,
757
+ convergence_interval: int,
758
+ max_iters: int,
759
+ ) -> bool:
760
+ done = state.iters_done >= int(max_iters)
761
+ check_matrix = _convergence_checkpoint(
762
+ state.iters_done,
763
+ min_iters=min_iters,
764
+ interval=convergence_interval,
765
+ max_iters=max_iters,
766
+ needs_variance=(tol_mode == "stderr"),
767
+ )
768
+
769
+ if tol_mode == "delta":
770
+ curr_f = hits_f_total.astype(np.float64) / float(max(1, state.total_rays))
771
+ curr_b = hits_b_total.astype(np.float64) / float(max(1, state.total_rays))
772
+ if check_matrix and state.prev_f is not None:
773
+ if np.all(np.abs(curr_f - state.prev_f) < tol) and np.all(np.abs(curr_b - state.prev_b) < tol):
774
+ done = True
775
+ if check_matrix:
776
+ state.prev_f = curr_f.copy()
777
+ state.prev_b = curr_b.copy()
778
+ elif tol_mode == "stderr":
779
+ if check_matrix and sum_f is not None and sumsq_f is not None and sum_b is not None and sumsq_b is not None:
780
+ se_f = _summary_to_stderr(sum_f, sumsq_f, state.iters_done)
781
+ se_b = _summary_to_stderr(sum_b, sumsq_b, state.iters_done)
782
+ if np.all(se_f[state.recv_idx] <= tol) and np.all(se_b[state.recv_idx] <= tol):
783
+ done = True
784
+ else:
785
+ raise ValueError(f"Unknown tol_mode: {tol_mode}")
786
+
787
+ return done
788
+
789
+
790
+ def _run_matrix_gpu_serial(
791
+ indices: List[int],
792
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
793
+ emitters: List[PreparedEmitter],
794
+ prepared_solver: PreparedSolver,
795
+ scene,
796
+ d_scene,
797
+ result: Dict[str, Dict[str, float]],
798
+ stats_result: Optional[Dict[str, Dict[str, float]]],
799
+ ws: _MatrixGpuWorkspace,
800
+ *,
801
+ samples: int,
802
+ rays: int,
803
+ seed: int,
804
+ gpu_raygen: bool,
805
+ cuda_async: bool,
806
+ flip_faces: bool,
807
+ reciprocity: bool,
808
+ tol: float,
809
+ tol_mode: str,
810
+ min_iters: int,
811
+ convergence_interval: int,
812
+ max_iters: int,
813
+ return_stats: bool,
814
+ areas: Optional[List[float]],
815
+ use_bvh: bool,
816
+ ) -> None:
817
+ allow_chunking = _matrix_can_chunk_iterations(cuda_async=cuda_async, gpu_raygen=gpu_raygen)
818
+ interval = convergence_interval if allow_chunking else 1
819
+ needs_variance = tol_mode == "stderr"
820
+
821
+ for idx_emit in indices:
822
+ state = _build_matrix_gpu_state(
823
+ idx_emit,
824
+ meshes,
825
+ emitters,
826
+ prepared_solver,
827
+ samples=samples,
828
+ rays=rays,
829
+ flip_faces=flip_faces,
830
+ reciprocity=reciprocity,
831
+ )
832
+ if state is None:
833
+ name_e, _, _ = meshes[idx_emit]
834
+ msg = f"({idx_emit+1}/{len(meshes)}) [{name_e}] 0 iter, 0 rays -> 0.000s (BVH={'builtin' if use_bvh else 'off'}, device=gpu)"
835
+ _log(msg)
836
+ continue
837
+
838
+ _reset_matrix_gpu_workspace(ws)
839
+ _load_matrix_gpu_active_mask(ws, state.surf_active)
840
+
841
+ while True:
842
+ state.pending_chunk = _iterations_to_next_checkpoint(
843
+ state.iters_done,
844
+ min_iters=min_iters,
845
+ interval=interval,
846
+ max_iters=max_iters,
847
+ needs_variance=needs_variance,
848
+ )
849
+ if state.pending_chunk <= 0:
850
+ break
851
+
852
+ _enqueue_matrix_gpu_chunk(
853
+ ws,
854
+ state,
855
+ scene,
856
+ d_scene,
857
+ rays=rays,
858
+ seed=seed,
859
+ gpu_raygen=gpu_raygen,
860
+ cuda_async=cuda_async,
861
+ )
862
+ _enqueue_matrix_gpu_summary_copy(ws)
863
+
864
+ state.iters_done += state.pending_chunk
865
+ state.total_rays += state.pending_chunk * state.n_rays_once
866
+
867
+ hits_f_total, hits_b_total, sum_f, sumsq_f, sum_b, sumsq_b = _read_matrix_gpu_summary(ws)
868
+ if _evaluate_matrix_gpu_state(
869
+ state,
870
+ hits_f_total=hits_f_total,
871
+ hits_b_total=hits_b_total,
872
+ sum_f=sum_f,
873
+ sumsq_f=sumsq_f,
874
+ sum_b=sum_b,
875
+ sumsq_b=sumsq_b,
876
+ tol=tol,
877
+ tol_mode=tol_mode,
878
+ min_iters=min_iters,
879
+ convergence_interval=interval,
880
+ max_iters=max_iters,
881
+ ):
882
+ _finalize_matrix_gpu_state(
883
+ state,
884
+ meshes,
885
+ result,
886
+ stats_result,
887
+ hits_f_total=hits_f_total,
888
+ hits_b_total=hits_b_total,
889
+ sum_f=sum_f,
890
+ sumsq_f=sumsq_f,
891
+ sum_b=sum_b,
892
+ sumsq_b=sumsq_b,
893
+ reciprocity=reciprocity,
894
+ areas=areas,
895
+ return_stats=return_stats,
896
+ use_bvh=use_bvh,
897
+ )
898
+ break
899
+
900
+
901
+ def _run_matrix_gpu_batched_group(
902
+ indices: List[int],
903
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
904
+ emitters: List[PreparedEmitter],
905
+ prepared_solver: PreparedSolver,
906
+ scene,
907
+ d_scene,
908
+ result: Dict[str, Dict[str, float]],
909
+ stats_result: Optional[Dict[str, Dict[str, float]]],
910
+ workspaces: List[_MatrixGpuWorkspace],
911
+ *,
912
+ samples: int,
913
+ rays: int,
914
+ seed: int,
915
+ gpu_raygen: bool,
916
+ cuda_async: bool,
917
+ flip_faces: bool,
918
+ reciprocity: bool,
919
+ tol: float,
920
+ tol_mode: str,
921
+ min_iters: int,
922
+ convergence_interval: int,
923
+ max_iters: int,
924
+ return_stats: bool,
925
+ areas: Optional[List[float]],
926
+ use_bvh: bool,
927
+ ) -> None:
928
+ queue = list(indices)
929
+ active: List[Optional[_MatrixGpuEmitterState]] = [None] * len(workspaces)
930
+ needs_variance = tol_mode == "stderr"
931
+
932
+ while queue or any(state is not None for state in active):
933
+ for slot, ws in enumerate(workspaces):
934
+ if active[slot] is not None:
935
+ continue
936
+ while queue:
937
+ idx_emit = queue.pop(0)
938
+ state = _build_matrix_gpu_state(
939
+ idx_emit,
940
+ meshes,
941
+ emitters,
942
+ prepared_solver,
943
+ samples=samples,
944
+ rays=rays,
945
+ flip_faces=flip_faces,
946
+ reciprocity=reciprocity,
947
+ )
948
+ if state is None:
949
+ name_e, _, _ = meshes[idx_emit]
950
+ msg = f"({idx_emit+1}/{len(meshes)}) [{name_e}] 0 iter, 0 rays -> 0.000s (BVH={'builtin' if use_bvh else 'off'}, device=gpu)"
951
+ _log(msg)
952
+ continue
953
+ _reset_matrix_gpu_workspace(ws)
954
+ _load_matrix_gpu_active_mask(ws, state.surf_active)
955
+ active[slot] = state
956
+ break
957
+
958
+ if not any(state is not None for state in active):
959
+ break
960
+
961
+ for slot, state in enumerate(active):
962
+ if state is None:
963
+ continue
964
+ state.pending_chunk = _iterations_to_next_checkpoint(
965
+ state.iters_done,
966
+ min_iters=min_iters,
967
+ interval=convergence_interval,
968
+ max_iters=max_iters,
969
+ needs_variance=needs_variance,
970
+ )
971
+ if state.pending_chunk <= 0:
972
+ state.pending_chunk = 1
973
+ _enqueue_matrix_gpu_chunk(
974
+ workspaces[slot],
975
+ state,
976
+ scene,
977
+ d_scene,
978
+ rays=rays,
979
+ seed=seed,
980
+ gpu_raygen=gpu_raygen,
981
+ cuda_async=cuda_async,
982
+ )
983
+ _enqueue_matrix_gpu_summary_copy(workspaces[slot])
984
+
985
+ for slot, state in enumerate(active):
986
+ if state is None:
987
+ continue
988
+ state.iters_done += state.pending_chunk
989
+ state.total_rays += state.pending_chunk * state.n_rays_once
990
+
991
+ hits_f_total, hits_b_total, sum_f, sumsq_f, sum_b, sumsq_b = _read_matrix_gpu_summary(workspaces[slot])
992
+ if _evaluate_matrix_gpu_state(
993
+ state,
994
+ hits_f_total=hits_f_total,
995
+ hits_b_total=hits_b_total,
996
+ sum_f=sum_f,
997
+ sumsq_f=sumsq_f,
998
+ sum_b=sum_b,
999
+ sumsq_b=sumsq_b,
1000
+ tol=tol,
1001
+ tol_mode=tol_mode,
1002
+ min_iters=min_iters,
1003
+ convergence_interval=convergence_interval,
1004
+ max_iters=max_iters,
1005
+ ):
1006
+ _finalize_matrix_gpu_state(
1007
+ state,
1008
+ meshes,
1009
+ result,
1010
+ stats_result,
1011
+ hits_f_total=hits_f_total,
1012
+ hits_b_total=hits_b_total,
1013
+ sum_f=sum_f,
1014
+ sumsq_f=sumsq_f,
1015
+ sum_b=sum_b,
1016
+ sumsq_b=sumsq_b,
1017
+ reciprocity=reciprocity,
1018
+ areas=areas,
1019
+ return_stats=return_stats,
1020
+ use_bvh=use_bvh,
1021
+ )
1022
+ active[slot] = None
1023
+
1024
+
1025
+ def _run_view_factor_matrix_gpu(
1026
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
1027
+ emitters: List[PreparedEmitter],
1028
+ prepared_solver: PreparedSolver,
1029
+ scene,
1030
+ d_scene,
1031
+ result: Dict[str, Dict[str, float]],
1032
+ stats_result: Optional[Dict[str, Dict[str, float]]],
1033
+ *,
1034
+ samples: int,
1035
+ rays: int,
1036
+ seed: int,
1037
+ gpu_raygen: bool,
1038
+ cuda_async: bool,
1039
+ flip_faces: bool,
1040
+ reciprocity: bool,
1041
+ tol: float,
1042
+ tol_mode: str,
1043
+ min_iters: int,
1044
+ convergence_interval: int,
1045
+ max_iters: int,
1046
+ return_stats: bool,
1047
+ areas: Optional[List[float]],
1048
+ use_bvh: bool,
1049
+ ) -> None:
1050
+ n_surf = len(meshes)
1051
+ ray_counts = [emitter.n_cells * rays for emitter in emitters]
1052
+ max_rays = max(ray_counts) if ray_counts else 1
1053
+ track_stats = _matrix_stats_required(tol_mode, return_stats)
1054
+ serial_ws = _create_matrix_gpu_workspace(
1055
+ max_rays,
1056
+ n_surf,
1057
+ cuda_async=cuda_async,
1058
+ gpu_raygen=gpu_raygen,
1059
+ track_stats=track_stats,
1060
+ )
1061
+
1062
+ batch_enabled = _matrix_can_batch_emitters(cuda_async=cuda_async, gpu_raygen=gpu_raygen)
1063
+ small_emitter_cap = max(1, min(max_rays, _GPU_SMALL_EMITTER_RAY_CAP))
1064
+ batch_workspaces: Optional[List[_MatrixGpuWorkspace]] = None
1065
+
1066
+ idx_emit = 0
1067
+ while idx_emit < len(meshes):
1068
+ if batch_enabled and ray_counts[idx_emit] <= small_emitter_cap:
1069
+ group: List[int] = []
1070
+ while idx_emit < len(meshes) and ray_counts[idx_emit] <= small_emitter_cap:
1071
+ group.append(idx_emit)
1072
+ idx_emit += 1
1073
+
1074
+ if len(group) > 1:
1075
+ group_ray_cap = max(ray_counts[g] for g in group)
1076
+ batch_slots = _matrix_batch_slot_count(
1077
+ group_ray_cap,
1078
+ len(group),
1079
+ n_surf,
1080
+ track_stats=track_stats,
1081
+ gpu_raygen=gpu_raygen,
1082
+ cuda_async=cuda_async,
1083
+ )
1084
+ if batch_slots > 1:
1085
+ if batch_workspaces is None or len(batch_workspaces) < batch_slots or batch_workspaces[0].ray_cap < group_ray_cap:
1086
+ batch_workspaces = [
1087
+ _create_matrix_gpu_workspace(
1088
+ group_ray_cap,
1089
+ n_surf,
1090
+ cuda_async=True,
1091
+ gpu_raygen=True,
1092
+ track_stats=track_stats,
1093
+ )
1094
+ for _ in range(batch_slots)
1095
+ ]
1096
+ active_workspaces = batch_workspaces[:batch_slots]
1097
+ _run_matrix_gpu_batched_group(
1098
+ group,
1099
+ meshes,
1100
+ emitters,
1101
+ prepared_solver,
1102
+ scene,
1103
+ d_scene,
1104
+ result,
1105
+ stats_result,
1106
+ active_workspaces,
1107
+ samples=samples,
1108
+ rays=rays,
1109
+ seed=seed,
1110
+ gpu_raygen=gpu_raygen,
1111
+ cuda_async=cuda_async,
1112
+ flip_faces=flip_faces,
1113
+ reciprocity=reciprocity,
1114
+ tol=tol,
1115
+ tol_mode=tol_mode,
1116
+ min_iters=min_iters,
1117
+ convergence_interval=convergence_interval,
1118
+ max_iters=max_iters,
1119
+ return_stats=return_stats,
1120
+ areas=areas,
1121
+ use_bvh=use_bvh,
1122
+ )
1123
+ continue
1124
+
1125
+ _run_matrix_gpu_serial(
1126
+ group,
1127
+ meshes,
1128
+ emitters,
1129
+ prepared_solver,
1130
+ scene,
1131
+ d_scene,
1132
+ result,
1133
+ stats_result,
1134
+ serial_ws,
1135
+ samples=samples,
1136
+ rays=rays,
1137
+ seed=seed,
1138
+ gpu_raygen=gpu_raygen,
1139
+ cuda_async=cuda_async,
1140
+ flip_faces=flip_faces,
1141
+ reciprocity=reciprocity,
1142
+ tol=tol,
1143
+ tol_mode=tol_mode,
1144
+ min_iters=min_iters,
1145
+ convergence_interval=convergence_interval,
1146
+ max_iters=max_iters,
1147
+ return_stats=return_stats,
1148
+ areas=areas,
1149
+ use_bvh=use_bvh,
1150
+ )
1151
+ continue
1152
+
1153
+ _run_matrix_gpu_serial(
1154
+ [idx_emit],
1155
+ meshes,
1156
+ emitters,
1157
+ prepared_solver,
1158
+ scene,
1159
+ d_scene,
1160
+ result,
1161
+ stats_result,
1162
+ serial_ws,
1163
+ samples=samples,
1164
+ rays=rays,
1165
+ seed=seed,
1166
+ gpu_raygen=gpu_raygen,
1167
+ cuda_async=cuda_async,
1168
+ flip_faces=flip_faces,
1169
+ reciprocity=reciprocity,
1170
+ tol=tol,
1171
+ tol_mode=tol_mode,
1172
+ min_iters=min_iters,
1173
+ convergence_interval=convergence_interval,
1174
+ max_iters=max_iters,
1175
+ return_stats=return_stats,
1176
+ areas=areas,
1177
+ use_bvh=use_bvh,
1178
+ )
1179
+ idx_emit += 1
1180
+
1181
+ def _matrix_skip(idx_emit: int, reciprocity: bool) -> Tuple[int, int]:
1182
+ return (idx_emit, idx_emit + 1) if reciprocity else (idx_emit, 0)
1183
+
1184
+
1185
+ def outside_workflow_shareable(matrix_params: MatrixParams, sky_params: SkyParams) -> bool:
1186
+ """Return True when outside-workflow can reuse one traced ray set.
1187
+
1188
+ The shared outside-workflow path requires the matrix and sky solves to use
1189
+ identical ray-generation and execution settings so that a single set of
1190
+ rays is valid for both outputs. The following fields must match:
1191
+
1192
+ - ``samples``
1193
+ - ``rays``
1194
+ - ``seed``
1195
+ - ``bvh``
1196
+ - ``device``
1197
+ - ``cuda_async``
1198
+ - ``gpu_raygen``
1199
+
1200
+ The matrix solve must also keep ``flip_faces=False`` because the sky solve
1201
+ assumes outward emission.
1202
+ """
1203
+ shared_fields = ("samples", "rays", "seed", "bvh", "device", "cuda_async", "gpu_raygen")
1204
+ if bool(matrix_params.flip_faces):
1205
+ return False
1206
+ return all(getattr(matrix_params, key) == getattr(sky_params, key) for key in shared_fields)
1207
+
1208
+
1209
+ def view_factor_matrix_and_sky(
1210
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
1211
+ *,
1212
+ matrix_params: MatrixParams,
1213
+ sky_params: SkyParams,
1214
+ prepared: PreparedSolver | None = None,
1215
+ ) -> Tuple[Dict[str, Dict[str, float]], Dict[str, Dict[str, float]]]:
1216
+ """Compute scene view factors and sky VF from a shared set of rays.
1217
+
1218
+ This routine is intended for the outside-workflow case where the scene
1219
+ matrix and the sky visibility use the same sampling setup. Instead of
1220
+ tracing once for the matrix and then tracing a second time for sky, it:
1221
+
1222
+ 1. generates one ray set per emitter/iteration
1223
+ 2. traces each ray once against the scene
1224
+ 3. accumulates matrix hits from the first visible surface
1225
+ 4. classifies rays that miss all geometry into merged or discrete sky bins
1226
+
1227
+ The matrix and sky convergence checks remain independent. If one side
1228
+ converges earlier, tracing continues only to satisfy the other side, while
1229
+ still using the same per-iteration ray samples.
1230
+
1231
+ For compatible parameter sets, this is intended to be numerically
1232
+ equivalent to calling :func:`view_factor_matrix` and
1233
+ :func:`view_factor_to_tregenza_sky` separately with the same shared
1234
+ sampling configuration, but without doubling the ray tracing work.
1235
+
1236
+ When ``prepared`` is provided, the solver reuses cached prepared geometry,
1237
+ BVHs, ray tables and CUDA uploads across repeated calls on the same scene.
1238
+ """
1239
+ if not isinstance(matrix_params, MatrixParams):
1240
+ raise TypeError("matrix_params must be a MatrixParams instance")
1241
+ if not isinstance(sky_params, SkyParams):
1242
+ raise TypeError("sky_params must be a SkyParams instance")
1243
+ if not outside_workflow_shareable(matrix_params, sky_params):
1244
+ raise ValueError("matrix_params and sky_params are not compatible for shared tracing")
1245
+
1246
+ mp = matrix_params.as_dict()
1247
+ sp = sky_params.as_dict()
1248
+ samples = mp["samples"]
1249
+ rays = mp["rays"]
1250
+ seed = mp["seed"]
1251
+ device = mp["device"]
1252
+ cuda_async = mp["cuda_async"]
1253
+ gpu_raygen = mp["gpu_raygen"]
1254
+ reciprocity = mp["reciprocity"]
1255
+ sky_discrete = sp["discrete"]
1256
+ matrix_interval = max(1, int(mp["convergence_interval"]))
1257
+ sky_interval = max(1, int(sp["convergence_interval"]))
1258
+
1259
+ use_gpu = _resolve_device(device)
1260
+ prepared_solver = _ensure_prepared(meshes, prepared)
1261
+ use_bvh = _select_bvh(mp["bvh"], prepared_solver.total_faces)
1262
+ emitters = prepared_solver.get_emitters(samples=samples, rays=rays, flip_faces=False)
1263
+ bounds_center, bounds_extent = prepared_solver.get_mesh_bounds()
1264
+ scene = prepared_solver.get_scene(use_bvh=use_bvh)
1265
+ areas = [emitter.total_area for emitter in emitters] if reciprocity else None
1266
+
1267
+ vf_scene: Dict[str, Dict[str, float]] = {name: {} for name, _, _ in meshes}
1268
+ if sky_discrete:
1269
+ sky_keys = [f"Sky_Patch_{i}" for i in range(1, 146)]
1270
+ sky_vf: Dict[str, Dict[str, float]] = {name: {key: 0.0 for key in sky_keys} for name, _, _ in meshes}
1271
+ else:
1272
+ sky_vf = {name: {"Sky": 0.0} for name, _, _ in meshes}
1273
+
1274
+ d_scene = prepared_solver.get_device_scene(use_bvh=use_bvh) if use_gpu else None
1275
+
1276
+ n_surf = len(meshes)
1277
+ for idx_emit, (name_e, _, _) in enumerate(meshes):
1278
+ t0 = time.time()
1279
+ emitter = emitters[idx_emit]
1280
+ surf_active = _build_emitter_surface_mask(idx_emit, emitter, bounds_center, bounds_extent)
1281
+ receivers, recv_idx = _matrix_active_receivers(idx_emit, n_surf, reciprocity, surf_active)
1282
+ emit_sid, matrix_min_sid = _matrix_skip(idx_emit, reciprocity)
1283
+ n_rays_once = emitter.n_cells * rays
1284
+
1285
+ matrix_enabled = len(receivers) > 0
1286
+ sky_enabled = True
1287
+ matrix_done = not matrix_enabled
1288
+ sky_done = False
1289
+
1290
+ hits_f = np.zeros(n_surf, np.int64)
1291
+ hits_b = np.zeros(n_surf, np.int64)
1292
+ mean_f = np.zeros(n_surf, np.float64)
1293
+ mean_b = np.zeros(n_surf, np.float64)
1294
+ M2_f = np.zeros(n_surf, np.float64)
1295
+ M2_b = np.zeros(n_surf, np.float64)
1296
+ prev_f = prev_b = None
1297
+ matrix_total_rays = 0
1298
+ matrix_iters_done = 0
1299
+
1300
+ counts_total = np.zeros(145, np.int64) if sky_discrete else None
1301
+ mean_bins = np.zeros(145, np.float64) if sky_discrete else None
1302
+ M2_bins = np.zeros(145, np.float64) if sky_discrete else None
1303
+ upward_total = 0
1304
+ mean_sky = 0.0
1305
+ M2_sky = 0.0
1306
+ prev_frac = None
1307
+ sky_total_rays = 0
1308
+ sky_iters_done = 0
1309
+
1310
+ trace_iters = 0
1311
+ total_traced_rays = 0
1312
+ max_trace_iters = max(int(mp["max_iters"]), int(sp["max_iters"]))
1313
+
1314
+ if use_gpu:
1315
+ emitter_dev = prepared_solver.get_device_emitter(idx_emit, samples=samples, rays=rays, flip_faces=False)
1316
+ d_orig = cuda.device_array((n_rays_once, 3), dtype=np.float32)
1317
+ d_dirs = cuda.device_array((n_rays_once, 3), dtype=np.float32)
1318
+ d_hit_sid = cuda.device_array(n_rays_once, dtype=np.int32)
1319
+ d_hit_front = cuda.device_array(n_rays_once, dtype=np.uint8)
1320
+ d_any_hit = cuda.device_array(n_rays_once, dtype=np.uint8)
1321
+ d_surf_active = cuda.to_device(surf_active)
1322
+ d_hf = cuda.device_array(n_surf, dtype=np.int64)
1323
+ d_hb = cuda.device_array(n_surf, dtype=np.int64)
1324
+ d_counts = cuda.device_array(145, dtype=np.int32) if sky_discrete else None
1325
+ d_upward = cuda.device_array(1, dtype=np.int32) if not sky_discrete else None
1326
+ stream = cuda.stream() if cuda_async else None
1327
+ if cuda_async:
1328
+ h_hf = cuda.pinned_array(n_surf, dtype=np.int64)
1329
+ h_hb = cuda.pinned_array(n_surf, dtype=np.int64)
1330
+ h_counts = cuda.pinned_array(145, dtype=np.int32) if sky_discrete else None
1331
+ h_upward = cuda.pinned_array(1, dtype=np.int32) if not sky_discrete else None
1332
+ if not gpu_raygen:
1333
+ h_orig = cuda.pinned_array((n_rays_once, 3), dtype=np.float32)
1334
+ h_dirs = cuda.pinned_array((n_rays_once, 3), dtype=np.float32)
1335
+ else:
1336
+ h_hf = h_hb = h_counts = h_upward = None
1337
+ if not gpu_raygen:
1338
+ h_orig, h_dirs = _host_ray_buffers(n_rays_once)
1339
+ blocks, threads = _compute_cuda_launch(n_rays_once, None)
1340
+ surf_blocks, surf_threads = _compute_cuda_launch(n_surf, threads)
1341
+ sky_blocks, sky_threads = _compute_cuda_launch(145 if sky_discrete else 1, threads)
1342
+ else:
1343
+ orig, dire = _host_ray_buffers(n_rays_once)
1344
+ hit_sid_iter = np.empty(n_rays_once, dtype=np.int32)
1345
+ front_iter = np.empty(n_rays_once, dtype=np.uint8)
1346
+ any_hit_iter = np.empty(n_rays_once, dtype=np.uint8)
1347
+ hits_f_iter = np.empty(n_surf, dtype=np.int64)
1348
+ hits_b_iter = np.empty(n_surf, dtype=np.int64)
1349
+ hitmask_iter = np.empty(n_rays_once, dtype=np.uint8)
1350
+ counts_iter = np.empty(145, dtype=np.int64) if sky_discrete else None
1351
+
1352
+ for itr in range(max_trace_iters):
1353
+ if matrix_done and sky_done:
1354
+ break
1355
+
1356
+ rng = np.random.default_rng(seed + idx_emit + itr)
1357
+ cp_grid = rng.random(2, dtype=np.float32)
1358
+ cp_dims = rng.random(5, dtype=np.float32)
1359
+
1360
+ if use_gpu:
1361
+ if gpu_raygen:
1362
+ launch = kernel_build_rays[blocks, threads, stream] if cuda_async else kernel_build_rays[blocks, threads]
1363
+ launch(
1364
+ emitter_dev.u_grid, emitter_dev.v_grid,
1365
+ emitter_dev.halton_tri, emitter_dev.halton_u, emitter_dev.halton_v,
1366
+ emitter_dev.halton_r1, emitter_dev.halton_r2, emitter_dev.cdf,
1367
+ emitter_dev.tri_a, emitter_dev.tri_e1, emitter_dev.tri_e2,
1368
+ emitter_dev.tri_u, emitter_dev.tri_v, emitter_dev.tri_n, emitter_dev.tri_origin_eps,
1369
+ rays, d_orig, d_dirs, cp_grid, cp_dims,
1370
+ )
1371
+ else:
1372
+ _build_rays_host(emitter, rays, h_orig, h_dirs, cp_grid, cp_dims)
1373
+ if cuda_async:
1374
+ d_orig.copy_to_device(h_orig, stream=stream)
1375
+ d_dirs.copy_to_device(h_dirs, stream=stream)
1376
+ else:
1377
+ d_orig.copy_to_device(h_orig)
1378
+ d_dirs.copy_to_device(h_dirs)
1379
+
1380
+ if not matrix_done and not sky_done:
1381
+ if scene.use_bvh:
1382
+ trace = kernel_trace_bvh_combined[blocks, threads, stream] if cuda_async else kernel_trace_bvh_combined[blocks, threads]
1383
+ trace(
1384
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.normals, d_scene.sid, d_surf_active,
1385
+ d_scene.bb_min, d_scene.bb_max, d_scene.left, d_scene.right, d_scene.start, d_scene.count,
1386
+ emit_sid, matrix_min_sid, d_hit_sid, d_hit_front, d_any_hit,
1387
+ )
1388
+ else:
1389
+ trace = kernel_trace_combined[blocks, threads, stream] if cuda_async else kernel_trace_combined[blocks, threads]
1390
+ trace(
1391
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.normals, d_scene.sid, d_surf_active,
1392
+ emit_sid, matrix_min_sid, d_hit_sid, d_hit_front, d_any_hit,
1393
+ )
1394
+
1395
+ zero_hits = kernel_zero_i64[surf_blocks, surf_threads, stream] if cuda_async else kernel_zero_i64[surf_blocks, surf_threads]
1396
+ zero_hits(d_hf)
1397
+ zero_hits(d_hb)
1398
+ reduce_kernel = kernel_reduce_hits[blocks, threads, stream] if cuda_async else kernel_reduce_hits[blocks, threads]
1399
+ reduce_kernel(d_hit_sid, d_hit_front, d_hf, d_hb, n_surf)
1400
+
1401
+ if sky_discrete:
1402
+ zero_sky = kernel_zero_i32[sky_blocks, sky_threads, stream] if cuda_async else kernel_zero_i32[sky_blocks, sky_threads]
1403
+ zero_sky(d_counts)
1404
+ sky_kernel = kernel_bin_tregenza[blocks, threads, stream] if cuda_async else kernel_bin_tregenza[blocks, threads]
1405
+ sky_kernel(d_dirs, d_any_hit, d_counts)
1406
+ else:
1407
+ zero_sky = kernel_zero_i32[sky_blocks, sky_threads, stream] if cuda_async else kernel_zero_i32[sky_blocks, sky_threads]
1408
+ zero_sky(d_upward)
1409
+ sky_kernel = kernel_count_upward_misses[blocks, threads, stream] if cuda_async else kernel_count_upward_misses[blocks, threads]
1410
+ sky_kernel(d_dirs, d_any_hit, d_upward)
1411
+
1412
+ if cuda_async:
1413
+ d_hf.copy_to_host(h_hf, stream=stream)
1414
+ d_hb.copy_to_host(h_hb, stream=stream)
1415
+ if sky_discrete:
1416
+ d_counts.copy_to_host(h_counts, stream=stream)
1417
+ else:
1418
+ d_upward.copy_to_host(h_upward, stream=stream)
1419
+ stream.synchronize()
1420
+ hits_f_iter = np.asarray(h_hf)
1421
+ hits_b_iter = np.asarray(h_hb)
1422
+ if sky_discrete:
1423
+ counts_iter_arr = np.asarray(h_counts, dtype=np.int64)
1424
+ else:
1425
+ upward_iter = int(h_upward[0])
1426
+ else:
1427
+ cuda.synchronize()
1428
+ hits_f_iter = d_hf.copy_to_host()
1429
+ hits_b_iter = d_hb.copy_to_host()
1430
+ if sky_discrete:
1431
+ counts_iter_arr = d_counts.copy_to_host().astype(np.int64)
1432
+ else:
1433
+ upward_iter = int(d_upward.copy_to_host()[0])
1434
+ elif not matrix_done:
1435
+ zero_hits = kernel_zero_i64[surf_blocks, surf_threads, stream] if cuda_async else kernel_zero_i64[surf_blocks, surf_threads]
1436
+ zero_hits(d_hf)
1437
+ zero_hits(d_hb)
1438
+ if scene.use_bvh:
1439
+ trace = kernel_trace_bvh_firsthit[blocks, threads, stream] if cuda_async else kernel_trace_bvh_firsthit[blocks, threads]
1440
+ trace(
1441
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.normals, d_scene.sid, d_surf_active,
1442
+ d_scene.bb_min, d_scene.bb_max, d_scene.left, d_scene.right, d_scene.start, d_scene.count,
1443
+ emit_sid, matrix_min_sid, d_hit_sid, d_hit_front,
1444
+ )
1445
+ else:
1446
+ trace = kernel_trace_firsthit[blocks, threads, stream] if cuda_async else kernel_trace_firsthit[blocks, threads]
1447
+ trace(d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.normals, d_scene.sid, d_surf_active, emit_sid, matrix_min_sid, d_hit_sid, d_hit_front)
1448
+ reduce_kernel = kernel_reduce_hits[blocks, threads, stream] if cuda_async else kernel_reduce_hits[blocks, threads]
1449
+ reduce_kernel(d_hit_sid, d_hit_front, d_hf, d_hb, n_surf)
1450
+
1451
+ if cuda_async:
1452
+ d_hf.copy_to_host(h_hf, stream=stream)
1453
+ d_hb.copy_to_host(h_hb, stream=stream)
1454
+ stream.synchronize()
1455
+ hits_f_iter = np.asarray(h_hf)
1456
+ hits_b_iter = np.asarray(h_hb)
1457
+ else:
1458
+ cuda.synchronize()
1459
+ hits_f_iter = d_hf.copy_to_host()
1460
+ hits_b_iter = d_hb.copy_to_host()
1461
+ else:
1462
+ zero_sky = kernel_zero_i32[sky_blocks, sky_threads, stream] if cuda_async else kernel_zero_i32[sky_blocks, sky_threads]
1463
+ if sky_discrete:
1464
+ zero_sky(d_counts)
1465
+ if scene.use_bvh:
1466
+ trace = kernel_trace_bvh_tregenza[blocks, threads, stream] if cuda_async else kernel_trace_bvh_tregenza[blocks, threads]
1467
+ trace(
1468
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.sid, d_surf_active,
1469
+ d_scene.bb_min, d_scene.bb_max, d_scene.left, d_scene.right, d_scene.start, d_scene.count,
1470
+ idx_emit, 0, d_counts,
1471
+ )
1472
+ else:
1473
+ trace = kernel_trace_tregenza[blocks, threads, stream] if cuda_async else kernel_trace_tregenza[blocks, threads]
1474
+ trace(d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.sid, d_surf_active, idx_emit, 0, d_counts)
1475
+ else:
1476
+ zero_sky(d_upward)
1477
+ if scene.use_bvh:
1478
+ trace = kernel_trace_bvh_count_upward[blocks, threads, stream] if cuda_async else kernel_trace_bvh_count_upward[blocks, threads]
1479
+ trace(
1480
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.sid, d_surf_active,
1481
+ d_scene.bb_min, d_scene.bb_max, d_scene.left, d_scene.right, d_scene.start, d_scene.count,
1482
+ idx_emit, 0, d_upward,
1483
+ )
1484
+ else:
1485
+ trace = kernel_trace_count_upward[blocks, threads, stream] if cuda_async else kernel_trace_count_upward[blocks, threads]
1486
+ trace(d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.sid, d_surf_active, idx_emit, 0, d_upward)
1487
+
1488
+ if cuda_async:
1489
+ if sky_discrete:
1490
+ d_counts.copy_to_host(h_counts, stream=stream)
1491
+ else:
1492
+ d_upward.copy_to_host(h_upward, stream=stream)
1493
+ stream.synchronize()
1494
+ if sky_discrete:
1495
+ counts_iter_arr = np.asarray(h_counts, dtype=np.int64)
1496
+ else:
1497
+ upward_iter = int(h_upward[0])
1498
+ else:
1499
+ cuda.synchronize()
1500
+ if sky_discrete:
1501
+ counts_iter_arr = d_counts.copy_to_host().astype(np.int64)
1502
+ else:
1503
+ upward_iter = int(d_upward.copy_to_host()[0])
1504
+ else:
1505
+ _build_rays_host(emitter, rays, orig, dire, cp_grid, cp_dims)
1506
+ if not matrix_done and not sky_done:
1507
+ if scene.use_bvh:
1508
+ trace_cpu_bvh_combined(
1509
+ orig, dire, scene.v0, scene.e1, scene.e2, scene.normals, scene.sid, surf_active,
1510
+ scene.bb_min, scene.bb_max, scene.left, scene.right, scene.start, scene.count,
1511
+ emit_sid, matrix_min_sid, hit_sid_iter, front_iter, any_hit_iter,
1512
+ )
1513
+ else:
1514
+ trace_cpu_combined(
1515
+ orig, dire, scene.v0, scene.e1, scene.e2, scene.normals, scene.sid, surf_active,
1516
+ emit_sid, matrix_min_sid, hit_sid_iter, front_iter, any_hit_iter,
1517
+ )
1518
+ reduce_first_hits(hit_sid_iter, front_iter, hits_f_iter, hits_b_iter)
1519
+ if sky_discrete:
1520
+ bin_tregenza_cpu(dire, any_hit_iter, counts_iter)
1521
+ counts_iter_arr = counts_iter
1522
+ else:
1523
+ upward_iter = int(count_upward_misses_cpu(dire, any_hit_iter))
1524
+ elif not matrix_done:
1525
+ if scene.use_bvh:
1526
+ trace_cpu_bvh_firsthit(
1527
+ orig, dire, scene.v0, scene.e1, scene.e2, scene.normals, scene.sid, surf_active,
1528
+ scene.bb_min, scene.bb_max, scene.left, scene.right, scene.start, scene.count,
1529
+ emit_sid, matrix_min_sid, hit_sid_iter, front_iter,
1530
+ )
1531
+ else:
1532
+ trace_cpu_firsthit(orig, dire, scene.v0, scene.e1, scene.e2, scene.normals, scene.sid, surf_active, emit_sid, matrix_min_sid, hit_sid_iter, front_iter)
1533
+ reduce_first_hits(hit_sid_iter, front_iter, hits_f_iter, hits_b_iter)
1534
+ else:
1535
+ if scene.use_bvh:
1536
+ trace_cpu_bvh_hitmask(
1537
+ orig, dire, scene.v0, scene.e1, scene.e2, scene.sid, surf_active,
1538
+ scene.bb_min, scene.bb_max, scene.left, scene.right, scene.start, scene.count,
1539
+ idx_emit, 0, hitmask_iter,
1540
+ )
1541
+ else:
1542
+ trace_cpu_hitmask(orig, dire, scene.v0, scene.e1, scene.e2, scene.sid, surf_active, idx_emit, 0, hitmask_iter)
1543
+ if sky_discrete:
1544
+ bin_tregenza_cpu(dire, hitmask_iter, counts_iter)
1545
+ counts_iter_arr = counts_iter
1546
+ else:
1547
+ upward_iter = int(count_upward_misses_cpu(dire, hitmask_iter))
1548
+
1549
+ trace_iters += 1
1550
+ total_traced_rays += n_rays_once
1551
+
1552
+ if not matrix_done:
1553
+ hits_f += hits_f_iter
1554
+ hits_b += hits_b_iter
1555
+ matrix_total_rays += n_rays_once
1556
+ matrix_iters_done += 1
1557
+
1558
+ f_iter = hits_f_iter.astype(np.float64) / float(n_rays_once)
1559
+ b_iter = hits_b_iter.astype(np.float64) / float(n_rays_once)
1560
+ delta_f = f_iter - mean_f
1561
+ mean_f += delta_f / matrix_iters_done
1562
+ M2_f += delta_f * (f_iter - mean_f)
1563
+ delta_b = b_iter - mean_b
1564
+ mean_b += delta_b / matrix_iters_done
1565
+ M2_b += delta_b * (b_iter - mean_b)
1566
+
1567
+ check_matrix = _convergence_checkpoint(
1568
+ matrix_iters_done,
1569
+ min_iters=int(mp["min_iters"]),
1570
+ interval=matrix_interval if use_gpu else 1,
1571
+ max_iters=int(mp["max_iters"]),
1572
+ needs_variance=(mp["tol_mode"] == "stderr"),
1573
+ )
1574
+ if mp["tol_mode"] == "delta":
1575
+ curr_f = hits_f / float(matrix_total_rays)
1576
+ curr_b = hits_b / float(matrix_total_rays)
1577
+ if check_matrix and prev_f is not None:
1578
+ if np.all(np.abs(curr_f - prev_f) < float(mp["tol"])) and np.all(np.abs(curr_b - prev_b) < float(mp["tol"])):
1579
+ matrix_done = True
1580
+ if check_matrix:
1581
+ prev_f = curr_f.copy()
1582
+ prev_b = curr_b.copy()
1583
+ elif mp["tol_mode"] == "stderr":
1584
+ if check_matrix:
1585
+ se_f = np.sqrt(np.maximum(M2_f / (matrix_iters_done - 1), 0.0) / matrix_iters_done)
1586
+ se_b = np.sqrt(np.maximum(M2_b / (matrix_iters_done - 1), 0.0) / matrix_iters_done)
1587
+ if np.all(se_f[recv_idx] <= float(mp["tol"])) and np.all(se_b[recv_idx] <= float(mp["tol"])):
1588
+ matrix_done = True
1589
+ else:
1590
+ raise ValueError(f"Unknown tol_mode: {mp['tol_mode']}")
1591
+ if matrix_iters_done >= int(mp["max_iters"]):
1592
+ matrix_done = True
1593
+
1594
+ if not sky_done:
1595
+ sky_total_rays += n_rays_once
1596
+ sky_iters_done += 1
1597
+ check_sky = _convergence_checkpoint(
1598
+ sky_iters_done,
1599
+ min_iters=int(sp["min_iters"]),
1600
+ interval=sky_interval if use_gpu else 1,
1601
+ max_iters=int(sp["max_iters"]),
1602
+ needs_variance=(sp["tol_mode"] == "stderr"),
1603
+ )
1604
+
1605
+ if sky_discrete:
1606
+ counts_total += counts_iter_arr
1607
+ frac_iter = counts_iter_arr.astype(np.float64) / float(n_rays_once)
1608
+ sky_iter = float(frac_iter.sum())
1609
+ delta = frac_iter - mean_bins
1610
+ mean_bins += delta / sky_iters_done
1611
+ M2_bins += delta * (frac_iter - mean_bins)
1612
+ delta_sky = sky_iter - mean_sky
1613
+ mean_sky += delta_sky / sky_iters_done
1614
+ M2_sky += delta_sky * (sky_iter - mean_sky)
1615
+
1616
+ if sp["tol_mode"] == "delta":
1617
+ if check_sky:
1618
+ curr = counts_total.astype(np.float64) / float(sky_total_rays)
1619
+ if prev_frac is not None and np.all(np.abs(curr - prev_frac) < float(sp["tol"])):
1620
+ sky_done = True
1621
+ prev_frac = curr.copy()
1622
+ elif sp["tol_mode"] == "stderr":
1623
+ if check_sky:
1624
+ se_bins = np.sqrt(np.maximum(M2_bins / (sky_iters_done - 1), 0.0) / sky_iters_done)
1625
+ if np.all(se_bins <= float(sp["tol"])):
1626
+ sky_done = True
1627
+ else:
1628
+ raise ValueError(f"Unknown tol_mode: {sp['tol_mode']}")
1629
+ else:
1630
+ upward_total += upward_iter
1631
+ frac_iter = upward_iter / float(n_rays_once)
1632
+ delta_sky = frac_iter - mean_sky
1633
+ mean_sky += delta_sky / sky_iters_done
1634
+ M2_sky += delta_sky * (frac_iter - mean_sky)
1635
+
1636
+ if sp["tol_mode"] == "delta":
1637
+ if check_sky:
1638
+ curr = upward_total / float(sky_total_rays)
1639
+ if prev_frac is not None and abs(curr - prev_frac) < float(sp["tol"]):
1640
+ sky_done = True
1641
+ prev_frac = curr
1642
+ elif sp["tol_mode"] == "stderr":
1643
+ if check_sky:
1644
+ se_sky = max(M2_sky / (sky_iters_done - 1), 0.0) ** 0.5 / sky_iters_done**0.5
1645
+ if se_sky <= float(sp["tol"]):
1646
+ sky_done = True
1647
+ else:
1648
+ raise ValueError(f"Unknown tol_mode: {sp['tol_mode']}")
1649
+ if sky_iters_done >= int(sp["max_iters"]):
1650
+ sky_done = True
1651
+
1652
+ if matrix_iters_done > 1:
1653
+ se_f = np.sqrt(np.maximum(M2_f / (matrix_iters_done - 1), 0.0) / matrix_iters_done)
1654
+ se_b = np.sqrt(np.maximum(M2_b / (matrix_iters_done - 1), 0.0) / matrix_iters_done)
1655
+ else:
1656
+ se_f = np.full_like(mean_f, np.inf, dtype=np.float64)
1657
+ se_b = np.full_like(mean_b, np.inf, dtype=np.float64)
1658
+
1659
+ row: Dict[str, float] = {}
1660
+ for j in receivers:
1661
+ name_r, _, _ = meshes[j]
1662
+ f = hits_f[j] / float(matrix_total_rays) if matrix_total_rays > 0 else 0.0
1663
+ b = hits_b[j] / float(matrix_total_rays) if matrix_total_rays > 0 else 0.0
1664
+ if f > 0.0:
1665
+ row[f"{name_r}_front"] = f
1666
+ if reciprocity and areas is not None and areas[j] > 0.0:
1667
+ vf_scene[name_r][f"{name_e}_front"] = f * (areas[idx_emit] / areas[j])
1668
+ if b > 0.0:
1669
+ row[f"{name_r}_back"] = b
1670
+ vf_scene[name_e].update(row)
1671
+
1672
+ if sky_total_rays > 0:
1673
+ if sky_discrete:
1674
+ frac = counts_total.astype(np.float64) / float(sky_total_rays)
1675
+ sky_vf[name_e].update({f"Sky_Patch_{i+1}": float(frac[i]) for i in range(145)})
1676
+ else:
1677
+ sky_vf[name_e]["Sky"] = float(upward_total / float(sky_total_rays))
1678
+
1679
+ msg = (
1680
+ f"({idx_emit+1}/{len(meshes)}) [{name_e}] traced {trace_iters} iter, {total_traced_rays:,} rays -> "
1681
+ f"{time.time() - t0:0.3f}s (scene={matrix_iters_done} iter, sky={sky_iters_done} iter, "
1682
+ f"BVH={'builtin' if use_bvh else 'off'}, device={'gpu' if use_gpu else 'cpu'})"
1683
+ )
1684
+ _log(msg)
1685
+
1686
+ return vf_scene, sky_vf
1687
+
1688
+
1689
+ def view_factor_matrix(
1690
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
1691
+ params: MatrixParams,
1692
+ *,
1693
+ prepared: PreparedSolver | None = None,
1694
+ ):
1695
+ if not isinstance(params, MatrixParams):
1696
+ raise TypeError("params must be a MatrixParams instance")
1697
+
1698
+ p = params.as_dict()
1699
+ samples = p["samples"]
1700
+ rays = p["rays"]
1701
+ seed = p["seed"]
1702
+ device = p["device"]
1703
+ cuda_async = p["cuda_async"]
1704
+ gpu_raygen = p["gpu_raygen"]
1705
+ max_iters = p["max_iters"]
1706
+ tol = p["tol"]
1707
+ tol_mode = p["tol_mode"]
1708
+ min_iters = p["min_iters"]
1709
+ convergence_interval = max(1, int(p["convergence_interval"]))
1710
+ reciprocity = p["reciprocity"]
1711
+ enforce_reciprocity_rowsum = p["enforce_reciprocity_rowsum"]
1712
+ flip_faces = p["flip_faces"]
1713
+ return_stats = False
1714
+
1715
+ use_gpu = _resolve_device(device)
1716
+ prepared_solver = _ensure_prepared(meshes, prepared)
1717
+ use_bvh = _select_bvh(p["bvh"], prepared_solver.total_faces)
1718
+ result: Dict[str, Dict[str, float]] = {name: {} for name, _, _ in meshes}
1719
+ stats_result: Dict[str, Dict[str, float]] = {} if return_stats else None # type: ignore[assignment]
1720
+ emitters = prepared_solver.get_emitters(samples=samples, rays=rays, flip_faces=flip_faces)
1721
+ areas = [emitter.total_area for emitter in emitters] if reciprocity else None
1722
+ bounds_center, bounds_extent = prepared_solver.get_mesh_bounds()
1723
+ scene = prepared_solver.get_scene(use_bvh=use_bvh)
1724
+
1725
+ d_scene = prepared_solver.get_device_scene(use_bvh=use_bvh) if use_gpu else None
1726
+ if use_gpu:
1727
+ _run_view_factor_matrix_gpu(
1728
+ meshes,
1729
+ emitters,
1730
+ prepared_solver,
1731
+ scene,
1732
+ d_scene,
1733
+ result,
1734
+ stats_result,
1735
+ samples=samples,
1736
+ rays=rays,
1737
+ seed=seed,
1738
+ gpu_raygen=gpu_raygen,
1739
+ cuda_async=cuda_async,
1740
+ flip_faces=flip_faces,
1741
+ reciprocity=reciprocity,
1742
+ tol=tol,
1743
+ tol_mode=tol_mode,
1744
+ min_iters=min_iters,
1745
+ convergence_interval=convergence_interval,
1746
+ max_iters=max_iters,
1747
+ return_stats=return_stats,
1748
+ areas=areas,
1749
+ use_bvh=use_bvh,
1750
+ )
1751
+ if enforce_reciprocity_rowsum:
1752
+ _enforce_reciprocity_and_rowsum(result, meshes, areas)
1753
+ if return_stats:
1754
+ return result, stats_result or {}
1755
+ return result
1756
+
1757
+ n_surf = len(meshes)
1758
+ for idx_emit, (name_e, _, _) in enumerate(meshes):
1759
+ t_tot = time.time()
1760
+ emitter = emitters[idx_emit]
1761
+ surf_active = _build_emitter_surface_mask(idx_emit, emitter, bounds_center, bounds_extent)
1762
+ receivers, recv_idx = _matrix_active_receivers(idx_emit, n_surf, reciprocity, surf_active)
1763
+ if not receivers:
1764
+ msg = f"({idx_emit+1}/{len(meshes)}) [{name_e}] 0 iter, 0 rays -> 0.000s (BVH={'builtin' if use_bvh else 'off'}, device={'gpu' if use_gpu else 'cpu'})"
1765
+ _log(msg)
1766
+ continue
1767
+
1768
+ emit_sid, min_sid = _matrix_skip(idx_emit, reciprocity)
1769
+ n_rays_once = emitter.n_cells * rays
1770
+ hits_f = np.zeros(n_surf, np.int64)
1771
+ hits_b = np.zeros(n_surf, np.int64)
1772
+ mean_f = np.zeros(n_surf, np.float64)
1773
+ mean_b = np.zeros(n_surf, np.float64)
1774
+ M2_f = np.zeros(n_surf, np.float64)
1775
+ M2_b = np.zeros(n_surf, np.float64)
1776
+ prev_f = prev_b = None
1777
+ total_rays = 0
1778
+ iters_done = 0
1779
+
1780
+ if use_gpu:
1781
+ emitter_dev = prepared_solver.get_device_emitter(idx_emit, samples=samples, rays=rays, flip_faces=flip_faces)
1782
+ d_orig = cuda.device_array((n_rays_once, 3), dtype=np.float32)
1783
+ d_dirs = cuda.device_array((n_rays_once, 3), dtype=np.float32)
1784
+ d_hit_sid = cuda.device_array(n_rays_once, dtype=np.int32)
1785
+ d_hit_front = cuda.device_array(n_rays_once, dtype=np.uint8)
1786
+ d_surf_active = cuda.to_device(surf_active)
1787
+ d_hf = cuda.device_array(n_surf, dtype=np.int64)
1788
+ d_hb = cuda.device_array(n_surf, dtype=np.int64)
1789
+ stream = cuda.stream() if cuda_async else None
1790
+ if cuda_async:
1791
+ h_hf = cuda.pinned_array(n_surf, dtype=np.int64)
1792
+ h_hb = cuda.pinned_array(n_surf, dtype=np.int64)
1793
+ if not gpu_raygen:
1794
+ h_orig = cuda.pinned_array((n_rays_once, 3), dtype=np.float32)
1795
+ h_dirs = cuda.pinned_array((n_rays_once, 3), dtype=np.float32)
1796
+ else:
1797
+ h_hf = h_hb = None
1798
+ if not gpu_raygen:
1799
+ h_orig, h_dirs = _host_ray_buffers(n_rays_once)
1800
+ blocks, threads = _compute_cuda_launch(n_rays_once, None)
1801
+ zblocks, zthreads = _compute_cuda_launch(n_surf, threads)
1802
+ else:
1803
+ orig, dire = _host_ray_buffers(n_rays_once)
1804
+ hit_sid_iter = np.empty(n_rays_once, dtype=np.int32)
1805
+ front_iter = np.empty(n_rays_once, dtype=np.uint8)
1806
+ hits_f_iter = np.empty(n_surf, dtype=np.int64)
1807
+ hits_b_iter = np.empty(n_surf, dtype=np.int64)
1808
+
1809
+ for itr in range(max_iters):
1810
+ rng = np.random.default_rng(seed + idx_emit + itr)
1811
+ cp_grid = rng.random(2, dtype=np.float32)
1812
+ cp_dims = rng.random(5, dtype=np.float32)
1813
+
1814
+ if use_gpu:
1815
+ if gpu_raygen:
1816
+ launch = kernel_build_rays[blocks, threads, stream] if cuda_async else kernel_build_rays[blocks, threads]
1817
+ launch(
1818
+ emitter_dev.u_grid, emitter_dev.v_grid,
1819
+ emitter_dev.halton_tri, emitter_dev.halton_u, emitter_dev.halton_v,
1820
+ emitter_dev.halton_r1, emitter_dev.halton_r2, emitter_dev.cdf,
1821
+ emitter_dev.tri_a, emitter_dev.tri_e1, emitter_dev.tri_e2,
1822
+ emitter_dev.tri_u, emitter_dev.tri_v, emitter_dev.tri_n, emitter_dev.tri_origin_eps,
1823
+ rays, d_orig, d_dirs, cp_grid, cp_dims,
1824
+ )
1825
+ else:
1826
+ _build_rays_host(emitter, rays, h_orig, h_dirs, cp_grid, cp_dims)
1827
+ if cuda_async:
1828
+ d_orig.copy_to_device(h_orig, stream=stream)
1829
+ d_dirs.copy_to_device(h_dirs, stream=stream)
1830
+ else:
1831
+ d_orig.copy_to_device(h_orig)
1832
+ d_dirs.copy_to_device(h_dirs)
1833
+
1834
+ zero = kernel_zero_i64[zblocks, zthreads, stream] if cuda_async else kernel_zero_i64[zblocks, zthreads]
1835
+ zero(d_hf)
1836
+ zero(d_hb)
1837
+ if scene.use_bvh:
1838
+ trace = kernel_trace_bvh_firsthit[blocks, threads, stream] if cuda_async else kernel_trace_bvh_firsthit[blocks, threads]
1839
+ trace(
1840
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.normals, d_scene.sid, d_surf_active,
1841
+ d_scene.bb_min, d_scene.bb_max, d_scene.left, d_scene.right, d_scene.start, d_scene.count,
1842
+ emit_sid, min_sid, d_hit_sid, d_hit_front,
1843
+ )
1844
+ else:
1845
+ trace = kernel_trace_firsthit[blocks, threads, stream] if cuda_async else kernel_trace_firsthit[blocks, threads]
1846
+ trace(d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.normals, d_scene.sid, d_surf_active, emit_sid, min_sid, d_hit_sid, d_hit_front)
1847
+ reduce_kernel = kernel_reduce_hits[blocks, threads, stream] if cuda_async else kernel_reduce_hits[blocks, threads]
1848
+ reduce_kernel(d_hit_sid, d_hit_front, d_hf, d_hb, n_surf)
1849
+
1850
+ if cuda_async:
1851
+ d_hf.copy_to_host(h_hf, stream=stream)
1852
+ d_hb.copy_to_host(h_hb, stream=stream)
1853
+ stream.synchronize()
1854
+ hits_f_iter = np.asarray(h_hf)
1855
+ hits_b_iter = np.asarray(h_hb)
1856
+ else:
1857
+ cuda.synchronize()
1858
+ hits_f_iter = d_hf.copy_to_host()
1859
+ hits_b_iter = d_hb.copy_to_host()
1860
+ else:
1861
+ _build_rays_host(emitter, rays, orig, dire, cp_grid, cp_dims)
1862
+ if scene.use_bvh:
1863
+ trace_cpu_bvh_firsthit(
1864
+ orig, dire, scene.v0, scene.e1, scene.e2, scene.normals, scene.sid, surf_active,
1865
+ scene.bb_min, scene.bb_max, scene.left, scene.right, scene.start, scene.count,
1866
+ emit_sid, min_sid, hit_sid_iter, front_iter,
1867
+ )
1868
+ else:
1869
+ trace_cpu_firsthit(orig, dire, scene.v0, scene.e1, scene.e2, scene.normals, scene.sid, surf_active, emit_sid, min_sid, hit_sid_iter, front_iter)
1870
+ reduce_first_hits(hit_sid_iter, front_iter, hits_f_iter, hits_b_iter)
1871
+
1872
+ hits_f += hits_f_iter
1873
+ hits_b += hits_b_iter
1874
+ total_rays += n_rays_once
1875
+ iters_done += 1
1876
+
1877
+ f_iter = hits_f_iter.astype(np.float64) / float(n_rays_once)
1878
+ b_iter = hits_b_iter.astype(np.float64) / float(n_rays_once)
1879
+ delta_f = f_iter - mean_f
1880
+ mean_f += delta_f / iters_done
1881
+ M2_f += delta_f * (f_iter - mean_f)
1882
+ delta_b = b_iter - mean_b
1883
+ mean_b += delta_b / iters_done
1884
+ M2_b += delta_b * (b_iter - mean_b)
1885
+
1886
+ check_matrix = _convergence_checkpoint(
1887
+ iters_done,
1888
+ min_iters=min_iters,
1889
+ interval=convergence_interval if use_gpu else 1,
1890
+ max_iters=max_iters,
1891
+ needs_variance=(tol_mode == "stderr"),
1892
+ )
1893
+ if tol_mode == "delta":
1894
+ curr_f = hits_f / float(total_rays)
1895
+ curr_b = hits_b / float(total_rays)
1896
+ if check_matrix and prev_f is not None:
1897
+ if np.all(np.abs(curr_f - prev_f) < tol) and np.all(np.abs(curr_b - prev_b) < tol):
1898
+ break
1899
+ if check_matrix:
1900
+ prev_f = curr_f.copy()
1901
+ prev_b = curr_b.copy()
1902
+ elif tol_mode == "stderr":
1903
+ if check_matrix:
1904
+ se_f = np.sqrt(np.maximum(M2_f / (iters_done - 1), 0.0) / iters_done)
1905
+ se_b = np.sqrt(np.maximum(M2_b / (iters_done - 1), 0.0) / iters_done)
1906
+ if np.all(se_f[recv_idx] <= tol) and np.all(se_b[recv_idx] <= tol):
1907
+ break
1908
+ else:
1909
+ raise ValueError(f"Unknown tol_mode: {tol_mode}")
1910
+
1911
+ if iters_done > 1:
1912
+ se_f = np.sqrt(np.maximum(M2_f / (iters_done - 1), 0.0) / iters_done)
1913
+ se_b = np.sqrt(np.maximum(M2_b / (iters_done - 1), 0.0) / iters_done)
1914
+ else:
1915
+ se_f = np.full_like(mean_f, np.inf, dtype=np.float64)
1916
+ se_b = np.full_like(mean_b, np.inf, dtype=np.float64)
1917
+
1918
+ row: Dict[str, float] = {}
1919
+ stats_row: Dict[str, float] = {}
1920
+ for j in receivers:
1921
+ name_r, _, _ = meshes[j]
1922
+ f = hits_f[j] / float(total_rays)
1923
+ b = hits_b[j] / float(total_rays)
1924
+ if f > 0.0:
1925
+ row[f"{name_r}_front"] = f
1926
+ if reciprocity and areas is not None and areas[j] > 0.0:
1927
+ result[name_r][f"{name_e}_front"] = f * (areas[idx_emit] / areas[j])
1928
+ if return_stats:
1929
+ stats_row[f"{name_r}_front"] = float(se_f[j])
1930
+ if b > 0.0:
1931
+ row[f"{name_r}_back"] = b
1932
+ if return_stats:
1933
+ stats_row[f"{name_r}_back"] = float(se_b[j])
1934
+ result[name_e].update(row)
1935
+ if return_stats:
1936
+ stats_result[name_e] = stats_row
1937
+
1938
+ msg = f"({idx_emit+1}/{len(meshes)}) [{name_e}] {iters_done} iter, {total_rays:,} rays -> {time.time() - t_tot:0.3f}s (BVH={'builtin' if use_bvh else 'off'}, device={'gpu' if use_gpu else 'cpu'})"
1939
+ _log(msg)
1940
+
1941
+ if enforce_reciprocity_rowsum:
1942
+ _enforce_reciprocity_and_rowsum(result, meshes, areas)
1943
+ if return_stats:
1944
+ return result, stats_result or {}
1945
+ return result
1946
+
1947
+
1948
+ def view_factor(sender, receiver, params: MatrixParams, *, prepared: PreparedSolver | None = None):
1949
+ senders = [sender] if isinstance(sender, tuple) else list(sender)
1950
+ receivers = [receiver] if isinstance(receiver, tuple) else list(receiver)
1951
+ meshes = senders + receivers
1952
+ vf_all = view_factor_matrix(meshes, params=params, prepared=prepared)
1953
+ sender_names = [s[0] for s in senders]
1954
+ return {name: vf_all.get(name, {}) for name in sender_names}
1955
+
1956
+
1957
+ def view_factor_to_tregenza_sky(
1958
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
1959
+ params: SkyParams,
1960
+ *,
1961
+ prepared: PreparedSolver | None = None,
1962
+ ):
1963
+ if not isinstance(params, SkyParams):
1964
+ raise TypeError("params must be a SkyParams instance")
1965
+ if len(meshes) == 0:
1966
+ raise ValueError("meshes must not be empty")
1967
+
1968
+ p = params.as_dict()
1969
+ samples = p["samples"]
1970
+ rays = p["rays"]
1971
+ seed = p["seed"]
1972
+ device = p["device"]
1973
+ cuda_async = p["cuda_async"]
1974
+ gpu_raygen = p["gpu_raygen"]
1975
+ max_iters = p["max_iters"]
1976
+ tol = p["tol"]
1977
+ tol_mode = p["tol_mode"]
1978
+ min_iters = p["min_iters"]
1979
+ convergence_interval = max(1, int(p["convergence_interval"]))
1980
+ discrete = p["discrete"]
1981
+
1982
+ use_gpu = _resolve_device(device)
1983
+ prepared_solver = _ensure_prepared(meshes, prepared)
1984
+ use_bvh = _select_bvh(p["bvh"], prepared_solver.total_faces)
1985
+ emitters = prepared_solver.get_emitters(samples=samples, rays=rays, flip_faces=False)
1986
+ bounds_center, bounds_extent = prepared_solver.get_mesh_bounds()
1987
+ scene = prepared_solver.get_scene(use_bvh=use_bvh)
1988
+ sky_name = "Sky"
1989
+ patch_prefix = "Sky_Patch_"
1990
+ sky_keys = [f"{patch_prefix}{i}" for i in range(1, 146)] if discrete else [sky_name]
1991
+ result: Dict[str, Dict[str, float]] = {name: {k: 0.0 for k in sky_keys} for name, _, _ in meshes}
1992
+
1993
+ d_scene = None
1994
+ if use_gpu:
1995
+ d_scene = prepared_solver.get_device_scene(use_bvh=use_bvh)
1996
+
1997
+ for idx_emit, (name_e, _, _) in enumerate(meshes):
1998
+ if len(meshes) <= 1:
1999
+ continue
2000
+ t0 = time.time()
2001
+ emitter = emitters[idx_emit]
2002
+ surf_active = _build_emitter_surface_mask(idx_emit, emitter, bounds_center, bounds_extent)
2003
+ n_rays_once = emitter.n_cells * rays
2004
+ counts_total = np.zeros(145, np.int64) if discrete else None
2005
+ mean_bins = np.zeros(145, np.float64) if discrete else None
2006
+ M2_bins = np.zeros(145, np.float64) if discrete else None
2007
+ upward_total = 0
2008
+ mean_sky = 0.0
2009
+ M2_sky = 0.0
2010
+ prev_frac = None
2011
+ total_rays = 0
2012
+ iters_done = 0
2013
+
2014
+ if use_gpu:
2015
+ emitter_dev = prepared_solver.get_device_emitter(idx_emit, samples=samples, rays=rays, flip_faces=False)
2016
+ d_orig = cuda.device_array((n_rays_once, 3), dtype=np.float32)
2017
+ d_dirs = cuda.device_array((n_rays_once, 3), dtype=np.float32)
2018
+ d_surf_active = cuda.to_device(surf_active)
2019
+ d_counts = cuda.device_array(145, dtype=np.int32) if discrete else None
2020
+ d_upward = cuda.device_array(1, dtype=np.int32) if not discrete else None
2021
+ stream = cuda.stream() if cuda_async else None
2022
+ if cuda_async:
2023
+ h_counts = cuda.pinned_array(145, dtype=np.int32) if discrete else None
2024
+ h_upward = cuda.pinned_array(1, dtype=np.int32) if not discrete else None
2025
+ if not gpu_raygen:
2026
+ h_orig = cuda.pinned_array((n_rays_once, 3), dtype=np.float32)
2027
+ h_dirs = cuda.pinned_array((n_rays_once, 3), dtype=np.float32)
2028
+ else:
2029
+ h_counts = h_upward = None
2030
+ if not gpu_raygen:
2031
+ h_orig, h_dirs = _host_ray_buffers(n_rays_once)
2032
+ blocks, threads = _compute_cuda_launch(n_rays_once, None)
2033
+ zblocks, zthreads = _compute_cuda_launch(145 if discrete else 1, threads)
2034
+ else:
2035
+ orig, dire = _host_ray_buffers(n_rays_once)
2036
+ hitmask = np.empty(n_rays_once, dtype=np.uint8)
2037
+ counts_iter = np.empty(145, dtype=np.int64) if discrete else None
2038
+
2039
+ for itr in range(max_iters):
2040
+ rng = np.random.default_rng(seed + idx_emit + itr)
2041
+ cp_grid = rng.random(2, dtype=np.float32)
2042
+ cp_dims = rng.random(5, dtype=np.float32)
2043
+
2044
+ if use_gpu:
2045
+ if gpu_raygen:
2046
+ launch = kernel_build_rays[blocks, threads, stream] if cuda_async else kernel_build_rays[blocks, threads]
2047
+ launch(
2048
+ emitter_dev.u_grid, emitter_dev.v_grid,
2049
+ emitter_dev.halton_tri, emitter_dev.halton_u, emitter_dev.halton_v,
2050
+ emitter_dev.halton_r1, emitter_dev.halton_r2, emitter_dev.cdf,
2051
+ emitter_dev.tri_a, emitter_dev.tri_e1, emitter_dev.tri_e2,
2052
+ emitter_dev.tri_u, emitter_dev.tri_v, emitter_dev.tri_n, emitter_dev.tri_origin_eps,
2053
+ rays, d_orig, d_dirs, cp_grid, cp_dims,
2054
+ )
2055
+ else:
2056
+ _build_rays_host(emitter, rays, h_orig, h_dirs, cp_grid, cp_dims)
2057
+ if cuda_async:
2058
+ d_orig.copy_to_device(h_orig, stream=stream)
2059
+ d_dirs.copy_to_device(h_dirs, stream=stream)
2060
+ else:
2061
+ d_orig.copy_to_device(h_orig)
2062
+ d_dirs.copy_to_device(h_dirs)
2063
+
2064
+ zero = kernel_zero_i32[zblocks, zthreads, stream] if cuda_async else kernel_zero_i32[zblocks, zthreads]
2065
+ if discrete:
2066
+ zero(d_counts)
2067
+ if scene.use_bvh:
2068
+ trace = kernel_trace_bvh_tregenza[blocks, threads, stream] if cuda_async else kernel_trace_bvh_tregenza[blocks, threads]
2069
+ trace(
2070
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.sid, d_surf_active,
2071
+ d_scene.bb_min, d_scene.bb_max, d_scene.left, d_scene.right, d_scene.start, d_scene.count,
2072
+ idx_emit, 0, d_counts,
2073
+ )
2074
+ else:
2075
+ trace = kernel_trace_tregenza[blocks, threads, stream] if cuda_async else kernel_trace_tregenza[blocks, threads]
2076
+ trace(d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.sid, d_surf_active, idx_emit, 0, d_counts)
2077
+ else:
2078
+ zero(d_upward)
2079
+ if scene.use_bvh:
2080
+ trace = kernel_trace_bvh_count_upward[blocks, threads, stream] if cuda_async else kernel_trace_bvh_count_upward[blocks, threads]
2081
+ trace(
2082
+ d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.sid, d_surf_active,
2083
+ d_scene.bb_min, d_scene.bb_max, d_scene.left, d_scene.right, d_scene.start, d_scene.count,
2084
+ idx_emit, 0, d_upward,
2085
+ )
2086
+ else:
2087
+ trace = kernel_trace_count_upward[blocks, threads, stream] if cuda_async else kernel_trace_count_upward[blocks, threads]
2088
+ trace(d_orig, d_dirs, d_scene.v0, d_scene.e1, d_scene.e2, d_scene.sid, d_surf_active, idx_emit, 0, d_upward)
2089
+
2090
+ if cuda_async:
2091
+ if discrete:
2092
+ d_counts.copy_to_host(h_counts, stream=stream)
2093
+ else:
2094
+ d_upward.copy_to_host(h_upward, stream=stream)
2095
+ stream.synchronize()
2096
+ if discrete:
2097
+ counts_iter_arr = np.asarray(h_counts, dtype=np.int64)
2098
+ else:
2099
+ upward_iter = int(h_upward[0])
2100
+ else:
2101
+ cuda.synchronize()
2102
+ if discrete:
2103
+ counts_iter_arr = d_counts.copy_to_host().astype(np.int64)
2104
+ else:
2105
+ upward_iter = int(d_upward.copy_to_host()[0])
2106
+ else:
2107
+ _build_rays_host(emitter, rays, orig, dire, cp_grid, cp_dims)
2108
+ if scene.use_bvh:
2109
+ trace_cpu_bvh_hitmask(
2110
+ orig, dire, scene.v0, scene.e1, scene.e2, scene.sid, surf_active,
2111
+ scene.bb_min, scene.bb_max, scene.left, scene.right, scene.start, scene.count,
2112
+ idx_emit, 0, hitmask,
2113
+ )
2114
+ else:
2115
+ trace_cpu_hitmask(orig, dire, scene.v0, scene.e1, scene.e2, scene.sid, surf_active, idx_emit, 0, hitmask)
2116
+ if discrete:
2117
+ bin_tregenza_cpu(dire, hitmask, counts_iter)
2118
+ counts_iter_arr = counts_iter
2119
+ else:
2120
+ upward_iter = int(count_upward_misses_cpu(dire, hitmask))
2121
+
2122
+ total_rays += n_rays_once
2123
+ iters_done += 1
2124
+ check_sky = _convergence_checkpoint(
2125
+ iters_done,
2126
+ min_iters=min_iters,
2127
+ interval=convergence_interval if use_gpu else 1,
2128
+ max_iters=max_iters,
2129
+ needs_variance=(tol_mode == "stderr"),
2130
+ )
2131
+ if discrete:
2132
+ counts_total += counts_iter_arr
2133
+ frac_iter = counts_iter_arr.astype(np.float64) / float(n_rays_once)
2134
+ sky_iter = float(frac_iter.sum())
2135
+ delta = frac_iter - mean_bins
2136
+ mean_bins += delta / iters_done
2137
+ M2_bins += delta * (frac_iter - mean_bins)
2138
+ delta_sky = sky_iter - mean_sky
2139
+ mean_sky += delta_sky / iters_done
2140
+ M2_sky += delta_sky * (sky_iter - mean_sky)
2141
+
2142
+ if tol_mode == "delta":
2143
+ if check_sky:
2144
+ curr = counts_total.astype(np.float64) / float(total_rays)
2145
+ if prev_frac is not None and np.all(np.abs(curr - prev_frac) < tol):
2146
+ break
2147
+ prev_frac = curr.copy()
2148
+ elif tol_mode == "stderr":
2149
+ if check_sky:
2150
+ se_bins = np.sqrt(np.maximum(M2_bins / (iters_done - 1), 0.0) / iters_done)
2151
+ if np.all(se_bins <= tol):
2152
+ break
2153
+ else:
2154
+ raise ValueError(f"Unknown tol_mode: {tol_mode}")
2155
+ else:
2156
+ upward_total += upward_iter
2157
+ frac_iter = upward_iter / float(n_rays_once)
2158
+ delta_sky = frac_iter - mean_sky
2159
+ mean_sky += delta_sky / iters_done
2160
+ M2_sky += delta_sky * (frac_iter - mean_sky)
2161
+
2162
+ if tol_mode == "delta":
2163
+ if check_sky:
2164
+ curr = upward_total / float(total_rays)
2165
+ if prev_frac is not None and abs(curr - prev_frac) < tol:
2166
+ break
2167
+ prev_frac = curr
2168
+ elif tol_mode == "stderr":
2169
+ if check_sky:
2170
+ se_sky = max(M2_sky / (iters_done - 1), 0.0) ** 0.5 / iters_done**0.5
2171
+ if se_sky <= tol:
2172
+ break
2173
+ else:
2174
+ raise ValueError(f"Unknown tol_mode: {tol_mode}")
2175
+
2176
+ if discrete:
2177
+ frac = counts_total.astype(np.float64) / float(max(1, total_rays))
2178
+ result[name_e].update({f"{patch_prefix}{i+1}": float(frac[i]) for i in range(145)})
2179
+ else:
2180
+ result[name_e][sky_name] = float(upward_total / float(max(1, total_rays)))
2181
+
2182
+ msg = f"({idx_emit+1}/{len(meshes)}) [{name_e}] {iters_done} iter, {total_rays:,} rays -> {time.time() - t0:0.3f}s (BVH={'builtin' if use_bvh else 'off'}, device={'gpu' if use_gpu else 'cpu'})"
2183
+ _log(msg)
2184
+
2185
+ return result
2186
+
2187
+
2188
+ __all__ = [
2189
+ "outside_workflow_shareable",
2190
+ "view_factor_matrix",
2191
+ "view_factor_matrix_and_sky",
2192
+ "view_factor",
2193
+ "view_factor_to_tregenza_sky",
2194
+ ]