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.
- {reboost-0.5.5 → reboost-0.6.1}/PKG-INFO +3 -3
- {reboost-0.5.5 → reboost-0.6.1}/pyproject.toml +5 -5
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/__init__.py +2 -1
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/_version.py +2 -2
- reboost-0.6.1/src/reboost/build_evt.py +134 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/core.py +74 -0
- reboost-0.6.1/src/reboost/math/stats.py +119 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/cli.py +45 -24
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/convolve.py +29 -18
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/create.py +46 -17
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/evt.py +43 -20
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/shape/group.py +38 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/utils.py +10 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/PKG-INFO +3 -3
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/SOURCES.txt +2 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/requires.txt +2 -2
- reboost-0.6.1/tests/evt/test_evt.py +60 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hit/test_build_hit.py +4 -2
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_dt_heuristic.py +1 -1
- {reboost-0.5.5 → reboost-0.6.1}/tests/test_core.py +53 -1
- {reboost-0.5.5 → reboost-0.6.1}/tests/test_math.py +25 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/test_optmap.py +32 -11
- reboost-0.6.1/tests/test_optmap_dets.gdml +24 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/test_shape.py +20 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/test_utils.py +10 -3
- reboost-0.5.5/src/reboost/build_evt.py +0 -166
- reboost-0.5.5/src/reboost/math/stats.py +0 -57
- {reboost-0.5.5 → reboost-0.6.1}/LICENSE +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/README.md +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/setup.cfg +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/build_glm.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/build_hit.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/cli.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/hpge/__init__.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/hpge/psd.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/hpge/surface.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/hpge/utils.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/iterator.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/log_utils.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/math/__init__.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/math/functions.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/__init__.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/mapview.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/numba_pdg.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/optmap/optmap.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/profile.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/shape/__init__.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/shape/cluster.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/shape/reduction.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost/units.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/dependency_links.txt +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/entry_points.txt +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/not-zip-safe +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/src/reboost.egg-info/top_level.txt +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/conftest.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/glm/test_build_glm.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/args.yaml +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/basic.yaml +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/geom.gdml +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/hit_config.yaml +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/pars.yaml +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hit/configs/reshape.yaml +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/simulation/gammas.mac +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/simulation/geometry.gdml +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/simulation/make_dt_map.jl +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/simulation/make_geom.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_current.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_files/drift_time_maps.lh5 +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_files/internal_electron.lh5 +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_hpge_map.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_r90.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/hpge/test_surface.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/test_cli.py +0 -0
- {reboost-0.5.5 → reboost-0.6.1}/tests/test_profile.py +0 -0
- {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.
|
|
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 = {"
|
|
14
|
+
lh5.settings.DEFAULT_HDF5_SETTINGS = {"compression": hdf5plugin.Zstd()}
|
|
@@ -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
|
-
|
|
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
|
|
39
|
-
evt_parser = subparsers.add_parser("evt", help="build evt file from remage
|
|
40
|
-
evt_parser.
|
|
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
|
|
43
|
-
required=True,
|
|
47
|
+
help="file with detector ids of all optical channels.",
|
|
44
48
|
)
|
|
45
|
-
evt_parser.add_argument("input", help="input
|
|
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=
|
|
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(
|
|
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="
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
28
|
-
det_ntuples = [m for m in maps if
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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 = "
|
|
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=
|
|
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 = "
|
|
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
|
-
|
|
365
|
-
|
|
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)
|