reboost 0.7.0__py3-none-any.whl → 0.8.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 CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.7.0'
32
- __version_tuple__ = version_tuple = (0, 7, 0)
31
+ __version__ = version = '0.8.0'
32
+ __version_tuple__ = version_tuple = (0, 8, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
reboost/core.py CHANGED
@@ -325,8 +325,6 @@ def get_one_detector_mapping(
325
325
  out_list = list(output_detector_expression)
326
326
 
327
327
  for expression_tmp in out_list:
328
- func, globs = utils.get_function_string(expression_tmp)
329
-
330
328
  # if no package was imported its just a name
331
329
  try:
332
330
  objs = evaluate_object(expression_tmp, local_dict={"ARGS": args, "OBJECTS": objects})
reboost/optmap/cli.py CHANGED
@@ -35,21 +35,7 @@ def optical_cli() -> None:
35
35
 
36
36
  subparsers = parser.add_subparsers(dest="command", required=True)
37
37
 
38
- # STEP 1: build evt file from stp tier
39
- evt_parser = subparsers.add_parser("evt", help="build optmap-evt file from remage stp file")
40
- evt_parser_det_group = evt_parser.add_mutually_exclusive_group(required=True)
41
- evt_parser_det_group.add_argument(
42
- "--geom",
43
- help="GDML geometry file",
44
- )
45
- evt_parser_det_group.add_argument(
46
- "--detectors",
47
- help="file with detector ids of all optical channels.",
48
- )
49
- evt_parser.add_argument("input", help="input stp LH5 file", metavar="INPUT_STP")
50
- evt_parser.add_argument("output", help="output evt LH5 file", metavar="OUTPUT_EVT")
51
-
52
- # STEP 2a: build map file from evt tier
38
+ # STEP 1a: build map file from evt tier
53
39
  map_parser = subparsers.add_parser("createmap", help="build optical map from evt file(s)")
54
40
  map_parser.add_argument(
55
41
  "--settings",
@@ -91,42 +77,67 @@ def optical_cli() -> None:
91
77
  )
92
78
  map_parser.add_argument("output", help="output map LH5 file", metavar="OUTPUT_MAP")
93
79
 
94
- # STEP 2b: view maps
95
- mapview_parser = subparsers.add_parser("viewmap", help="view optical map")
80
+ # STEP 1b: view maps
81
+ mapview_parser = subparsers.add_parser(
82
+ "viewmap",
83
+ help="view optical map (arrows: navigate slices/axes, 'c': channel selector)",
84
+ formatter_class=argparse.RawTextHelpFormatter,
85
+ description=(
86
+ "Interactively view optical maps stored in LH5 files.\n\n"
87
+ "Keyboard controls:\n"
88
+ " left/right - previous/next slice along the current axis\n"
89
+ " up/down - switch slicing axis (x, y, z)\n"
90
+ " c - open channel selector overlay to switch detector map\n\n"
91
+ "Display notes:\n"
92
+ " - Cells where no primary photons were simulated are shown in white.\n"
93
+ " - Cells where no photons were detected are shown in grey.\n"
94
+ " - Cells with values above the colormap maximum are shown in red.\n"
95
+ " - Use --hist to choose which histogram to display. 'prob_unc_rel' shows the\n"
96
+ " relative uncertainty prob_unc / prob where defined.\n"
97
+ " - Use --divide to show the ratio of two map files (this/other)."
98
+ ),
99
+ epilog=(
100
+ "Examples:\n"
101
+ " reboost-optical viewmap mymap.lh5\n"
102
+ " reboost-optical viewmap mymap.lh5 --channel _1067205\n"
103
+ " reboost-optical viewmap mymap.lh5 --hist prob_unc_rel --min 0 --max 1\n"
104
+ " reboost-optical viewmap mymap.lh5 --divide other.lh5 --title 'Comparison'"
105
+ ),
106
+ )
96
107
  mapview_parser.add_argument("input", help="input map LH5 file", metavar="INPUT_MAP")
97
108
  mapview_parser.add_argument(
98
109
  "--channel",
99
110
  action="store",
100
111
  default="all",
101
- help="default: %(default)s",
112
+ help="channel to display ('all' or '_<detid>'). Press 'c' in the viewer to switch. default: %(default)s",
102
113
  )
103
114
  mapview_parser.add_argument(
104
115
  "--hist",
105
- choices=("nr_gen", "nr_det", "p_det", "p_det_err", "p_det_err_rel"),
116
+ choices=("_nr_gen", "_nr_det", "prob", "prob_unc", "prob_unc_rel"),
106
117
  action="store",
107
- default="p_det",
118
+ default="prob",
108
119
  help="select optical map histogram to show. default: %(default)s",
109
120
  )
110
121
  mapview_parser.add_argument(
111
122
  "--divide",
112
123
  action="store",
113
- help="default: none",
124
+ help="divide by another map file before display (ratio). default: none",
114
125
  )
115
126
  mapview_parser.add_argument(
116
127
  "--min",
117
128
  default=1e-4,
118
129
  type=(lambda s: s if s == "auto" else float(s)),
119
- help="colormap min value. default: %(default)e",
130
+ help="colormap min value; use 'auto' for automatic scaling. default: %(default)e",
120
131
  )
121
132
  mapview_parser.add_argument(
122
133
  "--max",
123
134
  default=1e-2,
124
135
  type=(lambda s: s if s == "auto" else float(s)),
125
- help="colormap max value. default: %(default)e",
136
+ help="colormap max value; use 'auto' for automatic scaling. default: %(default)e",
126
137
  )
127
138
  mapview_parser.add_argument("--title", help="title of figure. default: stem of filename")
128
139
 
129
- # STEP 2c: merge maps
140
+ # STEP 1c: merge maps
130
141
  mapmerge_parser = subparsers.add_parser("mergemap", help="merge optical maps")
131
142
  mapmerge_parser.add_argument(
132
143
  "input", help="input map LH5 files", metavar="INPUT_MAP", nargs="+"
@@ -151,49 +162,10 @@ def optical_cli() -> None:
151
162
  help="""Check map statistics after creation. default: %(default)s""",
152
163
  )
153
164
 
154
- # STEP 2d: check map
165
+ # STEP 1d: check map
155
166
  checkmap_parser = subparsers.add_parser("checkmap", help="check optical maps")
156
167
  checkmap_parser.add_argument("input", help="input map LH5 file", metavar="INPUT_MAP")
157
168
 
158
- # STEP 3: convolve with hits from non-optical simulations
159
- convolve_parser = subparsers.add_parser(
160
- "convolve", help="convolve non-optical hits with optical map"
161
- )
162
- convolve_parser.add_argument(
163
- "--material",
164
- action="store",
165
- choices=("lar", "pen", "fiber"),
166
- default="lar",
167
- help="default: %(default)s",
168
- )
169
- convolve_parser.add_argument(
170
- "--map",
171
- action="store",
172
- required=True,
173
- metavar="INPUT_MAP",
174
- help="input map LH5 file",
175
- )
176
- convolve_parser.add_argument(
177
- "--edep",
178
- action="store",
179
- required=True,
180
- metavar="INPUT_EDEP",
181
- help="input non-optical LH5 hit file",
182
- )
183
- convolve_parser.add_argument(
184
- "--edep-lgdo",
185
- action="store",
186
- required=True,
187
- metavar="LGDO_PATH",
188
- help="path to LGDO inside non-optical LH5 hit file (e.g. /stp/detXX)",
189
- )
190
- convolve_parser.add_argument(
191
- "--dist-mode",
192
- action="store",
193
- default="poisson+no-fano",
194
- )
195
- convolve_parser.add_argument("--output", help="output hit LH5 file", metavar="OUTPUT_HIT")
196
-
197
169
  # STEP X: rebin maps
198
170
  rebin_parser = subparsers.add_parser("rebin", help="rebin optical maps")
199
171
  rebin_parser.add_argument("input", help="input map LH5 files", metavar="INPUT_MAP")
@@ -205,24 +177,7 @@ def optical_cli() -> None:
205
177
  log_level = (None, logging.INFO, logging.DEBUG)[min(args.verbose, 2)]
206
178
  setup_log(log_level)
207
179
 
208
- # STEP 1: build evt file from hit tier
209
- if args.command == "evt":
210
- from .evt import build_optmap_evt, get_optical_detectors_from_geom
211
-
212
- _check_input_file(parser, args.input)
213
- _check_output_file(parser, args.output)
214
-
215
- # load detector ids from the geometry.
216
- if args.geom is not None:
217
- _check_input_file(parser, args.geom, "geometry")
218
- detectors = get_optical_detectors_from_geom(args.geom)
219
- else:
220
- _check_input_file(parser, args.detectors, "detectors")
221
- detectors = dbetto.utils.load_dict(args.detectors)
222
-
223
- build_optmap_evt(args.input, args.output, detectors, args.bufsize)
224
-
225
- # STEP 2a: build map file from evt tier
180
+ # STEP 1a: build map file from evt tier
226
181
  if args.command == "createmap":
227
182
  from .create import create_optical_maps
228
183
 
@@ -250,7 +205,7 @@ def optical_cli() -> None:
250
205
  geom_fn=args.geom,
251
206
  )
252
207
 
253
- # STEP 2b: view maps
208
+ # STEP 1b: view maps
254
209
  if args.command == "viewmap":
255
210
  from .mapview import view_optmap
256
211
 
@@ -267,7 +222,7 @@ def optical_cli() -> None:
267
222
  histogram_choice=args.hist,
268
223
  )
