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

Implement a shower processor #1675

Merged
merged 36 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4c1546f
add shower processor
HealthyPear Apr 7, 2021
9ff874a
add test
HealthyPear Apr 7, 2021
9e67940
should be ok...
HealthyPear Apr 7, 2021
901b0d0
make the test check right things
HealthyPear Apr 7, 2021
1ccb56d
add class to init
HealthyPear Apr 7, 2021
72c7a27
remove geometry as an option (alwayd done if processor is called)
HealthyPear Apr 7, 2021
4b45fa1
attempt to fix the test
HealthyPear Apr 7, 2021
ec45fb3
Fix basic test bug and expand class API
HealthyPear Apr 8, 2021
5cd82d8
Expand test to all possible usage cases
HealthyPear Apr 8, 2021
4d80631
Improve quality criteria
HealthyPear Apr 8, 2021
f1c7eca
remove is_simulation
HealthyPear Apr 8, 2021
3e4228a
change _estimate_classification to __classify_particle_type
HealthyPear Apr 8, 2021
81b3433
Update docstrings
HealthyPear Apr 8, 2021
4d151ba
Improve hillas dict creation and shower criteria, general fixes
HealthyPear Apr 8, 2021
cfa4e70
Update test
HealthyPear Apr 8, 2021
49b8e9c
Make reconstruct_energy and classify traitlets and update documentation.
HealthyPear Apr 8, 2021
18441ec
Log shower criteria without f-string
HealthyPear Apr 27, 2021
1861355
Shower criteria table formatted string representation if log-level DEBUG
HealthyPear May 4, 2021
2a05825
Merge branch 'master' into feature-shower_processor
HealthyPear May 7, 2021
40c1232
Fix imports
HealthyPear May 7, 2021
1c465b4
Merge remote branch into local branch and fix conflicts
HealthyPear May 7, 2021
a5b0cbc
Missing list of public objects in shower_processor sub-module
HealthyPear May 7, 2021
967885f
Fix docstring of ctapipe.reco.shower_processor.ShowerProcessor.__call__
HealthyPear May 7, 2021
ea12d84
Add shower_processor submodule to automodapi
HealthyPear May 7, 2021
3097b5e
fix docs
HealthyPear May 7, 2021
d171929
Fix docs warnings
maxnoe May 7, 2021
1a2efad
Merge branch 'master' into feature-shower_processor
HealthyPear May 20, 2021
d61e8c4
Remove parts not implemented and make single functions public API
HealthyPear May 20, 2021
bebb455
Fix remaining bugs and update to new DL2 data model
HealthyPear May 20, 2021
e0c6a44
Fix docstring of ctapipe.reco.shower_processor.ShowerProcessor.recons…
HealthyPear May 20, 2021
a514d76
Import reconstructor from correct submodule
HealthyPear May 20, 2021
1233aa2
Fix docstring of ShowerProcessor
HealthyPear May 20, 2021
6b06e83
remove stale code
HealthyPear May 20, 2021
4ae1f3d
log shower geometry correctly
HealthyPear May 20, 2021
2144bc1
remove not implemented options from test
HealthyPear May 20, 2021
8ef9f5c
rename reconstruct_shower to reconstruct_geometry
HealthyPear May 20, 2021
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
6 changes: 5 additions & 1 deletion ctapipe/reco/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
from .HillasReconstructor import HillasReconstructor, Reconstructor
from .ImPACT import ImPACTReconstructor
from .shower_processor import ShowerProcessor


__all__ = ["HillasReconstructor", "Reconstructor", "ImPACTReconstructor"]
__all__ = ["ShowerProcessor",
"HillasReconstructor",
"Reconstructor",
"ImPACTReconstructor"]
193 changes: 193 additions & 0 deletions ctapipe/reco/shower_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""
High level processing of showers.

This processor will be able to process a shower/event in 3 steps:
- shower geometry
- estimation of energy (optional, currently unavailable)
- estimation of classification (optional, currently unavailable)

