reboost 0.6.0__tar.gz → 0.6.1__tar.gz

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.
Files changed (73) hide show
  1. {reboost-0.6.0 → reboost-0.6.1}/PKG-INFO +2 -3
  2. {reboost-0.6.0 → reboost-0.6.1}/pyproject.toml +3 -5
  3. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/_version.py +2 -2
  4. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/optmap/cli.py +45 -24
  5. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/optmap/convolve.py +29 -18
  6. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/optmap/create.py +46 -17
  7. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/optmap/evt.py +43 -20
  8. {reboost-0.6.0 → reboost-0.6.1}/src/reboost.egg-info/PKG-INFO +2 -3
  9. {reboost-0.6.0 → reboost-0.6.1}/src/reboost.egg-info/SOURCES.txt +1 -0
  10. {reboost-0.6.0 → reboost-0.6.1}/src/reboost.egg-info/requires.txt +1 -2
  11. {reboost-0.6.0 → reboost-0.6.1}/tests/test_optmap.py +32 -11
  12. reboost-0.6.1/tests/test_optmap_dets.gdml +24 -0
  13. {reboost-0.6.0 → reboost-0.6.1}/LICENSE +0 -0
  14. {reboost-0.6.0 → reboost-0.6.1}/README.md +0 -0
  15. {reboost-0.6.0 → reboost-0.6.1}/setup.cfg +0 -0
  16. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/__init__.py +0 -0
  17. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/build_evt.py +0 -0
  18. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/build_glm.py +0 -0
  19. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/build_hit.py +0 -0
  20. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/cli.py +0 -0
  21. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/core.py +0 -0
  22. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/hpge/__init__.py +0 -0
  23. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/hpge/psd.py +0 -0
  24. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/hpge/surface.py +0 -0
  25. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/hpge/utils.py +0 -0
  26. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/iterator.py +0 -0
  27. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/log_utils.py +0 -0
  28. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/math/__init__.py +0 -0
  29. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/math/functions.py +0 -0
  30. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/math/stats.py +0 -0
  31. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/optmap/__init__.py +0 -0
  32. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/optmap/mapview.py +0 -0
  33. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/optmap/numba_pdg.py +0 -0
  34. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/optmap/optmap.py +0 -0
  35. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/profile.py +0 -0
  36. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/shape/__init__.py +0 -0
  37. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/shape/cluster.py +0 -0
  38. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/shape/group.py +0 -0
  39. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/shape/reduction.py +0 -0
  40. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/units.py +0 -0
  41. {reboost-0.6.0 → reboost-0.6.1}/src/reboost/utils.py +0 -0
  42. {reboost-0.6.0 → reboost-0.6.1}/src/reboost.egg-info/dependency_links.txt +0 -0
  43. {reboost-0.6.0 → reboost-0.6.1}/src/reboost.egg-info/entry_points.txt +0 -0
  44. {reboost-0.6.0 → reboost-0.6.1}/src/reboost.egg-info/not-zip-safe +0 -0
  45. {reboost-0.6.0 → reboost-0.6.1}/src/reboost.egg-info/top_level.txt +0 -0
  46. {reboost-0.6.0 → reboost-0.6.1}/tests/conftest.py +0 -0
  47. {reboost-0.6.0 → reboost-0.6.1}/tests/evt/test_evt.py +0 -0
  48. {reboost-0.6.0 → reboost-0.6.1}/tests/glm/test_build_glm.py +0 -0
  49. {reboost-0.6.0 → reboost-0.6.1}/tests/hit/configs/args.yaml +0 -0
  50. {reboost-0.6.0 → reboost-0.6.1}/tests/hit/configs/basic.yaml +0 -0
  51. {reboost-0.6.0 → reboost-0.6.1}/tests/hit/configs/geom.gdml +0 -0
  52. {reboost-0.6.0 → reboost-0.6.1}/tests/hit/configs/hit_config.yaml +0 -0
  53. {reboost-0.6.0 → reboost-0.6.1}/tests/hit/configs/pars.yaml +0 -0
  54. {reboost-0.6.0 → reboost-0.6.1}/tests/hit/configs/reshape.yaml +0 -0
  55. {reboost-0.6.0 → reboost-0.6.1}/tests/hit/test_build_hit.py +0 -0
  56. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/simulation/gammas.mac +0 -0
  57. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/simulation/geometry.gdml +0 -0
  58. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/simulation/make_dt_map.jl +0 -0
  59. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/simulation/make_geom.py +0 -0
  60. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/test_current.py +0 -0
  61. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/test_dt_heuristic.py +0 -0
  62. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/test_files/drift_time_maps.lh5 +0 -0
  63. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/test_files/internal_electron.lh5 +0 -0
  64. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/test_hpge_map.py +0 -0
  65. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/test_r90.py +0 -0
  66. {reboost-0.6.0 → reboost-0.6.1}/tests/hpge/test_surface.py +0 -0
  67. {reboost-0.6.0 → reboost-0.6.1}/tests/test_cli.py +0 -0
  68. {reboost-0.6.0 → reboost-0.6.1}/tests/test_core.py +0 -0
  69. {reboost-0.6.0 → reboost-0.6.1}/tests/test_math.py +0 -0
  70. {reboost-0.6.0 → reboost-0.6.1}/tests/test_profile.py +0 -0
  71. {reboost-0.6.0 → reboost-0.6.1}/tests/test_shape.py +0 -0
  72. {reboost-0.6.0 → reboost-0.6.1}/tests/test_units.py +0 -0
  73. {reboost-0.6.0 → reboost-0.6.1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reboost
3
- Version: 0.6.0
3
+ Version: 0.6.1
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
 
@@ -37,8 +37,9 @@ dependencies = [
37
37
  "numpy",
38
38
  "scipy",
39
39
  "numba",
40
- "legend-pydataobj>=1.14",
41
- "legend-pygeom-optics>=0.9.2",
40
+ "legend-pydataobj >=1.14",
41
+ "legend-pygeom-optics >=0.9.2",
42
+ "legend-pygeom-tools >=0.0.11",
42
43
  "hist",
43
44
  "dbetto",
44
45
  "particle",
@@ -75,10 +76,7 @@ test = [
75
76
  "pytest>=6.0",
76
77
  "pytest-cov",
77
78
  "legend-pygeom-hpges",
78
- "legend-pygeom-tools",
79
- "pyg4ometry",
80
79
  "pylegendtestdata>=0.6",
81
-
82
80
  ]
83
81
 
84
82
  [project.scripts]
@@ -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.1'
21
+ __version_tuple__ = version_tuple = (0, 6, 1)
@@ -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)
@@ -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)
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reboost
3
- Version: 0.6.0
3
+ Version: 0.6.1
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
 
