#!/usr/bin/python -u
#
# bryan butler
# nrao
# spring 2012, summer 2016
#
# python functions to return expected flux density from solar system
# bodies.  the flux density depends on the geometry (distance, size of
# body, subearth latitude), and on the model brightness temperature.
# uncertainties on the flux density can also be returned, but are all
# set to 0.0 for now, because i don't have uncertainties on the model
# brightness temperatures.
#
# the model brightness temperatures for the various bodies are taken
# from a combination of modern models and historical observations.  see
# the written description for a more complete description of the model
# for each body.
#
# for all of the bodies but Mars, the model is contained in a text file
# that has tabulated brightness temperature as a function of frequency.
# for Mars it is also a function of time.  eventually, the full-up
# model calculations should be in the code (for those bodies that have
# proper models) but for now, just live with the tabulated versions.
#
# version 2.0
# last edited: 2016Aug15
# Modified by TT to avoid uncessary file open: 2012Dec13


from __future__ import absolute_import
from numpy import searchsorted
from numpy import array
from scipy.interpolate import interp1d
from math import exp, pi, cos, sin, isnan, sqrt
import os

from casatasks.private.casa_transition import is_CASA6
if is_CASA6:
    from casatools import table, measures, quanta, ctsys
    qa = quanta( )
    _tb = table( )
    _me = measures( )

    ctsys_resolve = ctsys.resolve
else:
    from taskinit import gentools
    (_tb,_me)=gentools(['tb','me'])
    from casac import *
    qa = casac.quanta()

    def ctsys_resolve(apath):
        dataPath = os.path.join(os.environ['CASAPATH'].split()[0],'data')
        return os.path.join(dataPath,apath)

HH = qa.constants('H')['value'] 
KK = qa.constants('K')['value']
CC = qa.constants('C')['value'] 

class solar_system_setjy:
    def __init__(self):
        self.models={}
    

    def solar_system_fd (self, source_name, MJDs, frequencies, observatory, casalog=None):
        '''
        find flux density for solar system bodies:
            Venus - Butler et al. 2001
            Mars - Butler et al. 2012
            Jupiter - Orton et al. 2012
            Uranus - Orton & Hofstadter 2012 (modified ESA4)
            Neptune - Orton & Hofstadter 2012 (modified ESA3)
            Io - Butler et al. 2012
            Europa - Butler et al. 2012
            Ganymede - Butler et al. 2012
            Titan - Gurwell et al. 2012
            Callisto - Butler et al. 2012
            Ceres - Mueller (private communication)
            Juno - Butler et al. 2012
            Pallas - Mueller (private communication)
            Vesta - Mueller (private communication)
            Lutetia - Mueller (private communication)

        inputs:
            source_name = source name string.  example: "Venus"
            MJDs = list of MJD times (day + fraction).  example:
                   [ 56018.232, 56018.273 ]
                   must be sorted in ascending order.
            frequencies = list of [start, stop] frequencies for
                          which to calculate the integrated model.
                          example:
                          [ [ 224.234567e9, 224.236567e9 ],
                            [ 224.236567e9, 224.238567e9 ] ]
            observatory = observatory name string.  example: "ALMA"

        returned is a list, first element is the return status:
            0 -> success
            1 -> Error: unsupported body
            2 -> Error: unsupported frequency or time for body
            3 -> Error: Tb model file not found
            4 -> Error: ephemeris table not found, or time out of range
                 (note - the case where the MJD times span two ephemeris
                  files is not supported)
            5 -> Error: unknown observatory
        second element is a list of flux densities, one per time and
            frequency range, frequency changes fastest.
        third element is list of uncertainties (if known; 0 if unknown),
            one per time and frequency range, frequency changes fastest.
        fourth element is a list of major axis, minor axis, and PAs in
            asec and deg, one per MJD time.
        fifth element is a list of CASA directions, one per MJD time.

        bjb
        nrao
        spring/summer/fall 2012 + spring 2016
        '''

        RAD2ASEC = 2.0626480624710e5
        AU = 1.4959787066e11
        SUPPORTED_BODIES = [ 'Venus', 'Mars', 'Jupiter', 'Uranus', 'Neptune',
                             'Io', 'Europa', 'Ganymede', 'Callisto', 'Titan',
                             'Ceres', 'Juno', 'Pallas', 'Vesta', 'Hygeia' ]
