Coverage for /wheeldirectory/casa-6.7.0-12-py3.10.el8/lib/py/lib/python3.10/site-packages/casatasks/private/update_spw.py: 5%

319 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-10-31 19:10 +0000

1#! /usr/bin/env python 

2# The above is for running the doctest, not normal use. 

3 

4""" 

5A set of functions for manipulating spw:chan selection strings. 

6 

7If this is run from a shell (i.e. not in casapy), doctest will be used to run 

8several unit tests from the doc strings, including the one below: 

9 

10Example: 

11>>> from update_spw import update_spw 

12>>> update_spw('0~2,5', None)[0] 

13'0~2,3' 

14>>> update_spw('0~2,5', None)[1]['5'] # doctest warning! dicts don't always print out in the same order! 

15'3' 

16""" 

17 

18import copy 

19import os 

20 

21from casatools import ms 

22from casatools import table as tbtool 

23_ms = ms( ) 

24 

25def update_spw(spw, spwmap=None): 

26 """ 

27 Given an spw:chan selection string, return what it should be after the spws 

28 have been remapped (i.e. by split), and a map from input to output spws 

29 (spwmap). It does not change spw OR the *channels* part of the output spw 

30 string! (See update_spwchan) 

31 

32 If given, spwmap will be used as a dictionary from (string) input spw to 

33 (string) output spws. Otherwise it will be freshly calculated. Supplying 

34 spwmap doesn't just save work: it is also necessary for chaining 

35 update_spw() calls when the first selection includes more spws than the 

36 subsequent one(s). HOWEVER, if given, spwmap must have slots for all the 

37 spws that will appear in the output MS, i.e. it can't be grown once made. 

38 

39 Examples: 

40 >>> from update_spw import update_spw 

41 >>> myfitspw, spws = update_spw('0~3,5;6:1~7;11~13', None) 

42 >>> myfitspw 

43 '0~3,4;5:1~7;11~13' 

44 >>> myspw = update_spw('1,5;6:8~10', spws)[0] 

45 >>> myspw # not '0,1,2:8~10' 

46 '1,4;5:8~10' 

47 >>> update_spw('0~3,5;6:1~7;11~13,7~9:0~3,11,7~8:6~8', None)[0] 

48 '0~3,4;5:1~7;11~13,6~8:0~3,9,6~7:6~8' 

49  

50 # Let's say we want updates of both fitspw and spw, but fitspw and spw 

51 # are disjoint (in spws). 

52 >>> fitspw = '1~10:5~122,15~22:5~122' 

53 >>> spw = '6~14' 

54  

55 # Initialize spwmap with the union of them. 

56 >>> spwmap = update_spw(join_spws(fitspw, spw), None)[1] 

57  

58 >>> myfitspw = update_spw(fitspw, spwmap)[0] 

59 >>> myfitspw 

60 '0~9:5~122,14~21:5~122' 

61 >>> myspw = update_spw(spw, spwmap)[0] 

62 >>> myspw 

63 '5~13' 

64 >>> myspw = update_spw('0,1,3;5~8:20~30;44~50^2', None)[0] 

65 >>> myspw 

66 '0,1,2;3~6:20~30;44~50^2' 

67 """ 

68 # Blank is valid. Blank is good. 

69 if not spw: 

70 return '', {} 

71 

72 # A list of [spw, chan] pairs. The chan parts will not be changed. 

73 spwchans = [] 

74 

75 make_spwmap = False 

76 if not spwmap: 

77 spwmap = {} 

78 make_spwmap = True 

79 spws = set([]) 

80 

81 # Because ; means different things when it separates spws and channel 

82 # ranges, I can't think of a better way to construct spwchans than an 

83 # explicit state machine. (But $spws_alone =~ s/:[^,]+//g;) 

84 inspw = True # until a : is encountered. 

85 spwgrp = '' 

86 chagrp = '' 

87 

88 def store_spwchan(sstr, cstr): 

