Coverage for /home/casatest/venv/lib/python3.12/site-packages/casatasks/private/task_tsdimaging.py: 9%

638 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-21 07:43 +0000

1# sd task for imaging 

2 

3import collections 

4import contextlib 

5import os 

6import re 

7import shutil 

8import time 

9 

10import numpy 

11 

12from functools import partial 

13 

14from casatasks import casalog 

15from casatools import image, imager 

16from casatools import ms as mstool 

17from casatools import quanta 

18 

19from . import mslisthelper, sdbeamutil, sdutil 

20# (1) Import the python application layer 

21from .imagerhelpers.imager_base import PySynthesisImager 

22from .imagerhelpers.input_parameters import ImagerParameters 

23 

24image_suffix = '.image' 

25residual_suffix = '.residual' 

26weight_suffix = '.weight' 

27associate_suffixes = ['.psf', '.sumwt', weight_suffix, residual_suffix] 

28 

29 

30@contextlib.contextmanager 

31def open_ia(imagename): 

32 ia = image() 

33 ia.open(imagename) 

34 try: 

35 yield ia 

36 finally: 

37 ia.close() 

38 

39 

40@contextlib.contextmanager 

41def open_ms(vis): 

42 ms = mstool() 

43 ms.open(vis) 

44 try: 

45 yield ms 

46 finally: 

47 ms.close() 

48 

49 

50class SelectionHandler(object): 

51 def __init__(self, sel): 

52 self.sel = sel 

53 if isinstance(self.sel, str): 

54 self.selector = self._select0 

55 elif len(self.sel) == 1: 

56 self.selector = self._select1 

57 else: 

58 self.selector = self._select2 

59 

60 def __call__(self, i): 

61 return self.selector(i) 

62 

63 def _select0(self, i): 

64 return self.sel 

65 

66 def _select1(self, i): 

67 return self.sel[0] 

68 

69 def _select2(self, i): 

70 return self.sel[i] 

71 

72 

73class OldImagerBasedTools(object): 

74 def __init__(self): 

75 self.imager = imager() 

76 

77 @contextlib.contextmanager 

78 def open_old_imager(self, vis): 

79 try: 

80 self.imager.open(vis) 

81 yield self.imager 

82 finally: 

83 self.imager.close() 

84 

85 @contextlib.contextmanager 

86 def open_and_select_old_imager(self, vislist, field, spw, antenna, scan, intent, timerange): 

87 if isinstance(vislist, str): 

88 with self.open_old_imager(vislist) as im: 

89 im.selectvis(field=field, 

90 spw=spw, 

91 nchan=-1, 

92 start=0, 

93 step=1, 

94 baseline=antenna, 

95 scan=scan, 

96 intent=intent, 

97 time=timerange) 

98 yield im 

99 else: 

100 fieldsel = SelectionHandler(field) 

101 spwsel = SelectionHandler(spw) 

102 antennasel = SelectionHandler(antenna) 

103 scansel = SelectionHandler(scan) 

104 intentsel = SelectionHandler(intent) 

105 timerangesel = SelectionHandler(timerange) 

106 try: 

107 for i in range(len(vislist)): 

108 vis = vislist[i] 

109 _field = fieldsel(i) 

110 _spw = spwsel(i) 

111 _antenna = antennasel(i) 

112 _scan = scansel(i) 

113 _intent = intentsel(i) 

114 _timerangesel = timerangesel(i) 

115 if len(_antenna) == 0: 

116 _baseline = _antenna 

117 elif len(_antenna) < 4 or _antenna[:-3] != '&&&': 

118 _baseline = _antenna + '&&&' 

119 else: 

120 _baseline = _antenna 

121 self.imager.selectvis( 

122 vis, field=_field, spw=_spw, nchan=-1, start=0, step=1, 

123 baseline=_baseline, scan=_scan, intent=_intent, time=_timerangesel) 

124 yield self.imager 

125 finally: 

126 self.imager.close() 

127 

128 def test(self, vis): 

129 with self.open_old_imager(vis): 

130 casalog.post('test') 

131 raise RuntimeError('ERROR!') 

132 

133 def get_pointing_sampling_params(self, vis, field, spw, baseline, scan, intent, timerange, 

134 outref, movingsource, pointingcolumntouse, antenna_name): 

135 with self.open_old_imager(vis) as im: 

136 im.selectvis(field=field, 

137 spw=spw, 

138 nchan=-1, 

139 start=0, 

140 step=1, 

141 baseline=baseline, 

142 scan=scan, 

143 intent=intent, 

144 time=timerange) 

145 sampling_params = im.pointingsampling(pattern='raster', 

146 ref=outref, 

147 movingsource=movingsource, 

148 pointingcolumntouse=pointingcolumntouse, 

149 antenna='{0}&&&'.format(antenna_name)) 

150 return sampling_params 

151 

152 def get_map_extent(self, vislist, field, spw, antenna, scan, intent, timerange, 

153 ref, movingsource, pointingcolumntouse): 

154 

155 with self.open_and_select_old_imager(vislist=vislist, field=field, 

156 spw=spw, antenna=antenna, scan=scan, 

157 intent=intent, timerange=timerange) as im: 

158 map_param = im.mapextent(ref=ref, movingsource=movingsource, 

159 pointingcolumntouse=pointingcolumntouse) 

160 return map_param 

161 

162 

163def check_conformance(mslist, check_result): 

164 """Check conformance of input MS list 

165 

166 Check conformance of input MS list, particularlly existence of 

167 WEIGHT_SPECTRUM column. 

168 

169 Args: 

170 mslist (list): list of names for input MS 

171 check_result (dict): result of conformance check. 

172 see mslisthelper.check_mslist for detail about 

173 the structure of check_result. 

174 

175 Returns: 

176 dict: Per-column set of names for MS that needs to be 

177 edited to resolve the conformance. Top level dict 

178 has two keys, "remove" and "add", which indicate 

179 the operation to be applied to the columns. 

180 """ 

181 process_dict = { 

182 'remove': collections.defaultdict(set), 

183 'add': collections.defaultdict(set) 

184 } 

185 for name, summary in check_result.items(): 

186 if 'Main' in summary: 

187 missingcol_list = [summary['Main']['missingcol_{}'.format(x)] for x in ['a', 'b']] 