# those which have Tb (or f.d.) that is tabulated vs. time and frequency
        TIME_VARIABLE_BODIES = [ 'Mars', 'Ceres', 'Pallas', 'Vesta', 'Lutetia' ]
# those which are tabulations of flux density
        MODEL_IS_FD_BODIES = [ 'Ceres', 'Pallas', 'Vesta', 'Lutetia' ]

        capitalized_source_name = source_name.capitalize()
        statuses = []
        fds = []
        dfds = []
        Rhats = []
        directions = []

    #
    # check that body is supported
    #
        if (not capitalized_source_name in SUPPORTED_BODIES):
            for MJD in MJDs:
                estatuses = []
                efds = []
                edfds = []
                for frequency in frequencies:
                    estatuses.append(1)
                    efds.append(0)
                    edfds.append(0)
                statuses.append(estatuses)
                fds.append(efds)
                dfds.append(edfds)
                Rhats.append([0.0, 0.0, 0.0])
                directions.append(_me.direction('J2000',0.0,0.0))
            return [ statuses, fds, dfds, Rhats, directions ]

    #
    # check that observatory is known
    #
        if not observatory in _me.obslist():
            for MJD in MJDs:
                estatuses = []
                efds = []
                edfds = []
                for frequency in frequencies:
                    estatuses.append(5)
                    efds.append(0)
                    edfds.append(0)
                statuses.append(estatuses)
                fds.append(efds)
                dfds.append(edfds)
                Rhats.append([0.0, 0.0, 0.0])
                directions.append(_me.direction('J2000',0.0,0.0))
            return [ statuses, fds, dfds, Rhats, directions ]

    #
    # before calculating the models be sure that we have the ephemeris 
    # information.  otherwise don't waste our time calculating the model.
    # only really important for mars, but do it for them all.
    #
        ephemeris_path = ctsys_resolve('ephemerides/JPL-Horizons')
        ephemeris_file_list = os.listdir(ephemeris_path)
        ephemeris_files = []
        for ephemeris_file in ephemeris_file_list:
            if (ephemeris_file.split('_')[0] == capitalized_source_name and 'J2000' in ephemeris_file):
                ephemeris_files.append(ephemeris_file)
        ephemeris_file_OK = False
    #
    # ephemeris tables have the following keywords:
    # GeoDist, GeoLat, GeoLong, MJD0, NAME, VS_CREATE, VS_DATE, VS_TYPE,
    # VS_VERSION, dMJD, earliest, latest, meanrad, obsloc, radii, rot_per
    # and columns:
    # MJD, RA, DEC, Rho (geodist), RadVel, NP_ang, NP_dist, DiskLong (Ob-long),
    # DiskLat(Ob-lat), Sl_lon, Sl_lat, r (heliodist), rdot, phang
    # Note by TT:
    # The column names, Obs_lon and Obs_lat have been changed to DiskLong and
    # DiskLat respectively to be consistent with what column names assumed for
    # ephemeris tables by casacore's MeasComet class.

        for ephemeris_file in ephemeris_files:
            _tb.open(os.path.join(ephemeris_path,ephemeris_file))
            table_source_name = _tb.getkeyword('NAME').capitalize()
            if (table_source_name != capitalized_source_name):
                continue
            first_time = _tb.getkeyword('earliest')['m0']['value']
            last_time = _tb.getkeyword('latest')['m0']['value']
            if (first_time < MJDs[0] and last_time > MJDs[-1]):
                ephemeris_file_OK = True
                break
            _tb.close()
    #
    # if we didn't find an ephemeris file, set the statuses and return.
    #
        if (not ephemeris_file_OK):
            for MJD in MJDs:
                estatuses = []
                efds = []
                edfds = []
                for frequency in frequencies:
                    estatuses.append(4)
                    efds.append(0)
                    edfds.append(0)
                statuses.append(estatuses)
                fds.append(efds)
                dfds.append(edfds)
                Rhats.append([0.0, 0.0, 0.0])
                directions.append(_me.direction('J2000',0.0,0.0))
            return [ statuses, fds, dfds, Rhats, directions ]

        Req = 1000.0 * (_tb.getkeyword('radii')['value'][0] + _tb.getkeyword('radii')['value'][1]) / 2
        Rp = 1000.0 * _tb.getkeyword('radii')['value'][2]
        times = _tb.getcol('MJD').tolist()
        RAs = _tb.getcol('RA').tolist()
        DECs = _tb.getcol('DEC').tolist()
        distances = _tb.getcol('Rho').tolist()
        RadVels = _tb.getcol('RadVel').tolist()
        column_names = _tb.colnames()
        if ('DiskLat' in column_names):
            selats = _tb.getcol('DiskLat').tolist()
            has_selats = 1
        else:
            has_selats = 0
            selat = 0.0
        if ('NP_ang' in column_names):
            NPangs = _tb.getcol('NP_ang').tolist()
            has_NPangs= 1
        else:
            has_NPangs = 0
            NPang = 0.0
        _tb.close()
        MJD_shifted_frequencies = []
        DDs = []
        Rmeans = []

        for ii in range(len(MJDs)):
            MJD = MJDs[ii]
            DDs.append(1.4959787066e11 * self.interpolate_list (times, distances, MJD)[1])
            if (has_selats):
                selat = self.interpolate_list (times, selats, MJD)[1]
                if (selat == -999.0):
                    selat = 0.0
    # apparent polar radius
            Rpap = sqrt (Req*Req * sin(selat)**2.0 +
                         Rp*Rp * cos(selat)**2.0)
            Rmean = sqrt (Rpap * Req)
            Rmeans.append(Rmean)
    #
    # need to check that the convention for NP angle is the
    # same as what is needed in the component list.
    #
            if (has_NPangs):
                NPang = self.interpolate_list (times, NPangs, MJD)[1]
                if (NPang == -999.0):
                    NPang = 0.0
            Rhats.append([2*RAD2ASEC*Req/DDs[-1], 2*RAD2ASEC*Rpap/DDs[-1], NPang])
            RA = self.interpolate_list (times, RAs, MJD)[1]
            RAstr=str(RA)+'deg'
            DEC = self.interpolate_list (times, DECs, MJD)[1]
            DECstr=str(DEC)+'deg'
            directions.append(_me.direction('J2000',RAstr,DECstr))
    #
    # now get the doppler shift
    #
    # NOTE: this is not exactly right, because it doesn't include the
    # distance to the body in any of these calls.  the distance will matter
    # because it will change the line-of-sight vector from the observatory
    # to the body, which will change the doppler shift.  jeff thinks using
    # the comet measures calls might fix this, but i haven't been able to
    # figure them out yet.  i thought i had it figured out, with the
    # call to me.framecomet(), but that doesn't give the right answer,
    # unfortunately.  i spot-checked the error introduced because of this,
    # and it looks to be of order 1 m/s for these bodies, so i'm not going
    # to worry about it.
    #
            _me.doframe(_me.observatory(observatory))
            _me.doframe(_me.epoch('utc',str(MJD)+'d'))
            _me.doframe(directions[-1])
    #
    # instead of the call to me.doframe() in the line above, i thought the
    # following call to me.framecomet() would be right, but it doesn't give
    # the right answer :/.
    #       me.framecomet(ephemeris_file)
    #
    # RadVel is currently in AU/day.  we want it in km/s.
    #
            RadVel = self.interpolate_list (times, RadVels, MJD)[1] * AU / 86400000.0
            rv = _me.radialvelocity('geo',str(RadVel)+'km/s')
            shifted_frequencies = []
            for frequency in frequencies:
    #
    # the measure for radial velocity could be obtained via:
    # me.measure(rv,'topo')['m0']['value']
    # but what we really want is a frequency shift.  i could do it by
    # hand, but i'd rather do it with casa toolkit calls.  unfortunately,
    # it's a bit convoluted in casa...
    #
                newfreq0 = _me.tofrequency('topo',_me.todoppler('optical',_me.measure(rv,'topo')),_me.frequency('topo',str(frequency[0])+'Hz'))['m0']['value']
                newfreq1 = _me.tofrequency('topo',_me.todoppler('optical',_me.measure(rv,'topo')),_me.frequency('topo',str(frequency[1])+'Hz'))['m0']['value']
    #
    # should check units to be sure frequencies are in Hz
    #
    # now, we want to calculate the model shifted in the opposite direction
    # as the doppler shift, so take that into account.
    #
                delta_frequency0 = frequency[0] - newfreq0
                newfreq0 = frequency[0] + delta_frequency0
                delta_frequency1 = frequency[1] - newfreq1
                newfreq1 = frequency[1] + delta_frequency1
                shifted_frequencies.append([newfreq0,newfreq1])
                average_delta_frequency = (delta_frequency0 + delta_frequency1)/2
    #
    # should we print this to the log?
    #
    #           casalog.post( 'MJD, geo & topo velocities (km/s), and shift (MHz) = %7.1f  %5.1f  %5.1f  %6.3f' % \
    #                 (MJD, RadVel, me.measure(rv,'topo')['m0']['value']/1000, average_delta_frequency/1.0e6))
                msg='MJD, geo & topo velocities (km/s), and shift (MHz) = %7.1f  %5.1f  %5.1f  %6.3f' % \
                     (MJD, RadVel, _me.measure(rv,'topo')['m0']['value']/1000, average_delta_frequency/1.0e6)
                casalog.post(msg, 'INFO2')
            MJD_shifted_frequencies.append(shifted_frequencies)
    #       _me.done()
        for ii in range(len(MJDs)):
            shifted_frequencies = MJD_shifted_frequencies[ii]
            if (capitalized_source_name in TIME_VARIABLE_BODIES):
                [tstatuses,brightnesses,dbrightnesses] = self.brightness_time_int (capitalized_source_name,[MJDs[ii]], shifted_frequencies)
                # modified by TT: take out an extra dimension (for times), to match the rest of the operation 
                tstatuses = tstatuses[0] 
                brightnesses = brightnesses[0]
                dbrightnesses = dbrightnesses[0]
            else:
                tstatuses = []
                brightnesses = []
                dbrightnesses = []
                for shifted_frequency in shifted_frequencies:
                    [status,brightness,dbrightness] = self.brightness_planet_int (capitalized_source_name, shifted_frequency)
                    tstatuses.append(status)
                    brightnesses.append(brightness)
                    dbrightnesses.append(dbrightness)
            tfds = []
            tdfds = []
            for jj in range (len(tstatuses)):