89 spwchans.append([sstr, cstr]) 

90 if make_spwmap: 

91 for sgrp in sstr.split(';'): 

92 if sgrp.find('~') > -1: 

93 start, end = map(int, sgrp.split('~')) 

94 spws.update(range(start, end + 1)) 

95 else: 

96 spws.add(int(sgrp)) 

97 

98 for c in spw: 

99 if c == ',': # Start new [spw, chan] pair. 

100 # Store old one. 

101 store_spwchan(spwgrp, chagrp) 

102 

103 # Initialize new one. 

104 spwgrp = '' 

105 chagrp = '' 

106 inspw = True 

107 elif c == ':': 

108 inspw = False 

109 elif inspw: 

110 spwgrp += c 

111 else: 

112 chagrp += c 

113 # Store final [spw, chan] pair. 

114 store_spwchan(spwgrp, chagrp) 

115 

116 # casalog.post("spwchans ={}".format(spwchans)) 

117 # casalog.post("spws ={}".format(spws)) 

118 

119 # Update spw (+ fitspw) 

120 if make_spwmap: 

121 i = 0 

122 for s in sorted(spws): 

123 spwmap[str(s)] = str(i) 

124 i += 1 

125 outstr = '' 

126 for sc in spwchans: 

127 sgrps = sc[0].split(';') 

128 for sind in range(len(sgrps)): 

129 sgrp = sgrps[sind] 

130 if sgrp.find('~') > -1: 

131 start, end = sgrp.split('~') 

132 sgrps[sind] = spwmap[start] + '~' + spwmap[end] 

133 else: 

134 sgrps[sind] = spwmap[sgrp] 

135 outstr += ';'.join(sgrps) 

136 if sc[1]: 

137 outstr += ':' + sc[1] 

138 outstr += ',' 

139 

140 return outstr.rstrip(','), spwmap # discard final comma. 

141 

142def spwchan_to_ranges(vis, spw): 

143 """ 

144 Returns the spw:chan selection string spw as a dict of channel selection 

145 ranges for vis, keyed by spectral window ID. 

146 

147 The ranges are stored as tuples of (start channel, 

148 end channel (inclusive!), 

149 step). 

150 

151 Note that '' returns an empty set! Use '*' to select everything! 

152 

153 Example: 

154 >>> from update_spw import spwchan_to_ranges 

155 >>> selranges = spwchan_to_ranges('uid___A002_X1acc4e_X1e7.ms', '7:10~20^2;40~55') 

156 ValueError: spwchan_to_ranges() does not support multiple channel ranges per spw. 

157 >>> selranges = spwchan_to_ranges('uid___A002_X1acc4e_X1e7.ms', '0~1:1~3,5;7:10~20^2') 

158 >>> selranges 

159 {0: (1, 3, 1), 1: (1, 3, 1), 5: (10, 20, 2), 7: (10, 20, 2)} 

160 """ 

161 selarr = _ms.msseltoindex(vis, spw=spw)['channel'] 

162 nspw = selarr.shape[0] 

163 selranges = {} 

164 for s in range(nspw): 

165 if selarr[s][0] in selranges: 

166 raise ValueError('spwchan_to_ranges() does not support multiple channel ranges per spw.') 

167 selranges[selarr[s][0]] = tuple(selarr[s][1:]) 

168 return selranges 

169 

170def spwchan_to_sets(vis, spw): 

171 """ 

172 Returns the spw:chan selection string spw as a dict of sets of selected 

173 channels for vis, keyed by spectral window ID. 

174 

175 Note that '' returns an empty set! Use '*' to select everything! 

176 

177 Example (16.ms has spws 0 and 1 with 16 chans each): 

178 >>> from update_spw import spwchan_to_sets 

179 >>> vis = casa['dirs']['data'] + '/regression/unittest/split/unordered_polspw.ms' 

180 >>> spwchan_to_sets(vis, '0:0') 

181 {0: set([0])} 

182 >>> selsets = spwchan_to_sets(vis, '1:1~3;5~9^2,9') # 9 is a bogus spw. 

183 >>> selsets 

184 {1: [1, 2, 3, 5, 7, 9]} 

185 >>> spwchan_to_sets(vis, '1:1~3;5~9^2,8') 

186 {1: set([1, 2, 3, 5, 7, 9]), 8: set([0])} 

187 >>> spwchan_to_sets(vis, '') 

188 {} 

189 """ 

