Пример #1
0
    def __init__(self, smoothing_window_size=0):

        # set calibration constants
        self.BB = .0132  # Ruze equation parameter
        self.UNDER_2GHZ_TAU_0 = 0.008
        self.SMOOTHING_WINDOW = smoothing_window_size
        self.pu = Pipeutils()
Пример #2
0
def preview_zenith_tau(log, row_list, cl_params, feeds, windows, pols):

    # if using the weather database
    if not cl_params.zenithtau:

        foo = row_list.get(cl_params.mapscans[0], feeds[0], windows[0],
                           pols[0])
        ff = fitsio.FITS(cl_params.infilename)
        extension = foo['EXTENSION']
        row = foo['ROW'][0]
        bar = ff[extension]['OBSFREQ', 'DATE-OBS'][row]
        dateobs = bar['DATE-OBS'][0]
        obsfreq = bar['OBSFREQ'][0]
        ff.close()

        weather = Weather()
        pu = Pipeutils()

        mjd = pu.dateToMjd(dateobs)
        zenithtau = weather.retrieve_zenith_opacity(mjd, obsfreq)
        log.doMessage('INFO',
                      'Zenith opacity for map: {0:.3f}'.format(zenithtau))

    # else if set at the command line
    else:

        log.doMessage('INFO',
                      'Zenith opacity for '
                      'map: {0:.3f}'.format(cl_params.zenithtau))
Пример #3
0
def test_dateToMjd():

    putils = Pipeutils()

    sdfits_date_string = '2009-02-10T21:09:00.10'
    result = putils.dateToMjd(sdfits_date_string)
    expected_result = 54872.8812512
    assert_almost_equal(result, expected_result)
Пример #4
0
def test_dateToMjd():

    putils = Pipeutils()
    
    sdfits_date_string = '2009-02-10T21:09:00.10'
    result = putils.dateToMjd( sdfits_date_string )
    expected_result = 54872.8812512
    assert_almost_equal( result, expected_result )
Пример #5
0
def test_masked_array():

    putils = Pipeutils()

    nan = float('nan')
    unmasked = np.array([1, 2, 3, 4, nan, 5, 6., 7.7, nan, 9])
    masked = putils.masked_array(unmasked)
    eq_(len(unmasked), len(masked))
    eq_(np.isnan(unmasked).tolist().count(True), 2)
    eq_(np.isnan(masked).tolist().count(True), 0)
    np.testing.assert_equal(masked.data, unmasked)
    eq_(masked.sum(), 37.7)
Пример #6
0
def test_masked_array():
    
    putils = Pipeutils()
    
    nan = float('nan')
    unmasked = np.array([1,2,3,4,nan,5,6.,7.7,nan,9])
    masked = putils.masked_array(unmasked)
    eq_( len(unmasked), len(masked) )
    eq_( np.isnan(unmasked).tolist().count(True), 2 )
    eq_( np.isnan(masked).tolist().count(True), 0 )
    np.testing.assert_equal( masked.data, unmasked )
    eq_( masked.sum(), 37.7 )
Пример #7
0
def preview_zenith_tau(log, row_list, cl_params, feeds, windows, pols):

    foo = None

    # if using the weather database
    if cl_params.zenithtau is None:
        for feed in feeds:
            for window in windows:
                for pol in pols:
                    try:
                        foo = row_list.get(cl_params.mapscans[0], feed, window,
                                           pol)
                        break  # if we found a row move on, otherwise try another feed/win/pol
                    except KeyError:
                        continue
        if not foo:
            log.doMessage('ERR',
                          'Could not find scan for zenith opacity preview')
            return

        ff = fitsio.FITS(cl_params.infilename)
        extension = foo['EXTENSION']
        row = foo['ROW'][0]
        bar = ff[extension]['OBSFREQ', 'DATE-OBS'][row]
        dateobs = bar['DATE-OBS'][0]
        obsfreq = bar['OBSFREQ'][0]
        ff.close()

        weather = Weather()
        pu = Pipeutils()

        mjd = pu.dateToMjd(dateobs)
        zenithtau = weather.retrieve_zenith_opacity(mjd, obsfreq, log)
        if zenithtau:
            log.doMessage(
                'INFO', 'Approximate zenith opacity for map: {0:.3f}'.format(
                    zenithtau))
        else:
            log.doMessage(
                'ERR', 'Not able to retrieve integration '
                'zenith opacity for calibration to:', cl_params.units,
                '\n  Please supply a zenith opacity or calibrate to Ta.')
            sys.exit(9)

    # else if set at the command line
    else:

        log.doMessage(
            'INFO', 'Zenith opacity for '
            'map: {0:.3f}'.format(cl_params.zenithtau))
Пример #8
0
    def __init__(self, smoothing_window_size=0):

        # set calibration constants
        self.BB = .0132  # Ruze equation parameter
        self.UNDER_2GHZ_TAU_0 = 0.008
        self.SMOOTHING_WINDOW = smoothing_window_size
        self.pu = Pipeutils()
Пример #9
0
    def __init__(self):

        # set calibration constants
        self.BB = .0132  # Ruze equation parameter
        self.UNDER_2GHZ_TAU_0 = 0.008
        self.SMOOTHING_WINDOW = 3
        self.pu = Pipeutils()
Пример #10
0
def preview_zenith_tau(log, row_list, cl_params, feeds, windows, pols):

    foo = None

    # if using the weather database
    if cl_params.zenithtau is None:
        for feed in feeds:
            for window in windows:
                for pol in pols:
                    try:
                        foo = row_list.get(cl_params.mapscans[0], feed, window, pol)
                        break  # if we found a row move on, otherwise try another feed/win/pol
                    except KeyError:
                        continue
        if not foo:
            log.doMessage('ERR', 'Could not find scan for zenith opacity preview')
            return

        ff = fitsio.FITS(cl_params.infilename)
        extension = foo['EXTENSION']
        row = foo['ROW'][0]
        bar = ff[extension]['OBSFREQ', 'DATE-OBS'][row]
        dateobs = bar['DATE-OBS'][0]
        obsfreq = bar['OBSFREQ'][0]
        ff.close()

        weather = Weather()
        pu = Pipeutils()

        mjd = pu.dateToMjd(dateobs)
        zenithtau = weather.retrieve_zenith_opacity(mjd, obsfreq, log)
        if zenithtau:
            log.doMessage('INFO',
                          'Approximate zenith opacity for map: {0:.3f}'.format(zenithtau))
        else:
            log.doMessage('ERR', 'Not able to retrieve integration '
                          'zenith opacity for calibration to:', cl_params.units,
                          '\n  Please supply a zenith opacity or calibrate to Ta.')
            sys.exit(9)

    # else if set at the command line
    else:

        log.doMessage('INFO',
                      'Zenith opacity for '
                      'map: {0:.3f}'.format(cl_params.zenithtau))
Пример #11
0
class Integration:

    def __init__(self, row):
        self.pu = Pipeutils()
        self.data = row

    def __getitem__(self, key):
        if key == 'DATA':
            return self.pu.masked_array(self.data[key][0])
        else:
            # strip leading and trailing whitespace
            return_val = self.data[key][0]
            if isinstance(return_val, str) or type(return_val) == np.string_:
                return return_val.strip()
            else:
                return return_val

    def __setitem__(self, key, value):
        self.data[key] = value
Пример #12
0
 def __init__(self, row):
     self.pu = Pipeutils()
     self.data = row
Пример #13
0
    def __init__(self):

        self.pu = Pipeutils()
