2525
2626from astropy import units as u
2727from astropy .coordinates .earth import EarthLocation
28- from astropy .coordinates import SkyCoord
28+ from astropy .coordinates import AltAz , SkyCoord
2929from astropy .table import (
3030 Table ,
3131 unique ,
3434)
3535from astropy .time import Time
3636
37+ from ctapipe .coordinates import CameraFrame
3738from ctapipe .core import Component , QualityQuery
3839from ctapipe .core .traits import (
3940 Bool ,
5152from ctapipe .io import read_table
5253from dl1_data_handler .image_mapper import ImageMapper
5354
54- # Reference (dummy) location to insert in the SkyCoord object as the default location
55- #: Area averaged position of LST-1, MAGIC-1 and MAGIC-2 (using 23**2 and 17**2 m2)
56- REFERENCE_LOCATION = EarthLocation (
57- lon = - 17.890879 * u .deg ,
58- lat = 28.761579 * u .deg ,
59- height = 2199 * u .m , # MC obs-level
60- )
6155# Reference (dummy) time to insert in the SkyCoord object as the default time
6256LST_EPOCH = Time ("2018-10-01T00:00:00" , scale = "utc" )
6357
@@ -227,7 +221,6 @@ def __init__(
227221 parent = None ,
228222 ** kwargs ,
229223 ):
230-
231224 super ().__init__ (config = config , parent = parent , ** kwargs )
232225
233226 # Register the destructor to close all open files properly
@@ -576,14 +569,11 @@ def _construct_stereo_example_identifiers(self):
576569 simulation_info = []
577570 example_identifiers = []
578571 for file_idx , (filename , f ) in enumerate (self .files .items ()):
579- if self .process_type == ProcessType .Simulation :
580- # Read simulation information for each observation
581- simulation_info .append (read_table (f , "/configuration/simulation/run" ))
582- # Construct the shower simulation table
583- simshower_table = read_table (f , "/simulation/event/subarray/shower" )
584572 # Read the trigger table.
585573 trigger_table = read_table (f , "/dl1/event/subarray/trigger" )
586574 if self .process_type == ProcessType .Simulation :
575+ # Construct the shower simulation table
576+ simshower_table = read_table (f , "/simulation/event/subarray/shower" )
587577 # The shower simulation table is joined with the subarray trigger table.
588578 trigger_table = join (
589579 left = trigger_table ,
@@ -627,7 +617,7 @@ def _construct_stereo_example_identifiers(self):
627617 right = tel_pointing ,
628618 keys = ["obs_id" , "tel_id" ],
629619 )
630- table_per_type = self ._transform_to_spherical_offsets (
620+ table_per_type = self ._transform_to_cam_coord_offsets (
631621 table_per_type
632622 )
633623 # Apply the multiplicity cut based on the telescope type
@@ -663,6 +653,24 @@ def _multiplicity_cut_subarray(table, key_colnames):
663653 events .add_column (
664654 true_shower_primary_class , name = "true_shower_primary_class"
665655 )
656+ # Read simulation information for each observation
657+ simulation_info_table = read_table (f , "/configuration/simulation/run" )
658+ # Append the simulation information to the list of simulation information
659+ simulation_info .append (simulation_info_table )
660+ # Assuming min_az = max_az and min_alt = max_alt
661+ fix_array_pointing = simulation_info_table .copy ()
662+ fix_array_pointing .keep_columns (["obs_id" , "min_az" , "min_alt" ])
663+ fix_array_pointing .rename_column ("min_az" , "pointing_azimuth" )
664+ fix_array_pointing .rename_column ("min_alt" , "pointing_altitude" )
665+ # Join the prediction table with the telescope pointing table
666+ events = join (
667+ left = events ,
668+ right = fix_array_pointing ,
669+ keys = ["obs_id" ],
670+ )
671+ # TODO: use keep_order for astropy v7.0.0
672+ events .sort (["obs_id" , "event_id" ])
673+ events = self ._transform_to_sky_spher_offsets (events )
666674 # Appending the events to the list of example identifiers
667675 example_identifiers .append (events )
668676
@@ -741,9 +749,9 @@ def _transform_to_log_energy(self, table):
741749 table .add_column (np .log10 (table ["true_energy" ]), name = "log_true_energy" )
742750 return table
743751
744- def _transform_to_spherical_offsets (self , table ) -> Table :
752+ def _transform_to_cam_coord_offsets (self , table ) -> Table :
745753 """
746- Transform Alt/Az coordinates to spherical offsets w.r.t. the telescope pointing.
754+ Transform Alt/Az coordinates to camera coordinate offsets w.r.t. the telescope pointing.
747755
748756 This method converts the Alt/Az coordinates in the provided table to spherical offsets
749757 w.r.t. the telescope pointing. It also calculates the angular separation between the
@@ -758,34 +766,98 @@ def _transform_to_spherical_offsets(self, table) -> Table:
758766 --------
759767 table : astropy.table.Table
760768 A Table with the spherical offsets and the angular separation added as new columns.
761- The telescope pointing columns are removed from the table.
769+ """
770+
771+ tel_tables = []
772+ for tel_id in self .tel_ids :
773+ tel_table = table .copy ()
774+ tel_table .keep_columns (
775+ [
776+ "obs_id" ,
777+ "tel_id" ,
778+ "true_alt" ,
779+ "true_az" ,
780+ "telescope_pointing_azimuth" ,
781+ "telescope_pointing_altitude" ,
782+ ]
783+ )
784+ tel_table = tel_table [tel_table ["tel_id" ] == tel_id ]
785+ # Set the telescope pointing of the SkyOffsetSeparation tranform to the fix pointing
786+ tel_ground_frame = self .subarray .tel_coords [
787+ self .subarray .tel_ids_to_indices (tel_id )
788+ ]
789+ fix_tel_pointing = SkyCoord (
790+ tel_table ["telescope_pointing_azimuth" ],
791+ tel_table ["telescope_pointing_altitude" ],
792+ location = tel_ground_frame .to_earth_location (),
793+ obstime = LST_EPOCH ,
794+ )
795+ camera_frame = CameraFrame (
796+ focal_length = self .subarray .tel [tel_id ].optics .equivalent_focal_length ,
797+ rotation = self .subarray .tel [tel_id ].camera .geometry .pix_rotation ,
798+ telescope_pointing = fix_tel_pointing ,
799+ )
800+ # Transform the true Alt/Az coordinates to camera coordinates
801+ true_direction = SkyCoord (
802+ tel_table ["true_az" ],
803+ tel_table ["true_alt" ],
804+ location = tel_ground_frame .to_earth_location (),
805+ obstime = LST_EPOCH ,
806+ )
807+ true_cam_position = true_direction .transform_to (camera_frame )
808+ true_cam_distance = np .sqrt (
809+ true_cam_position .x ** 2 + true_cam_position .y ** 2
810+ )
811+ tel_table .add_column (true_cam_position .x , name = "cam_coord_offset_x" )
812+ tel_table .add_column (true_cam_position .y , name = "cam_coord_offset_y" )
813+ tel_table .add_column (true_cam_distance , name = "cam_coord_distance" )
814+ tel_tables .append (tel_table )
815+ tel_tables = vstack (tel_tables )
816+ table = join (
817+ left = table ,
818+ right = tel_tables ,
819+ keys = ["obs_id" , "event_id" , "tel_id" ],
820+ )
821+ # TODO: use keep_order for astropy v7.0.0
822+ table .sort (["obs_id" , "event_id" , "tel_id" ])
823+ return table
824+
825+ def _transform_to_sky_spher_offsets (self , table ) -> Table :
826+ """
827+ Transform Alt/Az coordinates to sky spherical offsets w.r.t. the telescope pointing.
828+
829+ This method converts the Alt/Az coordinates in the provided table to sky spherical offsets
830+ w.r.t. the telescope pointing. It also calculates the angular separation between the
831+ true and telescope pointing directions.
832+
833+ Parameters:
834+ -----------
835+ table : astropy.table.Table
836+ A Table containing the true Alt/Az coordinates and telescope pointing.
837+
838+ Returns:
839+ --------
840+ table : astropy.table.Table
841+ A Table with the spherical offsets and the angular separation added as new columns.
762842 """
763843 # Set the telescope pointing of the SkyOffsetSeparation tranform to the fix pointing
764- fix_pointing = SkyCoord (
765- table ["telescope_pointing_azimuth" ],
766- table ["telescope_pointing_altitude" ],
767- frame = "altaz" ,
768- location = REFERENCE_LOCATION ,
844+ fix_array_pointing = SkyCoord (
845+ table ["pointing_azimuth" ],
846+ table ["pointing_altitude" ],
847+ location = self .subarray .reference_location ,
769848 obstime = LST_EPOCH ,
770849 )
771850 true_direction = SkyCoord (
772851 table ["true_az" ],
773852 table ["true_alt" ],
774- frame = "altaz" ,
775- location = REFERENCE_LOCATION ,
853+ location = self .subarray .reference_location ,
776854 obstime = LST_EPOCH ,
777855 )
778856 sky_offset = fix_pointing .spherical_offsets_to (true_direction )
779857 angular_separation = fix_pointing .separation (true_direction )
780- table .add_column (sky_offset [0 ], name = "spherical_offset_az" )
781- table .add_column (sky_offset [1 ], name = "spherical_offset_alt" )
782- table .add_column (angular_separation , name = "angular_separation" )
783- table .remove_columns (
784- [
785- "telescope_pointing_azimuth" ,
786- "telescope_pointing_altitude" ,
787- ]
788- )
858+ table .add_column (sky_offset [0 ], name = "sky_offset_lon" )
859+ table .add_column (sky_offset [1 ], name = "sky_offset_lat" )
860+ table .add_column (angular_separation , name = "sky_angular_separation" )
789861 return table
790862
791863 def get_parameters (self , batch , dl1b_parameter_list ) -> np .array :
@@ -901,7 +973,9 @@ def generate_stereo_batch(self, batch_indices) -> Table:
901973 batch = self ._append_features (batch )
902974 # Add blank inputs for missing telescopes in the batch
903975 if self .process_type == ProcessType .Simulation :
904- batch_grouped = batch .group_by (["obs_id" , "event_id" , "true_shower_primary_class" ])
976+ batch_grouped = batch .group_by (
977+ ["obs_id" , "event_id" , "true_shower_primary_class" ]
978+ )
905979 elif self .process_type == ProcessType .Observation :
906980 batch_grouped = batch .group_by (["obs_id" , "event_id" ])
907981 for group_element in batch_grouped .groups :
@@ -927,26 +1001,27 @@ def generate_stereo_batch(self, batch_indices) -> Table:
9271001 if "features" in group_element .colnames :
9281002 blank_input_row ["features" ] = blank_input
9291003 if "mono_feature_vectors" in group_element .colnames :
930- blank_input_row ["mono_feature_vectors" ] = (
931- blank_mono_feature_vectors
932- )
1004+ blank_input_row [
1005+ "mono_feature_vectors"
1006+ ] = blank_mono_feature_vectors
9331007 if "stereo_feature_vectors" in group_element .colnames :
934- blank_input_row ["stereo_feature_vectors" ] = (
935- blank_stereo_feature_vectors
936- )
1008+ blank_input_row [
1009+ "stereo_feature_vectors"
1010+ ] = blank_stereo_feature_vectors
9371011 batch .add_row (blank_input_row )
9381012 # Sort the batch with the new rows of blank inputs
9391013 batch .sort (["obs_id" , "event_id" , "tel_type_id" , "tel_id" ])
9401014 return batch
9411015
9421016 def __destructor (self ):
9431017 """Destructor to ensure all opened HDF5 files are properly closed."""
944- if hasattr (self , "files" ): # Ensure self.files exists before attempting to close
1018+ if hasattr (
1019+ self , "files"
1020+ ): # Ensure self.files exists before attempting to close
9451021 for file_name in list (self .files .keys ()):
9461022 if self .files [file_name ].isopen : # Check if file is still open
9471023 self .files [file_name ].close ()
9481024
949-
9501025 @abstractmethod
9511026 def _append_features (self , batch ) -> Table :
9521027 pass
@@ -1078,7 +1153,6 @@ def __init__(
10781153 parent = None ,
10791154 ** kwargs ,
10801155 ):
1081-
10821156 super ().__init__ (
10831157 input_url_signal = input_url_signal ,
10841158 input_url_background = input_url_background ,
@@ -1337,7 +1411,6 @@ def __init__(
13371411 parent = None ,
13381412 ** kwargs ,
13391413 ):
1340-
13411414 super ().__init__ (
13421415 input_url_signal = input_url_signal ,
13431416 input_url_background = input_url_background ,
0 commit comments