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.
@@ -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.lh5 import LH5Iterator
14
- from lgdo.types import Array, Histogram, Table
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
- detids: NDArray
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
- maps = lh5.ls(optmap_fn)
40
- # only accept _<number> (/all is read separately)
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/p_det", optmap_fn)
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, det_ntuples, strict=True):
53
- optmap = lh5.read(f"/{nt}/p_det", optmap_fn)
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
- ow[OPTMAP_SUM_CH] = np.sum(ow[0:-2], axis=0, where=(ow[0:-2] >= 0))
61
- assert not np.any(ow[OPTMAP_SUM_CH] < 0)
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
- detids = np.array([0])
65
- ow[OPTMAP_SUM_CH] = ow[OPTMAP_ANY_CH]
64
+ dets = np.array(["all"])
66
65
 
67
- # give this check some numerical slack.
68
- if np.any(
69
- np.abs(
70
- ow[OPTMAP_SUM_CH][ow[OPTMAP_ANY_CH] >= 0] - ow[OPTMAP_ANY_CH][ow[OPTMAP_ANY_CH] >= 0]
71
- )
72
- < -1e-15
73
- ):
74
- msg = "optical map does not fulfill relation sum(p_i) >= p_any"
75
- raise ValueError(msg)
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
- try:
78
- # check the exponent from the optical map file
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([spm_det_uid]), np.array([0]), optmap_edges, ow)
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
- det_uid: int,
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.detids == det_uid)[0][0],
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]] * map_scaling
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(