Commits

Vincent Geers authored 18dd01b3a70
PIPE-2394: update docstrings, type hints, commentary.
No tags

pipeline/h/tasks/tsyscal/tsyscal.py

Modified
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 @task_registry.set_equivalent_casa_task('h_tsyscal')
90 89 @task_registry.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

Everything looks good. We'll let you know here if there's anything you should know about.

Add shortcut