Skip to content

Commit 3eb9781

Browse files
pcorcamjlenain
andauthored
Add HiLo correction (#303)
* Add linearity range for HiLo correction (https://arxiv.org/abs/2311.11631) * Initial commit of `HiLoComponent` * Initial commit of `HiLoNectarCAMCalibrationTool` * update `__init__` files * add example file * remove `import logging` since we can use `self.logging` for any `Tool` or `Component` * update logger format * add unit tests * We assume the `$NECTARCAMDATA` environment variable is already set up in one's configuration * Update example script so that it can be run stand-alone. Made sure the `extractor_kwargs` for the charge are the same for the `gain_tool` and `hilo_tool`. * Add a description to the tool --------- Co-authored-by: Jean-Philippe Lenain <jlenain@in2p3.fr>
1 parent 265b63a commit 3eb9781

9 files changed

Lines changed: 492 additions & 0 deletions

File tree

src/nectarchain/makers/calibration/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
FlatFieldSPEHHVStdNectarCAMCalibrationTool,
66
FlatFieldSPENominalNectarCAMCalibrationTool,
77
FlatFieldSPENominalStdNectarCAMCalibrationTool,
8+
HiLoNectarCAMCalibrationTool,
89
PhotoStatisticNectarCAMCalibrationTool,
910
)
1011
from .pedestal_makers import PedestalNectarCAMCalibrationTool
@@ -18,4 +19,5 @@
1819
"FlatFieldSPENominalStdNectarCAMCalibrationTool",
1920
"PedestalNectarCAMCalibrationTool",
2021
"PhotoStatisticNectarCAMCalibrationTool",
22+
"HiLoNectarCAMCalibrationTool",
2123
]

src/nectarchain/makers/calibration/gain/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
FlatFieldSPENominalNectarCAMCalibrationTool,
66
FlatFieldSPENominalStdNectarCAMCalibrationTool,
77
)
8+
from .hilo_makers import HiLoNectarCAMCalibrationTool
89
from .photostat_makers import PhotoStatisticNectarCAMCalibrationTool
910