@@ -44,6 +44,7 @@ tests/test_cli.py
44
44
  tests/test_core.py
45
45
  tests/test_math.py
46
46
  tests/test_optmap.py
47
+ tests/test_optmap_dets.gdml
47
48
  tests/test_profile.py
48
49
  tests/test_shape.py
49
50
  tests/test_units.py
@@ -5,6 +5,7 @@ scipy
5
5
  numba
6
6
  legend-pydataobj>=1.14
7
7
  legend-pygeom-optics>=0.9.2
8
+ legend-pygeom-tools>=0.0.11
8
9
  hist
9
10
  dbetto
10
11
  particle
@@ -27,6 +28,4 @@ pre-commit
27
28
  pytest>=6.0
28
29
  pytest-cov
29
30
  legend-pygeom-hpges
30
- legend-pygeom-tools
31
- pyg4ometry
32
31
  pylegendtestdata>=0.6
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from pathlib import Path
4
+
3
5
  import numpy as np
4
6
  import pytest
5
7
  from lgdo import Array, Table, lh5
@@ -49,13 +51,13 @@ def tbl_hits(tmptestdir):
49
51
  hit_file = tmptestdir / "hit.lh5"
50
52
  lh5.write(tbl_vertices, name="vtx", lh5_file=hit_file, wo_mode="overwrite_file")
51
53
  lh5.write(tbl_optical, name="stp/optical", lh5_file=hit_file, wo_mode="overwrite")
52
- return hit_file
54
+ return (str(hit_file),)
53
55
 
54
56
 
55
57
  def test_optmap_evt(tbl_hits, tmptestdir):
56
58
  evt_out_file = tmptestdir / "evt-out.lh5"
