Skip to content

Commit 37305c6

Browse files
authored
Merge pull request #280 from cta-observatory/edep_cuts_irfs
Implement custom energy-dependent cuts for IRFs and DL3 creations
2 parents 5e4f46b + fa3c092 commit 37305c6

File tree

6 files changed

+185
-19
lines changed

6 files changed

+185
-19
lines changed

magicctapipe/io/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .io import (
1616
check_input_list,
1717
format_object,
18+
get_custom_cuts,
1819
get_dl2_mean,
1920
get_stereo_events,
2021
get_stereo_events_old,
@@ -40,6 +41,7 @@
4041
"create_gti_hdu",
4142
"create_pointing_hdu",
4243
"format_object",
44+
"get_custom_cuts",
4345
"get_dl2_mean",
4446
"get_stereo_events",
4547
"get_stereo_events_old",

magicctapipe/io/io.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pyirf.binning import join_bin_lo_hi
2323
from pyirf.simulations import SimulatedEventsInfo
2424
from pyirf.utils import calculate_source_fov_offset, calculate_theta
25+
from scipy.interpolate import interp1d
2526

2627
try:
2728
from importlib.resources import files
@@ -33,6 +34,7 @@
3334
__all__ = [
3435
"check_input_list",
3536
"format_object",
37+
"get_custom_cuts",
3638
"get_dl2_mean",
3739
"get_stereo_events",
3840
"get_stereo_events_old",
@@ -249,6 +251,53 @@ def format_object(input_object):
249251
return string
250252

251253

254+
def get_custom_cuts(config, energy_bins):
255+
"""
256+
Extract and interpolate custom, energy-dependent cuts for the IRFs creation.
257+
258+
Parameters
259+
----------
260+
config : dict
261+
Dictionary containing the custom cuts and the interpolation strategy.
262+
Required entries are a sub-dictionary 'custom_cuts' and a field
263+
'interpolate_kind' used in scipy.interpolate.interp1d.
264+
energy_bins : astropy.units.Quantity
265+
Energy bin edges targeted in the IRFs
266+
267+
Returns
268+
-------
269+
astropy.table.QTable
270+
Custom cut table in target 'energy_bins'
271+
"""
272+
cut_table = QTable()
273+
interpolate_kind = config["interpolate_kind"]
274+
energy_bins_cuts = config["custom_cuts"]["energy_bins"] * u.Unit(
275+
config["custom_cuts"]["energy_unit"]
276+
)
277+
energy_bins_cuts_center = 0.5 * (energy_bins_cuts[:-1] + energy_bins_cuts[1:]).to(
278+
energy_bins.unit
279+
)
280+
cuts = config["custom_cuts"]["cut"] * u.Unit(config["custom_cuts"]["cut_unit"])
281+
282+
cut_table["low"] = energy_bins[:-1]
283+
cut_table["high"] = energy_bins[1:]
284+
cut_table["center"] = 0.5 * (cut_table["low"] + cut_table["high"])
285+
f_cut = interp1d(
286+
energy_bins_cuts_center,
287+
cuts,
288+
kind=interpolate_kind,
289+
bounds_error=False,
290+
fill_value=(cuts[0], cuts[-1]),
291+
assume_sorted=True,
292+
)
293+
294+
cut_table["cut"] = f_cut(cut_table["center"]) * u.Unit(
295+
config["custom_cuts"]["cut_unit"]
296+
)
297+
298+
return cut_table
299+
300+
252301
def get_stereo_events_old(
253302
event_data, quality_cuts=None, group_index=["obs_id", "event_id"]
254303
):

magicctapipe/io/tests/test_io.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import glob
22

3+
import astropy.table
4+
import astropy.units as u
35
import numpy as np
46
import pandas as pd
57
import pytest
8+
import yaml
69
from ctapipe_io_lst import REFERENCE_LOCATION
710

811
from magicctapipe.io.io import (
912
check_input_list,
1013
format_object,
14+
get_custom_cuts,
1115
get_dl2_mean,
1216
get_stereo_events,
1317
load_dl2_data_file,
@@ -152,6 +156,41 @@ def test_check_input_list(self):
152156
}
153157
)
154158

159+
def test_get_custom_cuts(self, config):
160+
with open(config, "rb") as f:
161+
config = yaml.safe_load(f)
162+
# Create energy bins
163+
config_eng_bins = config["create_irf"]["energy_bins"]
164+
eng_bins_start = u.Quantity(config_eng_bins["start"])
165+
eng_bins_stop = u.Quantity(config_eng_bins["stop"])
166+
167+
energy_bins = u.TeV * np.geomspace(
168+
start=eng_bins_start.to_value("TeV").round(3),
169+
stop=eng_bins_stop.to_value("TeV").round(3),
170+
num=config_eng_bins["n_edges"],
171+
)
172+
173+
# Load the cuts configs. 'cut_type' will be ignored
174+
config_gh_irf = config["create_irf"]["gammaness"]
175+
config_theta_irf = config["create_irf"]["theta"]
176+
177+
# Get the custom cuts without interpolating on energy_bins
178+
cut_table_gh = get_custom_cuts(config_gh_irf, energy_bins)
179+
cut_table_theta = get_custom_cuts(config_theta_irf, energy_bins)
180+
for cut_table in (cut_table_gh, cut_table_theta):
181+
assert isinstance(cut_table, astropy.table.QTable)
182+
assert cut_table["low"].unit == "TeV"
183+
assert cut_table["high"].unit == "TeV"
184+
assert cut_table["center"].unit == "TeV"
185+
assert cut_table_gh["cut"].unit == u.dimensionless_unscaled
186+
assert cut_table_gh["cut"][15] == 0.5
187+
assert cut_table_theta["cut"][15] == 0.3 * u.deg
188+
189+
# Get the custom cuts with linear interpolating on energy_bins
190+
config_gh_irf["interpolate_kind"] = "linear"
191+
cut_table_gh = get_custom_cuts(config_gh_irf, energy_bins)
192+
assert np.isclose(cut_table_gh["cut"][15].value, 0.5074, atol=0.0001)
193+
155194
def test_telescope_combinations(self, config_gen, config_gen_4lst):
156195
"""
157196
Simple check on telescope combinations

magicctapipe/resources/config.yaml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,18 +256,34 @@ create_irf:
256256
n_edges: 21
257257

258258
gammaness:
259-
cut_type: "dynamic" # select "global" or "dynamic"
259+
cut_type: "dynamic" # select "global" or "dynamic" or "custom"
260260
global_cut_value: 0.8 # used for the global cut
261261
efficiency: 0.9 # used for the dynamic cuts
262262
min_cut: 0.05 # used for the dynamic cuts
263263
max_cut: 0.85 # used for the dynamic cuts
264+
custom_cuts: { # used for custom cuts
265+
energy_bins: [0.01, 0.1, 1, 10, 100],
266+
energy_unit: "TeV",
267+
cut: [0.4, 0.45, 0.50, 0.55],
268+
cut_unit: "",
269+
}
270+
interpolate_kind: "nearest" # used for custom cuts, option for scipy.interpolate.interp1d
271+
# Standard values are "nearest", "linear" and "cubic"
264272

265273
theta:
266-
cut_type: "dynamic" # select "global" or "dynamic"
274+
cut_type: "dynamic" # select "global" or "dynamic" or "custom"
267275
global_cut_value: "0.2 deg" # used for the global cut
268276
efficiency: 0.75 # used for the dynamic cuts
269277
min_cut: "0.1 deg" # used for the dynamic cuts
270278
max_cut: "0.25 deg" # used for the dynamic cuts
279+
custom_cuts: { # used for custom cuts
280+
energy_bins: [0.01, 0.1, 1, 10, 100],
281+
energy_unit: "TeV",
282+
cut: [0.4, 0.35, 0.30, 0.25],
283+
cut_unit: "deg",
284+
}
285+
interpolate_kind: "nearest" # used for custom cuts, option for scipy.interpolate.interp1d
286+
# Standard values are "nearest", "linear" and "cubic"
271287

272288

273289
dl2_to_dl3:

magicctapipe/resources/test_config.yaml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,19 +226,34 @@ create_irf:
226226
n_edges: 21
227227

228228
gammaness:
229-
cut_type: "dynamic" # select "global" or "dynamic"
229+
cut_type: "dynamic" # select "global" or "dynamic" or "custom"
230230
global_cut_value: 0.8 # used for the global cut
231231
efficiency: 0.9 # used for the dynamic cuts
232232
min_cut: 0.05 # used for the dynamic cuts
233233
max_cut: 0.85 # used for the dynamic cuts
234+
custom_cuts: { # used for custom cuts
235+
energy_bins: [0.01, 0.1, 1, 10, 100],
236+
energy_unit: "TeV",
237+
cut: [0.4, 0.45, 0.50, 0.55],
238+
cut_unit: "",
239+
}
240+
interpolate_kind: "nearest" # used for custom cuts, option for scipy.interpolate.interp1d
241+
# Standard values are "nearest", "linear" and "cubic"
234242

235243
theta:
236-
cut_type: "dynamic" # select "global" or "dynamic"
244+
cut_type: "dynamic" # select "global" or "dynamic" or "custom"
237245
global_cut_value: "0.2 deg" # used for the global cut
238246
efficiency: 0.75 # used for the dynamic cuts
239247
min_cut: "0.1 deg" # used for the dynamic cuts
240248
max_cut: "0.3 deg" # used for the dynamic cuts
241-
249+
custom_cuts: { # used for custom cuts
250+
energy_bins: [0.01, 0.1, 1, 10, 100],
251+
energy_unit: "TeV",
252+
cut: [0.4, 0.35, 0.30, 0.25],
253+
cut_unit: "deg",
254+
}
255+
interpolate_kind: "nearest" # used for custom cuts, option for scipy.interpolate.interp1d
256+
# Standard values are "nearest", "linear" and "cubic"
242257

243258
dl2_to_dl3:
244259
interpolation_method: "nearest" # select "nearest", "linear" or "cubic"

magicctapipe/scripts/lst1_magic/lst1_magic_create_irf.py

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@
2323
The "software(_only_3tel)" types are supposed for the software event
2424
coincidence with LST-mono and MAGIC-stereo observations, allowing for
2525
only the events triggering both M1 and M2. The "software" type allows
26-
for the events of the any 2-tel combinations except the MAGIC-stereo
26+
for the events of any 2-tel combinations except the MAGIC-stereo
2727
combination at the moment. The "software_only_3tel" type allows for only
2828
the events of the 3-tel combination. The "magic_only" type allows for
2929
only the events of the MAGIC-stereo combination.
3030
31-
There are two types of gammaness and theta cuts, "global" and "dynamic".
31+
There are three types of gammaness and theta cuts, "global", "dynamic" and
32+
"custom".
3233
In case of the dynamic cuts, the optimal cut satisfying a given
3334
efficiency will be calculated for every energy bin.
35+
In case of custom cuts, the provided cuts with be applied to the
36+
target energy binning using the selected scipy.interpolate.interpd1d method.
3437
3538
Usage:
3639
$ python lst1_magic_create_irf.py
@@ -74,7 +77,12 @@
7477
calculate_event_weights,
7578
)
7679

77-
from magicctapipe.io import create_gh_cuts_hdu, format_object, load_mc_dl2_data_file
80+
from magicctapipe.io import (
81+
create_gh_cuts_hdu,
82+
format_object,
83+
get_custom_cuts,
84+
load_mc_dl2_data_file,
85+
)
7886

7987
__all__ = ["create_irf"]
8088

@@ -110,6 +118,9 @@ def create_irf(
110118
If the input type of gammaness or theta cut is not known
111119
"""
112120

121+
dynamic_keys = ["efficiency", "min_cut", "max_cut"]
122+
custom_keys = ["custom_cuts", "interpolate_kind"]
123+
113124
config_irf = config["create_irf"]
114125

115126
quality_cuts = config_irf["quality_cuts"]
@@ -336,10 +347,12 @@ def create_irf(
336347
event_table_bkg = event_table_bkg[mask_gh]
337348

338349
elif cut_type_gh == "dynamic":
339-
config_gh_cuts.pop("global_cut_value", None)
340-
341350
logger.info("\nDynamic gammaness cuts:")
342-
logger.info(format_object(config_gh_cuts))
351+
logger.info(
352+
format_object(
353+
{k: config_gh_cuts[k] for k in dynamic_keys if k in config_gh_cuts}
354+
)
355+
)
343356

344357
gh_efficiency = config_gh_cuts["efficiency"]
345358
gh_cut_min = config_gh_cuts["min_cut"]
@@ -364,6 +377,20 @@ def create_irf(
364377
max_value=gh_cut_max,
365378
)
366379

380+
elif cut_type_gh == "custom":
381+
cut_table_gh = get_custom_cuts(config_gh_cuts, energy_bins)
382+
extra_header["GH_CUTS"] = "Custom energy-dependent"
383+
output_suffix = "gh_custom"
384+
logger.info("\nCustom gammaness cuts.")
385+
logger.info(
386+
format_object(
387+
{k: config_gh_cuts[k] for k in custom_keys if k in config_gh_cuts}
388+
)
389+
)
390+
else:
391+
raise ValueError(f"Unknown gammaness-cut type '{cut_type_gh}'.")
392+
393+
if cut_type_gh == "dynamic" or "custom":
367394
logger.info(f"\nGammaness-cut table:\n\n{cut_table_gh}")
368395

369396
# Apply the dynamic gammaness cuts
@@ -401,9 +428,6 @@ def create_irf(
401428

402429
irf_hdus.append(hdu_gh_cuts)
403430

404-
else:
405-
raise ValueError(f"Unknown gammaness-cut type '{cut_type_gh}'.")
406-
407431
if is_point_like:
408432
# Apply the theta cut
409433
config_theta_cuts = config_irf["theta"]
@@ -423,10 +447,17 @@ def create_irf(
423447
event_table_gamma = event_table_gamma[mask_theta]
424448

425449
elif cut_type_theta == "dynamic":
426-
config_theta_cuts.pop("global_cut_value", None)
427450

428451
logger.info("\nDynamic theta cuts:")
429-
logger.info(format_object(config_theta_cuts))
452+
logger.info(
453+
format_object(
454+
{
455+
k: config_theta_cuts[k]
456+
for k in dynamic_keys
457+
if k in config_theta_cuts
458+
}
459+
)
460+
)
430461

431462
theta_efficiency = config_theta_cuts["efficiency"]
432463
theta_cut_min = u.Quantity(config_theta_cuts["min_cut"])
@@ -450,7 +481,24 @@ def create_irf(
450481
min_value=theta_cut_min,
451482
max_value=theta_cut_max,
452483
)
484+
elif cut_type_theta == "custom":
485+
cut_table_theta = get_custom_cuts(config_theta_cuts, energy_bins)
486+
extra_header["THETA_CUTS"] = "Custom energy-dependent"
487+
output_suffix += "_theta_custom"
488+
logger.info("\nCustom theta cuts.")
489+
logger.info(
490+
format_object(
491+
{
492+
k: config_theta_cuts[k]
493+
for k in custom_keys
494+
if k in config_theta_cuts
495+
}
496+
)
497+
)
498+
else:
499+
raise ValueError(f"Unknown theta-cut type '{cut_type_theta}'.")
453500

501+
if cut_type_theta == "dynamic" or "custom":
454502
logger.info(f"\nTheta-cut table:\n\n{cut_table_theta}")
455503

456504
# Apply the dynamic theta cuts
@@ -479,9 +527,6 @@ def create_irf(
479527

480528
irf_hdus.append(hdu_rad_max)
481529

482-
else:
483-
raise ValueError(f"Unknown theta-cut type '{cut_type_theta}'.")
484-
485530
# Create an effective-area HDU
486531
logger.info("\nCreating an effective-area HDU...")
487532

0 commit comments

Comments
 (0)