188 ms_a = mslist[0] 

189 ms_b = name 

190 

191 # "remove" operation: 

192 # - MS list is opposite order to missingcol_list 

193 # - if WEIGHT_SPECTRUM column is missing in one MS, 

194 # the column should be removed from another MS. 

195 for c, t in zip(missingcol_list, [ms_b, ms_a]): 

196 if 'WEIGHT_SPECTRUM' in c: 

197 process_dict['remove']['WEIGHT_SPECTRUM'].add(t) 

198 # "add" operation: 

199 # - MS list is same order as missingcol_list 

200 # - if CORRECTED_DATA column is missing in one MS, 

201 # the column should be added to that MS. 

202 for c, t in zip(missingcol_list, [ms_a, ms_b]): 

203 if 'CORRECTED_DATA' in c: 

204 process_dict['add']['CORRECTED_DATA'].add(t) 

205 return process_dict 

206 

207 

208def report_conformance(mslist, column_name, process_set): 

209 """Report conformance of input MS 

210 

211 Report conformance of input MS, particularlly on the existence 

212 of the column given by column_name. 

213 

214 Args: 

215 mslist (list): list of names for input MS 

216 column_name (str): name of the column 

217 process_set (set): set of names of MS that need to be edited 

218 """ 

219 if len(process_set) > 0: 

220 casalog.post('Detected non-conformance of {} column in input list of MSes.'.format(column_name), priority='WARN') 

221 cols = ['exists?', 'MS name'] 

222 header = ' '.join(cols) 

223 casalog.post('', priority='WARN') 

224 casalog.post('Summary of existence of {}:'.format(column_name), priority='WARN') 

225 casalog.post(header, priority='WARN') 

226 casalog.post('-' * len(header), priority='WARN') 

227 for name in mslist: 

228 basename = os.path.basename(name.rstrip('/')) 

229 exists = 'YES' if name in process_set else 'NO' 

230 row = '{:^7s} {:<s}'.format(exists, basename) 

231 casalog.post(row, priority='WARN') 

232 

233 

234def fix_conformance(process_dict): 

235 """Resolve non-conformance by removing WEIGHT_SPECTRUM 

236 

237 Two non-conformances are fixed, WEIGHT_SPECTRUM and 

238 CORRECTED_DATA. Remove WEIGHT_SPECTRUM column from, or 

239 add CORRECTED_DATA to the MS provided by process_dict. 

240 Backup is created with the name: 

241 

242 <original_name>.sdimaging.backup-<timestamp> 

243 

244 Args: 

245 process_dict (dict): per-operation ("remove" and "add") 

246 key-value pair of column name and 

247 list of names for MS to be edited 

248 

249 Returns: 

250 dict: mapping of original MS name and the name of backup 

251 """ 

252 backup_list = {} 

253 process_list = set() 

254 for v in process_dict.values(): 

255 for w in v.values(): 

256 process_list = process_list.union(w) 

257 for name in process_list: 

258 basename = os.path.basename(name.rstrip('/')) 

259 timestamp = time.strftime('%Y%m%dT%H%M%S', time.gmtime()) 

260 backup_name = basename + '.sdimaging.backup-{}'.format(timestamp) 

261 with sdutil.table_manager(name) as tb: 

262 tb.copy(backup_name, deep=True, returnobject=True).close() 

263 backup_list[name] = backup_name 

264 casalog.post('Copy of "{}" has been saved to "{}"'.format(name, backup_name), priority='WARN') 

265 

266 for colname, msnames in process_dict['remove'].items(): 

267 for name in msnames: 

268 casalog.post('{} will be removed from "{}"'.format(colname, name), priority='WARN') 

269 with sdutil.table_manager(name, nomodify=False) as tb: 

270 if colname in tb.colnames(): 

271 tb.removecols(colname) 

272 

273 for colname, msnames in process_dict['add'].items(): 

274 for name in msnames: 

275 casalog.post('{} will be added to "{}"'.format(colname, name), priority='WARN') 

276 with sdutil.calibrater_manager(name, addmodel=False, addcorr=True): 

277 pass 

278 return backup_list 

279 

280 

281def conform_mslist(mslist, ignore_columns=['CORRECTED_DATA']): 

282 """Make given set of MS data conform 

283 

284 Here, only conformance on the existence of WEIGHT_SPECTRUM 

285 is checked and resolved because non-conformance of WEIGHT_SPECTRUM, 

286 i.e. some MS have the column while others don't, could cause 

287 the task to crash. If non-conformance is detected, all existing 

288 WEIGHT_SPECTRUM columns are removed. This opration modifies input 

289 MS so data will be backed up with the name: 

290 

291 <original_name>.sdimaging.backup-<timestamp> 

292 

293 Args: 

294 mslist (list): list of names for input MS 

295 """ 

296 check_result = mslisthelper.check_mslist(mslist, testcontent=False) 

297 process_dict = check_conformance(mslist, check_result) 

298 fix_dict = dict() 

299 for op, op_dict in process_dict.items(): 

300 fix_dict[op] = dict() 

301 for col, process_set in op_dict.items(): 

302 if col not in ignore_columns: 

303 report_conformance(mslist, col, process_set) 

304 fix_dict[op][col] = process_set 

305 fix_conformance(fix_dict) 

306 

307 

308def sort_vis(vislist, spw, mode, width, field, antenna, scan, intent, timerange): 

309 """Sort the given MeasurementSet path(s) by their earliest data-taking time, in increasing order. 

310 

311 Return a 7-tuple where 

312 * the first entry holds the re-ordered paths 

313 * the remaining entries hold their corresponding data selection parameters 

314 * FIXME: input parameters mode and width are not used 

315 """ 

316 if isinstance(vislist, str) or len(vislist) == 1: 

317 return vislist, field, spw, antenna, scan, intent, timerange 

318 # chronological sort 

319 sorted_vislist, sorted_timelist = mslisthelper.sort_mslist(vislist) 

320 _vislist = list(vislist) 

321 sorted_idx = [_vislist.index(vis) for vis in sorted_vislist] 

322 mslisthelper.report_sort_result(sorted_vislist, sorted_timelist, sorted_idx, mycasalog=casalog) 

323 # conform MS 

324 conform_mslist(sorted_vislist) 

