reboost 0.5.5__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 (75) hide show
  1. {reboost-0.5.5 → reboost-0.6.1}/PKG-INFO +3 -3
  2. {reboost-0.5.5 → reboost-0.6.1}/pyproject.toml +5 -5
  3. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/__init__.py +2 -1
  4. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/_version.py +2 -2
  5. reboost-0.6.1/src/reboost/build_evt.py +134 -0
  6. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/core.py +74 -0
  7. reboost-0.6.1/src/reboost/math/stats.py +119 -0
  8. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/cli.py +45 -24
  9. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/convolve.py +29 -18
  10. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/create.py +46 -17
  11. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/evt.py +43 -20
  12. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/shape/group.py +38 -0
  13. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/utils.py +10 -0
  14. {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/PKG-INFO +3 -3
  15. {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/SOURCES.txt +2 -0
  16. {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/requires.txt +2 -2
  17. reboost-0.6.1/tests/evt/test_evt.py +60 -0
  18. {reboost-0.5.5 → reboost-0.6.1}/tests/hit/test_build_hit.py +4 -2
  19. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_dt_heuristic.py +1 -1
  20. {reboost-0.5.5 → reboost-0.6.1}/tests/test_core.py +53 -1
  21. {reboost-0.5.5 → reboost-0.6.1}/tests/test_math.py +25 -0
  22. {reboost-0.5.5 → reboost-0.6.1}/tests/test_optmap.py +32 -11
  23. reboost-0.6.1/tests/test_optmap_dets.gdml +24 -0
  24. {reboost-0.5.5 → reboost-0.6.1}/tests/test_shape.py +20 -0
  25. {reboost-0.5.5 → reboost-0.6.1}/tests/test_utils.py +10 -3
  26. reboost-0.5.5/src/reboost/build_evt.py +0 -166
  27. reboost-0.5.5/src/reboost/math/stats.py +0 -57
  28. {reboost-0.5.5 → reboost-0.6.1}/LICENSE +0 -0
  29. {reboost-0.5.5 → reboost-0.6.1}/README.md +0 -0
  30. {reboost-0.5.5 → reboost-0.6.1}/setup.cfg +0 -0
  31. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/build_glm.py +0 -0
  32. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/build_hit.py +0 -0
  33. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/cli.py +0 -0
  34. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/hpge/__init__.py +0 -0
  35. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/hpge/psd.py +0 -0
  36. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/hpge/surface.py +0 -0
  37. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/hpge/utils.py +0 -0
  38. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/iterator.py +0 -0
  39. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/log_utils.py +0 -0
  40. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/math/__init__.py +0 -0
  41. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/math/functions.py +0 -0
  42. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/__init__.py +0 -0
  43. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/mapview.py +0 -0
  44. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/numba_pdg.py +0 -0
  45. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/optmap.py +0 -0
  46. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/profile.py +0 -0
  47. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/shape/__init__.py +0 -0
  48. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/shape/cluster.py +0 -0
  49. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/shape/reduction.py +0 -0
  50. {reboost-0.5.5 → reboost-0.6.1}/src/reboost/units.py +0 -0
  51. {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/dependency_links.txt +0 -0
  52. {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/entry_points.txt +0 -0
  53. {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/not-zip-safe +0 -0
  54. {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/top_level.txt +0 -0
  55. {reboost-0.5.5 → reboost-0.6.1}/tests/conftest.py +0 -0
  56. {reboost-0.5.5 → reboost-0.6.1}/tests/glm/test_build_glm.py +0 -0
  57. {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/args.yaml +0 -0
  58. {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/basic.yaml +0 -0
  59. {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/geom.gdml +0 -0
  60. {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/hit_config.yaml +0 -0
  61. {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/pars.yaml +0 -0
  62. {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/reshape.yaml +0 -0
  63. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/simulation/gammas.mac +0 -0
  64. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/simulation/geometry.gdml +0 -0
  65. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/simulation/make_dt_map.jl +0 -0
  66. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/simulation/make_geom.py +0 -0
  67. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_current.py +0 -0
  68. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_files/drift_time_maps.lh5 +0 -0
  69. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_files/internal_electron.lh5 +0 -0
  70. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_hpge_map.py +0 -0
  71. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_r90.py +0 -0
  72. {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_surface.py +0 -0
  73. {reboost-0.5.5 → reboost-0.6.1}/tests/test_cli.py +0 -0
  74. {reboost-0.5.5 → reboost-0.6.1}/tests/test_profile.py +0 -0
  75. {reboost-0.5.5 → reboost-0.6.1}/tests/test_units.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reboost
3
- Version: 0.5.5
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
@@ -696,12 +696,14 @@ Classifier: Topic :: Scientific/Engineering
696
696
  Requires-Python: >=3.9
697
697
  Description-Content-Type: text/markdown
698
698
  License-File: LICENSE
699
+ Requires-Dist: hdf5plugin
699
700
  Requires-Dist: colorlog
700
701
  Requires-Dist: numpy
701
702
  Requires-Dist: scipy
702
703
  Requires-Dist: numba
703
704
  Requires-Dist: legend-pydataobj>=1.14
704
705
  Requires-Dist: legend-pygeom-optics>=0.9.2
706
+ Requires-Dist: legend-pygeom-tools>=0.0.11
705
707
  Requires-Dist: hist
706
708
  Requires-Dist: dbetto
707
709
  Requires-Dist: particle
@@ -721,8 +723,6 @@ Requires-Dist: pre-commit; extra == "test"
721
723
  Requires-Dist: pytest>=6.0; extra == "test"
722
724
  Requires-Dist: pytest-cov; extra == "test"
723
725
  Requires-Dist: legend-pygeom-hpges; extra == "test"
724
- Requires-Dist: legend-pygeom-tools; extra == "test"
725
- Requires-Dist: pyg4ometry; extra == "test"
726
726
  Requires-Dist: pylegendtestdata>=0.6; extra == "test"
727
727
  Dynamic: license-file
728
728
 
@@ -32,12 +32,14 @@ classifiers = [
32
32
  ]
33
33
  requires-python = ">=3.9"
34
34
  dependencies = [
35
+ "hdf5plugin",
35
36
  "colorlog",
36
37
  "numpy",
37
38
  "scipy",
38
39
  "numba",
39
- "legend-pydataobj>=1.14",
40
- "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",
41
43
  "hist",
42
44
  "dbetto",
43
45
  "particle",
@@ -74,10 +76,7 @@ test = [
74
76
  "pytest>=6.0",
75
77
  "pytest-cov",
76
78
  "legend-pygeom-hpges",
77
- "legend-pygeom-tools",
78
- "pyg4ometry",
79
79
  "pylegendtestdata>=0.6",
80
-
81
80
  ]
82
81
 
83
82
  [project.scripts]
@@ -147,6 +146,7 @@ ignore = [
147
146
  "D213", # Multi-line docstring summary should start at the first line
148
147
  "D401", # Summary does not need to be in imperative mood
149
148
  "D413", # No blank line after last section in docstring
149
+ "PLC0415", # we sometimes use relative imports for performance reasons
150
150
  "PLC2401", # We like non-ASCII characters for math
151
151
  ]
152
152
  isort.required-imports = ["from __future__ import annotations"]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hdf5plugin
3
4
  from lgdo import lh5
4
5
 
5
6
  from ._version import version as __version__
@@ -10,4 +11,4 @@ __all__ = [
10
11
  "build_hit",
11
12
  ]
12
13
 
13
- lh5.settings.DEFAULT_HDF5_SETTINGS = {"shuffle": True, "compression": "lzf"}
14
+ lh5.settings.DEFAULT_HDF5_SETTINGS = {"compression": hdf5plugin.Zstd()}
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.5.5'
21
- __version_tuple__ = version_tuple = (0, 5, 5)
20
+ __version__ = version = '0.6.1'
21
+ __version_tuple__ = version_tuple = (0, 6, 1)
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ import awkward as ak
6
+ import numpy as np
7
+ from dbetto import AttrsDict
8
+ from lgdo import Array, Table, VectorOfVectors, lh5
9
+
10
+ from . import core, math, shape, utils
11
+ from .shape import group
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def build_evt(
17
+ tcm: VectorOfVectors,
18
+ hitfile: str,
19
+ outfile: str | None,
20
+ channel_groups: AttrsDict,
21
+ pars: AttrsDict,
22
+ run_part: AttrsDict,
23
+ ) -> Table | None:
24
+ """Build events out of a TCM.
25
+
26
+ Parameters
27
+ ----------
28
+ tcm
29
+ the time coincidence map.
30
+ hitfile
31
+ file with the hits.
32
+ outfile
33
+ the path to the output-file, if `None` with return
34
+ the events in memory.
35
+ channel_groups
36
+ a dictionary of groups of channels. For example:
37
+
38
+ .. code-block:: python
39
+
40
+ {"det1": "on", "det2": "off", "det3": "ac"}
41
+
42
+ pars
43
+ A dictionary of parameters. The first key should
44
+ be the run ID, followed by different sets of parameters
45
+ arranged in groups. Run numbers should be given in the
46
+ format `"p00-r001"`, etc.
47
+
48
+ For example:
49
+
50
+ .. code-block:: python
51
+
52
+ {"p03-r000": {"reso": {"det1": [1, 2], "det2": [0, 1]}}}
53
+
54
+ run_part
55
+ The run partitioning file giving the number of events
56
+ for each run. This should be organized as a dictionary
57
+ with the following format:
58
+
59
+ .. code-block:: python
60
+
61
+ {"p03-r000": 1000, "p03-r001": 2000}
62
+
63
+ Returns
64
+ -------
65
+ the event file in memory as a table if no output file is specified.
66
+ """
67
+ tcm_tables = utils.get_table_names(tcm)
68
+ tcm_ak = tcm.view_as("ak")
69
+
70
+ # loop over the runs
71
+ cum_sum = 0
72
+ tab = None
73
+
74
+ for idx, (run_full, n_event) in enumerate(run_part.items()):
75
+ period, run = run_full.split("-")
76
+ pars_tmp = pars[run_full]
77
+
78
+ # create an output table
79
+ out_tab = Table(size=n_event)
80
+
81
+ tcm_tmp = tcm_ak[cum_sum : cum_sum + n_event]
82
+
83
+ # usabilities
84
+
85
+ is_off = shape.group.get_isin_group(
86
+ tcm_tmp.table_key, channel_groups, tcm_tables, group="off"
87
+ )
88
+
89
+ # filter out off channels
90
+ channels = tcm_tmp.table_key[~is_off]
91
+ rows = tcm_tmp.row_in_table[~is_off]
92
+ out_tab.add_field("channel", VectorOfVectors(channels))
93
+ out_tab.add_field("row_in_table", VectorOfVectors(rows))
94
+
95
+ out_tab.add_field("period", Array(np.ones(len(channels)) * int(period[1:])))
96
+ out_tab.add_field("run", Array(np.ones(len(channels)) * int(run[1:])))
97
+
98
+ # now check for channels in ac
99
+ is_good = group.get_isin_group(channels, channel_groups, tcm_tables, group="on")
100
+
101
+ # get energy
102
+ energy_true = core.read_data_at_channel_as_ak(
103
+ channels, rows, hitfile, "energy", "hit", tcm_tables
104
+ )
105
+
106
+ energy = math.stats.apply_energy_resolution(
107
+ energy_true,
108
+ channels,
109
+ tcm_tables,
110
+ pars_tmp.reso,
111
+ lambda energy, sig0, sig1: np.sqrt(energy * sig1**2 + sig0**2),
112
+ )
113
+
114
+ out_tab.add_field("is_good", VectorOfVectors(is_good[energy > 25]))
115
+
116
+ out_tab.add_field("energy", VectorOfVectors(energy[energy > 25]))
117
+ out_tab.add_field("multiplicity", Array(ak.sum(energy > 25, axis=-1).to_numpy()))
118
+
119
+ # write table
120
+ wo_mode = "of" if idx == 0 else "append"
121
+
122
+ # add attrs
123
+ out_tab.attrs["tables"] = tcm.attrs["tables"]
124
+
125
+ if outfile is not None:
126
+ lh5.write(out_tab, "evt", outfile, wo_mode=wo_mode)
127
+ else:
128
+ tab = (
129
+ ak.concatenate((tab, out_tab.view_as("ak")))
130
+ if tab is not None
131
+ else out_tab.view_as("ak")
132
+ )
133
+
134
+ return Table(tab)
@@ -5,7 +5,9 @@ import time
5
5
  from typing import Any
6
6
 
7
7
  import awkward as ak
8
+ import numpy as np
8
9
  from dbetto import AttrsDict
10
+ from lgdo import lh5
9
11
  from lgdo.types import LGDO, Table
10
12
 
11
13
  from . import utils
@@ -14,6 +16,78 @@ from .profile import ProfileDict
14
16
  log = logging.getLogger(__name__)
15
17
 
16
18
 
19
+ def read_data_at_channel_as_ak(
20
+ channels: ak.Array, rows: ak.Array, file: str, field: str, group: str, tab_map: dict[int, str]
21
+ ) -> ak.Array:
22
+ r"""Read the data from a particular field to an awkward array. This replaces the TCM like object defined by the channels and rows with the corresponding data field.
23
+
24
+ Parameters
25
+ ----------
26
+ channels
27
+ Array of the channel indices (uids).
28
+ rows
29
+ Array of the rows in the files to gather data from.
30
+ file
31
+ File to read the data from.
32
+ field
33
+ the field to read.
34
+ group
35
+ the group to read data from (eg. `hit` or `stp`.)
36
+ tab_map
37
+ mapping between indices and table names. Of the form:
38
+
39
+ .. code:: python
40
+
41
+ {NAME: UID}
42
+
43
+ For example:
44
+
45
+ .. code:: python
46
+
47
+ {"det001": 1, "det002": 2}
48
+
49
+ Returns
50
+ -------
51
+ an array with the data, of the same same as the channels and rows.
52
+ """
53
+ # initialise the output
54
+ data_flat = None
55
+ tcm_rows_full = None
56
+
57
+ # save the unflattening
58
+ reorder = ak.num(rows)
59
+
60
+ for tab_name, key in tab_map.items():
61
+ # get the rows to read
62
+
63
+ idx = ak.flatten(rows[channels == key]).to_numpy()
64
+ arg_idx = np.argsort(idx)
65
+
66
+ # get the rows in the flattened data we want to append to
67
+ tcm_rows = np.where(ak.flatten(channels == key))[0]
68
+
69
+ # read the data with sorted idx
70
+ data_ch = lh5.read(f"{group}/{tab_name}/{field}", file, idx=idx[arg_idx]).view_as("ak")
71
+
72
+ # sort back to order for tcm
73
+ data_ch = data_ch[np.argsort(arg_idx)]
74
+
75
+ # append to output
76
+ data_flat = ak.concatenate((data_flat, data_ch)) if data_flat is not None else data_ch
77
+ tcm_rows_full = (
78
+ np.concatenate((tcm_rows_full, tcm_rows)) if tcm_rows_full is not None else tcm_rows
79
+ )
80
+
81
+ if len(data_flat) != len(tcm_rows_full):
82
+ msg = "every index in the tcm should have been read"
83
+ raise ValueError(msg)
84
+
85
+ # sort the final data
86
+ data_flat = data_flat[np.argsort(tcm_rows_full)]
87
+
88
+ return ak.unflatten(data_flat, reorder)
89
+
90
+
17
91
  def evaluate_output_column(
18
92
  hit_table: Table,
19
93
  expression: str,
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Callable
5
+
6
+ import awkward as ak
7
+ import numpy as np
8
+ from lgdo import Array
9
+ from numpy.typing import ArrayLike
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+
14
+ def get_resolution(
15
+ energies: ak.Array, channels: ak.Array, tcm_tables: dict, reso_pars: dict, reso_func: Callable
16
+ ) -> ak.Array:
17
+ """Get the resolution for each energy.
18
+
19
+ Parameters
20
+ ----------
21
+ energies
22
+ the energies to smear
23
+ channels
24
+ the channel index for each energy
25
+ tcm_tables
26
+ the mapping from indices to channel names.
27
+ reso_pars
28
+ the pars for each channel.
29
+ reso_func
30
+ the function to compute the resolution.
31
+ """
32
+ n_pars = len(reso_pars[next(iter(reso_pars))])
33
+
34
+ pars_shaped = []
35
+
36
+ for _ in range(n_pars):
37
+ pars_shaped.append(np.zeros(len(ak.flatten(channels))))
38
+
39
+ num = ak.num(channels, axis=-1)
40
+
41
+ for key, value in tcm_tables.items():
42
+ for i in range(n_pars):
43
+ pars_shaped[i][ak.flatten(channels) == value] = reso_pars[key][i]
44
+
45
+ ch_reso = reso_func(ak.flatten(energies), *pars_shaped)
46
+ return ak.unflatten(ch_reso, num)
47
+
48
+
49
+ def apply_energy_resolution(
50
+ energies: ak.Array, channels: ak.Array, tcm_tables: dict, reso_pars: dict, reso_func: Callable
51
+ ):
52
+ """Apply the energy resolution sampling to an array with many channels.
53
+
54
+ Parameters
55
+ ----------
56
+ energies
57
+ the energies to smear
58
+ channels
59
+ the channel index for each energy
60
+ tcm_tables
61
+ the mapping from indices to channel names.
62
+ reso_pars
63
+ the pars for each channel.
64
+ reso_func
65
+ the function to compute the resolution.
66
+ """
67
+ num = ak.num(channels, axis=-1)
68
+
69
+ ch_reso = get_resolution(energies, channels, tcm_tables, reso_pars, reso_func)
70
+ energies_flat_smear = gaussian_sample(ak.flatten(energies), ak.flatten(ch_reso))
71
+
72
+ return ak.unflatten(energies_flat_smear, num)
73
+
74
+
75
+ def gaussian_sample(mu: ArrayLike, sigma: ArrayLike | float, *, seed: int | None = None) -> Array:
76
+ r"""Generate samples from a gaussian.
77
+
78
+ Based on:
79
+
80
+ .. math::
81
+
82
+ y_i \sim \mathcal{N}(\mu_i,\sigma_i)
83
+
84
+ where $y_i$ is the output, $x_i$ the input (mu) and $\sigma$ is the standard
85
+ deviation for each point.
86
+
87
+ Parameters
88
+ ----------
89
+ mu
90
+ the mean positions to sample from, should be a flat (ArrayLike) object.
91
+ sigma
92
+ the standard deviation for each input value, can also be a single float.
93
+ seed
94
+ the random seed.
95
+
96
+ Returns
97
+ -------
98
+ sampled values.
99
+ """
100
+ # convert inputs
101
+
102
+ if isinstance(mu, Array):
103
+ mu = mu.view_as("np")
104
+ elif isinstance(mu, ak.Array):
105
+ mu = mu.to_numpy()
106
+ elif not isinstance(mu, np.ndarray):
107
+ mu = np.array(mu)
108
+
109
+ # similar for sigma
110
+ if isinstance(sigma, Array):
111
+ sigma = sigma.view_as("np")
112
+ elif isinstance(sigma, ak.Array):
113
+ sigma = sigma.to_numpy()
114
+ elif not isinstance(sigma, (float, int, np.ndarray)):
115
+ sigma = np.array(sigma)
116
+
117
+ rng = np.random.default_rng(seed=seed) # Create a random number generator
118
+
119
+ return Array(rng.normal(loc=mu, scale=sigma))
@@ -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)