Пример #14
0
class SdFits:
    """Class contains methods to read and write to the GBT SdFits format.

    This includes code for both the FITS files and associated index files.

    A description (but not a definition) of the SD FITS is here:
    https://safe.nrao.edu/wiki/bin/view/Main/SdfitsDetails

    """
    def __init__(self):

        self.pu = Pipeutils()

    def find_maps(self, indexfile, debug=False):
        """Find mapping blocks. Also find samplers used in each map

        Args:
            indexfile: input required to search for maps and samplers
            debug: optional debug flag

        Returns:
        a (list) of map blocks, with each entry a (tuple) of the form:
        (int) reference 1,
        (list of ints) mapscans,
        (int) reference 2

        """

        map_scans = {}
        observation, summary = self.parseSdfitsIndex(indexfile)
        feed = observation.feeds()[0]
        window = observation.windows()[0]
        pol = observation.pols()[0]

        # print results
        if debug:
            print '------------------------- All scans'
            for scanid in sorted(observation.scans()):
                scanstruct = observation.get(scanid, feed, window, pol)
                print(
                    'scan \'{0}\' obsid \'{1}\' procname \'{2}\' procscan \'{3}\''
                    .format(scanid, scanstruct['OBSID'],
                            scanstruct['PROCNAME'], scanstruct['PROCSCAN']))

        for scanid in observation.scans():
            scanstruct = observation.get(scanid, feed, window, pol)
            obsid = scanstruct['OBSID'].upper()
            procname = scanstruct['PROCNAME'].upper()
            procscan = scanstruct['PROCSCAN'].upper()

            # keyword check should depend on presence of PROCSCAN key, which is an
            # alternative to checking SDFITVER.
            # OBSID is the old way, PROCSCAN is the new way MR8Q312

            # create a new list that only has 'MAP' and 'OFF' scans
            if not procscan and (obsid == 'MAP' or obsid == 'OFF'):
                map_scans[scanid] = obsid
            elif (procscan == 'MAP' or procname == 'TRACK'
                  or (procname == 'ONOFF' and procscan == 'OFF')
                  or (procname == 'OFFON' and procscan == 'OFF')):
                map_scans[scanid] = procscan

        mapkeys = map_scans.keys()
        mapkeys.sort()

        if debug:
            print '------------------------- Relavant scans'
            for scanid in mapkeys:
                print 'scan', scanid, map_scans[scanid]

        maps = []  # final list of maps
        ref1 = None
        ref2 = None
        prev_ref2 = None
        mapscans = []  # temporary list of map scans for a single map

        if debug:
            print 'mapkeys', mapkeys

        MapParams = namedtuple("MapParams", "refscan1 mapscans refscan2")
        for idx, scan in enumerate(mapkeys):

            # look for the reference scans
            if (map_scans[scan]).upper() == 'OFF' or (
                    map_scans[scan]).upper() == 'ON':
                # if there is no ref1 or this is another ref1
                if not ref1 or (ref1 and bool(mapscans) == False):
                    ref1 = scan
                else:
                    ref2 = scan
                    prev_ref2 = ref2

            elif (map_scans[scan]).upper() == 'MAP':
                if not ref1 and prev_ref2:
                    ref1 = prev_ref2

                mapscans.append(scan)

            # see if this scan is the last one in the relevant scan list
            # or see if we have a ref2
            # if so, close out
            if ref2 or idx == len(mapkeys) - 1:
                maps.append(MapParams(ref1, mapscans, ref2))
                ref1 = False
                ref2 = False
                mapscans = []

        if debug:
            import pprint
            pprint.pprint(maps)

            for idx, mm in enumerate(maps):
                print "Map", idx
                if mm.refscan2:
                    print "\tReference scans.....", mm.refscan1, mm.refscan2
                else:
                    print "\tReference scan......", mm.refscan1
                print "\tMap scans...........", mm.mapscans

        return maps

    def parseSdfitsIndex(self, infile, mapscans=[]):

        try:
            ifile = open(infile)
        except IOError:
            print(
                "ERROR: Could not open file: {0}\n"
                "Please check and try again.".format(infile))
            raise

        observation = ObservationRows()

        while True:
            line = ifile.readline()
            # look for start of row data or EOF (i.e. not line)
            if '[rows]' in line or not line:
                break

        lookup_table = {}
        header = ifile.readline()

        fields = [xx.lstrip() for xx in re.findall(r' *\S+', header)]

        iterator = re.finditer(r' *\S+', header)
        for idx, mm in enumerate(iterator):
            lookup_table[fields[idx]] = slice(mm.start(), mm.end())

        rr = SdFitsIndexRowReader(lookup_table)

        summary = {'WINDOWS': set([]), 'FEEDS': set([])}

        # keep a list of suspect scans so we can know if the
        # user has already been warned
        suspectScans = set()

        for row in ifile:

            rr.setrow(row)

            scanid = int(rr['SCAN'])

            # have a look at the procedure
            #  if it is "Unknown", the data is suspect, so skip it
            procname = rr['PROCEDURE']
            if scanid in suspectScans:
                continue

            if ((scanid not in suspectScans)
                    and procname.lower() == 'unknown'):

                suspectScans.add(scanid)
                if scanid in mapscans:
                    print 'WARNING: scan', scanid, 'has "Unknown" procedure. Skipping.'
                continue

            feed = int(rr['FDNUM'])
            windowNum = int(rr['IFNUM'])
            pol = int(rr['PLNUM'])
            fitsExtension = int(rr['EXT'])
            rowOfFitsFile = int(rr['ROW'])
            obsid = rr['OBSID']
            procscan = rr['PROCSCAN']
            nchans = rr['NUMCHN']

            summary['WINDOWS'].add((windowNum, float(rr['RESTFREQ']) / 1e9))
            summary['FEEDS'].add(rr['FDNUM'])

            # we can assume all integrations of a single scan are within the same
            #   FITS extension
            observation.addRow(scanid, feed, windowNum, pol, fitsExtension,
                               rowOfFitsFile, obsid, procname, procscan,
                               nchans)

        try:
            ifile.close()
        except NameError:
            raise

        return observation, summary

    def getReferenceIntegration(self, cal_on, cal_off, scale):

        cal = Calibration()
        cal_ondata = cal_on['DATA']
        cal_offdata = cal_off['DATA']
        cref, exposure = cal.total_power(cal_ondata, cal_offdata,
                                         cal_on['EXPOSURE'],
                                         cal_off['EXPOSURE'])

        tcal = cal_off['TCAL'] * scale
        tsys = cal.tsys(tcal, cal_ondata, cal_offdata)

        dateobs = cal_off['DATE-OBS']
        timestamp = self.pu.dateToMjd(dateobs)

        tambient = cal_off['TAMBIENT']
        elevation = cal_off['ELEVATIO']

        return cref, tsys, exposure, timestamp, tambient, elevation

    def nameIndexFile(self, pathname):
        # -------------------------------------------------  name index file
        if not os.path.exists(pathname):
            print(
                'ERROR: Path does not exist {0}.\n'
                '       Please check and try again'.format(pathname))
            sys.exit(9)

        if os.path.isdir(pathname):
            bn = os.path.basename(pathname.rstrip('/'))
            return '{0}/{1}.index'.format(pathname, bn)

        elif os.path.isfile(pathname) and pathname.endswith('.fits'):
            return os.path.splitext(pathname)[0] + '.index'

        else:
            # doMessage(logger,msg.ERR,'input file not recognized as a fits file.',\
            #  ' Please check the file extension and change to \'fits\' if necessary.')
            print 'ERROR: Input file does not end with .fits:', pathname
            sys.exit(9)
