reboost 0.6.2__py3-none-any.whl → 0.7.0__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 +16 -3
- reboost/build_hit.py +102 -58
- reboost/cli.py +1 -0
- reboost/core.py +18 -7
- reboost/daq/__init__.py +5 -0
- reboost/daq/core.py +262 -0
- reboost/daq/utils.py +28 -0
- reboost/hpge/psd.py +444 -94
- reboost/hpge/surface.py +34 -1
- reboost/hpge/utils.py +2 -1
- reboost/iterator.py +4 -1
- reboost/math/stats.py +2 -2
- reboost/optmap/cli.py +1 -1
- reboost/optmap/convolve.py +270 -24
- reboost/optmap/create.py +2 -1
- reboost/optmap/optmap.py +2 -2
- reboost/shape/cluster.py +4 -4
- reboost/spms/__init__.py +5 -0
- reboost/spms/pe.py +99 -0
- reboost/units.py +40 -8
- reboost/utils.py +64 -2
- {reboost-0.6.2.dist-info → reboost-0.7.0.dist-info}/METADATA +4 -4
- reboost-0.7.0.dist-info/RECORD +42 -0
- reboost-0.6.2.dist-info/RECORD +0 -37
- {reboost-0.6.2.dist-info → reboost-0.7.0.dist-info}/WHEEL +0 -0
- {reboost-0.6.2.dist-info → reboost-0.7.0.dist-info}/entry_points.txt +0 -0
- {reboost-0.6.2.dist-info → reboost-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {reboost-0.6.2.dist-info → reboost-0.7.0.dist-info}/top_level.txt +0 -0
reboost/hpge/utils.py
CHANGED
reboost/iterator.py
CHANGED
|
@@ -107,7 +107,10 @@ class GLMIterator:
|
|
|
107
107
|
glm_n_rows = self.sto.read_n_rows(f"glm/{self.lh5_group}", self.glm_file)
|
|
108
108
|
|
|
109
109
|
# get the number of stp rows
|
|
110
|
-
|
|
110
|
+
try:
|
|
111
|
+
stp_n_rows = self.sto.read_n_rows(f"{self.stp_field}/{self.lh5_group}", self.stp_file)
|
|
112
|
+
except Exception:
|
|
113
|
+
stp_n_rows = 0
|
|
111
114
|
|
|
112
115
|
# heuristics for a good buffer length
|
|
113
116
|
if self.use_glm:
|
reboost/math/stats.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Callable
|
|
5
5
|
|
|
6
6
|
import awkward as ak
|
|
7
7
|
import numpy as np
|
|
@@ -111,7 +111,7 @@ def gaussian_sample(mu: ArrayLike, sigma: ArrayLike | float, *, seed: int | None
|
|
|
111
111
|
sigma = sigma.view_as("np")
|
|
112
112
|
elif isinstance(sigma, ak.Array):
|
|
113
113
|
sigma = sigma.to_numpy()
|
|
114
|
-
elif not isinstance(sigma,
|
|
114
|
+
elif not isinstance(sigma, float | int | np.ndarray):
|
|
115
115
|
sigma = np.array(sigma)
|
|
116
116
|
|
|
117
117
|
rng = np.random.default_rng(seed=seed) # Create a random number generator
|
reboost/optmap/cli.py
CHANGED
reboost/optmap/convolve.py
CHANGED
|
@@ -2,17 +2,20 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
|
+
from typing import NamedTuple
|
|
5
6
|
|
|
7
|
+
import awkward as ak
|
|
6
8
|
import legendoptics.scintillate as sc
|
|
7
9
|
import numba
|
|
8
10
|
import numpy as np
|
|
9
|
-
import
|
|
10
|
-
from legendoptics import lar
|
|
11
|
+
from legendoptics import fibers, lar, pen
|
|
11
12
|
from lgdo import lh5
|
|
12
13
|
from lgdo.lh5 import LH5Iterator
|
|
13
14
|
from lgdo.types import Array, Histogram, Table
|
|
14
15
|
from numba import njit, prange
|
|
15
16
|
from numpy.lib.recfunctions import structured_to_unstructured
|
|
17
|
+
from numpy.typing import NDArray
|
|
18
|
+
from pint import Quantity
|
|
16
19
|
|
|
17
20
|
from .numba_pdg import numba_pdgid_funcs
|
|
18
21
|
|
|
@@ -23,7 +26,16 @@ OPTMAP_ANY_CH = -1
|
|
|
23
26
|
OPTMAP_SUM_CH = -2
|
|
24
27
|
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
class OptmapForConvolve(NamedTuple):
|
|
30
|
+
"""A loaded optmap for convolving."""
|
|
31
|
+
|
|
32
|
+
detids: NDArray
|
|
33
|
+
detidx: NDArray
|
|
34
|
+
edges: NDArray
|
|
35
|
+
weights: NDArray
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def open_optmap(optmap_fn: str) -> OptmapForConvolve:
|
|
27
39
|
maps = lh5.ls(optmap_fn)
|
|
28
40
|
# only accept _<number> (/all is read separately)
|
|
29
41
|
det_ntuples = [m for m in maps if re.match(r"_\d+$", m)]
|
|
@@ -37,7 +49,7 @@ def open_optmap(optmap_fn: str):
|
|
|
37
49
|
ow = np.empty((detidx.shape[0] + 2, *optmap_all.weights.nda.shape), dtype=np.float64)
|
|
38
50
|
# 0, ..., len(detidx)-1 AND OPTMAP_ANY_CH might be negative.
|
|
39
51
|
ow[OPTMAP_ANY_CH] = optmap_all.weights.nda
|
|
40
|
-
for i, nt in zip(detidx, det_ntuples):
|
|
52
|
+
for i, nt in zip(detidx, det_ntuples, strict=True):
|
|
41
53
|
optmap = lh5.read(f"/{nt}/p_det", optmap_fn)
|
|
42
54
|
assert isinstance(optmap, Histogram)
|
|
43
55
|
ow[i] = optmap.weights.nda
|
|
@@ -69,15 +81,35 @@ def open_optmap(optmap_fn: str):
|
|
|
69
81
|
if np.isfinite(optmap_multi_det_exp):
|
|
70
82
|
msg = f"found finite _hitcounts_exp {optmap_multi_det_exp} which is not supported any more"
|
|
71
83
|
raise RuntimeError(msg)
|
|
72
|
-
except
|
|
84
|
+
except lh5.exceptions.LH5DecodeError: # the _hitcounts_exp might not be always present.
|
|
73
85
|
pass
|
|
74
86
|
|
|
75
|
-
return detids, detidx, optmap_edges, ow
|
|
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)
|
|
102
|
+
assert isinstance(optmap, Histogram)
|
|
103
|
+
ow = np.empty((1, *optmap.weights.nda.shape), dtype=np.float64)
|
|
104
|
+
ow[0] = optmap.weights.nda
|
|
105
|
+
optmap_edges = tuple([b.edges for b in optmap.binning])
|
|
106
|
+
|
|
107
|
+
return OptmapForConvolve(np.array([spm_det_uid]), np.array([0]), optmap_edges, ow)
|
|
76
108
|
|
|
77
109
|
|
|
78
110
|
def iterate_stepwise_depositions(
|
|
79
111
|
edep_df: np.rec.recarray,
|
|
80
|
-
|
|
112
|
+
optmap: OptmapForConvolve,
|
|
81
113
|
scint_mat_params: sc.ComputedScintParams,
|
|
82
114
|
rng: np.random.Generator = None,
|
|
83
115
|
dist: str = "poisson",
|
|
@@ -95,7 +127,17 @@ def iterate_stepwise_depositions(
|
|
|
95
127
|
|
|
96
128
|
rng = np.random.default_rng() if rng is None else rng
|
|
97
129
|
output_map, res = _iterate_stepwise_depositions(
|
|
98
|
-
edep_df,
|
|
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,
|
|
99
141
|
)
|
|
100
142
|
if res["any_no_stats"] > 0 or res["det_no_stats"] > 0:
|
|
101
143
|
log.warning(
|
|
@@ -120,6 +162,78 @@ def iterate_stepwise_depositions(
|
|
|
120
162
|
return output_map
|
|
121
163
|
|
|
122
164
|
|
|
165
|
+
def iterate_stepwise_depositions_pois(
|
|
166
|
+
edep_hits: ak.Array,
|
|
167
|
+
optmap: OptmapForConvolve,
|
|
168
|
+
scint_mat_params: sc.ComputedScintParams,
|
|
169
|
+
det_uid: int,
|
|
170
|
+
map_scaling: float = 1,
|
|
171
|
+
rng: np.random.Generator | None = None,
|
|
172
|
+
):
|
|
173
|
+
if edep_hits.particle.ndim == 1:
|
|
174
|
+
msg = "the pe processors only support already reshaped output"
|
|
175
|
+
raise ValueError(msg)
|
|
176
|
+
|
|
177
|
+
rng = np.random.default_rng() if rng is None else rng
|
|
178
|
+
res, output_list = _iterate_stepwise_depositions_pois(
|
|
179
|
+
edep_hits,
|
|
180
|
+
rng,
|
|
181
|
+
np.where(optmap.detids == det_uid)[0][0],
|
|
182
|
+
map_scaling,
|
|
183
|
+
optmap.edges,
|
|
184
|
+
optmap.weights,
|
|
185
|
+
scint_mat_params,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# convert the numba result back into an awkward array.
|
|
189
|
+
builder = ak.ArrayBuilder()
|
|
190
|
+
for r in output_list:
|
|
191
|
+
with builder.list():
|
|
192
|
+
for a in r:
|
|
193
|
+
builder.extend(a)
|
|
194
|
+
|
|
195
|
+
if res["det_no_stats"] > 0:
|
|
196
|
+
log.warning(
|
|
197
|
+
"had edep out in voxels without stats: %d",
|
|
198
|
+
res["det_no_stats"],
|
|
199
|
+
)
|
|
200
|
+
if res["oob"] > 0:
|
|
201
|
+
log.warning(
|
|
202
|
+
"had edep out of map bounds: %d (%.2f%%)",
|
|
203
|
+
res["oob"],
|
|
204
|
+
(res["oob"] / (res["ib"] + res["oob"])) * 100,
|
|
205
|
+
)
|
|
206
|
+
log.debug(
|
|
207
|
+
"VUV_primary %d ->hits %d (%.2f %% primaries detected in this channel)",
|
|
208
|
+
res["vuv_primary"],
|
|
209
|
+
res["hits"],
|
|
210
|
+
(res["hits"] / res["vuv_primary"]) * 100,
|
|
211
|
+
)
|
|
212
|
+
return builder.snapshot()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def iterate_stepwise_depositions_scintillate(
|
|
216
|
+
edep_hits: ak.Array,
|
|
217
|
+
scint_mat_params: sc.ComputedScintParams,
|
|
218
|
+
rng: np.random.Generator | None = None,
|
|
219
|
+
mode: str = "no-fano",
|
|
220
|
+
):
|
|
221
|
+
if edep_hits.particle.ndim == 1:
|
|
222
|
+
msg = "the pe processors only support already reshaped output"
|
|
223
|
+
raise ValueError(msg)
|
|
224
|
+
|
|
225
|
+
rng = np.random.default_rng() if rng is None else rng
|
|
226
|
+
output_list = _iterate_stepwise_depositions_scintillate(edep_hits, rng, scint_mat_params, mode)
|
|
227
|
+
|
|
228
|
+
# convert the numba result back into an awkward array.
|
|
229
|
+
builder = ak.ArrayBuilder()
|
|
230
|
+
for r in output_list:
|
|
231
|
+
with builder.list():
|
|
232
|
+
builder.extend(r)
|
|
233
|
+
|
|
234
|
+
return builder.snapshot()
|
|
235
|
+
|
|
236
|
+
|
|
123
237
|
_pdg_func = numba_pdgid_funcs()
|
|
124
238
|
|
|
125
239
|
|
|
@@ -318,6 +432,117 @@ def _iterate_stepwise_depositions(
|
|
|
318
432
|
return output_map, stats
|
|
319
433
|
|
|
320
434
|
|
|
435
|
+
# - run with NUMBA_FULL_TRACEBACKS=1 NUMBA_BOUNDSCHECK=1 for testing/checking
|
|
436
|
+
# - cache=True does not work with outer prange, i.e. loading the cached file fails (numba bug?)
|
|
437
|
+
# - the output dictionary is not threadsafe, so parallel=True is not working with it.
|
|
438
|
+
@njit(parallel=False, nogil=True, cache=True)
|
|
439
|
+
def _iterate_stepwise_depositions_pois(
|
|
440
|
+
edep_hits,
|
|
441
|
+
rng,
|
|
442
|
+
detidx: int,
|
|
443
|
+
map_scaling: float,
|
|
444
|
+
optmap_edges,
|
|
445
|
+
optmap_weights,
|
|
446
|
+
scint_mat_params: sc.ComputedScintParams,
|
|
447
|
+
):
|
|
448
|
+
pdgid_map = {}
|
|
449
|
+
oob = ib = ph_cnt = ph_det2 = det_no_stats = 0 # for statistics
|
|
450
|
+
output_list = []
|
|
451
|
+
|
|
452
|
+
for rowid in range(len(edep_hits)): # iterate hits
|
|
453
|
+
hit = edep_hits[rowid]
|
|
454
|
+
hit_output = []
|
|
455
|
+
|
|
456
|
+
assert len(hit.particle) == len(hit.num_scint_ph)
|
|
457
|
+
# iterate steps inside the hit
|
|
458
|
+
for si in range(len(hit.particle)):
|
|
459
|
+
loc = np.array([hit.xloc[si], hit.yloc[si], hit.zloc[si]])
|
|
460
|
+
# coordinates -> bins of the optical map.
|
|
461
|
+
bins = np.empty(3, dtype=np.int64)
|
|
462
|
+
for j in range(3):
|
|
463
|
+
bins[j] = np.digitize(loc[j], optmap_edges[j])
|
|
464
|
+
# normalize all out-of-bounds bins just to one end.
|
|
465
|
+
if bins[j] == optmap_edges[j].shape[0]:
|
|
466
|
+
bins[j] = 0
|
|
467
|
+
|
|
468
|
+
# note: subtract 1 from bins, to account for np.digitize output.
|
|
469
|
+
cur_bins = (bins[0] - 1, bins[1] - 1, bins[2] - 1)
|
|
470
|
+
if cur_bins[0] == -1 or cur_bins[1] == -1 or cur_bins[2] == -1:
|
|
471
|
+
oob += 1
|
|
472
|
+
continue # out-of-bounds of optmap
|
|
473
|
+
ib += 1
|
|
474
|
+
|
|
475
|
+
# get probabilities from map.
|
|
476
|
+
detp = optmap_weights[detidx, cur_bins[0], cur_bins[1], cur_bins[2]] * map_scaling
|
|
477
|
+
if detp < 0.0:
|
|
478
|
+
det_no_stats += 1
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
pois_cnt = rng.poisson(lam=hit.num_scint_ph[si] * detp)
|
|
482
|
+
ph_cnt += hit.num_scint_ph[si]
|
|
483
|
+
ph_det2 += pois_cnt
|
|
484
|
+
|
|
485
|
+
# get the particle information.
|
|
486
|
+
particle = hit.particle[si]
|
|
487
|
+
if particle not in pdgid_map:
|
|
488
|
+
pdgid_map[particle] = (_pdgid_to_particle(particle), _pdg_func.charge(particle))
|
|
489
|
+
part, _charge = pdgid_map[particle]
|
|
490
|
+
|
|
491
|
+
# get time spectrum.
|
|
492
|
+
# note: we assume "immediate" propagation after scintillation.
|
|
493
|
+
scint_times = sc.scintillate_times(scint_mat_params, part, pois_cnt, rng) + hit.time[si]
|
|
494
|
+
|
|
495
|
+
hit_output.append(scint_times)
|
|
496
|
+
|
|
497
|
+
output_list.append(hit_output)
|
|
498
|
+
|
|
499
|
+
stats = {
|
|
500
|
+
"oob": oob,
|
|
501
|
+
"ib": ib,
|
|
502
|
+
"vuv_primary": ph_cnt,
|
|
503
|
+
"hits": ph_det2,
|
|
504
|
+
"det_no_stats": det_no_stats,
|
|
505
|
+
}
|
|
506
|
+
return stats, output_list
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# - run with NUMBA_FULL_TRACEBACKS=1 NUMBA_BOUNDSCHECK=1 for testing/checking
|
|
510
|
+
# - cache=True does not work with outer prange, i.e. loading the cached file fails (numba bug?)
|
|
511
|
+
@njit(parallel=False, nogil=True, cache=True)
|
|
512
|
+
def _iterate_stepwise_depositions_scintillate(
|
|
513
|
+
edep_hits, rng, scint_mat_params: sc.ComputedScintParams, mode: str
|
|
514
|
+
):
|
|
515
|
+
pdgid_map = {}
|
|
516
|
+
output_list = []
|
|
517
|
+
|
|
518
|
+
for rowid in range(len(edep_hits)): # iterate hits
|
|
519
|
+
hit = edep_hits[rowid]
|
|
520
|
+
hit_output = []
|
|
521
|
+
|
|
522
|
+
# iterate steps inside the hit
|
|
523
|
+
for si in range(len(hit.particle)):
|
|
524
|
+
# get the particle information.
|
|
525
|
+
particle = hit.particle[si]
|
|
526
|
+
if particle not in pdgid_map:
|
|
527
|
+
pdgid_map[particle] = (_pdgid_to_particle(particle), _pdg_func.charge(particle))
|
|
528
|
+
part, _charge = pdgid_map[particle]
|
|
529
|
+
|
|
530
|
+
# do the scintillation.
|
|
531
|
+
num_phot = sc.scintillate_numphot(
|
|
532
|
+
scint_mat_params,
|
|
533
|
+
part,
|
|
534
|
+
hit.edep[si],
|
|
535
|
+
rng,
|
|
536
|
+
emission_term_model=("poisson" if mode == "no-fano" else "normal_fano"),
|
|
537
|
+
)
|
|
538
|
+
hit_output.append(num_phot)
|
|
539
|
+
|
|
540
|
+
assert len(hit_output) == len(hit.particle)
|
|
541
|
+
output_list.append(hit_output)
|
|
542
|
+
|
|
543
|
+
return output_list
|
|
544
|
+
|
|
545
|
+
|
|
321
546
|
def get_output_table(output_map):
|
|
322
547
|
ph_count_o = 0
|
|
323
548
|
for _rawid, (_evtid, det, _times) in output_map.items():
|
|
@@ -342,25 +567,12 @@ def convolve(
|
|
|
342
567
|
map_file: str,
|
|
343
568
|
edep_file: str,
|
|
344
569
|
edep_path: str,
|
|
345
|
-
material: str,
|
|
570
|
+
material: str | tuple[sc.ScintConfig, tuple[Quantity, ...]],
|
|
346
571
|
output_file: str | None = None,
|
|
347
572
|
buffer_len: int = int(1e6),
|
|
348
573
|
dist_mode: str = "poisson+no-fano",
|
|
349
574
|
):
|
|
350
|
-
|
|
351
|
-
msg = f"unknown material {material} for scintillation"
|
|
352
|
-
raise ValueError(msg)
|
|
353
|
-
|
|
354
|
-
if material == "lar":
|
|
355
|
-
scint_mat_params = sc.precompute_scintillation_params(
|
|
356
|
-
lar.lar_scintillation_params(),
|
|
357
|
-
lar.lar_lifetimes().as_tuple(),
|
|
358
|
-
)
|
|
359
|
-
elif material == "pen":
|
|
360
|
-
scint_mat_params = sc.precompute_scintillation_params(
|
|
361
|
-
lar.pen_scintillation_params(),
|
|
362
|
-
(1 * pint.get_application_registry().ns), # dummy!
|
|
363
|
-
)
|
|
575
|
+
scint_mat_params = _get_scint_params(material)
|
|
364
576
|
|
|
365
577
|
# special handling of distributions and flags.
|
|
366
578
|
dist, mode = dist_mode.split("+")
|
|
@@ -379,7 +591,7 @@ def convolve(
|
|
|
379
591
|
it = LH5Iterator(edep_file, edep_path, buffer_len=buffer_len)
|
|
380
592
|
|
|
381
593
|
for it_count, edep_lgdo in enumerate(it):
|
|
382
|
-
edep_df = edep_lgdo.view_as("
|
|
594
|
+
edep_df = _reflatten_scint_vov(edep_lgdo.view_as("ak")).to_numpy()
|
|
383
595
|
|
|
384
596
|
log.info("start event processing (%d)", it_count)
|
|
385
597
|
output_map = iterate_stepwise_depositions(
|
|
@@ -393,3 +605,37 @@ def convolve(
|
|
|
393
605
|
)
|
|
394
606
|
if output_file is not None:
|
|
395
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
|
+
def _get_scint_params(material: str):
|
|
623
|
+
if material == "lar":
|
|
624
|
+
return sc.precompute_scintillation_params(
|
|
625
|
+
lar.lar_scintillation_params(),
|
|
626
|
+
lar.lar_lifetimes().as_tuple(),
|
|
627
|
+
)
|
|
628
|
+
if material == "pen":
|
|
629
|
+
return sc.precompute_scintillation_params(
|
|
630
|
+
pen.pen_scintillation_params(),
|
|
631
|
+
(pen.pen_scint_timeconstant(),),
|
|
632
|
+
)
|
|
633
|
+
if material == "fiber":
|
|
634
|
+
return sc.precompute_scintillation_params(
|
|
635
|
+
fibers.fiber_core_scintillation_params(),
|
|
636
|
+
(fibers.fiber_wls_timeconstant(),),
|
|
637
|
+
)
|
|
638
|
+
if isinstance(material, str):
|
|
639
|
+
msg = f"unknown material {material} for scintillation"
|
|
640
|
+
raise ValueError(msg)
|
|
641
|
+
return sc.precompute_scintillation_params(*material)
|
reboost/optmap/create.py
CHANGED
reboost/optmap/optmap.py
CHANGED
|
@@ -112,7 +112,7 @@ class OpticalMap:
|
|
|
112
112
|
idx = np.zeros(xyz.shape[1], np.int64) # bin indices for flattened array
|
|
113
113
|
oor_mask = np.ones(xyz.shape[1], np.bool_) # mask to remove out of range values
|
|
114
114
|
dims = range(xyz.shape[0])
|
|
115
|
-
for col, ax, s, dim in zip(xyz, self.binning, self._single_stride, dims):
|
|
115
|
+
for col, ax, s, dim in zip(xyz, self.binning, self._single_stride, dims, strict=True):
|
|
116
116
|
assert ax.is_range
|
|
117
117
|
assert ax.closedleft
|
|
118
118
|
oor_mask &= (ax.first <= col) & (col < ax.last)
|
|
@@ -326,4 +326,4 @@ class OpticalMap:
|
|
|
326
326
|
assert all(isinstance(b, np.ndarray) for b in e1)
|
|
327
327
|
assert all(isinstance(b, np.ndarray) for b in e2)
|
|
328
328
|
|
|
329
|
-
return
|
|
329
|
+
return all(np.all(x1 == x2) for x1, x2 in zip(e1, e2, strict=True))
|
reboost/shape/cluster.py
CHANGED
|
@@ -97,9 +97,9 @@ def cluster_by_step_length(
|
|
|
97
97
|
|
|
98
98
|
pos = np.vstack(
|
|
99
99
|
[
|
|
100
|
-
ak.flatten(pos_x).to_numpy(),
|
|
101
|
-
ak.flatten(pos_y).to_numpy(),
|
|
102
|
-
ak.flatten(pos_z).to_numpy(),
|
|
100
|
+
ak.flatten(pos_x).to_numpy().astype(np.float64),
|
|
101
|
+
ak.flatten(pos_y).to_numpy().astype(np.float64),
|
|
102
|
+
ak.flatten(pos_z).to_numpy().astype(np.float64),
|
|
103
103
|
]
|
|
104
104
|
).T
|
|
105
105
|
|
|
@@ -164,7 +164,7 @@ def cluster_by_distance_numba(
|
|
|
164
164
|
return np.sqrt(np.sum((a - b) ** 2))
|
|
165
165
|
|
|
166
166
|
n = len(local_index)
|
|
167
|
-
out = np.zeros(n, dtype=numba.int32)
|
|
167
|
+
out = np.zeros((n,), dtype=numba.int32)
|
|
168
168
|
|
|
169
169
|
trackid_prev = -1
|
|
170
170
|
pos_prev = np.zeros(3, dtype=numba.float64)
|
reboost/spms/__init__.py
ADDED
reboost/spms/pe.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import awkward as ak
|
|
6
|
+
from lgdo import VectorOfVectors
|
|
7
|
+
|
|
8
|
+
from ..optmap import convolve
|
|
9
|
+
from ..units import units_conv_ak
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_optmap_all(map_file: str) -> convolve.OptmapForConvolve:
|
|
15
|
+
"""Load an optical map file for later use with :py:func:`detected_photoelectrons`."""
|
|
16
|
+
return convolve.open_optmap(map_file)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_optmap(map_file: str, spm_det_uid: int) -> convolve.OptmapForConvolve:
|
|
20
|
+
"""Load an optical map file for later use with :py:func:`detected_photoelectrons`."""
|
|
21
|
+
return convolve.open_optmap_single(map_file, spm_det_uid)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detected_photoelectrons(
|
|
25
|
+
num_scint_ph: ak.Array,
|
|
26
|
+
particle: ak.Array,
|
|
27
|
+
time: ak.Array,
|
|
28
|
+
xloc: ak.Array,
|
|
29
|
+
yloc: ak.Array,
|
|
30
|
+
zloc: ak.Array,
|
|
31
|
+
optmap: convolve.OptmapForConvolve,
|
|
32
|
+
material: str,
|
|
33
|
+
spm_detector_uid: int,
|
|
34
|
+
map_scaling: float = 1,
|
|
35
|
+
) -> VectorOfVectors:
|
|
36
|
+
"""Derive the number of detected photoelectrons (p.e.) from scintillator hits using an optical map.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
num_scint_ph
|
|
41
|
+
array of emitted scintillation photons, as generated by
|
|
42
|
+
:func:`emitted_scintillation_photons`.
|
|
43
|
+
particle
|
|
44
|
+
array of particle PDG IDs of scintillation events.
|
|
45
|
+
time
|
|
46
|
+
array of timestamps of scintillation events.
|
|
47
|
+
xloc
|
|
48
|
+
array of x coordinate position of scintillation events.
|
|
49
|
+
yloc
|
|
50
|
+
array of y coordinate position of scintillation events.
|
|
51
|
+
zloc
|
|
52
|
+
array of z coordinate position of scintillation events.
|
|
53
|
+
optmap
|
|
54
|
+
the optical map loaded via py:func:`load_optmap`.
|
|
55
|
+
material
|
|
56
|
+
scintillating material name.
|
|
57
|
+
spm_detector_uid
|
|
58
|
+
SiPM detector uid as used in the optical map.
|
|
59
|
+
map_scaling
|
|
60
|
+
scale the detection probability in the map for this detector by this factor.
|
|
61
|
+
"""
|
|
62
|
+
hits = ak.Array(
|
|
63
|
+
{
|
|
64
|
+
"num_scint_ph": num_scint_ph,
|
|
65
|
+
"particle": particle,
|
|
66
|
+
"time": units_conv_ak(time, "ns"),
|
|
67
|
+
"xloc": units_conv_ak(xloc, "m"),
|
|
68
|
+
"yloc": units_conv_ak(yloc, "m"),
|
|
69
|
+
"zloc": units_conv_ak(zloc, "m"),
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
scint_mat_params = convolve._get_scint_params(material)
|
|
74
|
+
pe = convolve.iterate_stepwise_depositions_pois(
|
|
75
|
+
hits, optmap, scint_mat_params, spm_detector_uid, map_scaling
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return VectorOfVectors(pe, attrs={"units": "ns"})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def emitted_scintillation_photons(
|
|
82
|
+
edep: ak.Array, particle: ak.Array, material: str
|
|
83
|
+
) -> VectorOfVectors:
|
|
84
|
+
"""Derive the number of emitted scintillation photons from scintillator hits.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
edep
|
|
89
|
+
array of deposited energy in scintillation events.
|
|
90
|
+
particle
|
|
91
|
+
array of particle PDG IDs of scintillation events.
|
|
92
|
+
material
|
|
93
|
+
scintillating material name.
|
|
94
|
+
"""
|
|
95
|
+
hits = ak.Array({"edep": units_conv_ak(edep, "keV"), "particle": particle})
|
|
96
|
+
|
|
97
|
+
scint_mat_params = convolve._get_scint_params(material)
|
|
98
|
+
ph = convolve.iterate_stepwise_depositions_scintillate(hits, scint_mat_params)
|
|
99
|
+
return VectorOfVectors(ph)
|
reboost/units.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
import awkward as ak
|
|
6
7
|
import pint
|
|
7
8
|
import pyg4ometry as pg4
|
|
8
9
|
from lgdo import LGDO
|
|
@@ -16,7 +17,7 @@ ureg = pint.get_application_registry()
|
|
|
16
17
|
ureg.formatter.default_format = "~P"
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def pg4_to_pint(obj) -> pint.Quantity:
|
|
20
|
+
def pg4_to_pint(obj: pint.Quantity | pg4.gdml.Defines.VectorBase) -> pint.Quantity:
|
|
20
21
|
"""Convert pyg4ometry object to pint Quantity."""
|
|
21
22
|
if isinstance(obj, pint.Quantity):
|
|
22
23
|
return obj
|
|
@@ -26,32 +27,54 @@ def pg4_to_pint(obj) -> pint.Quantity:
|
|
|
26
27
|
raise ValueError(msg)
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def units_convfact(data: Any, target_units: pint.
|
|
30
|
+
def units_convfact(data: Any | LGDO | ak.Array, target_units: pint.Unit | str) -> float:
|
|
30
31
|
"""Calculate numeric conversion factor to reach `target_units`.
|
|
31
32
|
|
|
32
33
|
Parameters
|
|
33
34
|
----------
|
|
34
35
|
data
|
|
35
|
-
starting data structure. If an LGDO
|
|
36
|
-
into its attributes. Otherwise, just return 1.
|
|
36
|
+
starting data structure. If an :class:`LGDO` or :class:`ak.Array`, try to
|
|
37
|
+
determine units by peeking into its attributes. Otherwise, just return 1.
|
|
37
38
|
target_units
|
|
38
39
|
units you wish to convert data to.
|
|
39
40
|
"""
|
|
40
41
|
if isinstance(data, LGDO) and "units" in data.attrs:
|
|
41
42
|
return ureg(data.attrs["units"]).to(target_units).magnitude
|
|
43
|
+
if isinstance(data, ak.Array) and "units" in ak.parameters(data):
|
|
44
|
+
return ureg(ak.parameters(data)["units"]).to(target_units).magnitude
|
|
42
45
|
return 1
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
def
|
|
48
|
+
def units_conv_ak(data: Any | LGDO | ak.Array, target_units: pint.Unit | str) -> ak.Array:
|
|
49
|
+
"""Calculate numeric conversion factor to reach `target_units`, and apply to data converted to ak.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
data
|
|
54
|
+
starting data structure. If an :class:`LGDO` or :class:`ak.Array`, try to
|
|
55
|
+
determine units by peeking into its attributes. Otherwise, return the data
|
|
56
|
+
unchanged.
|
|
57
|
+
target_units
|
|
58
|
+
units you wish to convert data to.
|
|
59
|
+
"""
|
|
60
|
+
fact = units_convfact(data, target_units)
|
|
61
|
+
if isinstance(data, LGDO) and fact != 1:
|
|
62
|
+
return ak.without_parameters(data.view_as("ak") * fact)
|
|
63
|
+
if isinstance(data, ak.Array) and fact != 1:
|
|
64
|
+
return ak.without_parameters(data * fact)
|
|
65
|
+
return data.view_as("ak") if isinstance(data, LGDO) else data
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def unwrap_lgdo(data: Any | LGDO | ak.Array, library: str = "ak") -> tuple[Any, pint.Unit | None]:
|
|
46
69
|
"""Return a view of the data held by the LGDO and its physical units.
|
|
47
70
|
|
|
48
71
|
Parameters
|
|
49
72
|
----------
|
|
50
73
|
data
|
|
51
|
-
the data container. If not an LGDO
|
|
52
|
-
``None`` units.
|
|
74
|
+
the data container. If not an :class:`LGDO` or :class:`ak.Array`, it will be
|
|
75
|
+
returned as is with ``None`` units.
|
|
53
76
|
library
|
|
54
|
-
forwarded to :
|
|
77
|
+
forwarded to :meth:`LGDO.view_as`.
|
|
55
78
|
|
|
56
79
|
Returns
|
|
57
80
|
-------
|
|
@@ -64,6 +87,15 @@ def unwrap_lgdo(data: Any, library: str = "ak") -> tuple(Any, pint.Unit | None):
|
|
|
64
87
|
if "units" in data.attrs:
|
|
65
88
|
ret_units = ureg(data.attrs["units"]).u
|
|
66
89
|
|
|
90
|
+
if isinstance(data, ak.Array):
|
|
91
|
+
if library != "ak":
|
|
92
|
+
msg = "cannot unwrap an awkward array as a non-awkward type"
|
|
93
|
+
raise ValueError(msg)
|
|
94
|
+
|
|
95
|
+
if "units" in ak.parameters(data):
|
|
96
|
+
ret_units = ureg(ak.parameters(data)["units"]).u
|
|
97
|
+
ret_data = ak.without_parameters(data)
|
|
98
|
+
|
|
67
99
|
return ret_data, ret_units
|
|
68
100
|
|
|
69
101
|
|