reboost 0.7.0__py3-none-any.whl → 0.8.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.
- reboost/_version.py +2 -2
- reboost/core.py +0 -2
- reboost/hpge/psd.py +11 -9
- reboost/optmap/cli.py +40 -101
- reboost/optmap/convolve.py +51 -367
- reboost/optmap/create.py +37 -124
- reboost/optmap/evt.py +5 -2
- reboost/optmap/mapview.py +9 -7
- reboost/optmap/optmap.py +11 -12
- reboost/spms/pe.py +83 -4
- reboost/utils.py +1 -1
- {reboost-0.7.0.dist-info → reboost-0.8.1.dist-info}/METADATA +5 -2
- {reboost-0.7.0.dist-info → reboost-0.8.1.dist-info}/RECORD +17 -17
- {reboost-0.7.0.dist-info → reboost-0.8.1.dist-info}/WHEEL +0 -0
- {reboost-0.7.0.dist-info → reboost-0.8.1.dist-info}/entry_points.txt +0 -0
- {reboost-0.7.0.dist-info → reboost-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {reboost-0.7.0.dist-info → reboost-0.8.1.dist-info}/top_level.txt +0 -0
reboost/optmap/convolve.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import re
|
|
5
4
|
from typing import NamedTuple
|
|
6
5
|
|
|
7
6
|
import awkward as ak
|
|
@@ -10,12 +9,9 @@ import numba
|
|
|
10
9
|
import numpy as np
|
|
11
10
|
from legendoptics import fibers, lar, pen
|
|
12
11
|
from lgdo import lh5
|
|
13
|
-
from lgdo.
|
|
14
|
-
from
|
|
15
|
-
from numba import njit, prange
|
|
16
|
-
from numpy.lib.recfunctions import structured_to_unstructured
|
|
12
|
+
from lgdo.types import Histogram
|
|
13
|
+
from numba import njit
|
|
17
14
|
from numpy.typing import NDArray
|
|
18
|
-
from pint import Quantity
|
|
19
15
|
|
|
20
16
|
from .numba_pdg import numba_pdgid_funcs
|
|
21
17
|
|
|
@@ -23,163 +19,100 @@ log = logging.getLogger(__name__)
|
|
|
23
19
|
|
|
24
20
|
|
|
25
21
|
OPTMAP_ANY_CH = -1
|
|
26
|
-
OPTMAP_SUM_CH = -2
|
|
27
22
|
|
|
28
23
|
|
|
29
24
|
class OptmapForConvolve(NamedTuple):
|
|
30
25
|
"""A loaded optmap for convolving."""
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
dets: NDArray
|
|
33
28
|
detidx: NDArray
|
|
34
29
|
edges: NDArray
|
|
35
30
|
weights: NDArray
|
|
36
31
|
|
|
37
32
|
|
|
38
33
|
def open_optmap(optmap_fn: str) -> OptmapForConvolve:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
det_ntuples = [m for m in maps if re.match(r"_\d+$", m)]
|
|
42
|
-
detids = np.array([int(m.lstrip("_")) for m in det_ntuples])
|
|
43
|
-
detidx = np.arange(0, detids.shape[0])
|
|
34
|
+
dets = lh5.ls(optmap_fn, "/channels/")
|
|
35
|
+
detidx = np.arange(0, dets.shape[0])
|
|
44
36
|
|
|
45
|
-
optmap_all = lh5.read("/all/
|
|
37
|
+
optmap_all = lh5.read("/all/prob", optmap_fn)
|
|
46
38
|
assert isinstance(optmap_all, Histogram)
|
|
47
39
|
optmap_edges = tuple([b.edges for b in optmap_all.binning])
|
|
48
40
|
|
|
49
41
|
ow = np.empty((detidx.shape[0] + 2, *optmap_all.weights.nda.shape), dtype=np.float64)
|
|
50
42
|
# 0, ..., len(detidx)-1 AND OPTMAP_ANY_CH might be negative.
|
|
51
43
|
ow[OPTMAP_ANY_CH] = optmap_all.weights.nda
|
|
52
|
-
for i, nt in zip(detidx,
|
|
53
|
-
optmap = lh5.read(f"/{nt}/
|
|
44
|
+
for i, nt in zip(detidx, dets, strict=True):
|
|
45
|
+
optmap = lh5.read(f"/{nt}/prob", optmap_fn)
|
|
54
46
|
assert isinstance(optmap, Histogram)
|
|
55
47
|
ow[i] = optmap.weights.nda
|
|
56
48
|
|
|
57
49
|
# if we have any individual channels registered, the sum is potentially larger than the
|
|
58
50
|
# probability to find _any_ hit.
|
|
59
51
|
if len(detidx) != 0:
|
|
60
|
-
|
|
61
|
-
assert not np.any(
|
|
52
|
+
map_sum = np.sum(ow[0:-2], axis=0, where=(ow[0:-2] >= 0))
|
|
53
|
+
assert not np.any(map_sum < 0)
|
|
54
|
+
|
|
55
|
+
# give this check some numerical slack.
|
|
56
|
+
if np.any(
|
|
57
|
+
np.abs(map_sum[ow[OPTMAP_ANY_CH] >= 0] - ow[OPTMAP_ANY_CH][ow[OPTMAP_ANY_CH] >= 0])
|
|
58
|
+
< -1e-15
|
|
59
|
+
):
|
|
60
|
+
msg = "optical map does not fulfill relation sum(p_i) >= p_any"
|
|
61
|
+
raise ValueError(msg)
|
|
62
62
|
else:
|
|
63
63
|
detidx = np.array([OPTMAP_ANY_CH])
|
|
64
|
-
|
|
65
|
-
ow[OPTMAP_SUM_CH] = ow[OPTMAP_ANY_CH]
|
|
64
|
+
dets = np.array(["all"])
|
|
66
65
|
|
|
67
|
-
#
|
|
68
|
-
if
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
# check the exponent from the optical map file
|
|
67
|
+
if "_hitcounts_exp" in lh5.ls(optmap_fn):
|
|
68
|
+
msg = "found _hitcounts_exp which is not supported any more"
|
|
69
|
+
raise RuntimeError(msg)
|
|
70
|
+
|
|
71
|
+
dets = [d.replace("/channels/", "") for d in dets]
|
|
72
|
+
|
|
73
|
+
return OptmapForConvolve(dets, detidx, optmap_edges, ow)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def open_optmap_single(optmap_fn: str, spm_det: str) -> OptmapForConvolve:
|
|
77
|
+
# check the exponent from the optical map file
|
|
78
|
+
if "_hitcounts_exp" in lh5.ls(optmap_fn):
|
|
79
|
+
msg = "found _hitcounts_exp which is not supported any more"
|
|
80
|
+
raise RuntimeError(msg)
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
optmap_multi_det_exp = lh5.read("/_hitcounts_exp", optmap_fn).value
|
|
80
|
-
assert isinstance(optmap_multi_det_exp, float)
|
|
81
|
-
if np.isfinite(optmap_multi_det_exp):
|
|
82
|
-
msg = f"found finite _hitcounts_exp {optmap_multi_det_exp} which is not supported any more"
|
|
83
|
-
raise RuntimeError(msg)
|
|
84
|
-
except lh5.exceptions.LH5DecodeError: # the _hitcounts_exp might not be always present.
|
|
85
|
-
pass
|
|
86
|
-
|
|
87
|
-
return OptmapForConvolve(detids, detidx, optmap_edges, ow)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def open_optmap_single(optmap_fn: str, spm_det_uid: int) -> OptmapForConvolve:
|
|
91
|
-
try:
|
|
92
|
-
# check the exponent from the optical map file
|
|
93
|
-
optmap_multi_det_exp = lh5.read("/_hitcounts_exp", optmap_fn).value
|
|
94
|
-
assert isinstance(optmap_multi_det_exp, float)
|
|
95
|
-
if np.isfinite(optmap_multi_det_exp):
|
|
96
|
-
msg = f"found finite _hitcounts_exp {optmap_multi_det_exp} which is not supported any more"
|
|
97
|
-
raise RuntimeError(msg)
|
|
98
|
-
except lh5.exceptions.LH5DecodeError: # the _hitcounts_exp might not be always present.
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
optmap = lh5.read(f"/_{spm_det_uid}/p_det", optmap_fn)
|
|
82
|
+
h5_path = f"channels/{spm_det}" if spm_det != "all" else spm_det
|
|
83
|
+
optmap = lh5.read(f"/{h5_path}/prob", optmap_fn)
|
|
102
84
|
assert isinstance(optmap, Histogram)
|
|
103
85
|
ow = np.empty((1, *optmap.weights.nda.shape), dtype=np.float64)
|
|
104
86
|
ow[0] = optmap.weights.nda
|
|
105
87
|
optmap_edges = tuple([b.edges for b in optmap.binning])
|
|
106
88
|
|
|
107
|
-
return OptmapForConvolve(np.array([
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def iterate_stepwise_depositions(
|
|
111
|
-
edep_df: np.rec.recarray,
|
|
112
|
-
optmap: OptmapForConvolve,
|
|
113
|
-
scint_mat_params: sc.ComputedScintParams,
|
|
114
|
-
rng: np.random.Generator = None,
|
|
115
|
-
dist: str = "poisson",
|
|
116
|
-
mode: str = "no-fano",
|
|
117
|
-
):
|
|
118
|
-
# those np functions are not supported by numba, but needed for efficient array access below.
|
|
119
|
-
if "xloc_pre" in edep_df.dtype.names:
|
|
120
|
-
x0 = structured_to_unstructured(edep_df[["xloc_pre", "yloc_pre", "zloc_pre"]], np.float64)
|
|
121
|
-
x1 = structured_to_unstructured(
|
|
122
|
-
edep_df[["xloc_post", "yloc_post", "zloc_post"]], np.float64
|
|
123
|
-
)
|
|
124
|
-
else:
|
|
125
|
-
x0 = structured_to_unstructured(edep_df[["xloc", "yloc", "zloc"]], np.float64)
|
|
126
|
-
x1 = None
|
|
127
|
-
|
|
128
|
-
rng = np.random.default_rng() if rng is None else rng
|
|
129
|
-
output_map, res = _iterate_stepwise_depositions(
|
|
130
|
-
edep_df,
|
|
131
|
-
x0,
|
|
132
|
-
x1,
|
|
133
|
-
rng,
|
|
134
|
-
optmap.detids,
|
|
135
|
-
optmap.detidx,
|
|
136
|
-
optmap.edges,
|
|
137
|
-
optmap.weights,
|
|
138
|
-
scint_mat_params,
|
|
139
|
-
dist,
|
|
140
|
-
mode,
|
|
141
|
-
)
|
|
142
|
-
if res["any_no_stats"] > 0 or res["det_no_stats"] > 0:
|
|
143
|
-
log.warning(
|
|
144
|
-
"had edep out in voxels without stats: %d (%.2f%%)",
|
|
145
|
-
res["any_no_stats"],
|
|
146
|
-
res["det_no_stats"],
|
|
147
|
-
)
|
|
148
|
-
if res["oob"] > 0:
|
|
149
|
-
log.warning(
|
|
150
|
-
"had edep out of map bounds: %d (%.2f%%)",
|
|
151
|
-
res["oob"],
|
|
152
|
-
(res["oob"] / (res["ib"] + res["oob"])) * 100,
|
|
153
|
-
)
|
|
154
|
-
log.debug(
|
|
155
|
-
"VUV_primary %d ->hits_any %d ->hits %d (%.2f %% primaries detected)",
|
|
156
|
-
res["vuv_primary"],
|
|
157
|
-
res["hits_any"],
|
|
158
|
-
res["hits"],
|
|
159
|
-
(res["hits_any"] / res["vuv_primary"]) * 100,
|
|
160
|
-
)
|
|
161
|
-
log.debug("hits/hits_any %.2f", res["hits"] / res["hits_any"])
|
|
162
|
-
return output_map
|
|
89
|
+
return OptmapForConvolve(np.array([spm_det]), np.array([0]), optmap_edges, ow)
|
|
163
90
|
|
|
164
91
|
|
|
165
92
|
def iterate_stepwise_depositions_pois(
|
|
166
93
|
edep_hits: ak.Array,
|
|
167
94
|
optmap: OptmapForConvolve,
|
|
168
95
|
scint_mat_params: sc.ComputedScintParams,
|
|
169
|
-
|
|
96
|
+
det: str,
|
|
170
97
|
map_scaling: float = 1,
|
|
98
|
+
map_scaling_sigma: float = 0,
|
|
171
99
|
rng: np.random.Generator | None = None,
|
|
172
100
|
):
|
|
173
101
|
if edep_hits.particle.ndim == 1:
|
|
174
102
|
msg = "the pe processors only support already reshaped output"
|
|
175
103
|
raise ValueError(msg)
|
|
176
104
|
|
|
105
|
+
if det not in optmap.dets:
|
|
106
|
+
msg = f"channel {det} not available in optical map (contains {optmap.dets})"
|
|
107
|
+
raise ValueError(msg)
|
|
108
|
+
|
|
177
109
|
rng = np.random.default_rng() if rng is None else rng
|
|
178
110
|
res, output_list = _iterate_stepwise_depositions_pois(
|
|
179
111
|
edep_hits,
|
|
180
112
|
rng,
|
|
181
|
-
np.where(optmap.
|
|
113
|
+
np.where(optmap.dets == det)[0][0],
|
|
182
114
|
map_scaling,
|
|
115
|
+
map_scaling_sigma,
|
|
183
116
|
optmap.edges,
|
|
184
117
|
optmap.weights,
|
|
185
118
|
scint_mat_params,
|
|
@@ -254,184 +187,6 @@ def _pdgid_to_particle(pdgid: int) -> sc.ParticleIndex:
|
|
|
254
187
|
__counts_per_bin_key_type = numba.types.UniTuple(numba.types.int64, 3)
|
|
255
188
|
|
|
256
189
|
|
|
257
|
-
# - run with NUMBA_FULL_TRACEBACKS=1 NUMBA_BOUNDSCHECK=1 for testing/checking
|
|
258
|
-
# - cache=True does not work with outer prange, i.e. loading the cached file fails (numba bug?)
|
|
259
|
-
# - the output dictionary is not threadsafe, so parallel=True is not working with it.
|
|
260
|
-
@njit(parallel=False, nogil=True, cache=True)
|
|
261
|
-
def _iterate_stepwise_depositions(
|
|
262
|
-
edep_df,
|
|
263
|
-
x0,
|
|
264
|
-
x1,
|
|
265
|
-
rng,
|
|
266
|
-
detids,
|
|
267
|
-
detidx,
|
|
268
|
-
optmap_edges,
|
|
269
|
-
optmap_weights,
|
|
270
|
-
scint_mat_params: sc.ComputedScintParams,
|
|
271
|
-
dist: str,
|
|
272
|
-
mode: str,
|
|
273
|
-
):
|
|
274
|
-
pdgid_map = {}
|
|
275
|
-
output_map = {}
|
|
276
|
-
oob = ib = ph_cnt = ph_det = ph_det2 = any_no_stats = det_no_stats = 0 # for statistics
|
|
277
|
-
for rowid in prange(edep_df.shape[0]):
|
|
278
|
-
# if rowid % 100000 == 0:
|
|
279
|
-
# print(rowid)
|
|
280
|
-
t = edep_df[rowid]
|
|
281
|
-
|
|
282
|
-
# get the particle information.
|
|
283
|
-
if t.particle not in pdgid_map:
|
|
284
|
-
pdgid_map[t.particle] = (_pdgid_to_particle(t.particle), _pdg_func.charge(t.particle))
|
|
285
|
-
|
|
286
|
-
# do the scintillation.
|
|
287
|
-
part, charge = pdgid_map[t.particle]
|
|
288
|
-
|
|
289
|
-
# if we have both pre and post step points use them
|
|
290
|
-
# else pass as None
|
|
291
|
-
|
|
292
|
-
scint_times = sc.scintillate(
|
|
293
|
-
scint_mat_params,
|
|
294
|
-
x0[rowid],
|
|
295
|
-
x1[rowid] if x1 is not None else None,
|
|
296
|
-
t.v_pre if x1 is not None else None,
|
|
297
|
-
t.v_post if x1 is not None else None,
|
|
298
|
-
t.time,
|
|
299
|
-
part,
|
|
300
|
-
charge,
|
|
301
|
-
t.edep,
|
|
302
|
-
rng,
|
|
303
|
-
emission_term_model=("poisson" if mode == "no-fano" else "normal_fano"),
|
|
304
|
-
)
|
|
305
|
-
if scint_times.shape[0] == 0: # short-circuit if we have no photons at all.
|
|
306
|
-
continue
|
|
307
|
-
ph_cnt += scint_times.shape[0]
|
|
308
|
-
|
|
309
|
-
# coordinates -> bins of the optical map.
|
|
310
|
-
bins = np.empty((scint_times.shape[0], 3), dtype=np.int64)
|
|
311
|
-
for j in range(3):
|
|
312
|
-
bins[:, j] = np.digitize(scint_times[:, j + 1], optmap_edges[j])
|
|
313
|
-
# normalize all out-of-bounds bins just to one end.
|
|
314
|
-
bins[:, j][bins[:, j] == optmap_edges[j].shape[0]] = 0
|
|
315
|
-
|
|
316
|
-
# there are _much_ less unique bins, unfortunately np.unique(..., axis=n) does not work
|
|
317
|
-
# with numba; also np.sort(..., axis=n) also does not work.
|
|
318
|
-
|
|
319
|
-
counts_per_bin = numba.typed.Dict.empty(
|
|
320
|
-
key_type=__counts_per_bin_key_type,
|
|
321
|
-
value_type=np.int64,
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
# get probabilities from map.
|
|
325
|
-
hitcount = np.zeros((detidx.shape[0], bins.shape[0]), dtype=np.int64)
|
|
326
|
-
for j in prange(bins.shape[0]):
|
|
327
|
-
# note: subtract 1 from bins, to account for np.digitize output.
|
|
328
|
-
cur_bins = (bins[j, 0] - 1, bins[j, 1] - 1, bins[j, 2] - 1)
|
|
329
|
-
if cur_bins[0] == -1 or cur_bins[1] == -1 or cur_bins[2] == -1:
|
|
330
|
-
oob += 1
|
|
331
|
-
continue # out-of-bounds of optmap
|
|
332
|
-
ib += 1
|
|
333
|
-
|
|
334
|
-
px_any = optmap_weights[OPTMAP_ANY_CH, cur_bins[0], cur_bins[1], cur_bins[2]]
|
|
335
|
-
if px_any < 0.0:
|
|
336
|
-
any_no_stats += 1
|
|
337
|
-
continue
|
|
338
|
-
if px_any == 0.0:
|
|
339
|
-
continue
|
|
340
|
-
|
|
341
|
-
if dist == "multinomial":
|
|
342
|
-
if rng.uniform() >= px_any:
|
|
343
|
-
continue
|
|
344
|
-
ph_det += 1
|
|
345
|
-
# we detect this energy deposition; we should at least get one photon out here!
|
|
346
|
-
|
|
347
|
-
detsel_size = 1
|
|
348
|
-
|
|
349
|
-
px_sum = optmap_weights[OPTMAP_SUM_CH, cur_bins[0], cur_bins[1], cur_bins[2]]
|
|
350
|
-
assert px_sum >= 0.0 # should not be negative.
|
|
351
|
-
detp = np.empty(detidx.shape, dtype=np.float64)
|
|
352
|
-
had_det_no_stats = 0
|
|
353
|
-
for d in detidx:
|
|
354
|
-
# normalize so that sum(detp) = 1
|
|
355
|
-
detp[d] = optmap_weights[d, cur_bins[0], cur_bins[1], cur_bins[2]] / px_sum
|
|
356
|
-
if detp[d] < 0.0:
|
|
357
|
-
had_det_no_stats = 1
|
|
358
|
-
detp[d] = 0.0
|
|
359
|
-
det_no_stats += had_det_no_stats
|
|
360
|
-
|
|
361
|
-
# should be equivalent to rng.choice(detidx, size=detsel_size, p=detp)
|
|
362
|
-
detsel = detidx[
|
|
363
|
-
np.searchsorted(np.cumsum(detp), rng.random(size=(detsel_size,)), side="right")
|
|
364
|
-
]
|
|
365
|
-
for d in detsel:
|
|
366
|
-
hitcount[d, j] += 1
|
|
367
|
-
ph_det2 += detsel.shape[0]
|
|
368
|
-
|
|
369
|
-
elif dist == "poisson":
|
|
370
|
-
# store the photon count in each bin, to sample them all at once below.
|
|
371
|
-
if cur_bins not in counts_per_bin:
|
|
372
|
-
counts_per_bin[cur_bins] = 1
|
|
373
|
-
else:
|
|
374
|
-
counts_per_bin[cur_bins] += 1
|
|
375
|
-
|
|
376
|
-
else:
|
|
377
|
-
msg = "unknown distribution"
|
|
378
|
-
raise RuntimeError(msg)
|
|
379
|
-
|
|
380
|
-
if dist == "poisson":
|
|
381
|
-
for j, (cur_bins, ph_counts_to_poisson) in enumerate(counts_per_bin.items()):
|
|
382
|
-
had_det_no_stats = 0
|
|
383
|
-
had_any = 0
|
|
384
|
-
for d in detidx:
|
|
385
|
-
detp = optmap_weights[d, cur_bins[0], cur_bins[1], cur_bins[2]]
|
|
386
|
-
if detp < 0.0:
|
|
387
|
-
had_det_no_stats = 1
|
|
388
|
-
continue
|
|
389
|
-
pois_cnt = rng.poisson(lam=ph_counts_to_poisson * detp)
|
|
390
|
-
hitcount[d, j] += pois_cnt
|
|
391
|
-
ph_det2 += pois_cnt
|
|
392
|
-
had_any = 1
|
|
393
|
-
ph_det += had_any
|
|
394
|
-
det_no_stats += had_det_no_stats
|
|
395
|
-
|
|
396
|
-
assert scint_times.shape[0] >= hitcount.shape[1] # TODO: use the right assertion here.
|
|
397
|
-
out_hits_len = np.sum(hitcount)
|
|
398
|
-
if out_hits_len > 0:
|
|
399
|
-
out_times = np.empty(out_hits_len, dtype=np.float64)
|
|
400
|
-
out_det = np.empty(out_hits_len, dtype=np.int64)
|
|
401
|
-
out_idx = 0
|
|
402
|
-
for d in detidx:
|
|
403
|
-
hc_d_plane_max = np.max(hitcount[d, :])
|
|
404
|
-
# untangle the hitcount array in "planes" that only contain the given number of hits per
|
|
405
|
-
# channel. example: assume a "histogram" of hits per channel:
|
|
406
|
-
# x | | <-- this is plane 2 with 1 hit ("max plane")
|
|
407
|
-
# x | | x <-- this is plane 1 with 2 hits
|
|
408
|
-
# ch: 1 | 2 | 3
|
|
409
|
-
for hc_d_plane_cnt in range(1, hc_d_plane_max + 1):
|
|
410
|
-
hc_d_plane = hitcount[d, :] >= hc_d_plane_cnt
|
|
411
|
-
hc_d_plane_len = np.sum(hc_d_plane)
|
|
412
|
-
if hc_d_plane_len == 0:
|
|
413
|
-
continue
|
|
414
|
-
|
|
415
|
-
# note: we assume "immediate" propagation after scintillation. Here, a single timestamp
|
|
416
|
-
# might be coipied to output/"detected" twice.
|
|
417
|
-
out_times[out_idx : out_idx + hc_d_plane_len] = scint_times[hc_d_plane, 0]
|
|
418
|
-
out_det[out_idx : out_idx + hc_d_plane_len] = detids[d]
|
|
419
|
-
out_idx += hc_d_plane_len
|
|
420
|
-
assert out_idx == out_hits_len # ensure that all of out_{det,times} is filled.
|
|
421
|
-
output_map[np.int64(rowid)] = (t.evtid, out_det, out_times)
|
|
422
|
-
|
|
423
|
-
stats = {
|
|
424
|
-
"oob": oob,
|
|
425
|
-
"ib": ib,
|
|
426
|
-
"vuv_primary": ph_cnt,
|
|
427
|
-
"hits_any": ph_det,
|
|
428
|
-
"hits": ph_det2,
|
|
429
|
-
"any_no_stats": any_no_stats,
|
|
430
|
-
"det_no_stats": det_no_stats,
|
|
431
|
-
}
|
|
432
|
-
return output_map, stats
|
|
433
|
-
|
|
434
|
-
|
|
435
190
|
# - run with NUMBA_FULL_TRACEBACKS=1 NUMBA_BOUNDSCHECK=1 for testing/checking
|
|
436
191
|
# - cache=True does not work with outer prange, i.e. loading the cached file fails (numba bug?)
|
|
437
192
|
# - the output dictionary is not threadsafe, so parallel=True is not working with it.
|
|
@@ -441,6 +196,7 @@ def _iterate_stepwise_depositions_pois(
|
|
|
441
196
|
rng,
|
|
442
197
|
detidx: int,
|
|
443
198
|
map_scaling: float,
|
|
199
|
+
map_scaling_sigma: float,
|
|
444
200
|
optmap_edges,
|
|
445
201
|
optmap_weights,
|
|
446
202
|
scint_mat_params: sc.ComputedScintParams,
|
|
@@ -453,6 +209,10 @@ def _iterate_stepwise_depositions_pois(
|
|
|
453
209
|
hit = edep_hits[rowid]
|
|
454
210
|
hit_output = []
|
|
455
211
|
|
|
212
|
+
map_scaling_evt = map_scaling
|
|
213
|
+
if map_scaling_sigma > 0:
|
|
214
|
+
map_scaling_evt = rng.normal(loc=map_scaling, scale=map_scaling_sigma)
|
|
215
|
+
|
|
456
216
|
assert len(hit.particle) == len(hit.num_scint_ph)
|
|
457
217
|
# iterate steps inside the hit
|
|
458
218
|
for si in range(len(hit.particle)):
|
|
@@ -473,7 +233,7 @@ def _iterate_stepwise_depositions_pois(
|
|
|
473
233
|
ib += 1
|
|
474
234
|
|
|
475
235
|
# get probabilities from map.
|
|
476
|
-
detp = optmap_weights[detidx, cur_bins[0], cur_bins[1], cur_bins[2]] *
|
|
236
|
+
detp = optmap_weights[detidx, cur_bins[0], cur_bins[1], cur_bins[2]] * map_scaling_evt
|
|
477
237
|
if detp < 0.0:
|
|
478
238
|
det_no_stats += 1
|
|
479
239
|
continue
|
|
@@ -543,82 +303,6 @@ def _iterate_stepwise_depositions_scintillate(
|
|
|
543
303
|
return output_list
|
|
544
304
|
|
|
545
305
|
|
|
546
|
-
def get_output_table(output_map):
|
|
547
|
-
ph_count_o = 0
|
|
548
|
-
for _rawid, (_evtid, det, _times) in output_map.items():
|
|
549
|
-
ph_count_o += det.shape[0]
|
|
550
|
-
|
|
551
|
-
out_idx = 0
|
|
552
|
-
out_evtid = np.empty(ph_count_o, dtype=np.int64)
|
|
553
|
-
out_det = np.empty(ph_count_o, dtype=np.int64)
|
|
554
|
-
out_times = np.empty(ph_count_o, dtype=np.float64)
|
|
555
|
-
for _rawid, (evtid, det, times) in output_map.items():
|
|
556
|
-
o_len = det.shape[0]
|
|
557
|
-
out_evtid[out_idx : out_idx + o_len] = evtid
|
|
558
|
-
out_det[out_idx : out_idx + o_len] = det
|
|
559
|
-
out_times[out_idx : out_idx + o_len] = times
|
|
560
|
-
out_idx += o_len
|
|
561
|
-
|
|
562
|
-
tbl = Table({"evtid": Array(out_evtid), "det_uid": Array(out_det), "time": Array(out_times)})
|
|
563
|
-
return ph_count_o, tbl
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
def convolve(
|
|
567
|
-
map_file: str,
|
|
568
|
-
edep_file: str,
|
|
569
|
-
edep_path: str,
|
|
570
|
-
material: str | tuple[sc.ScintConfig, tuple[Quantity, ...]],
|
|
571
|
-
output_file: str | None = None,
|
|
572
|
-
buffer_len: int = int(1e6),
|
|
573
|
-
dist_mode: str = "poisson+no-fano",
|
|
574
|
-
):
|
|
575
|
-
scint_mat_params = _get_scint_params(material)
|
|
576
|
-
|
|
577
|
-
# special handling of distributions and flags.
|
|
578
|
-
dist, mode = dist_mode.split("+")
|
|
579
|
-
if (
|
|
580
|
-
dist not in ("multinomial", "poisson")
|
|
581
|
-
or mode not in ("", "no-fano")
|
|
582
|
-
or (dist == "poisson" and mode != "no-fano")
|
|
583
|
-
):
|
|
584
|
-
msg = f"unsupported statistical distribution {dist_mode} for scintillation emission"
|
|
585
|
-
raise ValueError(msg)
|
|
586
|
-
|
|
587
|
-
log.info("opening map %s", map_file)
|
|
588
|
-
optmap_for_convolve = open_optmap(map_file)
|
|
589
|
-
|
|
590
|
-
log.info("opening energy deposition hit output %s", edep_file)
|
|
591
|
-
it = LH5Iterator(edep_file, edep_path, buffer_len=buffer_len)
|
|
592
|
-
|
|
593
|
-
for it_count, edep_lgdo in enumerate(it):
|
|
594
|
-
edep_df = _reflatten_scint_vov(edep_lgdo.view_as("ak")).to_numpy()
|
|
595
|
-
|
|
596
|
-
log.info("start event processing (%d)", it_count)
|
|
597
|
-
output_map = iterate_stepwise_depositions(
|
|
598
|
-
edep_df, optmap_for_convolve, scint_mat_params, dist=dist, mode=mode
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
log.info("store output photon hits (%d)", it_count)
|
|
602
|
-
ph_count_o, tbl = get_output_table(output_map)
|
|
603
|
-
log.debug(
|
|
604
|
-
"output photons: %d energy depositions -> %d photons", len(output_map), ph_count_o
|
|
605
|
-
)
|
|
606
|
-
if output_file is not None:
|
|
607
|
-
lh5.write(tbl, "optical", lh5_file=output_file, group="stp", wo_mode="append")
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
def _reflatten_scint_vov(arr: ak.Array) -> ak.Array:
|
|
611
|
-
if all(arr[f].ndim == 1 for f in ak.fields(arr)):
|
|
612
|
-
return arr
|
|
613
|
-
|
|
614
|
-
group_num = ak.num(arr["edep"]).to_numpy()
|
|
615
|
-
flattened = {
|
|
616
|
-
f: ak.flatten(arr[f]) if arr[f].ndim > 1 else np.repeat(arr[f].to_numpy(), group_num)
|
|
617
|
-
for f in ak.fields(arr)
|
|
618
|
-
}
|
|
619
|
-
return ak.Array(flattened)
|
|
620
|
-
|
|
621
|
-
|
|
622
306
|
def _get_scint_params(material: str):
|
|
623
307
|
if material == "lar":
|
|
624
308
|
return sc.precompute_scintillation_params(
|