"""
import numpy as np

from astropy.coordinates import SkyCoord, AltAz

from ctapipe.core import Component, QualityQuery
from ctapipe.core.traits import List
from ctapipe.containers import ArrayEventContainer, ReconstructedShowerContainer
from ctapipe.instrument import SubarrayDescription
from ctapipe.reco import HillasReconstructor


DEFAULT_SHOWER_PARAMETERS = ReconstructedShowerContainer()


class ShowerQualityQuery(QualityQuery):
"""Configuring shower-wise data checks."""

quality_criteria = List(
default_value=[
("Positive charge", "lambda hillas: hillas.intensity > 0"),
("Positive width", "lambda hillas: hillas.width.value > 0"),
("Positive length", "lambda hillas: hillas.length.value > 0"),
("Ellipticity > 0.1", "lambda hillas: hillas.width.value / hillas.length.value > 0.1"),
("Ellipticity < 0.6", "lambda hillas: hillas.width.value / hillas.length.value < 0.6")
],
help=QualityQuery.quality_criteria.help,
).tag(config=True)


class ShowerProcessor(Component):
"""
Takes DL1/parameters data and estimates the shower geometry.
Should be run after ImageProcessor to produce all DL1b information.
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved

For now the only supported reconstructor is HillasReconstructor.
"""

def __init__(
self,
subarray: SubarrayDescription,
is_simulation,
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved
reconstruct_energy,
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved
classify,
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved
config=None,
parent=None,
**kwargs,
):
"""
Parameters
----------
subarray: SubarrayDescription
Description of the subarray. Provides information about the
camera which are useful in calibration. Also required for
configuring the TelescopeParameter traitlets.
is_simulation: bool
If true, also process simulated images if they exist
config: traitlets.loader.Config
Configuration specified by config file or cmdline arguments.
Used to set traitlet values.
This is mutually exclusive with passing a ``parent``.
parent: ctapipe.core.Component or ctapipe.core.Tool
Parent of this component in the configuration hierarchy,
this is mutually exclusive with passing ``config``
"""

super().__init__(config=config, parent=parent, **kwargs)
self.subarray = subarray
self.check_shower = ShowerQualityQuery(parent=self)
self._is_simulation = is_simulation
self.reconstruct_energy = reconstruct_energy
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved
self.classify = classify
self.reconstructor = HillasReconstructor()
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved

def _reconstruct_shower(
self,
event,
default=DEFAULT_SHOWER_PARAMETERS,
) -> ReconstructedShowerContainer:
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved
"""Perform shower reconstruction.

Parameters
----------
tel_id: int
which telescope is being cleaned
image: np.ndarray
image to process
signal_pixels: np.ndarray[bool]
image mask
peak_time: np.ndarray
peak time image
Returns
-------
ReconstructedShowerContainer:
direction in the sky with uncertainty,
core position on the ground with uncertainty,
h_max with uncertainty,
is_valid boolean for successfull reconstruction,
average intensity of the intensities used for reconstruction,
measure of algorithm success (if fit),
list of tel_ids used if stereo, or None if Mono
"""

# Read only valid HillasContainers (minimum condition to continue)
hillas_dict = {
tel_id: dl1.parameters.hillas
for tel_id, dl1 in event.dl1.tel.items()
if np.isfinite(event.dl1.tel[tel_id].parameters.hillas.intensity)
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved
}

# On top of this check if the shower should be considered based
# on the user's configuration
shower_criteria = [self.check_shower(hillas_dict[tel_id]) for tel_id in hillas_dict]
self.log.debug(
"shower_criteria: %s",
list(zip(self.check_shower.criteria_names[1:], shower_criteria)),
)

# Reconstruct the shower only if all shower criteria are met
if np.count_nonzero(shower_criteria) > 2:
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved

array_pointing = SkyCoord(
az=event.pointing.array_azimuth,
alt=event.pointing.array_altitude,
frame=AltAz(),
)

telescopes_pointings = {
tel_id: SkyCoord(
alt=event.pointing.tel[tel_id].altitude,
az=event.pointing.tel[tel_id].azimuth,
frame=AltAz(),
)
for tel_id in hillas_dict
}

result = self.reconstructor.predict(hillas_dict,
self.subarray,
array_pointing,
telescopes_pointings)

return result

else:
return default

def _reconstruct_energy(self, event: ArrayEventContainer):
raise NotImplementedError("TO DO")

def _estimate_classification(self, event: ArrayEventContainer):
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved
raise NotImplementedError("TO DO")

def _process_reconstructed_energy(self, event: ArrayEventContainer):
self._reconstruct_energy(event)

def _process_reconstructed_classification(self, event: ArrayEventContainer):
self._estimate_classification(event)

def _process_shower_geometry(self, event: ArrayEventContainer):
"""Record the reconstructed shower geometry into the ArrayEventContainer."""

shower_geometry = self._reconstruct_shower(event)

self.log.debug("shower geometry: %s", shower_geometry.as_dict(recursive=True))
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved

event.dl2.shower["HillasReconstructor"] = shower_geometry
HealthyPear marked this conversation as resolved.
Show resolved Hide resolved

def __call__(self, event: ArrayEventContainer):
"""
Perform the full shower geometry reconstruction on the input event.