325 fieldsel = SelectionHandler(field) 

326 sorted_field = [fieldsel(i) for i in sorted_idx] 

327 spwsel = SelectionHandler(spw) 

328 sorted_spw = [spwsel(i) for i in sorted_idx] 

329 antennasel = SelectionHandler(antenna) 

330 sorted_antenna = [antennasel(i) for i in sorted_idx] 

331 scansel = SelectionHandler(scan) 

332 sorted_scan = [scansel(i) for i in sorted_idx] 

333 intentsel = SelectionHandler(intent) 

334 sorted_intent = [intentsel(i) for i in sorted_idx] 

335 timerangesel = SelectionHandler(timerange) 

336 sorted_timerange = [timerangesel(i) for i in sorted_idx] 

337 return sorted_vislist, sorted_field, sorted_spw, sorted_antenna, sorted_scan, sorted_intent, sorted_timerange 

338 

339 

340def _configure_spectral_axis(mode, nchan, start, width, restfreq): 

341 # fix default 

342 if mode == 'channel': 

343 if start == '': 

344 start = 0 

345 if width == '': 

346 width = 1 

347 else: 

348 if start == 0: 

349 start = '' 

350 if width == 1: 

351 width = '' 

352 # fix unit 

353 if mode == 'frequency': 

354 myunit = 'Hz' 

355 elif mode == 'velocity': 

356 myunit = 'km/s' 

357 else: # channel 

358 myunit = '' 

359 

360 tmp_start = _format_quantum_unit(start, myunit) 

361 if tmp_start is None: 

362 raise ValueError("Invalid unit for %s in mode %s: %s" % ('start', mode, start)) 

363 start = tmp_start 

364 if mode == 'channel': 

365 start = int(start) 

366 tmp_width = _format_quantum_unit(width, myunit) 

367 if tmp_width is None: 

368 raise ValueError("Invalid unit for %s in mode %s: %s" % ('width', mode, width)) 

369 width = tmp_width 

370 if mode == 'channel': 

371 width = int(width) 

372 

373 # TODO: work for nchan 

374 imnchan = nchan 

375 imstart = start 

376 imwidth = width 

377 return imnchan, imstart, imwidth 

378 

379 

380def _format_quantum_unit(data, unit): 

381 """Format quantity data. 

382 

383 Returns False if data has an unit which in not a variation of 

384 input unit. 

385 Otherwise, returns input data as a quantum string. The input 

386 unit is added to the return value if no unit is in data. 

387 """ 

388 my_qa = quanta() 

389 if data == '' or my_qa.compare(data, unit): 

390 return data 

391 if my_qa.getunit(data) == '': 

392 casalog.post("No unit specified. Using '%s'" % unit) 

393 return '%f%s' % (data, unit) 

394 return None 

395 

396 

397def _handle_grid_defaults(value): 

398 ret = '' 

399 if isinstance(value, int) or isinstance(value, float): 

400 ret = str(value) 

401 elif isinstance(value, str): 

402 ret = value 

403 return ret 

404 

405 

406def _calc_PB(vis, antenna_id, restfreq): 

407 """Calculate the primary beam size of antenna. 

408 

409 Calculate the primary beam size of antenna, using dish diamenter 

410 and rest frequency 

411 Average antenna diamter and reference frequency are adopted for 

412 calculation. 

413 The input argument should be a list of antenna IDs. 

414 """ 

415 logger = sdutil.Casalog(origin="_calc_PB") 

416 logger.post("Calculating Primary beam size:") 

417 # CAS-5410 Use private tools inside task scripts 

418 my_qa = quanta() 

419 

420 pb_factor = 1.175 

421 # Reference frequency 

422 ref_freq = restfreq 

423 if type(ref_freq) in [float, numpy.float64]: 

424 ref_freq = my_qa.tos(my_qa.quantity(ref_freq, 'Hz')) 

425 if not my_qa.compare(ref_freq, 'Hz'): 

426 msg = "Could not get the reference frequency. " + \ 

427 "Your data does not seem to have valid one in selected field.\n" + \ 

428 "PB is not calculated.\n" + \ 

429 "Please set restreq or cell manually to generate an image." 

430 raise RuntimeError(msg) 

431 # Antenna diameter 

432 with sdutil.table_manager(os.path.join(vis, 'ANTENNA')) as tb: 

433 antdiam_ave = tb.getcell('DISH_DIAMETER', antenna_id) 

434 # antdiam_ave = self._get_average_antenna_diameter(antenna) 

435 # Calculate PB 

436 wave_length = 0.2997924 / my_qa.convert(my_qa.quantity(ref_freq), 'GHz')['value'] 

437 D_m = my_qa.convert(antdiam_ave, 'm')['value'] 

438 lambda_D = wave_length / D_m * 3600. * 180 / numpy.pi 

439 PB = my_qa.quantity(pb_factor * lambda_D, 'arcsec') 

440 # Summary 

441 logger.post(f"- Antenna diameter: {D_m} m") 

442 logger.post(f"- Reference Frequency: {ref_freq}") 

443 logger.post(f"PB size = {pb_factor:5.3f} * lambda/D = {my_qa.tos(PB)}") 

444 return PB 

445 

446 

447def _get_imsize(width, height, dx, dy): 

448 casalog.post("Calculating pixel size.") 

449 # CAS-5410 Use private tools inside task scripts 

450 my_qa = quanta() 

451 ny = numpy.ceil((my_qa.convert(height, my_qa.getunit(dy))['value'] / 

452 my_qa.getvalue(dy))) 

453 nx = numpy.ceil((my_qa.convert(width, my_qa.getunit(dx))['value'] / 

454 my_qa.getvalue(dx))) 

455 casalog.post("- Map extent: [%s, %s]" % (my_qa.tos(width), my_qa.tos(height))) 

456 casalog.post("- Cell size: [%s, %s]" % (my_qa.tos(dx), my_qa.tos(dy))) 

457 casalog.post("Image pixel numbers to cover the extent: [%d, %d] (projected)" % 

458 (nx + 1, ny + 1)) 

459 return [int(nx + 1), int(ny + 1)] 

460 

461 

462def _get_pointing_extent(phasecenter, vislist, field, spw, antenna, scan, intent, timerange, 

463 pointingcolumntouse, ephemsrcname): 