#
# if a status of 6 was returned, then it's a body with a current
# flux density model, but a model from the past which was not
# flux density was used, so don't multiply by apparent size.
#
                 if (tstatuses[jj] == 0 or tstatuses[jj] == 6):
                    flux_density = brightnesses[jj]  # brigtnesses contains flux density already
                    if (capitalized_source_name not in MODEL_IS_FD_BODIES or tstatuses[jj] == 6):
                        flux_density *= 1.0e26 * pi * Rmeans[ii]*Rmeans[ii]/ (DDs[ii]*DDs[ii])
                    tstatuses[jj] = 0
    #
    # mean apparent planet radius, in arcseconds (used if we ever
    # calculate the primary beam reduction)
    #
                    psize = (Rmeans[ii] / DDs[ii]) * RAD2ASEC
    #
    # primary beam reduction factor (should call a function, but
    # just set to 1.0 for now...
    #
                    pbfactor = 1.0
                    flux_density *= pbfactor
                    tfds.append(flux_density)
                    tdfds.append(0.0)
                 else:
                    tfds.append(0.0)
                    tdfds.append(0.0)
            statuses.append(tstatuses)
            fds.append(tfds)
            dfds.append(tdfds)
        return [ statuses, fds, dfds, Rhats, directions ]


    def brightness_time_int (self, source_name, MJDs, frequencies):
        '''
        Planck brightness for those bodies for which the data file
        is a function of both frequency *and* time.
        inputs:
            source_name = source name (first character capitalized)
            MJDs = list of MJD times
            frequencies = list of [start, stop] frequencies for
                        which to calculate the integrated model.
                        example: [ [ 224.234567e9, 224.236567e9 ] ]
        '''

    # those bodies which are tabulations of flux density
        MODEL_IS_FD_BODIES = [ 'Ceres', 'Pallas', 'Vesta', 'Lutetia' ]
        HAS_OLD_MODEL_BODIES = [ 'Ceres', 'Pallas', 'Vesta' ]

        statuses = []
        Tbs = []
        dTbs = []
        model_data_path = ctsys_resolve('alma/SolarSystemModels')
        if (source_name in MODEL_IS_FD_BODIES):
            model_data_filename = os.path.join(model_data_path,source_name + '_fd_time.dat')
        else:
            model_data_filename = os.path.join(model_data_path,source_name + '_Tb_time.dat')
        try:
            ff = open(model_data_filename)
        except:
            for MJD in MJDs:
                estatuses = []
                eTbs = []
                edTbs = []
                for frequency in frequencies:
                    estatuses.append(3)
                    eTbs.append(0)
                    edTbs.append(0)
                statuses.append(estatuses)
                Tbs.append(eTbs)
                dTbs.append(edTbs)
            return [ statuses, Tbs, dTbs ]

    # first line holds frequencies, like:
    # 30.0 80.0 115.0 150.0 200.0 230.0 260.0 300.0 330.0 360.0 425.0 650.0 800.0 950.0 1000.0
        line = ff.readline()[:-1]
        fields = line.split()
        freqs = []
        for field in fields:
            freqs.append(1.0e9*float(field))
    # model output lines look like:
    #2010 01 01 00 00  55197.00000 189.2 195.8 198.9 201.2 203.7 204.9 205.9 207.1 207.8 208.5 209.8 213.0 214.6 214.8 214.5 
        modelMJDs = []
        modelTbs = []
        for line in ff:
            fields = line[:-1].split()
            modelMJDs.append(float(fields[5]))
            lTbs = []
            for ii in range(len(freqs)):
                lTbs.append(float(fields[6+ii]))
            modelTbs.append(lTbs)
        ff.close()
        old_model_already_called = False
        for MJD in MJDs:
    #
    # first, check if it's even a supported MJD (in the file)
    #
            if (MJD > modelMJDs[-1]):
                estatuses = []
                eTbs = []
                edTbs = []
                for frequency in frequencies:
                    estatuses.append(2)
                    eTbs.append(0.0)
                    edTbs.append(0.0)
            elif (MJD < modelMJDs[0] and source_name in HAS_OLD_MODEL_BODIES):
