11"""Module containing classes related to event loading and preprocessing"""
22
3- from enum import StrEnum , auto
4-
53from astropy .coordinates import angular_separation
6- from traitlets import default
74
85from ..coordinates import altaz_to_nominal
96from ..core import (
1714__all__ = ["EventPreprocessor" ]
1815
1916
20- class PreprocessorFeatureSet (StrEnum ):
21- """Pre-defined configurations for DL2EventPreprocessor for specific use cases."""
17+ from typing import Callable
18+
19+
20+ class FeatureSetRegistry :
21+ """Registry for custom feature set configurations."""
2222
23- custom = auto () #: use user-supplied configuration
24- dl2_irf = auto () #: support IRF preprocessing use case
23+ _registry = {}
24+
25+ @classmethod
26+ def register (cls , name : str ):
27+ """Register a feature set configuration.
28+
29+ Examples
30+ --------
31+ >>> @FeatureSetRegistry.register("my_analysis")
32+ ... def my_config(preprocessor):
33+ ... return {
34+ ... "features_to_generate": [("custom", "col_a / col_b")],
35+ ... "quality_criteria": [("cut", "custom > 0.5")],
36+ ... "output_features": ["event_id", "custom"]
37+ ... }
38+ """
39+
40+ def decorator (func : Callable ):
41+ cls ._registry [name ] = func
42+ return func
43+
44+ return decorator
45+
46+ @classmethod
47+ def get (cls , name : str ):
48+ """Get a registered configuration function."""
49+ return cls ._registry .get (name )
50+
51+ @classmethod
52+ def list_available (cls ):
53+ """List all registered feature set names."""
54+ return list (cls ._registry .keys ())
55+
56+
57+ @FeatureSetRegistry .register ("dl2_irf" )
58+ def _dl2_irf_config (preprocessor ):
59+ """Built-in configuration for DL2 IRF generation."""
60+ return {
61+ "features_to_generate" : [
62+ ("reco_energy" , f"{ preprocessor .energy_reconstructor } _energy" ),
63+ ("reco_alt" , f"{ preprocessor .geometry_reconstructor } _alt" ),
64+ ("reco_az" , f"{ preprocessor .geometry_reconstructor } _az" ),
65+ ("gh_score" , f"{ preprocessor .gammaness_reconstructor } _prediction" ),
66+ ("theta" , "angular_separation(reco_az, reco_alt, true_az, true_alt)" ),
67+ (
68+ "reco_fov_coord" ,
69+ "altaz_to_nominal(reco_az, reco_alt, subarray_pointing_lon, subarray_pointing_lat)" ,
70+ ),
71+ (
72+ "reco_fov_lon" ,
73+ "reco_fov_coord[:,0]" ,
74+ ), # note: GADF IRFs use the negative of this
75+ ("reco_fov_lat" , "reco_fov_coord[:,1]" ),
76+ (
77+ "true_fov_coord" ,
78+ "altaz_to_nominal(true_az, true_alt, subarray_pointing_lon, subarray_pointing_lat)" ,
79+ ),
80+ (
81+ "true_fov_lon" ,
82+ "true_fov_coord[:,0]" ,
83+ ), # note: GADF IRFs use the negative of this
84+ ("true_fov_lat" , "true_fov_coord[:,1]" ),
85+ (
86+ "true_fov_offset" ,
87+ "angular_separation(true_fov_lon, true_fov_lat, 0*u.deg, 0*u.deg)" ,
88+ ),
89+ (
90+ "reco_fov_offset" ,
91+ "angular_separation(reco_fov_lon, reco_fov_lat, 0*u.deg, 0*u.deg)" ,
92+ ),
93+ (
94+ "multiplicity" ,
95+ f"np.count_nonzero({ preprocessor .gammaness_reconstructor } _telescopes,axis=1)" ,
96+ ),
97+ ],
98+ "quality_criteria" : [
99+ ("Valid geometry" , f"{ preprocessor .geometry_reconstructor } _is_valid" ),
100+ ("valid energy" , f"{ preprocessor .energy_reconstructor } _is_valid" ),
101+ ("valid gammaness" , f"{ preprocessor .gammaness_reconstructor } _is_valid" ),
102+ ("sufficient multiplicity" , "multiplicity >= 4" ),
103+ ],
104+ "output_features" : [
105+ "event_id" ,
106+ "obs_id" ,
107+ "reco_energy" ,
108+ "reco_alt" ,
109+ "reco_az" ,
110+ "gh_score" ,
111+ "true_energy" ,
112+ "true_alt" ,
113+ "true_az" ,
114+ "true_fov_offset" ,
115+ "reco_fov_offset" ,
116+ "theta" ,
117+ "reco_fov_lat" ,
118+ "true_fov_lat" ,
119+ "reco_fov_lon" ,
120+ "true_fov_lon" ,
121+ "multiplicity" ,
122+ ],
123+ }
25124
26125
27126class EventPreprocessor (Component ):
@@ -56,9 +155,9 @@ class with the columns you to retain in the output table.
56155 help = "Prefix of the classifier `_prediction` column" ,
57156 ).tag (config = True )
58157
59- feature_set = traits .UseEnum (
60- PreprocessorFeatureSet ,
61- default_value = PreprocessorFeatureSet . dl2_irf ,
158+ feature_set = traits .CaselessStrEnum (
159+ [ "custom" ] + FeatureSetRegistry . list_available () ,
160+ default_value = "custom" ,
62161 help = (
63162 "Set up the FeatureGenerator.features, output features, and quality criteria "
64163 "based on standard use cases."
@@ -79,16 +178,18 @@ class with the columns you to retain in the output table.
79178
80179 def __init__ (self , config = None , parent = None , ** kwargs ):
81180 super ().__init__ (config = config , parent = parent , ** kwargs )
82- if self .feature_set == PreprocessorFeatureSet . custom :
181+ if self .feature_set == " custom" :
83182 self .feature_generator = FeatureGenerator (parent = self )
84183 self .quality_query = QualityQuery (parent = self )
85- else :
184+ else : # use a pre-registered feature set
185+ feature_set = FeatureSetRegistry .get (self .feature_set )(self )
86186 self .feature_generator = FeatureGenerator (
87- parent = self , features = self . _get_predefined_features_to_generate ()
187+ parent = self , features = feature_set [ "features_to_generate" ]
88188 )
89189 self .quality_query = QualityQuery (
90- parent = self , quality_criteria = self . _get_predefined_quality_criteria ()
190+ parent = self , quality_criteria = feature_set [ "quality_criteria" ]
91191 )
192+ self .features = feature_set ["output_features" ]
92193 # sanity checks:
93194 if len (self .features ) == 0 :
94195 raise ToolConfigurationError (
@@ -114,92 +215,3 @@ def __call__(self, table):
114215 # return only the columns specified in `self.features`, and rows in
115216 # `selected_mask`
116217 return generated [self .features ][selected_mask ]
117-
118- def _get_predefined_features_to_generate (self ) -> list [tuple ]:
119- """Return a default list of FeatureGenerator features."""
120- if self .feature_set == PreprocessorFeatureSet .dl2_irf :
121- # Default features for DL2/Subarray events
122- return [
123- ("reco_energy" , f"{ self .energy_reconstructor } _energy" ),
124- ("reco_alt" , f"{ self .geometry_reconstructor } _alt" ),
125- ("reco_az" , f"{ self .geometry_reconstructor } _az" ),
126- ("gh_score" , f"{ self .gammaness_reconstructor } _prediction" ),
127- ("theta" , "angular_separation(reco_az, reco_alt, true_az, true_alt)" ),
128- (
129- "reco_fov_coord" ,
130- "altaz_to_nominal(reco_az, reco_alt, subarray_pointing_lon, subarray_pointing_lat)" ,
131- ),
132- (
133- "reco_fov_lon" ,
134- "reco_fov_coord[:,0]" ,
135- ), # note: GADF IRFs use the negative of this
136- ("reco_fov_lat" , "reco_fov_coord[:,1]" ),
137- (
138- "true_fov_coord" ,
139- "altaz_to_nominal(true_az, true_alt, subarray_pointing_lon, subarray_pointing_lat)" ,
140- ),
141- (
142- "true_fov_lon" ,
143- "true_fov_coord[:,0]" ,
144- ), # note: GADF IRFs use the negative of this
145- ("true_fov_lat" , "true_fov_coord[:,1]" ),
146- (
147- "true_fov_offset" ,
148- "angular_separation(true_fov_lon, true_fov_lat, 0*u.deg, 0*u.deg)" ,
149- ),
150- (
151- "reco_fov_offset" ,
152- "angular_separation(reco_fov_lon, reco_fov_lat, 0*u.deg, 0*u.deg)" ,
153- ),
154- (
155- "multiplicity" ,
156- f"np.count_nonzero({ self .gammaness_reconstructor } _telescopes,axis=1)" ,
157- ),
158- ]
159- else :
160- raise NotImplementedError (f"unsupported feature_set: { self .feature_set } " )
161-
162- def _get_predefined_quality_criteria (self ) -> list [tuple ]:
163- """
164- Set the quality criteria for a DL2FeatureSet.
165-
166- Here you can use any columns in the input table, or any that are
167- specified in the FeatureGenerator.
168- """
169- if self .feature_set == PreprocessorFeatureSet .dl2_irf :
170- return [
171- ("Valid geometry" , f"{ self .geometry_reconstructor } _is_valid" ),
172- ("valid energy" , f"{ self .energy_reconstructor } _is_valid" ),
173- ("valid gammaness" , f"{ self .gammaness_reconstructor } _is_valid" ),
174- ("sufficient multiplicity" , "multiplicity >= 4" ),
175- ]
176- else :
177- raise NotImplementedError (f"unsupported feature_set: { self .feature_set } " )
178-
179- @default ("features" )
180- def _features (self ):
181- """Set the columns to output, for a given FeatureSet."""
182- if self .feature_set == PreprocessorFeatureSet .dl2_irf :
183- return [
184- "event_id" ,
185- "obs_id" ,
186- "reco_energy" ,
187- "reco_alt" ,
188- "reco_az" ,
189- "gh_score" ,
190- "true_energy" ,
191- "true_alt" ,
192- "true_az" ,
193- "true_fov_offset" ,
194- "reco_fov_offset" ,
195- "theta" ,
196- "reco_fov_lat" ,
197- "true_fov_lat" ,
198- "reco_fov_lon" ,
199- "true_fov_lon" ,
200- "multiplicity" ,
201- ]
202- elif self .feature_set == PreprocessorFeatureSet .custom :
203- return []
204- else :
205- raise NotImplementedError (f"unsupported feature_set: { self .feature_set } " )
0 commit comments