464 # MS selection is ignored. This is not quite right. 

465 casalog.post("Calculating map extent from pointings.") 

466 # CAS-5410 Use private tools inside task scripts 

467 my_qa = quanta() 

468 ret_dict = {} 

469 

470 if isinstance(vislist, str): 

471 vis = vislist 

472 else: 

473 vis = vislist[0] 

474 

475 # colname = pointingcolumntouse.upper() 

476 

477 if phasecenter == "": 

478 # defaut is J2000 

479 base_mref = 'J2000' 

480 elif isinstance(phasecenter, int) or phasecenter.isdigit(): 

481 # may be field id 

482 with sdutil.table_manager(os.path.join(vis, 'FIELD')) as tb: 

483 base_mref = tb.getcolkeyword('PHASE_DIR', 'MEASINFO')['Ref'] 

484 else: 

485 # may be phasecenter is explicitly specified 

486 # numeric value: 3.14, -.3e1, etc. 

487 numeric_pattern = r'[-+]?([0-9]+(.[0-9]*)?|\.[0-9]+)([eE]-?[0-9])?' 

488 # HMS string: 9:15:29, -9h15m29 

489 hms_pattern = r'[-+]?[0-9]+[:h][0-9]+[:m][0-9.]+s?' 

490 # DMS string: 9.15.29, -9d15m29s 

491 dms_pattern = r'[-+]?[0-9]+[.d][0-9]+[.m][0-9.]+s?' 

492 # composite pattern 

493 pattern = fr'^({numeric_pattern}|{hms_pattern}|{dms_pattern})$' 

494 items = phasecenter.split() 

495 base_mref = 'J2000' 

496 for i in items: 

497 s = i.strip() 

498 if re.match(pattern, s) is None: 

499 base_mref = s 

500 break 

501 

502 t = OldImagerBasedTools() 

503 mapextent = t.get_map_extent(vislist, field, spw, antenna, scan, intent, timerange, 

504 ref=base_mref, movingsource=ephemsrcname, 

505 pointingcolumntouse=pointingcolumntouse) 

506 # mapextent = self.imager.mapextent(ref=base_mref, movingsource=ephemsrcname, 

507 # pointingcolumntouse=colname) 

508 if mapextent['status']: 

509 qheight = my_qa.quantity(mapextent['extent'][1], 'rad') 

510 qwidth = my_qa.quantity(mapextent['extent'][0], 'rad') 

511 qcent0 = my_qa.quantity(mapextent['center'][0], 'rad') 

512 qcent1 = my_qa.quantity(mapextent['center'][1], 'rad') 

513 scenter = '%s %s %s' % (base_mref, my_qa.formxxx(qcent0, 'hms'), 

514 my_qa.formxxx(qcent1, 'dms')) 

515 

516 casalog.post("- Pointing center: %s" % scenter) 

517 casalog.post("- Pointing extent: [%s, %s] (projected)" % (my_qa.tos(qwidth), 

518 my_qa.tos(qheight))) 

519 ret_dict['center'] = scenter 

520 ret_dict['width'] = qwidth 

521 ret_dict['height'] = qheight 

522 else: 

523 casalog.post( 

524 'Failed to derive map extent from the MSs registered to the imager probably ' 

525 'due to mising valid data.', 

526 priority='SEVERE') 

527 ret_dict['center'] = '' 

528 ret_dict['width'] = my_qa.quantity(0.0, 'rad') 

529 ret_dict['height'] = my_qa.quantity(0.0, 'rad') 

530 return ret_dict 

531 

532 

533def _handle_image_params(imsize, cell, phasecenter, 

534 vislist, field, spw, antenna, scan, intent, timerange, 

535 restfreq, pointingcolumntouse, ephemsrcname): 

536 logger = sdutil.Casalog(origin="_handle_image_params") 

537 # round-up imsize 

538 _imsize = sdutil.to_list(imsize, int) or sdutil.to_list(imsize, numpy.integer) 

539 if _imsize is None: 

540 _imsize = imsize if hasattr(imsize, '__iter__') else [imsize] 

541 _imsize = [int(numpy.ceil(v)) for v in _imsize] 

542 logger.post( 

543 "imsize is not integers. force converting to integer pixel numbers.", 

544 priority="WARN") 

545 logger.post("rounded-up imsize: %s --> %s" % (str(imsize), str(_imsize))) 

546 

547 # calculate cell based on PB if it is not given 

548 _cell = cell 

549 if _cell == '' or _cell[0] == '': 

550 # calc PB 

551 if isinstance(vislist, str): 

552 vis = vislist 

553 else: 

554 vis = vislist[0] 

555 if isinstance(antenna, str): 

556 antsel = antenna 

557 else: 

558 antsel = antenna[0] 

559 if antsel == '': 

560 antenna_id = 0 

561 else: 

562 if len(antsel) > 3 and antsel[:-3] == '&&&': 

563 baseline = antsel 

564 else: 

565 baseline = antsel + '&&&' 

566 with open_ms(vis) as ms: 

567 ms.msselect({'baseline': baseline}) 

568 ndx = ms.msselectedindices() 

569 antenna_id = ndx['antenna1'][0] 

570 grid_factor = 3. 

571 logger.post("The cell size will be calculated using PB size of antennas in the first MS") 

572 qpb = _calc_PB(vis, antenna_id, restfreq) 

573 _cell = '%f%s' % (qpb['value'] / grid_factor, qpb['unit']) 

574 logger.post("Using cell size = PB/%4.2F = %s" % (grid_factor, _cell)) 

575 

576 # Calculate Pointing center and extent (if necessary) 

577 _phasecenter = phasecenter 

578 if _phasecenter == '' or len(_imsize) == 0 or _imsize[0] < 1: 

579 # return a dictionary with keys 'center', 'width', 'height' 

580 map_param = _get_pointing_extent(_phasecenter, vislist, field, spw, antenna, scan, intent, 

581 timerange, pointingcolumntouse, ephemsrcname) 

582 # imsize 

583 (cellx, celly) = sdutil.get_cellx_celly(_cell, unit='arcmin') 

584 if len(_imsize) == 0 or _imsize[0] < 1: 

585 _imsize = _get_imsize(map_param['width'], map_param['height'], cellx, celly) 

