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()
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))
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)
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 )
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)
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 )
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))
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()
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))
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
def __init__(self, row): self.pu = Pipeutils() self.data = row
def __init__(self): self.pu = Pipeutils()
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)
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
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)
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)
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
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