Пример #15
0
class Calibration(object):
    """Class containing all the calibration methods for the GBT Pipeline.

    This includes both Position-switched and Frequency-switched calibration.

    """

    def __init__(self):

        # set calibration constants
        self.BB = .0132  # Ruze equation parameter
        self.UNDER_2GHZ_TAU_0 = 0.008
        self.SMOOTHING_WINDOW = 3
        self.pu = Pipeutils()

    # ------------- Unit methods: do not depend on any other pipeline methods

    def total_power(self, cal_on, cal_off, t_on, t_off):
        return np.ma.mean((cal_on, cal_off), axis=0), t_on+t_off

    def tsky_correction(self, tsky_sig, tsky_ref, spillover):
        return spillover*(tsky_sig-tsky_ref)

    def aperture_efficiency(self, reference_eta_a, freq_hz):
        """Determine aperture efficiency

        Keyword attributes:
        freq_hz -- input frequency in Hz

        Returns:
        eta -- point or main beam efficiency (range 0 to 1)

        EtaA model is from memo by Jim Condon, provided by Ron Maddalena

        >>> cal = Calibration()
        >>> round(cal.aperture_efficiency(.71, 23e9), 6)
        0.647483
        >>> round(cal.aperture_efficiency(.91, 23e9), 6)
        0.829872

        """

        freq_ghz = float(freq_hz)/1e9
        return reference_eta_a * math.e**-((self.BB * freq_ghz)**2)

    def main_beam_efficiency(self, reference_eta_b, freq_hz):
        """Determine main beam efficiency, given reference etaB value and freq.

        This is the same equation as is used to determine aperture efficiency.
        The only difference is the reference value.

        """

        return self.aperture_efficiency(reference_eta_b, freq_hz)

    def elevation_adjusted_opacity(self, zenith_opacity, elevation):
        """Compute elevation-corrected opacities.

        Keywords:

        zenith_opacity -- opacity based only on time
        elevation -- (float) elevation angle of integration or scan

        """
        number_of_atmospheres = self._natm(elevation)

        corrected_opacity = zenith_opacity * number_of_atmospheres

        return corrected_opacity

    def _natm(self, el_deg):
        """Compute number of atmospheres at elevation (deg)

        Keyword arguments:
        el_deg -- input elevation in degrees

        Returns:
        n_atmos -- output number of atmospheres

        Estimate the number of atmospheres along the line of site
        at an input elevation

        This comes from a model reported by Ron Maddale

        1) A = 1/sin(elev) is a good approximation down to about 15 deg but
        starts to get pretty poor below that.  Here's a quick-to-calculate,
        better approximation that I determined from multiple years worth of
        weather data and which is good down to elev = 1 deg:

        if (elev LT 39):
        A = -0.023437  + 1.0140 / math.sin( (math.pi/180.)*(elev + 5.1774 /
            (elev + 3.3543) ) )
        else:
        A = math.sin(math.pi*elev/180.)

        natm model is provided by Ron Maddalena

        """

        degree = math.pi/180.

        if (el_deg < 39.):
            first_term = -0.023437
            denominator = math.sin(degree*(el_deg + 5.1774/(el_deg + 3.3543)))
            second_term = 1.0140 / denominator
            n_atmos = first_term + second_term
        else:
            n_atmos = math.sin(degree*el_deg)

        #print('Model Number of Atmospheres:', n_atmos,
        #      ' at elevation ', el_deg)
        return n_atmos

    def _tatm(self, freq_hz, tmp_c):
        """Estimates the atmospheric effective temperature

        Keyword arguments:
        freq_hz -- input frequency in Hz
        where: tmp_c     - input ground temperature in Celsius

        Returns:
        air_temp_k -- output Air Temperature in Kelvin

        Based on local ground temperature measurements.  These estimates
        come from a model reported by Ron Maddalena

        1) A = 1/sin(elev) is a good approximation down to about 15 deg but
        starts to get pretty poor below that.  Here's a quick-to-calculate,
        better approximation that I determined from multiple years worth of
        weather data and which is good down to elev = 1 deg:

            if (elev LT 39) then begin
                A = -0.023437  + 1.0140 / sin( (!pi/180.)
                     * (elev + 5.1774 / (elev + 3.3543) ) )
            else begin
                A = sin(!pi*elev/180.)
            endif

        2) Using Tatm = 270 is too rough an approximation since Tatm can vary
        from 244 to 290, depending upon the weather conditions and observing
        frequency.  One can derive an approximation for the default Tatm that
        is accurate to about 3.5 K from the equation:

        TATM = (A0 + A1*FREQ + A2*FREQ^2 +A3*FREQ^3 + A4*FREQ^4 + A5*FREQ^5)
                    + (B0 + B1*FREQ + B2*FREQ^2 + B3*FREQ^3 + B4*FREQ^4 +
        B5*FREQ^5)*TMPC

        where TMPC = ground-level air temperature in C and Freq is in GHz.  The
        A and B coefficients are:

                                    A0=    259.69185966 +/- 0.117749542
                                    A1=     -1.66599001 +/- 0.0313805607
                                    A2=     0.226962192 +/- 0.00289457549
                                    A3=   -0.0100909636 +/- 0.00011905765
                                    A4=   0.00018402955 +/- 0.00000223708
                                    A5=  -0.00000119516 +/- 0.00000001564
                                    B0=      0.42557717 +/- 0.0078863791
                                    B1=     0.033932476 +/- 0.00210078949
                                    B2=    0.0002579834 +/- 0.00019368682
                                    B3=  -0.00006539032 +/- 0.00000796362
                                    B4=   0.00000157104 +/- 0.00000014959
                                    B5=  -0.00000001182 +/- 0.00000000105


        tatm model is provided by Ron Maddalena

        >>> cal = Calibration()
        >>> round(cal._tatm(23e9, 40), 6)
        298.885174
        >>> round(cal._tatm(23e9, 30), 6)
        289.780603
        >>> round(cal._tatm(1.42e9, 30), 6)
        271.978666

        """

        # where TMPC = ground-level air temperature in C and Freq is in GHz.
        # The A and B coefficients are:
        aaa = [259.69185966, -1.66599001, 0.226962192,
               -0.0100909636,  0.00018402955, -0.00000119516]
        bbb = [0.42557717,    0.033932476, 0.0002579834,
               -0.00006539032, 0.00000157104, -0.00000001182]
        freq_ghz = float(freq_hz)/1e9
        freq = float(freq_ghz)
        freq2 = freq*freq
        freq3 = freq2*freq
        freq4 = freq3*freq
        freq5 = freq4*freq

        air_temp_k = (aaa[0] + aaa[1]*freq + aaa[2]*freq2 + aaa[3]*freq3
                      + aaa[4]*freq4 + aaa[5]*freq5)
        air_temp_k = (air_temp_k +
                      (bbb[0] + bbb[1]*freq + bbb[2]*freq2
                       + bbb[3]*freq3 + bbb[4]*freq4 + bbb[5]*freq5)
                      * float(tmp_c))

        return air_temp_k

    def zenith_opacity(self, coeffs, freq_ghz):
        """Interpolate low and high opacities across a vector of frequencies

        Keywords:
        coeffs -- (list) opacitiy coefficients from archived text file,
                    produced by GBT weather prediction code
        freq_ghz -- frequency value in GHz

        Returns:
        A zenith opacity at requested frequency.

        """
        # interpolate between the coefficients based on time for a
        # given frequency
        def _interpolated_zenith_opacity(freq):
            # for frequencies < 2 GHz, return a default zenith opacity
            if np.array(freq).mean() < 2:
                result = np.ones(np.array(freq).shape)*self.UNDER_2GHZ_TAU_0
                return result
            result = 0
            for idx, term in enumerate(coeffs):
                if idx > 0:
                    result = result + term*freq**idx
                else:
                    result = term
            return result

        zenith_opacity = _interpolated_zenith_opacity(freq_ghz)
        return zenith_opacity

    def tsys(self, tcal, cal_on, cal_off):
        nchan = len(cal_off)
        low = int(.1*nchan)
        high = int(.9*nchan)
        cal_off = (cal_off[low:high]).mean()
        cal_on = (cal_on[low:high]).mean()
        return np.float(tcal*(cal_off/(cal_on-cal_off))+tcal/2)

    def antenna_temp(self, tsys, sig, ref, t_sig, t_ref):
        ref_smoothed = smoothing.boxcar(ref, self.SMOOTHING_WINDOW)
        ref_smoothed = self.pu.masked_array(ref_smoothed)

        spectrum = tsys * ((sig-ref_smoothed)/ref_smoothed)
        exposure_time = (t_sig * t_ref * self.SMOOTHING_WINDOW
                         / (t_sig + t_ref*self.SMOOTHING_WINDOW))
        return spectrum, exposure_time

    def _ta_fs_one_state(self, sigref_state, sigid, refid):

        sig = sigref_state[sigid]['TP']

        ref = sigref_state[refid]['TP']
        ref_cal_on = sigref_state[refid]['cal_on']
        ref_cal_off = sigref_state[refid]['cal_off']

        tcal = ref_cal_off['TCAL']

        tsys = self.tsys(tcal,  ref_cal_on['DATA'],  ref_cal_off['DATA'])

        a_temp_params = {'tsys': tsys, 'sig': sig, 'ref': ref,
                         't_sig': sigref_state[sigid]['EXPOSURE'],
                         't_ref': sigref_state[refid]['EXPOSURE']}
        antenna_temp, exposure = self.antenna_temp(**a_temp_params)

        return antenna_temp, tsys, exposure

    def ta_fs(self, sigref_state):

        ta0, tsys0, exposure0 = self._ta_fs_one_state(sigref_state, 0, 1)
        ta1, tsys1, exposure1 = self._ta_fs_one_state(sigref_state, 1, 0)

        # shift in frequency
        sig_centerfreq = sigref_state[0]['cal_off']['OBSFREQ']
        ref_centerfreq = sigref_state[1]['cal_off']['OBSFREQ']

        sig_delta = sigref_state[0]['cal_off']['CDELT1']
        channel_shift = -((sig_centerfreq-ref_centerfreq)/sig_delta)

        # do integer channel shift to second spectrum
        ta1_ishifted = np.roll(ta1, int(channel_shift))
        if channel_shift > 0:
            ta1_ishifted[:channel_shift] = float('nan')
        elif channel_shift < 0:
            ta1_ishifted[channel_shift:] = float('nan')

        # do fractional channel shift
        fractional_shift = channel_shift - int(channel_shift)
        #doMessage(logger, msg.DBG, 'Fractional channel shift is',
        #          fractional_shift)
        xxp = range(len(ta1_ishifted))
        yyp = ta1_ishifted
        xxx = xxp-fractional_shift

        yyy = np.interp(xxx, xxp, yyp)
        ta1_shifted = self.pu.masked_array(yyy)

        exposures = np.array([exposure0, exposure1])
        tsyss = np.array([tsys0, tsys1])
        tas = [ta0, ta1_shifted]

        # average shifted spectra
        ta = self.average_spectra(tas, tsyss, exposures)

        # average tsys
        tsys = self.average_tsys(tsyss, exposures)

        # only sum the exposure if frequency switch is "in band" (i.e.
        # overlapping channels); otherwise use the exposure from the
        # first state only
        if abs(channel_shift) < len(ta1):
            exposure_sum = exposure0 + exposure1
        else:
            exposure_sum = exposure0

        return ta, tsys, exposure_sum

    def ta_star(self, antenna_temp, beam_scaling, opacity, spillover):
        # opacity is corrected for elevation
        return antenna_temp*((beam_scaling*(math.e**opacity))/spillover)

    def jansky(self, ta_star, aperture_efficiency):
        return ta_star/(2.85*aperture_efficiency)

    def interpolate_by_time(self, reference1, reference2,
                            first_ref_timestamp, second_ref_timestamp,
                            integration_timestamp):

        time_btwn_ref_scans = second_ref_timestamp-first_ref_timestamp
        aa1 = ((second_ref_timestamp-integration_timestamp)
               / time_btwn_ref_scans)
        aa2 = (integration_timestamp-first_ref_timestamp) / time_btwn_ref_scans
        return aa1*reference1 + aa2*reference2

    def make_weights(self, tsyss, exposures):
        return exposures / tsyss**2

    def average_tsys(self, tsyss, exposures):
        weights = self.make_weights(tsyss, exposures)
        return np.sqrt(np.average(tsyss**2, axis=0, weights=weights))

    def average_spectra(self, specs, tsyss, exposures):
        weights = self.make_weights(tsyss, exposures)
        if float('nan') in specs[0] or float('nan') in specs[1]:
            weight0 = np.ma.array([weights[0]]*len(specs[0]),
                                  mask=specs[0].mask)
            weight1 = np.ma.array([weights[1]]*len(specs[1]),
                                  mask=specs[1].mask)
            weights = [weight0.filled(0), weight1.filled(0)]
        return np.ma.average(specs, axis=0, weights=weights)

    def getReferenceAverage(self, crefs, tsyss, exposures, timestamps,
                            tambients, elevations):

        # convert to numpy arrays
        crefs = np.array(crefs)
        tsyss = np.array(tsyss)
        exposures = np.array(exposures)
        timestamps = np.array(timestamps)
        tambients = np.array(tambients)
        elevations = np.array(elevations)

        avg_tsys = self.average_tsys(tsyss, exposures)

        avg_tsys80 = avg_tsys.mean(0)  # single value for mid 80% of band
        avg_cref = self.average_spectra(crefs, tsyss, exposures)
        exposure = np.sum(exposures)

        avg_timestamp = timestamps.mean()
        avg_tambient = tambients.mean()
        avg_elevation = elevations.mean()

        return (avg_cref, avg_tsys80, avg_timestamp, avg_tambient,
                avg_elevation, exposure)

    def tsky(self, ambient_temp_k, freq_hz, tau):
        """Determine the sky temperature contribution at a frequency

        Keywords:
        ambient_temp_k -- (float) mean ambient temperature value, in kelvin
        freq -- (float)
        tau -- (float) opacity value
        Returns:
        the sky model temperature contribution at frequncy channel

        """
        ambient_temp_c = ambient_temp_k-273.15  # convert to celcius
        airTemp = self._tatm(freq_hz, ambient_temp_c)

        tsky = airTemp * (1-math.e**(-tau))

        return tsky
