Repository: cta-observatory/ctapipe
Pull request #2295 (2023-03-28 09:13:58)

Better errors in case of no events in train tools
As reported by @arrabito , the errors in case of now events in the input file were a bit sub-optimal. Added additional checks to provide better errors.


Patch:
diff --git a/ctapipe/exceptions.py b/ctapipe/exceptions.py
new file mode 100644
index 00000000..c75f2318
--- /dev/null
+++ b/ctapipe/exceptions.py
@@ -0,0 +1,6 @@
+class CTAPipeException(Exception):
+    pass
+
+
+class TooFewEvents(CTAPipeException):
+    """Raised if something that needs a minimum number of event gets fewer"""
diff --git a/ctapipe/reco/__init__.py b/ctapipe/reco/__init__.py
index 267b68b4..7c4edc10 100644
--- a/ctapipe/reco/__init__.py
+++ b/ctapipe/reco/__init__.py
@@ -5,11 +5,7 @@
 from .hillas_intersection import HillasIntersection
 from .hillas_reconstructor import HillasReconstructor
 from .impact import ImPACTReconstructor
-from .reconstructor import (
-    HillasGeometryReconstructor,
-    ReconstructionProperty,
-    Reconstructor,
-)
+from .reconstructor import GeometryReconstructor, ReconstructionProperty, Reconstructor
 from .shower_processor import ShowerProcessor
 from .sklearn import (
     CrossValidator,
@@ -21,7 +17,7 @@ from .stereo_combination import StereoCombiner, StereoMeanCombiner
 
 __all__ = [
     "Reconstructor",
-    "HillasGeometryReconstructor",
+    "GeometryReconstructor",
     "ReconstructionProperty",
     "ShowerProcessor",
     "HillasReconstructor",
diff --git a/ctapipe/reco/hillas_intersection.py b/ctapipe/reco/hillas_intersection.py
index ea8e4f74..36bd987a 100644
--- a/ctapipe/reco/hillas_intersection.py
+++ b/ctapipe/reco/hillas_intersection.py
@@ -30,7 +30,7 @@ from ..coordinates import (
 )
 from ..core import traits
 from .reconstructor import (
-    HillasGeometryReconstructor,
+    GeometryReconstructor,
     InvalidWidthException,
     TooFewTelescopesException,
 )
@@ -44,7 +44,7 @@ INVALID = ReconstructedGeometryContainer(
 )
 
 
-class HillasIntersection(HillasGeometryReconstructor):
+class HillasIntersection(GeometryReconstructor):
     """
     This class is a simple re-implementation of Hillas parameter based event
     reconstruction. e.g. https://arxiv.org/abs/astro-ph/0607333
@@ -249,8 +249,8 @@ class HillasIntersection(HillasGeometryReconstructor):
             az=sky_pos.altaz.az.to(u.rad),
             core_x=grd.x,
             core_y=grd.y,
-            core_tilted_x=tilt.x,
-            core_tilted_y=tilt.y,
+            core_tilted_x=u.Quantity(core_x, u.m),
+            core_tilted_y=u.Quantity(core_y, u.m),
             core_tilted_uncert_x=u.Quantity(core_err_x, u.m),
             core_tilted_uncert_y=u.Quantity(core_err_y, u.m),
             telescopes=[h for h in hillas_dict_mod.keys()],
diff --git a/ctapipe/reco/hillas_reconstructor.py b/ctapipe/reco/hillas_reconstructor.py
index 1fbd305d..e602a204 100644
--- a/ctapipe/reco/hillas_reconstructor.py
+++ b/ctapipe/reco/hillas_reconstructor.py
@@ -20,7 +20,7 @@ from ..coordinates import (
     project_to_ground,
 )
 from .reconstructor import (
-    HillasGeometryReconstructor,
+    GeometryReconstructor,
     InvalidWidthException,
     TooFewTelescopesException,
 )
@@ -94,7 +94,7 @@ def line_line_intersection_3d(uvw_vectors, origins):
     return np.linalg.inv(S) @ C
 
 
-class HillasReconstructor(HillasGeometryReconstructor):
+class HillasReconstructor(GeometryReconstructor):
     """
     class that reconstructs the direction of an atmospheric shower
     using a simple hillas parametrisation of the camera images it
diff --git a/ctapipe/reco/reconstructor.py b/ctapipe/reco/reconstructor.py
index 9b38bf08..57b73d3b 100644
--- a/ctapipe/reco/reconstructor.py
+++ b/ctapipe/reco/reconstructor.py
@@ -15,7 +15,7 @@ from ..coordinates import shower_impact_distance
 
 __all__ = [
     "Reconstructor",
-    "HillasGeometryReconstructor",
+    "GeometryReconstructor",
     "TooFewTelescopesException",
     "InvalidWidthException",
     "ReconstructionProperty",
@@ -142,9 +142,9 @@ class Reconstructor(TelescopeComponent):
         return instance
 
 
-class HillasGeometryReconstructor(Reconstructor):
+class GeometryReconstructor(Reconstructor):
     """
-    Base class for algorithms predicting only the shower geometry using Hillas Based methods
+    Base class for algorithms predicting only the shower geometry
     """
 
     def _create_hillas_dict(self, event):
diff --git a/ctapipe/reco/sklearn.py b/ctapipe/reco/sklearn.py
index 27a73b25..59bf5dd4 100644
--- a/ctapipe/reco/sklearn.py
+++ b/ctapipe/reco/sklearn.py
@@ -18,6 +18,8 @@ from sklearn.utils import all_estimators
 from tqdm import tqdm
 from traitlets import TraitError
 
+from ctapipe.exceptions import TooFewEvents
+
 from ..containers import (
     ArrayEventContainer,
     DispContainer,
@@ -847,7 +849,7 @@ class CrossValidator(Component):
 
     def __call__(self, telescope_type, table):
         if len(table) <= self.n_cross_validations:
-            raise ValueError(f"Too few events for {telescope_type}.")
+            raise TooFewEvents(f"Too few events for {telescope_type}.")
 
         self.log.info(
             "Starting cross-validation with %d folds for type %s.",
diff --git a/ctapipe/tools/train_disp_reconstructor.py b/ctapipe/tools/train_disp_reconstructor.py
index 5e0c88c9..6674077b 100644
--- a/ctapipe/tools/train_disp_reconstructor.py
+++ b/ctapipe/tools/train_disp_reconstructor.py
@@ -3,6 +3,7 @@ import numpy as np
 
 from ctapipe.core import Tool
 from ctapipe.core.traits import Bool, Int, IntTelescopeParameter, Path
+from ctapipe.exceptions import TooFewEvents
 from ctapipe.io import TableLoader
 from ctapipe.reco import CrossValidator, DispReconstructor
 from ctapipe.reco.preprocessing import check_valid_rows, horizontal_to_telescope
@@ -113,10 +114,18 @@ class TrainDispReconstructor(Tool):
     def _read_table(self, telescope_type):
         table = self.loader.read_telescope_events([telescope_type])
         self.log.info("Events read from input: %d", len(table))
+        if len(table) == 0:
+            raise TooFewEvents(
+                f"Input file does not contain any events for telescope type {telescope_type}"
+            )
 
         mask = self.models.quality_query.get_table_mask(table)
         table = table[mask]
         self.log.info("Events after applying quality query: %d", len(table))
+        if len(table) == 0:
+            raise TooFewEvents(
+                f"No events after quality query for telescope type {telescope_type}"
+            )
 
         table = self.models.feature_generator(table)
 
diff --git a/ctapipe/tools/train_energy_regressor.py b/ctapipe/tools/train_energy_regressor.py
index b022507e..16f0c241 100644
--- a/ctapipe/tools/train_energy_regressor.py
+++ b/ctapipe/tools/train_energy_regressor.py
@@ -5,6 +5,7 @@ import numpy as np
 
 from ctapipe.core import Tool
 from ctapipe.core.traits import Int, IntTelescopeParameter, Path
+from ctapipe.exceptions import TooFewEvents
 from ctapipe.io import TableLoader
 from ctapipe.reco import CrossValidator, EnergyRegressor
 from ctapipe.reco.preprocessing import check_valid_rows
@@ -113,11 +114,19 @@ class TrainEnergyRegressor(Tool):
 
     def _read_table(self, telescope_type):
         table = self.loader.read_telescope_events([telescope_type])
-
         self.log.info("Events read from input: %d", len(table))
+        if len(table) == 0:
+            raise TooFewEvents(
+                f"Input file does not contain any events for telescope type {telescope_type}"
+            )
+
         mask = self.regressor.quality_query.get_table_mask(table)
         table = table[mask]
         self.log.info("Events after applying quality query: %d", len(table))
+        if len(table) == 0:
+            raise TooFewEvents(
+                f"No events after quality query for telescope type {telescope_type}"
+            )
 
         table = self.regressor.feature_generator(table)
 
diff --git a/ctapipe/tools/train_particle_classifier.py b/ctapipe/tools/train_particle_classifier.py
index 5c7a8852..b6e29014 100644
--- a/ctapipe/tools/train_particle_classifier.py
+++ b/ctapipe/tools/train_particle_classifier.py
@@ -6,6 +6,7 @@ from astropy.table import vstack
 
 from ctapipe.core.tool import Tool
 from ctapipe.core.traits import Int, IntTelescopeParameter, Path
+from ctapipe.exceptions import TooFewEvents
 from ctapipe.io import TableLoader
 from ctapipe.reco import CrossValidator, ParticleClassifier
 from ctapipe.reco.preprocessing import check_valid_rows
@@ -164,11 +165,19 @@ class TrainParticleClassifier(Tool):
 
     def _read_table(self, telescope_type, loader, n_events=None):
         table = loader.read_telescope_events([telescope_type])
-
         self.log.info("Events read from input: %d", len(table))
+        if len(table) == 0:
+            raise TooFewEvents(
+                f"Input file does not contain any events for telescope type {telescope_type}"
+            )
+
         mask = self.classifier.quality_query.get_table_mask(table)
         table = table[mask]
         self.log.info("Events after applying quality query: %d", len(table))
+        if len(table) == 0:
+            raise TooFewEvents(
+                f"No events after quality query for telescope type {telescope_type}"
+            )
 
         table = self.classifier.feature_generator(table)
 
diff --git a/docs/changes/2293.api.rst b/docs/changes/2293.api.rst
deleted file mode 100644
index 28e0725c..00000000
--- a/docs/changes/2293.api.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Renames ``GeometryReconstructor`` to ``HillasGeometryReconstructor``
diff --git a/docs/changes/2295.maintenance.rst b/docs/changes/2295.maintenance.rst
new file mode 100644
index 00000000..b4b7fb6f
--- /dev/null
+++ b/docs/changes/2295.maintenance.rst
@@ -0,0 +1,2 @@
+The tools to train ml models now provide better error messages in case
+the input files did not contain any events for specific telescope types.


Test patch:
diff --git a/ctapipe/reco/tests/test_shower_processor.py b/ctapipe/reco/tests/test_shower_processor.py
index 607568b8..49a7436a 100644
--- a/ctapipe/reco/tests/test_shower_processor.py
+++ b/ctapipe/reco/tests/test_shower_processor.py
@@ -9,14 +9,14 @@ from traitlets.config.loader import Config
 
 from ctapipe.calib import CameraCalibrator
 from ctapipe.image import ImageProcessor
-from ctapipe.reco import HillasGeometryReconstructor, ShowerProcessor
+from ctapipe.reco import GeometryReconstructor, ShowerProcessor
 
 
 @pytest.mark.parametrize(
     "reconstructor_types",
     [
         [reco_type]
-        for reco_type in HillasGeometryReconstructor.non_abstract_subclasses().keys()
+        for reco_type in GeometryReconstructor.non_abstract_subclasses().keys()
     ]
     + [["HillasReconstructor", "HillasIntersection"]],
 )
diff --git a/ctapipe/tools/tests/test_train.py b/ctapipe/tools/tests/test_train.py
index 845a44d7..9dd6a724 100644
--- a/ctapipe/tools/tests/test_train.py
+++ b/ctapipe/tools/tests/test_train.py
@@ -1,6 +1,7 @@
 import pytest
 
 from ctapipe.core import run_tool
+from ctapipe.exceptions import TooFewEvents
 from ctapipe.utils.datasets import resource_file
 
 
@@ -29,7 +30,7 @@ def test_too_few_events(tmp_path, dl2_shower_geometry_file):
     config = resource_file("train_energy_regressor.yaml")
     out_file = tmp_path / "energy.pkl"
 
-    with pytest.raises(ValueError, match="Too few events"):
+    with pytest.raises(TooFewEvents, match="No events after quality query"):
         run_tool(
             tool,
             argv=[