#
# if the time is before the first time in the model data file,
# then use the model that is not time variable (if this body
# has one).  set the status to 6 in that case, so that we know
# in the calling function to multiply by the apparent size
# to get true flux density.
#
                if (not old_model_already_called):
#
# only call this once - not for every time (since the old
# models are not a function of time)
#
                    tstatuses = []
                    tTbs = []
                    tdTbs = []
                    for frequency in frequencies:
                        [tstatus,tTb,tdTb] = self.brightness_planet_int (source_name, frequency)
                        tstatuses.append(tstatus)
                        tTbs.append(tTb)
                        tdTbs.append(tdTb)
                    old_model_already_called = True
                estatuses = []
                eTbs = []
                edTbs = []
                ii = 0
                for frequency in frequencies:
                    if (tstatuses[ii]):
                        estatuses.append(tstatuses[ii])
                    else:
                        estatuses.append(6)
                    eTbs.append(tTbs[ii])
                    edTbs.append(tdTbs[ii])
                    ii += 1
            else:
                nind = self.nearest_index (modelMJDs, MJD)
                mTbs = []
                mfds = []
                for ii in range(len(freqs)):
                    lMJD = []
                    lTb = []
                    for jj in range(nind-10, nind+10):
                        lMJD.append(modelMJDs[jj])
                        lTb.append(modelTbs[jj][ii])
                    if (source_name in MODEL_IS_FD_BODIES):
    # don't know what Tb is, so just set to 0.0
                        mTbs.append(0.0)
                        mfds.append(self.interpolate_list(lMJD, lTb, MJD)[1])
                    else:
                        mTbs.append(self.interpolate_list(lMJD, lTb, MJD)[1])
    #
    # note: here, when we have the planck results, get a proper
    # estimate of the background temperature.
    #
    # note also that we want to do this here because the integral
    # needs to be done on the brightness, not on the brightness
    # *temperature*.
    #
                        Tbg = 2.725
                        mfds.append((2.0 * HH * freqs[ii]**3.0 / CC**2.0) * \
                                    ((1.0 / (exp(HH * freqs[ii] / (KK * mTbs[-1])) - 1.0)) - \
                                     (1.0 / (exp(HH * freqs[ii] / (KK * Tbg)) - 1.0))))
                estatuses = []
                eTbs = []
                edTbs = []
                for frequency in frequencies:
                    if (frequency[0] < freqs[0] or frequency[1] > freqs[-1]):
                        estatuses.append(2)
                        eTbs.append(0.0)
                        edTbs.append(0.0)
                    else:
                        [estatus, eTb, edTb] = self.integrate_Tb (freqs, mfds, frequency)
    #
    # should we print out the Tb we found?  not sure.  i have a
    # vague recollection that crystal requested it, but i'm not
    # sure if it's really needed.  we'd have to back out the 
    # planck correction (along with the background), so it wouldn't
    # be trivial.
    #
                        estatuses.append(estatus)
                        eTbs.append(eTb)
                        edTbs.append(edTb)
            statuses.append(estatuses)
            Tbs.append(eTbs)
            dTbs.append(edTbs)
        return [statuses, Tbs, dTbs ]


    def brightness_planet_int (self, source_name, frequency):
        '''
        brightness temperature for supported planets.  integrates over
        a tabulated model.  inputs:
            source_name = source name string
            frequency = list of [start, stop] frequencies for
                        which to calculate the integrated model.
                        example: [ 224.234567e9, 224.236567e9 ]
        '''

    # those bodies which are tabulations of flux density.  currently, for
    # the non-time-variable ones, none of them are flux density. but
    # leave this in just in case somewhere down the road we have bodies
    # that we have a flux density model for that isn't time variable
    # (evolved stars, for instance).
        MODEL_IS_FD_BODIES = [ ]

        if not source_name in self.models:
            model_data_path = ctsys_resolve('alma/SolarSystemModels')
            if (source_name in MODEL_IS_FD_BODIES):
                model_data_filename = os.path.join(model_data_path,source_name + '_fd.dat')
            else:
                model_data_filename = os.path.join(model_data_path,source_name + '_Tb.dat')
            try:
                ff = open(model_data_filename)
            except:
                return [ 3, 0.0, 0.0 ]
            fds = []
            Tbs = []
            freqs = []
            for line in ff:
                [freq,Tb] = line[:-1].split()
                freqs.append(1.0e9*float(freq))
                if (source_name in MODEL_IS_FD_BODIES):