Пример #16
0
class SdFits:
    """Class contains methods to read and write to the GBT SdFits format.

    This includes code for both the FITS files and associated index files.

    A description (but not a definition) of the SD FITS is here:
    https://safe.nrao.edu/wiki/bin/view/Main/SdfitsDetails

    """

    def __init__(self):

        self.pu = Pipeutils()

    def find_maps(self, indexfile, debug=False):
        """Find mapping blocks. Also find samplers used in each map

        Args:
            indexfile: input required to search for maps and samplers
            debug: optional debug flag

        Returns:
        a (list) of map blocks, with each entry a (tuple) of the form:
        (int) reference 1,
        (list of ints) mapscans,
        (int) reference 2

        """

        map_scans = {}
        observation, summary = self.parseSdfitsIndex(indexfile)
        feed = observation.feeds()[0]
        window = observation.windows()[0]
        pol = observation.pols()[0]

        # print results
        if debug:
            print '------------------------- All scans'
            for scanid in sorted(observation.scans()):
                scanstruct = observation.get(scanid, feed, window, pol)
                print('scan \'{0}\' obsid \'{1}\' procname \'{2}\' procscan \'{3}\''.format(scanid,
                                                                                            scanstruct['OBSID'],
                                                                                            scanstruct['PROCNAME'],
                                                                                            scanstruct['PROCSCAN']))

        for scanid in observation.scans():
            scanstruct = observation.get(scanid, feed, window, pol)
            obsid = scanstruct['OBSID'].upper()
            procname = scanstruct['PROCNAME'].upper()
            procscan = scanstruct['PROCSCAN'].upper()

            # keyword check should depend on presence of PROCSCAN key, which is an
            # alternative to checking SDFITVER.
            # OBSID is the old way, PROCSCAN is the new way MR8Q312

            # create a new list that only has 'MAP' and 'OFF' scans
            if not procscan and (obsid == 'MAP' or obsid == 'OFF'):
                map_scans[scanid] = obsid
            elif (procscan == 'MAP' or
                  procname == 'TRACK' or
                  (procname == 'ONOFF' and procscan == 'OFF') or
                  (procname == 'OFFON' and procscan == 'OFF')):
                map_scans[scanid] = procscan

        mapkeys = map_scans.keys()
        mapkeys.sort()

        if debug:
            print '------------------------- Relavant scans'
            for scanid in mapkeys:
                print 'scan', scanid, map_scans[scanid]

        maps = []  # final list of maps
        ref1 = None
        ref2 = None
        prev_ref2 = None
        mapscans = []  # temporary list of map scans for a single map

        if debug:
            print 'mapkeys', mapkeys

        MapParams = namedtuple("MapParams", "refscan1 mapscans refscan2")
        for idx, scan in enumerate(mapkeys):

            # look for the reference scans
            if (map_scans[scan]).upper() == 'OFF' or (map_scans[scan]).upper() == 'ON':
                # if there is no ref1 or this is another ref1
                if not ref1 or (ref1 and bool(mapscans) == False):
                    ref1 = scan
                else:
                    ref2 = scan
                    prev_ref2 = ref2

            elif (map_scans[scan]).upper() == 'MAP':
                if not ref1 and prev_ref2:
                    ref1 = prev_ref2

                mapscans.append(scan)

            # see if this scan is the last one in the relevant scan list
            # or see if we have a ref2
            # if so, close out
            if ref2 or idx == len(mapkeys)-1:
                maps.append(MapParams(ref1, mapscans, ref2))
                ref1 = False
                ref2 = False
                mapscans = []

        if debug:
            import pprint
            pprint.pprint(maps)

            for idx, mm in enumerate(maps):
                print "Map", idx
                if mm.refscan2:
                    print "\tReference scans.....", mm.refscan1, mm.refscan2
                else:
                    print "\tReference scan......", mm.refscan1
                print "\tMap scans...........", mm.mapscans

        return maps

    def parseSdfitsIndex(self, infile, mapscans=[]):

        try:
            ifile = open(infile)
        except IOError:
            print("ERROR: Could not open file: {0}\n"
                  "Please check and try again.".format(infile))
            raise

        observation = ObservationRows()

        while True:
            line = ifile.readline()
            # look for start of row data or EOF (i.e. not line)
            if '[rows]' in line or not line:
                break

        lookup_table = {}
        header = ifile.readline()

        fields = [xx.lstrip() for xx in re.findall(r' *\S+', header)]

        iterator = re.finditer(r' *\S+', header)
        for idx, mm in enumerate(iterator):
            lookup_table[fields[idx]] = slice(mm.start(), mm.end())

        rr = SdFitsIndexRowReader(lookup_table)

        summary = {'WINDOWS': set([]), 'FEEDS': set([])}

        # keep a list of suspect scans so we can know if the
        # user has already been warned
        suspectScans = set()

        for row in ifile:

            rr.setrow(row)

            scanid = int(rr['SCAN'])

            # have a look at the procedure
            #  if it is "Unknown", the data is suspect, so skip it
            procname = rr['PROCEDURE']
            if scanid in suspectScans:
                continue

            if ((scanid not in suspectScans) and procname.lower() == 'unknown'):

                suspectScans.add(scanid)
                if scanid in mapscans:
                    print 'WARNING: scan', scanid, 'has "Unknown" procedure. Skipping.'
                continue

            feed = int(rr['FDNUM'])
            windowNum = int(rr['IFNUM'])
            pol = int(rr['PLNUM'])
            fitsExtension = int(rr['EXT'])
            rowOfFitsFile = int(rr['ROW'])
            obsid = rr['OBSID']
            procscan = rr['PROCSCAN']
            nchans = rr['NUMCHN']

            summary['WINDOWS'].add((windowNum, float(rr['RESTFREQ'])/1e9))
            summary['FEEDS'].add(rr['FDNUM'])

            # we can assume all integrations of a single scan are within the same
            #   FITS extension
            observation.addRow(scanid, feed, windowNum, pol,
                               fitsExtension, rowOfFitsFile, obsid,
                               procname, procscan, nchans)

        try:
            ifile.close()
        except NameError:
            raise

        return observation, summary

    def getReferenceIntegration(self, cal_on, cal_off, scale):

        cal = Calibration()
        cal_ondata = cal_on['DATA']
        cal_offdata = cal_off['DATA']
        cref, exposure = cal.total_power(cal_ondata, cal_offdata, cal_on['EXPOSURE'], cal_off['EXPOSURE'])

        tcal = cal_off['TCAL'] * scale
        tsys = cal.tsys(tcal, cal_ondata, cal_offdata)

        dateobs = cal_off['DATE-OBS']
        timestamp = self.pu.dateToMjd(dateobs)

        tambient = cal_off['TAMBIENT']
        elevation = cal_off['ELEVATIO']

        return cref, tsys, exposure, timestamp, tambient, elevation

    def nameIndexFile(self, pathname):
        # -------------------------------------------------  name index file
        if not os.path.exists(pathname):
            print ('ERROR: Path does not exist {0}.\n'
                   '       Please check and try again'.format(pathname))
            sys.exit(9)

        if os.path.isdir(pathname):
            bn = os.path.basename(pathname.rstrip('/'))
            return '{0}/{1}.index'.format(pathname, bn)

        elif os.path.isfile(pathname) and pathname.endswith('.fits'):
            return os.path.splitext(pathname)[0]+'.index'

        else:
            # doMessage(logger,msg.ERR,'input file not recognized as a fits file.',\
            #  ' Please check the file extension and change to \'fits\' if necessary.')
            print 'ERROR: Input file does not end with .fits:', pathname
            sys.exit(9)
Пример #17
0
 def __init__(self):
     
     self.pu = Pipeutils()
Пример #18
0
class SdFits:
    """Class contains methods to read and write to the GBT SdFits format.
    
    This includes code for both the FITS files and associated index files.
    
    A description (but not a definition) of the SD FITS is here:
    https://safe.nrao.edu/wiki/bin/view/Main/SdfitsDetails
    
    """
    
    def __init__(self):
        
        self.pu = Pipeutils()
        
    def find_maps(self, indexfile, debug=False):
        """Find mapping blocks. Also find samplers used in each map
    
        Keywords:
        indexfile -- input required to search for maps and samplers
        debug -- optional debug flag
    
        Returns:
        a (list) of map blocks, with each entry a (tuple) of the form:
        (int) reference 1,
        (list of ints) mapscans,
        (int) reference 2
        
        """
    
        map_scans = {}
        observation = self.parseSdfitsIndex(indexfile)
        feed = observation.feeds()[0]
        window = observation.windows()[0]
        pol = observation.pols()[0]
 
        # print results
        if debug:
            print '------------------------- All scans'
            for scanid in observation.scans():
                scanstruct = observation.get(scanid, feed, window, pol)
                print 'scan', scanid, scanstruct['OBSID']
    
            print '------------------------- Relavant scans'
   
        for scanid in observation.scans():
            scanstruct = observation.get(scanid, feed, window, pol)
            obsid = scanstruct['OBSID'].upper()
            procscan = scanstruct['PROCSCAN'].upper()

            # keyword check should depend on presence of PROCSCAN key, which is an
            # alternative to checking SDFITVER.
            # OBSID is the old way, PROCSCAN is the new way MR8Q312
            # only do the following for an "OLD" SDFITS version

            # create a new list that only has 'MAP' and 'OFF' scans
            if not procscan and (obsid=='MAP' or obsid=='OFF'):
                    map_scans[scanid] = obsid
            elif procscan=='MAP' or procscan=='OFF':
                    map_scans[scanid] = procscan
     
        mapkeys = map_scans.keys()
        mapkeys.sort()
    
        if debug:
            for scanid in mapkeys:
                print 'scan', scanid, map_scans[scanid]
    
        maps = [] # final list of maps
        ref1 = None
        ref2 = None
        prev_ref2 = None
        mapscans = [] # temporary list of map scans for a single map
    
        if debug:
            print 'mapkeys', mapkeys
    
        MapParams = namedtuple("MapParams", "refscan1 mapscans refscan2")
        for idx,scan in enumerate(mapkeys):
    
            # look for the offs
            if (map_scans[scan]).upper()=='OFF':
                # if there is no ref1 or this is another ref1
                if not ref1 or (ref1 and bool(mapscans)==False):
                    ref1 = scan
                else:
                    ref2 = scan
                    prev_ref2 = ref2
    
            elif (map_scans[scan]).upper()=='MAP':
                if not ref1 and prev_ref2:
                    ref1 = prev_ref2
            
                mapscans.append(scan)
    
            # see if this scan is the last one in the relevant scan list
            # or see if we have a ref2
            # if so, close out
            if ref2 or idx==len(mapkeys)-1:
                maps.append(MapParams(ref1,mapscans,ref2))
                ref1 = False
                ref2 = False
                mapscans = []
                
        if debug:
            import pprint; pprint.pprint(maps)
    
            for idx,mm in enumerate(maps):
                print "Map", idx
                if mm.refscan2:
                    print "\tReference scans.....", mm.refscan1, mm.refscan2
                else:
                    print "\tReference scan......", mm.refscan1
                print "\tMap scans...........", mm.mapscans
    
        return maps
    
    def parseSdfitsIndex(self, infile):
        
        try:
            ifile = open(infile)
        except IOError:
            print "ERROR: Could not open file.  Please check and try again."
            raise
        
        observation = ObservationRows()
        
        while True:
            line = ifile.readline()
            # look for start of row data or EOF (i.e. not line)
            if '[rows]' in line or not line:
                break
    
        lookup_table = {}
        header = ifile.readline()
        
        fields = [xx.lstrip() for xx in re.findall(r' *\S+',header)]
        
        iterator = re.finditer(r' *\S+',header)
        for idx,mm in enumerate(iterator):
            lookup_table[fields[idx]] = slice(mm.start(),mm.end())
       
        rr = SdFitsIndexRowReader(lookup_table)

        for row in ifile:
        
            rr.setrow(row)
            scanid = int(rr['SCAN'])
            feed = int(rr['FDNUM'])
            windowNum = int(rr['IFNUM'])
            pol = int(rr['PLNUM'])
            fitsExtension = int(rr['EXT'])
            rowOfFitsFile = int(rr['ROW'])
            typeOfScan = rr['PROCEDURE']
            obsid = rr['OBSID']
            procscan = rr['PROCSCAN']
            
            # we can assume all integrations of a single scan are within the same
            #   FITS extension
            observation.addRow(scanid, feed, windowNum, pol,
                               fitsExtension, rowOfFitsFile, typeOfScan, obsid, procscan)
            
        try:
            ifile.close()
        except NameError:
            raise
        
        return observation

    def getReferenceIntegration(self, cal_on, cal_off):
        
        cal = Calibration()
        cal_ondata = cal_on['DATA']
        cal_offdata = cal_off['DATA']
        cref, exposure = cal.total_power(cal_ondata, cal_offdata, cal_on['EXPOSURE'], cal_off['EXPOSURE'])

        tcal = cal_off['TCAL']
        tsys = cal.tsys( tcal, cal_ondata, cal_offdata )
        
        dateobs = cal_off['DATE-OBS']
        timestamp = self.pu.dateToMjd(dateobs)
            
        tambient = cal_off['TAMBIENT']
        elevation = cal_off['ELEVATIO']
        
        return cref, tsys, exposure, timestamp, tambient, elevation

    def nameIndexFile(self, fitsfile):
        # -------------------------------------------------  name index file
        
        if fitsfile.endswith('.fits'):
            return os.path.splitext(fitsfile)[0]+'.index'
        
        else:
            #doMessage(logger,msg.ERR,'input file not recognized as a fits file.',\
            #  ' Please check the file extension and change to \'fits\' if necessary.')
            print 'ERROR: Input file does not end with .fits:', fitsfile
            sys.exit(9)