269
224
 
270
- # STEP 2c: merge maps
225
+ # STEP 1c: merge maps
271
226
  if args.command == "mergemap":
272
227
  from .create import merge_optical_maps
273
228
 
@@ -281,29 +236,13 @@ def optical_cli() -> None:
281
236
  args.input, args.output, settings, check_after_create=args.check, n_procs=args.n_procs
282
237
  )
283
238
 
284
- # STEP 2d: check maps
239
+ # STEP 1d: check maps
285
240
  if args.command == "checkmap":
286
241
  from .create import check_optical_map
287
242
 
288
243
  _check_input_file(parser, args.input)
289
244
  check_optical_map(args.input)
290
245
 
291
- # STEP 3: convolve with hits from non-optical simulations
292
- if args.command == "convolve":
293
- from .convolve import convolve
294
-
295
- _check_input_file(parser, [args.map, args.edep])
296
- _check_output_file(parser, args.output, optional=True)
297
- convolve(
298
- args.map,
299
- args.edep,
300
- args.edep_lgdo,
301
- args.material,
302
- args.output,
303
- args.bufsize,
304
- dist_mode=args.dist_mode,
305
- )
306
-
307
246
  # STEP X: rebin maps
308
247
  if args.command == "rebin":
309
248
  from .create import rebin_optical_maps
@@ -10,12 +10,9 @@ import numba
10
10
  import numpy as np
11
11
  from legendoptics import fibers, lar, pen
12
12
  from lgdo import lh5
13
- from lgdo.lh5 import LH5Iterator
14
13
  from lgdo.types import Array, Histogram, Table
15
- from numba import njit, prange
16
- from numpy.lib.recfunctions import structured_to_unstructured
14
+ from numba import njit
17
15
  from numpy.typing import NDArray
18
- from pint import Quantity
19
16
 
20
17
  from .numba_pdg import numba_pdgid_funcs
21
18
 
@@ -42,7 +39,7 @@ def open_optmap(optmap_fn: str) -> OptmapForConvolve:
42
39
  detids = np.array([int(m.lstrip("_")) for m in det_ntuples])
43
40
  detidx = np.arange(0, detids.shape[0])
44
41
 
45
- optmap_all = lh5.read("/all/p_det", optmap_fn)
42
+ optmap_all = lh5.read("/all/prob", optmap_fn)
46
43
  assert isinstance(optmap_all, Histogram)
47
44
  optmap_edges = tuple([b.edges for b in optmap_all.binning])
48
45
 
@@ -50,7 +47,7 @@ def open_optmap(optmap_fn: str) -> OptmapForConvolve:
50
47
  # 0, ..., len(detidx)-1 AND OPTMAP_ANY_CH might be negative.
51
48
  ow[OPTMAP_ANY_CH] = optmap_all.weights.nda
52
49
  for i, nt in zip(detidx, det_ntuples, strict=True):
53
- optmap = lh5.read(f"/{nt}/p_det", optmap_fn)
50
+ optmap = lh5.read(f"/{nt}/prob", optmap_fn)
54
51
  assert isinstance(optmap, Histogram)
55
52
  ow[i] = optmap.weights.nda
56
53
 
@@ -98,7 +95,7 @@ def open_optmap_single(optmap_fn: str, spm_det_uid: int) -> OptmapForConvolve:
98
95
  except lh5.exceptions.LH5DecodeError: # the _hitcounts_exp might not be always present.
99
96
  pass
100
97
 
101
- optmap = lh5.read(f"/_{spm_det_uid}/p_det", optmap_fn)
98
+ optmap = lh5.read(f"/_{spm_det_uid}/prob", optmap_fn)
102
99
  assert isinstance(optmap, Histogram)
103
100
  ow = np.empty((1, *optmap.weights.nda.shape), dtype=np.float64)
104
101
  ow[0] = optmap.weights.nda
@@ -107,67 +104,13 @@ def open_optmap_single(optmap_fn: str, spm_det_uid: int) -> OptmapForConvolve:
107
104
  return OptmapForConvolve(np.array([spm_det_uid]), np.array([0]), optmap_edges, ow)
108
105
 
109
106
 
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
163
-
164
-
165
107
  def iterate_stepwise_depositions_pois(
166
108
  edep_hits: ak.Array,
167
109
  optmap: OptmapForConvolve,
168
110
  scint_mat_params: sc.ComputedScintParams,
169
111
  det_uid: int,
170
112
  map_scaling: float = 1,
113
+ map_scaling_sigma: float = 0,
171
114
  rng: np.random.Generator | None = None,
172
115
  ):
173
116
  if edep_hits.particle.ndim == 1:
@@ -180,6 +123,7 @@ def iterate_stepwise_depositions_pois(
180
123
  rng,
181
124
  np.where(optmap.detids == det_uid)[0][0],
182
125
  map_scaling,
126
+ map_scaling_sigma,
183
127
  optmap.edges,
184
128
  optmap.weights,
185
129
  scint_mat_params,
@@ -254,184 +198,6 @@ def _pdgid_to_particle(pdgid: int) -> sc.ParticleIndex:
254
198
  __counts_per_bin_key_type = numba.types.UniTuple(numba.types.int64, 3)
255
199
 
256
200
 
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
201
  # - run with NUMBA_FULL_TRACEBACKS=1 NUMBA_BOUNDSCHECK=1 for testing/checking
436
202
  # - cache=True does not work with outer prange, i.e. loading the cached file fails (numba bug?)
437
203
  # - the output dictionary is not threadsafe, so parallel=True is not working with it.
@@ -441,6 +207,7 @@ def _iterate_stepwise_depositions_pois(
441
207
  rng,
442
208
  detidx: int,
443
209
  map_scaling: float,
210
+ map_scaling_sigma: float,
444
211
  optmap_edges,
445
212
  optmap_weights,
446
213
  scint_mat_params: sc.ComputedScintParams,
@@ -453,6 +220,10 @@ def _iterate_stepwise_depositions_pois(
453
220
  hit = edep_hits[rowid]
454
221
  hit_output = []
455
222
 
223
+ map_scaling_evt = map_scaling
224
+ if map_scaling_sigma > 0:
225
+ map_scaling_evt = rng.normal(loc=map_scaling, scale=map_scaling_sigma)
226
+
456
227
  assert len(hit.particle) == len(hit.num_scint_ph)
457
228
  # iterate steps inside the hit
458
229
  for si in range(len(hit.particle)):
@@ -473,7 +244,7 @@ def _iterate_stepwise_depositions_pois(
473
244
  ib += 1
474
245
 
475
246
  # get probabilities from map.
476
- detp = optmap_weights[detidx, cur_bins[0], cur_bins[1], cur_bins[2]] * map_scaling
247
+ detp = optmap_weights[detidx, cur_bins[0], cur_bins[1], cur_bins[2]] * map_scaling_evt
477
248
  if detp < 0.0:
478
249
  det_no_stats += 1
479
250
  continue
@@ -563,50 +334,6 @@ def get_output_table(output_map):
563
334
  return ph_count_o, tbl
564
335
 
565
336
 
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
337
  def _reflatten_scint_vov(arr: ak.Array) -> ak.Array:
611
338
  if all(arr[f].ndim == 1 for f in ak.fields(arr)):
612
339
  return arr
reboost/optmap/create.py CHANGED
@@ -9,17 +9,14 @@ from pathlib import Path
9
9
  from typing import Literal
10
10
 
11
11
  import numpy as np
12
- import scipy.optimize
13
- from lgdo import Array, Histogram, Scalar, lh5
12
+ from lgdo import Histogram, lh5
14
13
  from numba import njit
15
14
  from numpy.typing import NDArray
16
15
 
17
16
  from ..log_utils import setup_log
18
17
  from .evt import (
19
- EVT_TABLE_NAME,
20
18
  generate_optmap_evt,
21
19
  get_optical_detectors_from_geom,
22
- read_optmap_evt,
23
20
  )
24
21
  from .optmap import OpticalMap
25
22
 
@@ -27,23 +24,21 @@ log = logging.getLogger(__name__)
27
24
 
28
25
 
29
26
  def _optmaps_for_channels(
30
- optmap_evt_columns: list[str],
27
+ all_det_ids: dict[int, str],
31
28
  settings,
32
29
  chfilter: tuple[str | int] | Literal["*"] = (),
33
30
  use_shmem: bool = False,
34
31
  ):
35
- all_det_ids = [ch_id for ch_id in optmap_evt_columns if ch_id.isnumeric()]
36
-
37
32
  if chfilter != "*":
38
33
  chfilter = [str(ch) for ch in chfilter] # normalize types
39
- optmap_det_ids = [det for det in all_det_ids if str(det) in chfilter]
34
+ optmap_det_ids = {det: name for det, name in all_det_ids.items() if str(det) in chfilter}
40
35
  else:
41
36
  optmap_det_ids = all_det_ids
42
37
 
43
38
  log.info("creating empty optmaps")
44
39
  optmap_count = len(optmap_det_ids) + 1
45
40
  optmaps = [
46
- OpticalMap("all" if i == 0 else optmap_det_ids[i - 1], settings, use_shmem)
41
+ OpticalMap("all" if i == 0 else list(optmap_det_ids.values())[i - 1], settings, use_shmem)
47
42
  for i in range(optmap_count)
48
43
  ]
49
44
 
@@ -76,34 +71,6 @@ def _fill_hit_maps(optmaps: list[OpticalMap], loc, hitcounts: NDArray, ch_idx_to
76
71
  optmaps[i].fill_hits(locm)
77
72
 
78
73
 
79
- def _count_multi_ph_detection(hitcounts) -> NDArray:
80
- hits_per_primary = hitcounts.sum(axis=1)
81
- bins = np.arange(0, hits_per_primary.max() + 1.5) - 0.5
82
- return np.histogram(hits_per_primary, bins)[0]
83
-
84
-
85
- def _fit_multi_ph_detection(hits_per_primary) -> float:
86
- if len(hits_per_primary) <= 2: # have only 0 and 1 hits, can't fit (and also don't need to).
87
- return np.inf
88
-
89
- x = np.arange(0, len(hits_per_primary))
90
- popt, pcov = scipy.optimize.curve_fit(
91
- lambda x, p0, k: p0 * np.exp(-k * x), x[1:], hits_per_primary[1:]
92
- )
93
- best_fit_exponent = popt[1]
94
-
95
- log.info(
96
- "p(> 1 detected photon)/p(1 detected photon) = %f",
97
- sum(hits_per_primary[2:]) / hits_per_primary[1],
98
- )
99
- log.info(
100
- "p(> 1 detected photon)/p(<=1 detected photon) = %f",
101
- sum(hits_per_primary[2:]) / sum(hits_per_primary[0:2]),
102
- )
103
-
104
- return best_fit_exponent
105
-
106
-
107
74
  def _create_optical_maps_process_init(optmaps, log_level) -> None:
108
75
  # need to use shared global state. passing the shared memory arrays via "normal" arguments to
109
76
  # the worker function is not supported...
@@ -115,34 +82,29 @@ def _create_optical_maps_process_init(optmaps, log_level) -> None:
115
82
 
116
83
 
117
84
  def _create_optical_maps_process(
118
- optmap_events_fn, buffer_len, is_stp_file, all_det_ids, ch_idx_to_map_idx
119
- ) -> None:
85
+ optmap_events_fn, buffer_len, all_det_ids, ch_idx_to_map_idx
86
+ ) -> bool:
120
87
  log.info("started worker task for %s", optmap_events_fn)
121
88
  x = _create_optical_maps_chunk(
122
89
  optmap_events_fn,
123
90
  buffer_len,
124
- is_stp_file,
125
91
  all_det_ids,
126
92
  _shared_optmaps,
127
93
  ch_idx_to_map_idx,
128
94
  )
129
95
  log.info("finished worker task for %s", optmap_events_fn)
130
- return tuple(int(i) for i in x)
96
+ return x
131
97
 
132
98
 
133
99
  def _create_optical_maps_chunk(
134
- optmap_events_fn, buffer_len, is_stp_file, all_det_ids, optmaps, ch_idx_to_map_idx
135
- ) -> None:
136
- if not is_stp_file:
137
- optmap_events_it = read_optmap_evt(optmap_events_fn, buffer_len)
138
- else:
139
- optmap_events_it = generate_optmap_evt(optmap_events_fn, all_det_ids, buffer_len)
100
+ optmap_events_fn, buffer_len, all_det_ids, optmaps, ch_idx_to_map_idx
101
+ ) -> bool:
102
+ cols = [str(c) for c in all_det_ids]
103
+ optmap_events_it = generate_optmap_evt(optmap_events_fn, cols, buffer_len)
140
104
 
141
- hits_per_primary = np.zeros(10, dtype=np.int64)
142
- hits_per_primary_len = 0
143
105
  for it_count, events_lgdo in enumerate(optmap_events_it):
144
106
  optmap_events = events_lgdo.view_as("pd")
145
- hitcounts = optmap_events[all_det_ids].to_numpy()
107
+ hitcounts = optmap_events[cols].to_numpy()
146
108
  loc = optmap_events[["xloc", "yloc", "zloc"]].to_numpy()
147
109
 
148
110
  log.debug("filling vertex histogram (%d)", it_count)
@@ -150,23 +112,19 @@ def _create_optical_maps_chunk(
150
112
 
151
113
  log.debug("filling hits histogram (%d)", it_count)
152
114
  _fill_hit_maps(optmaps, loc, hitcounts, ch_idx_to_map_idx)
153
- hpp = _count_multi_ph_detection(hitcounts)
154
- hits_per_primary_len = max(hits_per_primary_len, len(hpp))
155
- hits_per_primary[0 : len(hpp)] += hpp
156
115
 
157
116
  # commit the final part of the hits to the maps.
158
117
  for i in range(len(optmaps)):
159
118
  optmaps[i].fill_hits_flush()
160
119
  gc.collect()
161
120
 
162
- return hits_per_primary[0:hits_per_primary_len]
121
+ return True
163
122
 
164
123
 
165
124
  def create_optical_maps(
166
125
  optmap_events_fn: list[str],
167
126
  settings,
168
127
  buffer_len: int = int(5e6),
169
- is_stp_file: bool = True,
170
128
  chfilter: tuple[str | int] | Literal["*"] = (),
171
129
  output_lh5_fn: str | None = None,
172
130
  after_save: Callable[[int, str, OpticalMap]] | None = None,
@@ -182,8 +140,6 @@ def create_optical_maps(
182
140
  list of filenames to lh5 files, that can either be stp files from remage or "optmap-evt"
183
141
  files with a table ``/optmap_evt`` with columns ``{x,y,z}loc`` and one column (with numeric
184
142
  header) for each SiPM channel.
185
- is_stp_file
186
- if true, do convert a remage output file (stp file) on-the-fly to an optmap-evt file.
187
143
  chfilter
188
144
  tuple of detector ids that will be included in the resulting optmap. Those have to match
189
145
  the column names in ``optmap_events_fn``.
@@ -196,12 +152,7 @@ def create_optical_maps(
196
152
 
197
153
  use_shmem = n_procs is None or n_procs > 1
198
154
 
199
- if not is_stp_file:
200
- optmap_evt_columns = list(
201
- lh5.read(EVT_TABLE_NAME, optmap_events_fn[0], start_row=0, n_rows=1).keys()
202
- ) # peek into the (first) file to find column names.
203
- else:
204
- optmap_evt_columns = [str(i) for i in get_optical_detectors_from_geom(geom_fn)]
155
+ optmap_evt_columns = get_optical_detectors_from_geom(geom_fn)
205
156
 
206
157
  all_det_ids, optmaps, optmap_det_ids = _optmaps_for_channels(
207
158
  optmap_evt_columns, settings, chfilter=chfilter, use_shmem=use_shmem
@@ -209,11 +160,17 @@ def create_optical_maps(
209
160
 
210
161
  # indices for later use in _compute_hit_maps.
211
162
  ch_idx_to_map_idx = np.array(
212
- [optmap_det_ids.index(d) + 1 if d in optmap_det_ids else -1 for d in all_det_ids]
163
+ [
164
+ list(optmap_det_ids.keys()).index(d) + 1 if d in optmap_det_ids else -1
165
+ for d in all_det_ids
166
+ ]
213
167
  )
214
168
  assert np.sum(ch_idx_to_map_idx > 0) == len(optmaps) - 1
215
169
 
216
- log.info("creating optical map groups: %s", ", ".join(["all", *optmap_det_ids]))
170
+ log.info(
171
+ "creating optical map groups: %s",
172
+ ", ".join(["all", *[str(t) for t in optmap_det_ids.items()]]),
173
+ )
217
174
 
218
175
  q = []
219
176
 
@@ -221,9 +178,7 @@ def create_optical_maps(
221
178
  if not use_shmem:
222
179
  for fn in optmap_events_fn:
223
180
  q.append(
224
- _create_optical_maps_chunk(
225
- fn, buffer_len, is_stp_file, all_det_ids, optmaps, ch_idx_to_map_idx
226
- )
181
+ _create_optical_maps_chunk(fn, buffer_len, all_det_ids, optmaps, ch_idx_to_map_idx)
227
182
  )
228
183
  else:
229
184
  ctx = mp.get_context("forkserver")
@@ -243,14 +198,14 @@ def create_optical_maps(
243
198
  for fn in optmap_events_fn:
244
199
  r = pool.apply_async(
245
200
  _create_optical_maps_process,
246
- args=(fn, buffer_len, is_stp_file, all_det_ids, ch_idx_to_map_idx),
201
+ args=(fn, buffer_len, all_det_ids, ch_idx_to_map_idx),
247
202
  )
248
203
  pool_results.append((r, fn))
249
204
 
250
205
  pool.close()
251
206
  for r, fn in pool_results:
252
207
  try:
253
- q.append(np.array(r.get()))
208
+ q.append(r.get())
254
209
  except BaseException as e:
255
210
  msg = f"error while processing file {fn}"
256
211
  raise RuntimeError(msg) from e # re-throw errors of workers.
@@ -258,17 +213,8 @@ def create_optical_maps(
258
213
  pool.join()
259
214
  log.info("joined worker process pool")
260
215
 
261
- # merge hitcounts.
262
216
  if len(q) != len(optmap_events_fn):
263
217
  log.error("got %d results for %d files", len(q), len(optmap_events_fn))
264
- hits_per_primary = np.zeros(10, dtype=np.int64)
265
- hits_per_primary_len = 0
266
- for hitcounts in q:
267
- hits_per_primary[0 : len(hitcounts)] += hitcounts
268
- hits_per_primary_len = max(hits_per_primary_len, len(hitcounts))
269
-
270
- hits_per_primary = hits_per_primary[0:hits_per_primary_len]
271
- hits_per_primary_exponent = _fit_multi_ph_detection(hits_per_primary)
272
218
 
273
219
  # all maps share the same vertex histogram.
274
220
  for i in range(1, len(optmaps)):
@@ -279,7 +225,7 @@ def create_optical_maps(
279
225
  optmaps[i].create_probability()
280
226
  if check_after_create:
281
227
  optmaps[i].check_histograms()
282
- group = "all" if i == 0 else "_" + optmap_det_ids[i - 1]
228
+ group = "all" if i == 0 else "channels/" + list(optmap_det_ids.values())[i - 1]
283
229
  if output_lh5_fn is not None:
284
230
  optmaps[i].write_lh5(lh5_file=output_lh5_fn, group=group)
285
231
 
@@ -288,14 +234,12 @@ def create_optical_maps(
288
234
 
289
235
  optmaps[i] = None # clear some memory.
290
236
 
291
- if output_lh5_fn is not None:
292
- lh5.write(Array(hits_per_primary), "_hitcounts", lh5_file=output_lh5_fn)
293
- lh5.write(Scalar(hits_per_primary_exponent), "_hitcounts_exp", lh5_file=output_lh5_fn)
294
-
295
237
 
296
238
  def list_optical_maps(lh5_file: str) -> list[str]:
297
- maps = lh5.ls(lh5_file)
298
- return [m for m in maps if m not in ("_hitcounts", "_hitcounts_exp")]
239
+ maps = list(lh5.ls(lh5_file, "/channels/"))
240
+ if "all" in lh5.ls(lh5_file):
241
+ maps.append("all")
242
+ return maps
299
243
 
300
244
 
301
245
  def _merge_optical_maps_process(
@@ -313,9 +257,9 @@ def _merge_optical_maps_process(
313
257
 
314
258
  all_edges = None
315
259
  for optmap_fn in map_l5_files:
316
- nr_det = lh5.read(f"/{d}/nr_det", optmap_fn)
260
+ nr_det = lh5.read(f"/{d}/_nr_det", optmap_fn)
317
261
  assert isinstance(nr_det, Histogram)
318
- nr_gen = lh5.read(f"/{d}/nr_gen", optmap_fn)
262
+ nr_gen = lh5.read(f"/{d}/_nr_gen", optmap_fn)
319
263
  assert isinstance(nr_gen, Histogram)
320
264
 
321
265
  assert OpticalMap._edges_eq(nr_det.binning, nr_gen.binning)
@@ -333,7 +277,8 @@ def _merge_optical_maps_process(
333
277
  merged_map.check_histograms(include_prefix=True)
334
278
 
335
279
  if write_part_file:
336
- output_lh5_fn = f"{output_lh5_fn}_{d}.mappart.lh5"
280
+ d_for_tmp = d.replace("/", "_")
281
+ output_lh5_fn = f"{output_lh5_fn}_{d_for_tmp}.mappart.lh5"
337
282
  wo_mode = "overwrite_file" if write_part_file else "write_safe"
338
283
  merged_map.write_lh5(lh5_file=output_lh5_fn, group=d, wo_mode=wo_mode)
339
284
 
@@ -410,7 +355,7 @@ def merge_optical_maps(
410
355
  # transfer to actual output file.
411
356
  for d, part_fn in q:
412
357
  assert isinstance(part_fn, str)
413
- for h_name in ("nr_det", "nr_gen", "p_det", "p_det_err"):
358
+ for h_name in ("_nr_det", "_nr_gen", "prob", "prob_unc"):
414
359
  obj = f"/{d}/{h_name}"
415
360
  log.info("transfer %s from %s", obj, part_fn)
416
361
  h = lh5.read(obj, part_fn)
@@ -418,43 +363,19 @@ def merge_optical_maps(
418
363
  lh5.write(h, obj, output_lh5_fn, wo_mode="write_safe")
419
364
  Path(part_fn).unlink()
420
365
 
421
- # merge hitcounts.
422
- hits_per_primary = np.zeros(10, dtype=np.int64)
423
- hits_per_primary_len = 0
424
- for optmap_fn in map_l5_files:
425
- if "_hitcounts" not in lh5.ls(optmap_fn):
426
- log.warning("skipping _hitcounts calculations, missing in file %s", optmap_fn)
427
- return
428
- hitcounts = lh5.read("/_hitcounts", optmap_fn)
429
- assert isinstance(hitcounts, Array)
430
- hits_per_primary[0 : len(hitcounts)] += hitcounts
431
- hits_per_primary_len = max(hits_per_primary_len, len(hitcounts))
432
-
433
- hits_per_primary = hits_per_primary[0:hits_per_primary_len]
434
- lh5.write(Array(hits_per_primary), "_hitcounts", lh5_file=output_lh5_fn)
435
-
436
- # re-calculate hitcounts exponent.
437
- hits_per_primary_exponent = _fit_multi_ph_detection(hits_per_primary)
438
- lh5.write(Scalar(hits_per_primary_exponent), "_hitcounts_exp", lh5_file=output_lh5_fn)
439
-
440
366
 
441
367
  def check_optical_map(map_l5_file: str):
442
368
  """Run a health check on the map file.
443
369
 
444
370
  This checks for consistency, and outputs details on map statistics.
445
371
  """
446
- if "_hitcounts_exp" not in lh5.ls(map_l5_file):
447
- log.info("no _hitcounts_exp found")
448
- elif lh5.read("_hitcounts_exp", lh5_file=map_l5_file).value != np.inf:
372
+ if (
373
+ "_hitcounts_exp" in lh5.ls(map_l5_file)
374
+ and lh5.read("_hitcounts_exp", lh5_file=map_l5_file).value != np.inf
375
+ ):
449
376
  log.error("unexpected _hitcounts_exp not equal to positive infinity")
450
377
  return
451
378
 
452
- if "_hitcounts" not in lh5.ls(map_l5_file):
453
- log.info("no _hitcounts found")
454
- elif lh5.read("_hitcounts", lh5_file=map_l5_file).nda.shape != (2,):
455
- log.error("unexpected _hitcounts shape")
456
- return
457
-
458
379
  all_binning = None
459
380
  for submap in list_optical_maps(map_l5_file):
460
381
  try:
@@ -503,8 +424,3 @@ def rebin_optical_maps(map_l5_file: str, output_lh5_file: str, factor: int):
503
424
  om_new.h_hits = _rebin_map(om.h_hits, factor)
504
425
  om_new.create_probability()
505
426
  om_new.write_lh5(lh5_file=output_lh5_file, group=submap, wo_mode="write_safe")
506
-
507
- # just copy hitcounts exponent.
508
- for dset in ("_hitcounts_exp", "_hitcounts"):
509
- if dset in lh5.ls(map_l5_file):
510
- lh5.write(lh5.read(dset, lh5_file=map_l5_file), dset, lh5_file=output_lh5_file)
reboost/optmap/evt.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ from collections import OrderedDict
4
5
  from collections.abc import Generator, Iterable
5
6
  from pathlib import Path
6
7
 
@@ -125,13 +126,15 @@ def build_optmap_evt(
125
126
  lh5_out_file_tmp.rename(lh5_out_file)
126
127
 
127
128
 
128
- def get_optical_detectors_from_geom(geom_fn) -> list[int]:
129
+ def get_optical_detectors_from_geom(geom_fn) -> dict[int, str]:
129
130
  import pyg4ometry
130
131
  import pygeomtools
131
132
 
132
133
  geom_registry = pyg4ometry.gdml.Reader(geom_fn).getRegistry()
133
134
  detectors = pygeomtools.get_all_sensvols(geom_registry)
134
- return [d.uid for d in detectors.values() if d.detector_type == "optical"]
135
+ return OrderedDict(
136
+ [(d.uid, name) for name, d in detectors.items() if d.detector_type == "optical"]
137
+ )
135
138
 
136
139
 
137
140
  def read_optmap_evt(lh5_file: str, buffer_len: int = int(5e6)) -> LH5Iterator:
reboost/optmap/mapview.py CHANGED
@@ -92,14 +92,16 @@ def _channel_selector(fig) -> None:
92
92
  def _read_data(
93
93
  optmap_fn: str,
94
94
  detid: str = "all",
95
- histogram_choice: str = "p_det",
95
+ histogram_choice: str = "prob",
96
96
  ) -> tuple[tuple[NDArray], NDArray]:
97
- histogram = histogram_choice if histogram_choice != "p_det_err_rel" else "p_det"
97
+ histogram = histogram_choice if histogram_choice != "prob_unc_rel" else "prob"
98
+ detid = f"channels/{detid}" if detid != all and not detid.startswith("channels/") else detid
99
+
98
100
  optmap_all = lh5.read(f"/{detid}/{histogram}", optmap_fn)
99
101
  optmap_edges = tuple([b.edges for b in optmap_all.binning])
100
102
  optmap_weights = optmap_all.weights.nda.copy()
101
- if histogram_choice == "p_det_err_rel":
102
- optmap_err = lh5.read(f"/{detid}/p_det_err", optmap_fn)
103
+ if histogram_choice == "prob_unc_rel":
104
+ optmap_err = lh5.read(f"/{detid}/prob_unc", optmap_fn)
103
105
  divmask = optmap_weights > 0
104
106
  optmap_weights[divmask] = optmap_err.weights.nda[divmask] / optmap_weights[divmask]
105
107
  optmap_weights[~divmask] = -1
@@ -112,13 +114,13 @@ def _prepare_data(
112
114
  divide_fn: str | None = None,
113
115
  cmap_min: float | Literal["auto"] = 1e-4,
114
116
  cmap_max: float | Literal["auto"] = 1e-2,
115
- histogram_choice: str = "p_det",
117
+ histogram_choice: str = "prob",
116
118
  detid: str = "all",
117
119
  ) -> tuple[tuple[NDArray], NDArray]:
118
120
  optmap_edges, optmap_weights = _read_data(optmap_fn, detid, histogram_choice)
119
121
 
120
122
  if divide_fn is not None:
121
- divide_edges, divide_map = _read_data(divide_fn, detid, histogram_choice)
123
+ _, divide_map = _read_data(divide_fn, detid, histogram_choice)
122
124
  divmask = divide_map > 0
123
125
  optmap_weights[divmask] = optmap_weights[divmask] / divide_map[divmask]
124
126
  optmap_weights[~divmask] = -1
@@ -158,7 +160,7 @@ def view_optmap(
158
160
  start_axis: int = 2,
159
161
  cmap_min: float | Literal["auto"] = 1e-4,
160
162
  cmap_max: float | Literal["auto"] = 1e-2,
161
- histogram_choice: str = "p_det",
163
+ histogram_choice: str = "prob",
162
164
  title: str | None = None,
163
165
  ) -> None:
164
166
  available_dets = list_optical_maps(optmap_fn)
reboost/optmap/optmap.py CHANGED
@@ -8,7 +8,7 @@ import multiprocessing as mp
8
8
  from collections.abc import Mapping
9
9
 
10
10
  import numpy as np
11
- from lgdo import Histogram, lh5
11
+ from lgdo import Histogram, Struct, lh5
12
12
  from numpy.typing import NDArray
13
13
 
14
14
  log = logging.getLogger(__name__)
@@ -66,10 +66,10 @@ class OpticalMap:
66
66
  raise RuntimeError(msg)
67
67
  return h.weights.nda, h.binning
68
68
 
69
- om.h_vertex, bin_nr_gen = read_hist("nr_gen", lh5_file, group=group)
70
- om.h_hits, bin_nr_det = read_hist("nr_det", lh5_file, group=group)
71
- om.h_prob, bin_p_det = read_hist("p_det", lh5_file, group=group)
72
- om.h_prob_uncert, bin_p_det_err = read_hist("p_det_err", lh5_file, group=group)
69
+ om.h_vertex, bin_nr_gen = read_hist("_nr_gen", lh5_file, group=group)
70
+ om.h_hits, bin_nr_det = read_hist("_nr_det", lh5_file, group=group)
71
+ om.h_prob, bin_p_det = read_hist("prob", lh5_file, group=group)
72
+ om.h_prob_uncert, bin_p_det_err = read_hist("prob_unc", lh5_file, group=group)
73
73
 
74
74
  for bins in (bin_nr_det, bin_p_det, bin_p_det_err):
75
75
  if not OpticalMap._edges_eq(bin_nr_gen, bins):
@@ -227,18 +227,17 @@ class OpticalMap:
227
227
 
228
228
  def write_hist(h: NDArray, name: str, fn: str, group: str, wo_mode: str):
229
229
  lh5.write(
230
- Histogram(self._nda(h), self.binning),
231
- name,
230
+ Struct({name: Histogram(self._nda(h), self.binning)}),
231
+ group,
232
232
  fn,
233
- group=group,
234
233
  wo_mode=wo_mode,
235
234
  )
236
235
 
237
236
  # only use the passed wo_mode for the first file.
238
- write_hist(self.h_vertex, "nr_gen", lh5_file, group, wo_mode)
239
- write_hist(self.h_hits, "nr_det", lh5_file, group, "write_safe")
240
- write_hist(self.h_prob, "p_det", lh5_file, group, "write_safe")
241
- write_hist(self.h_prob_uncert, "p_det_err", lh5_file, group, "write_safe")
237
+ write_hist(self.h_vertex, "_nr_gen", lh5_file, group, wo_mode)
238
+ write_hist(self.h_hits, "_nr_det", lh5_file, group, "append_column")
239
+ write_hist(self.h_prob, "prob", lh5_file, group, "append_column")
240
+ write_hist(self.h_prob_uncert, "prob_unc", lh5_file, group, "append_column")
242
241
 
243
242
  def get_settings(self) -> dict:
244
243
  """Get the binning settings that were used to create this optical map instance."""
reboost/spms/pe.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
 
5
5
  import awkward as ak
6
+ import numpy as np
6
7
  from lgdo import VectorOfVectors
7
8
 
8
9
  from ..optmap import convolve
@@ -21,6 +22,80 @@ def load_optmap(map_file: str, spm_det_uid: int) -> convolve.OptmapForConvolve:
21
22
  return convolve.open_optmap_single(map_file, spm_det_uid)
22
23
 
23
24
 
25
+ def _nested_unflatten(data: ak.Array, lengths: ak.Array):
26
+ return ak.unflatten(ak.unflatten(ak.flatten(data), ak.flatten(lengths)), ak.num(lengths))
27
+
28
+
29
+ def corrected_photoelectrons(
30
+ simulated_pe: ak.Array,
31
+ simulated_uids: ak.Array,
32
+ data_pe: ak.Array,
33
+ data_uids: ak.Array,
34
+ *,
35
+ seed: int | None = None,
36
+ ) -> tuple[ak.Array, ak.Array]:
37
+ r"""Add a correction to the observed number of photoelectrons (p.e.) using forced trigger data.
38
+
39
+ For every simulated event a corresponding forced trigger event in data is chosen
40
+ and the resulting number of p.e. for each channel (i) is:
41
+
42
+ .. math::
43
+
44
+ n_i = n_{\text{sim},i} + n_{\text{data},i}
45
+
46
+ .. warning::
47
+ The number of supplied forced trigger events in data should ideally be
48
+ more than that in the simulations. If this is not the case and "allow_data_reuse"
49
+ is True then some data events will be used multiple times. This introduces
50
+ a small amount of correlation between the simulated events, but is probably acceptable
51
+ in most circumstances.
52
+
53
+ Parameters
54
+ ----------
55
+ simulated_pe
56
+ The number of number of detected pe per sipm channel.
57
+ simulated_uids
58
+ The unique identifier (uid) for each sipm hit.
59
+ data_pe
60
+ The collection of forced trigger pe.
61
+ data_uids
62
+ The uids for each forced trigger event.
63
+ seed
64
+ Seed for random number generator
65
+
66
+ Returns
67
+ -------
68
+ a tuple of the corrected pe and sipm uids.
69
+ """
70
+ rand = np.random.default_rng(seed=seed)
71
+ rand_ints = rand.integers(0, len(data_pe), size=len(simulated_pe))
72
+
73
+ selected_data_pe = data_pe[rand_ints]
74
+ selected_data_uids = data_uids[rand_ints]
75
+
76
+ # combine sims with data
77
+ pe_tot = ak.concatenate([simulated_pe, selected_data_pe], axis=1)
78
+ uid_tot = ak.concatenate([simulated_uids, selected_data_uids], axis=1)
79
+
80
+ # sort by uid
81
+ order = ak.argsort(uid_tot)
82
+ pe_tot = pe_tot[order]
83
+ uid_tot = uid_tot[order]
84
+
85
+ # add an extra axis
86
+ n = ak.run_lengths(uid_tot)
87
+
88
+ # add another dimension
89
+ pe_tot = _nested_unflatten(pe_tot, n)
90
+ uid_tot = _nested_unflatten(uid_tot, n)
91
+
92
+ # sum pe and take the first uid (should all be the same)
93
+ corrected_pe = ak.sum(pe_tot, axis=-1)
94
+ uid_tot = ak.fill_none(ak.firsts(uid_tot, axis=-1), np.nan)
95
+
96
+ return corrected_pe, uid_tot
97
+
98
+
24
99
  def detected_photoelectrons(
25
100
  num_scint_ph: ak.Array,
26
101
  particle: ak.Array,
@@ -32,6 +107,7 @@ def detected_photoelectrons(
32
107
  material: str,
33
108
  spm_detector_uid: int,
34
109
  map_scaling: float = 1,
110
+ map_scaling_sigma: float = 0,
35
111
  ) -> VectorOfVectors:
36
112
  """Derive the number of detected photoelectrons (p.e.) from scintillator hits using an optical map.
37
113
 
@@ -58,6 +134,9 @@ def detected_photoelectrons(
58
134
  SiPM detector uid as used in the optical map.
59
135
  map_scaling
60
136
  scale the detection probability in the map for this detector by this factor.
137
+ map_scaling_sigma
138
+ if larger than zero, sample the used scaling factor for each (reshaped) event
139
+ from a normal distribution with this standard deviation.
61
140
  """
62
141
  hits = ak.Array(
63
142
  {
@@ -72,7 +151,7 @@ def detected_photoelectrons(
72
151
 
73
152
  scint_mat_params = convolve._get_scint_params(material)
74
153
  pe = convolve.iterate_stepwise_depositions_pois(
75
- hits, optmap, scint_mat_params, spm_detector_uid, map_scaling
154
+ hits, optmap, scint_mat_params, spm_detector_uid, map_scaling, map_scaling_sigma
76
155
  )
77
156
 
78
157
  return VectorOfVectors(pe, attrs={"units": "ns"})
reboost/utils.py CHANGED
@@ -284,7 +284,7 @@ def get_function_string(expr: str, aliases: dict | None = None) -> tuple[str, di
284
284
  if "." not in func_call:
285
285
  continue
286
286
 
287
- subpackage, func = func_call.rsplit(".", 1)
287
+ subpackage, _func = func_call.rsplit(".", 1)
288
288
  package = subpackage.split(".")[0]
289
289
 
290
290
  # import the subpackage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reboost
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: New LEGEND Monte-Carlo simulation post-processing
5
5
  Author-email: Manuel Huber <info@manuelhu.de>, Toby Dixon <toby.dixon.23@ucl.ac.uk>, Luigi Pertoldi <gipert@pm.me>
6
6
  Maintainer: The LEGEND Collaboration
@@ -700,7 +700,7 @@ Requires-Dist: hdf5plugin
700
700
  Requires-Dist: colorlog
701
701
  Requires-Dist: numpy
702
702
  Requires-Dist: scipy
703
- Requires-Dist: numba
703
+ Requires-Dist: numba>=0.60
704
704
  Requires-Dist: legend-pydataobj>=1.15.1
705
705
  Requires-Dist: legend-pygeom-optics>=0.12.0
706
706
  Requires-Dist: legend-pygeom-tools>=0.0.11
@@ -728,6 +728,8 @@ Dynamic: license-file
728
728
 
729
729
  # reboost
730
730
 
731
+ [![PyPI](https://img.shields.io/pypi/v/reboost?logo=pypi)](https://pypi.org/project/reboost/)
732
+ [![conda-forge](https://img.shields.io/conda/vn/conda-forge/reboost.svg)](https://anaconda.org/conda-forge/reboost)
731
733
  ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/legend-exp/reboost?logo=git)
732
734
  [![GitHub Workflow Status](https://img.shields.io/github/checks-status/legend-exp/reboost/main?label=main%20branch&logo=github)](https://github.com/legend-exp/reboost/actions)
733
735
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
@@ -1,15 +1,15 @@
1
1
  reboost/__init__.py,sha256=VZz9uo7i2jgAx8Zi15SptLZnE_qcnGuNWwqkD3rYHFA,278
2
- reboost/_version.py,sha256=uLbRjFSUZAgfl7V7O8zKV5Db36k7tz87ZIVq3l2SWs0,704
2
+ reboost/_version.py,sha256=Rttl-BDadtcW1QzGnNffCWA_Wc9mUKDMOBPZp--Mnsc,704
3
3
  reboost/build_evt.py,sha256=VXIfK_pfe_Cgym6gI8dESwONZi-v_4fll0Pn09vePQY,3767
4
4
  reboost/build_glm.py,sha256=IerSLQfe51ZO7CQP2kmfPnOIVaDtcfw3byOM02Vaz6o,9472
5
5
  reboost/build_hit.py,sha256=pjEaiPW63Q3MfpjI29uJXx9gtwfiOIgOcADRDrDpRrA,17409
6
6
  reboost/cli.py,sha256=68EzKiWTHJ2u1RILUv7IX9HaVq6nTTM80_W_MUnWRe4,6382
7
- reboost/core.py,sha256=wIIDxaPzEaozZOeKabz4l-29_IyY47RbNy6qS8Bai1Y,15297
7
+ reboost/core.py,sha256=TPxvZgUaHZdxfQSDdX2zIerQXt3Gq-zQaA6AeXZKNvA,15232
8
8
  reboost/iterator.py,sha256=qlEqRv5qOh8eIs-dyVOLYTvH-ZpQDx9fLckpcAdtWjs,6975
9
9
  reboost/log_utils.py,sha256=VqS_9OC5NeNU3jcowVOBB0NJ6ssYvNWnirEY-JVduEA,766
10
10
  reboost/profile.py,sha256=EOTmjmS8Rm_nYgBWNh6Rntl2XDsxdyed7yEdWtsZEeg,2598
11
11
  reboost/units.py,sha256=LUwl6swLQoG09Rt9wcDdu6DTrwDsy-C751BNGzX4sz8,3651
12
- reboost/utils.py,sha256=SfrqWdQZPNXw0x0DVucxHC5OwFXewM43tO-PNnIwjKI,14562
12
+ reboost/utils.py,sha256=-4315U6m1M-rwoaI_inI1wIo_l20kvoGmC84D_QOhkE,14563
13
13
  reboost/daq/__init__.py,sha256=rNPhxx1Yawt3tENYhmOYSum9_TdV57ZU5kjxlWFAGuo,107
14
14
  reboost/daq/core.py,sha256=Rs6Q-17fzEod2iX_2WqEmnqKnNRFoWTYURl3wYhFihU,9915
15
15
  reboost/daq/utils.py,sha256=KcH6zvlInmD2YiF6V--DSYBTYudJw3G-hp2JGOcES2o,1042
@@ -21,22 +21,22 @@ reboost/math/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  reboost/math/functions.py,sha256=OymiYTcA0NXxxm-MBDw5kqyNwHoLCmuv4J48AwnSrbU,5633
22
22
  reboost/math/stats.py,sha256=Rq4Wdzv-3aoSK7EsPZCuOEHfnOz3w0moIzCEHbC07xw,3173
23
23
  reboost/optmap/__init__.py,sha256=imvuyld-GLw8qdwqW-lXCg2feptcTyQo3wIzPvDHwmY,93
24
- reboost/optmap/cli.py,sha256=CoH0P_WLEek1T9zxmlsM2TuxxzqobP-W-ym72uhxrO4,10252
25
- reboost/optmap/convolve.py,sha256=9fM5zVKkwp03a3kqBwWppe1UOGdVm1Q6yF4P0v2I8VQ,22936
26
- reboost/optmap/create.py,sha256=qwUvYiUotmLCrB6RsfwxRMGI4EYRn7Q4XlwppUTVZTE,18166
27
- reboost/optmap/evt.py,sha256=UrjjNNeS7Uie4Ah9y_f5PyroFutLGo5aOFcwReOEy7o,5556
28
- reboost/optmap/mapview.py,sha256=73kpe0_SKDj9bIhEx1ybX1sBP8TyvufiLfps84A_ijA,6798
24
+ reboost/optmap/cli.py,sha256=KJFyO4Sd2oYlRC65Kvrdq0BN_Qygp6o7LgO36D5O6_s,8887
25
+ reboost/optmap/convolve.py,sha256=UVc-KjiYvi8LoAlykL__5qDd5T8teT89aDjiE7WYjE4,12464
26
+ reboost/optmap/create.py,sha256=R9W8Wyl8cgXIYESenVpCRuN_MoHhLfaEv4a44cf3AxU,14479
27
+ reboost/optmap/evt.py,sha256=p5ngsCuvOxIZDDQNL9efcLn8Q4CfGL7G726BRrCpE6Y,5637
28
+ reboost/optmap/mapview.py,sha256=_zGiONqCWc4kCp1yHvNFRkh7svvud4ZLqaSHkNieo_M,6878
29
29
  reboost/optmap/numba_pdg.py,sha256=y8cXR5PWE2Liprp4ou7vl9do76dl84vXU52ZJD9_I7A,731
30
- reboost/optmap/optmap.py,sha256=f8Y0zLmXZyOz4-GDUPViIY_vf_shY3ZQG9lSD31J8gc,12820
30
+ reboost/optmap/optmap.py,sha256=3clc1RA8jA4YJte83w085MY8zLpG-G7DBkpZ2UeKPpM,12825
31
31
  reboost/shape/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  reboost/shape/cluster.py,sha256=nwR1Dnf00SDICGPqpXeM1Q7_DwTtO9uP3wmuML45c3g,8195
33
33
  reboost/shape/group.py,sha256=gOCYgir2gZqmW1JXtbNRPlQqP0gmUcbe7RVb9CbY1pU,5540
34
34
  reboost/shape/reduction.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  reboost/spms/__init__.py,sha256=ffi0rH-ZFGmpURbFt-HY1ZAHCdady0mLXNsblRNh-JY,207
36
- reboost/spms/pe.py,sha256=9KWmc4fGW5_bx4imsguLm-xp6rokt89iZcM-WD62Tj4,3090
37
- reboost-0.7.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
38
- reboost-0.7.0.dist-info/METADATA,sha256=-CFo2D7zHSJXcSsfMQtXIv8T4nx3qoR4OMD6EbaKLU0,44226
39
- reboost-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
- reboost-0.7.0.dist-info/entry_points.txt,sha256=DxhD6BidSWNot9BrejHJjQ7RRLmrMaBIl52T75oWTwM,93
41
- reboost-0.7.0.dist-info/top_level.txt,sha256=q-IBsDepaY_AbzbRmQoW8EZrITXRVawVnNrB-_zyXZs,8
42
- reboost-0.7.0.dist-info/RECORD,,
36
+ reboost/spms/pe.py,sha256=Q_GzN5HEp2BmgB4fni5mfc5ZOXh4mlJepDwZd1EzFdc,5696
37
+ reboost-0.8.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
38
+ reboost-0.8.0.dist-info/METADATA,sha256=t8uYwYlKBZPoO5Rbrv4RVr5AQj4D_rglFCQ-HFrYe1I,44442
39
+ reboost-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
+ reboost-0.8.0.dist-info/entry_points.txt,sha256=DxhD6BidSWNot9BrejHJjQ7RRLmrMaBIl52T75oWTwM,93
41
+ reboost-0.8.0.dist-info/top_level.txt,sha256=q-IBsDepaY_AbzbRmQoW8EZrITXRVawVnNrB-_zyXZs,8
42
+ reboost-0.8.0.dist-info/RECORD,,