66from scipy import spatial
77from scipy .sparse import csr_matrix
88from collections import Counter , namedtuple
9+ import tables
10+ from importlib .resources import files
11+ import astropy .units as u
912
1013from ctapipe .instrument .camera import PixelShape
11- from ctapipe .core import Component
12- from ctapipe .core .traits import Bool , Int , Float
14+ from ctapipe .core import TelescopeComponent
15+ from ctapipe .core .traits import Bool , Int
1316
1417__all__ = [
1518 "ImageMapper" ,
2124 "RebinMapper" ,
2225 "ShiftingMapper" ,
2326 "SquareMapper" ,
27+ "HexagonalPatchMapper" ,
2428]
2529
26- class ImageMapper (Component ):
30+ class ImageMapper (TelescopeComponent ):
2731 """
2832 Base component for mapping raw 1D vectors into 2D mapped images.
2933
@@ -56,6 +60,8 @@ class ImageMapper(Component):
5660 Multiplication factor used for rebinning.
5761 index_matrix : numpy.ndarray or None
5862 Matrix used for indexing, initialized to None.
63+ cam_neighbor_array : numpy.ndarray or None
64+ Matrix used for indexing, initialized to None.
5965
6066 Methods
6167 -------
@@ -84,29 +90,15 @@ def __init__(
8490 parent : ctapipe.core.Component or ctapipe.core.Tool
8591 Parent of this component in the configuration hierarchy,
8692 this is mutually exclusive with passing ``config``
87- **kwargs
88- Additional keyword arguments for traitlets. Non-traitlet kwargs
89- (like 'subarray') are filtered out for compatibility.
9093 """
9194
92- # Filter out non-traitlet kwargs before passing to Component
93- # This allows compatibility with ctapipe's reader which may pass extra kwargs
94- component_kwargs = {
95- key : value for key , value in kwargs .items ()
96- if self .class_own_traits ().get (key ) is not None
97- }
98-
99- super ().__init__ (
100- config = config ,
101- parent = parent ,
102- ** component_kwargs ,
103- )
104-
10595 # Camera types
10696 self .geometry = geometry
10797 self .camera_type = self .geometry .name
10898 self .n_pixels = self .geometry .n_pixels
10999 # Rotate the pixel positions by the pixel to align
100+ if self .camera_type == "AdvCamSiPM" :
101+ self .geometry .pix_rotation = 8.213 * u .deg
110102 self .geometry .rotate (self .geometry .pix_rotation )
111103
112104 self .pix_x = np .around (
@@ -122,7 +114,7 @@ def __init__(
122114 # Additional smooth the ticks for 'DigiCam', 'RealLSTCam' and 'CHEC' cameras
123115 if self .camera_type in ["DigiCam" , "RealLSTCam" ]:
124116 self .pix_y , self .y_ticks = self ._smooth_ticks (self .pix_y , self .y_ticks )
125- if self .camera_type == "CHEC" :
117+ if self .camera_type in [ "CHEC" , "AdvCamSiPM" ] :
126118 self .pix_x , self .x_ticks = self ._smooth_ticks (self .pix_x , self .x_ticks )
127119 self .pix_y , self .y_ticks = self ._smooth_ticks (self .pix_y , self .y_ticks )
128120
@@ -140,6 +132,7 @@ def __init__(
140132
141133 # Set the indexed matrix to None
142134 self .index_matrix = None
135+ self .cam_neighbor_array = None
143136
144137 def map_image (self , raw_vector : np .array ) -> np .array :
145138 """
@@ -374,7 +367,7 @@ def _get_grids_for_oversampling(
374367 )
375368 # Adjust for odd tick_diff
376369 # TODO: Check why MAGICCam, VERITAS, and UNKNOWN-7987PX (AdvCam) do not need this adjustment
377- if tick_diff % 2 != 0 and self .camera_type not in ["MAGICCam" , "VERITAS" , "UNKNOWN-7987PX " ]:
370+ if tick_diff % 2 != 0 and self .camera_type not in ["MAGICCam" , "VERITAS" , "AdvCamSiPM " ]:
378371 grid_second .insert (
379372 0 ,
380373 np .around (
@@ -664,6 +657,90 @@ def _get_grids(
664657 return input_grid , output_grid
665658
666659
660+ class HexagonalPatchMapper (ImageMapper ):
661+ """
662+ HexagonalPatchMapper retrieves the necessary information to perform indexed
663+ convolutions, also allows croping the images following the "sipm_patches.h5"
664+ patches geometry and reorders the pixels.
665+
666+ This class extends the functionality of ImageMapper by implementing
667+ methods to look up at the configuration file and perform the image cropping.
668+ It is particularly useful for applications where we are working with waveforms
669+ with high time dimension.
670+ """
671+
672+ def __init__ (
673+ self ,
674+ geometry ,
675+ config = None ,
676+ parent = None ,
677+ ** kwargs ,
678+ ):
679+ super ().__init__ (
680+ geometry = geometry ,
681+ config = config ,
682+ parent = parent ,
683+ ** kwargs ,
684+ )
685+
686+ if geometry .pix_type != PixelShape .HEXAGON :
687+ raise ValueError (
688+ f"HexagonalPatchMapper is only available for hexagonal pixel cameras. Pixel type of the selected camera is '{ geometry .pix_type } '."
689+ )
690+
691+ if geometry .name == "AdvCamSiPM" :
692+ path = files ("dl1_data_handler.ressources" ).joinpath ("triggergeometry_AdvCam_v1.h5" )
693+ with tables .open_file (path , mode = "r" ) as f :
694+ self .trigger_patches = f .root .patches .masks [:]
695+ self .index_map = f .root .mappings .index_map [:]
696+ self .neighbor_array = f .root .neighbors .patch0_neighbors [:]
697+ self .cam_neighbor_array = f .root .neighbors .camera_neighbors [:]
698+ self .fl_neighbor_array_tdscan = f .root .neighbors .flower_neighbors_tdscan [:]
699+ self .fl_neighbor_array_l1 = f .root .neighbors .flower_neighbors_l1 [:]
700+ self .feb_indices = f .root .modules .indices [:]
701+ self .feb_neighbors = f .root .neighbors .feb_neighbors [:]
702+ self .supfl_neighbor_array_l1 = f .root .neighbors .superflower_neighbors_l1 [:]
703+ self .sectors_bool = f .root .sectors .mask [:]
704+ self .sectors_indices = f .root .sectors .sectors_indices [:]
705+ self .sect0_neighbors = f .root .sectors .sect0_neighbors [:]
706+ self .sector_mappings = f .root .sectors .mapping [:]
707+ # Remove -1 padding from each row
708+ self .neighbor_tdscan_eps1_list = [row [row != - 1 ].tolist () for row in self .fl_neighbor_array_tdscan ]
709+ self .fl_neighbor_l1_list = [row [row != - 1 ].tolist () for row in self .fl_neighbor_array_l1 ]
710+ self .supfl_neighbor_l1_list = [row [row != - 1 ].tolist () for row in self .supfl_neighbor_array_l1 ]
711+
712+
713+ self .num_patches = len (self .trigger_patches )
714+ self .patch_size = self .neighbor_array .shape [0 ]
715+ self .sector_size = self .sect0_neighbors .shape [0 ]
716+
717+ self .supfl_neighbor_l1_mask = self .supfl_neighbor_array_l1 >= 0
718+ # Retrieve the camera neighbor array to perform convolutions with cameras different from AdvCamSiPM.
719+ else :
720+ self .log .debug (f"Computing neighbor array for { geometry .name } ..." )
721+ neighbor_matrix = geometry .neighbor_matrix
722+ num_pixels = neighbor_matrix .shape [0 ]
723+ neighbor_lists = []
724+ for i in range (num_pixels ):
725+ # Find indices where the row is True
726+ neighbors = np .where (neighbor_matrix [i ])[0 ]
727+ neighbor_lists .append ([i ] + neighbors .tolist ())
728+
729+ self .cam_neighbor_array = np .full ((num_pixels , 7 ), - 1 , dtype = int )
730+ for i , neighbors in enumerate (neighbor_lists ):
731+ self .cam_neighbor_array [i , :len (neighbors )] = neighbors
732+
733+ def get_reordered_patch (self , raw_vector , patch_index , out_size ):
734+ # Retrieve the patch needed remapped to a standarized patch order.
735+ if out_size == "patch" :
736+ mapper = self .index_map
737+ else : #sector
738+ mapper = self .sector_mappings
739+ mapper = mapper [patch_index ]
740+ unmapped_waveform = raw_vector [mapper ]
741+ return unmapped_waveform
742+
743+
667744class ShiftingMapper (ImageMapper ):
668745 """
669746 ShiftingMapper applies a shifting transformation to map images
@@ -1182,16 +1259,6 @@ class RebinMapper(ImageMapper):
11821259 ),
11831260 ).tag (config = True )
11841261
1185- max_memory_gb = Float (
1186- default_value = 10 ,
1187- allow_none = True ,
1188- help = (
1189- "Maximum memory in GB that RebinMapper is allowed to allocate. "
1190- "Set to None to disable memory checks. Default is 10 GB. "
1191- "Note: RebinMapper uses approximately (image_shape * 10)^2 * image_shape^2 * 4 bytes."
1192- ),
1193- ).tag (config = True )
1194-
11951262 def __init__ (
11961263 self ,
11971264 geometry ,
@@ -1231,26 +1298,6 @@ def __init__(
12311298 self .image_shape = self .interpolation_image_shape
12321299 self .internal_shape = self .image_shape + self .internal_pad * 2
12331300 self .rebinning_mult_factor = 10
1234-
1235- # Validate memory requirements before proceeding (if max_memory_gb is set)
1236- if self .max_memory_gb is not None :
1237- # RebinMapper uses a fine grid (internal_shape * rebinning_mult_factor)^2
1238- # and creates a mapping matrix of shape (fine_grid_size, internal_shape, internal_shape)
1239- fine_grid_size = (self .internal_shape * self .rebinning_mult_factor ) ** 2
1240- estimated_memory_gb = (
1241- fine_grid_size * self .internal_shape * self .internal_shape * 4
1242- ) / (1024 ** 3 ) # 4 bytes per float32
1243-
1244- if estimated_memory_gb > self .max_memory_gb :
1245- raise ValueError (
1246- f"RebinMapper with image_shape={ self .image_shape } would require "
1247- f"approximately { estimated_memory_gb :.1f} GB of memory, which exceeds "
1248- f"the limit of { self .max_memory_gb :.1f} GB. "
1249- f"To allow this allocation, set max_memory_gb to a higher value or None. "
1250- f"Alternatively, consider using a smaller interpolation_image_shape (recommended < 60) "
1251- f"or use BilinearMapper or BicubicMapper instead, which are more memory-efficient."
1252- )
1253-
12541301 # Creating the hexagonal and the output grid for the conversion methods.
12551302 input_grid , output_grid = super ()._get_grids_for_interpolation ()
12561303 # Calculate the mapping table
0 commit comments