Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional LST stereo trigger requirement #131

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 23 additions & 19 deletions docs/pipeline/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ pipeline
Introduction
------------

`protopipe.pipeline` contains classes that are used in scripts to produce
``protopipe.pipeline`` contains classes that are used in scripts to produce

- tables with images information (DL1), typically for g/h classifier and energy regressor,
- tables with event information, typically used for performance estimation (DL2).

Two classes from the sub-module are used to process the events:

- ``EventPreparer`` class, which loops on events and to provide event parameters
(e.g. impact parameter) and image parameters (e.g. Hillas parameters),
- ``EventPreparer``, which loops on events and to provide event parameters
(e.g. impact parameter) and image parameters (e.g. Hillas parameters),
- ``ImageCleaner``, cleans the images according to different options.

Details
Expand All @@ -31,6 +31,26 @@ Details
The following is a description of the *default* algorithms and settings, chosen
to mimic the CTA-MARS pipeline.

Management of events, triggers and images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

- for each event ALL images from ALL telescopes are processed up to DL1b included,

- to define what happens *after* DL1b, the ``Reconstruction`` section of the ``analysis.yaml`` configuration
file accepts two options,

- ``LST_stereo``, trigger requirement (at least 2 LST images - ``True`` by default)
- ``min_tel``, minimum number of telescopes from ANY telescope type to consider events,

In case ``LST_stereo`` is set to ``True``, events with,

- 1 LST trigger and less than 2 triggers from any other telescope type will not
be processed after DL1b
- 1 LST trigger and at least 2 triggers from any other telescope type will get processed
at DL2a **without** the single-LST image and provided the remaining
images are still at least 2 after the image quality cuts have been taken into
consideration.

Calibration
^^^^^^^^^^^

Expand Down Expand Up @@ -84,22 +104,6 @@ per event.
The camera corrections correspond to those of *ctapipe* and are performed inside
the reconstructor.

Proposals for improvements and/or fixes
---------------------------------------

.. note::

This section will be moved to the repository as a issues.
Any further update will appear there.


* The EventPreparer class is a bit messy: it should return the event and one container
with several results (hillas parameters, reconstructed shower, etc.). In addition
some things are hard-coded , e.g. for now calibration is done in the same way
(not a problem since only LSTCam and NectarCam have been considered until now),
camera radius is also hard-coded for LST and MST, and computation of the impact
parameters in the frame of the shower system should be better implemented.

Reference/API
-------------

Expand Down
9 changes: 7 additions & 2 deletions protopipe/aux/example_config_files/analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,17 @@ ImageSelection:

# Minimal number of telescopes to consider events
Reconstruction:
min_tel: 2
# for events with <2 LST images the single-LST image is removed
# before shower geometry
LST_stereo: True
# after this we check if the remaining images satisfy the min_tel condition
min_tel: 2 # any tel_type


# Parameters for energy estimation
EnergyRegressor:
# Name of the regression method (e.g. AdaBoostRegressor, etc.)
method_name: 'AdaBoostRegressor'
method_name: 'RandomForestRegressor'
estimation_weight: 'STD'