190 if not spw: # _ms.msseltoindex(vis, spw='')['channel'] returns a 

191 return {} # different kind of empty array. Skip it. 

192 

193 # Currently distinguishing whether or not vis is a valid MS from whether it 

194 # just doesn't have all the channels in spw is a bit crude. Sanjay is 

195 # working on adding some flexibility to _ms.msseltoindex. 

196 if not os.path.isdir(vis): 

197 raise ValueError(str(vis) + ' is not a valid MS.') 

198 

199 sets = {} 

200 try: 

201 scharr = _ms.msseltoindex(vis, spw=spw)['channel'] 

202 for scr in scharr: 

203 if not scr[0] in sets: 

204 sets[scr[0]] = set([]) 

205 

206 # scr[2] is the last selected channel. Bump it up for range(). 

207 scr[2] += 1 

208 sets[scr[0]].update(range(*scr[1:])) 

209 except: 

210 # spw includes channels that aren't in vis, so it needs to be trimmed 

211 # down to make _ms.msseltoindex happy. 

212 allrec = _ms.msseltoindex(vis, spw='*') 

213 # casalog.post("Trimming {}".format(spw)) 

214 spwd = spw_to_dict(spw, {}, False) 

215 for s in spwd: 

216 if s in allrec['spw']: 

217 endchan = allrec['channel'][s, 2] 

218 if not s in sets: 

219 sets[s] = set([]) 

220 if spwd[s] == '': 

221 # We need to get the spw's # of channels without using 

222 # _ms.msseltoindex. 

223 mytb = tbtool() 

224 mytb.open(vis + '/SPECTRAL_WINDOW') 

225 spwd[s] = range(mytb.getcell('NUM_CHAN', s)) 

226 mytb.close() 

227 sets[s].update([c for c in spwd[s] if c <= endchan]) 

228 return sets 

229 

230def set_to_chanstr(chanset, totnchan=None): 

231 """ 

232 Essentially the reverse of expand_tilde. Given a set or list of integers 

233 chanset, returns the corresponding string form. It will not use non-unity 

234 steps (^) if multiple ranges (;) are necessary, but it will use ^ if it 

235 helps to eliminate any ;s. 

236 

237 totnchan: the total number of channels for the input spectral window, used 

238 to abbreviate the return string. 

239 

240 It returns '' for the empty set and '*' if  

241 

242 Examples: 

243 >>> from update_spw import set_to_chanstr 

244 >>> set_to_chanstr(set([0, 1, 2, 4, 5, 6, 7, 9, 11, 13])) 

245 '0~2;4~7;9;11;13' 

246 >>> set_to_chanstr(set([7, 9, 11, 13])) 

247 '7~13^2' 

248 >>> set_to_chanstr(set([7, 9])) 

249 '7~9^2' 

250 >>> set_to_chanstr([0, 1, 2]) 

251 '0~2' 

252 >>> set_to_chanstr([0, 1, 2], 3) 

253 '*' 

254 >>> set_to_chanstr([0, 1, 2, 6], 3) 

255 '*' 

256 >>> set_to_chanstr([0, 1, 2, 6]) 

257 '0~2;6' 

258 >>> set_to_chanstr([1, 2, 4, 5, 6, 7, 8, 9, 10, 11], 12) 

259 '1~2;4~11' 

260 """ 

261 if totnchan: 

262 mylist = [c for c in chanset if c < totnchan] 

263 else: 

264 mylist = list(chanset) 