586 if _phasecenter != "": 

587 logger.post( 

588 "You defined phasecenter but not imsize. " 

589 "The image will cover as wide area as pointing in MS extends, " 

590 "but be centered at phasecenter. " 

591 "This could result in a strange image if your phasecenter is " 

592 "apart from the center of pointings", 

593 priority='WARN') 

594 if _imsize[0] > 1024 or _imsize[1] > 1024: 

595 logger.post( 

596 "The calculated image pixel number is larger than 1024. " 

597 "It could take time to generate the image depending on your computer resource. " 

598 "Please wait...", 

599 priority='WARN') 

600 

601 # phasecenter 

602 # if empty, it should be determined here... 

603 if _phasecenter == "": 

604 _phasecenter = map_param['center'] 

605 

606 return _imsize, _cell, _phasecenter 

607 

608 

609def _get_param(ms_index, param): 

610 if isinstance(param, str): 

611 return param 

612 elif hasattr(param, '__iter__'): 

613 if len(param) == 1: 

614 return param[0] 

615 else: 

616 return param[ms_index] 

617 else: 

618 raise RuntimeError('Invalid parameter') 

619 

620 

621def _remove_image(imagename): 

622 if os.path.exists(imagename): 

623 if os.path.isdir(imagename): 

624 shutil.rmtree(imagename) 

625 elif os.path.isfile(imagename): 

626 os.remove(imagename) 

627 else: 

628 # could be a symlink 

629 os.remove(imagename) 

630 

631 

632def _get_restfreq_if_empty(vislist, spw, field, restfreq): 

633 qa = quanta() 

634 rf = None 

635 # if restfreq is nonzero float value, return it 

636 if isinstance(restfreq, float): 

637 if restfreq != 0.0: 

638 rf = restfreq 

639 # if restfreq is valid frequency string, return it 

640 # numeric string is interpreted as a value in the unit of Hz 

641 elif isinstance(restfreq, str): 

642 q = qa.convert(qa.quantity(restfreq), 'Hz') 

643 if q['unit'] == 'Hz' and q['value'] > 0.0: 

644 rf = restfreq 

645 # if restfreq is valid quantity, return it 

646 elif isinstance(restfreq, dict): 

647 q = qa.convert(restfreq, 'Hz') 

648 if q['unit'] == 'Hz' and q['value'] > 0.0: 

649 rf = restfreq 

650 

651 if isinstance(vislist, str): 

652 vis = vislist 

653 elif hasattr(vislist, '__iter__'): 

654 vis = vislist[0] 

655 else: 

656 raise RuntimeError( 

657 'Internal Error: invalid vislist \'{0}\''.format(vislist)) 

658 

659 if isinstance(spw, str): 

660 spwsel = spw 

661 elif hasattr(spw, '__iter__'): 

662 spwsel = spw[0] 

663 else: 

664 raise RuntimeError( 

665 'Internal Error: invalid spw selection \'{0}\''.format(spw)) 

666 

667 if isinstance(field, str): 

668 fieldsel = field 

669 elif hasattr(field, '__iter__'): 

670 fieldsel = field[0] 

671 else: 

672 raise RuntimeError('Internal Error: invalid field selection \'{0}\''.format(field)) 

673 

674 with open_ms(vis) as ms: 

675 ms.msselect({'spw': spwsel, 'field': fieldsel}) 

676 ndx = ms.msselectedindices() 

677 if len(ndx['spw']) > 0: 

678 spwid = ndx['spw'][0] 

679 else: 

680 spwid = None 

681 if len(ndx['field']) > 0: 

682 fieldid = ndx['field'][0] 

683 else: 

684 fieldid = None 

685 sourceid = None 

686 if fieldid is not None: 

687 with sdutil.table_manager(os.path.join(vis, 'FIELD')) as tb: 

688 sourceid = tb.getcell('SOURCE_ID', fieldid) 

689 if sourceid < 0: 

690 sourceid = None 

691 if rf is None: 

692 # if restfrequency is defined in SOURCE table, return it 

693 with sdutil.table_manager(os.path.join(vis, 'SOURCE')) as tb: 

694 if 'REST_FREQUENCY' in tb.colnames(): 

695 tsel = None 

696 taql = '' 

697 if spwid is not None: 

698 taql = 'SPECTRAL_WINDOW_ID == {0}'.format(spwid) 

699 if sourceid is not None: 

700 delimiter = '&&' if len(taql) > 0 else '' 

701 taql += '{0}SOURCE_ID == {1}'.format(delimiter, sourceid) 

702 if len(taql) > 0: 

703 tsel = tb.query(taql) 

704 t = tsel 

705 else: 

706 t = tb 

707 try: 

708 nrow = t.nrows() 

709 if nrow > 0: 

710 for irow in range(nrow): 

711 if t.iscelldefined('REST_FREQUENCY', irow): 

712 rfs = t.getcell('REST_FREQUENCY', irow) 

713 if len(rfs) > 0: 

714 rf = rfs[0] 

715 break 

716 finally: 

717 if tsel is not None: 

718 tsel.close() 

719 

720 if rf is None: 

721 if spwid is None: 

722 spwid = 0 

723 # otherwise, return mean frequency of given spectral window 

724 with sdutil.table_manager(os.path.join(vis, 'SPECTRAL_WINDOW')) as tb: 

725 cf = tb.getcell('CHAN_FREQ', spwid) 

726 rf = cf.mean() 

727 

728 assert rf is not None 

729 

730 return rf 

731 

732 

733def set_beam_size(vis, imagename, 

734 field, spw, baseline, scan, intent, timerange, 

735 ephemsrcname, pointingcolumntouse, 

736 antenna_name, antenna_diameter, 

737 restfreq, 

738 gridfunction, convsupport, truncate, gwidth, jwidth): 

739 """Set estimated beam size to the image.""" 

740 is_alma = antenna_name[0:2] in ['PM', 'DV', 'DA', 'CM'] 

741 blockage = '0.75m' if is_alma else '0.0m' 

742 log_origin = 'set_beam_size' 

743 

744 with open_ia(imagename) as ia: 

745 csys = ia.coordsys() 

746 outref = csys.referencecode('direction')[0] 

747 cell = list(csys.increment(type='direction', format='s')['string']) 

748 csys.done() 

