Source code for pyspeckit.spectrum.readers.gbt

"""
GBTIDL SDFITS file
==================

GBTIDL SDFITS files representing GBT observing sessions can be read into
pyspeckit.  Additional documentation is needed.  Nodding reduction is
supported, frequency switching is not.

"""
from __future__ import print_function
from six import iteritems
try:
    import astropy.io.fits as pyfits
except ImportError:
    import pyfits
import pyspeckit
import numpy as np
try:
    import coords
except ImportError:
    coords = None

def unique_targets(sdfitsfile):
    bintable = _get_bintable(sdfitsfile)
    return uniq(bintable.data['OBJECT'])

[docs]def count_integrations(sdfitsfile, target): """ Return the number of integrations for a given target (uses one sampler; assumes same number for all samplers) """ bintable = _get_bintable(sdfitsfile) whobject = bintable.data['OBJECT'] == target any_sampler = bintable.data['SAMPLER'][whobject][0] whsampler = bintable.data['SAMPLER'][whobject] == any_sampler return (whsampler).sum()
[docs]def list_targets(sdfitsfile, doprint=True): """ List the targets, their location on the sky... """ bintable = _get_bintable(sdfitsfile) # Things to include: # Scan Source Vel Proc Seqn RestF nIF nInt nFd Az El # 1 G33.13-0.09 87.4 Nod 1 14.488 4 15 2 209.7 47.9 strings = [ "\n%18s %10s %10s %26s%8s %9s %9s %9s" % ("Object Name","RA","DEC","%12s%14s"%("RA","DEC"),"N(ptgs)","Exp.Time","requested","n(ints)") ] for objectname in uniq(bintable.data['OBJECT']): whobject = bintable.data['OBJECT'] == objectname RA,DEC = bintable.data['TRGTLONG'][whobject],bintable.data['TRGTLAT'][whobject] RADEC = zip(RA,DEC) midRA,midDEC = np.median(RA),np.median(DEC) npointings = len(set(RADEC)) if coords is not None: sexagesimal = coords.Position((midRA,midDEC)).hmsdms() else: sexagesimal = "" firstsampler = bintable.data['SAMPLER'][whobject][0] whfirstsampler = bintable.data['SAMPLER'] == firstsampler exptime = bintable.data['EXPOSURE'][whobject*whfirstsampler].sum() duration = bintable.data['DURATION'][whobject*whfirstsampler].sum() n_ints = count_integrations(bintable, objectname) strings.append( "%18s %10f %10f %26s%8i %9g %9g %9i" % (objectname,midRA,midDEC,sexagesimal, npointings, exptime, duration, n_ints) ) if doprint: print("\n".join(strings)) return strings
[docs]def read_gbt_scan(sdfitsfile, obsnumber=0): """ Read a single scan from a GBTIDL SDFITS file """ bintable = _get_bintable(sdfitsfile) data = bintable.data[obsnumber]['DATA'] header = pyfits.Header() for par in bintable.data.dtype.names: if par not in ('DATA',): try: header[par[:8]] = bintable.data[obsnumber][par] except ValueError: header[par[:8]] = str(bintable.data[obsnumber][par]) header['CUNIT1'] = 'Hz' HDU = pyfits.PrimaryHDU(data=data,header=header) sp = pyspeckit.Spectrum(HDU,filetype='pyfits') #sp.xarr.frame = 'topo' # sp.header.get('VELDEF') #'topo' # HACK - temporary! # Convert xarr to LSR units #sp.xarr.convert_to_unit('m/s') obsfreq = header['OBSFREQ'] centerfreq = pyspeckit.spectrum.units.SpectroscopicAxis(np.float(obsfreq),'Hz',refX=obsfreq,refX_unit='Hz') delta_freq = (centerfreq.as_unit('m/s') + header['VFRAME']).as_unit(centerfreq.units) - centerfreq sp.xarr -= delta_freq return sp
[docs]def read_gbt_target(sdfitsfile, objectname, verbose=False): """ Give an object name, get all observations of that object as an 'obsblock' """ bintable = _get_bintable(sdfitsfile) whobject = bintable.data['OBJECT'] == objectname if verbose: print("Number of individual scans for Object %s: %i" % (objectname,whobject.sum())) calON = bintable.data['CAL'] == 'T' # HACK: apparently bintable.data can sometimes treat itself as scalar... if np.isscalar(calON): calON = np.array([(val in ['T',True]) for val in bintable.data['CAL']]) n_nods = np.unique(bintable.data['PROCSIZE']) blocks = {} for sampler in np.unique(bintable.data[whobject]['SAMPLER']): whsampler = bintable.data['SAMPLER'] == sampler nods = np.unique(bintable.data['PROCSEQN'][whsampler*whobject]) for nod in nods: whnod = bintable.data['PROCSEQN'] == nod for onoff in ('ON','OFF'): calOK = (calON - (onoff=='OFF')) whOK = (whobject*whsampler*calOK*whnod) if whOK.sum() == 0: continue if verbose: print("Number of spectra for sampler %s, nod %i, cal%s: %i" % (sampler,nod,onoff,whOK.sum())) crvals = bintable.data[whOK]['CRVAL1'] if len(crvals) > 1: maxdiff = np.diff(crvals).max() else: maxdiff = 0 freqres = np.max(bintable.data[whOK]['FREQRES']) if maxdiff < freqres: splist = [read_gbt_scan(bintable,ii) for ii in np.where(whOK)[0]] blocks[sampler+onoff+str(nod)] = pyspeckit.ObsBlock(splist,force=True) blocks[sampler+onoff+str(nod)]._arithmetic_threshold = np.diff(blocks[sampler+onoff+str(nod)].xarr).min() / 5. else: print("Maximum frequency difference > frequency resolution: %f > %f" % (maxdiff, freqres)) return blocks
[docs]def reduce_gbt_target(sdfitsfile, objectname, nbeams, verbose=False): """ Wrapper - read an SDFITS file, get an object, reduce it (assuming nodded) and return it """ # for efficiency, this should be stored - how? blocks = read_gbt_target(sdfitsfile, objectname, verbose=verbose) reduced_nods = reduce_nod(blocks, nbeams) return reduced_nods
[docs]def reduce_nod(blocks, verbose=False, average=True, fdid=(1,2)): """ Do a nodded on/off observation given a dict of observation blocks as produced by read_gbt_target Parameters ---------- fdid : 2-tuple """ fd0,fd1 = fdid # strip off trailing digit to define pairs nodpairs = uniq([s[:-1].replace("ON","").replace("OFF","") for s in blocks]) reduced_nods = {} # for each pair... for sampname in nodpairs: on1 = sampname+"ON1" off1 = sampname+"OFF1" on2 = sampname+"ON2" off2 = sampname+"OFF2" feednumber = blocks[on1].header.get('FEED') # don't need this - if feednumber is 1, ref feed should be 2, and vice-versa reference_feednumber = blocks[on1].header.get('SRFEED') on1avg = blocks[on1].average() off1avg = blocks[off1].average() on2avg = blocks[on2].average() off2avg = blocks[off2].average() # first find TSYS tsys1 = dcmeantsys(on1avg,off1avg,on1avg.header.get('TCAL')) tsys2 = dcmeantsys(on2avg,off2avg,on2avg.header.get('TCAL')) if verbose: print("Nod Pair %s (feed %i) has tsys1=%f tsys2=%f" % (sampname, feednumber, tsys1, tsys2)) # then get the total power if average: tp1 = totalpower(on1avg, off1avg, average=False) tp2 = totalpower(on2avg, off2avg, average=False) else: tp1 = totalpower(blocks[on1], blocks[off1], average=False) tp2 = totalpower(blocks[on2], blocks[off2], average=False) # then do the signal-reference bit if feednumber == fd0: nod = sigref(tp1,tp2,tsys2) elif feednumber == fd1: nod = sigref(tp2,tp1,tsys1) else: raise ValueError("Feed number %i is not understood. Try specifying fd0, fd1 keywords if this is a genuine nodded observation." % feednumber) reduced_nods[sampname] = nod return reduced_nods
[docs]def reduce_totalpower(blocks, verbose=False, average=True, fdid=1): """ Reduce a total power observation """ # strip off trailing digit to define pairs ids = uniq([s[:-1].replace("ON","").replace("OFF","") for s in blocks]) reduced_tps = {} # for each pair... for sampname in ids: on1 = sampname+"ON1" off1 = sampname+"OFF1" feednumber = blocks[on1].header.get('FEED') # don't need this - if feednumber is 1, ref feed should be 2, and vice-versa reference_feednumber = blocks[on1].header.get('SRFEED') on1avg = blocks[on1].average() off1avg = blocks[off1].average() # first find TSYS tsys1 = dcmeantsys(on1avg,off1avg,on1avg.header.get('TCAL')) if verbose: print("TP: %s (feed %i) has tsys1=%f" % (sampname, feednumber, tsys1)) # then get the total power if average: tp1 = totalpower(on1avg, off1avg, average=False) else: tp1 = totalpower(blocks[on1], blocks[off1], average=False) reduced_tps[sampname] = tp1 return reduced_tps
def _get_bintable(sdfitsfile): """ Private function: given a filename, HDUlist, or bintable, return a bintable """ if isinstance(sdfitsfile, pyfits.HDUList): bintable = sdfitsfile[1] elif isinstance(sdfitsfile, pyfits.BinTableHDU): bintable = sdfitsfile elif isinstance (sdfitsfile, pyfits.FITS_rec): bintable = sdfitsfile else: bintable = pyfits.open(sdfitsfile)[1] return bintable
[docs]def dcmeantsys(calon,caloff,tcal,debug=False): """ from GBTIDL's dcmeantsys.py ; mean_tsys = tcal * mean(nocal) / (mean(withcal-nocal)) + tcal/2.0 """ # Use the inner 80% of data to calculate mean Tsys nchans = calon.data.shape[0] pct10 = nchans/10 pct90 = nchans - pct10 meanoff = np.mean(caloff.slice(pct10,pct90,units='pixels').data) meandiff = np.mean(calon.slice(pct10,pct90,units='pixels').data - caloff.slice(pct10,pct90,units='pixels').data) meanTsys = ( meanoff / meandiff * tcal + tcal/2.0 ) if debug: print(caloff) print(caloff.slice(pct10,pct90,units='pixels')) print(calon) print(calon.slice(pct10,pct90,units='pixels')) print("pct10: %i pct90: %i mean1: %f mean2: %f tcal: %f tsys: %f" % (pct10,pct90,meanoff,meandiff,tcal,meanTsys)) return meanTsys
[docs]def totalpower(calon, caloff, average=True): """ Do a total-power calibration of an on/off data set (see dototalpower.pro in GBTIDL) """ if hasattr(calon,'average') and average: ON = calon.average() elif hasattr(calon,'average'): ON = calon.data else: ON = calon if hasattr(caloff,'average') and average: OFF = caloff.average() elif hasattr(caloff,'average'): OFF = caloff.data else: OFF = caloff TP = (ON+OFF)/2. return TP
[docs]def sigref(nod1, nod2, tsys_nod2): """ Signal-Reference ('nod') calibration ; ((dcsig-dcref)/dcref) * dcref.tsys see GBTIDL's dosigref """ return (nod1-nod2)/nod2*tsys_nod2
[docs]def find_matched_freqs(reduced_blocks, debug=False): """ Use frequency-matching to find which samplers observed the same parts of the spectrum *WARNING* These IF numbers don't match GBTIDL's! I don't know how to get those to match up! """ # IF order sampler_numbers = [int(name[1:]) for name in reduced_blocks.keys()] sorted_pairs = sorted(zip(sampler_numbers,reduced_blocks.keys())) sorted_names = zip(*sorted_pairs)[1] # how many IFs? frequencies = dict((name,reduced_blocks[name].header.get('OBSFREQ')) for name in sorted_names) if debug: print(frequencies) round_frequencies = dict((name, round_to_resolution(reduced_blocks[name].header.get('OBSFREQ'), reduced_blocks[name].header.get('FREQRES'))) for name in sorted_names) if debug: print(round_frequencies) unique_frequencies = uniq(round_frequencies.values()) # uniq is an order-preserving function if debug: print(unique_frequencies) nIFs = len(unique_frequencies) if debug: print(frequencies.keys()) print(round_frequencies.keys()) print(unique_frequencies) IF_dict = {} # most annoying part of this whole process... (besides the obvious 1+1=23452342345 error...) # ifnumbers are approximately defined by the order things are written.... but only very approximately for ifnum,freq in enumerate(unique_frequencies): IF_dict[ifnum] = [sampler for (sampler,rf) in iteritems(round_frequencies) if rf == freq] if debug: print(ifnum,freq,IF_dict[ifnum]) return IF_dict
[docs]def uniq(seq): """ from http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order """ seen = set() seen_add = seen.add return [ x for x in seq if x not in seen and not seen_add(x)]
[docs]def round_to_resolution(frequency, resolution): """ kind of a hack, but round the frequency to the nearest integer multiple of the resolution, then multiply it back into frequency space """ # doesn't really work... divisor = 10**np.floor(np.log10(resolution)) divisor = resolution return (np.round(frequency/divisor)) * divisor
[docs]def find_pols(block): """ Get a dictionary of the polarization for each sampler """ pols = dict((name,polnum_to_pol[int(block[name].header.get('CRVAL4'))]) for name in block) return pols
[docs]def find_feeds(block): """ Get a dictionary of the feed numbers for each sampler """ feeds = dict((name,block[name].header.get('FEED')) for name in block) return feeds
[docs]def identify_samplers(block): """ Identify each sampler with an IF number, a feed number, and a polarization """ feeds = find_feeds(block) pols = find_pols(block) ifs = find_matched_freqs(block) IDs = dict( (name, {'pol': pols[name], 'feed': feeds[name], 'IF': [k for k,v in iteritems(ifs) if name in v][0]}) for name in block.keys() ) return IDs
[docs]def average_pols(block): """ Average the polarizations for each feed in each IF """ # will have names like "(IFnum)(feed)" averaged_pol_dict = {} ifdict = find_matched_freqs(block) feeddict = find_feeds(block) IDs = identify_samplers(block) for ifnum,ifsampler in iteritems(ifdict): for sampler,feednum in iteritems(feeddict): if sampler not in ifsampler: continue newname = "if%ifd%i" % (ifnum, feednum) matched_samplers = [sampler_name for (sampler_name,ID) in iteritems(IDs) if ID['feed'] == feednum and ID['IF'] == ifnum] if len(matched_samplers) != 2: raise ValueError("Too few/many matches: %s" % matched_samplers) if newname not in averaged_pol_dict: average = (block[matched_samplers[0]] + block[matched_samplers[1]]) / 2. averaged_pol_dict[newname] = average return averaged_pol_dict
[docs]def average_IF(block, debug=False): """ Average the polarizations for each feed in each IF """ # will have names like "(IFnum)" averaged_dict = {} ifdict = find_matched_freqs(block, debug=debug) import operator for ifnum,ifsamplers in iteritems(ifdict): if debug: print("if%i: freq %g" % (ifnum, block[ifsamplers[0]].header['OBSFREQ'])) averaged_dict["if%i" % ifnum] = reduce(operator.add,[block[name] for name in ifsamplers]) / len(ifsamplers) return averaged_dict
polnum_to_pol = { 1: 'I', 2: 'Q', 3: 'U', 4: 'V', -1: 'RR', -2: 'LL', -3: 'RL', -4: 'LR', -5: 'XX', -6: 'YY', -7: 'XY', -8: 'YX'}
[docs]class GBTSession(object): """ A class wrapping all of the above features """ def __init__(self, sdfitsfile): """ Load an SDFITS file or a pre-loaded FITS file """ self.bintable = _get_bintable(sdfitsfile) self.targets = dict((target,None) for target in unique_targets(self.bintable)) self.print_header = "\n".join( ("Observer: " + self.bintable.data[0]['OBSERVER'], "Project: %s" % self.bintable.header.get('PROJID'), "Backend: %s" % self.bintable.header.get('BACKEND'), "Telescope: %s" % self.bintable.header.get('TELESCOP'), "Bandwidth: %s" % self.bintable.data[0]['BANDWID'], "Date: %s" % self.bintable.data[0]['DATE-OBS']) ) @property def nbeams(self): # caching: if hasattr(self,'_nbeams'): return self._nbeams else: self._beams = np.unique(self.bintable.data['FEED']) self._nbeams = len(self._beams) return self._nbeams @property def beams(self): if hasattr(self,'_beams'): return self._beams else: self._beams = np.unique(self.bintable.data['FEED']) return self._beams def __repr__(self): self.instance_info = super(GBTSession,self).__repr__() if not hasattr(self,'StringDescription'): self.StringDescription = list_targets(self.bintable, doprint=False) return self.print_header+"\n"+"\n".join(self.StringDescription) def __str__(self): if not hasattr(self,'StringDescription'): self.StringDescription = list_targets(self.bintable, doprint=False) return self.print_header+"\n"+"\n".join(self.StringDescription) def __getitem__(self, ind): return self.targets[ind]
[docs] def load_target(self, target, **kwargs): """ Load a Target... """ self.targets[target] = GBTTarget(self, target, **kwargs) return self.targets[target]
[docs] def reduce_target(self, target, **kwargs): """ Reduce the data for a given object name """ if self.targets[target] is None: self.load_target(target) self.targets[target].reduce(**kwargs) return self.targets[target]
[docs] def reduce_all(self): """ """ for target in self.targets: self.reduce_target(target)
[docs]class GBTTarget(object): """ A collection of ObsBlocks or Spectra """ def __init__(self, Session, target, **kwargs): """ Container for the individual scans of a target from a GBT session """ self.name = target self.Session = Session self.blocks = read_gbt_target(Session.bintable, target, **kwargs) self.spectra = {} @property def nbeams(self): return self.Session.nbeams @property def beams(self): return self.Session.beams def __getitem__(self, ind): return self.spectra[ind] def __repr__(self): self.instance_info = super(GBTTarget,self).__repr__() self.StringDescription = [("Object %s with %i scan blocks and %i 'reduced' spectra" % (self.name,len(self.blocks),len(self.spectra)))] if hasattr(self,'reduced_scans'): self.StringDescription += ["%s" % ID for ID in identify_samplers(self.reduced_scans)] return "\n".join(self.StringDescription)
[docs] def reduce(self, obstype='nod', **kwargs): """ Reduce nodded observations (they should have been read in __init__) """ if obstype == 'nod': self.reduced_scans = reduce_nod(self.blocks, fdid=self.beams, **kwargs) elif obstype == 'tp': self.reduced_scans = reduce_totalpower(self.blocks, fdid=self.beams, **kwargs) else: raise NotImplementedError('Obstype {0} not implemented.'.format(obstype)) self.spectra.update(self.reduced_scans)
def average_pols(self): if hasattr(self,'reduced_scans'): self.averaged_pols = average_pols(self.reduced_scans) self.spectra.update(self.averaged_pols) else: raise ValueError("Must reduce the scans before averaging pols") def average_IFs(self, **kwargs): if hasattr(self,'reduced_scans'): self.averaged_IFs = average_IF(self.reduced_scans, **kwargs) self.spectra.update(self.averaged_IFs) else: raise ValueError("Must reduce the scans before averaging IFs")
""" TEST CODE for name in reduced_nods: for num in '1','2': name='A10' num='1' tsys = 10 while (tsys>5): av1 = blocks[name+'ON'+num].average() av2 = blocks[name+'OFF'+num].average() tsys = dcmeantsys(av1, av2, blocks[name+'OFF'+num].header['TCAL'],debug=True) if tsys < 5: print "%s %s: %f" % (name,num,tsys), print av1,np.mean(av1.slice(409,3687,units='pixels').data) print av2,np.mean(av2.slice(409,3687,units='pixels').data) print av1-av2,np.mean(av1.slice(409,3687,units='pixels').data-av2.slice(409,3687,units='pixels').data) print blocks[name+'OFF'+num].header['TCAL'] """