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.
@@ -0,0 +1,442 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, List, Optional, Tuple
5
+
6
+ import numpy as np
7
+
8
+ from .bvh import build_bvh
9
+ from .halton import cached_halton, cached_halton_dims
10
+ from .helpers import grid_from_density
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class PreparedScene:
15
+ v0: np.ndarray
16
+ e1: np.ndarray
17
+ e2: np.ndarray
18
+ normals: np.ndarray
19
+ sid: np.ndarray
20
+ bb_min: Optional[np.ndarray]
21
+ bb_max: Optional[np.ndarray]
22
+ left: Optional[np.ndarray]
23
+ right: Optional[np.ndarray]
24
+ start: Optional[np.ndarray]
25
+ count: Optional[np.ndarray]
26
+ use_bvh: bool
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class PreparedEmitter:
31
+ tri_a: np.ndarray
32
+ tri_e1: np.ndarray
33
+ tri_e2: np.ndarray
34
+ tri_u: np.ndarray
35
+ tri_v: np.ndarray
36
+ tri_n: np.ndarray
37
+ tri_origin_eps: np.ndarray
38
+ plane_origin: np.ndarray
39
+ plane_normal: np.ndarray
40
+ plane_tol: float
41
+ plane_is_planar: bool
42
+ cdf: np.ndarray
43
+ total_area: float
44
+ g: int
45
+ u_grid: np.ndarray
46
+ v_grid: np.ndarray
47
+ halton_tri: np.ndarray
48
+ halton_u: np.ndarray
49
+ halton_v: np.ndarray
50
+ halton_r1: np.ndarray
51
+ halton_r2: np.ndarray
52
+
53
+ @property
54
+ def n_cells(self) -> int:
55
+ return int(self.u_grid.shape[0])
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class PreparedDeviceScene:
60
+ v0: Any
61
+ e1: Any
62
+ e2: Any
63
+ normals: Any
64
+ sid: Any
65
+ bb_min: Optional[Any]
66
+ bb_max: Optional[Any]
67
+ left: Optional[Any]
68
+ right: Optional[Any]
69
+ start: Optional[Any]
70
+ count: Optional[Any]
71
+ use_bvh: bool
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class PreparedDeviceEmitter:
76
+ u_grid: Any
77
+ v_grid: Any
78
+ halton_tri: Any
79
+ halton_u: Any
80
+ halton_v: Any
81
+ halton_r1: Any
82
+ halton_r2: Any
83
+ cdf: Any
84
+ tri_a: Any
85
+ tri_e1: Any
86
+ tri_e2: Any
87
+ tri_u: Any
88
+ tri_v: Any
89
+ tri_n: Any
90
+ tri_origin_eps: Any
91
+
92
+
93
+ def _safe_normalize(v: np.ndarray) -> np.ndarray:
94
+ n = np.linalg.norm(v, axis=1, keepdims=True)
95
+ n = np.maximum(n, 1e-12)
96
+ return v / n
97
+
98
+
99
+ def _triangle_frames(tri_n: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
100
+ tri_u = np.empty_like(tri_n, dtype=np.float32)
101
+ tri_v = np.empty_like(tri_n, dtype=np.float32)
102
+
103
+ axis_x = np.asarray([1.0, 0.0, 0.0], dtype=np.float32)
104
+ axis_y = np.asarray([0.0, 1.0, 0.0], dtype=np.float32)
105
+
106
+ for i in range(tri_n.shape[0]):
107
+ n = tri_n[i]
108
+ ref = axis_x if abs(float(n[0])) < 0.9 else axis_y
109
+ u = np.cross(ref, n).astype(np.float32)
110
+ u_len = float(np.linalg.norm(u))
111
+ if u_len <= 1e-12:
112
+ ref = axis_y if ref is axis_x else axis_x
113
+ u = np.cross(ref, n).astype(np.float32)
114
+ u_len = float(np.linalg.norm(u))
115
+ if u_len <= 1e-12:
116
+ tri_u[i] = axis_x
117
+ tri_v[i] = axis_y
118
+ continue
119
+ u /= u_len
120
+ tri_u[i] = u
121
+ tri_v[i] = np.cross(n, u).astype(np.float32)
122
+ return tri_u, tri_v
123
+
124
+
125
+ def _triangle_origin_eps(tri_e1: np.ndarray, tri_e2: np.ndarray) -> np.ndarray:
126
+ edge_a = np.linalg.norm(tri_e1, axis=1)
127
+ edge_b = np.linalg.norm(tri_e2, axis=1)
128
+ edge_c = np.linalg.norm(tri_e2 - tri_e1, axis=1)
129
+ scale = np.maximum(edge_a, np.maximum(edge_b, edge_c))
130
+ return np.maximum(scale * 1.0e-6, 1.0e-8).astype(np.float32, copy=False)
131
+
132
+
133
+ def _emitter_plane(
134
+ tri_a: np.ndarray,
135
+ tri_e1: np.ndarray,
136
+ tri_e2: np.ndarray,
137
+ tri_n: np.ndarray,
138
+ tri_origin_eps: np.ndarray,
139
+ ) -> Tuple[np.ndarray, np.ndarray, float, bool]:
140
+ plane_origin = np.zeros(3, dtype=np.float32)
141
+ plane_normal = np.zeros(3, dtype=np.float32)
142
+ plane_tol = float(max(1.0e-7, np.max(tri_origin_eps) if tri_origin_eps.size else 0.0))
143
+
144
+ if tri_a.shape[0] == 0:
145
+ return plane_origin, plane_normal, plane_tol, False
146
+
147
+ plane_origin = np.asarray(tri_a[0], dtype=np.float32)
148
+ plane_normal = np.asarray(tri_n[0], dtype=np.float32)
149
+ normal_len = float(np.linalg.norm(plane_normal))
150
+ if normal_len <= 1.0e-12:
151
+ return plane_origin, plane_normal, plane_tol, False
152
+
153
+ plane_normal = (plane_normal / normal_len).astype(np.float32, copy=False)
154
+ normal_align = tri_n @ plane_normal
155
+ if np.any(normal_align < (1.0 - 1.0e-4)):
156
+ return plane_origin, plane_normal, plane_tol, False
157
+
158
+ offsets = (
159
+ np.abs((tri_a - plane_origin) @ plane_normal),
160
+ np.abs((tri_a + tri_e1 - plane_origin) @ plane_normal),
161
+ np.abs((tri_a + tri_e2 - plane_origin) @ plane_normal),
162
+ )
163
+ max_dev = max(float(np.max(arr)) if arr.size else 0.0 for arr in offsets)
164
+ if max_dev > plane_tol:
165
+ return plane_origin, plane_normal, plane_tol, False
166
+
167
+ return plane_origin, plane_normal, plane_tol, True
168
+
169
+
170
+ def prepare_scene(
171
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
172
+ *,
173
+ use_bvh: bool,
174
+ ) -> PreparedScene:
175
+ v0s = []
176
+ e1s = []
177
+ e2s = []
178
+ normals = []
179
+ sids = []
180
+
181
+ for sid, (_, V, F) in enumerate(meshes):
182
+ tri_a = np.asarray(V[F[:, 0]], dtype=np.float32)
183
+ tri_b = np.asarray(V[F[:, 1]], dtype=np.float32)
184
+ tri_c = np.asarray(V[F[:, 2]], dtype=np.float32)
185
+
186
+ tri_e1 = (tri_b - tri_a).astype(np.float32, copy=False)
187
+ tri_e2 = (tri_c - tri_a).astype(np.float32, copy=False)
188
+ tri_n = np.cross(tri_e1, tri_e2).astype(np.float32, copy=False)
189
+ tri_n = _safe_normalize(tri_n).astype(np.float32, copy=False)
190
+
191
+ v0s.append(tri_a)
192
+ e1s.append(tri_e1)
193
+ e2s.append(tri_e2)
194
+ normals.append(tri_n)
195
+ sids.append(np.full(F.shape[0], sid, dtype=np.int32))
196
+
197
+ if not v0s:
198
+ empty3 = np.empty((0, 3), dtype=np.float32)
199
+ empty1 = np.empty((0,), dtype=np.int32)
200
+ return PreparedScene(
201
+ empty3,
202
+ empty3,
203
+ empty3,
204
+ empty3,
205
+ empty1,
206
+ None,
207
+ None,
208
+ None,
209
+ None,
210
+ None,
211
+ None,
212
+ False,
213
+ )
214
+
215
+ v0 = np.concatenate(v0s, axis=0)
216
+ e1 = np.concatenate(e1s, axis=0)
217
+ e2 = np.concatenate(e2s, axis=0)
218
+ tri_n = np.concatenate(normals, axis=0)
219
+ sid = np.concatenate(sids, axis=0)
220
+
221
+ bb_min = bb_max = left = right = start = count = None
222
+ if use_bvh and v0.shape[0] > 0:
223
+ bb_min, bb_max, left, right, start, count, perm = build_bvh(v0, e1, e2)
224
+ v0 = v0[perm]
225
+ e1 = e1[perm]
226
+ e2 = e2[perm]
227
+ tri_n = tri_n[perm]
228
+ sid = sid[perm]
229
+
230
+ return PreparedScene(
231
+ v0=v0,
232
+ e1=e1,
233
+ e2=e2,
234
+ normals=tri_n,
235
+ sid=sid,
236
+ bb_min=bb_min,
237
+ bb_max=bb_max,
238
+ left=left,
239
+ right=right,
240
+ start=start,
241
+ count=count,
242
+ use_bvh=bool(use_bvh and v0.shape[0] > 0),
243
+ )
244
+
245
+
246
+ def prepare_emitters(
247
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
248
+ *,
249
+ samples: int,
250
+ rays: int,
251
+ flip_faces: bool,
252
+ ) -> List[PreparedEmitter]:
253
+ emitters: List[PreparedEmitter] = []
254
+ for _, V, F in meshes:
255
+ F_emit = F[:, [0, 2, 1]] if flip_faces else F
256
+ tri_a = np.asarray(V[F_emit[:, 0]], dtype=np.float32)
257
+ tri_b = np.asarray(V[F_emit[:, 1]], dtype=np.float32)
258
+ tri_c = np.asarray(V[F_emit[:, 2]], dtype=np.float32)
259
+
260
+ tri_e1 = (tri_b - tri_a).astype(np.float32, copy=False)
261
+ tri_e2 = (tri_c - tri_a).astype(np.float32, copy=False)
262
+
263
+ tri_n_raw = np.cross(tri_e1, tri_e2).astype(np.float32, copy=False)
264
+ twice_area = np.linalg.norm(tri_n_raw, axis=1)
265
+ tri_n = _safe_normalize(tri_n_raw).astype(np.float32, copy=False)
266
+ tri_u, tri_v = _triangle_frames(tri_n)
267
+ tri_origin_eps = _triangle_origin_eps(tri_e1, tri_e2)
268
+ plane_origin, plane_normal, plane_tol, plane_is_planar = _emitter_plane(
269
+ tri_a,
270
+ tri_e1,
271
+ tri_e2,
272
+ tri_n,
273
+ tri_origin_eps,
274
+ )
275
+
276
+ areas = 0.5 * twice_area
277
+ total_area = float(areas.sum())
278
+ if total_area <= 0.0:
279
+ cdf = np.ones(F_emit.shape[0], dtype=np.float32)
280
+ g = 4
281
+ u_grid = np.zeros(g * g, dtype=np.float32)
282
+ v_grid = np.zeros_like(u_grid)
283
+ halton_tri = np.zeros(g * g * rays, dtype=np.float32)
284
+ halton_u = np.zeros_like(halton_tri)
285
+ halton_v = np.zeros_like(halton_tri)
286
+ halton_r1 = np.zeros_like(halton_tri)
287
+ halton_r2 = np.zeros_like(halton_tri)
288
+ else:
289
+ cdf = np.cumsum(areas, dtype=np.float64)
290
+ cdf = (cdf / cdf[-1]).astype(np.float32)
291
+ g = grid_from_density(total_area, samples)
292
+ u_grid, v_grid = cached_halton(g)
293
+ n_rays_once = g * g * rays
294
+ halton_tri, halton_u, halton_v, halton_r1, halton_r2 = cached_halton_dims(n_rays_once)
295
+
296
+ emitters.append(
297
+ PreparedEmitter(
298
+ tri_a=tri_a,
299
+ tri_e1=tri_e1,
300
+ tri_e2=tri_e2,
301
+ tri_u=tri_u.astype(np.float32, copy=False),
302
+ tri_v=tri_v.astype(np.float32, copy=False),
303
+ tri_n=tri_n,
304
+ tri_origin_eps=tri_origin_eps,
305
+ plane_origin=plane_origin,
306
+ plane_normal=plane_normal,
307
+ plane_tol=plane_tol,
308
+ plane_is_planar=plane_is_planar,
309
+ cdf=cdf,
310
+ total_area=total_area,
311
+ g=g,
312
+ u_grid=u_grid,
313
+ v_grid=v_grid,
314
+ halton_tri=halton_tri,
315
+ halton_u=halton_u,
316
+ halton_v=halton_v,
317
+ halton_r1=halton_r1,
318
+ halton_r2=halton_r2,
319
+ )
320
+ )
321
+ return emitters
322
+
323
+
324
+ class PreparedSolver:
325
+ """Cache prepared geometry, ray-generation tables and CUDA uploads.
326
+
327
+ Reusing a single instance across repeated solves on the same mesh set
328
+ avoids rebuilding triangle buffers, BVHs, Halton tables and device copies.
329
+ """
330
+
331
+ def __init__(self, meshes: List[Tuple[str, np.ndarray, np.ndarray]]):
332
+ self.meshes = list(meshes)
333
+ self.total_faces = int(sum(F.shape[0] for _, _, F in self.meshes))
334
+ self._scene_cache: Dict[bool, PreparedScene] = {}
335
+ self._emitter_cache: Dict[Tuple[int, int, bool], List[PreparedEmitter]] = {}
336
+ self._device_scene_cache: Dict[Tuple[int, bool], PreparedDeviceScene] = {}
337
+ self._device_emitter_cache: Dict[Tuple[int, int, int, int, bool], PreparedDeviceEmitter] = {}
338
+ self._mesh_bounds_cache: Optional[Tuple[np.ndarray, np.ndarray]] = None
339
+
340
+ def get_scene(self, *, use_bvh: bool) -> PreparedScene:
341
+ key = bool(use_bvh)
342
+ scene = self._scene_cache.get(key)
343
+ if scene is None:
344
+ scene = prepare_scene(self.meshes, use_bvh=key)
345
+ self._scene_cache[key] = scene
346
+ return scene
347
+
348
+ def get_emitters(self, *, samples: int, rays: int, flip_faces: bool) -> List[PreparedEmitter]:
349
+ key = (int(samples), int(rays), bool(flip_faces))
350
+ emitters = self._emitter_cache.get(key)
351
+ if emitters is None:
352
+ emitters = prepare_emitters(self.meshes, samples=samples, rays=rays, flip_faces=flip_faces)
353
+ self._emitter_cache[key] = emitters
354
+ return emitters
355
+
356
+ def get_emitter(self, index: int, *, samples: int, rays: int, flip_faces: bool) -> PreparedEmitter:
357
+ return self.get_emitters(samples=samples, rays=rays, flip_faces=flip_faces)[int(index)]
358
+
359
+ def get_mesh_bounds(self) -> Tuple[np.ndarray, np.ndarray]:
360
+ bounds = self._mesh_bounds_cache
361
+ if bounds is None:
362
+ n_mesh = len(self.meshes)
363
+ centers = np.zeros((n_mesh, 3), dtype=np.float32)
364
+ extents = np.zeros((n_mesh, 3), dtype=np.float32)
365
+ for idx, (_, V, _) in enumerate(self.meshes):
366
+ if V.size == 0:
367
+ continue
368
+ v = np.asarray(V, dtype=np.float32)
369
+ vmin = np.min(v, axis=0)
370
+ vmax = np.max(v, axis=0)
371
+ centers[idx] = 0.5 * (vmin + vmax)
372
+ extents[idx] = 0.5 * (vmax - vmin)
373
+ bounds = (centers, extents)
374
+ self._mesh_bounds_cache = bounds
375
+ return bounds
376
+
377
+ def clear_device_cache(self) -> None:
378
+ self._device_scene_cache.clear()
379
+ self._device_emitter_cache.clear()
380
+
381
+ def get_device_scene(self, *, use_bvh: bool) -> PreparedDeviceScene:
382
+ from numba import cuda
383
+
384
+ key = (int(cuda.get_current_device().id), bool(use_bvh))
385
+ scene = self._device_scene_cache.get(key)
386
+ if scene is None:
387
+ host_scene = self.get_scene(use_bvh=use_bvh)
388
+ scene = PreparedDeviceScene(
389
+ v0=cuda.to_device(host_scene.v0),
390
+ e1=cuda.to_device(host_scene.e1),
391
+ e2=cuda.to_device(host_scene.e2),
392
+ normals=cuda.to_device(host_scene.normals),
393
+ sid=cuda.to_device(host_scene.sid),
394
+ bb_min=None if host_scene.bb_min is None else cuda.to_device(host_scene.bb_min),
395
+ bb_max=None if host_scene.bb_max is None else cuda.to_device(host_scene.bb_max),
396
+ left=None if host_scene.left is None else cuda.to_device(host_scene.left),
397
+ right=None if host_scene.right is None else cuda.to_device(host_scene.right),
398
+ start=None if host_scene.start is None else cuda.to_device(host_scene.start),
399
+ count=None if host_scene.count is None else cuda.to_device(host_scene.count),
400
+ use_bvh=host_scene.use_bvh,
401
+ )
402
+ self._device_scene_cache[key] = scene
403
+ return scene
404
+
405
+ def get_device_emitter(self, index: int, *, samples: int, rays: int, flip_faces: bool) -> PreparedDeviceEmitter:
406
+ from numba import cuda
407
+
408
+ idx = int(index)
409
+ key = (int(cuda.get_current_device().id), idx, int(samples), int(rays), bool(flip_faces))
410
+ emitter = self._device_emitter_cache.get(key)
411
+ if emitter is None:
412
+ host_emitter = self.get_emitter(idx, samples=samples, rays=rays, flip_faces=flip_faces)
413
+ emitter = PreparedDeviceEmitter(
414
+ u_grid=cuda.to_device(host_emitter.u_grid),
415
+ v_grid=cuda.to_device(host_emitter.v_grid),
416
+ halton_tri=cuda.to_device(host_emitter.halton_tri),
417
+ halton_u=cuda.to_device(host_emitter.halton_u),
418
+ halton_v=cuda.to_device(host_emitter.halton_v),
419
+ halton_r1=cuda.to_device(host_emitter.halton_r1),
420
+ halton_r2=cuda.to_device(host_emitter.halton_r2),
421
+ cdf=cuda.to_device(host_emitter.cdf),
422
+ tri_a=cuda.to_device(host_emitter.tri_a),
423
+ tri_e1=cuda.to_device(host_emitter.tri_e1),
424
+ tri_e2=cuda.to_device(host_emitter.tri_e2),
425
+ tri_u=cuda.to_device(host_emitter.tri_u),
426
+ tri_v=cuda.to_device(host_emitter.tri_v),
427
+ tri_n=cuda.to_device(host_emitter.tri_n),
428
+ tri_origin_eps=cuda.to_device(host_emitter.tri_origin_eps),
429
+ )
430
+ self._device_emitter_cache[key] = emitter
431
+ return emitter
432
+
433
+
434
+ __all__ = [
435
+ "PreparedScene",
436
+ "PreparedEmitter",
437
+ "PreparedDeviceScene",
438
+ "PreparedDeviceEmitter",
439
+ "PreparedSolver",
440
+ "prepare_scene",
441
+ "prepare_emitters",
442
+ ]
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ import numba as nb
6
+
7
+
8
+ @nb.njit(inline="always", cache=True)
9
+ def _binary_search_cdf(cdf, x):
10
+ lo = 0
11
+ hi = cdf.shape[0] - 1
12
+ while lo <= hi:
13
+ mid = (lo + hi) // 2
14
+ if cdf[mid] < x:
15
+ lo = mid + 1
16
+ else:
17
+ hi = mid - 1
18
+ if lo < 0:
19
+ return 0
20
+ if lo >= cdf.shape[0]:
21
+ return cdf.shape[0] - 1
22
+ return lo
23
+
24
+
25
+ @nb.njit(parallel=True, cache=True, fastmath=True)
26
+ def build_rays(
27
+ u_grid,
28
+ v_grid,
29
+ halton_tri,
30
+ halton_u,
31
+ halton_v,
32
+ halton_r1,
33
+ halton_r2,
34
+ cdf,
35
+ tri_a,
36
+ tri_e1,
37
+ tri_e2,
38
+ tri_u,
39
+ tri_v,
40
+ tri_n,
41
+ tri_origin_eps,
42
+ rays_per_cell,
43
+ orig,
44
+ dire,
45
+ cp_grid,
46
+ cp_dims,
47
+ ):
48
+ """Create ray origins and directions from precomputed emitter data."""
49
+ n_rays = orig.shape[0]
50
+ two_pi = 6.283185307179586
51
+
52
+ for idx in nb.prange(n_rays):
53
+ cell = idx // rays_per_cell
54
+ ug = (u_grid[cell] + cp_grid[0]) % 1.0
55
+ vg = (v_grid[cell] + cp_grid[1]) % 1.0
56
+
57
+ q_tri = (halton_tri[idx] + cp_dims[0]) % 1.0
58
+ tri = _binary_search_cdf(cdf, q_tri)
59
+
60
+ ur = (halton_u[idx] + cp_dims[1] + ug) % 1.0
61
+ vr = (halton_v[idx] + cp_dims[2] + vg) % 1.0
62
+
63
+ s = math.sqrt(ur)
64
+ mix_b = s * vr
65
+ mix_c = s * (1.0 - vr)
66
+
67
+ ax = tri_a[tri, 0]
68
+ ay = tri_a[tri, 1]
69
+ az = tri_a[tri, 2]
70
+
71
+ px = ax + mix_b * tri_e1[tri, 0] + mix_c * tri_e2[tri, 0]
72
+ py = ay + mix_b * tri_e1[tri, 1] + mix_c * tri_e2[tri, 1]
73
+ pz = az + mix_b * tri_e1[tri, 2] + mix_c * tri_e2[tri, 2]
74
+
75
+ r1 = (halton_r1[idx] + cp_dims[3]) % 1.0
76
+ r2 = (halton_r2[idx] + cp_dims[4]) % 1.0
77
+
78
+ sin_t = math.sqrt(1.0 - r1)
79
+ phi = two_pi * r2
80
+ x = sin_t * math.cos(phi)
81
+ y = sin_t * math.sin(phi)
82
+ z = math.sqrt(r1)
83
+
84
+ dx = x * tri_u[tri, 0] + y * tri_v[tri, 0] + z * tri_n[tri, 0]
85
+ dy = x * tri_u[tri, 1] + y * tri_v[tri, 1] + z * tri_n[tri, 1]
86
+ dz = x * tri_u[tri, 2] + y * tri_v[tri, 2] + z * tri_n[tri, 2]
87
+ eps = tri_origin_eps[tri]
88
+
89
+ orig[idx, 0] = px + eps * tri_n[tri, 0]
90
+ orig[idx, 1] = py + eps * tri_n[tri, 1]
91
+ orig[idx, 2] = pz + eps * tri_n[tri, 2]
92
+ dire[idx, 0] = dx
93
+ dire[idx, 1] = dy
94
+ dire[idx, 2] = dz
95
+
96
+
97
+ __all__ = ["build_rays"]
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: raystrack
3
+ Version: 1.0.1
4
+ Summary: Lightweight Monte-Carlo view-factor solver with CPU, CUDA and BVH paths
5
+ Author-email: Philip Balizki <philip@metis.earth>
6
+ License-Expression: GPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/philip-ba/raystrack
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Requires-Python: <3.13,>=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy<3.0,>=1.24
16
+ Requires-Dist: numba<0.60,>=0.59
17
+ Dynamic: license-file
18
+
19
+ # Raystrack
20
+
21
+ <p align="left">
22
+ <img src="raystrack_icon.svg" alt="Raystrack icon" width="160">
23
+ </p>
24
+
25
+ Lightweight Monte-Carlo view-factor solver for polygonal meshes.
26
+
27
+ Raystrack computes radiative view factors F(i->j) between triangulated surfaces
28
+ using quasi-Monte-Carlo ray tracing. It runs on CPU, can leverage Numba/CUDA on
29
+ NVIDIA GPUs when available, and optionally accelerates ray intersection with a
30
+ BVH. The repository also ships a pure-Python API you can use outside Rhino.
31
+
32
+ ## Features
33
+ - Efficient Monte-Carlo view factors: front/back hits, optional reciprocity
34
+ - CPU and optional CUDA GPU backends (Numba)
35
+ - Optional BVH acceleration structures
36
+ - Python API plus Grasshopper components (Rhino installer coming soon)
37
+
38
+ ## Installation
39
+
40
+ ### Python package
41
+ Use Raystrack as a normal Python package outside Rhino or Grasshopper.
42
+
43
+ From a local clone of this repository:
44
+ ```
45
+ pip install .
46
+ ```
47
+
48
+ Or from an absolute path:
49
+ ```
50
+ pip install /path/to/raystrack
51
+ ```
52
+
53
+ Requirements: Python 3.9+, `numpy`, `numba`. CUDA acceleration is enabled
54
+ automatically when `numba.cuda` detects a compatible GPU.
55
+
56
+ ### Rhino / Grasshopper status
57
+ The Rhino 8 Grasshopper installers are not yet published. Follow this README or the issue tracker for updates when a
58
+ tested Rhino workflow becomes available.
59
+
60
+ ## Examples
61
+
62
+ All examples live in `examples/`. Start by running `ex00_street_canyon_geometry`
63
+ to generate `street_canyon.json`; subsequent scripts expect that file. Each
64
+ Python example now documents its own inputs and parameters inline, so open the
65
+ scripts directly for usage guidance and tunable options.
66
+
67
+ ## Quick start (Python)
68
+ ```python
69
+ import numpy as np
70
+ from raystrack import view_factor_matrix, MatrixParams
71
+
72
+ # Each mesh is a tuple: (name: str, V: (N,3) float32, F: (M,3) int32)
73
+ V_a = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], dtype=np.float32)
74
+ F_a = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int32) # two triangles
75
+
76
+ V_b = np.array([[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], dtype=np.float32)
77
+ F_b = F_a.copy()
78
+
79
+ meshes = [
80
+ ("A", V_a, F_a),
81
+ ("B", V_b, F_b),
82
+ ]
83
+
84
+ params = MatrixParams(
85
+ samples=256, # sampling density per unit area (QMC grid)
86
+ rays=256, # rays per cell
87
+ bvh="builtin", # optional BVH acceleration (auto|off|builtin)
88
+ reciprocity=True,
89
+ )
90
+
91
+ res = view_factor_matrix(meshes, params=params)
92
+
93
+ print(res["A"]) # e.g. {"B_front": 0.5, "B_back": 0.0, ...}
94
+ ```
95
+
96
+ To persist a matrix without directional suffixes in the saved JSON:
97
+ ```python
98
+ from raystrack import save_vf_matrix_json
99
+
100
+ save_vf_matrix_json(res, "vf_matrix.json", strip_dir=True)
101
+ ```
102
+ This collapses receiver keys like `"B_front"` and `"B_back"` into `"B"` and
103
+ sums both values per sender row.
104
+
105
+ ## Parameter presets
106
+ Raystrack uses two parameter containers to keep configuration consistent:
107
+ - `MatrixParams`: controls the scene-to-scene view-factor solve (sampling, BVH,
108
+ device selection, convergence tolerances, and reciprocity enforcement).
109
+ - `SkyParams`: controls the sky view-factor solve (sampling, device selection,
110
+ and convergence tolerances, plus `discrete=True` for 145 sky patches or
111
+ `False` for a single merged `"Sky"` output).
112
+
113
+ Typical usage:
114
+ ```python
115
+ from raystrack import MatrixParams, SkyParams, view_factor_matrix, view_factor_to_tregenza_sky
116
+
117
+ matrix_params = MatrixParams(samples=32, rays=256, reciprocity=True, flip_faces=False)
118
+ sky_params = SkyParams(samples=32, rays=256)
119
+
120
+ vf_scene = view_factor_matrix(meshes, params=matrix_params)
121
+ vf_sky = view_factor_to_tregenza_sky(meshes, params=sky_params)
122
+ ```
123
+
124
+ ## License
125
+ GPL-3.0-only - see `LICENSE`.
@@ -0,0 +1,19 @@
1
+ raystrack/__init__.py,sha256=QVC1j713ZEl4BcWD-CDvQmHmXKbj2_fUAGsIhW_kt1M,677
2
+ raystrack/api.py,sha256=yvtM7LoKZhihAZrziwMWgSB5NHdLUXiZ-Kx099gyKPY,7742
3
+ raystrack/io.py,sha256=CsU6pSplFqYUBwPHOXuU48IKrTgfFoyc0j-N5BMUic8,8154
4
+ raystrack/main.py,sha256=U3LgHHiwz5dHsZfssE50vGQLDQPu_ZArpt0nsQ9KwUA,91750
5
+ raystrack/params.py,sha256=bIwRfzb0sdTDEwvldDIRg8xz529zA2l2cKDvJdS1-6Y,4313
6
+ raystrack/utils/__init__.py,sha256=hJ0nNPOGhBbYQOxdiOLca3kjc6XK-897cPqmnGpjHiM,702
7
+ raystrack/utils/bvh.py,sha256=H-nGkBMjvFxuhWHoksz0cmiRS5mxnEU36szMTPmLcTQ,2283
8
+ raystrack/utils/cpu_trace.py,sha256=-Jztd6moL0-3QseJ1XBNbwPZrrbU7nxiYnwakr4LJfE,22785
9
+ raystrack/utils/cuda_trace.py,sha256=lIgPG9h-HocPWZlJ0UFyAOCEbXznRNqvDTFPEo4SLoo,34636
10
+ raystrack/utils/geometry.py,sha256=D3ABPcrYDJZXxAGiw3MuLPTPaKRPKr-w52ygxcu1YzQ,2151
11
+ raystrack/utils/halton.py,sha256=2_ShczxX-WTpZpOLLiJPAdlNuu3rNKSo7Zw7tRDieCQ,1467
12
+ raystrack/utils/helpers.py,sha256=Hu4f-2QRU_8lWa74DUXA2u9HPgCbr3m2cK58KSER_N8,8995
13
+ raystrack/utils/prepared.py,sha256=FvOSwN_3zCktBEJdKl223qe7cZ_0nCb7Gpu3mpzVDiE,15219
14
+ raystrack/utils/ray_builder.py,sha256=IV-81KdGxuYfqZcRHhwtoMYDLvH9zFxYBcgAT4Y76Bw,2372
15
+ raystrack-1.0.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
+ raystrack-1.0.1.dist-info/METADATA,sha256=vpyrXC_CJWWzIsUVLE5TzjNGuhKmujNjmEupHpRkNgw,4226
17
+ raystrack-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ raystrack-1.0.1.dist-info/top_level.txt,sha256=tLhnPDw74DVcYVMnJeqw8bPMFLURvyR756B05cQrJKE,10
19
+ raystrack-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+