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/__init__.py +30 -0
- raystrack/api.py +198 -0
- raystrack/io.py +238 -0
- raystrack/main.py +2194 -0
- raystrack/params.py +129 -0
- raystrack/utils/__init__.py +23 -0
- raystrack/utils/bvh.py +74 -0
- raystrack/utils/cpu_trace.py +811 -0
- raystrack/utils/cuda_trace.py +1285 -0
- raystrack/utils/geometry.py +66 -0
- raystrack/utils/halton.py +61 -0
- raystrack/utils/helpers.py +284 -0
- raystrack/utils/prepared.py +442 -0
- raystrack/utils/ray_builder.py +97 -0
- raystrack-1.0.1.dist-info/METADATA +125 -0
- raystrack-1.0.1.dist-info/RECORD +19 -0
- raystrack-1.0.1.dist-info/WHEEL +5 -0
- raystrack-1.0.1.dist-info/licenses/LICENSE +674 -0
- raystrack-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
|