# Parameters for g/h separation
Expand Down
114 changes: 111 additions & 3 deletions protopipe/pipeline/event_preparer.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,23 @@ def __init__(

# Add cuts on events
self.min_ntel = config["Reconstruction"]["min_tel"]
self.min_ntel_LST = 2 # LST stereo trigger
try:
self.LST_stereo = config["Reconstruction"]["LST_stereo"]
except KeyError:
print(
bcolors.WARNING
+ "WARNING: the 'LST_stereo' setting has not been specified in the analysis configuration file."
+ "\t It has been set to 'True'!"
+ bcolors.ENDC
)
self.LST_stereo = True
self.event_cutflow.set_cuts(
OrderedDict(
[
("noCuts", None),
("min2Tels trig", lambda x: x < self.min_ntel),
("no-LST-stereo + <2 other types", lambda x, y: (x < self.min_ntel_LST) and (y < 2)),
("min2Tels reco", lambda x: x < self.min_ntel),
("direction nan", lambda x: x.is_valid is False),
]
Expand Down Expand Up @@ -341,11 +353,36 @@ def prepare_event(self, source, return_stub=True, save_images=False, debug=False

self.event_cutflow.count("noCuts")

# LST stereo condition
# whenever there is only 1 LST in an event, we remove that telescope
# if the remaining telescopes are less than min_tel we remove the event

lst_tel_ids = set(source.subarray.get_tel_ids_for_type("LST_LST_LSTCam"))
triggered_LSTs = set(event.r0.tel.keys()).intersection(lst_tel_ids)
n_triggered_LSTs = len(triggered_LSTs)
n_triggered_non_LSTs = len(event.r0.tel.keys()) - n_triggered_LSTs

bad_LST_stereo = False
if self.LST_stereo and self.event_cutflow.cut("no-LST-stereo + <2 other types", n_triggered_LSTs, n_triggered_non_LSTs):
bad_LST_stereo = True
if return_stub:
print(
bcolors.WARNING
+ "WARNING: LST_stereo is set to 'True'\n"
+ f"This event has < {self.min_ntel_LST} triggered LSTs\n"
+ "and < 2 triggered telescopes from other telescope types.\n"
+ "The event will be processed up to DL1b."
+ bcolors.ENDC
)
# we show this, but we proceed to analyze the event up to
# DL1a/b for the associated benchmarks

# this checks for < 2 triggered telescopes of ANY type
if self.event_cutflow.cut("min2Tels trig", len(event.r1.tel.keys())):
if return_stub:
print(
bcolors.WARNING
+ f"WARNING : < {self.min_tel} triggered telescopes!"
+ f"WARNING : < {self.min_ntel} triggered telescopes!"
+ bcolors.ENDC
)
# we show this, but we proceed to analyze it
Expand Down Expand Up @@ -739,9 +776,79 @@ def prepare_event(self, source, return_stub=True, save_images=False, debug=False
# DIRECTION RECONSTRUCTION
# =============================================================

if bad_LST_stereo:
if debug:
print(
bcolors.WARNING
+ "WARNING: This event was triggered with 1 LST image and <2 images from other telescope types."
+ "\nWARNING : direction reconstruction will not be performed."
+ bcolors.ENDC
)

# Set all the involved images as NOT good for recosntruction
# even though they might have been
# but this is because of the LST stereo trigger....
for tel_id in event.r0.tel.keys():
good_for_reco[tel_id] = 0
# and set the number of good and bad images accordingly
n_tels["GOOD images"] = 0
n_tels["BAD images"] = n_tels["Triggered"] - n_tels["GOOD images"]
# create a dummy container for direction reconstruction
reco_result = ReconstructedShowerContainer()

if return_stub: # if saving all events (default)
if debug:
print(bcolors.OKBLUE + "Recording event..." + bcolors.ENDC)
print(
bcolors.WARNING
+ "WARNING: This is event shall NOT be used further along the pipeline."
+ bcolors.ENDC
)
yield stub( # record event with dummy info
event,
mc_phe_image,
dl1_phe_image,
dl1_phe_image_mask_reco,
dl1_phe_image_mask_clusters,
good_for_reco,
hillas_dict,
hillas_dict_reco,
n_tels,
leakage_dict,
concentration_dict
)
continue
else:
continue

# Now in case the only triggered telescopes were
# - < self.min_ntel_LST LST,
# - >=2 any other telescope type,
# we remove the single-LST image and continue reconstruction with
# the images from the other telescope types
if self.LST_stereo and (n_triggered_LSTs < self.min_ntel_LST) and (n_triggered_LSTs != 0) and (n_triggered_non_LSTs >= 2):
if debug:
print(
bcolors.WARNING
+ f"WARNING: LST stereo trigger condition is active.\n"
+ f"This event triggered < {self.min_ntel_LST} LSTs "
+ f"and {n_triggered_non_LSTs} images from other telescope types.\n"
+ bcolors.ENDC
)
for tel_id in triggered_LSTs: # in case we test for min_ntel_LST>2
if good_for_reco[tel_id]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not needed: just set every telescope in triggers_LSTs to 0 - it doesn't matter if it was true before.

What should good_for_reco be in the end? a variable-length list of tel_ids? or a boolean array of tel indices? Right now it is a dict, but it seems you encode both the tels with data, and which ones pass the cuts in it at the same time, which is why it is confusing. I'd suggest just storing those separately if you need both. In ctapipe, we automatically convert telescope lists into boolean arrays so they can be stored in the hdf5 output without using var-length arrays (SubarrayDescription provides a method for that:)

In [3]: source.subarray.tel_ids_to_mask([1,3,4])
Out[3]:
array([ True, False,  True,  True, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False])
)

If it's a boolean mask like that, you can get rid of this explicit loop and just use array indexing or set subtraction:

good_for_reco[subarray.tel_ids_to_indices(list(triggered_LSTs))] = False

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way you remove this explicit loop entirely

Copy link
Member Author

@HealthyPear HealthyPear May 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good_for_reco is not a boolean mask, but a dictionary where the keys are tel_ids (only the ones related to that particular event) and the values are 1 if the cleaned+parametrized image passed all quality cuts, 0 otherwise.
It is then written to file

honestly, I would avoid trying to make this more fancier now considering that soon it will all disappear, unless it it clear that there is a bug due to how I wrote it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so basically what is happening here is that I set the value to 0 only if the image passed the quality cuts (otherwise it is already been set to 0) thus I "remove" the single-LST image by not using it later for shower geometry reconstruction, but since I need to process it up to DL1b for the benchmarking I still need to know (via my dictionary of labels "good_for_reco") if it would have been used in other circumstances

Copy link
Contributor

@kosack kosack May 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, if this is not meant to be long-term code, fine. This was just so that you could write out the used telescope pattern as we do in ctapipe, but maybe that is not needed since we will soon move to ctapipe's version.

I was more thinking about doing it right here, so we can just move the code to ctapipe later.

# we don't use it for reconstruction
good_for_reco[tel_id] = 0
print(
bcolors.WARNING
+ f"WARNING: LST image #{tel_id} removed, even though it passed quality cuts."
+ bcolors.ENDC
)
# TODO: book-keeping of this kind of events doesn't seem easy

# convert dictionary in numpy array to get a "mask"
images_status = np.asarray(list(good_for_reco.values()))
# record how many images will used for reconstruction
# record how many images will be used for reconstruction
n_tels["GOOD images"] = len(np.extract(images_status == 1, images_status))
n_tels["BAD images"] = n_tels["Triggered"] - n_tels["GOOD images"]

Expand Down Expand Up @@ -810,7 +917,8 @@ def prepare_event(self, source, return_stub=True, save_images=False, debug=False
if debug:
print(
bcolors.PURPLE
+ f"{len(good_hillas_dict)} images will be "
+ f"{len(good_hillas_dict)} images "
+ f"(from telescopes #{list(good_hillas_dict.keys())}) will be "
+ "used to recover the shower's direction..."
+ bcolors.ENDC
)
Expand Down