Пример #19
0
class Calibration(object):

    def __init__(self, smoothing_window_size=0):

        # set calibration constants
        self.BB = .0132  # Ruze equation parameter
        self.UNDER_2GHZ_TAU_0 = 0.008
        self.SMOOTHING_WINDOW = smoothing_window_size
        self.pu = Pipeutils()

    # ------------- Unit methods: do not depend on any other pipeline methods

    def total_power(self, cal_on, cal_off, t_on, t_off):
        r"""Calculate the total power of spectrum with noise diode-switching.

        Args:
            cal_on(1d array): Spectrum *with* noise diode applied.
            cal_off(1d array): Spectrum *without* noise diode applied.
            t_on(float): Exposure time of the spectrum *with* noise diode.
            t_off(float): Exposure time of the spectrum *without* noise diode.

        Returns:
            1d array and float:

            A spectrum and a total exposure time.
            The spectrum is the average of the input spectra.
            The exposure time is the sum of the input exposure times.

        """
        return np.ma.mean((cal_on, cal_off), axis=0), t_on + t_off

    def tsky_correction(self, tsky_sig, tsky_ref, spillover):
        r"""Correction factor for sky brightness variation between reference and current integration.

        Args:
            tsky_sig(float): Sky brightness at current temperature, \
                frequency and elevation.
            tsky_ref(float): Sky brightness at reference temperature, \
                frequency and elevation.
            spillover(float): Spillover factor.

        Returns:
            float:
            A sky brightness correction factor.

            .. math::

               spillover * (tsky\_{sig} - tsky\_{ref})

        """
        return spillover * (tsky_sig - tsky_ref)

    def aperture_efficiency(self, reference_eta_a, freq_hz):
        r"""Determine telescope aperture efficiency at a given frequency.

        EtaA model is from memo by Jim Condon, provided by Ron Maddalena.

        Args:
            reference_eta_a(float): Reference aperture efficiency.
            freq_hz(float): Frequency in Hertz.

        Returns:
            float:
            Point or main beam efficiency (ranges from 0 to 1).

        .. testsetup::

           from Calibration import Calibration

        .. doctest:: :hide:

           >>> cal = Calibration()
           >>> print '{0:.6f}'.format(cal.aperture_efficiency(.71, 23e9))
           0.647483
           >>> print '{0:.6f}'.format(cal.aperture_efficiency(.91, 23e9))
           0.829872

        """
        freq_ghz = float(freq_hz)/1e9
        return reference_eta_a * math.e**-((self.BB * freq_ghz)**2)

    def main_beam_efficiency(self, reference_eta_b, freq_hz):
        r"""Determine main beam efficiency, given a reference etaB value and frequency.

        This is the same equation as is used to determine aperture efficiency.
        The only difference is the reference value.

        Args:
            reference_eta_b(float): The main beam efficiency. \
             For the GBT, the default is :math:`1.28 * \eta_A`, where :math:`\eta_A` is aperture efficiency.
            freq_hz(float): The frequency in Hertz.

        Returns:
            float:
            An aperture efficiency at a given frequency.
        """
        return self.aperture_efficiency(reference_eta_b, freq_hz)

    def elevation_adjusted_opacity(self, zenith_opacity, elev):
        r"""Compute elevation-corrected opacities.

        We need to estimate the number of atmospheres along the
        line of site at an input elevation

        This comes from a model reported by Ron Maddalena:

        :math:`A = \frac{1}{\sin(elev)}` is a good approximation down to about 15 deg but
        starts to get pretty poor below that.  Here's a quick-to-calculate,
        better approximation that I determined from multiple years worth of
        weather data and which is good down to elev = 1 deg:

        .. math::

           A = -0.023437  + \frac{1.0140}{\sin( \frac{pi}{180} * (elev + \frac{5.1774}{elev + 3.3543} )}

        Args:
            zenith_opacity(float): Opacity at zenith based only on time.
            elev(float): Elevation angle of integration or scan.

        Returns:
            float:
            Elevation-adjusted opacity

        .. testsetup::

           from Calibration import Calibration

        .. doctest:: :hide:

           >>> cal = Calibration()
           >>> print ['{0:.6f}'.format(cal.elevation_adjusted_opacity(1, el)) for el in range(90)]
           ['37.621216', '26.523488', '19.566942', '15.217485', '12.341207', '10.331365', '8.861127', '7.745094', '6.872195', '6.172545', '5.600276', '5.124171', '4.722318', '4.378917', '4.082311', '3.823718', '3.596410', '3.395144', '3.215779', '3.055004', '2.910137', '2.778989', '2.659751', '2.550918', '2.451229', '2.359617', '2.275175', '2.197126', '2.124803', '2.057628', '1.995099', '1.936775', '1.882273', '1.831253', '1.783416', '1.738495', '1.696253', '1.656478', '1.618982', '1.583595', '1.550162', '1.518545', '1.488619', '1.460271', '1.433397', '1.407903', '1.383703', '1.360719', '1.338878', '1.318115', '1.298369', '1.279585', '1.261710', '1.244698', '1.228504', '1.213089', '1.198415', '1.184446', '1.171152', '1.158501', '1.146467', '1.135024', '1.124146', '1.113814', '1.104005', '1.094700', '1.085882', '1.077533', '1.069639', '1.062184', '1.055156', '1.048543', '1.042331', '1.036512', '1.031074', '1.026009', '1.021309', '1.016966', '1.012972', '1.009322', '1.006009', '1.003029', '1.000376', '0.998047', '0.996038', '0.994346', '0.992968', '0.991902', '0.991147', '0.990701']

        """
        deg2rad = (math.pi/180)  # factor to convert degrees to radians
        num_atmospheres = -0.023437 + 1.0140 / math.sin(deg2rad * (elev + 5.1774 / (elev + 3.3543)))
        corrected_opacity = zenith_opacity * num_atmospheres

        return corrected_opacity

    def _tatm(self, freq_hz, tmp_c):
        """Estimates the atmospheric effective temperature.

        Keyword arguments:
        freq_hz -- input frequency in Hz
        where: tmp_c -- input ground temperature in Celsius

        Returns:
        air_temp_k -- output Air Temperature in Kelvin

        Based on local ground temperature measurements.  These estimates
        come from a model reported by Ron Maddalena

        Using Tatm = 270 is too rough an approximation since Tatm can vary
        from 244 to 290, depending upon the weather conditions and observing
        frequency.  One can derive an approximation for the default Tatm that
        is accurate to about 3.5 K from the equation:

        TATM = (A0 + A1*FREQ + A2*FREQ^2 +A3*FREQ^3 + A4*FREQ^4 + A5*FREQ^5)
                    + (B0 + B1*FREQ + B2*FREQ^2 + B3*FREQ^3 + B4*FREQ^4 +
        B5*FREQ^5)*TMPC

        where TMPC = ground-level air temperature in C and Freq is in GHz.  The
        A and B coefficients are:

                                    A0=    259.69185966 +/- 0.117749542
                                    A1=     -1.66599001 +/- 0.0313805607
                                    A2=     0.226962192 +/- 0.00289457549
                                    A3=   -0.0100909636 +/- 0.00011905765
                                    A4=   0.00018402955 +/- 0.00000223708
                                    A5=  -0.00000119516 +/- 0.00000001564
                                    B0=      0.42557717 +/- 0.0078863791
                                    B1=     0.033932476 +/- 0.00210078949
                                    B2=    0.0002579834 +/- 0.00019368682
                                    B3=  -0.00006539032 +/- 0.00000796362
                                    B4=   0.00000157104 +/- 0.00000014959
                                    B5=  -0.00000001182 +/- 0.00000000105


        tatm model is provided by Ron Maddalena

        >>> print '{0:.6f}'.format(Calibration()._tatm(23e9, 40))
        298.885174
        >>> print '{0:.6f}'.format(Calibration()._tatm(23e9, 30))
        289.780603
        >>> print '{0:.6f}'.format(Calibration()._tatm(1.42e9, 30))
        271.978666

        """

        # where TMPC = ground-level air temperature in C and Freq is in GHz.
        # The A and B coefficients are:
        aaa = [259.69185966, -1.66599001, 0.226962192, -0.0100909636,  0.00018402955, -0.00000119516]
        bbb = [0.42557717, 0.033932476, 0.0002579834, -0.00006539032, 0.00000157104, -0.00000001182]
        freq_ghz = float(freq_hz)/1e9

        air_temp_k_A = air_temp_k_B = 0
        for idx, term in enumerate(zip(aaa, bbb)):
            if idx > 0:
                air_temp_k_A = air_temp_k_A + term[0] * (freq_ghz**idx)
                air_temp_k_B = air_temp_k_B + term[1] * (freq_ghz**idx)
            else:
                air_temp_k_A = term[0]
                air_temp_k_B = term[1]

        air_temp_k = air_temp_k_A + (air_temp_k_B * float(tmp_c))
        return air_temp_k

    def zenith_opacity(self, coeffs, freq_ghz):
        r"""Interpolate low and high opacities across a vector of frequencies.

        Args:
            coeffs(1d array): Opacitiy coefficients from archived text file, \
                produced by GBT weather prediction code.
            freq_ghz(float): Frequency value in GHz.

        Returns:
            float:
            A zenith opacity at requested frequency.

        """
        # interpolate between the coefficients based on time for a
        # given frequency
        def _interpolated_zenith_opacity(freq):
            # for frequencies < 2 GHz, return a default zenith opacity
            if np.array(freq).mean() < 2:
                result = np.ones(np.array(freq).shape)*self.UNDER_2GHZ_TAU_0
                return result
            result = 0
            for idx, term in enumerate(coeffs):
                if idx > 0:
                    result = result + term*freq**idx
                else:
                    result = term
            return result

        zenith_opacity = _interpolated_zenith_opacity(freq_ghz)
        return zenith_opacity

    def tsys(self, tcal, cal_on, cal_off):
        r"""Calculate the system temperature for an integration.

        Args:
            tcal(float): Lab-measured receiver calibration temperature.
            cal_on(1d array): Spectrum *with* noise diode applied.
            cal_off(1d array): Spectrum *without* noise diode applied.

        Returns:
            float:
            .. math::
               tcal * \frac{cal\_{off}}{cal\_{on} - cal\_{off}} + \frac{tcal}{2}

        """
        nchan = len(cal_off)
        low = int(.1 * nchan)
        high = int(.9 * nchan)
        cal_off = (cal_off[low:high]).mean()
        cal_on = (cal_on[low:high]).mean()
        return np.float(tcal * (cal_off / (cal_on - cal_off)) + tcal / 2)

    def antenna_temp(self, tsys, sig, ref, t_sig, t_ref):
        r"""Calibrate a spectrum to units of antenna temperature.

        Args:
            tsys(float): System temperature of the reference scan.
            sig(1d array): Signal ("on") spectrum.
            ref(1d array): Reference ("off") spectrum.
            t_sig(float): Exposure time of the signal spectrum.
            t_ref(float): Exposure time of the reference spectrum.

        Returns:
            1d array or float:
            A calibrated spectrum with an exposure time.
            The spectrum is

            .. math:: tsys * \frac{sig - ref}{ref}.

            The exposure time is

            .. math::
               \frac{t\_{sig} * t\_{ref} * window\_{size}}{t\_{sig} + (t\_{ref} * window\_{size})}

            where the window size is an optional smoothing kernel size for the
            reference spectrum.

        """
        if self.SMOOTHING_WINDOW > 1:
            ref = smoothing.boxcar(ref, self.SMOOTHING_WINDOW)
            window_size = self.SMOOTHING_WINDOW
        else:
            window_size = 1

        ref = self.pu.masked_array(ref)

        spectrum = tsys * ((sig-ref)/ref)
        exposure_time = (t_sig * t_ref * window_size / (t_sig + t_ref*window_size))
        return spectrum, exposure_time

    def _ta_fs_one_state(self, sigref_state, sigid, refid, scale):

        sig = sigref_state[sigid]['TP']

        ref = sigref_state[refid]['TP']
        ref_cal_on = sigref_state[refid]['cal_on']
        ref_cal_off = sigref_state[refid]['cal_off']

        tcal = ref_cal_off['TCAL'] * scale

        tsys = self.tsys(tcal,  ref_cal_on['DATA'],  ref_cal_off['DATA'])

        a_temp_params = {'tsys': tsys, 'sig': sig, 'ref': ref,
                         't_sig': sigref_state[sigid]['EXPOSURE'],
                         't_ref': sigref_state[refid]['EXPOSURE']}
        antenna_temp, exposure = self.antenna_temp(**a_temp_params)

        return antenna_temp, tsys, exposure

    def ta_fs(self, sigref_state, scale):
        r"""Calibrate a frequency-switched integration to units of antenna temperature.

        Args:
            sigref_state(struct): A structure holding the noise diode off and on \
                integrations (which are full rows from the FITS table, including the DATA column), \
                a total power integration, FITS table row number and \
                exposure time.
            scale(float): A relative beam scaling factor.  Default is 1, or no scaling.

        Returns:
            1d array, float, float:
            An averaged spectrum calibrated to units of antenna temperature, \
            a system temperature and a total exposure time for the spectrum.

        """

        ta0, tsys0, exposure0 = self._ta_fs_one_state(sigref_state, 0, 1, scale)
        ta1, tsys1, exposure1 = self._ta_fs_one_state(sigref_state, 1, 0, scale)

        # shift in frequency
        sig_centerfreq = sigref_state[0]['cal_off']['OBSFREQ']
        ref_centerfreq = sigref_state[1]['cal_off']['OBSFREQ']

        sig_delta = sigref_state[0]['cal_off']['CDELT1']
        channel_shift = -((sig_centerfreq-ref_centerfreq)/sig_delta)

        # do integer channel shift to second spectrum
        ta1_ishifted = np.roll(ta1, int(channel_shift))
        if channel_shift > 0:
            ta1_ishifted[:channel_shift] = float('nan')
        elif channel_shift < 0:
            ta1_ishifted[channel_shift:] = float('nan')

        # do fractional channel shift
        fractional_shift = channel_shift - int(channel_shift)
        # doMessage(logger, msg.DBG, 'Fractional channel shift is',
        #          fractional_shift)
        xxp = range(len(ta1_ishifted))
        yyp = ta1_ishifted
        xxx = xxp-fractional_shift

        yyy = np.interp(xxx, xxp, yyp)
        ta1_shifted = self.pu.masked_array(yyy)

        exposures = np.array([exposure0, exposure1])
        tsyss = np.array([tsys0, tsys1])
        tas = [ta0, ta1_shifted]

        # average shifted spectra
        ta = self.average_spectra(tas, tsyss, exposures)

        # average tsys
        tsys = self.average_tsys(tsyss, exposures)

        # only sum the exposure if frequency switch is "in band" (i.e.
        # overlapping channels); otherwise use the exposure from the
        # first state only
        if abs(channel_shift) < len(ta1):
            exposure_sum = exposure0 + exposure1
        else:
            exposure_sum = exposure0

        return ta, tsys, exposure_sum

    def ta_star(self, antenna_temp, opacity, spillover):
        r"""Calibrate a spectrum to units of **ta***.

        Args:
            antenna_temp(1d array): Spectrum calibrated to units of antenna temperature.
            opacity(float): Elevation-adjusted atmospheric opacity.
            spillover(float): Correction factor for rear-spillover,	ohmic loss and blockage	efficiency.

        Returns:
            1d array:
            A calibrated spectrum.

            .. math::
               antenna\_{temp} * e^{opacity} * \frac{1}{spillover}

        """
        # opacity is corrected for elevation
        return antenna_temp * math.e**opacity * 1 / spillover

    def jansky(self, spectrum, aperture_efficiency):
        r"""Calibrate a spectrum to units of **Jansky**.

        Args:
            spectrum(1d array): A spectrum previously calibrated to **ta***.
            aperture_efficiency(float): The aperture efficiency factor.

        Returns:
            1d array:

            .. math::
               \frac{spectrum}{2.85 * aperture\_{efficiency}}

        """
        return spectrum / (2.85 * aperture_efficiency)

    def interpolate_by_time(self, reference1, reference2,
                            first_ref_timestamp, second_ref_timestamp,
                            integration_timestamp):
        r"""Calculate interpolated value(s).

        This function can be used to calculate a single interpolated value
        or an array of values at a specified time.

        Args:
            reference1(float or 1d array): Value(s) for first time.
            reference2(float or 1d array): Value(s) for second time.
            first_ref_timestamp(float): First time.
            second_ref_timestamp(float): Second time.
            integration_timestamp(float): The time for which we want a value.

        Returns:
            float or 1d array:
            Interpolated value(s) for a specific time.

        .. testsetup::

           from Calibration import Calibration
           import numpy as np

        .. doctest:: :hide:

           >>> cal = Calibration()
           >>> cal.interpolate_by_time(1, 2, 0, 100, 75)
           1.75
           >>> cal.interpolate_by_time(np.array([1, 2]), np.array([2, 3]), 0, 100, 75)
           array([ 1.75,  2.75])

        """

        time_btwn_ref_scans = float(second_ref_timestamp) - float(first_ref_timestamp)
        aa1 = (second_ref_timestamp - integration_timestamp) / time_btwn_ref_scans
        aa2 = (integration_timestamp - first_ref_timestamp) / time_btwn_ref_scans
        return aa1 * reference1 + aa2 * reference2

    def make_weights(self, tsyss, exposures):
        r"""Create weights for integration averaging.

        Args:
            tsyss(1d array): A list of system temperatures.
            exposures(1d array): A list of exposure times.  \
            The number of exposure times must match the number of system temperatures.

        Returns:
            1d array:
            A list of weights.  The weights are computed with the following formula.

            .. math::

               \frac{exposure\ time}{tsys^2}

        """
        return exposures / tsyss**2

    def average_tsys(self, tsyss, exposures):
        r"""Compute a weighted average multiple system temperatures.

        Args:
            tsyss(1d array): The system temperatures to average.
            exposures(1d array): The exposure times corresponding to each system temperature.

        Returns:
            1d array:
            A weighted average system temperature.  See the *make_weights* method to see how
            the weights are computed.

        """
        weights = self.make_weights(tsyss, exposures)
        return np.sqrt(np.average(tsyss**2, axis=0, weights=weights))

    def average_spectra(self, specs, tsyss, exposures):
        r"""Perform a weighted average of two spectra.

        Args:
            specs(two 1d arrays): The two input spectra to be averaged.
            tsyss(two floats): System temperatures corresponding to each input spectrum.
            exposures(two floats): Exposure times corresponding to each input spectrum.

        Returns:
            1d array:
            A weighted average spectrum.  See the *make_weights* method to see how the weights
            are computed.

        """
        weights = self.make_weights(tsyss, exposures)

        if float('nan') in specs[0] or float('nan') in specs[1]:

            weight0 = np.ma.array([weights[0]] * len(specs[0]), mask=specs[0].mask)
            weight1 = np.ma.array([weights[1]] * len(specs[1]), mask=specs[1].mask)
            weights = [weight0.filled(0), weight1.filled(0)]

        return np.ma.average(specs, axis=0, weights=weights)

    def getReferenceAverage(self, crefs, tsyss, exposures, timestamps,
                            tambients, elevations):
        r"""Average the total power integrations from a reference scan.

        Args:
            crefs(stack of 1d arrays): The total power integrations (spectra) for a single reference scan.
            tsyss(1d array): The system temperatures; one for each input spectrum.
            exposures(1d array): The exposure times; one for each input spectrum.
            timestamps(1d array): The timestamps; one for each input spectrum.
            tambients(1d array): Ambient temperatures in Kelvin; one for each input spectrum.
            elevations(1d array): Elevation in degrees; one for each input spectrum.

        Returns:
            1d array, float, float, float, float, float:
            An average value for each of the input parameters.  An average spectrum along with
            average system temperature, exposure time, timestamp, ambient temperature and elevation.

        """

        # convert to numpy arrays
        crefs = np.array(crefs)
        tsyss = np.array(tsyss)
        exposures = np.array(exposures)
        timestamps = np.array(timestamps)
        tambients = np.array(tambients)
        elevations = np.array(elevations)

        avg_tsys = self.average_tsys(tsyss, exposures)

        avg_tsys80 = avg_tsys.mean(0)  # single value for mid 80% of band
        avg_cref = self.average_spectra(crefs, tsyss, exposures)
        exposure = np.sum(exposures)

        avg_timestamp = timestamps.mean()
        avg_tambient = tambients.mean()
        avg_elevation = elevations.mean()

        return avg_cref, avg_tsys80, avg_timestamp, avg_tambient, avg_elevation, exposure

    def tsky(self, ambient_temp_k, freq_hz, tau):
        r"""Determine the sky brightness temperature at a frequency.

        Args:
            ambient_temp_k(float): Mean ambient temperature in Kelvin.
            freq_hz(float): Frequency in Hz.
            tau(float): Atmospheric opacity value.

        Returns:
            float:
            The sky model temperature contribution at frequency channel.

        """
        ambient_temp_c = ambient_temp_k - 273.15  # convert to Celsius
        airTemp = self._tatm(freq_hz, ambient_temp_c)

        tsky = airTemp * (1 - math.e**(-tau))

        return tsky
