From df83934eb9992e12c2632688a6750a8844ee62be Mon Sep 17 00:00:00 2001 From: Karl Kosack Date: Mon, 18 May 2026 17:35:00 +0200 Subject: [PATCH 1/2] add altaz_to_icrs helper function --- src/ctapipe/coordinates/__init__.py | 2 ++ src/ctapipe/coordinates/utils.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/ctapipe/coordinates/__init__.py b/src/ctapipe/coordinates/__init__.py index 7d6ed4ea864..2a3f2e6ad8a 100644 --- a/src/ctapipe/coordinates/__init__.py +++ b/src/ctapipe/coordinates/__init__.py @@ -22,6 +22,7 @@ from .nominal_frame import NominalFrame from .telescope_frame import TelescopeFrame from .utils import ( + altaz_to_icrs, altaz_to_nominal, altaz_to_righthanded_cartesian, get_point_on_shower_axis, @@ -42,6 +43,7 @@ "shower_impact_distance", "get_point_on_shower_axis", "altaz_to_nominal", + "altaz_to_icrs", ] diff --git a/src/ctapipe/coordinates/utils.py b/src/ctapipe/coordinates/utils.py index 52a993357ff..181727f170a 100644 --- a/src/ctapipe/coordinates/utils.py +++ b/src/ctapipe/coordinates/utils.py @@ -11,6 +11,7 @@ "altaz_to_righthanded_cartesian", "get_point_on_shower_axis", "altaz_to_nominal", + "altaz_to_icrs", ] @@ -103,3 +104,23 @@ def altaz_to_nominal(az, alt, pointing_az, pointing_alt) -> u.Quantity: return u.Quantity( np.column_stack((nominal_coord.fov_lon.deg, nominal_coord.fov_lat.deg)), u.deg ) + + +def altaz_to_icrs(az, alt, obstime, location) -> u.Quantity: + """ + Compute nominal (FOV) coordinates from alt/az coordinates. + + This can be used in a FeatureGenerator or ExpressionEngine to get a single + column with fov_lon, fov_lat coordinates. + + Returns + ------- + u.Quantity: + 2D array of coordinates with 2 columns: ra, dec + """ + event_coord = SkyCoord( + az=az, alt=alt, frame="altaz", obstime=obstime, location=location + ) + icrs_coord = event_coord.transform_to("icrs") + + return u.Quantity(np.column_stack((icrs_coord.ra.deg, icrs_coord.dec.deg)), u.deg) From 7f5cb95a697c5859ed9585071f721324720dc7ba Mon Sep 17 00:00:00 2001 From: Karl Kosack Date: Mon, 18 May 2026 17:35:20 +0200 Subject: [PATCH 2/2] add a featureset for dl2_to_dl3 --- src/ctapipe/io/event_preprocessor.py | 57 ++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/src/ctapipe/io/event_preprocessor.py b/src/ctapipe/io/event_preprocessor.py index d311035815d..faa23291379 100644 --- a/src/ctapipe/io/event_preprocessor.py +++ b/src/ctapipe/io/event_preprocessor.py @@ -2,7 +2,7 @@ from astropy.coordinates import angular_separation -from ..coordinates import altaz_to_nominal +from ..coordinates import altaz_to_icrs, altaz_to_nominal from ..core import ( Component, FeatureGenerator, @@ -123,6 +123,45 @@ def _dl2_irf_config(preprocessor): } +@FeatureSetRegistry.register("dl2_to_dl3") +def _dl2_to_dl3_config(preprocessor: "EventPreprocessor"): + return { + "features_to_generate": [ + ("ENERGY", f"{preprocessor.energy_reconstructor}_energy"), + ("ALT", f"{preprocessor.geometry_reconstructor}_alt"), + ("AZ", f"{preprocessor.geometry_reconstructor}_az"), + ("TIME", "time"), + ("EVENT_ID", "event_id"), + ("GAMMANESS", f"{preprocessor.gammaness_reconstructor}_prediction"), + ( + "reco_fov_coord", + "altaz_to_nominal(AZ, ALT, subarray_pointing_lon, subarray_pointing_lat)", + ), + ("FOV_LON", "reco_fov_coord[:,0]"), + ("FOV_LAT", "reco_fov_coord[:,1]"), + ( + "reco_icrs_coord", + "altaz_to_icrs(AZ, ALT, TIME, LOCATION)", + ), + ("RA", "reco_icrs_coord[:,0]"), + ("DEC", "reco_icrs_coord[:,1]"), + ], + "quality_criteria": [], + "output_features": [ + "EVENT_ID", + "TIME", + "RA", + "DEC", + "ENERGY", + "ALT", + "AZ", + "FOV_LON", + "FOV_LAT", + "GAMMANESS", + ], + } + + class EventPreprocessor(Component): """ Selects or generates features and filters tables of events. @@ -198,14 +237,26 @@ def __init__(self, config=None, parent=None, **kwargs): "of features in the configuration (DL2EventPreprocessor.features)." ) - def __call__(self, table): - """Return new table with only the columns in features.""" + def __call__(self, table, **other_attributes): + """ + Return new table with only the columns in features. + + Parameters + ---------- + table: Table + Table to process + **other_attributes: Any + Other functions or objects that the FeatureGenerator should have + access to, in addition to the default ones. + """ # generate new features, which includes renaming columns: generated = self.feature_generator( table, angular_separation=angular_separation, altaz_to_nominal=altaz_to_nominal, + altaz_to_icrs=altaz_to_icrs, + **other_attributes, ) # apply event selection on the resulting table