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,66 @@
1
+ from __future__ import annotations
2
+ from typing import Iterable, List, Tuple
3
+ import numpy as np
4
+
5
+
6
+
7
+ def flatten_receivers(meshes, idx_emit, skip: Iterable[int] = ()): # type: ignore[assignment]
8
+ """Return flattened receiver triangle arrays.
9
+
10
+ Parameters
11
+ ----------
12
+ meshes : list
13
+ List of ``(name, V, F)`` mesh tuples.
14
+ idx_emit : int
15
+ Index of the emitting surface in ``meshes`` (kept for callers that
16
+ want to decide whether to exclude it or not).
17
+ skip : Iterable[int], optional
18
+ Surface indices to exclude from the receiver list. Note: the emitter
19
+ is NOT excluded automatically anymore; callers must pass it here when
20
+ they want to exclude the whole emitting mesh.
21
+ """
22
+ v0s, e1s, e2s, sids, norms = [], [], [], [], []
23
+ skip_set = set(skip)
24
+ for sid, (_, V, F) in enumerate(meshes):
25
+ if sid in skip_set:
26
+ continue
27
+ v0 = V[F[:, 0]].astype(np.float32)
28
+ v1 = V[F[:, 1]].astype(np.float32)
29
+ v2 = V[F[:, 2]].astype(np.float32)
30
+
31
+ v0s.append(v0)
32
+ e1s.append(v1 - v0)
33
+ e2s.append(v2 - v0)
34
+ sids.append(np.full(len(F), sid, np.int32))
35
+
36
+ n = np.cross(v1 - v0, v2 - v0)
37
+ n /= np.linalg.norm(n, axis=1)[:, None]
38
+ norms.append(n.astype(np.float32))
39
+
40
+ if not v0s:
41
+ return (
42
+ np.empty((0, 3), np.float32),
43
+ np.empty((0, 3), np.float32),
44
+ np.empty((0, 3), np.float32),
45
+ np.empty((0,), np.int32),
46
+ np.empty((0, 3), np.float32),
47
+ )
48
+
49
+ v0 = np.concatenate(v0s)
50
+ e1 = np.concatenate(e1s)
51
+ e2 = np.concatenate(e2s)
52
+ sid = np.concatenate(sids)
53
+ norm = np.concatenate(norms)
54
+ return v0, e1, e2, sid, norm
55
+
56
+
57
+ def flip_meshes(meshes: List[Tuple[str, np.ndarray, np.ndarray]]
58
+ ) -> List[Tuple[str, np.ndarray, np.ndarray]]:
59
+ """Return a deep-copied list whose every triangle has reversed order."""
60
+ flipped = []
61
+ for name, V, F in meshes:
62
+ F_flipped = F[:, [0, 2, 1]].copy()
63
+ flipped.append((name, V.copy(), F_flipped))
64
+ return flipped
65
+
66
+ __all__ = ["flatten_receivers", "flip_meshes"]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+
5
+ import numpy as np
6
+ import numba as nb
7
+
8
+
9
+ @nb.njit(inline="always", cache=True)
10
+ def _halton(i: int, base: int) -> float:
11
+ """Return the i-th value of a 1-D Halton sequence in the given base."""
12
+ f = 1.0
13
+ r = 0.0
14
+ while i:
15
+ f /= base
16
+ r += f * (i % base)
17
+ i //= base
18
+ return r
19
+
20
+
21
+ @nb.njit(cache=True)
22
+ def _build_halton_grid(samples: int):
23
+ g = samples
24
+ cells = g * g
25
+ u = np.empty(cells, np.float32)
26
+ v = np.empty(cells, np.float32)
27
+ for c in range(cells):
28
+ i, j = divmod(c, g)
29
+ u[c] = (_halton(c + 1, 2) + i) / g
30
+ v[c] = (_halton(c + 1, 3) + j) / g
31
+ return u, v
32
+
33
+
34
+ @nb.njit(cache=True)
35
+ def _build_halton_dim(length: int, base: int):
36
+ out = np.empty(length, np.float32)
37
+ for i in range(length):
38
+ out[i] = _halton(i + 1, base)
39
+ return out
40
+
41
+
42
+ @lru_cache(maxsize=128)
43
+ def cached_halton(samples: int):
44
+ """Return a cached 2-D Halton jitter grid of size ``samples * samples``."""
45
+ return _build_halton_grid(int(samples))
46
+
47
+
48
+ @lru_cache(maxsize=128)
49
+ def cached_halton_dims(length: int):
50
+ """Return cached low-discrepancy dimensions for ray generation."""
51
+ n = int(length)
52
+ return (
53
+ _build_halton_dim(n, 5),
54
+ _build_halton_dim(n, 2),
55
+ _build_halton_dim(n, 3),
56
+ _build_halton_dim(n, 7),
57
+ _build_halton_dim(n, 11),
58
+ )
59
+
60
+
61
+ __all__ = ["cached_halton", "cached_halton_dims"]
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, Iterable, List, Tuple
3
+ import os
4
+ import sys
5
+ import numpy as np
6
+
7
+
8
+ def grid_from_density(area: float, density: float) -> int:
9
+ """Return Halton grid size for a given surface area and sample density."""
10
+ g = int(np.ceil(np.sqrt(max(area, 0.0) * density)))
11
+ return max(g, 4)
12
+
13
+
14
+ def enforce_reciprocity_and_rowsum(
15
+ result: Dict[str, Dict[str, float]],
16
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
17
+ areas: List[float] | None,
18
+ row_targets: Iterable[float] | None = None,
19
+ tol: float = 1e-10,
20
+ max_iter: int = 500,
21
+ ) -> None:
22
+ """In-place adjust result so that rows sum to targets and reciprocity holds.
23
+
24
+ Operates on totals per pair (front+back) using symmetric diagonal scaling
25
+ of G = diag(A) F after symmetrization, then maps back to front/back totals
26
+ proportionally to the original split.
27
+
28
+ Parameters
29
+ ----------
30
+ row_targets : iterable of float, optional
31
+ Desired total view factor per emitter (same order as ``meshes``). When
32
+ omitted, rows are normalised to sum to one. When provided, the helper
33
+ enforces the specified totals while still honouring reciprocity.
34
+ """
35
+ n = len(meshes)
36
+ names = [m[0] for m in meshes]
37
+ name_to_idx = {name: i for i, name in enumerate(names)}
38
+
39
+ if areas is None:
40
+ areas = []
41
+ for _, V_a, F_a in meshes:
42
+ A_a = 0.5 * np.linalg.norm(
43
+ np.cross(
44
+ V_a[F_a[:, 1]] - V_a[F_a[:, 0]],
45
+ V_a[F_a[:, 2]] - V_a[F_a[:, 0]],
46
+ ),
47
+ axis=1,
48
+ )
49
+ areas.append(float(A_a.sum()))
50
+ A = np.asarray(areas, dtype=np.float64)
51
+ if row_targets is None:
52
+ target = A
53
+ else:
54
+ target = np.asarray(list(row_targets), dtype=np.float64)
55
+ if target.shape != A.shape:
56
+ raise ValueError("row_targets must match number of meshes")
57
+ target = np.clip(target, 0.0, None)
58
+ target = A * target
59
+
60
+ F = np.zeros((n, n), dtype=np.float64)
61
+
62
+ def base_of(key: str) -> str:
63
+ if key.endswith("_front"):
64
+ return key[:-6]
65
+ if key.endswith("_back"):
66
+ return key[:-5]
67
+ return key
68
+
69
+ for si, sname in enumerate(names):
70
+ row = result.get(sname, {})
71
+ if not isinstance(row, dict):
72
+ continue
73
+ accum: Dict[str, float] = {}
74
+ for rkey, val in row.items():
75
+ b = base_of(rkey)
76
+ accum[b] = accum.get(b, 0.0) + float(val)
77
+ for bname, v in accum.items():
78
+ j = name_to_idx.get(bname, None)
79
+ if j is None:
80
+ continue
81
+ F[si, j] = v
82
+
83
+ G = (A[:, None] * F)
84
+ G = 0.5 * (G + G.T)
85
+
86
+ d = np.ones(n, dtype=np.float64)
87
+ for _ in range(max_iter):
88
+ row = d * (G @ d)
89
+ row = np.maximum(row, 1e-30)
90
+ upd = target / row if row_targets is not None else A / row
91
+ upd = np.maximum(upd, 0.0)
92
+ d_new = d * np.sqrt(upd)
93
+ if np.max(np.abs(d_new - d)) < tol:
94
+ d = d_new
95
+ break
96
+ d = d_new
97
+
98
+ Gp = (d[:, None] * G) * d[None, :]
99
+ with np.errstate(divide="ignore", invalid="ignore"):
100
+ Fp = np.divide(Gp, A[:, None], out=np.zeros_like(Gp), where=A[:, None] > 0.0)
101
+
102
+ for si, sname in enumerate(names):
103
+ row = result.get(sname, {})
104
+ fb: Dict[str, Tuple[float, float]] = {}
105
+ for rkey, val in row.items():
106
+ if rkey.endswith("_front"):
107
+ base = rkey[:-6]
108
+ cur_f, cur_b = fb.get(base, (0.0, 0.0))
109
+ fb[base] = (cur_f + float(val), cur_b)
110
+ elif rkey.endswith("_back"):
111
+ base = rkey[:-5]
112
+ cur_f, cur_b = fb.get(base, (0.0, 0.0))
113
+ fb[base] = (cur_f, cur_b + float(val))
114
+ else:
115
+ base = rkey
116
+ cur_f, cur_b = fb.get(base, (0.0, 0.0))
117
+ fb[base] = (cur_f, cur_b + float(val))
118
+
119
+ for bj, rname in enumerate(names):
120
+ t_new = float(max(Fp[si, bj], 0.0))
121
+ cur_f, cur_b = fb.get(rname, (0.0, 0.0))
122
+ t_old = cur_f + cur_b
123
+ if t_old > 0.0:
124
+ s = t_new / t_old
125
+ new_f = cur_f * s
126
+ new_b = cur_b * s
127
+ else:
128
+ new_f = 0.0
129
+ new_b = t_new
130
+
131
+ if new_f > 0.0:
132
+ row[f"{rname}_front"] = new_f
133
+ elif f"{rname}_front" in row:
134
+ del row[f"{rname}_front"]
135
+ if new_b > 0.0:
136
+ row[f"{rname}_back"] = new_b
137
+ elif f"{rname}_back" in row:
138
+ del row[f"{rname}_back"]
139
+
140
+ result[sname] = row
141
+
142
+
143
+ def enforce_reciprocity_only(
144
+ result: Dict[str, Dict[str, float]],
145
+ meshes: List[Tuple[str, np.ndarray, np.ndarray]],
146
+ tol: float = 1e-12,
147
+ ) -> None:
148
+ """In-place adjust result so that reciprocity holds without row scaling."""
149
+ if tol <= 0.0:
150
+ tol = 1e-12
151
+
152
+ n = len(meshes)
153
+ names = [m[0] for m in meshes]
154
+ name_to_idx = {name: i for i, name in enumerate(names)}
155
+
156
+ areas: List[float] = []
157
+ for _, V_a, F_a in meshes:
158
+ A_a = 0.5 * np.linalg.norm(
159
+ np.cross(
160
+ V_a[F_a[:, 1]] - V_a[F_a[:, 0]],
161
+ V_a[F_a[:, 2]] - V_a[F_a[:, 0]],
162
+ ),
163
+ axis=1,
164
+ )
165
+ areas.append(float(A_a.sum()))
166
+ A = np.asarray(areas, dtype=np.float64)
167
+
168
+ def base_of(key: str) -> str:
169
+ if key.endswith("_front"):
170
+ return key[:-6]
171
+ if key.endswith("_back"):
172
+ return key[:-5]
173
+ return key
174
+
175
+ F = np.zeros((n, n), dtype=np.float64)
176
+ for si, sname in enumerate(names):
177
+ row = result.get(sname, {})
178
+ if not isinstance(row, dict):
179
+ continue
180
+ accum: Dict[str, float] = {}
181
+ for rkey, val in row.items():
182
+ b = base_of(rkey)
183
+ accum[b] = accum.get(b, 0.0) + float(val)
184
+ for bname, v in accum.items():
185
+ j = name_to_idx.get(bname, None)
186
+ if j is None:
187
+ continue
188
+ F[si, j] = v
189
+
190
+ F_new = F.copy()
191
+ for i in range(n):
192
+ Ai = A[i]
193
+ for j in range(i + 1, n):
194
+ Aj = A[j]
195
+ fij = F[i, j]
196
+ fji = F[j, i]
197
+ if fij <= tol and fji <= tol:
198
+ F_new[i, j] = 0.0
199
+ F_new[j, i] = 0.0
200
+ continue
201
+ gij = 0.5 * (Ai * fij + Aj * fji)
202
+ if Ai > 0.0:
203
+ F_new[i, j] = max(gij / Ai, 0.0)
204
+ else:
205
+ F_new[i, j] = 0.0
206
+ if Aj > 0.0:
207
+ F_new[j, i] = max(gij / Aj, 0.0)
208
+ else:
209
+ F_new[j, i] = 0.0
210
+
211
+ for si, sname in enumerate(names):
212
+ row = result.get(sname, {})
213
+ if not isinstance(row, dict):
214
+ row = {}
215
+ fb: Dict[str, Tuple[float, float]] = {}
216
+ for rkey, val in row.items():
217
+ if rkey.endswith("_front"):
218
+ base = rkey[:-6]
219
+ cur_f, cur_b = fb.get(base, (0.0, 0.0))
220
+ fb[base] = (cur_f + float(val), cur_b)
221
+ elif rkey.endswith("_back"):
222
+ base = rkey[:-5]
223
+ cur_f, cur_b = fb.get(base, (0.0, 0.0))
224
+ fb[base] = (cur_f, cur_b + float(val))
225
+ else:
226
+ base = rkey
227
+ cur_f, cur_b = fb.get(base, (0.0, 0.0))
228
+ fb[base] = (cur_f, cur_b + float(val))
229
+
230
+ for bj, rname in enumerate(names):
231
+ if si == bj:
232
+ continue
233
+ t_new = float(max(F_new[si, bj], 0.0))
234
+ cur_f, cur_b = fb.get(rname, (0.0, 0.0))
235
+ t_old = cur_f + cur_b
236
+ if t_old > 0.0:
237
+ s = t_new / t_old
238
+ new_f = cur_f * s
239
+ new_b = cur_b * s
240
+ else:
241
+ new_f = 0.0
242
+ new_b = t_new
243
+
244
+ front_key = f"{rname}_front"
245
+ back_key = f"{rname}_back"
246
+
247
+ if new_f > tol:
248
+ row[front_key] = new_f
249
+ elif front_key in row:
250
+ del row[front_key]
251
+
252
+ if new_b > tol:
253
+ row[back_key] = new_b
254
+ elif back_key in row:
255
+ del row[back_key]
256
+
257
+ result[sname] = row
258
+
259
+
260
+ def hold_console_open(prompt: str = "Press Enter to close...") -> None:
261
+ """Keep the console window open when running scripts directly.
262
+
263
+ Controlled by the environment variable ``RAYSTRACK_HOLD_CONSOLE``.
264
+ Set it to ``0`` or ``false`` to disable the prompt.
265
+ """
266
+ flag = os.environ.get("RAYSTRACK_HOLD_CONSOLE", "1").lower()
267
+ if flag in {"0", "false", "no"}:
268
+ return
269
+ stdin = getattr(sys, "stdin", None)
270
+ if stdin is None or not stdin.isatty():
271
+ return
272
+ try:
273
+ input(prompt)
274
+ except EOFError:
275
+ pass
276
+
277
+
278
+ __all__ = [
279
+ "grid_from_density",
280
+ "enforce_reciprocity_and_rowsum",
281
+ "enforce_reciprocity_only",
282
+ "hold_console_open",
283
+ ]
284
+