265 

266 if totnchan == len(mylist): 

267 return '*' 

268 

269 mylist.sort() 

270 

271 retstr = '' 

272 if len(mylist) > 1: 

273 # Check whether the same step can be used throughout. 

274 step = mylist[1] - mylist[0] 

275 samestep = True 

276 for i in range(2, len(mylist)): 

277 if mylist[i] - mylist[i - 1] != step: 

278 samestep = False 

279 break 

280 if samestep: 

281 retstr = str(mylist[0]) + '~' + str(mylist[-1]) 

282 if step > 1: 

283 retstr += '^' + str(step) 

284 else: 

285 sc = mylist[0] 

286 oldc = sc 

287 retstr = str(sc) 

288 nc = len(mylist) 

289 for i in range(1, nc): 

290 cc = mylist[i] 

291 if (cc > oldc + 1) or (i == nc - 1): 

292 if (i == nc - 1) and (cc == oldc + 1): 

293 retstr += '~' + str(cc) 

294 else: 

295 if oldc != sc: 

296 retstr += '~' + str(oldc) 

297 retstr += ';' + str(cc) 

298 sc = cc 

299 oldc = cc 

300 elif len(mylist) > 0: 

301 retstr = str(mylist[0]) 

302 return retstr 

303 

304def sets_to_spwchan(spwsets, nchans={}): 

305 """ 

306 Returns a spw:chan selection string for a dict of sets of selected 

307 channels keyed by spectral window ID. 

308 

309 nchans is a dict of the total number of channels keyed by spw, used to 

310 abbreviate the return string. 

311 

312 Examples: 

313 >>> from update_spw import sets_to_spwchan 

314 >>> # Use nchans to get '1' instead of '1:0~3'. 

315 >>> sets_to_spwchan({1: [0, 1, 2, 3]}, {1: 4}) 

316 '1' 

317 >>> sets_to_spwchan({1: set([1, 2, 3, 5, 7, 9]), 8: set([0])}) 

318 '1:1~3;5;7;9,8:0' 

319 >>> sets_to_spwchan({0: set([4, 5, 6]), 1: [4, 5, 6], 2: [4, 5, 6]}) 

320 '0~2:4~6' 

321 >>> sets_to_spwchan({0: [4], 1: [4], 3: [0, 1], 4: [0, 1], 7: [0, 1]}, {3: 2, 4: 2, 7: 2}) 

322 '0~1:4,3~4,7' 

323 """ 

324 # Make a list of spws for each channel selection. 

325 csd = {} 

326 for s in spwsets: 

327 # Convert the set of channels to a string. 

328 if spwsets[s]: 

329 cstr = set_to_chanstr(spwsets[s], nchans.get(s)) 

330 

331 if cstr: 

332 if not cstr in csd: 

333 csd[cstr] = [] 

334 csd[cstr].append(s) 

335 

336 # Now convert those spw lists into strings, inverting as we go so the final 

337 # string can be sorted by spw: 

338 scd = {} 

339 while csd: 

340 cstr, slist = csd.popitem() 

341 slist.sort() 

342 startspw = slist[0] 

343 oldspw = startspw 

344 sstr = str(startspw) 

345 nselspw = len(slist) 

346 for sind in range(1, nselspw): 

347 currspw = slist[sind] 

348 if (currspw > oldspw + 1) or (sind == nselspw - 1): 

349 if currspw > oldspw + 1: 

350 if oldspw != startspw: 

351 sstr += '~' + str(oldspw) 

352 sstr += ';' + str(currspw) 

353 startspw = currspw 

354 else: # The range has come to an end on the last spw. 

355 sstr += '~' + str(currspw) 

356 oldspw = currspw 

357 scd[sstr] = cstr 

358 spwgrps = sorted(scd.keys()) 

359 

360 # Finally stitch together the final string. 

361 scstr = '' 

362 for sstr in spwgrps: 

363 scstr += sstr 

364 if scd[sstr] != '*': 

365 scstr += ':' + scd[sstr] 

366 scstr += ',' 

367 return scstr.rstrip(',') 

368 

369def update_spwchan(vis, sch0, sch1, truncate=False, widths={}): 

370 """ 

371 Given an spw:chan selection string sch1, return what it must be changed to 

372 to get the same result if used with the output of split(vis, spw=sch0). 

373 

374 '' is taken to mean '*' in the input but NOT the output! For the output 

375 '' means sch0 and sch1 do not intersect. 

376 

377 truncate: If True and sch0 only partially overlaps sch1, return the update 

378 of the intersection. 

379 If (False and sch0 does not cover sch1), OR 

380 there is no intersection, raises a ValueError. 

381 

382 widths is a dictionary of averaging widths (default 1) for each spw. 

383 

384 Examples: 

385 >>> from update_spw import update_spwchan 

386 >>> newspw = update_spwchan('anything.ms', 'anything', 'anything') 

387 >>> newspw 

388 '*' 

389 >>> vis = casa['dirs']['data'] + '/regression/unittest/split/unordered_polspw.ms' 

390 >>> update_spwchan(vis, '0~1:1~3,5;7:10~20^2', '0~1:2~3,5;7:12~18^2') 

391 '0~1:1~2,2~3:1~4' 

392 >>> update_spwchan(vis, '7', '3') 

393 ValueError: '3' is not a subset of '7'. 

394 >>> update_spwchan(vis, '7:10~20^2', '7:12~18^3') 

395 ValueError: '7:12~18^3' is not a subset of '7:10~20^2'. 

396 >>> update_spwchan(vis, '7:10~20^2', '7:12~18^3', truncate=True) 

397 '0:1~4^3' 

398 >>> update_spwchan(vis, '7:10~20^2', '7:12~18^3', truncate=True, widths={7: 2}) 

399 '0:0~2^2' 

400 """ 

401 # Convert '' to 'select everything'. 

402 if not sch0: 

403 sch0 = '*' 

404 if not sch1: 

405 sch1 = '*' 

406 

407 # Short circuits 

408 if sch1 == '*': 

409 return '*' 

410 elif sch1 in (sch0, '*'): 

411 return '*' 

412 

413 sch0sets = spwchan_to_sets(vis, sch0) 

414 sch1sets = spwchan_to_sets(vis, sch1) 

415 

416 outsets = {} 

417 outspw = 0 

418 s0spws = sorted(sch0sets.keys()) 

419 s1spws = sorted(sch1sets.keys()) 

420 ns0spw = len(s0spws) 

421 nchans = {} 

422 for s in s1spws: 

423 if s in s0spws: 

424 s0 = sch0sets[s] 

425 s1 = sch1sets[s] 

426 

427 # Check for and handle (throw or dispose) channels in sch1 that aren't in 

428 # sch0. 

429 if s1.difference(s0): 

430 if truncate: 

431 s1.intersection_update(s0) 

432 if not s1: 

433 raise ValueError("'%s' does not overlap '%s'." % (sch1, sch0)) 

434 else: 

435 raise ValueError("'%s' is not a subset of '%s'." % (sch1, sch0)) 

436 

437 # Adapt s1 for a post-s0 world. 

438 s0list = sorted(list(s0)) 

439 s1list = sorted(list(s1)) 

440 outchan = 0 

441 nc0 = len(s0list) 

442 for s1ind in range(len(s1list)): 

443 while (outchan < nc0) and (s0list[outchan] < s1list[s1ind]): 

444 outchan += 1 

445 if outchan == nc0: # Shouldn't happen 

446 outchan -= 1 

447 s1list[s1ind] = outchan // widths.get(s, 1) 

448 

449 # Determine outspw. 

450 while (outspw < ns0spw) and (s0spws[outspw] < s): 

451 outspw += 1 

452 if outspw == ns0spw: # Shouldn't happen 

453 outspw -= 1 

454 

455 outsets[outspw] = set(s1list) 

456 

457 # Get the number of channels per spw that are selected by s0. 

458 nchans[outspw] = len(s0) 

459 elif not truncate: 

460 raise ValueError(str(s) + ' is not a selected spw of ' + sch0) 

461 

462 return sets_to_spwchan(outsets, nchans) 

463 

464def expand_tilde(tstr, conv_multiranges=False): 

465 """ 

466 Expands a string like '8~11' to [8, 9, 10, 11]. 

467 Returns '*' if tstr is ''! 

468 

469 conv_multiranges: If True, '*' will be returned if tstr contains ';'. 

470 (split can't yet handle multiple channel ranges per spw.) 

471 

472 Examples: 

473 >>> from update_spw import expand_tilde 

474 >>> expand_tilde('8~11') 

475 [8, 9, 10, 11] 

476 >>> expand_tilde(None) 

477 '*' 

478 >>> expand_tilde('3~7^2;9~11') 

479 [3, 5, 7, 9, 10, 11] 

480 >>> expand_tilde('3~7^2;9~11', True) 

481 '*' 

482 """ 

483 tstr = str(tstr) # Allows bare ints. 

484 if (not tstr) or (conv_multiranges and tstr.find(';') > -1): 

485 return '*' 

486 

487 tstr = tstr.replace("'", '') # Dequote 

488 tstr = tstr.replace('"', '') 

489 

490 numset = set([]) 

491 

492 for numrang in tstr.split(';'): 

493 step = 1 

494 try: 

495 if numrang.find('~') > -1: 

496 if numrang.find('^') > -1: 

497 numrang, step = numrang.split('^') 

498 step = int(step) 

499 start, end = map(int, numrang.split('~')) 

500 else: 

501 start = int(numrang) 

502 end = start 

503 except: 

504 raise ValueError('numrang = ' + numrang + ', tstr = ' + tstr + ', conv_multiranges = ' + str(conv_multiranges)) 

505 numset.update(range(start, end + 1, step)) 

506 return sorted(list(numset)) 

507 

508def spw_to_dict(spw, spwdict={}, conv_multiranges=True): 

509 """ 

510 Expand an spw:chan string to {s0: [s0chans], s1: [s1chans, ...], ...} 

511 where s0, s1, ... are integers for _each_ selected spw, and s0chans is a 

512 set of selected chans (as integers) for s0. '' instead of a channel set 

513 means that all of the channels are selected. 

514 

515 The spw:chan dict is unioned with spwdict. 

516 

517 Returning an empty dict means everything should be selected (i.e. spw = ''). 

518 (split can't yet handle multiple channel ranges per spw.) 

519 

520 conv_multiranges: If True, any spw with > 1 channel range selected will 

521 have ALL of its channels selected. 

522 (split can't yet handle multiple channel ranges per spw.) 

523 

524 Examples: 

525 >>> from update_spw import spw_to_dict 

526 >>> spw_to_dict('', {}) 

527 {} 

528 >>> spw_to_dict('6~8:2~5', {})[6] 

529 set([2, 3, 4, 5]) 

530 >>> spw_to_dict('6~8:2~5', {})[8] 

531 set([2, 3, 4, 5]) 

532 >>> spw_to_dict('6~8:2~5', {6: ''})[6] 

533 '' 

534 >>> spw_to_dict('6~8:2~5', {6: '', 7: set([1, 7])})[7] 

535 set([1, 2, 3, 4, 5, 7]) 

536 >>> spw_to_dict('7', {6: '', 7: set([1, 7])})[7] 

537 '' 

538 >>> spw_to_dict('7:123~127;233~267', {6: '', 7: set([1, 7])})[7] # Multiple chan ranges 

539 '' 

540 >>> spw_to_dict('5,7:123~127;233~267', {6: '', 7: set([1, 7])})[5] 

541 '' 

542 >>> spw_to_dict('5:3~5,7:123~127;233~267', {6: '', 7: set([1, 7])})[5] 

543 set([3, 4, 5]) 

544 """ 

