2121
2222__all__ = ["Mir" , "generate_sma_antpos_dict" ]
2323
24+ MAX_SMA_ANTENNA_NUMBER = 10
25+
2426
2527def generate_sma_antpos_dict (filepath ):
2628 """
@@ -70,6 +72,102 @@ def generate_sma_antpos_dict(filepath):
7072 return {item ["antenna" ]: item ["xyz_pos" ] for item in mir_antpos }
7173
7274
75+ def calc_delta_uvw_from_offset_illum (
76+ * ,
77+ illum_dict ,
78+ ant_1_array ,
79+ ant_2_array ,
80+ app_ra ,
81+ app_dec ,
82+ frame_pa ,
83+ lst_array ,
84+ telescope_lat ,
85+ ):
86+ """
87+ Calculate uvw offsets from illumination pattern offsets.
88+
89+ Parameters
90+ ----------
91+ illum_dict : dict
92+ Dictionary which defines the illumination offset constants for each antenna.
93+ Keys are matched to antenna numbers, values are themselves dicts containing
94+ four key/value pairs - "x0" (constant horizontal offset on the primary/after the
95+ Nasmyth), "y0" (constant vertical offset), "x1" (horizontal offset before the
96+ Nasmyth), "y1" (vertical offset before the Nasmyth). Values are in units of
97+ meters, as realized on the primary.
98+ ant_1_array : ndarray of int
99+ Number of the first antenna in a given baseline pair. Shape (Nblts,).
100+ ant_2_array : ndarray of int
101+ Number of the second antenna in a given baseline pair. Shape (Nblts,).
102+ app_ra : ndarray of float
103+ Apparent right ascension for each baseline record. Shape (Nblts,), units of
104+ radians.
105+ app_dec : ndarray of float
106+ Apparent declination for each baseline record. Shape (Nblts,), units of radians.
107+ frame_pa : ndarray of float
108+ Frame position angle for each baseline record. Shape (Nblts,), units of radians.
109+ lst_array : ndarray of float
110+ LST value for each baseline-time record. Shape (Nblts,), units of radians.
111+ telescope_lat : float
112+ Telescope latitude. Units of radians.
113+
114+ """
115+ import erfa
116+
117+ fix_x = np .zeros (MAX_SMA_ANTENNA_NUMBER + 1 )
118+ fix_y = np .zeros (MAX_SMA_ANTENNA_NUMBER + 1 )
119+ rot_x = np .zeros (MAX_SMA_ANTENNA_NUMBER + 1 )
120+ rot_y = np .zeros (MAX_SMA_ANTENNA_NUMBER + 1 )
121+
122+ if np .any (ant_1_array < 0 ) or np .any (ant_1_array > MAX_SMA_ANTENNA_NUMBER ):
123+ raise ValueError (
124+ f"Values in ant_1_array out of range [0, { MAX_SMA_ANTENNA_NUMBER } ])."
125+ )
126+ if np .any (ant_2_array < 0 ) or np .any (ant_2_array > MAX_SMA_ANTENNA_NUMBER ):
127+ raise ValueError (
128+ f"Values in ant_2_array out of range [0, { MAX_SMA_ANTENNA_NUMBER } ])."
129+ )
130+
131+ try :
132+ for ant , indv_dict in illum_dict .items ():
133+ fix_x [ant ] = indv_dict ["x0" ]
134+ fix_y [ant ] = indv_dict ["y0" ]
135+ rot_x [ant ] = indv_dict ["x1" ]
136+ rot_y [ant ] = indv_dict ["y1" ]
137+ except KeyError as err :
138+ raise KeyError ("Invalid keys in illum_dict." ) from err
139+
140+ # Add the things we need to rotate first
141+ uvw_offsets = np .zeros ((len (ant_1_array ), 3 , 1 ))
142+ uvw_offsets [:, 0 ] = rot_x [ant_1_array ] - rot_x [ant_2_array ]
143+ uvw_offsets [:, 1 ] = rot_y [ant_1_array ] - rot_y [ant_2_array ]
144+
145+ # Calculate the elevation angle, then start some rotations!
146+ _ , el_arr = erfa .hd2ae (lst_array - app_ra , app_dec , telescope_lat )
147+ uvw_offsets = utils .phasing ._rotate_one_axis (
148+ uvw_offsets [:, :, np .newaxis ], rot_amount = el_arr , rot_axis = 2
149+ )
150+
151+ # Add the static offsets - everything is now oriented in the frame of the antenna
152+ # Note we need the extra axis here since nvec=1 for _rotate_one_axis
153+ uvw_offsets [:, 0 , 0 ] += fix_x [ant_1_array ] - fix_x [ant_2_array ]
154+ uvw_offsets [:, 1 , 0 ] += fix_y [ant_1_array ] - fix_y [ant_2_array ]
155+
156+ # Calculate the position angle, accounting for the frame PA as well
157+ pa_array = frame_pa + utils .phasing .calc_parallactic_angle (
158+ app_ra = app_ra , app_dec = app_dec , lst_array = lst_array , telescope_lat = telescope_lat
159+ )
160+
161+ # Rotate by the PA so that we get the UVWs in the right orientation
162+ uvw_offsets = utils .phasing ._rotate_one_axis (
163+ uvw_offsets , rot_amount = pa_array , rot_axis = 2
164+ )
165+
166+ # This should be ready to straight-up add, striping out the extra axis that
167+ # the _rotate functions add (since nvec=1)
168+ return np .squeeze (uvw_offsets , axis = - 1 )
169+
170+
73171class Mir (UVData ):
74172 """
75173 A class for Mir file objects.
@@ -98,6 +196,7 @@ def read_mir(
98196 apply_tsys = True ,
99197 apply_flags = True ,
100198 apply_dedoppler = False ,
199+ illum_dict = None ,
101200 pseudo_cont = False ,
102201 rechunk = None ,
103202 compass_soln = None ,
@@ -181,6 +280,7 @@ def read_mir(
181280 apply_flags = apply_flags ,
182281 apply_tsys = apply_tsys ,
183282 apply_dedoppler = apply_dedoppler ,
283+ illum_dict = illum_dict ,
184284 metadata_only = metadata_only ,
185285 )
186286
@@ -285,6 +385,7 @@ def _init_from_mir_parser(
285385 apply_flags = True ,
286386 apply_dedoppler = False ,
287387 metadata_only = False ,
388+ illum_dict = None ,
288389 ):
289390 """
290391 Convert a MirParser object into a UVData object.
@@ -299,6 +400,28 @@ def _init_from_mir_parser(
299400 "flexible polarization", which compresses the polarization-axis of various
300401 attributes to be of length 1, sets the `flex_spw_polarization_array`
301402 attribute to define the polarization per spectral window. Default is True.
403+ apply_flags : bool
404+ If set to True, apply "wideband" flags to the visibilities, which are
405+ recorded by the realtime system to denote when data are expected to be bad
406+ (e.g., antennas not on source, dewar warm). Default it true.
407+ apply_tsys : bool
408+ If set to False, data are returned as correlation coefficients (normalized
409+ by the auto-correlations). Default is True, which instead scales the raw
410+ visibilities and forward-gain of the antenna to produce values in Jy
411+ (uncalibrated).
412+ apply_dedoppler : bool
413+ If set to True, data will be corrected for any doppler-tracking performed
414+ during observations, and brought into the topocentric rest frame (default
415+ for UVData objects). Default is False.
416+ metadata_only : bool
417+ Read in only the metadata, ignore visibility data. Default is False.
418+ illum_dict : dict
419+ Dictionary which defines the illumination offset constants for each antenna.
420+ Keys are matched to antenna numbers, values are themselves dicts containing
421+ four key/value pairs - "x0" (constant horizontal offset on the primary/after
422+ the Nasmyth), "y0" (constant vertical offset), "x1" (horizontal offset
423+ before the Nasmyth), "y1" (vertical offset before the Nasmyth). Values are
424+ in units of meters, as realized on the primary.
302425 """
303426 # Create a simple array/list for broadcasting values stored on a
304427 # per-blt basis into per-spw records, and per-time into per-blt records
@@ -745,7 +868,7 @@ def _init_from_mir_parser(
745868
746869 # Need to flip the sign convention here on uvw, since we use a1-a2 versus the
747870 # standard a2-a1 that uvdata expects
748- self .uvw_array = ( - 1.0 ) * uvw_array
871+ self .uvw_array = - uvw_array
749872
750873 self .vis_units = "Jy"
751874 self .pol_convention = "avg"
@@ -991,6 +1114,18 @@ def _init_from_mir_parser(
9911114 # Plug in the new cat_id into the phase_center_id_array
9921115 self .phase_center_id_array [inhid_mask ] = cat_id
9931116
1117+ if illum_dict is not None :
1118+ self .uvw_array += calc_delta_uvw_from_offset_illum (
1119+ illum_dict = illum_dict ,
1120+ ant_1_array = self .ant_1_array ,
1121+ ant_2_array = self .ant_2_array ,
1122+ app_ra = self .phase_center_app_ra ,
1123+ app_dec = self .phase_center_app_dec ,
1124+ frame_pa = self .phase_center_frame_pa ,
1125+ lst_array = self .lst_array ,
1126+ telescope_lat = self .telescope .location .lat .rad ,
1127+ )
1128+
9941129 def write_mir (self , filename ):
9951130 """
9961131 Write out the SMA MIR files.
0 commit comments