reboost 0.7.0__tar.gz → 0.8.0__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 (81) hide show
  1. {reboost-0.7.0 → reboost-0.8.0}/PKG-INFO +4 -2
  2. {reboost-0.7.0 → reboost-0.8.0}/README.md +2 -0
  3. {reboost-0.7.0 → reboost-0.8.0}/pyproject.toml +1 -1
  4. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/_version.py +3 -3
  5. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/core.py +0 -2
  6. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/optmap/cli.py +40 -101
  7. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/optmap/convolve.py +12 -285
  8. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/optmap/create.py +39 -123
  9. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/optmap/evt.py +5 -2
  10. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/optmap/mapview.py +9 -7
  11. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/optmap/optmap.py +11 -12
  12. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/spms/pe.py +80 -1
  13. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/utils.py +1 -1
  14. {reboost-0.7.0 → reboost-0.8.0}/src/reboost.egg-info/PKG-INFO +4 -2
  15. {reboost-0.7.0 → reboost-0.8.0}/src/reboost.egg-info/SOURCES.txt +2 -1
  16. {reboost-0.7.0 → reboost-0.8.0}/src/reboost.egg-info/requires.txt +1 -1
  17. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/test_build_hit.py +1 -1
  18. reboost-0.8.0/tests/spms/test_pe.py +34 -0
  19. {reboost-0.7.0 → reboost-0.8.0}/tests/test_optmap.py +57 -75
  20. {reboost-0.7.0 → reboost-0.8.0}/LICENSE +0 -0
  21. {reboost-0.7.0 → reboost-0.8.0}/setup.cfg +0 -0
  22. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/__init__.py +0 -0
  23. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/build_evt.py +0 -0
  24. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/build_glm.py +0 -0
  25. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/build_hit.py +0 -0
  26. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/cli.py +0 -0
  27. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/daq/__init__.py +0 -0
  28. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/daq/core.py +0 -0
  29. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/daq/utils.py +0 -0
  30. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/hpge/__init__.py +0 -0
  31. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/hpge/psd.py +0 -0
  32. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/hpge/surface.py +0 -0
  33. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/hpge/utils.py +0 -0
  34. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/iterator.py +0 -0
  35. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/log_utils.py +0 -0
  36. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/math/__init__.py +0 -0
  37. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/math/functions.py +0 -0
  38. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/math/stats.py +0 -0
  39. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/optmap/__init__.py +0 -0
  40. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/optmap/numba_pdg.py +0 -0
  41. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/profile.py +0 -0
  42. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/shape/__init__.py +0 -0
  43. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/shape/cluster.py +0 -0
  44. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/shape/group.py +0 -0
  45. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/shape/reduction.py +0 -0
  46. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/spms/__init__.py +0 -0
  47. {reboost-0.7.0 → reboost-0.8.0}/src/reboost/units.py +0 -0
  48. {reboost-0.7.0 → reboost-0.8.0}/src/reboost.egg-info/dependency_links.txt +0 -0
  49. {reboost-0.7.0 → reboost-0.8.0}/src/reboost.egg-info/entry_points.txt +0 -0
  50. {reboost-0.7.0 → reboost-0.8.0}/src/reboost.egg-info/not-zip-safe +0 -0
  51. {reboost-0.7.0 → reboost-0.8.0}/src/reboost.egg-info/top_level.txt +0 -0
  52. {reboost-0.7.0 → reboost-0.8.0}/tests/conftest.py +0 -0
  53. {reboost-0.7.0 → reboost-0.8.0}/tests/evt/test_evt.py +0 -0
  54. {reboost-0.7.0 → reboost-0.8.0}/tests/glm/test_build_glm.py +0 -0
  55. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/configs/args.yaml +0 -0
  56. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/configs/basic.yaml +0 -0
  57. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/configs/foward_only.yaml +0 -0
  58. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/configs/geom.gdml +0 -0
  59. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/configs/hit_config.yaml +0 -0
  60. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/configs/pars.yaml +0 -0
  61. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/configs/reshape.yaml +0 -0
  62. {reboost-0.7.0 → reboost-0.8.0}/tests/hit/configs/spms.yaml +0 -0
  63. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/simulation/gammas.mac +0 -0
  64. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/simulation/geometry.gdml +0 -0
  65. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/simulation/make_dt_map.jl +0 -0
  66. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/simulation/make_geom.py +0 -0
  67. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/test_current.py +0 -0
  68. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/test_dt_heuristic.py +0 -0
  69. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/test_files/drift_time_maps.lh5 +0 -0
  70. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/test_files/internal_electron.lh5 +0 -0
  71. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/test_hpge_map.py +0 -0
  72. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/test_r90.py +0 -0
  73. {reboost-0.7.0 → reboost-0.8.0}/tests/hpge/test_surface.py +0 -0
  74. {reboost-0.7.0 → reboost-0.8.0}/tests/test_cli.py +0 -0
  75. {reboost-0.7.0 → reboost-0.8.0}/tests/test_core.py +0 -0
  76. {reboost-0.7.0 → reboost-0.8.0}/tests/test_math.py +0 -0
  77. {reboost-0.7.0 → reboost-0.8.0}/tests/test_optmap_dets.gdml +0 -0
  78. {reboost-0.7.0 → reboost-0.8.0}/tests/test_profile.py +0 -0
  79. {reboost-0.7.0 → reboost-0.8.0}/tests/test_shape.py +0 -0
  80. {reboost-0.7.0 → reboost-0.8.0}/tests/test_units.py +0 -0
  81. {reboost-0.7.0 → reboost-0.8.0}/tests/test_utils.py +0 -0
@@ -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,5 +1,7 @@
1
1
  # reboost
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/reboost?logo=pypi)](https://pypi.org/project/reboost/)
4
+ [![conda-forge](https://img.shields.io/conda/vn/conda-forge/reboost.svg)](https://anaconda.org/conda-forge/reboost)
3
5
  ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/legend-exp/reboost?logo=git)
4
6
  [![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)
5
7
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
@@ -36,7 +36,7 @@ dependencies = [
36
36
  "colorlog",
37
37
  "numpy",
38
38
  "scipy",
39
- "numba",
39
+ "numba >=0.60",
40
40
  "legend-pydataobj >=1.15.1",
41
41
  "legend-pygeom-optics >=0.12.0",
42
42
  "legend-pygeom-tools >=0.0.11",
@@ -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
- __commit_id__ = commit_id = 'gbfb61789b'
34
+ __commit_id__ = commit_id = 'g1c98fe8fc'
@@ -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})
@@ -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