reboost 0.6.0__py3-none-any.whl → 0.6.2__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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.6.0'
21
- __version_tuple__ = version_tuple = (0, 6, 0)
20
+ __version__ = version = '0.6.2'
21
+ __version_tuple__ = version_tuple = (0, 6, 2)
reboost/build_hit.py CHANGED
@@ -235,6 +235,7 @@ def build_hit(
235
235
  files = utils.get_file_dict(stp_files=stp_files, glm_files=glm_files, hit_files=hit_files)
236
236
 
237
237
  output_tables = {}
238
+ output_tables_names = set()
238
239
 
239
240
  # iterate over files
240
241
  for file_idx, (stp_file, glm_file) in enumerate(zip(files.stp, files.glm)):
@@ -363,16 +364,12 @@ def build_hit(
363
364
  # now write
364
365
  if files.hit[file_idx] is not None:
365
366
  # get modes to write with
366
- new_hit_file = (file_idx == 0) or (
367
- files.hit[file_idx] != files.hit[file_idx - 1]
368
- )
369
-
370
367
  wo_mode = utils.get_wo_mode(
371
368
  group=group_idx,
372
369
  out_det=out_det_idx,
373
370
  in_det=in_det_idx,
374
371
  chunk=chunk_idx,
375
- new_hit_file=new_hit_file,
372
+ new_hit_file=utils.is_new_hit_file(files, file_idx),
376
373
  overwrite=overwrite,
377
374
  )
378
375
  # write the file
@@ -390,6 +387,8 @@ def build_hit(
390
387
  hit_table, output_tables[out_detector]
391
388
  )
392
389
 
390
+ output_tables_names.add(out_detector)
391
+
393
392
  # forward some data, if requested
394
393
  # possible improvement: iterate over data if it's a lot
395
394
  if "forward" in config and files.hit[file_idx] is not None:
@@ -400,12 +399,16 @@ def build_hit(
400
399
 
401
400
  for obj in obj_list:
402
401
  try:
402
+ wo_mode = utils.get_wo_mode_forwarded(
403
+ output_tables_names, utils.is_new_hit_file(files, file_idx), overwrite
404
+ )
403
405
  lh5.write(
404
406
  lh5.read(obj, stp_file),
405
407
  obj,
406
408
  files.hit[file_idx],
407
- wo_mode="write_safe" if file_idx == 0 else "append",
409
+ wo_mode=wo_mode,
408
410
  )
411
+ output_tables_names.add(obj)
409
412
  except LH5EncodeError as e:
410
413
  msg = f"cannot forward object {obj} as it has been already processed by reboost"
411
414
  raise RuntimeError(msg) from e
reboost/optmap/cli.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
- import json
5
4
  import logging
6
- from pathlib import Path
5
+
6
+ import dbetto
7
7
 
8
8
  from ..log_utils import setup_log
9
9
  from ..utils import _check_input_file, _check_output_file
@@ -35,14 +35,18 @@ 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 hit tier
39
- evt_parser = subparsers.add_parser("evt", help="build evt file from remage hit file")
40
- evt_parser.add_argument(
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(
41
46
  "--detectors",
42
- help="file that contains a list of detector ids that are part of the input file",
43
- required=True,
47
+ help="file with detector ids of all optical channels.",
44
48
  )
45
- evt_parser.add_argument("input", help="input hit LH5 file", metavar="INPUT_HIT")
49
+ evt_parser.add_argument("input", help="input stp LH5 file", metavar="INPUT_STP")
46
50
  evt_parser.add_argument("output", help="output evt LH5 file", metavar="OUTPUT_EVT")
47
51
 
48
52
  # STEP 2a: build map file from evt tier
@@ -55,7 +59,20 @@ def optical_cli() -> None:
55
59
  )
56
60
  map_parser.add_argument(
57
61
  "--detectors",
58
- help="file that contains a list of detector ids that will be produced as additional output maps.",
62
+ help=(
63
+ "file that contains a list of detector ids that will be produced as additional output maps."
64
+ + "By default, all channels will be included."
65
+ ),
66
+ )
67
+ map_parser_det_group = map_parser.add_mutually_exclusive_group(required=True)
68
+ map_parser_det_group.add_argument(
69
+ "--geom",
70
+ help="GDML geometry file",
71
+ )
72
+ map_parser_det_group.add_argument(
73
+ "--evt",
74
+ action="store_true",
75
+ help="the input file is already an optmap-evt file.",
59
76
  )
60
77
  map_parser.add_argument(
61
78
  "--n-procs",
@@ -69,7 +86,9 @@ def optical_cli() -> None:
69
86
  action="store_true",
70
87
  help="""Check map statistics after creation. default: %(default)s""",
71
88
  )
72
- map_parser.add_argument("input", help="input evt LH5 file", metavar="INPUT_EVT", nargs="+")
89
+ map_parser.add_argument(
90
+ "input", help="input stp or optmap-evt LH5 file", metavar="INPUT_EVT", nargs="+"
91
+ )
73
92
  map_parser.add_argument("output", help="output map LH5 file", metavar="OUTPUT_MAP")
74
93
 
75
94
  # STEP 2b: view maps
@@ -171,7 +190,7 @@ def optical_cli() -> None:
171
190
  convolve_parser.add_argument(
172
191
  "--dist-mode",
173
192
  action="store",
174
- default="multinomial+no-fano",
193
+ default="poisson+no-fano",
175
194
  )
176
195
  convolve_parser.add_argument("--output", help="output hit LH5 file", metavar="OUTPUT_HIT")
177
196
 
@@ -188,15 +207,18 @@ def optical_cli() -> None:
188
207
 
189
208
  # STEP 1: build evt file from hit tier
190
209
  if args.command == "evt":
191
- from .evt import build_optmap_evt
210
+ from .evt import build_optmap_evt, get_optical_detectors_from_geom
192
211
 
193
- _check_input_file(parser, args.detectors)
194
212
  _check_input_file(parser, args.input)
195
213
  _check_output_file(parser, args.output)
196
214
 
197
- # load detector ids from a JSON array
198
- with Path.open(Path(args.detectors)) as detectors_f:
199
- detectors = json.load(detectors_f)
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)
200
222
 
201
223
  build_optmap_evt(args.input, args.output, detectors, args.bufsize)
202
224
 
@@ -209,23 +231,23 @@ def optical_cli() -> None:
209
231
 
210
232
  # load settings for binning from config file.
211
233
  _check_input_file(parser, args.input, "settings")
212
- with Path.open(Path(args.settings)) as settings_f:
213
- settings = json.load(settings_f)
234
+ settings = dbetto.utils.load_dict(args.settings)
214
235
 
215
- chfilter = ()
236
+ chfilter = "*"
216
237
  if args.detectors is not None:
217
- # load detector ids from a JSON array
218
- with Path.open(Path(args.detectors)) as detectors_f:
219
- chfilter = json.load(detectors_f)
238
+ # load detector ids from a JSON/YAML array
239
+ chfilter = dbetto.utils.load_dict(args.detectors)
220
240
 
221
241
  create_optical_maps(
222
242
  args.input,
223
243
  settings,
224
244
  args.bufsize,
245
+ is_stp_file=(not args.evt),
225
246
  chfilter=chfilter,
226
247
  output_lh5_fn=args.output,
227
248
  check_after_create=args.check,
228
249
  n_procs=args.n_procs,
250
+ geom_fn=args.geom,
229
251
  )
230
252
 
231
253
  # STEP 2b: view maps
@@ -251,8 +273,7 @@ def optical_cli() -> None:
251
273
 
252
274
  # load settings for binning from config file.
253
275
  _check_input_file(parser, args.input, "settings")
254
- with Path.open(Path(args.settings)) as settings_f:
255
- settings = json.load(settings_f)
276
+ settings = dbetto.utils.load_dict(args.settings)
256
277
 
257
278
  _check_input_file(parser, args.input)
258
279
  _check_output_file(parser, args.output)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import re
4
5
 
5
6
  import legendoptics.scintillate as sc
6
7
  import numba
@@ -24,8 +25,8 @@ OPTMAP_SUM_CH = -2
24
25
 
25
26
  def open_optmap(optmap_fn: str):
26
27
  maps = lh5.ls(optmap_fn)
27
- # TODO: rewrite logic to only accept _<number> instead of a blacklist
28
- det_ntuples = [m for m in maps if m not in ("all", "_hitcounts", "_hitcounts_exp", "all_orig")]
28
+ # only accept _<number> (/all is read separately)
29
+ det_ntuples = [m for m in maps if re.match(r"_\d+$", m)]
29
30
  detids = np.array([int(m.lstrip("_")) for m in det_ntuples])
30
31
  detidx = np.arange(0, detids.shape[0])
31
32
 
@@ -53,17 +54,25 @@ def open_optmap(optmap_fn: str):
53
54
 
54
55
  # give this check some numerical slack.
55
56
  if np.any(
56
- ow[OPTMAP_SUM_CH][ow[OPTMAP_ANY_CH] >= 0] - ow[OPTMAP_ANY_CH][ow[OPTMAP_ANY_CH] >= 0]
57
+ np.abs(
58
+ ow[OPTMAP_SUM_CH][ow[OPTMAP_ANY_CH] >= 0] - ow[OPTMAP_ANY_CH][ow[OPTMAP_ANY_CH] >= 0]
59
+ )
57
60
  < -1e-15
58
61
  ):
59
62
  msg = "optical map does not fulfill relation sum(p_i) >= p_any"
60
63
  raise ValueError(msg)
61
64
 
62
- # get the exponent from the optical map file
63
- optmap_multi_det_exp = lh5.read("/_hitcounts_exp", optmap_fn).value
64
- assert isinstance(optmap_multi_det_exp, float)
65
+ try:
66
+ # check the exponent from the optical map file
67
+ optmap_multi_det_exp = lh5.read("/_hitcounts_exp", optmap_fn).value
68
+ assert isinstance(optmap_multi_det_exp, float)
69
+ if np.isfinite(optmap_multi_det_exp):
70
+ msg = f"found finite _hitcounts_exp {optmap_multi_det_exp} which is not supported any more"
71
+ raise RuntimeError(msg)
72
+ except KeyError: # the _hitcounts_exp might not be always present.
73
+ pass
65
74
 
66
- return detids, detidx, optmap_edges, ow, optmap_multi_det_exp
75
+ return detids, detidx, optmap_edges, ow
67
76
 
68
77
 
69
78
  def iterate_stepwise_depositions(
@@ -71,7 +80,7 @@ def iterate_stepwise_depositions(
71
80
  optmap_for_convolve,
72
81
  scint_mat_params: sc.ComputedScintParams,
73
82
  rng: np.random.Generator = None,
74
- dist: str = "multinomial",
83
+ dist: str = "poisson",
75
84
  mode: str = "no-fano",
76
85
  ):
77
86
  # those np functions are not supported by numba, but needed for efficient array access below.
@@ -144,7 +153,6 @@ def _iterate_stepwise_depositions(
144
153
  detidx,
145
154
  optmap_edges,
146
155
  optmap_weights,
147
- optmap_multi_det_exp,
148
156
  scint_mat_params: sc.ComputedScintParams,
149
157
  dist: str,
150
158
  mode: str,
@@ -223,8 +231,6 @@ def _iterate_stepwise_depositions(
223
231
  # we detect this energy deposition; we should at least get one photon out here!
224
232
 
225
233
  detsel_size = 1
226
- if np.isfinite(optmap_multi_det_exp):
227
- detsel_size = rng.geometric(1 - np.exp(-optmap_multi_det_exp))
228
234
 
229
235
  px_sum = optmap_weights[OPTMAP_SUM_CH, cur_bins[0], cur_bins[1], cur_bins[2]]
230
236
  assert px_sum >= 0.0 # should not be negative.
@@ -238,7 +244,7 @@ def _iterate_stepwise_depositions(
238
244
  detp[d] = 0.0
239
245
  det_no_stats += had_det_no_stats
240
246
 
241
- # should be equivalent to rng.choice(detidx, size=(detsel_size, p=detp)
247
+ # should be equivalent to rng.choice(detidx, size=detsel_size, p=detp)
242
248
  detsel = detidx[
243
249
  np.searchsorted(np.cumsum(detp), rng.random(size=(detsel_size,)), side="right")
244
250
  ]
@@ -339,7 +345,7 @@ def convolve(
339
345
  material: str,
340
346
  output_file: str | None = None,
341
347
  buffer_len: int = int(1e6),
342
- dist_mode: str = "multinomial+no-fano",
348
+ dist_mode: str = "poisson+no-fano",
343
349
  ):
344
350
  if material not in ["lar", "pen"]:
345
351
  msg = f"unknown material {material} for scintillation"
@@ -356,13 +362,18 @@ def convolve(
356
362
  (1 * pint.get_application_registry().ns), # dummy!
357
363
  )
358
364
 
359
- log.info("opening map %s", map_file)
360
- optmap_for_convolve = open_optmap(map_file)
361
-
362
365
  # special handling of distributions and flags.
363
366
  dist, mode = dist_mode.split("+")
364
- assert dist in ("multinomial", "poisson")
365
- assert mode in ("", "no-fano")
367
+ if (
368
+ dist not in ("multinomial", "poisson")
369
+ or mode not in ("", "no-fano")
370
+ or (dist == "poisson" and mode != "no-fano")
371
+ ):
372
+ msg = f"unsupported statistical distribution {dist_mode} for scintillation emission"
373
+ raise ValueError(msg)
374
+
375
+ log.info("opening map %s", map_file)
376
+ optmap_for_convolve = open_optmap(map_file)
366
377
 
367
378
  log.info("opening energy deposition hit output %s", edep_file)
368
379
  it = LH5Iterator(edep_file, edep_path, buffer_len=buffer_len)
reboost/optmap/create.py CHANGED
@@ -14,7 +14,12 @@ from numba import njit
14
14
  from numpy.typing import NDArray
15
15
 
16
16
  from ..log_utils import setup_log
17
- from .evt import EVT_TABLE_NAME, read_optmap_evt
17
+ from .evt import (
18
+ EVT_TABLE_NAME,
19
+ generate_optmap_evt,
20
+ get_optical_detectors_from_geom,
21
+ read_optmap_evt,
22
+ )
18
23
  from .optmap import OpticalMap
19
24
 
20
25
  log = logging.getLogger(__name__)
@@ -109,12 +114,13 @@ def _create_optical_maps_process_init(optmaps, log_level) -> None:
109
114
 
110
115
 
111
116
  def _create_optical_maps_process(
112
- optmap_events_fn, buffer_len, all_det_ids, ch_idx_to_map_idx
117
+ optmap_events_fn, buffer_len, is_stp_file, all_det_ids, ch_idx_to_map_idx
113
118
  ) -> None:
114
119
  log.info("started worker task for %s", optmap_events_fn)
115
120
  x = _create_optical_maps_chunk(
116
121
  optmap_events_fn,
117
122
  buffer_len,
123
+ is_stp_file,
118
124
  all_det_ids,
119
125
  _shared_optmaps,
120
126
  ch_idx_to_map_idx,
@@ -124,9 +130,12 @@ def _create_optical_maps_process(
124
130
 
125
131
 
126
132
  def _create_optical_maps_chunk(
127
- optmap_events_fn, buffer_len, all_det_ids, optmaps, ch_idx_to_map_idx
133
+ optmap_events_fn, buffer_len, is_stp_file, all_det_ids, optmaps, ch_idx_to_map_idx
128
134
  ) -> None:
129
- optmap_events_it = read_optmap_evt(optmap_events_fn, buffer_len)
135
+ if not is_stp_file:
136
+ optmap_events_it = read_optmap_evt(optmap_events_fn, buffer_len)
137
+ else:
138
+ optmap_events_it = generate_optmap_evt(optmap_events_fn, all_det_ids, buffer_len)
130
139
 
131
140
  hits_per_primary = np.zeros(10, dtype=np.int64)
132
141
  hits_per_primary_len = 0
@@ -156,19 +165,24 @@ def create_optical_maps(
156
165
  optmap_events_fn: list[str],
157
166
  settings,
158
167
  buffer_len: int = int(5e6),
168
+ is_stp_file: bool = True,
159
169
  chfilter: tuple[str | int] | Literal["*"] = (),
160
170
  output_lh5_fn: str | None = None,
161
171
  after_save: Callable[[int, str, OpticalMap]] | None = None,
162
172
  check_after_create: bool = False,
163
173
  n_procs: int | None = 1,
174
+ geom_fn: str | None = None,
164
175
  ) -> None:
165
176
  """Create optical maps.
166
177
 
167
178
  Parameters
168
179
  ----------
169
180
  optmap_events_fn
170
- list of filenames to lh5 files with a table ``/optmap_evt`` with columns ``{x,y,z}loc``
171
- and one column (with numeric header) for each SiPM channel.
181
+ list of filenames to lh5 files, that can either be stp files from remage or "optmap-evt"
182
+ files with a table ``/optmap_evt`` with columns ``{x,y,z}loc`` and one column (with numeric
183
+ header) for each SiPM channel.
184
+ is_stp_file
185
+ if true, do convert a remage output file (stp file) on-the-fly to an optmap-evt file.
172
186
  chfilter
173
187
  tuple of detector ids that will be included in the resulting optmap. Those have to match
174
188
  the column names in ``optmap_events_fn``.
@@ -181,9 +195,13 @@ def create_optical_maps(
181
195
 
182
196
  use_shmem = n_procs is None or n_procs > 1
183
197
 
184
- optmap_evt_columns = list(
185
- lh5.read(EVT_TABLE_NAME, optmap_events_fn[0], start_row=0, n_rows=1).keys()
186
- ) # peek into the (first) file to find column names.
198
+ if not is_stp_file:
199
+ optmap_evt_columns = list(
200
+ lh5.read(EVT_TABLE_NAME, optmap_events_fn[0], start_row=0, n_rows=1).keys()
201
+ ) # peek into the (first) file to find column names.
202
+ else:
203
+ optmap_evt_columns = [str(i) for i in get_optical_detectors_from_geom(geom_fn)]
204
+
187
205
  all_det_ids, optmaps, optmap_det_ids = _optmaps_for_channels(
188
206
  optmap_evt_columns, settings, chfilter=chfilter, use_shmem=use_shmem
189
207
  )
@@ -202,7 +220,9 @@ def create_optical_maps(
202
220
  if not use_shmem:
203
221
  for fn in optmap_events_fn:
204
222
  q.append(
205
- _create_optical_maps_chunk(fn, buffer_len, all_det_ids, optmaps, ch_idx_to_map_idx)
223
+ _create_optical_maps_chunk(
224
+ fn, buffer_len, is_stp_file, all_det_ids, optmaps, ch_idx_to_map_idx
225
+ )
206
226
  )
207
227
  else:
208
228
  ctx = mp.get_context("forkserver")
@@ -222,7 +242,7 @@ def create_optical_maps(
222
242
  for fn in optmap_events_fn:
223
243
  r = pool.apply_async(
224
244
  _create_optical_maps_process,
225
- args=(fn, buffer_len, all_det_ids, ch_idx_to_map_idx),
245
+ args=(fn, buffer_len, is_stp_file, all_det_ids, ch_idx_to_map_idx),
226
246
  )
227
247
  pool_results.append((r, fn))
228
248
 
@@ -401,6 +421,9 @@ def merge_optical_maps(
401
421
  hits_per_primary = np.zeros(10, dtype=np.int64)
402
422
  hits_per_primary_len = 0
403
423
  for optmap_fn in map_l5_files:
424
+ if "_hitcounts" not in lh5.ls(optmap_fn):
425
+ log.warning("skipping _hitcounts calculations, missing in file %s", optmap_fn)
426
+ return
404
427
  hitcounts = lh5.read("/_hitcounts", optmap_fn)
405
428
  assert isinstance(hitcounts, Array)
406
429
  hits_per_primary[0 : len(hitcounts)] += hitcounts
@@ -417,13 +440,18 @@ def merge_optical_maps(
417
440
  def check_optical_map(map_l5_file: str):
418
441
  """Run a health check on the map file.
419
442
 
420
- This checks for consistency, and output details on map statistics.
443
+ This checks for consistency, and outputs details on map statistics.
421
444
  """
422
- if lh5.read("_hitcounts_exp", lh5_file=map_l5_file).value != np.inf:
423
- log.error("unexpected hitcount exp not equal to positive infinity")
445
+ if "_hitcounts_exp" not in lh5.ls(map_l5_file):
446
+ log.info("no _hitcounts_exp found")
447
+ elif lh5.read("_hitcounts_exp", lh5_file=map_l5_file).value != np.inf:
448
+ log.error("unexpected _hitcounts_exp not equal to positive infinity")
424
449
  return
425
- if lh5.read("_hitcounts", lh5_file=map_l5_file).nda.shape != (2,):
426
- log.error("unexpected hitcount shape")
450
+
451
+ if "_hitcounts" not in lh5.ls(map_l5_file):
452
+ log.info("no _hitcounts found")
453
+ elif lh5.read("_hitcounts", lh5_file=map_l5_file).nda.shape != (2,):
454
+ log.error("unexpected _hitcounts shape")
427
455
  return
428
456
 
429
457
  all_binning = None
@@ -477,4 +505,5 @@ def rebin_optical_maps(map_l5_file: str, output_lh5_file: str, factor: int):
477
505
 
478
506
  # just copy hitcounts exponent.
479
507
  for dset in ("_hitcounts_exp", "_hitcounts"):
480
- lh5.write(lh5.read(dset, lh5_file=map_l5_file), dset, lh5_file=output_lh5_file)
508
+ if dset in lh5.ls(map_l5_file):
509
+ lh5.write(lh5.read(dset, lh5_file=map_l5_file), dset, lh5_file=output_lh5_file)
reboost/optmap/evt.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from collections.abc import Iterable
4
+ from collections.abc import Generator, Iterable
5
5
  from pathlib import Path
6
6
 
7
7
  import numpy as np
@@ -14,20 +14,18 @@ log = logging.getLogger(__name__)
14
14
  EVT_TABLE_NAME = "optmap_evt"
15
15
 
16
16
 
17
- def build_optmap_evt(
18
- lh5_in_file: str, lh5_out_file: str, detectors: Iterable[str | int], buffer_len: int = int(5e6)
19
- ) -> None:
17
+ def generate_optmap_evt(
18
+ lh5_in_file: str, detectors: Iterable[str | int], buffer_len: int = int(5e6)
19
+ ) -> Generator[Table, None, None]:
20
20
  """Create a faster map for lookup of the hits in each detector, for each primary event."""
21
21
  log.info("reading file %s", lh5_in_file)
22
22
 
23
- lh5_out_file = Path(lh5_out_file)
24
- lh5_out_file_tmp = lh5_out_file.with_stem(".evt-tmp." + lh5_out_file.stem)
25
- if lh5_out_file_tmp.exists():
26
- msg = f"temporary output file {lh5_out_file_tmp} already exists"
27
- raise RuntimeError(msg)
28
23
  vert_it = LH5Iterator(lh5_in_file, "vtx", buffer_len=buffer_len)
29
24
  opti_it = LH5Iterator(lh5_in_file, "stp/optical", buffer_len=buffer_len)
30
25
 
26
+ if len(detectors) == 0:
27
+ msg = "detector array cannot be empty for optmap-evt building"
28
+ raise ValueError(msg)
31
29
  detectors = [str(d) for d in detectors]
32
30
  for d in detectors:
33
31
  if not d.isnumeric():
@@ -35,11 +33,11 @@ def build_optmap_evt(
35
33
 
36
34
  vert_df = None
37
35
  vert_df_bounds = None
38
- vert_it_count = 0
39
36
  hits_expected = 0
37
+ had_last_chunk = False
40
38
 
41
- def _store_vert_df():
42
- nonlocal vert_df
39
+ def _store_vert_df(last_chunk: bool) -> Generator[Table, None, None]:
40
+ nonlocal vert_df, had_last_chunk
43
41
  if vert_df is None:
44
42
  return
45
43
 
@@ -49,8 +47,8 @@ def build_optmap_evt(
49
47
  hits_sum += np.sum(vert_df[d])
50
48
  assert hits_sum == hits_expected
51
49
 
52
- log.info("store evt file %s (%d)", lh5_out_file_tmp, vert_it_count - 1)
53
- lh5.write(Table(vert_df), name=EVT_TABLE_NAME, lh5_file=lh5_out_file_tmp, wo_mode="append")
50
+ yield Table(vert_df)
51
+ had_last_chunk = last_chunk
54
52
  vert_df = None
55
53
 
56
54
  # helper function for "windowed join". while iterating the optical hits, we have to
@@ -59,8 +57,8 @@ def build_optmap_evt(
59
57
  # This function follows the assumption, that the output event ids are at least "somewhat"
60
58
  # monotonic, i.e. later chunks do not contain lower evtids than the previous chunk(s).
61
59
  # Going back is not implemented.
62
- def _ensure_vert_df(vert_it: LH5Iterator, evtid: int) -> None:
63
- nonlocal vert_df, vert_df_bounds, vert_it_count, hits_expected
60
+ def _ensure_vert_df(vert_it: LH5Iterator, evtid: int) -> Generator[Table, None, None]:
61
+ nonlocal vert_df, vert_df_bounds, hits_expected
64
62
 
65
63
  # skipping multiple chunks is possible in sparsely populated simulations.
66
64
  while vert_df_bounds is None or evtid > vert_df_bounds[1] or evtid < vert_df_bounds[0]:
@@ -74,9 +72,8 @@ def build_optmap_evt(
74
72
  # here, evtid > vert_df_bounds[1] (or vert_df_bounds is still None). We need to fetch
75
73
  # the next event table chunk.
76
74
 
77
- vert_it_count += 1
78
75
  # we might have filled a dataframe, save it to disk.
79
- _store_vert_df()
76
+ yield from _store_vert_df(last_chunk=False)
80
77
 
81
78
  # read the next vertex chunk into memory.
82
79
  vert_df = next(vert_it).view_as("pd")
@@ -98,11 +95,28 @@ def build_optmap_evt(
98
95
  log.info("build evt table (%d)", opti_it_count)
99
96
 
100
97
  for t in opti_df[["evtid", "det_uid"]].itertuples(name=None, index=False):
101
- _ensure_vert_df(vert_it, t[0])
98
+ yield from _ensure_vert_df(vert_it, t[0])
102
99
  vert_df.loc[t[0], str(t[1])] += 1
103
100
  hits_expected += 1
104
101
 
105
- _store_vert_df() # store the last chunk.
102
+ yield from _store_vert_df(last_chunk=True) # store the last chunk.
103
+
104
+ assert had_last_chunk, "did not reach last chunk in optmap-evt building"
105
+
106
+
107
+ def build_optmap_evt(
108
+ lh5_in_file: str, lh5_out_file: str, detectors: Iterable[str | int], buffer_len: int = int(5e6)
109
+ ) -> None:
110
+ """Create a faster map for lookup of the hits in each detector, for each primary event."""
111
+ lh5_out_file = Path(lh5_out_file)
112
+ lh5_out_file_tmp = lh5_out_file.with_stem(".evt-tmp." + lh5_out_file.stem)
113
+ if lh5_out_file_tmp.exists():
114
+ msg = f"temporary output file {lh5_out_file_tmp} already exists"
115
+ raise RuntimeError(msg)
116
+
117
+ for vert_it_count, chunk in enumerate(generate_optmap_evt(lh5_in_file, detectors, buffer_len)):
118
+ log.info("store evt file %s (%d)", lh5_out_file_tmp, vert_it_count - 1)
119
+ lh5.write(Table(chunk), name=EVT_TABLE_NAME, lh5_file=lh5_out_file_tmp, wo_mode="append")
106
120
 
107
121
  # after finishing the output file, rename to the actual output file name.
108
122
  if lh5_out_file.exists():
@@ -111,5 +125,14 @@ def build_optmap_evt(
111
125
  lh5_out_file_tmp.rename(lh5_out_file)
112
126
 
113
127
 
128
+ def get_optical_detectors_from_geom(geom_fn) -> list[int]:
129
+ import pyg4ometry
130
+ import pygeomtools
131
+
132
+ geom_registry = pyg4ometry.gdml.Reader(geom_fn).getRegistry()
133
+ detectors = pygeomtools.get_all_sensvols(geom_registry)
134
+ return [d.uid for d in detectors.values() if d.detector_type == "optical"]
135
+
136
+
114
137
  def read_optmap_evt(lh5_file: str, buffer_len: int = int(5e6)) -> LH5Iterator:
115
138
  return LH5Iterator(lh5_file, EVT_TABLE_NAME, buffer_len=buffer_len)
reboost/utils.py CHANGED
@@ -51,7 +51,9 @@ def get_wo_mode(
51
51
  chunk
52
52
  the chunk index
53
53
  new_hit_file
54
- a flag of whether we are writing a new hit file
54
+ a flag of whether we are writing a new hit file. This does not indicate whether
55
+ the file already exists on disk, but whether the file name is different from the
56
+ last written chunk for this detector.
55
57
  overwrite
56
58
  a flag of whether to overwrite the old file.
57
59
 
@@ -75,6 +77,49 @@ def get_wo_mode(
75
77
  return "append"
76
78
 
77
79
 
80
+ def get_wo_mode_forwarded(
81
+ written_tables: set[str], new_hit_file: bool, overwrite: bool = False
82
+ ) -> str:
83
+ """Get the mode for lh5 file writing for forwarded tables tahat will be copied without chunking.
84
+
85
+ If we are writing a new output file and no other tables had been written yet, then
86
+ the mode "overwrite_file" is used if the overwrite flag is set, otherwise the mode
87
+ "write_safe" is used.
88
+
89
+ Otherwise "append" is used.
90
+
91
+ Parameters
92
+ ----------
93
+ written_tables
94
+ a set of already written table names, also including other table names of
95
+ non-forwarded (i.e. processed) tables.
96
+ new_hit_file
97
+ a flag of whether we are writing a new hit file. This does not indicate whether
98
+ the file already exists on disk, but whether the file name is different from the
99
+ last written chunk for this forwarded table.
100
+ overwrite
101
+ a flag of whether to overwrite the old file.
102
+
103
+ Returns
104
+ -------
105
+ the mode for IO
106
+ """
107
+ if not new_hit_file:
108
+ return "append"
109
+ if overwrite and len(written_tables) == 0:
110
+ return "overwrite_file"
111
+ return "write_safe"
112
+
113
+
114
+ def is_new_hit_file(files: AttrsDict, file_idx: int) -> bool:
115
+ """Return whether the hit file with the given index is a "new" hit file.
116
+
117
+ A new file is either the first file written, or when the previous file index has a
118
+ different file name.
119
+ """
120
+ return (file_idx == 0) or (files.hit[file_idx] != files.hit[file_idx - 1])
121
+
122
+
78
123
  def get_file_dict(
79
124
  stp_files: list[str] | str,
80
125
  glm_files: list[str] | str | None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reboost
3
- Version: 0.6.0
3
+ Version: 0.6.2
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
@@ -703,6 +703,7 @@ Requires-Dist: scipy
703
703
  Requires-Dist: numba
704
704
  Requires-Dist: legend-pydataobj>=1.14
705
705
  Requires-Dist: legend-pygeom-optics>=0.9.2
706
+ Requires-Dist: legend-pygeom-tools>=0.0.11
706
707
  Requires-Dist: hist
707
708
  Requires-Dist: dbetto
708
709
  Requires-Dist: particle
@@ -722,8 +723,6 @@ Requires-Dist: pre-commit; extra == "test"
722
723
  Requires-Dist: pytest>=6.0; extra == "test"
723
724
  Requires-Dist: pytest-cov; extra == "test"
724
725
  Requires-Dist: legend-pygeom-hpges; extra == "test"
725
- Requires-Dist: legend-pygeom-tools; extra == "test"
726
- Requires-Dist: pyg4ometry; extra == "test"
727
726
  Requires-Dist: pylegendtestdata>=0.6; extra == "test"
728
727
  Dynamic: license-file
729
728
 
@@ -1,15 +1,15 @@
1
1
  reboost/__init__.py,sha256=VZz9uo7i2jgAx8Zi15SptLZnE_qcnGuNWwqkD3rYHFA,278
2
- reboost/_version.py,sha256=jF9TuoEIJRaca3ScKo6qaz6PzaMlu7jjuSQIrJ3nX4U,511
2
+ reboost/_version.py,sha256=DhO-RAVJdQTjYPaMf0M6q_WAXdD6jcx2tWl_B9FtuhM,511
3
3
  reboost/build_evt.py,sha256=VXIfK_pfe_Cgym6gI8dESwONZi-v_4fll0Pn09vePQY,3767
4
4
  reboost/build_glm.py,sha256=IerSLQfe51ZO7CQP2kmfPnOIVaDtcfw3byOM02Vaz6o,9472
5
- reboost/build_hit.py,sha256=23JL5B7qThdHZqAK_HWoytqcEOWDhGsk4n5UMtojJ1c,15513
5
+ reboost/build_hit.py,sha256=BwcAzr7hkhLqQpyULYPqM70VEvENaw-j90fvBYKh-68,15649
6
6
  reboost/cli.py,sha256=HZgqUZK0tSmnlGqoXjrbmLitW_i001TzibxvDrRxLLg,6324
7
7
  reboost/core.py,sha256=WGbWe2rcfMDEaehVyw7peqAHoTFWoCu5J6CdWHC5aWA,14974
8
8
  reboost/iterator.py,sha256=fATFDxu2PUc0e48OdJJujZo2kwykfRLH1oBtcB-s5pM,6905
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=3EH8XlpbsObdu5vLgxhm1600L6UNYD5jng4SjJT_1QE,2202
12
- reboost/utils.py,sha256=WT2jlyRT3LEMBEBRJtYIat3KIIZKocv4rHlTViBbhYM,10938
12
+ reboost/utils.py,sha256=dzDiQWwv_snoWoRBDnDNZ27hG0CpCRo8V4jSEG2b82c,12592
13
13
  reboost/hpge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  reboost/hpge/psd.py,sha256=jAUAoQ_PMz76wyA1NXYHNKtOwoCnRT3My8_LCFrKi-U,13860
15
15
  reboost/hpge/surface.py,sha256=lbWcFnFFWKxtFKs755GyM9US_IfyxaoM6MpOIZgIMM0,7478
@@ -18,10 +18,10 @@ reboost/math/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  reboost/math/functions.py,sha256=OymiYTcA0NXxxm-MBDw5kqyNwHoLCmuv4J48AwnSrbU,5633
19
19
  reboost/math/stats.py,sha256=cG-6mQx33Dzpv3ABkHLEwC104WJ_PMgbWtmjg37SBj4,3164
20
20
  reboost/optmap/__init__.py,sha256=imvuyld-GLw8qdwqW-lXCg2feptcTyQo3wIzPvDHwmY,93
21
- reboost/optmap/cli.py,sha256=SzbPRgsbR5Llm3aSJubH02Ym8FQyTH7kvuLjK7faLiY,9572
22
- reboost/optmap/convolve.py,sha256=_volpLmhW5mOPA0KkzXRyHyqkj4_zDSnvfHv1Dtuxm8,14390
23
- reboost/optmap/create.py,sha256=B-MWurmnzl4Y62N2Pj7IwM1IaEEt2fydpZa_t0gmsxo,17048
24
- reboost/optmap/evt.py,sha256=UYESkMAwDbE_ap4Jb-a2n0uWxHRnYmHzQiXh0vexaPQ,4513
21
+ reboost/optmap/cli.py,sha256=N8J2Hd8m_csYU9CtpAp7Rc3LHy6eNzZ26gWZgHCiUso,10250
22
+ reboost/optmap/convolve.py,sha256=x7boLDcBJIsontoB0yemvzHSE2hlRpUomlDXc3jqdr4,14668
23
+ reboost/optmap/create.py,sha256=_6GZbdRvmjDFs6DDbWC-THZxaNPUiLAOIDNaigMKJSQ,18139
24
+ reboost/optmap/evt.py,sha256=UrjjNNeS7Uie4Ah9y_f5PyroFutLGo5aOFcwReOEy7o,5556
25
25
  reboost/optmap/mapview.py,sha256=73kpe0_SKDj9bIhEx1ybX1sBP8TyvufiLfps84A_ijA,6798
26
26
  reboost/optmap/numba_pdg.py,sha256=y8cXR5PWE2Liprp4ou7vl9do76dl84vXU52ZJD9_I7A,731
27
27
  reboost/optmap/optmap.py,sha256=j4rfbQ84PYSpE-BvP4Rdt96ZjPdwy8P4e4eZz1mATys,12817
@@ -29,9 +29,9 @@ reboost/shape/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  reboost/shape/cluster.py,sha256=RIvBlhHzp88aaUZGofp5SD9bimnoiqIOddhQ84jiwoM,8135
30
30
  reboost/shape/group.py,sha256=gOCYgir2gZqmW1JXtbNRPlQqP0gmUcbe7RVb9CbY1pU,5540
31
31
  reboost/shape/reduction.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- reboost-0.6.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
33
- reboost-0.6.0.dist-info/METADATA,sha256=QGF9b0vthakVY3Wl9OUQnPIyAv3IYpui4WK9Zkp7Nwo,44274
34
- reboost-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
- reboost-0.6.0.dist-info/entry_points.txt,sha256=DxhD6BidSWNot9BrejHJjQ7RRLmrMaBIl52T75oWTwM,93
36
- reboost-0.6.0.dist-info/top_level.txt,sha256=q-IBsDepaY_AbzbRmQoW8EZrITXRVawVnNrB-_zyXZs,8
37
- reboost-0.6.0.dist-info/RECORD,,
32
+ reboost-0.6.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
33
+ reboost-0.6.2.dist-info/METADATA,sha256=o0DAQXxkMCG68Uf-WHTHeVsIEMKMsj044lWWMJLAx_k,44222
34
+ reboost-0.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ reboost-0.6.2.dist-info/entry_points.txt,sha256=DxhD6BidSWNot9BrejHJjQ7RRLmrMaBIl52T75oWTwM,93
36
+ reboost-0.6.2.dist-info/top_level.txt,sha256=q-IBsDepaY_AbzbRmQoW8EZrITXRVawVnNrB-_zyXZs,8
37
+ reboost-0.6.2.dist-info/RECORD,,