# don't know what Tb is, so just set to 0.0
                    Tbs.append(0.0)
                    fds.append(float(Tb))
                else:
                    Tbs.append(float(Tb))
#
# note: here, when we have the planck results, get a proper
# estimate of the background temperature.
#
# note also that we want to do this here because the integral
# needs to be done on the brightness, not on the brightness
# *temperature*.
#
                    Tbg = 2.725
                    fds.append((2.0 * HH * freqs[-1]**3.0 / CC**2.0) * \
                                ((1.0 / (exp(HH * freqs[-1] / (KK * Tbs[-1])) - 1.0)) - \
                                 (1.0 / (exp(HH * freqs[-1] / (KK * Tbg)) - 1.0))))
            ff.close()
# TT added.  store them for future calls
            srcn=source_name
            self.models[srcn]={}
            self.models[srcn]['fds']=fds
            self.models[srcn]['freqs']=freqs
        else:
            #recover fds, freqs, instead of reading them in from the file
            fds=self.models[source_name]['fds']
            freqs=self.models[source_name]['freqs']
        if (frequency[0] < freqs[0] or frequency[1] > freqs[-1]):
            return [ 2, 0.0, 0.0 ]
        else:
    #
    # should we print out the Tb we found?  not sure.  i have a
    # vague recollection that crystal requested it, but i'm not
    # sure if it's really needed.  we'd have to back out the 
    # planck correction (along with the background), so it wouldn't
    # be trivial.  and here, we'd have to return a variable and
    # work on that.
    #
            return self.integrate_Tb (freqs, fds, frequency)


    def nearest_index (self, input_list, value):
        """
        find the index of the list input_list that is closest to value
        """

        ind = searchsorted(input_list, value)
        ind = min(len(input_list)-1, ind)
        ind = max(1, ind)
        if value < (input_list[ind-1] + input_list[ind]) / 2.0:
            ind = ind - 1
        return ind


    def interpolate_list (self, freqs, Tbs, frequency):
        ind = self.nearest_index (freqs, frequency)
        low = max(0,ind-5)
        if (low == 0):
            high = 11
        else:
            high = min(len(freqs),ind+5)
            if (high == len(freqs)):
                low = high - 11
    #
    # i wanted to put in a check for tabulated values that change
    # derivative, since that confuses the interpolator.  benign cases are
    # fine, like radial velocity, but for the model Tbs, where there are
    # sharp spectral lines, then the fitting won't be sensible when you're
    # right at the center of the line, because the inflection is so severe.
    # i thought if i just only took values that had the same derivative as
    # the location where the desired value is that would work, but it
    # doesn't :/.  i'm either not doing it right or there's something
    # deeper.
    #
    #   if freqs[ind] < frequency:
    #       deriv = Tbs[ind+1] - Tbs[ind]
    #   else:
    #       deriv = Tbs[ind] - Tbs[ind-1]
    #   tTbs = []
    #   tfreqs = []
    #   for ii in range(low,high):
    #       nderiv = Tbs[ii+1] - Tbs[ii]
    #       if (nderiv >= 0.0 and deriv >= 0.0) or (nderiv < 0.0 and deriv < 0.0):
    #           tTbs.append(Tbs[ii])
    #           tfreqs.append(freqs[ii])
    #   aTbs = array(tTbs)
    #   afreqs = array(tfreqs)
        aTbs = array(Tbs[low:high])
        afreqs = array(freqs[low:high])
    #
    # cubic interpolation blows up near line centers (see above comment),
    # so check that it doesn't fail completely (put it in a try/catch), and
    # also that it's not a NaN and within the range of the tabulated values
    #
        range = max(aTbs) - min(aTbs)
        try:
            func = interp1d (afreqs, aTbs, kind='cubic')
            if isnan(func(frequency)) or func(frequency) < min(aTbs)-range/2 or func(frequency) > max(aTbs)+range/2:
                func = interp1d (afreqs, aTbs, kind='linear')
        except:
            func = interp1d (afreqs, aTbs, kind='linear')
    #
    # if it still failed, even with the linear interpolation, just take the
    # nearest tabulated point.
    #
        if isnan(func(frequency)) or func(frequency) < min(aTbs)-range/2 or func(frequency) > max(aTbs)+range/2:
            brightness = Tbs[ind]
        else:
            brightness = float(func(frequency))
        return [ 0, brightness, 0.0 ]


    def integrate_Tb (self, freqs, Tbs, frequency):
        [status,low_Tb,low_dTb] = self.interpolate_list (freqs, Tbs, frequency[0])
        low_index = self.nearest_index (freqs, frequency[0])
        if (frequency[0] > freqs[low_index]):
            low_index = low_index + 1

        [status,hi_Tb,hi_dTb] = self.interpolate_list (freqs, Tbs, frequency[1])
        hi_index = self.nearest_index (freqs, frequency[1])
        if (frequency[1] < freqs[hi_index]):
            hi_index = hi_index - 1

        if (low_index > hi_index):
            Tb = (frequency[1] - frequency[0]) * (low_Tb + hi_Tb) / 2
        else:
            Tb = (freqs[low_index] - frequency[0]) * (low_Tb + Tbs[low_index]) / 2 + \
                 (frequency[1] - freqs[hi_index]) * (hi_Tb + Tbs[hi_index]) / 2
            ii = low_index
            while (ii < hi_index):
               Tb += (freqs[ii+1] - freqs[ii]) * (Tbs[ii+1] + Tbs[ii]) / 2
               ii += 1
        Tb /= (frequency[1] - frequency[0])
        return [ 0, Tb, 0.0 ]