Parameters
----------
event : container
A `ctapipe` event container
"""

# This is always done when calling the ShowerProcessor
self._process_shower_geometry(event)

if self.reconstruct_energy:
self._process_reconstructed_classification(event)

if self.classify:
self._process_reconstructed_energy(event)
92 changes: 92 additions & 0 deletions ctapipe/reco/tests/test_shower_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Tests for ShowerProcessor functionalities.
"""
import pytest
from numpy import isfinite

from traitlets.config.loader import Config

from ctapipe.calib import CameraCalibrator
from ctapipe.image import ImageProcessor
from ctapipe.reco import ShowerProcessor


def test_shower_processor_geometry(example_event, example_subarray):
"""Ensure we get shower geometry when we input an event with parametrized images."""

calibrate = CameraCalibrator(subarray=example_subarray)

process_images = ImageProcessor(
subarray=example_subarray,
is_simulation=True,
image_cleaner_type="MARSImageCleaner",
)

process_shower = ShowerProcessor(
subarray=example_subarray,
is_simulation=True,
reconstruct_energy=False,
classify=False
)

calibrate(example_event)
process_images(example_event)

process_shower(example_event)
print(process_shower.check_shower.to_table())

DL2a = example_event.dl2.shower["HillasReconstructor"]
print(DL2a)
assert isfinite(DL2a.alt)
assert isfinite(DL2a.az)
assert isfinite(DL2a.core_x)
assert isfinite(DL2a.core_x)
assert isfinite(DL2a.core_y)
assert DL2a.is_valid
assert isfinite(DL2a.average_intensity)

# Increase some quality cuts and check that we get defaults
config = Config()
config.ShowerQualityQuery.quality_criteria = [("> 500 phes", "lambda hillas: hillas.intensity > 500")]

process_shower = ShowerProcessor(
config=config,
subarray=example_subarray,
is_simulation=True,
reconstruct_energy=False,
classify=False
)

process_shower(example_event)
print(process_shower.check_shower.to_table())

DL2a = example_event.dl2.shower["HillasReconstructor"]
assert not isfinite(DL2a.alt)
assert not isfinite(DL2a.az)
assert not isfinite(DL2a.core_x)
assert not isfinite(DL2a.core_x)
assert not isfinite(DL2a.core_y)
assert not DL2a.is_valid
assert not isfinite(DL2a.average_intensity)

# Now check that if energy reconstruction is enabled we get a TODO error
process_shower = ShowerProcessor(
config=config,
subarray=example_subarray,
is_simulation=True,
reconstruct_energy=True,
classify=False
)
with pytest.raises(NotImplementedError) as error_info:
process_shower(example_event)

# also for classification
process_shower = ShowerProcessor(
config=config,
subarray=example_subarray,
is_simulation=True,
reconstruct_energy=False,
classify=True
)
with pytest.raises(NotImplementedError) as error_info:
process_shower(example_event)