749 

750 old_tool = OldImagerBasedTools() 

751 sampling_params = old_tool.get_pointing_sampling_params( 

752 vis, field, spw, baseline, 

753 scan, intent, timerange, 

754 outref=outref, 

755 movingsource=ephemsrcname, 

756 pointingcolumntouse=pointingcolumntouse, 

757 antenna_name=antenna_name) 

758 qa = quanta() 

759 casalog.post( 

760 f'sampling_params={sampling_params}', 

761 origin=log_origin 

762 ) 

763 xsampling, ysampling = qa.getvalue(qa.convert(sampling_params['sampling'], 

764 'arcsec')) 

765 angle = qa.getvalue(qa.convert(sampling_params['angle'], 'deg'))[0] 

766 

767 casalog.post( 

768 f'Detected raster sampling = [{xsampling:f}, {ysampling:f}] arcsec', 

769 origin=log_origin 

770 ) 

771 

772 # handling of failed sampling detection 

773 valid_sampling = True 

774 # TODO: copy from sdimaging implementation 

775 sampling = [xsampling, ysampling] 

776 if abs(xsampling) < 2.2e-3 or not numpy.isfinite(xsampling): 

777 casalog.post( 

778 f"Invalid sampling={xsampling} arcsec. " 

779 f"Using the value of orthogonal direction={ysampling} arcsec", 

780 priority="WARN", 

781 origin=log_origin 

782 ) 

783 sampling = [ysampling] 

784 angle = 0.0 

785 valid_sampling = False 

786 if abs(ysampling) < 1.0e-3 or not numpy.isfinite(ysampling): 

787 if valid_sampling: 

788 casalog.post( 

789 f"Invalid sampling={ysampling} arcsec. " 

790 f"Using the value of orthogonal direction={xsampling} arcsec", 

791 priority="WARN", 

792 origin=log_origin 

793 ) 

794 sampling = [xsampling] 

795 angle = 0.0 

796 valid_sampling = True 

797 # reduce sampling and cell if it's possible 

798 if (len(sampling) > 1 and 

799 abs(sampling[0] - sampling[1]) <= 0.01 * abs(sampling[0])): 

800 sampling = [sampling[0]] 

801 angle = 0.0 

802 if cell[0] == cell[1]: 

803 cell = [cell[0]] 

804 if valid_sampling: 

805 # actual calculation of beam size 

806 bu = sdbeamutil.TheoreticalBeam() 

807 bu.set_antenna(antenna_diameter, blockage) 

808 bu.set_sampling(sampling, "%fdeg" % angle) 

809 bu.set_image_param(cell, restfreq, gridfunction, 

810 convsupport, truncate, gwidth, 

811 jwidth, is_alma) 

812 bu.summary() 

813 imbeam_dict = bu.get_beamsize_image() 

814 casalog.post( 

815 f"Setting image beam: " 

816 f"major={imbeam_dict['major']}, " 

817 f"minor={imbeam_dict['minor']}, " 

818 f"pa={imbeam_dict['pa']}", 

819 origin=log_origin 

820 ) 

821 # set beam size to image 

822 with open_ia(imagename) as ia: 

823 ia.setrestoringbeam(**imbeam_dict) 

824 else: 

825 # BOTH sampling was invalid 

826 casalog.post( 

827 "Could not detect valid raster sampling. " 

828 "Exiting without setting beam size to image", 

829 priority='WARN', 

830 origin=log_origin 

831 ) 

832 

833 

834def do_weight_mask(imagename, weightimage, minweight): 

835 # Mask image pixels whose weight are smaller than minweight. 

836 # Weight image should have 0 weight for pixels below < minweight 

837 logger = sdutil.Casalog(origin="do_weight_mask") 

838 logger.post(f"Start masking the map using minweight = {minweight:f}", 

839 priority="INFO") 

840 with open_ia(weightimage) as ia: 

841 try: 

842 stat = ia.statistics(mask="'" + weightimage + "' > 0.0", 

843 robust=True) 

844 valid_pixels = stat['npts'] 

845 except RuntimeError as e: 

846 if 'No valid data found.' in str(e): 

847 valid_pixels = [0] 

848 else: 

849 raise e 

850 

851 if len(valid_pixels) == 0 or valid_pixels[0] == 0: 

852 logger.post( 

853 "All pixels weight zero. " 

854 "This indicates no data in MS is in image area. " 

855 "Mask will not be set. Please check your image parameters.", 

856 priority="WARN") 

857 return 

858 median_weight = stat['median'][0] 

859 weight_threshold = median_weight * minweight 

860 logger.post(f"Median of weight in the map is {median_weight:f}", 

861 priority="INFO") 

862 logger.post("Pixels in map with weight <= median(weight)*minweight = " 

863 f"{weight_threshold:f} will be masked.", 

864 priority="INFO") 

865 # Leaving the original logic to calculate the number of masked pixels via 

866 # product of median of and min_weight (which i don't understand the logic) 

867 

868 # Modify default mask 

869 with open_ia(imagename) as ia: 

870 ia.calcmask("'%s'>%f" % (weightimage, weight_threshold), 

871 asdefault=True) 

872 

873 ndim = len(ia.shape()) 

874 _axes = numpy.arange(start=0 if ndim <= 2 else 2, stop=ndim) 

875 

876 try: 

877 collapsed = ia.collapse('npts', axes=_axes) 

878 valid_pixels_after = collapsed.getchunk().sum() 

879 collapsed.close() 

880 except RuntimeError as e: 

881 if 'All selected pixels are masked' in str(e): 

882 valid_pixels_after = 0 

883 else: 

884 raise 

885 

886 masked_fraction = 100. * (1. - valid_pixels_after / float(valid_pixels[0])) 

887 

888 logger.post(f"This amounts to {masked_fraction:5.1f} % " 

889 "of the area with nonzero weight.", 

890 priority="INFO") 

891 logger.post( 

892 f"The weight image '{weightimage}' is returned by this task, " 

893 "if the user wishes to assess the results in detail.", 

894 priority="INFO") 

895 

896 

897def get_ms_column_unit(tb, colname): 

898 col_unit = '' 

899 if colname in tb.colnames(): 

900 cdkw = tb.getcoldesc(colname)['keywords'] 