1011
# from .white_target_spe_makers import *
@@ -16,4 +17,5 @@
1617
"FlatFieldSPEHHVStdNectarCAMCalibrationTool",
1718
"FlatFieldSPECombinedStdNectarCAMCalibrationTool",
1819
"PhotoStatisticNectarCAMCalibrationTool",
20+
"HiLoNectarCAMCalibrationTool",
1921
]
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import numpy as np
2+
from ctapipe.core.traits import ComponentNameList
3+
4+
from nectarchain.makers.component import NectarCAMComponent
5+
6+
from ....data.container import (
7+
ChargesContainer,
8+
ChargesContainers,
9+
merge_map_ArrayDataContainer,
10+
)
11+
from ....data.management import DataManagement
12+
from ...component import ArrayDataComponent
13+
from ...extractor.utils import CtapipeExtractor
14+
from .core import GainNectarCAMCalibrationTool
15+
16+
__all__ = ["HiLoNectarCAMCalibrationTool"]
17+
18+
19+
class HiLoNectarCAMCalibrationTool(GainNectarCAMCalibrationTool):
20+
name = "HiLoNectarCAMCalibrationTool"
21+
description = (
22+
"Calibrate the gain of the low-gain channel. "
23+
"Do so by computing the HiLo ratio in the regime where the gain is linear."
24+
)
25+
26+
componentsList = ComponentNameList(
27+
NectarCAMComponent,
28+
default_value=["HiLoComponent"],
29+
help="List of Component names to be apply, the order will be respected",
30+
).tag(config=True)
31+
32+
def __init__(self, *args, **kwargs):
33+
super().__init__(*args, **kwargs)
34+
35+
def _init_output_path(self):
36+
if self.gain_file is not None:
37+
self.output_path = self.gain_file.with_name(
38+
f"{self.gain_file.stem}_hilo_corrected{self.gain_file.suffix}"
39+
)
40+
else:
41+
# The HiLoComponent will raise an error if no gain_file is provided
42+
super()._init_output_path()
43+
44+
def start(self, n_events=np.inf, *args, **kwargs):
45+
str_extractor_kwargs = CtapipeExtractor.get_extractor_kwargs_str(
46+
method=self.method,
47+
extractor_kwargs=self.extractor_kwargs,
48+
)
49+
try:
50+
files = DataManagement.find_charges(
51+
run_number=self.run_number,
52+
method=self.method,
53+
str_extractor_kwargs=str_extractor_kwargs,
54+
max_events=self.max_events,
55+
)
56+
except Exception as e:
57+
self.log.warning(e)
58+
files = []
59+
if self.reload_events or len(files) != 1:
60+
if len(files) != 1:
61+
self.log.info(
62+
f"{len(files)} computed charges files found with max_events >"
63+
f"{self.max_events} for run {self.run_number} with extraction"
64+
f"method {self.method} and {str_extractor_kwargs},\n reload"
65+
f"charges from event loop"
66+
)
67+
super().start(
68+
n_events=n_events,
69+
restart_from_begining=False,
70+
*args,
71+
**kwargs,
72+
)
73+
else:
74+
self.log.info(f"reading computed charge from files {files[0]}")
75+
chargesContainers = ChargesContainers.from_hdf5(files[0])
76+
if isinstance(chargesContainers, ChargesContainer):
77+
self.components[0]._chargesContainers = chargesContainers
78+
else:
79+
n_slices = 0
80+
try:
81+
while True:
82+
next(chargesContainers)
83+
n_slices += 1
84+
except StopIteration:
85+
pass
86+
chargesContainers = ChargesContainers.from_hdf5(files[0])
87+
if n_slices == 1:
88+
self.log.info("merging along TriggerType")
89+
self.components[
90+
0
91+
]._chargesContainers = merge_map_ArrayDataContainer(
92+
next(chargesContainers)
93+
)
94+
else:
95+
self.log.info("merging along slices")
96+
chargesContainers_merged_along_slices = (
97+
ArrayDataComponent.merge_along_slices(
98+
containers_generator=chargesContainers
99+
)
100+
)
101+
self.log.info("merging along TriggerType")
102+
self.components[
103+
0
104+
]._chargesContainers = merge_map_ArrayDataContainer(
105+
chargesContainers_merged_along_slices
106+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from nectarchain.makers.calibration.gain import HiLoNectarCAMCalibrationTool
6+
7+
8+
class TestHiLoNectarCAMCalibrationTool:
9+
@pytest.fixture
10+
def instance(self):
11+
tool = HiLoNectarCAMCalibrationTool()
12+
return tool
13+
14+
def test_init_output_path(self, instance):
15+
instance.gain_file = Path("/tmp/gain.h5")
16+
instance._init_output_path()
17+
assert instance.output_path == Path("/tmp/gain_hilo_corrected.h5")

src/nectarchain/makers/component/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
FlatFieldSingleNominalSPEStdNectarCAMComponent,
1010
)
1111
from .gain_component import GainNectarCAMComponent
12+
from .hilo_component import HiLoComponent
1213
from .pedestal_component import PedestalEstimationComponent
1314
from .photostatistic_algorithm import PhotoStatisticAlgorithm
1415
from .photostatistic_component import PhotoStatisticNectarCAMComponent
@@ -42,4 +43,5 @@
4243
"PhotoStatisticAlgorithm",
4344
"GainNectarCAMComponent",
4445
"FlatFieldComponent",
46+
"HiLoComponent",
4547
]
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import copy
2+
3+
import numpy as np
4+
from ctapipe.containers import EventType
5+
from ctapipe.core.traits import Path
6+
from ctapipe_io_nectarcam.containers import NectarCAMDataContainer
7+
8+
from ...data.container import (
9+
PhotostatContainer,
10+
SPEfitContainer,
11+
merge_map_ArrayDataContainer,
12+
)
13+
from ...makers.component import ChargesComponent, GainNectarCAMComponent
14+
from ...utils import ComponentUtils, ContainerUtils
15+
from ...utils.constants import GAIN_DEFAULT, GAIN_LINEAR_RANGE, HILO_DEFAULT
16+
17+
GAIN_CONTAINER_CLASSES = [PhotostatContainer, SPEfitContainer]
18+
19+
20+
class HiLoComponent(GainNectarCAMComponent):
21+
"""
22+
Component that computes the HiLo ratio.
23+
"""
24+
25+
gain_file = Path(
26+
default_value=None,
27+
help="Path to h5 file with gain calibration coefficients",
28+
allow_none=True,
29+
).tag(config=True)
30+
31+
SubComponents = copy.deepcopy(GainNectarCAMComponent.SubComponents)
32+
SubComponents.default_value = [
33+
"ChargesComponent",
34+
]
35+
SubComponents.read_only = True
36+
37+
def __init__(self, subarray, config=None, parent=None, *args, **kwargs):
38+
chargesComponent_kwargs = {}
39+
other_kwargs = {}
40+
chargesComponent_configurable_traits = ComponentUtils.get_configurable_traits(
41+
ChargesComponent
42+
)
43+
for key in kwargs.keys():
44+
if key in chargesComponent_configurable_traits.keys():
45+
chargesComponent_kwargs[key] = kwargs[key]
46+
else:
47+
other_kwargs[key] = kwargs[key]
48+
49+
super().__init__(
50+
subarray=subarray, config=config, parent=parent, *args, **other_kwargs
51+
)
52+
53+
self.chargesComponent = ChargesComponent(
54+
subarray=subarray,
55+
config=config,
56+
parent=parent,
57+
*args,
58+
**chargesComponent_kwargs,
59+
)
60+
self._chargesContainer = None
61+
self.log.debug(f"{kwargs.keys()}")
62+
63+
self._init_gain_container()
64+
65+
def _init_gain_container(self):
66+
self.__gain_container = None
67+
68+
if self.gain_file is None:
69+
raise ValueError("Need to provide a gain_file to compute HiLo ratio")
70+
71+
try:
72+
self.__gain_container = ContainerUtils.get_container_from_hdf5(
73+
self.gain_file,
74+
GAIN_CONTAINER_CLASSES,
75+
)
76+
ContainerUtils.add_missing_pixels_to_container(
77+
self.__gain_container, pad_value=GAIN_DEFAULT
78+
)
79+
except Exception as e:
80+
raise RuntimeError(
81+
f"Failed to initialize gain container from {self.gain_file}"
82+
) from e
83+
84+
def __call__(self, event: NectarCAMDataContainer, *args, **kwargs):
85+
# For now only flat-field events, to be updated for e.g. white-target
86+
if event.trigger.event_type == EventType.FLATFIELD:
87+
self.chargesComponent(event=event, *args, **kwargs)
88+
89+
def finish(self, *args, **kwargs):
90+
if self._chargesContainer is None:
91+
chargesContainers = self.chargesComponent.finish(*args, **kwargs)
92+
self._chargesContainer = merge_map_ArrayDataContainer(chargesContainers)
93+
94+
self._compute_low_gain()
95+
96+
return self.__gain_container
97+
98+
def _compute_low_gain(self):
99+
ContainerUtils.add_missing_pixels_to_container(self._chargesContainer)
100+
charges_hg = self._chargesContainer["charges_hg"]
101+
charges_lg = self._chargesContainer["charges_lg"]
102+
high_gain = self.__gain_container["high_gain"][:, 0]
103+
104+
# Mask the linear regime between low gain and high gain
105+
charges_hg_pe = charges_hg / high_gain
106+
mask_linearity = np.logical_and(
107+
charges_hg_pe > np.min(GAIN_LINEAR_RANGE),
108+
charges_hg_pe < np.max(GAIN_LINEAR_RANGE),
109+
)
110+
111+
# Add failsafe to not divide by 0
112+
mask = np.logical_and(mask_linearity, charges_lg > 0)
113+
114+
# Compute HiLo ratio (per pixel) for each event
115+
hilo_ratio_all_events = np.divide(
116+
charges_hg,
117+
charges_lg,
118+
where=mask,
119+
out=np.full_like(charges_hg, np.nan, dtype=float),
120+
)
121+
122+
# Compute HiLo ratio (per pixel) averaged over all events
123+
hilo_ratio = np.nanmean(hilo_ratio_all_events, axis=0)
124+
125+
# Set default values if all events were masked
126+
hilo_ratio = np.where(np.isnan(hilo_ratio), HILO_DEFAULT, hilo_ratio)
127+
128+
# Fill gain container
129+
self.__gain_container["low_gain"][:, 0] = high_gain / hilo_ratio

0 commit comments

Comments
 (0)