Пример #20
0
class Calibration(object):
    def __init__(self, smoothing_window_size=0):

        # set calibration constants
        self.BB = .0132  # Ruze equation parameter
        self.UNDER_2GHZ_TAU_0 = 0.008
        self.SMOOTHING_WINDOW = smoothing_window_size
        self.pu = Pipeutils()

    # ------------- Unit methods: do not depend on any other pipeline methods

    def total_power(self, cal_on, cal_off, t_on, t_off):
        r"""Calculate the total power of spectrum with noise diode-switching.

        Args:
            cal_on(1d array): Spectrum *with* noise diode applied.
            cal_off(1d array): Spectrum *without* noise diode applied.
            t_on(float): Exposure time of the spectrum *with* noise diode.
            t_off(float): Exposure time of the spectrum *without* noise diode.

        Returns:
            1d array and float:

            A spectrum and a total exposure time.
            The spectrum is the average of the input spectra.
            The exposure time is the sum of the input exposure times.

        """
        return np.ma.mean((cal_on, cal_off), axis=0), t_on + t_off

    def tsky_correction(self, tsky_sig, tsky_ref, spillover):
        r"""Correction factor for sky brightness variation between reference and current integration.

        Args:
            tsky_sig(float): Sky brightness at current temperature, \
                frequency and elevation.
            tsky_ref(float): Sky brightness at reference temperature, \
                frequency and elevation.
            spillover(float): Spillover factor.

        Returns:
            float:
            A sky brightness correction factor.

            .. math::

               spillover * (tsky\_{sig} - tsky\_{ref})

        """
        return spillover * (tsky_sig - tsky_ref)

    def aperture_efficiency(self, reference_eta_a, freq_hz):
        r"""Determine telescope aperture efficiency at a given frequency.

        EtaA model is from memo by Jim Condon, provided by Ron Maddalena.

        Args:
            reference_eta_a(float): Reference aperture efficiency.
            freq_hz(float): Frequency in Hertz.

        Returns:
            float:
            Point or main beam efficiency (ranges from 0 to 1).

        .. testsetup::

           from Calibration import Calibration

        .. doctest:: :hide:

           >>> cal = Calibration()
           >>> print '{0:.6f}'.format(cal.aperture_efficiency(.71, 23e9))
           0.647483
           >>> print '{0:.6f}'.format(cal.aperture_efficiency(.91, 23e9))
           0.829872

        """
        freq_ghz = float(freq_hz) / 1e9
        return reference_eta_a * math.e**-((self.BB * freq_ghz)**2)

    def main_beam_efficiency(self, reference_eta_b, freq_hz):
        r"""Determine main beam efficiency, given a reference etaB value and frequency.

        This is the same equation as is used to determine aperture efficiency.
        The only difference is the reference value.

        Args:
            reference_eta_b(float): The main beam efficiency. \
             For the GBT, the default is :math:`1.28 * \eta_A`, where :math:`\eta_A` is aperture efficiency.
            freq_hz(float): The frequency in Hertz.

        Returns:
            float:
            An aperture efficiency at a given frequency.
        """
        return self.aperture_efficiency(reference_eta_b, freq_hz)

    def elevation_adjusted_opacity(self, zenith_opacity, elev):
        r"""Compute elevation-corrected opacities.

        We need to estimate the number of atmospheres along the
        line of site at an input elevation

        This comes from a model reported by Ron Maddalena:

        :math:`A = \frac{1}{\sin(elev)}` is a good approximation down to about 15 deg but
        starts to get pretty poor below that.  Here's a quick-to-calculate,
        better approximation that I determined from multiple years worth of
        weather data and which is good down to elev = 1 deg:

        .. math::

           A = -0.023437  + \frac{1.0140}{\sin( \frac{pi}{180} * (elev + \frac{5.1774}{elev + 3.3543} )}

        Args:
            zenith_opacity(float): Opacity at zenith based only on time.
            elev(float): Elevation angle of integration or scan.

        Returns:
            float:
            Elevation-adjusted opacity

        .. testsetup::

           from Calibration import Calibration

        .. doctest:: :hide:

           >>> cal = Calibration()
           >>> print ['{0:.6f}'.format(cal.elevation_adjusted_opacity(1, el)) for el in range(90)]
           ['37.621216', '26.523488', '19.566942', '15.217485', '12.341207', '10.331365', '8.861127', '7.745094', '6.872195', '6.172545', '5.600276', '5.124171', '4.722318', '4.378917', '4.082311', '3.823718', '3.596410', '3.395144', '3.215779', '3.055004', '2.910137', '2.778989', '2.659751', '2.550918', '2.451229', '2.359617', '2.275175', '2.197126', '2.124803', '2.057628', '1.995099', '1.936775', '1.882273', '1.831253', '1.783416', '1.738495', '1.696253', '1.656478', '1.618982', '1.583595', '1.550162', '1.518545', '1.488619', '1.460271', '1.433397', '1.407903', '1.383703', '1.360719', '1.338878', '1.318115', '1.298369', '1.279585', '1.261710', '1.244698', '1.228504', '1.213089', '1.198415', '1.184446', '1.171152', '1.158501', '1.146467', '1.135024', '1.124146', '1.113814', '1.104005', '1.094700', '1.085882', '1.077533', '1.069639', '1.062184', '1.055156', '1.048543', '1.042331', '1.036512', '1.031074', '1.026009', '1.021309', '1.016966', '1.012972', '1.009322', '1.006009', '1.003029', '1.000376', '0.998047', '0.996038', '0.994346', '0.992968', '0.991902', '0.991147', '0.990701']

        """
        deg2rad = (math.pi / 180)  # factor to convert degrees to radians
        num_atmospheres = -0.023437 + 1.0140 / math.sin(deg2rad *
                                                        (elev + 5.1774 /
                                                         (elev + 3.3543)))
        corrected_opacity = zenith_opacity * num_atmospheres

        return corrected_opacity

    def _tatm(self, freq_hz, tmp_c):
        """Estimates the atmospheric effective temperature.

        Keyword arguments:
        freq_hz -- input frequency in Hz
        where: tmp_c -- input ground temperature in Celsius

        Returns:
        air_temp_k -- output Air Temperature in Kelvin

        Based on local ground temperature measurements.  These estimates
        come from a model reported by Ron Maddalena

        Using Tatm = 270 is too rough an approximation since Tatm can vary
        from 244 to 290, depending upon the weather conditions and observing
        frequency.  One can derive an approximation for the default Tatm that
        is accurate to about 3.5 K from the equation:

        TATM = (A0 + A1*FREQ + A2*FREQ^2 +A3*FREQ^3 + A4*FREQ^4 + A5*FREQ^5)
                    + (B0 + B1*FREQ + B2*FREQ^2 + B3*FREQ^3 + B4*FREQ^4 +
        B5*FREQ^5)*TMPC

        where TMPC = ground-level air temperature in C and Freq is in GHz.  The
        A and B coefficients are:

                                    A0=    259.69185966 +/- 0.117749542
                                    A1=     -1.66599001 +/- 0.0313805607
                                    A2=     0.226962192 +/- 0.00289457549
                                    A3=   -0.0100909636 +/- 0.00011905765
                                    A4=   0.00018402955 +/- 0.00000223708
                                    A5=  -0.00000119516 +/- 0.00000001564
                                    B0=      0.42557717 +/- 0.0078863791
                                    B1=     0.033932476 +/- 0.00210078949
                                    B2=    0.0002579834 +/- 0.00019368682
                                    B3=  -0.00006539032 +/- 0.00000796362
                                    B4=   0.00000157104 +/- 0.00000014959
                                    B5=  -0.00000001182 +/- 0.00000000105


        tatm model is provided by Ron Maddalena

        >>> print '{0:.6f}'.format(Calibration()._tatm(23e9, 40))
        298.885174
        >>> print '{0:.6f}'.format(Calibration()._tatm(23e9, 30))
        289.780603
        >>> print '{0:.6f}'.format(Calibration()._tatm(1.42e9, 30))
        271.978666

        """

        # where TMPC = ground-level air temperature in C and Freq is in GHz.
        # The A and B coefficients are:
        aaa = [
            259.69185966, -1.66599001, 0.226962192, -0.0100909636,
            0.00018402955, -0.00000119516
        ]
        bbb = [
            0.42557717, 0.033932476, 0.0002579834, -0.00006539032,
            0.00000157104, -0.00000001182
        ]
        freq_ghz = float(freq_hz) / 1e9

        air_temp_k_A = air_temp_k_B = 0
        for idx, term in enumerate(zip(aaa, bbb)):
            if idx > 0:
                air_temp_k_A = air_temp_k_A + term[0] * (freq_ghz**idx)
                air_temp_k_B = air_temp_k_B + term[1] * (freq_ghz**idx)
            else:
                air_temp_k_A = term[0]
                air_temp_k_B = term[1]

        air_temp_k = air_temp_k_A + (air_temp_k_B * float(tmp_c))
        return air_temp_k

    def zenith_opacity(self, coeffs, freq_ghz):
        r"""Interpolate low and high opacities across a vector of frequencies.

        Args:
            coeffs(1d array): Opacitiy coefficients from archived text file, \
                produced by GBT weather prediction code.
            freq_ghz(float): Frequency value in GHz.

        Returns:
            float:
            A zenith opacity at requested frequency.

        """

        # interpolate between the coefficients based on time for a
        # given frequency
        def _interpolated_zenith_opacity(freq):
            # for frequencies < 2 GHz, return a default zenith opacity
            if np.array(freq).mean() < 2:
                result = np.ones(np.array(freq).shape) * self.UNDER_2GHZ_TAU_0
                return result
            result = 0
            for idx, term in enumerate(coeffs):
                if idx > 0:
                    result = result + term * freq**idx
                else:
                    result = term
            return result

        zenith_opacity = _interpolated_zenith_opacity(freq_ghz)
        return zenith_opacity

    def tsys(self, tcal, cal_on, cal_off):
        r"""Calculate the system temperature for an integration.

        Args:
            tcal(float): Lab-measured receiver calibration temperature.
            cal_on(1d array): Spectrum *with* noise diode applied.
            cal_off(1d array): Spectrum *without* noise diode applied.

        Returns:
            float:
            .. math::
               tcal * \frac{cal\_{off}}{cal\_{on} - cal\_{off}} + \frac{tcal}{2}

        """
        nchan = len(cal_off)
        low = int(.1 * nchan)
        high = int(.9 * nchan)
        cal_off = (cal_off[low:high]).mean()
        cal_on = (cal_on[low:high]).mean()
        return np.float(tcal * (cal_off / (cal_on - cal_off)) + tcal / 2)

    def antenna_temp(self, tsys, sig, ref, t_sig, t_ref):
        r"""Calibrate a spectrum to units of antenna temperature.

        Args:
            tsys(float): System temperature of the reference scan.
            sig(1d array): Signal ("on") spectrum.
            ref(1d array): Reference ("off") spectrum.
            t_sig(float): Exposure time of the signal spectrum.
            t_ref(float): Exposure time of the reference spectrum.

        Returns:
            1d array or float:
            A calibrated spectrum with an exposure time.
            The spectrum is

            .. math:: tsys * \frac{sig - ref}{ref}.

            The exposure time is

            .. math::
               \frac{t\_{sig} * t\_{ref} * window\_{size}}{t\_{sig} + (t\_{ref} * window\_{size})}

            where the window size is an optional smoothing kernel size for the
            reference spectrum.

        """
        if self.SMOOTHING_WINDOW > 1:
            ref = smoothing.boxcar(ref, self.SMOOTHING_WINDOW)
            window_size = self.SMOOTHING_WINDOW
        else:
            window_size = 1

        ref = self.pu.masked_array(ref)

        spectrum = tsys * ((sig - ref) / ref)
        exposure_time = (t_sig * t_ref * window_size /
                         (t_sig + t_ref * window_size))
        return spectrum, exposure_time

    def _ta_fs_one_state(self, sigref_state, sigid, refid, scale):

        sig = sigref_state[sigid]['TP']

        ref = sigref_state[refid]['TP']
        ref_cal_on = sigref_state[refid]['cal_on']
        ref_cal_off = sigref_state[refid]['cal_off']

        tcal = ref_cal_off['TCAL'] * scale

        tsys = self.tsys(tcal, ref_cal_on['DATA'], ref_cal_off['DATA'])

        a_temp_params = {
            'tsys': tsys,
            'sig': sig,
            'ref': ref,
            't_sig': sigref_state[sigid]['EXPOSURE'],
            't_ref': sigref_state[refid]['EXPOSURE']
        }
        antenna_temp, exposure = self.antenna_temp(**a_temp_params)

        return antenna_temp, tsys, exposure

    def ta_fs(self, sigref_state, scale):
        r"""Calibrate a frequency-switched integration to units of antenna temperature.

        Args:
            sigref_state(struct): A structure holding the noise diode off and on \
                integrations (which are full rows from the FITS table, including the DATA column), \
                a total power integration, FITS table row number and \
                exposure time.
            scale(float): A relative beam scaling factor.  Default is 1, or no scaling.

        Returns:
            1d array, float, float:
            An averaged spectrum calibrated to units of antenna temperature, \
            a system temperature and a total exposure time for the spectrum.

        """

        ta0, tsys0, exposure0 = self._ta_fs_one_state(sigref_state, 0, 1,
                                                      scale)
        ta1, tsys1, exposure1 = self._ta_fs_one_state(sigref_state, 1, 0,
                                                      scale)

        # shift in frequency
        sig_centerfreq = sigref_state[0]['cal_off']['OBSFREQ']
        ref_centerfreq = sigref_state[1]['cal_off']['OBSFREQ']

        sig_delta = sigref_state[0]['cal_off']['CDELT1']
        channel_shift = -((sig_centerfreq - ref_centerfreq) / sig_delta)

        # do integer channel shift to second spectrum
        ta1_ishifted = np.roll(ta1, int(channel_shift))
        if channel_shift > 0:
            ta1_ishifted[:channel_shift] = float('nan')
        elif channel_shift < 0:
            ta1_ishifted[channel_shift:] = float('nan')

        # do fractional channel shift
        fractional_shift = channel_shift - int(channel_shift)
        # doMessage(logger, msg.DBG, 'Fractional channel shift is',
        #          fractional_shift)
        xxp = range(len(ta1_ishifted))
        yyp = ta1_ishifted
        xxx = xxp - fractional_shift

        yyy = np.interp(xxx, xxp, yyp)
        ta1_shifted = self.pu.masked_array(yyy)

        exposures = np.array([exposure0, exposure1])
        tsyss = np.array([tsys0, tsys1])
        tas = [ta0, ta1_shifted]

        # average shifted spectra
        ta = self.average_spectra(tas, tsyss, exposures)

        # average tsys
        tsys = self.average_tsys(tsyss, exposures)

        # only sum the exposure if frequency switch is "in band" (i.e.
        # overlapping channels); otherwise use the exposure from the
        # first state only
        if abs(channel_shift) < len(ta1):
            exposure_sum = exposure0 + exposure1
        else:
            exposure_sum = exposure0

        return ta, tsys, exposure_sum

    def ta_star(self, antenna_temp, opacity, spillover):
        r"""Calibrate a spectrum to units of **ta***.

        Args:
            antenna_temp(1d array): Spectrum calibrated to units of antenna temperature.
            opacity(float): Elevation-adjusted atmospheric opacity.
            spillover(float): Correction factor for rear-spillover,	ohmic loss and blockage	efficiency.

        Returns:
            1d array:
            A calibrated spectrum.

            .. math::
               antenna\_{temp} * e^{opacity} * \frac{1}{spillover}

        """
        # opacity is corrected for elevation
        return antenna_temp * math.e**opacity * 1 / spillover

    def jansky(self, spectrum, aperture_efficiency):
        r"""Calibrate a spectrum to units of **Jansky**.

        Args:
            spectrum(1d array): A spectrum previously calibrated to **ta***.
            aperture_efficiency(float): The aperture efficiency factor.

        Returns:
            1d array:

            .. math::
               \frac{spectrum}{2.85 * aperture\_{efficiency}}

        """
        return spectrum / (2.85 * aperture_efficiency)

    def interpolate_by_time(self, reference1, reference2, first_ref_timestamp,
                            second_ref_timestamp, integration_timestamp):
        r"""Calculate interpolated value(s).

        This function can be used to calculate a single interpolated value
        or an array of values at a specified time.

        Args:
            reference1(float or 1d array): Value(s) for first time.
            reference2(float or 1d array): Value(s) for second time.
            first_ref_timestamp(float): First time.
            second_ref_timestamp(float): Second time.
            integration_timestamp(float): The time for which we want a value.

        Returns:
            float or 1d array:
            Interpolated value(s) for a specific time.

        .. testsetup::

           from Calibration import Calibration
           import numpy as np

        .. doctest:: :hide:

           >>> cal = Calibration()
           >>> cal.interpolate_by_time(1, 2, 0, 100, 75)
           1.75
           >>> cal.interpolate_by_time(np.array([1, 2]), np.array([2, 3]), 0, 100, 75)
           array([ 1.75,  2.75])

        """

        time_btwn_ref_scans = float(second_ref_timestamp) - float(
            first_ref_timestamp)
        aa1 = (second_ref_timestamp -
               integration_timestamp) / time_btwn_ref_scans
        aa2 = (integration_timestamp -
               first_ref_timestamp) / time_btwn_ref_scans
        return aa1 * reference1 + aa2 * reference2

    def make_weights(self, tsyss, exposures):
        r"""Create weights for integration averaging.

        Args:
            tsyss(1d array): A list of system temperatures.
            exposures(1d array): A list of exposure times.  \
            The number of exposure times must match the number of system temperatures.

        Returns:
            1d array:
            A list of weights.  The weights are computed with the following formula.

            .. math::

               \frac{exposure\ time}{tsys^2}

        """
        return exposures / tsyss**2

    def average_tsys(self, tsyss, exposures):
        r"""Compute a weighted average multiple system temperatures.

        Args:
            tsyss(1d array): The system temperatures to average.
            exposures(1d array): The exposure times corresponding to each system temperature.

        Returns:
            1d array:
            A weighted average system temperature.  See the *make_weights* method to see how
            the weights are computed.

        """
        weights = self.make_weights(tsyss, exposures)
        return np.sqrt(np.average(tsyss**2, axis=0, weights=weights))

    def average_spectra(self, specs, tsyss, exposures):
        r"""Perform a weighted average of two spectra.

        Args:
            specs(two 1d arrays): The two input spectra to be averaged.
            tsyss(two floats): System temperatures corresponding to each input spectrum.
            exposures(two floats): Exposure times corresponding to each input spectrum.

        Returns:
            1d array:
            A weighted average spectrum.  See the *make_weights* method to see how the weights
            are computed.

        """
        weights = self.make_weights(tsyss, exposures)

        if float('nan') in specs[0] or float('nan') in specs[1]:

            weight0 = np.ma.array([weights[0]] * len(specs[0]),
                                  mask=specs[0].mask)
            weight1 = np.ma.array([weights[1]] * len(specs[1]),
                                  mask=specs[1].mask)
            weights = [weight0.filled(0), weight1.filled(0)]

        return np.ma.average(specs, axis=0, weights=weights)

    def getReferenceAverage(self, crefs, tsyss, exposures, timestamps,
                            tambients, elevations):
        r"""Average the total power integrations from a reference scan.

        Args:
            crefs(stack of 1d arrays): The total power integrations (spectra) for a single reference scan.
            tsyss(1d array): The system temperatures; one for each input spectrum.
            exposures(1d array): The exposure times; one for each input spectrum.
            timestamps(1d array): The timestamps; one for each input spectrum.
            tambients(1d array): Ambient temperatures in Kelvin; one for each input spectrum.
            elevations(1d array): Elevation in degrees; one for each input spectrum.

        Returns:
            1d array, float, float, float, float, float:
            An average value for each of the input parameters.  An average spectrum along with
            average system temperature, exposure time, timestamp, ambient temperature and elevation.

        """

        # convert to numpy arrays
        crefs = np.array(crefs)
        tsyss = np.array(tsyss)
        exposures = np.array(exposures)
        timestamps = np.array(timestamps)
        tambients = np.array(tambients)
        elevations = np.array(elevations)

        avg_tsys = self.average_tsys(tsyss, exposures)

        avg_tsys80 = avg_tsys.mean(0)  # single value for mid 80% of band
        avg_cref = self.average_spectra(crefs, tsyss, exposures)
        exposure = np.sum(exposures)

        avg_timestamp = timestamps.mean()
        avg_tambient = tambients.mean()
        avg_elevation = elevations.mean()

        return avg_cref, avg_tsys80, avg_timestamp, avg_tambient, avg_elevation, exposure

    def tsky(self, ambient_temp_k, freq_hz, tau):
        r"""Determine the sky brightness temperature at a frequency.

        Args:
            ambient_temp_k(float): Mean ambient temperature in Kelvin.
            freq_hz(float): Frequency in Hz.
            tau(float): Atmospheric opacity value.

        Returns:
            float:
            The sky model temperature contribution at frequency channel.

        """
        ambient_temp_c = ambient_temp_k - 273.15  # convert to Celsius
        airTemp = self._tatm(freq_hz, ambient_temp_c)

        tsky = airTemp * (1 - math.e**(-tau))

        return tsky