CTLearn 0.8.0__py3-none-any.whl
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.
- ctlearn/__init__.py +8 -0
- ctlearn/build_irf.py +676 -0
- ctlearn/data_loader.py +221 -0
- ctlearn/default_config_files/CNNRNN.yml +57 -0
- ctlearn/default_config_files/SingleCNN.yml +56 -0
- ctlearn/default_config_files/TRN.yml +54 -0
- ctlearn/default_config_files/calwaveSingleCNN.yml +58 -0
- ctlearn/default_config_files/calwaveTRN.yml +56 -0
- ctlearn/default_config_files/mergedTRN.yml +60 -0
- ctlearn/default_config_files/rawwaveSingleCNN.yml +58 -0
- ctlearn/default_config_files/rawwaveTRN.yml +56 -0
- ctlearn/default_models/attention.py +97 -0
- ctlearn/default_models/basic.py +175 -0
- ctlearn/default_models/cnn_rnn.py +78 -0
- ctlearn/default_models/head.py +63 -0
- ctlearn/default_models/resnet.py +296 -0
- ctlearn/default_models/single_cnn.py +123 -0
- ctlearn/default_models/variable_input_model.py +193 -0
- ctlearn/output_handler.py +271 -0
- ctlearn/run_model.py +787 -0
- ctlearn/utils.py +226 -0
- ctlearn/version.py +188 -0
- ctlearn-0.8.0.dist-info/LICENSE +29 -0
- ctlearn-0.8.0.dist-info/METADATA +173 -0
- ctlearn-0.8.0.dist-info/RECORD +28 -0
- ctlearn-0.8.0.dist-info/WHEEL +5 -0
- ctlearn-0.8.0.dist-info/entry_points.txt +3 -0
- ctlearn-0.8.0.dist-info/top_level.txt +1 -0
ctlearn/__init__.py
ADDED
ctlearn/build_irf.py
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build IRFs and sensitivity curves from CTLearn DL2-like files using pyirf.
|
|
3
|
+
Edited from pyirf examples (Credits Noethe et al.):
|
|
4
|
+
https://github.com/cta-observatory/pyirf/blob/master/examples/calculate_eventdisplay_irfs.py
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import glob
|
|
8
|
+
import logging
|
|
9
|
+
import operator
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from astropy import table
|
|
14
|
+
from astropy.table import QTable, MaskedColumn
|
|
15
|
+
import astropy.units as u
|
|
16
|
+
from astropy.io import fits
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from pyirf.binning import (
|
|
20
|
+
bin_center,
|
|
21
|
+
create_bins_per_decade,
|
|
22
|
+
add_overflow_bins,
|
|
23
|
+
create_histogram_table,
|
|
24
|
+
)
|
|
25
|
+
from pyirf.cuts import calculate_percentile_cut, evaluate_binned_cut
|
|
26
|
+
from pyirf.sensitivity import calculate_sensitivity, estimate_background
|
|
27
|
+
from pyirf.simulations import SimulatedEventsInfo
|
|
28
|
+
from pyirf.utils import calculate_theta, calculate_source_fov_offset
|
|
29
|
+
from pyirf.benchmarks import energy_bias_resolution, angular_resolution
|
|
30
|
+
|
|
31
|
+
from pyirf.spectral import (
|
|
32
|
+
calculate_event_weights,
|
|
33
|
+
PowerLaw,
|
|
34
|
+
CRAB_HEGRA,
|
|
35
|
+
IRFDOC_PROTON_SPECTRUM,
|
|
36
|
+
IRFDOC_ELECTRON_SPECTRUM,
|
|
37
|
+
)
|
|
38
|
+
from pyirf.cut_optimization import optimize_gh_cut
|
|
39
|
+
|
|
40
|
+
from pyirf.irf import (
|
|
41
|
+
effective_area_per_energy,
|
|
42
|
+
energy_dispersion,
|
|
43
|
+
psf_table,
|
|
44
|
+
background_2d,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
from pyirf.io import (
|
|
48
|
+
create_aeff2d_hdu,
|
|
49
|
+
create_psf_table_hdu,
|
|
50
|
+
create_energy_dispersion_hdu,
|
|
51
|
+
create_rad_max_hdu,
|
|
52
|
+
create_background_2d_hdu,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
log = logging.getLogger("pyirf")
|
|
57
|
+
|
|
58
|
+
# Map the particle ids to the particle information
|
|
59
|
+
particles = {
|
|
60
|
+
0: {
|
|
61
|
+
"name": "gamma",
|
|
62
|
+
"target_spectrum": CRAB_HEGRA,
|
|
63
|
+
"mc_header": pd.DataFrame(),
|
|
64
|
+
"events": QTable(),
|
|
65
|
+
},
|
|
66
|
+
101: {
|
|
67
|
+
"name": "proton",
|
|
68
|
+
"target_spectrum": IRFDOC_PROTON_SPECTRUM,
|
|
69
|
+
"mc_header": pd.DataFrame(),
|
|
70
|
+
"events": QTable(),
|
|
71
|
+
},
|
|
72
|
+
1: {
|
|
73
|
+
"name": "electron",
|
|
74
|
+
"target_spectrum": IRFDOC_ELECTRON_SPECTRUM,
|
|
75
|
+
"mc_header": pd.DataFrame(),
|
|
76
|
+
"events": QTable(),
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Map column names
|
|
81
|
+
name_mapping = {
|
|
82
|
+
"gammaness": "gh_score",
|
|
83
|
+
"source_alt": "true_alt",
|
|
84
|
+
"source_az": "true_az",
|
|
85
|
+
}
|
|
86
|
+
# Map units
|
|
87
|
+
unit_mapping = {
|
|
88
|
+
"true_energy": u.TeV,
|
|
89
|
+
"reco_energy": u.TeV,
|
|
90
|
+
"pointing_alt": u.rad,
|
|
91
|
+
"pointing_az": u.rad,
|
|
92
|
+
"true_alt": u.rad,
|
|
93
|
+
"true_az": u.rad,
|
|
94
|
+
"reco_alt": u.rad,
|
|
95
|
+
"reco_az": u.rad,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main():
|
|
100
|
+
parser = argparse.ArgumentParser(
|
|
101
|
+
description=(
|
|
102
|
+
"Build IRFs and sensitivity curves from CTLearn DL2-like files using pyirf."
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--input",
|
|
107
|
+
"-i",
|
|
108
|
+
help="Input directories; default is ./",
|
|
109
|
+
default=["./"],
|
|
110
|
+
nargs="+",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--pattern",
|
|
114
|
+
"-p",
|
|
115
|
+
help="Pattern to mask unwanted files from the data input directory; default is *.h5",
|
|
116
|
+
default=["*.h5"],
|
|
117
|
+
nargs="+",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--output",
|
|
121
|
+
"-o",
|
|
122
|
+
help="Output file; default is ./pyirf.fits.gz",
|
|
123
|
+
default="./pyirf.fits.gz",
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--energy_range",
|
|
127
|
+
"-e",
|
|
128
|
+
help="Energy range in TeV; default is [0.03, 30.0]",
|
|
129
|
+
default=[0.03, 30.0],
|
|
130
|
+
nargs="+",
|
|
131
|
+
type=float,
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--theta_range",
|
|
135
|
+
"-t",
|
|
136
|
+
help="Theta cut range in deg; default is [0.05, 0.3]",
|
|
137
|
+
default=[0.05, 0.3],
|
|
138
|
+
nargs="+",
|
|
139
|
+
type=float,
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--obstime",
|
|
143
|
+
help="Observation time in hours; default is 50",
|
|
144
|
+
default=50,
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--alpha",
|
|
148
|
+
help="Scaling between on and off region; default is 0.2",
|
|
149
|
+
default=0.2,
|
|
150
|
+
)
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--fov_offset_min",
|
|
153
|
+
help="Minimum distance from the fov center for background events to be taken into account; default is 0.0",
|
|
154
|
+
default=0.0,
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--fov_offset_max",
|
|
158
|
+
help="Maximum distance from the fov center in deg for background events to be taken into account; default is 1.0",
|
|
159
|
+
default=1.0,
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--max_gh_cut_eff",
|
|
163
|
+
help="Maximum gamma/hadron cut efficiency; default is 0.9",
|
|
164
|
+
default=0.9,
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--gh_cut_eff_step",
|
|
168
|
+
help="Gamma/hadron cut efficiency step; default is 0.01",
|
|
169
|
+
default=0.01,
|
|
170
|
+
)
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
"--init_gh_cut_eff",
|
|
173
|
+
help="Initial gamma/hadron cut efficiency; default is 0.4",
|
|
174
|
+
default=0.4,
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"--quality_cuts",
|
|
178
|
+
"-c",
|
|
179
|
+
help="String of the quality cuts",
|
|
180
|
+
type=str,
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"--size_cut",
|
|
184
|
+
"-z",
|
|
185
|
+
help="Minimum size values",
|
|
186
|
+
nargs="+",
|
|
187
|
+
type=float,
|
|
188
|
+
)
|
|
189
|
+
parser.add_argument(
|
|
190
|
+
"--leakage_cut",
|
|
191
|
+
"-l",
|
|
192
|
+
help="Maximum leakage2 intensity values",
|
|
193
|
+
nargs="+",
|
|
194
|
+
type=float,
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"--energy_dependent_gh_efficiency",
|
|
198
|
+
help="Gamma/hadron efficiency cut for an energy dependent energy cut. Valid options: [0.0, 1.0]",
|
|
199
|
+
type=float,
|
|
200
|
+
)
|
|
201
|
+
parser.add_argument(
|
|
202
|
+
"--global_gh_cut",
|
|
203
|
+
help="Gamma/hadron cut for a global cut. Valid options: [0.0, 1.0]",
|
|
204
|
+
type=float,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
args = parser.parse_args()
|
|
208
|
+
|
|
209
|
+
logging.basicConfig(level=logging.INFO)
|
|
210
|
+
logging.getLogger("pyirf").setLevel(logging.DEBUG)
|
|
211
|
+
|
|
212
|
+
T_OBS = args.obstime * u.hour
|
|
213
|
+
|
|
214
|
+
# scaling between on and off region.
|
|
215
|
+
# (Default) Make off region 5 times larger than on region for better
|
|
216
|
+
# background statistics
|
|
217
|
+
ALPHA = args.alpha
|
|
218
|
+
|
|
219
|
+
# Radius to use for calculating bg rate
|
|
220
|
+
FOV_OFFSET_MIN = args.fov_offset_min * u.deg
|
|
221
|
+
FOV_OFFSET_MAX = args.fov_offset_max * u.deg
|
|
222
|
+
MAX_GH_CUT_EFFICIENCY = args.max_gh_cut_eff
|
|
223
|
+
GH_CUT_EFFICIENCY_STEP = args.gh_cut_eff_step
|
|
224
|
+
|
|
225
|
+
# gh cut used for first calculation of the binned theta cuts
|
|
226
|
+
INITIAL_GH_CUT_EFFICENCY = args.init_gh_cut_eff
|
|
227
|
+
|
|
228
|
+
MIN_ENERGY = args.energy_range[0] * u.TeV
|
|
229
|
+
MAX_ENERGY = args.energy_range[-1] * u.TeV
|
|
230
|
+
|
|
231
|
+
MIN_THETA_CUT = args.theta_range[0] * u.deg
|
|
232
|
+
MAX_THETA_CUT = args.theta_range[-1] * u.deg
|
|
233
|
+
|
|
234
|
+
global_tel_ids = []
|
|
235
|
+
n_showers_factor = 1
|
|
236
|
+
for input in args.input:
|
|
237
|
+
abs_file_dir = os.path.abspath(input)
|
|
238
|
+
for pattern in args.pattern:
|
|
239
|
+
files = glob.glob(os.path.join(abs_file_dir, pattern))
|
|
240
|
+
if not files:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
for file in np.sort(files):
|
|
244
|
+
tel_ids = []
|
|
245
|
+
with pd.HDFStore(file, mode="r") as f:
|
|
246
|
+
file_keys = list(f.keys())
|
|
247
|
+
events = f["/dl2/reco"]
|
|
248
|
+
events = events.rename(columns=name_mapping)
|
|
249
|
+
particle_type = int(events["true_shower_primary_id"][0])
|
|
250
|
+
drop_cols = ["event_id", "obs_id", "true_shower_primary_id"]
|
|
251
|
+
for k in [key for key in file_keys if key.startswith("/dl1b/")]:
|
|
252
|
+
tel_ids_string = k.split("/")[-1].replace("tel_", "")
|
|
253
|
+
n_showers_factor = len(tel_ids_string.split("_"))
|
|
254
|
+
tel_ids.append(int(tel_ids_string))
|
|
255
|
+
parameters = f[k].rename(
|
|
256
|
+
lambda x: f"tel_{int(tel_ids_string)}_" + x, axis="columns"
|
|
257
|
+
)
|
|
258
|
+
drop_cols.extend(parameters.keys())
|
|
259
|
+
events = pd.concat([events, parameters], axis=1)
|
|
260
|
+
|
|
261
|
+
if not global_tel_ids:
|
|
262
|
+
global_tel_ids = tel_ids
|
|
263
|
+
else:
|
|
264
|
+
if global_tel_ids != tel_ids:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"Tel ids inconsistent. '{global_tel_ids}' is not equal to '{tel_ids}' from '{file}'."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Apply quality cuts
|
|
270
|
+
mask = None
|
|
271
|
+
if args.quality_cuts:
|
|
272
|
+
mask = args.quality_cuts
|
|
273
|
+
|
|
274
|
+
if args.size_cut:
|
|
275
|
+
for s, size in enumerate(args.size_cut):
|
|
276
|
+
if mask:
|
|
277
|
+
mask += f"& tel_{global_tel_ids[s]}_hillas_intensity > {size} "
|
|
278
|
+
else:
|
|
279
|
+
mask = f"tel_{global_tel_ids[s]}_hillas_intensity > {size} "
|
|
280
|
+
if args.leakage_cut:
|
|
281
|
+
for l, leakage in enumerate(args.leakage_cut):
|
|
282
|
+
if mask:
|
|
283
|
+
mask += f"& tel_{global_tel_ids[l]}_leakage_intensity_width_2 < {leakage} "
|
|
284
|
+
else:
|
|
285
|
+
mask = f"tel_{global_tel_ids[l]}_leakage_intensity_width_2 < {leakage} "
|
|
286
|
+
if mask:
|
|
287
|
+
events.query(mask, inplace=True)
|
|
288
|
+
events = events.drop(drop_cols, axis=1)
|
|
289
|
+
events = table.QTable.from_pandas(events)
|
|
290
|
+
for k, v in unit_mapping.items():
|
|
291
|
+
events[k] *= v
|
|
292
|
+
|
|
293
|
+
particles[particle_type]["events"] = table.vstack(
|
|
294
|
+
[particles[particle_type]["events"], events]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Sims info
|
|
298
|
+
mc_header = f["/info/mc_header"]
|
|
299
|
+
|
|
300
|
+
# Check if ringwobbles then set the viewcone radius to zero
|
|
301
|
+
particles[particle_type]["mc_header"] = pd.concat(
|
|
302
|
+
[particles[particle_type]["mc_header"], mc_header],
|
|
303
|
+
ignore_index=True,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
for particle_type, p in particles.items():
|
|
307
|
+
log.info(f'Simulated {p["name"]} Events:')
|
|
308
|
+
|
|
309
|
+
simulation_info = SimulatedEventsInfo(
|
|
310
|
+
n_showers=int(n_showers_factor * p["mc_header"]["n_showers"].sum()),
|
|
311
|
+
energy_min=u.Quantity(p["mc_header"]["energy_range_min"].min(), u.TeV),
|
|
312
|
+
energy_max=u.Quantity(p["mc_header"]["energy_range_max"].max(), u.TeV),
|
|
313
|
+
spectral_index=p["mc_header"]["spectral_index"][0],
|
|
314
|
+
max_impact=u.Quantity(p["mc_header"]["max_scatter_range"].max(), u.m),
|
|
315
|
+
viewcone=u.Quantity(
|
|
316
|
+
p["mc_header"]["max_viewcone_radius"][0]
|
|
317
|
+
- p["mc_header"]["min_viewcone_radius"][0],
|
|
318
|
+
u.deg,
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
p["simulation_info"] = simulation_info
|
|
322
|
+
p["simulated_spectrum"] = PowerLaw.from_simulation(simulation_info, T_OBS)
|
|
323
|
+
p["events"]["weight"] = MaskedColumn(
|
|
324
|
+
data=calculate_event_weights(
|
|
325
|
+
p["events"]["true_energy"],
|
|
326
|
+
p["target_spectrum"],
|
|
327
|
+
p["simulated_spectrum"],
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
for prefix in ("true", "reco"):
|
|
331
|
+
k = f"{prefix}_source_fov_offset"
|
|
332
|
+
p["events"][k] = calculate_source_fov_offset(p["events"], prefix=prefix)
|
|
333
|
+
|
|
334
|
+
# calculate theta / distance between reco and true direction of the gamma-ray
|
|
335
|
+
p["events"]["theta"] = calculate_theta(
|
|
336
|
+
p["events"],
|
|
337
|
+
assumed_source_az=p["events"]["true_az"],
|
|
338
|
+
assumed_source_alt=p["events"]["true_alt"],
|
|
339
|
+
)
|
|
340
|
+
log.info(simulation_info)
|
|
341
|
+
log.info("")
|
|
342
|
+
|
|
343
|
+
gammas = particles[0]["events"]
|
|
344
|
+
# background table composed of both electrons and protons
|
|
345
|
+
background = table.vstack([particles[101]["events"], particles[1]["events"]])
|
|
346
|
+
|
|
347
|
+
mask_theta_cuts = np.full(len(gammas["theta"]), True)
|
|
348
|
+
if not bool(args.energy_dependent_gh_efficiency) or bool(args.global_gh_cut):
|
|
349
|
+
INITIAL_GH_CUT = np.quantile(gammas["gh_score"], (1 - INITIAL_GH_CUT_EFFICENCY))
|
|
350
|
+
log.info(f"Using fixed G/H cut of {INITIAL_GH_CUT} to calculate theta cuts")
|
|
351
|
+
mask_theta_cuts = gammas["gh_score"] >= INITIAL_GH_CUT
|
|
352
|
+
|
|
353
|
+
# event display uses much finer bins for the theta cut than
|
|
354
|
+
# for the sensitivity
|
|
355
|
+
theta_bins = add_overflow_bins(create_bins_per_decade(MIN_ENERGY, MAX_ENERGY, 50))
|
|
356
|
+
# same bins as event display uses
|
|
357
|
+
sensitivity_bins = add_overflow_bins(
|
|
358
|
+
create_bins_per_decade(MIN_ENERGY, MAX_ENERGY, bins_per_decade=5)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# theta cut is 68 percent containmente of the gammas
|
|
362
|
+
# for now with a fixed global, unoptimized score cut
|
|
363
|
+
# the cut is calculated in the same bins as the sensitivity,
|
|
364
|
+
# but then interpolated to 10x the resolution.
|
|
365
|
+
|
|
366
|
+
theta_cuts_coarse = calculate_percentile_cut(
|
|
367
|
+
gammas["theta"][mask_theta_cuts],
|
|
368
|
+
gammas["reco_energy"][mask_theta_cuts],
|
|
369
|
+
bins=sensitivity_bins,
|
|
370
|
+
min_value=MIN_THETA_CUT,
|
|
371
|
+
fill_value=MAX_THETA_CUT,
|
|
372
|
+
max_value=MAX_THETA_CUT,
|
|
373
|
+
percentile=68,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# interpolate to 50 bins per decade
|
|
377
|
+
theta_center = bin_center(theta_bins)
|
|
378
|
+
inter_center = bin_center(sensitivity_bins)
|
|
379
|
+
theta_cuts = table.QTable(
|
|
380
|
+
{
|
|
381
|
+
"low": theta_bins[:-1],
|
|
382
|
+
"high": theta_bins[1:],
|
|
383
|
+
"center": theta_center,
|
|
384
|
+
"cut": np.interp(
|
|
385
|
+
np.log10(theta_center / u.TeV),
|
|
386
|
+
np.log10(inter_center / u.TeV),
|
|
387
|
+
theta_cuts_coarse["cut"],
|
|
388
|
+
),
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
# binnings for the irfs
|
|
392
|
+
true_energy_bins = add_overflow_bins(
|
|
393
|
+
create_bins_per_decade(MIN_ENERGY, MAX_ENERGY, 5)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
reco_energy_bins = add_overflow_bins(
|
|
397
|
+
create_bins_per_decade(MIN_ENERGY, MAX_ENERGY, 5)
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
gh_center = bin_center(reco_energy_bins)
|
|
401
|
+
inter_center = bin_center(sensitivity_bins)
|
|
402
|
+
|
|
403
|
+
if args.energy_dependent_gh_efficiency:
|
|
404
|
+
|
|
405
|
+
log.info(
|
|
406
|
+
f"Performing a energy dependant G/H cut using gamma efficiency of {args.energy_dependent_gh_efficiency}"
|
|
407
|
+
)
|
|
408
|
+
gh_cuts_coarse = calculate_percentile_cut(
|
|
409
|
+
gammas["gh_score"],
|
|
410
|
+
gammas["reco_energy"],
|
|
411
|
+
bins=sensitivity_bins,
|
|
412
|
+
min_value=0.05,
|
|
413
|
+
max_value=0.95,
|
|
414
|
+
fill_value=gammas["gh_score"].max(),
|
|
415
|
+
percentile=100 * (1 - args.energy_dependent_gh_efficiency),
|
|
416
|
+
smoothing=None,
|
|
417
|
+
min_events=100,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
gh_cuts = table.QTable(
|
|
421
|
+
{
|
|
422
|
+
"low": reco_energy_bins[:-1],
|
|
423
|
+
"high": reco_energy_bins[1:],
|
|
424
|
+
"center": gh_center,
|
|
425
|
+
"cut": np.interp(
|
|
426
|
+
np.log10(gh_center / u.TeV),
|
|
427
|
+
np.log10(inter_center / u.TeV),
|
|
428
|
+
gh_cuts_coarse["cut"],
|
|
429
|
+
),
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
for tab in (gammas, background):
|
|
434
|
+
tab["selected_gh"] = evaluate_binned_cut(
|
|
435
|
+
tab["gh_score"], tab["reco_energy"], gh_cuts, operator.ge
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
gammas["selected_theta"] = evaluate_binned_cut(
|
|
439
|
+
gammas["theta"], gammas["reco_energy"], theta_cuts, operator.le
|
|
440
|
+
)
|
|
441
|
+
gammas["selected"] = gammas["selected_theta"] & gammas["selected_gh"]
|
|
442
|
+
|
|
443
|
+
sensitivities = []
|
|
444
|
+
|
|
445
|
+
gammas_hist = create_histogram_table(
|
|
446
|
+
gammas[gammas["selected"]], sensitivity_bins, "reco_energy"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
background_hist = estimate_background(
|
|
450
|
+
events=background[background["selected_gh"]],
|
|
451
|
+
reco_energy_bins=sensitivity_bins,
|
|
452
|
+
theta_cuts=theta_cuts,
|
|
453
|
+
alpha=ALPHA,
|
|
454
|
+
fov_offset_min=FOV_OFFSET_MIN,
|
|
455
|
+
fov_offset_max=FOV_OFFSET_MAX,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
sensitivity_coarse = calculate_sensitivity(
|
|
459
|
+
gammas_hist, background_hist, alpha=ALPHA
|
|
460
|
+
)
|
|
461
|
+
sensitivities.append(sensitivity_coarse)
|
|
462
|
+
|
|
463
|
+
sensitivity = sensitivities[0].copy()
|
|
464
|
+
for bin_id in range(len(sensitivity_bins) - 1):
|
|
465
|
+
sensitivities_bin = [
|
|
466
|
+
s["relative_sensitivity"][bin_id] for s in sensitivities
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
if not np.all(np.isnan(sensitivities_bin)):
|
|
470
|
+
# nanargmin won't return the index of nan entries
|
|
471
|
+
best = np.nanargmin(sensitivities_bin)
|
|
472
|
+
else:
|
|
473
|
+
# if all are invalid, just use the first one
|
|
474
|
+
best = 0
|
|
475
|
+
|
|
476
|
+
sensitivity[bin_id] = sensitivities[best][bin_id]
|
|
477
|
+
|
|
478
|
+
elif args.global_gh_cut and bool(args.energy_dependent_gh_efficiency):
|
|
479
|
+
log.info(
|
|
480
|
+
"Both a global G/H cut and a energy dependant G/H cut were selected. The energy dependant G/H cut was performed"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
elif args.global_gh_cut and not bool(args.energy_dependent_gh_efficiency):
|
|
484
|
+
|
|
485
|
+
log.info("Using a global G/H cut of " f"{args.global_gh_cut}")
|
|
486
|
+
|
|
487
|
+
for tab in (gammas, background):
|
|
488
|
+
tab["selected_gh"] = tab["gh_score"] > args.global_gh_cut
|
|
489
|
+
|
|
490
|
+
gh_cuts = table.QTable(
|
|
491
|
+
{
|
|
492
|
+
"low": reco_energy_bins[:-1],
|
|
493
|
+
"high": reco_energy_bins[1:],
|
|
494
|
+
"center": gh_center,
|
|
495
|
+
"cut": np.full(len(reco_energy_bins) - 1, args.global_gh_cut),
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
gammas["selected_theta"] = evaluate_binned_cut(
|
|
500
|
+
gammas["theta"], gammas["reco_energy"], theta_cuts, operator.le
|
|
501
|
+
)
|
|
502
|
+
gammas["selected"] = gammas["selected_theta"] & gammas["selected_gh"]
|
|
503
|
+
|
|
504
|
+
sensitivities = []
|
|
505
|
+
|
|
506
|
+
gammas_hist = create_histogram_table(
|
|
507
|
+
gammas[gammas["selected"]], sensitivity_bins, "reco_energy"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
background_hist = estimate_background(
|
|
511
|
+
events=background[background["selected_gh"]],
|
|
512
|
+
reco_energy_bins=sensitivity_bins,
|
|
513
|
+
theta_cuts=theta_cuts,
|
|
514
|
+
alpha=ALPHA,
|
|
515
|
+
fov_offset_min=FOV_OFFSET_MIN,
|
|
516
|
+
fov_offset_max=FOV_OFFSET_MAX,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
sensitivity_coarse = calculate_sensitivity(
|
|
520
|
+
gammas_hist, background_hist, alpha=ALPHA
|
|
521
|
+
)
|
|
522
|
+
sensitivities.append(sensitivity_coarse)
|
|
523
|
+
|
|
524
|
+
sensitivity = sensitivities[0].copy()
|
|
525
|
+
for bin_id in range(len(sensitivity_bins) - 1):
|
|
526
|
+
sensitivities_bin = [
|
|
527
|
+
s["relative_sensitivity"][bin_id] for s in sensitivities
|
|
528
|
+
]
|
|
529
|
+
|
|
530
|
+
if not np.all(np.isnan(sensitivities_bin)):
|
|
531
|
+
# nanargmin won't return the index of nan entries
|
|
532
|
+
best = np.nanargmin(sensitivities_bin)
|
|
533
|
+
else:
|
|
534
|
+
# if all are invalid, just use the first one
|
|
535
|
+
best = 0
|
|
536
|
+
|
|
537
|
+
sensitivity[bin_id] = sensitivities[best][bin_id]
|
|
538
|
+
|
|
539
|
+
else:
|
|
540
|
+
log.info("Optimizing G/H separation cut for best sensitivity")
|
|
541
|
+
gh_cut_efficiencies = np.arange(
|
|
542
|
+
GH_CUT_EFFICIENCY_STEP,
|
|
543
|
+
MAX_GH_CUT_EFFICIENCY + GH_CUT_EFFICIENCY_STEP / 2,
|
|
544
|
+
GH_CUT_EFFICIENCY_STEP,
|
|
545
|
+
)
|
|
546
|
+
sensitivity, gh_cuts = optimize_gh_cut(
|
|
547
|
+
gammas,
|
|
548
|
+
background,
|
|
549
|
+
reco_energy_bins=sensitivity_bins,
|
|
550
|
+
gh_cut_efficiencies=gh_cut_efficiencies,
|
|
551
|
+
op=operator.ge,
|
|
552
|
+
theta_cuts=theta_cuts,
|
|
553
|
+
alpha=ALPHA,
|
|
554
|
+
fov_offset_min=FOV_OFFSET_MIN,
|
|
555
|
+
fov_offset_max=FOV_OFFSET_MAX,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# now that we have the optimized gh cuts, we recalculate the theta
|
|
559
|
+
# cut as 68 percent containment on the events surviving these cuts.
|
|
560
|
+
log.info("Recalculating theta cut for optimized GH Cuts")
|
|
561
|
+
for tab in (gammas, background):
|
|
562
|
+
tab["selected_gh"] = evaluate_binned_cut(
|
|
563
|
+
tab["gh_score"], tab["reco_energy"], gh_cuts, operator.ge
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
gammas["selected_theta"] = evaluate_binned_cut(
|
|
567
|
+
gammas["theta"], gammas["reco_energy"], theta_cuts, operator.le
|
|
568
|
+
)
|
|
569
|
+
gammas["selected"] = gammas["selected_theta"] & gammas["selected_gh"]
|
|
570
|
+
|
|
571
|
+
# scale relative sensitivity by Crab flux to get the flux sensitivity
|
|
572
|
+
spectrum = particles[0]["target_spectrum"]
|
|
573
|
+
sensitivity["flux_sensitivity"] = sensitivity["relative_sensitivity"] * spectrum(
|
|
574
|
+
sensitivity["reco_energy_center"]
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
log.info("Calculating IRFs")
|
|
578
|
+
hdus = [
|
|
579
|
+
fits.PrimaryHDU(),
|
|
580
|
+
fits.BinTableHDU(sensitivity, name="SENSITIVITY"),
|
|
581
|
+
fits.BinTableHDU(theta_cuts, name="THETA_CUTS"),
|
|
582
|
+
fits.BinTableHDU(gh_cuts, name="GH_CUTS"),
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
masks = {
|
|
586
|
+
"": gammas["selected"],
|
|
587
|
+
"_NO_CUTS": slice(None),
|
|
588
|
+
"_ONLY_GH": gammas["selected_gh"],
|
|
589
|
+
"_ONLY_THETA": gammas["selected_theta"],
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
fov_offset_bins = [0, 0.5] * u.deg
|
|
593
|
+
source_offset_bins = np.arange(0, 1 + 1e-4, 1e-3) * u.deg
|
|
594
|
+
energy_migration_bins = np.geomspace(0.2, 5, 200)
|
|
595
|
+
|
|
596
|
+
for label, mask in masks.items():
|
|
597
|
+
effective_area = effective_area_per_energy(
|
|
598
|
+
gammas[mask],
|
|
599
|
+
particles[0]["simulation_info"],
|
|
600
|
+
true_energy_bins=true_energy_bins,
|
|
601
|
+
)
|
|
602
|
+
hdus.append(
|
|
603
|
+
create_aeff2d_hdu(
|
|
604
|
+
effective_area[..., np.newaxis], # add one dimension for FOV offset
|
|
605
|
+
true_energy_bins,
|
|
606
|
+
fov_offset_bins,
|
|
607
|
+
extname="EFFECTIVE_AREA" + label,
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
edisp = energy_dispersion(
|
|
611
|
+
gammas[mask],
|
|
612
|
+
true_energy_bins=true_energy_bins,
|
|
613
|
+
fov_offset_bins=fov_offset_bins,
|
|
614
|
+
migration_bins=energy_migration_bins,
|
|
615
|
+
)
|
|
616
|
+
hdus.append(
|
|
617
|
+
create_energy_dispersion_hdu(
|
|
618
|
+
edisp,
|
|
619
|
+
true_energy_bins=true_energy_bins,
|
|
620
|
+
migration_bins=energy_migration_bins,
|
|
621
|
+
fov_offset_bins=fov_offset_bins,
|
|
622
|
+
extname="ENERGY_DISPERSION" + label,
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
bias_resolution = energy_bias_resolution(
|
|
627
|
+
gammas[gammas["selected"]], reco_energy_bins, energy_type="reco"
|
|
628
|
+
)
|
|
629
|
+
ang_res = angular_resolution(
|
|
630
|
+
gammas[gammas["selected_gh"]], reco_energy_bins, energy_type="reco"
|
|
631
|
+
)
|
|
632
|
+
psf = psf_table(
|
|
633
|
+
gammas[gammas["selected_gh"]],
|
|
634
|
+
true_energy_bins,
|
|
635
|
+
fov_offset_bins=fov_offset_bins,
|
|
636
|
+
source_offset_bins=source_offset_bins,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
background_rate = background_2d(
|
|
640
|
+
background[background["selected_gh"]],
|
|
641
|
+
reco_energy_bins,
|
|
642
|
+
fov_offset_bins=np.arange(0, 11) * u.deg,
|
|
643
|
+
t_obs=T_OBS,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
hdus.append(
|
|
647
|
+
create_background_2d_hdu(
|
|
648
|
+
background_rate,
|
|
649
|
+
reco_energy_bins,
|
|
650
|
+
fov_offset_bins=np.arange(0, 11) * u.deg,
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
hdus.append(
|
|
654
|
+
create_psf_table_hdu(
|
|
655
|
+
psf,
|
|
656
|
+
true_energy_bins,
|
|
657
|
+
source_offset_bins,
|
|
658
|
+
fov_offset_bins,
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
hdus.append(
|
|
662
|
+
create_rad_max_hdu(
|
|
663
|
+
theta_cuts["cut"][:, np.newaxis], theta_bins, fov_offset_bins
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
hdus.append(fits.BinTableHDU(ang_res, name="ANGULAR_RESOLUTION"))
|
|
667
|
+
hdus.append(fits.BinTableHDU(bias_resolution, name="ENERGY_BIAS_RESOLUTION"))
|
|
668
|
+
|
|
669
|
+
if not args.output.endswith(".fits.gz"):
|
|
670
|
+
args.output += ".fits.gz"
|
|
671
|
+
log.info(f"Writing outputfile in {args.output}")
|
|
672
|
+
fits.HDUList(hdus).writeto(args.output, overwrite=True)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
if __name__ == "__main__":
|
|
676
|
+
main()
|