57
59
  build_optmap_evt(
58
- str(tbl_hits),
60
+ tbl_hits[0],
59
61
  str(evt_out_file),
60
62
  detectors=("1", "002", "003"),
61
63
  buffer_len=20, # note: shorter window sizes (e.g. 10) do not work.
@@ -67,16 +69,17 @@ def tbl_evt_fns(tmptestdir) -> tuple[str]:
67
69
  evt_count = 100
68
70
  rng = np.random.default_rng(1234)
69
71
  loc = rng.uniform(size=(evt_count, 3))
70
- hits = rng.geometric(p=0.9, size=(evt_count, 3)) - 1
72
+ # hits = rng.geometric(p=0.9, size=(evt_count, 3)) - 1
73
+ hits = rng.choice([1, 2, 3], size=evt_count)
71
74
 
72
75
  tbl_evt = Table(
73
76
  {
74
77
  "xloc": Array(loc[:, 0]),
75
78
  "yloc": Array(loc[:, 1]),
76
79
  "zloc": Array(loc[:, 2]),
77
- "001": Array(hits[:, 0]),
78
- "002": Array(hits[:, 1]),
79
- "003": Array(hits[:, 2]),
80
+ "001": Array((hits == 1).astype(int)),
81
+ "002": Array((hits == 2).astype(int)),
82
+ "003": Array((hits == 3).astype(int)),
80
83
  }
81
84
  )
82
85
 
@@ -86,43 +89,56 @@ def tbl_evt_fns(tmptestdir) -> tuple[str]:
86
89
 
87
90
 
88
91
  @pytest.mark.filterwarnings("ignore::scipy.optimize._optimize.OptimizeWarning")
89
- def test_optmap_create(tbl_evt_fns):
92
+ @pytest.mark.parametrize("input_fixture", ["tbl_evt_fns", "tbl_hits"])
93
+ def test_optmap_create(input_fixture, request):
90
94
  settings = {
91
95
  "range_in_m": [[0, 1], [0, 1], [0, 1]],
92
96
  "bins": [10, 10, 10],
93
97
  }
94
98
 
99
+ extra_params = {
100
+ "is_stp_file": input_fixture == "tbl_hits",
101
+ "geom_fn": (
102
+ f"{Path(__file__).parent}/test_optmap_dets.gdml" if input_fixture == "tbl_hits" else ""
103
+ ),
104
+ }
105
+ input_fixture = request.getfixturevalue(input_fixture)
106
+
95
107
  # test creation only with the summary map.
96
108
  create_optical_maps(
97
- tbl_evt_fns,
109
+ input_fixture,
98
110
  settings,
99
111
  chfilter=(),
100
112
  output_lh5_fn=None,
113
+ **extra_params,
101
114
  )
102
115
 
103
116
  # test creation with all detectors.
104
117
  create_optical_maps(
105
- tbl_evt_fns,
118
+ input_fixture,
106
119
  settings,
107
120
  chfilter=("001", "002", "003"),
108
121
  output_lh5_fn=None,
122
+ **extra_params,
109
123
  )
110
124
 
111
125
  # test creation with some detectors.
112
126
  create_optical_maps(
113
- tbl_evt_fns,
127
+ input_fixture,
114
128
  settings,
115
129
  chfilter=("001"),
116
130
  output_lh5_fn=None,
131
+ **extra_params,
117
132
  )
118
133
 
119
134
  # test creation on multiple cores.
120
135
  create_optical_maps(
121
- tbl_evt_fns,
136
+ input_fixture,
122
137
  settings,
123
138
  chfilter=("001", "002", "003"),
124
139
  output_lh5_fn=None,
125
140
  n_procs=2,
141
+ **extra_params,
126
142
  )
127
143
 
128
144
 
@@ -139,6 +155,7 @@ def test_optmap_merge(tbl_evt_fns, tmptestdir):
139
155
  settings,
140
156
  chfilter=("001", "002", "003"),
141
157
  output_lh5_fn=map1_fn,
158
+ is_stp_file=False,
142
159
  )
143
160
  map2_fn = str(tmptestdir / "map2.lh5")
144
161
  create_optical_maps(
@@ -146,6 +163,7 @@ def test_optmap_merge(tbl_evt_fns, tmptestdir):
146
163
  settings,
147
164
  chfilter=("001", "002", "003"),
148
165
  output_lh5_fn=map2_fn,
166
+ is_stp_file=False,
149
167
  )
150
168
 
151
169
  # test in sequential mode.
@@ -170,6 +188,7 @@ def test_optmap_rebin(tbl_evt_fns, tmptestdir):
170
188
  settings,
171
189
  chfilter=("001", "002", "003"),
172
190
  output_lh5_fn=map1_fn,
191
+ is_stp_file=False,
173
192
  )
174
193
 
175
194
  map_rebinned_fn = str(tmptestdir / "map-rebinned.lh5")
@@ -219,6 +238,7 @@ def test_optmap_convolve(tbl_evt_fns, tbl_edep, tmptestdir):
219
238
  settings,
220
239
  chfilter=("001"),
221
240
  output_lh5_fn=map_fn,
241
+ is_stp_file=False,
222
242
  )
223
243
 
224
244
  out_fn = str(tmptestdir / "convolved.lh5")
@@ -245,6 +265,7 @@ def test_optmap_save_and_load(tmptestdir, tbl_evt_fns):
245
265
  settings,
246
266
  chfilter=("001", "002", "003"),
247
267
  output_lh5_fn=map_fn,
268
+ is_stp_file=False,
248
269
  )
249
270
 
250
271
  assert list_optical_maps(map_fn) == ["_001", "_002", "_003", "all"]
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" ?>
2
+ <gdml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd">
3
+ <materials></materials>
4
+ <solids>
5
+ <box name="world" x="20" y="20" z="20" lunit="m"/>
6
+ </solids>
7
+ <structure>
8
+ <volume name="world">
9
+ <materialref ref="G4_Galactic"/>
10
+ <solidref ref="world"/>
11
+ </volume>
12
+ </structure>
13
+ <userinfo>
14
+ <auxiliary auxtype="RMG_detector_meta" auxvalue=""/>
15
+ <auxiliary auxtype="RMG_detector" auxvalue="optical">
16
+ <auxiliary auxtype="S01" auxvalue="001"/>
17
+ <auxiliary auxtype="S02" auxvalue="002"/>
18
+ <auxiliary auxtype="S03" auxvalue="003"/>
19
+ </auxiliary>
20
+ </userinfo>
21
+ <setup>
22
+ <world ref="world" />
23
+ </setup>
24
+ </gdml>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes