casa  $Rev:20696$
 All Classes Namespaces Files Functions Variables
update_spw.py
Go to the documentation of this file.
00001 #! /usr/bin/env python
00002 # The above is for running the doctest, not normal use.
00003 
00004 """
00005 A set of functions for manipulating spw:chan selection strings.
00006 
00007 If this is run from a shell (i.e. not in casapy), doctest will be used to run
00008 several unit tests from the doc strings, including the one below:
00009 
00010 Example:
00011 >>> from update_spw import update_spw
00012 >>> update_spw('0~2,5', None)[0]
00013 '0~2,3'
00014 >>> update_spw('0~2,5', None)[1]['5']  # doctest warning!  dicts don't always print out in the same order!
00015 '3'
00016 """
00017 
00018 import copy
00019 import os
00020 #from taskinit import mstool
00021 from casac import *
00022 from taskinit import ms
00023 
00024 def update_spw(spw, spwmap=None):
00025     """
00026     Given an spw:chan selection string, return what it should be after the spws
00027     have been remapped (i.e. by split), and a map from input to output spws
00028     (spwmap).  It does not change spw OR the *channels* part of the output spw
00029     string!  (See update_spwchan)
00030 
00031     If given, spwmap will be used as a dictionary from (string) input spw to
00032     (string) output spws.  Otherwise it will be freshly calculated.  Supplying
00033     spwmap doesn't just save work: it is also necessary for chaining
00034     update_spw() calls when the first selection includes more spws than the
00035     subsequent one(s).  HOWEVER, if given, spwmap must have slots for all the
00036     spws that will appear in the output MS, i.e. it can't be grown once made.
00037 
00038     Examples:
00039     >>> from update_spw import update_spw
00040     >>> myfitspw, spws = update_spw('0~3,5;6:1~7;11~13', None)
00041     >>> myfitspw
00042     '0~3,4;5:1~7;11~13'
00043     >>> myspw = update_spw('1,5;6:8~10', spws)[0]
00044     >>> myspw   # not '0,1,2:8~10'
00045     '1,4;5:8~10'
00046     >>> update_spw('0~3,5;6:1~7;11~13,7~9:0~3,11,7~8:6~8', None)[0]
00047     '0~3,4;5:1~7;11~13,6~8:0~3,9,6~7:6~8'
00048     
00049     # Let's say we want updates of both fitspw and spw, but fitspw and spw
00050     # are disjoint (in spws).
00051     >>> fitspw = '1~10:5~122,15~22:5~122'
00052     >>> spw = '6~14'
00053     
00054     #  Initialize spwmap with the union of them.
00055     >>> spwmap = update_spw(join_spws(fitspw, spw), None)[1]
00056     
00057     >>> myfitspw = update_spw(fitspw, spwmap)[0]
00058     >>> myfitspw
00059     '0~9:5~122,14~21:5~122'
00060     >>> myspw = update_spw(spw, spwmap)[0]
00061     >>> myspw
00062     '5~13'
00063     >>> myspw = update_spw('0,1,3;5~8:20~30;44~50^2', None)[0]
00064     >>> myspw
00065     '0,1,2;3~6:20~30;44~50^2'
00066     """
00067     # Blank is valid.  Blank is good.
00068     if not spw:
00069         return '', {}
00070 
00071     # A list of [spw, chan] pairs.  The chan parts will not be changed.
00072     spwchans = [] 
00073 
00074     make_spwmap = False
00075     if not spwmap:
00076         spwmap = {}
00077         make_spwmap = True
00078         spws = set([])
00079 
00080     # Because ; means different things when it separates spws and channel
00081     # ranges, I can't think of a better way to construct spwchans than an
00082     # explicit state machine.  (But $spws_alone =~ s/:[^,]+//g;)
00083     inspw = True     # until a : is encountered.
00084     spwgrp = ''
00085     chagrp = ''
00086 
00087     def store_spwchan(sstr, cstr):
00088         spwchans.append([sstr, cstr])
00089         if make_spwmap:
00090             for sgrp in sstr.split(';'):
00091                 if sgrp.find('~') > -1:
00092                     start, end = map(int, sgrp.split('~'))
00093                     spws.update(range(start, end + 1))
00094                 else:
00095                     spws.add(int(sgrp))        
00096     
00097     for c in spw:
00098         if c == ',':               # Start new [spw, chan] pair.
00099             # Store old one.
00100             store_spwchan(spwgrp, chagrp)
00101 
00102             # Initialize new one.
00103             spwgrp = ''
00104             chagrp = ''
00105             inspw = True
00106         elif c == ':':
00107             inspw = False
00108         elif inspw:
00109             spwgrp += c
00110         else:
00111             chagrp += c
00112     # Store final [spw, chan] pair.
00113     store_spwchan(spwgrp, chagrp)
00114 
00115     # print "spwchans =", spwchans
00116     # print "spws =", spws
00117         
00118     # Update spw (+ fitspw)
00119     if make_spwmap:
00120         i = 0
00121         for s in sorted(spws):
00122             spwmap[str(s)] = str(i)
00123             i += 1
00124     outstr = ''
00125     for sc in spwchans:
00126         sgrps = sc[0].split(';')
00127         for sind in xrange(len(sgrps)):
00128             sgrp = sgrps[sind]
00129             if sgrp.find('~') > -1:
00130                 start, end = sgrp.split('~')
00131                 sgrps[sind] = spwmap[start] + '~' + spwmap[end]
00132             else:
00133                 sgrps[sind] = spwmap[sgrp]
00134         outstr += ';'.join(sgrps)
00135         if sc[1]:
00136             outstr += ':' + sc[1]
00137         outstr += ','
00138 
00139     return outstr.rstrip(','), spwmap # discard final comma.
00140 
00141 def spwchan_to_ranges(vis, spw):
00142     """
00143     Returns the spw:chan selection string spw as a dict of channel selection
00144     ranges for vis, keyed by spectral window ID.
00145 
00146     The ranges are stored as tuples of (start channel,
00147                                         end channel (inclusive!),
00148                                         step).
00149 
00150     Note that '' returns an empty set!  Use '*' to select everything!
00151 
00152     Example:
00153     >>> from update_spw import spwchan_to_ranges
00154     >>> selranges = spwchan_to_ranges('uid___A002_X1acc4e_X1e7.ms', '7:10~20^2;40~55')
00155     ValueError: spwchan_to_ranges() does not support multiple channel ranges per spw.
00156     >>> selranges = spwchan_to_ranges('uid___A002_X1acc4e_X1e7.ms', '0~1:1~3,5;7:10~20^2')
00157     >>> selranges
00158     {0: (1,  3,  1), 1: (1,  3,  1), 5: (10, 20,  2), 7: (10, 20,  2)}
00159     """
00160     selarr = ms.msseltoindex(vis, spw=spw)['channel']
00161     nspw = selarr.shape[0]
00162     selranges = {}
00163     for s in xrange(nspw):
00164         if selranges.has_key(selarr[s][0]):
00165             raise ValueError, 'spwchan_to_ranges() does not support multiple channel ranges per spw.'
00166         selranges[selarr[s][0]] = tuple(selarr[s][1:])
00167     return selranges
00168 
00169 def spwchan_to_sets(vis, spw):
00170     """
00171     Returns the spw:chan selection string spw as a dict of sets of selected
00172     channels for vis, keyed by spectral window ID.
00173 
00174     Note that '' returns an empty set!  Use '*' to select everything!
00175 
00176     Example (16.ms has spws 0 and 1 with 16 chans each):
00177     >>> from update_spw import spwchan_to_sets
00178     >>> vis = casa['dirs']['data'] + '/regression/unittest/split/unordered_polspw.ms'
00179     >>> spwchan_to_sets(vis, '0:0')
00180     {0: set([0])}
00181     >>> selsets = spwchan_to_sets(vis, '1:1~3;5~9^2,9') # 9 is a bogus spw.
00182     >>> selsets
00183     {1: [1, 2, 3, 5, 7, 9]}
00184     >>> spwchan_to_sets(vis, '1:1~3;5~9^2,8')
00185     {1: set([1, 2, 3, 5, 7, 9]), 8: set([0])}
00186     >>> spwchan_to_sets(vis, '')
00187     {}
00188     """
00189     if not spw:        # ms.msseltoindex(vis, spw='')['channel'] returns a
00190         return {}      # different kind of empty array.  Skip it.
00191 
00192     # Currently distinguishing whether or not vis is a valid MS from whether it
00193     # just doesn't have all the channels in spw is a bit crude.  Sanjay is
00194     # working on adding some flexibility to ms.msseltoindex.
00195     if not os.path.isdir(vis):
00196         raise ValueError, str(vis) + ' is not a valid MS.'
00197         
00198     sets = {}
00199     try:
00200         scharr = ms.msseltoindex(vis, spw=spw)['channel']
00201         for scr in scharr:
00202             if not sets.has_key(scr[0]):
00203                 sets[scr[0]] = set([])
00204 
00205             # scr[2] is the last selected channel.  Bump it up for range().
00206             scr[2] += 1
00207             sets[scr[0]].update(range(*scr[1:]))
00208     except:
00209         # spw includes channels that aren't in vis, so it needs to be trimmed
00210         # down to make ms.msseltoindex happy.
00211         allrec = ms.msseltoindex(vis, spw='*')
00212         #print "Trimming", spw
00213         spwd = spw_to_dict(spw, {}, False)
00214         for s in spwd:
00215             if s in allrec['spw']:
00216                 endchan = allrec['channel'][s, 2]
00217                 if not sets.has_key(s):
00218                     sets[s] = set([])
00219                 if spwd[s] == '':
00220                     # We need to get the spw's # of channels without using
00221                     # ms.msseltoindex.
00222                     mytb = casac.table()
00223                     mytb.open(vis + '/SPECTRAL_WINDOW')
00224                     spwd[s] = range(mytb.getcell('NUM_CHAN', s))
00225                     mytb.close()
00226                 sets[s].update([c for c in spwd[s] if c <= endchan])
00227     return sets
00228 
00229 def set_to_chanstr(chanset, totnchan=None):
00230     """
00231     Essentially the reverse of expand_tilde.  Given a set or list of integers
00232     chanset, returns the corresponding string form.  It will not use non-unity
00233     steps (^) if multiple ranges (;) are necessary, but it will use ^ if it
00234     helps to eliminate any ;s.
00235 
00236     totnchan: the total number of channels for the input spectral window, used
00237               to abbreviate the return string.
00238 
00239     It returns '' for the empty set and '*' if 
00240 
00241     Examples:
00242     >>> from update_spw import set_to_chanstr
00243     >>> set_to_chanstr(set([0, 1, 2, 4, 5, 6, 7, 9, 11, 13]))
00244     '0~2;4~7;9;11;13'
00245     >>> set_to_chanstr(set([7, 9, 11, 13]))
00246     '7~13^2'
00247     >>> set_to_chanstr(set([7, 9]))
00248     '7~9^2'
00249     >>> set_to_chanstr([0, 1, 2])
00250     '0~2'
00251     >>> set_to_chanstr([0, 1, 2], 3)
00252     '*'
00253     >>> set_to_chanstr([0, 1, 2, 6], 3)
00254     '*'
00255     >>> set_to_chanstr([0, 1, 2, 6])
00256     '0~2;6'
00257     >>> set_to_chanstr([1, 2, 4, 5, 6, 7, 8, 9, 10, 11], 12)
00258     '1~2;4~11'
00259     """
00260     if totnchan:
00261         mylist = [c for c in chanset if c < totnchan]
00262     else:
00263         mylist = list(chanset)
00264 
00265     if totnchan == len(mylist):
00266         return '*'
00267 
00268     mylist.sort()
00269 
00270     retstr = ''
00271     if len(mylist) > 1:
00272         # Check whether the same step can be used throughout.
00273         step = mylist[1] - mylist[0]
00274         samestep = True
00275         for i in xrange(2, len(mylist)):
00276             if mylist[i] - mylist[i - 1] != step:
00277                 samestep = False
00278                 break
00279         if samestep:
00280             retstr = str(mylist[0]) + '~' + str(mylist[-1])
00281             if step > 1:
00282                 retstr += '^' + str(step)
00283         else:
00284             sc = mylist[0]
00285             oldc = sc
00286             retstr = str(sc)
00287             nc = len(mylist)
00288             for i in xrange(1, nc):
00289                 cc = mylist[i]
00290                 if (cc > oldc + 1) or (i == nc - 1):
00291                     if (i == nc - 1) and (cc == oldc + 1):
00292                         retstr += '~' + str(cc)
00293                     else:
00294                         if oldc != sc:
00295                             retstr += '~' + str(oldc)
00296                         retstr += ';' + str(cc)
00297                         sc = cc
00298                 oldc = cc
00299     elif len(mylist) > 0:
00300         retstr = str(mylist[0])
00301     return retstr
00302         
00303 def sets_to_spwchan(spwsets, nchans={}):
00304     """
00305     Returns a spw:chan selection string for a dict of sets of selected
00306     channels keyed by spectral window ID.
00307 
00308     nchans is a dict of the total number of channels keyed by spw, used to
00309     abbreviate the return string.
00310 
00311     Examples:
00312     >>> from update_spw import sets_to_spwchan
00313     >>> # Use nchans to get '1' instead of '1:0~3'.
00314     >>> sets_to_spwchan({1: [0, 1, 2, 3]}, {1: 4})
00315     '1'
00316     >>> sets_to_spwchan({1: set([1, 2, 3, 5, 7, 9]), 8: set([0])})
00317     '1:1~3;5;7;9,8:0'
00318     >>> sets_to_spwchan({0: set([4, 5, 6]), 1: [4, 5, 6], 2: [4, 5, 6]})
00319     '0~2:4~6'
00320     >>> sets_to_spwchan({0: [4], 1: [4], 3: [0, 1], 4: [0, 1], 7: [0, 1]}, {3: 2, 4: 2, 7: 2})
00321     '0~1:4,3~4,7'
00322     """
00323     # Make a list of spws for each channel selection.
00324     csd = {}
00325     for s in spwsets:
00326         # Convert the set of channels to a string.
00327         if spwsets[s]:
00328             cstr = set_to_chanstr(spwsets[s], nchans.get(s))
00329 
00330             if cstr:
00331                 if not csd.has_key(cstr):
00332                     csd[cstr] = []
00333                 csd[cstr].append(s)
00334 
00335     # Now convert those spw lists into strings, inverting as we go so the final
00336     # string can be sorted by spw:
00337     scd = {}
00338     while csd:
00339         cstr, slist = csd.popitem()
00340         slist.sort()
00341         startspw = slist[0]
00342         oldspw = startspw
00343         sstr = str(startspw)
00344         nselspw = len(slist)
00345         for sind in xrange(1, nselspw):
00346             currspw = slist[sind]
00347             if (currspw > oldspw + 1) or (sind == nselspw - 1):
00348                 if currspw > oldspw + 1:
00349                     if oldspw != startspw:
00350                         sstr += '~' + str(oldspw)
00351                     sstr += ';' + str(currspw)
00352                     startspw = currspw
00353                 else:               # The range has come to an end on the last spw.
00354                     sstr += '~' + str(currspw)
00355             oldspw = currspw
00356         scd[sstr] = cstr
00357     spwgrps = sorted(scd.keys())
00358 
00359     # Finally stitch together the final string.
00360     scstr = ''
00361     for sstr in spwgrps:
00362         scstr += sstr
00363         if scd[sstr] != '*':
00364             scstr += ':' + scd[sstr]
00365         scstr += ','
00366     return scstr.rstrip(',')
00367 
00368 def update_spwchan(vis, sch0, sch1, truncate=False, widths={}):
00369     """
00370     Given an spw:chan selection string sch1, return what it must be changed to
00371     to get the same result if used with the output of split(vis, spw=sch0).
00372 
00373     '' is taken to mean '*' in the input but NOT the output!  For the output
00374     '' means sch0 and sch1 do not intersect.
00375 
00376     truncate: If True and sch0 only partially overlaps sch1, return the update
00377               of the intersection.
00378               If (False and sch0 does not cover sch1), OR
00379                  there is no intersection, raises a ValueError.
00380 
00381     widths is a dictionary of averaging widths (default 1) for each spw.
00382 
00383     Examples:
00384     >>> from update_spw import update_spwchan
00385     >>> newspw = update_spwchan('anything.ms', 'anything', 'anything')
00386     >>> newspw
00387     '*'
00388     >>> vis = casa['dirs']['data'] + '/regression/unittest/split/unordered_polspw.ms'
00389     >>> update_spwchan(vis, '0~1:1~3,5;7:10~20^2', '0~1:2~3,5;7:12~18^2')
00390     '0~1:1~2,2~3:1~4'
00391     >>> update_spwchan(vis, '7', '3')
00392     ValueError: '3' is not a subset of '7'.
00393     >>> update_spwchan(vis, '7:10~20^2', '7:12~18^3')
00394     ValueError: '7:12~18^3' is not a subset of '7:10~20^2'.
00395     >>> update_spwchan(vis, '7:10~20^2', '7:12~18^3', truncate=True)
00396     '0:1~4^3'
00397     >>> update_spwchan(vis, '7:10~20^2', '7:12~18^3', truncate=True, widths={7: 2})
00398     '0:0~2^2'
00399     """
00400     # Convert '' to 'select everything'.
00401     if not sch0:
00402         sch0 = '*'
00403     if not sch1:
00404         sch1 = '*'
00405 
00406     # Short circuits
00407     if sch1 == '*':
00408         return '*'
00409     elif sch1 in (sch0, '*'):
00410         return '*'
00411 
00412     sch0sets = spwchan_to_sets(vis, sch0)
00413     sch1sets = spwchan_to_sets(vis, sch1)
00414 
00415     outsets = {}
00416     outspw = 0
00417     s0spws = sorted(sch0sets.keys())
00418     s1spws = sorted(sch1sets.keys())
00419     ns0spw = len(s0spws)
00420     nchans = {}
00421     for s in s1spws:
00422         if s in s0spws:
00423             s0 = sch0sets[s]
00424             s1 = sch1sets[s]
00425 
00426             # Check for and handle (throw or dispose) channels in sch1 that aren't in
00427             # sch0.
00428             if s1.difference(s0):
00429                 if truncate:
00430                     s1.intersection_update(s0)
00431                     if not s1:
00432                         raise ValueError, "'%s' does not overlap '%s'." % (sch1, sch0)
00433                 else:
00434                     raise ValueError, "'%s' is not a subset of '%s'." % (sch1, sch0)
00435 
00436             # Adapt s1 for a post-s0 world.
00437             s0list = sorted(list(s0))
00438             s1list = sorted(list(s1))
00439             outchan = 0
00440             nc0 = len(s0list)
00441             for s1ind in xrange(len(s1list)):
00442                 while (outchan < nc0) and (s0list[outchan] < s1list[s1ind]):
00443                     outchan += 1
00444                 if outchan == nc0:  # Shouldn't happen
00445                     outchan -= 1
00446                 s1list[s1ind] = outchan / widths.get(s, 1)
00447 
00448             # Determine outspw.
00449             while (outspw < ns0spw) and (s0spws[outspw] < s):
00450                 outspw += 1
00451             if outspw == ns0spw:  # Shouldn't happen
00452                 outspw -= 1
00453 
00454             outsets[outspw] = set(s1list)
00455 
00456             # Get the number of channels per spw that are selected by s0.
00457             nchans[outspw] = len(s0)
00458         elif not truncate:
00459             raise ValueError, str(s) + ' is not a selected spw of ' + sch0
00460 
00461     return sets_to_spwchan(outsets, nchans)
00462 
00463 def expand_tilde(tstr, conv_multiranges=False):
00464     """
00465     Expands a string like '8~11' to [8, 9, 10, 11].
00466     Returns '*' if tstr is ''!
00467 
00468     conv_multiranges: If True, '*' will be returned if tstr contains ';'.
00469                       (split can't yet handle multiple channel ranges per spw.)
00470 
00471     Examples:
00472     >>> from update_spw import expand_tilde
00473     >>> expand_tilde('8~11')
00474     [8, 9, 10, 11]
00475     >>> expand_tilde(None)
00476     '*'
00477     >>> expand_tilde('3~7^2;9~11')
00478     [3, 5, 7, 9, 10, 11]
00479     >>> expand_tilde('3~7^2;9~11', True)
00480     '*'
00481     """
00482     tstr = str(tstr)  # Allows bare ints.
00483     if (not tstr) or (conv_multiranges and tstr.find(';') > -1):
00484         return '*'
00485 
00486     tstr = tstr.replace("'", '')  # Dequote
00487     tstr = tstr.replace('"', '')
00488 
00489     numset = set([])
00490 
00491     for numrang in tstr.split(';'):
00492         step = 1
00493         try:
00494             if numrang.find('~') > -1:
00495                 if numrang.find('^') > -1:
00496                     numrang, step = numrang.split('^')
00497                     step = int(step)
00498                 start, end = map(int, numrang.split('~'))
00499             else:
00500                 start = int(numrang)
00501                 end = start
00502         except:
00503             raise ValueError, 'numrang = ' + numrang + ', tstr = ' + tstr + ', conv_multiranges = ' + str(conv_multiranges)
00504         numset.update(range(start, end + 1, step))
00505     return sorted(list(numset))
00506 
00507 def spw_to_dict(spw, spwdict={}, conv_multiranges=True):
00508     """
00509     Expand an spw:chan string to {s0: [s0chans], s1: [s1chans, ...], ...}
00510     where s0, s1, ... are integers for _each_ selected spw, and s0chans is a
00511     set of selected chans (as integers) for s0.  '' instead of a channel set
00512     means that all of the channels are selected.
00513 
00514     The spw:chan dict is unioned with spwdict.
00515 
00516     Returning an empty dict means everything should be selected (i.e. spw = '').
00517     (split can't yet handle multiple channel ranges per spw.)
00518 
00519     conv_multiranges: If True, any spw with > 1 channel range selected will
00520                       have ALL of its channels selected.
00521                       (split can't yet handle multiple channel ranges per spw.)
00522 
00523     Examples:
00524     >>> from update_spw import spw_to_dict
00525     >>> spw_to_dict('', {})
00526     {}
00527     >>> spw_to_dict('6~8:2~5', {})[6]
00528     set([2, 3, 4, 5])
00529     >>> spw_to_dict('6~8:2~5', {})[8]
00530     set([2, 3, 4, 5])
00531     >>> spw_to_dict('6~8:2~5', {6: ''})[6]
00532     ''
00533     >>> spw_to_dict('6~8:2~5', {6: '', 7: set([1, 7])})[7]
00534     set([1, 2, 3, 4, 5, 7])
00535     >>> spw_to_dict('7', {6: '', 7: set([1, 7])})[7]
00536     ''
00537     >>> spw_to_dict('7:123~127;233~267', {6: '', 7: set([1, 7])})[7]  # Multiple chan ranges
00538     ''
00539     >>> spw_to_dict('5,7:123~127;233~267', {6: '', 7: set([1, 7])})[5]
00540     ''
00541     >>> spw_to_dict('5:3~5,7:123~127;233~267', {6: '', 7: set([1, 7])})[5]
00542     set([3, 4, 5])
00543     """
00544     if not spw:
00545         return {}
00546 
00547     myspwdict = copy.deepcopy(spwdict)
00548 
00549     # Because ; means different things when it separates spws and channel
00550     # ranges, I can't think of a better way to construct myspwdict than an
00551     # explicit state machine.  (But $spws_alone =~ s/:[^,]+//g;)
00552     inspw = True    # Must start with an spw.
00553     spwgrp = ''
00554     chagrp = ''
00555 
00556     def enter_ranges(spwg, chag):
00557         spwrange = expand_tilde(spwg)
00558         if spwrange == '*':  # This shouldn't happen.
00559             return {}
00560         else:
00561             charange = expand_tilde(chag, conv_multiranges)
00562         for s in spwrange:
00563             if charange == '*':
00564                 myspwdict[s] = ''
00565             else:
00566                 if not myspwdict.has_key(s):
00567                     myspwdict[s] = set([])
00568                 if myspwdict[s] != '':
00569                     myspwdict[s].update(charange)        
00570 
00571     for c in spw:
00572         if c == ',' or (inspw and c == ';'):  # Start new [spw, chan] pair.
00573             # Store old one.
00574             enter_ranges(spwgrp, chagrp)
00575 
00576             # Initialize new one.
00577             spwgrp = ''
00578             chagrp = ''
00579             inspw = True
00580         elif c == ':':
00581             inspw = False
00582         elif inspw:
00583             spwgrp += c
00584         else:
00585             chagrp += c
00586 
00587     # Store final [spw, chan] pair.
00588     enter_ranges(spwgrp, chagrp)
00589     return myspwdict
00590 
00591 def join_spws(spw1, spw2, span_semicolon=True):
00592     """
00593     Returns the union of spw selection strings spw1 and spw2.
00594 
00595     span_semicolon (default True): If True, for any spws
00596     that have > 1 channel range, the entire spw will be selected.
00597 
00598     Examples:
00599     >>> from update_spw import join_spws
00600     >>> join_spws('0~2:3~5,3:9~13', '')
00601     ''
00602     >>> join_spws('0~2:3~5,3:9~13', '1~3:4~7')
00603     '0:3~5,1~2:3~7,3'
00604     >>> join_spws('1~10:5~122,15~22:5~122', '1~10:5~122,15~22:5~122')
00605     '1~10:5~122,15~22:5~122'
00606     >>> join_spws('', '')
00607     ''
00608     >>> join_spws('1~10:5~122,15~22:5~122', '0~21')
00609     '0~21,22:5~122'
00610     """
00611     if not spw1 or not spw2:
00612         return ''
00613         
00614     spwdict = spw_to_dict(spw1, {})
00615     spwdict = spw_to_dict(spw2, spwdict)
00616 
00617     res = ''
00618     # Convert channel sets to strings
00619     for s in spwdict:
00620         cstr = ''
00621         if isinstance(spwdict[s], set):
00622             cstr = set_to_chanstr(spwdict[s])
00623             if span_semicolon and ';' in cstr:
00624                 cstr = ''
00625         spwdict[s] = cstr
00626 
00627     # If consecutive spws have the same channel selection, merge them.
00628     slist = spwdict.keys()
00629     slist.sort()
00630     res = str(slist[0])
00631     laststart = 0
00632     for i in xrange(1, len(slist)):
00633         # If consecutive spws have the same channel list,
00634         if slist[i] == slist[i - 1] + 1 and spwdict[slist[i]] == spwdict[slist[i - 1]]:
00635             if slist[i] == slist[laststart] + 1:
00636                 res += '~'  # Continue the spw range.
00637         else:           # Terminate it and start a new one.
00638             if res[-1] == '~':          # if start != end
00639                 res += str(slist[i - 1])
00640             if spwdict[slist[i - 1]] != '':          # Add channel range, if any.
00641                 res += ':' + spwdict[slist[i - 1]]
00642             res += ',' + str(slist[i])
00643             laststart = i
00644     if res[-1] == '~':               # Finish the last range if it is dangling.
00645         res += str(slist[-1])
00646     if spwdict[slist[-1]] != '':          # Add channel range, if any.
00647         res += ':' + spwdict[slist[-1]]
00648     return res
00649 
00650 def intersect_spws(spw1, spw2):
00651     """
00652     Almost the opposite of join_spws(), this returns the list of spws that the
00653     spw:chan selection strings spw1 and spw2 have in common.  Unlike join_spws(),
00654     channel ranges are ignored.  '' in the input counts as 'select everything',
00655     so the intersection of '' with anything is anything.  If the intersection
00656     really is everything, '' is returned instead of a set.
00657 
00658     Examples:
00659     >>> from update_spw import intersect_spws
00660     >>> intersect_spws('0~2:3~5,3:9~13', '')
00661     set([0, 1, 2, 3])
00662     >>> intersect_spws('0~2:3~5,3:9~13', '0~2:7~9,5')
00663     set([0, 1, 2])
00664     >>> intersect_spws('0~2:3~5;10~13,3:9~13', '0~2:7~9,5')
00665     set([0, 1, 2])
00666     >>> intersect_spws('0~2:3~5,3:9~13', '10~12:7~9,5')  # Empty set
00667     set([])
00668     >>> intersect_spws('', '')                           # Everything
00669     ''
00670     """
00671     if spw1 == '':
00672         if spw2 == '':
00673             return ''     # intersection('', '') = ''
00674         else:             # intersection('', spw2) = spw2
00675             return set(spw_to_dict(spw2, {}).keys()) # Just the spws, no chan ranges
00676     elif spw2 == '':      # intersection('', spw1) = spw1
00677         return set(spw_to_dict(spw1, {}).keys())     # Just the spws, no chan ranges
00678     else:
00679         spwset1 = set(spw_to_dict(spw1, {}).keys())  # spws are the keys, chan
00680         spwset2 = set(spw_to_dict(spw2, {}).keys())  # ranges are the values.
00681         return spwset1.intersection(spwset2)
00682 
00683 def subtract_spws(spw1, spw2):
00684     """
00685     Returns the set of spws of spw selection string spw1 that are not in spw2.
00686     Like intersect_spws(), this intentionally ignores channel ranges.  It
00687     assumes that spw1 and spw2 refer to the same MS (this only matters for '').
00688     subtract_spws('', '0~5') is a tough case: it is impossible to know whether
00689     '' is equivalent to '0~5' without reading the MS's SPECTRAL_WINDOW
00690     subtable, so it returns 'UNKNOWN'.
00691 
00692     Examples:
00693     >>> from update_spw import subtract_spws
00694     >>> subtract_spws('0~2:3~5,3:9~13', '') # Anything - Everything
00695     set([])
00696     >>> subtract_spws('0~2:3~5,3:9~13', '0~2:7~9,5')
00697     set([3])
00698     >>> subtract_spws('', '0~2:7~9,5') # Everything - Something
00699     'UNKNOWN'
00700     >>> subtract_spws('0~2,3:9~13', '4~7:7')  # Something - Something Else
00701     set([0, 1, 2, 3])
00702     >>> subtract_spws('', '')              # Everything - Everything
00703     set([])
00704     """
00705     if spw1 == '':
00706         if spw2 == '':
00707             return set([])
00708         else:
00709             return 'UNKNOWN'
00710     elif spw2 == '':
00711         return set([])
00712     else:
00713         spwset1 = set(spw_to_dict(spw1, {}).keys())  # spws are the keys, chan
00714         spwset2 = set(spw_to_dict(spw2, {}).keys())  # ranges are the values.
00715         return spwset1.difference(spwset2)
00716     
00717 if __name__ == '__main__':
00718     import doctest, sys
00719     doctest.testmod(verbose=True)