545 if not spw: 

546 return {} 

547 

548 myspwdict = copy.deepcopy(spwdict) 

549 

550 # Because ; means different things when it separates spws and channel 

551 # ranges, I can't think of a better way to construct myspwdict than an 

552 # explicit state machine. (But $spws_alone =~ s/:[^,]+//g;) 

553 inspw = True # Must start with an spw. 

554 spwgrp = '' 

555 chagrp = '' 

556 

557 def enter_ranges(spwg, chag): 

558 spwrange = expand_tilde(spwg) 

559 if spwrange == '*': # This shouldn't happen. 

560 return {} 

561 else: 

562 charange = expand_tilde(chag, conv_multiranges) 

563 for s in spwrange: 

564 if charange == '*': 

565 myspwdict[s] = '' 

566 else: 

567 if not s in myspwdict: 

568 myspwdict[s] = set([]) 

569 if myspwdict[s] != '': 

570 myspwdict[s].update(charange) 

571 

572 for c in spw: 

573 if c == ',' or (inspw and c == ';'): # Start new [spw, chan] pair. 

574 # Store old one. 

575 enter_ranges(spwgrp, chagrp) 

576 

577 # Initialize new one. 

578 spwgrp = '' 

579 chagrp = '' 

580 inspw = True 

581 elif c == ':': 

582 inspw = False 

583 elif inspw: 

584 spwgrp += c 

585 else: 

586 chagrp += c 

587 

588 # Store final [spw, chan] pair. 

589 enter_ranges(spwgrp, chagrp) 

590 return myspwdict 

591 

592def join_spws(spw1, spw2, span_semicolon=True): 

593 """ 

594 Returns the union of spw selection strings spw1 and spw2. 

595 

596 span_semicolon (default True): If True, for any spws 

597 that have > 1 channel range, the entire spw will be selected. 

598 

599 Examples: 

600 >>> from update_spw import join_spws 

601 >>> join_spws('0~2:3~5,3:9~13', '') 

602 '' 

603 >>> join_spws('0~2:3~5,3:9~13', '1~3:4~7') 

604 '0:3~5,1~2:3~7,3' 

605 >>> join_spws('1~10:5~122,15~22:5~122', '1~10:5~122,15~22:5~122') 

606 '1~10:5~122,15~22:5~122' 

607 >>> join_spws('', '') 

608 '' 

609 >>> join_spws('1~10:5~122,15~22:5~122', '0~21') 

610 '0~21,22:5~122' 

611 """ 

612 if not spw1 or not spw2: 

613 return '' 

614 

615 spwdict = spw_to_dict(spw1, {}) 

616 spwdict = spw_to_dict(spw2, spwdict) 

617 

618 res = '' 

619 # Convert channel sets to strings 

620 for s in spwdict: 

621 cstr = '' 

622 if isinstance(spwdict[s], set): 

623 cstr = set_to_chanstr(spwdict[s]) 

624 if span_semicolon and ';' in cstr: 

625 cstr = '' 

626 spwdict[s] = cstr 

627 

628 # If consecutive spws have the same channel selection, merge them. 

629 slist = list(spwdict.keys()) 

630 slist.sort() 

631 res = str(slist[0]) 

632 laststart = 0 

633 for i in range(1, len(slist)): 

634 # If consecutive spws have the same channel list, 

635 if slist[i] == slist[i - 1] + 1 and spwdict[slist[i]] == spwdict[slist[i - 1]]: 

636 if slist[i] == slist[laststart] + 1: 

637 res += '~' # Continue the spw range. 

638 else: # Terminate it and start a new one. 

639 if res[-1] == '~': # if start != end 

640 res += str(slist[i - 1]) 

641 if spwdict[slist[i - 1]] != '': # Add channel range, if any. 