901 for key in ['UNIT', 'QuantumUnits']: 

902 if key in cdkw: 

903 u = cdkw[key] 

904 if isinstance(u, str): 

905 col_unit = u.strip() 

906 elif isinstance(u, (list, numpy.ndarray)) and len(u) > 0: 

907 col_unit = u[0].strip() 

908 if col_unit: 

909 break 

910 return col_unit 

911 

912 

913def get_brightness_unit_from_ms(msname): 

914 image_unit = '' 

915 with sdutil.table_manager(msname) as tb: 

916 for column in ['CORRECTED_DATA', 'FLOAT_DATA', 'DATA']: 

917 image_unit = get_ms_column_unit(tb, column) 

918 if image_unit: 

919 break 

920 

921 if image_unit.upper() == 'K': 

922 image_unit = 'K' 

923 else: 

924 image_unit = 'Jy/beam' 

925 

926 return image_unit 

927 

928 

929@sdutil.sdtask_decorator 

930def tsdimaging( 

931 # Input data: list of MeasurementSets 

932 infiles, 

933 # Output data: CASA images: their path prefix, overwrite control 

934 outfile, overwrite, 

935 # Select data from input MeasurementSets, by 

936 field, spw, antenna, scan, intent, timerange, 

937 # Output images definition: frequency axis 

938 outframe, # velocity frame 

939 mode, nchan, start, width, veltype, # gridding type 

940 specmode, # Doppler handling 

941 interpolation, # interpolation mode 

942 # Output images definition: spatial axes 

943 pointingcolumn, convertfirst, 

944 projection, 

945 imsize, cell, phasecenter, 

946 # Output images definition: stokes axis 

947 stokes, 

948 # Gridder parameters 

949 gridfunction, convsupport, truncate, gwidth, jwidth, 

950 clipminmax, 

951 # Single-dish image: mask control 

952 minweight, 

953 # Single-dish image: metadata 

954 brightnessunit, 

955 # rest frequency to assign to image 

956 restfreq): 

957 

958 origin = 'tsdimaging' 

959 imager = None 

960 

961 try: # Create the Single-Dish Image 

962 # Validate brightnessunit parameter CAS-11503 

963 image_unit = brightnessunit.lower().capitalize() 

964 if image_unit not in ['', 'K', 'Jy/beam']: 

965 raise ValueError(f"Invalid brightness unit: {brightnessunit}") 

966 

967 # Handle outfile and overwrite parameters 

968 output_path_prefix = outfile.rstrip('/') 

969 singledish_image_path = output_path_prefix + image_suffix 

970 if os.path.exists(singledish_image_path): 

971 if not overwrite: 

972 raise RuntimeError( 

973 f"Output file exists: '{singledish_image_path}'" 

974 ) 

975 else: 

976 # delete existing images 

977 if os.path.exists(singledish_image_path): 

978 casalog.post(f"Removing '{singledish_image_path}'") 

979 _remove_image(singledish_image_path) 

980 assert not os.path.exists(singledish_image_path) 

981 for _suffix in associate_suffixes: 

982 path_to_remove = output_path_prefix + _suffix 

983 if os.path.exists(path_to_remove): 

984 casalog.post(f"Removing '{path_to_remove}'") 

985 _remove_image(path_to_remove) 

986 assert not os.path.exists(path_to_remove) 

987 

988 # Tweak spw parameter into _spw 

989 if isinstance(spw, str): 

990 _spw = '*' + spw if spw.startswith(':') else spw 

991 else: 

992 _spw = ['*' + v if v.startswith(':') else v for v in spw] 

993 

994 # Handle image spectral axis parameters 

995 imnchan, imstart, imwidth = _configure_spectral_axis( 

996 mode, nchan, start, width, restfreq 

997 ) 

998 

999 # Handle image restfreq parameter's default value 

1000 _restfreq = _get_restfreq_if_empty( 

1001 infiles, _spw, field, restfreq 

1002 ) 

1003 

1004 # Handle gridder parameters 

1005 # convert type of task's default values 

1006 # to ones supported by the Synthesis Imager framework 

1007 gtruncate = _handle_grid_defaults(truncate) 

1008 ggwidth = _handle_grid_defaults(gwidth) 

1009 gjwidth = _handle_grid_defaults(jwidth) 

1010 

1011 # handle infiles parameter 

1012 # sort input data to get results consistent with old sdimaging task 

1013 _sorted = sort_vis( 

1014 infiles, _spw, mode, imwidth, field, 

1015 antenna, scan, intent, timerange 

1016 ) 

1017 sorted_vis = _sorted[0] 

1018 sorted_field = _sorted[1] 

1019 sorted_spw = _sorted[2] 

1020 sorted_antenna = _sorted[3] 

1021 sorted_scan = _sorted[4] 

1022 sorted_intent = _sorted[5] 

1023 sorted_timerange = _sorted[6] 

1024 

1025 def antenna_to_baseline(s): 

1026 if len(s) == 0: 

1027 return s 

1028 elif len(s) > 3 and s.endswith('&&&'): 

1029 return s 

1030 else: 

1031 return '{0}&&&'.format(s) 

1032 

1033 if isinstance(sorted_antenna, str): 

1034 sorted_baseline = antenna_to_baseline(sorted_antenna) 

1035 else: 

1036 sorted_baseline = [antenna_to_baseline(a) for a in sorted_antenna] 

1037 

1038 # Handle image geometric parameters 

1039 _ephemsrcname = '' 

1040 ephem_sources = ['MERCURY', 'VENUS', 'MARS', 'JUPITER', 'SATURN', 

1041 'URANUS', 'NEPTUNE', 'PLUTO', 'SUN', 'MOON', 

1042 'TRACKFIELD'] 

1043 if (isinstance(phasecenter, str) 

1044 and phasecenter.strip().upper() in ephem_sources): 

1045 _ephemsrcname = phasecenter 

1046 _imsize, _cell, _phasecenter = _handle_image_params( 

1047 imsize, cell, phasecenter, sorted_vis, 

1048 sorted_field, sorted_spw, sorted_antenna, 

1049 sorted_scan, sorted_intent, sorted_timerange, 

1050 _restfreq, pointingcolumn, _ephemsrcname 

1051 ) 

1052 

1053 # Set up PySynthesisImager input parameters 

