示例#1
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 )
示例#2
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)
示例#3
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
示例#4
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
示例#5
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
示例#6
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