642 res += ':' + spwdict[slist[i - 1]] 

643 res += ',' + str(slist[i]) 

644 laststart = i 

645 if res[-1] == '~': # Finish the last range if it is dangling. 

646 res += str(slist[-1]) 

647 if spwdict[slist[-1]] != '': # Add channel range, if any. 

648 res += ':' + spwdict[slist[-1]] 

649 return res 

650 

651def intersect_spws(spw1, spw2): 

652 """ 

653 Almost the opposite of join_spws(), this returns the list of spws that the 

654 spw:chan selection strings spw1 and spw2 have in common. Unlike join_spws(), 

655 channel ranges are ignored. '' in the input counts as 'select everything', 

656 so the intersection of '' with anything is anything. If the intersection 

657 really is everything, '' is returned instead of a set. 

658 

659 Examples: 

660 >>> from update_spw import intersect_spws 

661 >>> intersect_spws('0~2:3~5,3:9~13', '') 

662 set([0, 1, 2, 3]) 

663 >>> intersect_spws('0~2:3~5,3:9~13', '0~2:7~9,5') 

664 set([0, 1, 2]) 

665 >>> intersect_spws('0~2:3~5;10~13,3:9~13', '0~2:7~9,5') 

666 set([0, 1, 2]) 

667 >>> intersect_spws('0~2:3~5,3:9~13', '10~12:7~9,5') # Empty set 

668 set([]) 

669 >>> intersect_spws('', '') # Everything 

670 '' 

671 """ 

672 if spw1 == '': 

673 if spw2 == '': 

674 return '' # intersection('', '') = '' 

675 else: # intersection('', spw2) = spw2 

676 return set(spw_to_dict(spw2, {}).keys()) # Just the spws, no chan ranges 

677 elif spw2 == '': # intersection('', spw1) = spw1 

678 return set(spw_to_dict(spw1, {}).keys()) # Just the spws, no chan ranges 

679 else: 

680 spwset1 = set(spw_to_dict(spw1, {}).keys()) # spws are the keys, chan 

681 spwset2 = set(spw_to_dict(spw2, {}).keys()) # ranges are the values. 

682 return spwset1.intersection(spwset2) 

683 

684def subtract_spws(spw1, spw2): 

685 """ 

686 Returns the set of spws of spw selection string spw1 that are not in spw2. 

687 Like intersect_spws(), this intentionally ignores channel ranges. It 

688 assumes that spw1 and spw2 refer to the same MS (this only matters for ''). 

689 subtract_spws('', '0~5') is a tough case: it is impossible to know whether 

690 '' is equivalent to '0~5' without reading the MS's SPECTRAL_WINDOW 

691 subtable, so it returns 'UNKNOWN'. 

692 

693 Examples: 

694 >>> from update_spw import subtract_spws 

695 >>> subtract_spws('0~2:3~5,3:9~13', '') # Anything - Everything 

696 set([]) 

697 >>> subtract_spws('0~2:3~5,3:9~13', '0~2:7~9,5') 

698 set([3]) 

699 >>> subtract_spws('', '0~2:7~9,5') # Everything - Something 

700 'UNKNOWN' 

701 >>> subtract_spws('0~2,3:9~13', '4~7:7') # Something - Something Else 

702 set([0, 1, 2, 3]) 

703 >>> subtract_spws('', '') # Everything - Everything 

704 set([]) 

705 """ 

706 if spw1 == '': 

707 if spw2 == '': 

708 return set([]) 

709 else: 

710 return 'UNKNOWN' 

711 elif spw2 == '': 

712 return set([]) 

713 else: 

714 spwset1 = set(spw_to_dict(spw1, {}).keys()) # spws are the keys, chan 

715 spwset2 = set(spw_to_dict(spw2, {}).keys()) # ranges are the values. 

716 return spwset1.difference(spwset2) 

717 

718if __name__ == '__main__': 

719 import doctest, sys 

720 doctest.testmod(verbose=True)