1054 # - List all parameters that you need here 

1055 # - Defaults will be assumed for unspecified parameters 

1056 # - Nearly all parameters are identical to that in the task. Please look at the 

1057 # list of parameters under __init__ using " help ImagerParameters " ) 

1058 casalog.post('*** Creating paramList ***', origin=origin) 

1059 paramList = ImagerParameters( 

1060 # input file name 

1061 msname=sorted_vis, # 'sdimaging.ms', 

1062 # data selection 

1063 field=sorted_field, # '', 

1064 spw=sorted_spw, # '0', 

1065 timestr=sorted_timerange, 

1066 antenna=sorted_baseline, 

1067 scan=sorted_scan, 

1068 state=sorted_intent, 

1069 # image parameters 

1070 imagename=output_path_prefix, # 'try2', 

1071 nchan=imnchan, # 1024, 

1072 start=imstart, # '0', 

1073 width=imwidth, # '1', 

1074 outframe=outframe, 

1075 veltype=veltype, 

1076 restfreq=_restfreq, 

1077 phasecenter=_phasecenter, # 'J2000 17:18:29 +59.31.23', 

1078 imsize=_imsize, # [75,75], 

1079 cell=_cell, # ['3arcmin', '3arcmin'], 

1080 projection=projection, 

1081 stokes=stokes, 

1082 specmode=specmode, 

1083 gridder='singledish', 

1084 # single dish specific parameters 

1085 gridfunction=gridfunction, 

1086 convsupport=convsupport, 

1087 truncate=gtruncate, 

1088 gwidth=ggwidth, 

1089 jwidth=gjwidth, 

1090 pointingcolumntouse=pointingcolumn, 

1091 convertfirst=convertfirst, 

1092 minweight=minweight, 

1093 clipminmax=clipminmax, 

1094 # normalizer 

1095 normtype='flatsky', 

1096 pblimit=1e-16, # TODO: explain why 1e-16 ? 

1097 interpolation=interpolation, 

1098 makesingledishnormalizer=True 

1099 ) 

1100 

1101 # Construct the PySynthesisImager object, with all input parameters 

1102 casalog.post('*** Creating imager object ***', origin=origin) 

1103 imager = PySynthesisImager(params=paramList) 

1104 

1105 # Initialize PySynthesisImager "modules" 

1106 # required for single-dish imaging 

1107 # - Pick only the modules you will need later on. 

1108 # For example, to only make the PSF, there is no need for 

1109 # the deconvolver or iteration control modules. 

1110 casalog.post('*** Initializing imagers ***', origin=origin) 

1111 # This is where the underlying C++ synthesis imager is created 

1112 imager.initializeImagers() 

1113 casalog.post('*** Initializing normalizers ***', origin=origin) 

1114 imager.initializeNormalizers() 

1115 

1116 # Create Single-Dish images 

1117 casalog.post('*** Creating single-dish images ***', origin=origin) 

1118 imager.makeSdImage() 

1119 casalog.post('*** Created single-dish images ***', origin=origin) 

1120 

1121 finally: # Close tools and rename Synthesis Imager's residual image 

1122 casalog.post('*** Cleaning up tools ***', origin=origin) 

1123 if imager is not None: 

1124 imager.deleteTools() 

1125 # Change image suffix from .residual to .image 

1126 # residual_image_path = output_path_prefix + residual_suffix 

1127 # if os.path.exists(residual_image_path): 

1128 # os.rename(residual_image_path, singledish_image_path) 

1129 

1130 # Set single-dish image's beam size 

1131 # TODO: re-define related functions in the new tool framework (sdms?) 

1132 # ms_index = 0 

1133 rep_ms = _get_param(0, infiles) 

1134 rep_field = _get_param(0, field) 

1135 rep_spw = _get_param(0, _spw) 

1136 rep_antenna = _get_param(0, antenna) 

1137 rep_scan = _get_param(0, scan) 

1138 rep_intent = _get_param(0, intent) 

1139 rep_timerange = _get_param(0, timerange) 

1140 if len(rep_antenna) > 0: 

1141 baseline = '{0}&&&'.format(rep_antenna) 

1142 else: 

1143 baseline = '*&&&' 

1144 with open_ms(rep_ms) as ms: 

1145 ms.msselect({'baseline': baseline}) 

1146 ndx = ms.msselectedindices() 

1147 antenna_index = ndx['antenna1'][0] 

1148 with sdutil.table_manager(os.path.join(rep_ms, 'ANTENNA')) as tb: 

1149 antenna_name = tb.getcell('NAME', antenna_index) 

1150 antenna_diameter = tb.getcell('DISH_DIAMETER', antenna_index) 

1151 casalog.post("Setting single-dish image's beam") 

1152 set_beam_size( 

1153 rep_ms, singledish_image_path, 

1154 rep_field, rep_spw, baseline, rep_scan, rep_intent, rep_timerange, 

1155 _ephemsrcname, pointingcolumn, antenna_name, antenna_diameter, 

1156 _restfreq, gridfunction, convsupport, truncate, gwidth, jwidth 

1157 ) 

1158 

1159 # Set single-dish image's brightness unit (CAS-11503) 

1160 if len(image_unit) == 0: 

1161 image_unit = get_brightness_unit_from_ms(rep_ms) 

1162 if len(image_unit) > 0: 

1163 with open_ia(singledish_image_path) as ia: 

1164 casalog.post( 

1165 "Setting single-dish image's brightness unit to " 

1166 f"'{image_unit}'") 

1167 ia.setbrightnessunit(image_unit) 

1168 

1169 # Update single-dish image's mask: mask low weight pixels 

1170 weight_image_path = output_path_prefix + weight_suffix 

1171 casalog.post("Creating weight image mask") 

1172 do_weight_mask(singledish_image_path, weight_image_path, minweight) 

1173 

1174 # Delete images systematically generated by the Synthesis Imager 

1175 # which are either not required or currently useless 

1176 # in the context of single-dish imaging 

1177 # CAS-10891 

1178 _remove_image(output_path_prefix + '.sumwt') 

1179 

1180 # CAS-10893 

1181 # TODO: remove the following line once the 'correct' SD 

1182 # PSF image based on primary beam can be generated 

1183 _remove_image(output_path_prefix + '.psf')