Commits
Vincent Geers authored 18dd01b3a70
1 1 | import collections |
2 2 | from operator import itemgetter, attrgetter |
3 - | from typing import Dict, List, Optional, Set |
4 3 | |
5 4 | import pipeline.infrastructure as infrastructure |
6 5 | import pipeline.infrastructure.basetask as basetask |
7 6 | import pipeline.infrastructure.callibrary as callibrary |
8 7 | import pipeline.infrastructure.utils as utils |
9 8 | import pipeline.infrastructure.vdp as vdp |
10 9 | from pipeline.domain import MeasurementSet |
11 10 | from pipeline.h.heuristics import caltable as caltable_heuristic |
12 11 | from pipeline.h.heuristics.tsysspwmap import tsysspwmap |
13 12 | from pipeline.infrastructure import casa_tasks |
84 83 | 'vis': self.vis, |
85 84 | 'caltable': self.caltable |
86 85 | } |
87 86 | |
88 87 | |
89 88 | set_equivalent_casa_task('h_tsyscal') | .
90 89 | set_casa_commands_comment('The Tsys calibration and spectral window map is computed.') | .
91 90 | class Tsyscal(basetask.StandardTaskTemplate): |
92 91 | Inputs = TsyscalInputs |
93 92 | |
94 - | def prepare(self): |
93 + | def prepare(self) -> resultobjects.TsyscalResults: |
95 94 | inputs = self.inputs |
96 95 | |
97 96 | # make a note of the current inputs state before we start fiddling |
98 97 | # with it. This origin will be attached to the final CalApplication. |
99 98 | origin = callibrary.CalAppOrigin(task=Tsyscal, inputs=inputs.to_casa_args()) |
100 99 | |
101 100 | # construct the Tsys cal file |
102 101 | gencal_args = inputs.to_casa_args() |
103 102 | gencal_job = casa_tasks.gencal(caltype='tsys', **gencal_args) |
104 103 | self._executor.execute(gencal_job) |
109 108 | LOG.todo('tsysspwmap heuristic won\'t handle missing file') |
110 109 | nospwmap, spwmap = tsysspwmap(ms=inputs.ms, tsystable=tsys_table, tsysChanTol=inputs.chantol) |
111 110 | |
112 111 | calfrom_defaults = dict(caltype='tsys', spwmap=spwmap, interp='linear,linear') |
113 112 | |
114 113 | is_single_dish = utils.contains_single_dish(inputs.context) |
115 114 | calapps = get_calapplications(inputs.ms, tsys_table, calfrom_defaults, origin, spwmap, is_single_dish) |
116 115 | |
117 116 | return resultobjects.TsyscalResults(pool=calapps, unmappedspws=nospwmap) |
118 117 | |
119 - | def analyse(self, result): |
118 + | def analyse(self, result: resultobjects.TsyscalResults) -> resultobjects.TsyscalResults: |
120 119 | # double-check that the caltable was actually generated |
121 120 | on_disk = [ca for ca in result.pool if ca.exists()] |
122 121 | result.final[:] = on_disk |
123 122 | |
124 123 | missing = [ca for ca in result.pool if ca not in on_disk] |
125 124 | result.error.clear() |
126 125 | result.error.update(missing) |
127 126 | |
128 127 | return result |
129 128 | |
130 129 | |
131 130 | # Holds an observing intent and the preferred/fallback gainfield args to be used for that intent |
132 131 | GainfieldMapping = collections.namedtuple('GainfieldMapping', 'intent preferred fallback') |
133 132 | |
134 133 | |
135 - | def get_solution_map(ms: MeasurementSet, is_single_dish: bool) -> List[GainfieldMapping]: |
134 + | def get_solution_map(ms: MeasurementSet, is_single_dish: bool) -> list[GainfieldMapping]: |
136 135 | """ |
137 136 | Get gainfield solution map. Different solution maps are returned for |
138 137 | single dish and interferometric data. |
139 138 | |
140 - | :param ms: MS to analyse |
141 - | :param is_single_dish: True if MS is single dish data |
142 - | :return: list of GainfieldMappings |
139 + | Args: |
140 + | ms: MS to analyse. |
141 + | is_single_dish: True if MS is single dish data. |
142 + | |
143 + | Returns: |
144 + | List of GainfieldMappings. |
143 145 | """ |
144 146 | # define function to get Tsys fields for intent |
145 - | def f(intent, exclude: Optional[str] = None): |
147 + | def f(intent, exclude: str | None = None) -> str: |
146 148 | if ',' in intent: |
147 149 | head, tail = intent.split(',', 1) |
148 150 | # the 'if o' test filters out results for intents that do not have |
149 151 | # fields, e.g., PHASE for SD data |
150 152 | return ','.join(o for o in (f(head, exclude=exclude), f(tail, exclude=exclude)) if o) |
151 - | return ','.join(str(s) for s in get_tsys_fields_for_intent(ms, intent, exclude=exclude)) |
153 + | return ','.join(str(s) for s in get_tsys_fields_for_intent(ms, intent, exclude_intents=exclude)) |
152 154 | |
153 155 | # return different gainfield maps for single dish and interferometric |
154 156 | if is_single_dish: |
155 157 | return [ |
156 158 | GainfieldMapping(intent='BANDPASS', preferred=f('BANDPASS'), fallback='nearest'), |
157 159 | GainfieldMapping(intent='AMPLITUDE', preferred=f('AMPLITUDE'), fallback='nearest'), |
158 160 | # non-empty magic string to differentiate between no field found and a null fallback |
159 161 | GainfieldMapping(intent='TARGET', preferred=f('TARGET'), fallback='___EMPTY_STRING___') |
160 162 | ] |
161 163 | |
162 164 | else: |
163 - | # Intent mapping extracted from CAS-12213 ticket. |
165 + | # CAS-12213: original intent mapping. |
164 166 | # PIPE-2080: updated to add mapping for DIFFGAINREF, DIFFGAINSRC intent. |
167 + | # PIPE-2394: updated mapping for PHASE, TARGET, CHECK |
165 168 | # |
166 - | # ObjectToBeCalibrated TsysSolutionToUse IfNoSolutionPresentThenUse |
167 - | # BANDPASS cal all BANDPASS cals fallback to 'nearest' |
168 - | # FLUX cal all FLUX cals fallback to 'nearest' |
169 - | # DIFFGAIN[REF|SRC] all DIFFGAIN cals fallback to BANDPASS |
170 - | # PHASE cal all PHASE cals all TARGETs |
171 - | # TARGET all TARGETs all PHASE cals |
172 - | # CHECK_SOURCE all TARGETs all PHASE cals |
169 + | # Intent to be calibrated: |
170 + | # - BANDPASS cal |
171 + | # * Preferred: all BANDPASS cals. |
172 + | # * Fallback: 'nearest'. |
173 + | # - FLUX cal |
174 + | # * Preferred: all FLUX cals. |
175 + | # * Fallback: 'nearest'. |
176 + | # - DIFFGAIN[REF|SRC] |
177 + | # * Preferred: all DIFFGAIN cals. |
178 + | # * Fallback to BANDPASS. |
179 + | # - PHASE cal |
180 + | # * Preferred: ATMOSPHERE cals, but excluding AMP, BP, DIFFGAIN*, POL* cals, and TARGET. |
181 + | # * Fallback: ATMOSPHERE cals, but excluding AMP, BP, DIFFGAIN*, POL* cals. |
182 + | # - TARGET |
183 + | # * Preferred: ATMOSPHERE cals, but excluding AMP, BP, DIFFGAIN*, POL* cals, and PHASE. |
184 + | # * Fallback: ATMOSPHERE cals, but excluding AMP, BP, DIFFGAIN*, POL* cals. |
185 + | # - CHECK_SOURCE |
186 + | # * Preferred: ATMOSPHERE cals, but excluding AMP, BP, DIFFGAIN*, POL* cals, and PHASE. |
187 + | # * Fallback: ATMOSPHERE cals, but excluding AMP, BP, DIFFGAIN*, POL* cals. |
188 + | |
189 + | # PIPE-2394: typical calibrator intents to avoid (all but PHASE) |
190 + | # matching searching for nearby Tsys field for PHASE, TARGET, and/or |
191 + | # CHECK. |
192 + | exclude_intents = 'AMPLITUDE,BANDPASS,DIFFGAINREF,DIFFGAINSRC,POLARIZATION,POLANGLE,POLLEAKAGE' |
193 + | |
173 194 | return [ |
174 195 | GainfieldMapping(intent='BANDPASS', preferred=f('BANDPASS'), fallback='nearest'), |
175 196 | GainfieldMapping(intent='AMPLITUDE', preferred=f('AMPLITUDE'), fallback='nearest'), |
176 197 | GainfieldMapping(intent='DIFFGAINREF', preferred=f('DIFFGAINREF'), fallback=f('BANDPASS')), |
177 198 | GainfieldMapping(intent='DIFFGAINSRC', preferred=f('DIFFGAINSRC'), fallback=f('BANDPASS')), |
178 - | GainfieldMapping(intent='PHASE', |
179 - | preferred=f('ATMOSPHERE', exclude='AMPLITUDE,BANDPASS,DIFFGAINREF,DIFFGAINSRC,POLARIZATION,POLANGLE,POLLEAKAGE,TARGET'), |
180 - | fallback=f('ATMOSPHERE', exclude='AMPLITUDE,BANDPASS,DIFFGAINREF,DIFFGAINSRC,POLARIZATION,POLANGLE,POLLEAKAGE')), |
181 - | GainfieldMapping(intent='TARGET', |
182 - | preferred=f('ATMOSPHERE', exclude='AMPLITUDE,BANDPASS,DIFFGAINREF,DIFFGAINSRC,POLARIZATION,POLANGLE,POLLEAKAGE,PHASE'), |
183 - | fallback=f('ATMOSPHERE', exclude='AMPLITUDE,BANDPASS,DIFFGAINREF,DIFFGAINSRC,POLARIZATION,POLANGLE,POLLEAKAGE')), |
184 - | GainfieldMapping(intent='CHECK', |
185 - | preferred=f('ATMOSPHERE', exclude='AMPLITUDE,BANDPASS,DIFFGAINREF,DIFFGAINSRC,POLARIZATION,POLANGLE,POLLEAKAGE,PHASE'), |
186 - | fallback=f('ATMOSPHERE', exclude='AMPLITUDE,BANDPASS,DIFFGAINREF,DIFFGAINSRC,POLARIZATION,POLANGLE,POLLEAKAGE')), |
199 + | GainfieldMapping(intent='PHASE', preferred=f('ATMOSPHERE', exclude=f'{exclude_intents},TARGET'), |
200 + | fallback=f('ATMOSPHERE', exclude=exclude_intents)), |
201 + | GainfieldMapping(intent='TARGET', preferred=f('ATMOSPHERE', exclude=f'{exclude_intents},PHASE'), |
202 + | fallback=f('ATMOSPHERE', exclude=exclude_intents)), |
203 + | GainfieldMapping(intent='CHECK', preferred=f('ATMOSPHERE', exclude=f'{exclude_intents},PHASE'), |
204 + | fallback=f('ATMOSPHERE', exclude=exclude_intents)), |
187 205 | ] |
188 206 | |
189 207 | |
190 - | def get_gainfield_map(ms: MeasurementSet, is_single_dish: bool) -> Dict: |
208 + | def get_gainfield_map(ms: MeasurementSet, is_single_dish: bool) -> dict: |
191 209 | """ |
192 210 | Get the mapping of observing intent to gainfield parameter for a |
193 211 | measurement set. |
194 212 | |
195 213 | The mapping follows the observing intent to gainfield intent defined in |
196 214 | CAS-12213. |
197 215 | |
198 - | :param ms: MS to analyse |
199 - | :param is_single_dish: boolean for if SD data or not |
200 - | :return: dict of {observing intent: gainfield} |
201 - | """ |
216 + | Args: |
217 + | ms: MS to analyse. |
218 + | is_single_dish: boolean for if SD data or not. |
202 219 | |
220 + | Returns: |
221 + | Dictionary of {observing intent: gainfield}. |
222 + | """ |
203 223 | soln_map = get_solution_map(ms, is_single_dish) |
204 224 | final_map = {s.intent: s.preferred if s.preferred else s.fallback for s in soln_map} |
205 225 | |
206 226 | # Detect cases where there's no preferred or fallback gainfield mapping, |
207 227 | # e.g., if there are no Tsys scans on a target or phase calibrator. |
208 228 | undefined_intents = [k for k, v in final_map.items() |
209 229 | if not v # gainfield mapping is empty.. |
210 230 | and k in ms.intents] # ..for a valid intent in the MS |
211 231 | if undefined_intents: |
212 232 | msg = 'Undefined Tsys gainfield mapping for {} intents: {}'.format(ms.basename, undefined_intents) |
213 233 | LOG.error(msg) |
214 234 | raise AssertionError(msg) |
215 235 | |
216 236 | # convert magic string back to empty string |
217 237 | converted = {k: v.replace('___EMPTY_STRING___', '') for k, v in final_map.items()} |
218 238 | |
219 239 | return converted |
220 240 | |
221 241 | |
222 - | def get_tsys_fields_for_intent(ms: MeasurementSet, intent: str, exclude: Optional[str] = None) -> Set[str]: |
242 + | def get_tsys_fields_for_intent(ms: MeasurementSet, intent: str, exclude_intents: str | None = None) -> set[str]: |
223 243 | """ |
224 244 | Returns the identity of the Tsys field(s) for an intent. |
225 245 | |
226 - | :param ms: MS to analyse |
227 - | :param intent: intent to retrieve fields for. |
228 - | :return: set of field identifiers corresponding to intent |
246 + | Args: |
247 + | ms: MS to analyse. |
248 + | intent: Intent to retrieve fields for. |
249 + | exclude_intents: String of intent(s) (comma-separated) that should not |
250 + | be covered by the Tsys field. |
251 + | |
252 + | Returns: |
253 + | Set of field identifiers corresponding to given intent, while not |
254 + | associated with intents (optionally) given by ``exclude_intents``. |
229 255 | """ |
230 256 | # In addition to the science intent scan, a field must also have a Tsys |
231 257 | # scan observed for a Tsys solution to be considered present. The |
232 258 | # exception is science mosaics, which are handled as a special case. |
233 259 | |
234 260 | # We need to know which science intent scans have Tsys scans; the ones |
235 261 | # that don't will be checked for science mosaics separately. This lets |
236 262 | # us handle single field, single pointing science targets alongside mosaic |
237 263 | # targets mixed together in the same EB. Theoretically, at least... |
238 264 | intent_fields = ms.get_fields(intent=intent) |
239 - | if exclude is not None: |
240 - | intent_fields = [f for f in intent_fields if f.intents.isdisjoint(set(exclude.split(',')))] |
265 + | |
266 + | # PIPE-2394: If requested, avoid matching fields that cover any of the |
267 + | # intents that are to be excluded. |
268 + | if exclude_intents is not None: |
269 + | intent_fields = [f for f in intent_fields if f.intents.isdisjoint(set(exclude_intents.split(',')))] |
241 270 | |
242 271 | # contains fields of this intent that also have a companion Tsys scan |
243 272 | intent_fields_with_tsys = [f for f in intent_fields if 'ATMOSPHERE' in f.intents] |
244 273 | |
245 274 | # contains fields without a companion Tsys scan. These might be science |
246 275 | # mosaics. |
247 276 | intent_fields_without_tsys = [f for f in intent_fields if f not in intent_fields_with_tsys] |
248 277 | |
249 278 | tsys_fields_for_mosaics = [] |
250 279 | if intent == 'TARGET': |
267 296 | |
268 297 | r = {field.id for field in intent_fields_with_tsys} |
269 298 | r.update({field.id for field in tsys_fields_for_mosaics}) |
270 299 | |
271 300 | # when field names are not unique, as is usually the case for science |
272 301 | # mosaics, then we must reference the numeric field ID instead |
273 302 | field_identifiers = utils.get_field_identifiers(ms) |
274 303 | return {field_identifiers[i] for i in r} |
275 304 | |
276 305 | |
277 - | def get_calapplications(ms: MeasurementSet, tsys_table: str, calfrom_defaults: Dict, origin: callibrary.CalAppOrigin, |
278 - | spw_map: List, is_single_dish: bool) -> List[callibrary.CalApplication]: |
306 + | def get_calapplications(ms: MeasurementSet, tsys_table: str, calfrom_defaults: dict, origin: callibrary.CalAppOrigin, |
307 + | spw_map: list, is_single_dish: bool) -> list[callibrary.CalApplication]: |
279 308 | """ |
280 309 | Get a list of CalApplications that apply a Tsys caltable to a measurement |
281 310 | set using the gainfield mapping defined in CAS-12213. |
282 311 | |
283 312 | Note: this function only provides the gainfield argument for the CalFrom |
284 313 | constructor. Any other required CalFrom constructor arguments should be |
285 314 | provided to this function via the calfrom_defaults parameter. |
286 315 | |
287 - | :param ms: MeasurementSet to apply calibrations to |
288 - | :param tsys_table: name of Tsys table |
289 - | :param calfrom_defaults: dict of CalFrom constructor arguments |
290 - | :param origin: CalOrigin for the created CalApplications |
291 - | :param spw_map: Tsys SpW map |
292 - | :param is_single_dish: boolean declaring if current MS is for Single-Dish |
293 - | :return: list of CalApplications |
316 + | Args: |
317 + | ms: MeasurementSet to apply calibrations to. |
318 + | tsys_table: name of Tsys table. |
319 + | calfrom_defaults: dict of CalFrom constructor arguments. |
320 + | origin: CalOrigin for the created CalApplications. |
321 + | spw_map: Tsys SpW map. |
322 + | is_single_dish: boolean declaring if current MS is for Single-Dish. |
323 + | |
324 + | Returns: |
325 + | List of CalApplications. |
294 326 | """ |
295 327 | # Get the map of intent:gainfield |
296 328 | soln_map = get_gainfield_map(ms, is_single_dish) |
297 329 | |
298 330 | # Create the static dict of calfrom arguments. Only the 'gainfield' argument changes from calapp to calapp; the |
299 331 | # other arguments remain unchanged. |
300 332 | calfrom_args = dict(calfrom_defaults) |
301 333 | calfrom_args['gaintable'] = tsys_table |
302 334 | |
303 335 | # get the mapping of field ID to unambiguous identifier for more user friendly logs |