def clip_spectrum(wl,fx,wlmin,wlmax,pad=0): """This crops a spectrum wl,fx to wlmin and wlmax. If pad !=0, it should be set to a positive velocity by which the output spectrum will be padded around wlmin,wlmax, which is returned as well as the narrower cropped spectrum. Parameters ---------- wl : np.ndarray() The stellar model wavelength(s) in nm. fx : np.ndarray() The stellar model flux. wlmin: float The minimum cropping wavelength. wlmax: float The maximum cropping wavelength. pad: float, m/s, optional A velocity corresponding to a shift around which the cropped array will be padded. Returns ------- wl,fx: np.array(), np.array() The wavelength and flux of the integrated spectrum. """ import lib.test as test import numpy as np #First run standard tests on input test.typetest(wl,np.ndarray,varname='wl in clip_spectrum') test.typetest(fx,np.ndarray,varname='fx in clip_spectrum') test.typetest(wlmin,[int,float],varname='wlmin in clip_spectrum') test.typetest(wlmax,[int,float],varname='wlmax in clip_spectrum') test.typetest(pad,[int,float],varname='pad in clip_spectrum') test.notnegativetest(pad,varname='pad in clip_spectrum') test.nantest(wlmin,varname='wlmin in clip_spectrum') test.nantest(wlmax,varname='wlmax in clip_spectrum') test.nantest(pad,varname='pad in clip_spectrum') if wlmin >= wlmax: raise ValueError('wlmin in clip_spectrum should be smaller than wlmax.') wlc = wl[(wl >= wlmin) & (wl <= wlmax)]#This is the wavelength grid onto which we will interpolate the final result. fxc = fx[(wl >= wlmin) & (wl <= wlmax)] if pad > 0: wlmin_wide = wlmin/doppler(pad) wlmax_wide = wlmax*doppler(pad) wlc_wide = wl[(wl >= wlmin_wide) & (wl <= wlmax_wide)] fxc_wide = fx[(wl >= wlmin_wide) & (wl <= wlmax_wide)] return(wlc,fxc,wlc_wide,fxc_wide) else: return(wlc,fxc)
def input_tests_local(xp, yp, RpRs): """This wraps input tests for the local integrator functions, as these are the same in both cases.""" import lib.test as test #xp and yp should be ints or floats, and may not be NaN. test.typetest(xp, [int, float], varname='xp in build_local_spectrum_fast') test.typetest(yp, [int, float], varname='xp in build_local_spectrum_fast') test.nantest(xp, varname='xp in build_local_spectrum_fast') test.nantest(yp, varname='yp in build_local_spectrum_fast') #RpRs should be a non-negative float, and may not be NaN: test.typetest(RpRs, float, varname='RpRs in build_local_spectrum_fast') test.nantest(RpRs, varname='RpRs in build_local_spectrum_fast') test.notnegativetest(RpRs, varname='RpRs in build_local_spectrum_fast')
def calc_vel_stellar(x,y,i_stellar, vel_eq, diff_rot_rate, proj_obliquity): """ based on Cegla+2016. See Fig. 3 and equations 2 - 8 https://arxiv.org/pdf/1602.00322.pdf It takes the stellar parameters and then calculates the stellar velocities in all bins for one quarter of the stellar disk input: x, y: 1D numpy arrays to create the stellar grid in units of stellar radius i_stellar: inclination in degrees vel_eq: equatorial stellar velocity diff_rot_rate: differential rotation rate proj_obliquity: projected obliquity output: vel_stellar_grid: 2D numpy array of stellar velocities over one quarter of the stellar disk """ import numpy as np import pdb import lib.test as test #Standard tests on input test.typetest(x,np.ndarray,varname='x in calc_vel_stellar') test.typetest(y,np.ndarray,varname='x in calc_vel_stellar') test.typetest(i_stellar,float,varname='i_stellar in calc_vel_stellar') test.typetest(vel_eq,float,varname='vel_eq in calc_vel_stellar') test.typetest(diff_rot_rate,float,varname='diff_rot_rate in calc_vel_stellar') test.typetest(proj_obliquity,float,varname='proj_obliquity in calc_vel_stellar') test.nantest(x,varname='x in calc_vel_stellar') test.nantest(y,varname='y in calc_vel_stellar') test.nantest(i_stellar,varname='i_stellar in calc_vel_stellar') test.nantest(vel_eq,varname='vel_eq in calc_vel_stellar') test.nantest(diff_rot_rate,varname='diff_rot_rate in calc_vel_stellar') test.nantest(proj_obliquity,varname='proj_obliquity in calc_vel_stellar') test.notnegativetest(vel_eq,varname='vel_eq in calc_vel_stellar') #Careful! all angles have to be in radians! #Think carefully about which ones of these have to be transposed #convert angles to rad alpha = np.radians(proj_obliquity) beta = np.radians(i_stellar) #pre calculate matrices z,x_full,y_full = calc_z(x,y) #equation 8 from Cegla+2016 # vel_stellar_grid = (x_full*np.cos(alpha)-y_full*np.sin(alpha))*vel_eq*np.sin(beta)*(1.-diff_rot_rate*(z*np.sin(np.pi/2.-beta)+np.cos(np.pi/2.-beta)*(x_full*np.sin(alpha)-y_full*np.cos(alpha)))) #this has the star aligned to the coordinate system. Like this the projected obliquity is not incorporated here. The tilt of the star compared to the planet has to be incorporated in the planet's path. vel_stellar_grid = x_full*vel_eq*np.sin(beta)*(1.-diff_rot_rate*(z*np.sin(np.pi/2.-beta)+np.cos(np.pi/2.-beta)*y_full)) return(vel_stellar_grid)
def crop_spectrum(wl,fx,pad): """This crops a spectrum wl,fx to wlmin and wlmax. If pad !=0, it should be set to a positive velocity by which the output spectrum will be padded within wlmin,wlmax, to allow for velocity shifts. The difference with clip_spectrum above is that this routine pads towards the inside (only returning the narrow spectrum), while clip_spectrum pads towards the outside, returning both the cropped and the padded cropped spectra. Parameters ---------- wl : np.ndarray() The stellar model wavelength(s) in nm. fx : np.ndarray() The stellar model flux. pad: float, m/s A velocity corresponding to a shift around which the cropped array will be padded. Returns ------- wl,fx: np.array(), np.array() The wavelength and flux of the integrated spectrum. """ import lib.test as test import numpy as np wlmin = min(wl) wlmax = max(wl) #First run standard tests on input test.typetest(wl,np.ndarray,varname= 'wl in crop_spectrum') test.typetest(fx,np.ndarray,varname= 'fx in crop_spectrum') test.typetest(pad,[int,float],varname= 'pad in crop_spectrum') test.notnegativetest(pad,varname= 'pad in crop_spectrum') test.nantest(pad,varname= 'pad in crop_spectrum') if wlmin >= wlmax: raise ValueError('wlmin in crop_spectrum should be smaller than wlmax.') wlmin_wide = wlmin*doppler(pad)#Crop towards the inside wlmax_wide = wlmax/doppler(pad)#Crop towards the inside wlc_narrow = wl[(wl >= wlmin_wide) & (wl <= wlmax_wide)] fxc_narrow = fx[(wl >= wlmin_wide) & (wl <= wlmax_wide)] return(wlc_narrow,fxc_narrow)
def vactoair(wlnm): """Convert vaccuum to air wavelengths. Parameters ---------- wlnm : float, np.array() The wavelength(s) in nm. Returns ------- wl: float, np.array() The wavelengths. """ import lib.test as test import numpy #First run standard tests on the input test.typetest(wlnm,[int,float,numpy.ndarray],varname='wlnm in vactoair') test.notnegativetest(wlnm,varname='wlnm in vactoair') test.nantest(wlnm,varname='wlnm in vactoair') wlA = wlnm*10.0 s = 1e4/wlA f = 1.0 + 5.792105e-2/(238.0185e0 - s**2) + 1.67917e-3/( 57.362e0 - s**2) return(wlA/f/10.0)
def read_system(self,star_path='demo_star.txt',planet_path='demo_planet.txt',obs_path='demo_observations.txt'): """Reads in the stellar, planet and observation parameters from file; performing tests on the input and lifting the read variables to the class object. Parameters ---------- star_path : str Path to parameter file defining the star. planet_path: str Path to the parameter file defining the planet and its orbit. obs_path: str Path to the parameter file defining the timestamps of the observations. """ planetparams = open(planet_path,'r').read().splitlines() starparams = open(star_path,'r').read().splitlines() obsparams = open(obs_path,'r').read().splitlines() self.velStar = float(starparams[0].split()[0]) self.stelinc = float(starparams[1].split()[0]) self.drr = float(starparams[2].split()[0]) self.T = float(starparams[3].split()[0]) self.Z = float(starparams[4].split()[0]) self.logg = float(starparams[5].split()[0]) self.u1 = float(starparams[6].split()[0]) self.u2 = float(starparams[7].split()[0]) self.mus = float(starparams[8].split()[0]) self.R = float(starparams[9].split()[0]) self.sma_Rs = float(planetparams[0].split()[0]) self.ecc = float(planetparams[1].split()[0]) self.omega = float(planetparams[2].split()[0]) self.orbinc = float(planetparams[3].split()[0]) self.pob = float(planetparams[4].split()[0])#Obliquity. self.Rp_Rs = float(planetparams[5].split()[0]) self.orb_p = float(planetparams[6].split()[0]) self.transitC = float(planetparams[7].split()[0]) self.mode = planetparams[8].split()[0]#This is a string. times = [] #These are in JD-24000000.0 or in orbital phase. self.exptimes = [] for i in obsparams: times.append(float(i.split()[0])) self.times = np.array(times) self.Nexp = len(self.times)#Number of exposures. if self.mode == 'times': for i in obsparams: self.exptimes.append(float(i.split()[1])) self.exptimes = np.array(self.exptimes) try: test.typetest(self.wave_start,float,varname='wave_start in input') test.nantest(self.wave_start,varname='wave_start in input') test.notnegativetest(self.wave_start,varname='wave_start in input') test.notnegativetest(self.velStar,varname='velStar in input') test.notnegativetest(self.stelinc,varname='stelinc in input') #add all the other input parameters except ValueError as err: print("Parser: ",err.args) if self.mus != 0: self.mus = np.linspace(0.0,1.0,self.mus)#Uncomment this to run in CLV mode with SPECTRUM.
def airtovac(wlnm): """Convert air to vaccuum wavelengths. Parameters ---------- wlnm : float, np.array() The wavelength(s) in nm. Returns ------- wl: float, np.array() The wavelengths. """ import lib.test as test import numpy #First run standard tests on the input test.typetest(wlnm,[int,float,numpy.ndarray],varname='wlnm in airtovac') test.notnegativetest(wlnm,varname='wlnm in airtovac') test.nantest(wlnm,varname='wlnm in airtovac') #Would still need to test that wl is in a physical range. wlA=wlnm*10.0 s = 1e4 / wlA n = 1 + 0.00008336624212083 + 0.02408926869968 / (130.1065924522 - s**2) + 0.0001599740894897 / (38.92568793293 - s**2) return(wlA*n/10.0)
def read_spectrum(T, logg, metallicity=0.0, alpha=0.0): """Wrapper for the function get_spectrum() above, that checks that the input T, log(g), metallicity and alpha are in accordance with what is provided by PHOENIX (as of November 1st, 2019), and passes them on to get_spectrum(). Parameters ---------- T : int,float The model photosphere temperature. Acceptable values are: 2300 - 7000 in steps of 100, and 7200 - 12000 in steps of 200. logg : int,float The model log(g) value. Acceptable values are: 0.0 - 6.0 in steps of 0.5. metallicity : int,float (optional, default = 0) The model metallicity [Fe/H] value. Acceptable values are: -4.0, -3.0, -2.0 and -1.5 to +1.0 in steps of 0.5. If no location is given, the ``location`` attribute of the Time object is used. alpha : int,float (optional, default = 0) The model alpha element enhancement [alpha/M]. Acceptable values are: -0.2 to 1.2 in steps of 0.2, but only for Fe/H of -3.0 to 0.0. Returns ------- wl,f : np.array(),np.array() The wavelength (nm) and flux (erg/s/cm^2/cm) axes of the requested spectrum. """ import numpy as np import sys import astropy.io.fits as fits import lib.test as test phoenix_factor = np.pi #This is a factor to correct the PHOENIX spectra to produce the correctly calibrated output flux. #If you compare PHOENIX to SPECTRUM, a factor of 3 seems to be missing. Brett Morris encountered this problem when trying #to use PHOENIX to predict the received flux of real sources, to construct an ETC. He also needed a factor of 3, which he #interpreted as a missing factor of pi. So pi it is, until something else is deemed better (or until PHOENIX updates #their grid). #First run standard tests on the input: test.typetest(T, [int, float], varname='T in read_spectrum') test.typetest(logg, [int, float], varname='logg in read_spectrum') test.typetest(metallicity, [int, float], varname='metallicity in read_spectrum') test.typetest(alpha, [int, float], varname='alpha in read_spectrum') test.nantest(T, varname='T in get_spectrum') test.nantest(logg, varname='logg in get_spectrum') test.nantest(metallicity, varname='Z in get_spectrum') test.nantest(alpha, varname='a in get_spectrum') test.notnegativetest(T, varname='T in get_spectrum') #These contain the acceptable values. T_a = np.concatenate((np.arange(2300, 7100, 100), np.arange(7200, 12200, 200))) logg_a = np.arange(0, 6.5, 0.5) FeH_a = np.concatenate((np.arange(-4, -1, 1), np.arange(-1.5, 1.5, 0.5))) alpha_a = np.arange(0, 1.6, 0.2) - 0.2 #Check that the input is contained in them: if T in T_a and metallicity in FeH_a and alpha in alpha_a and logg in logg_a: if (metallicity < -3 or metallicity > 0.0) and alpha != 0: print( 'Error: Alpha element enhancement != 0 only available for -3.0<[Fe/H]<0.0' ) sys.exit() #If so, retrieve the spectra: wavename, specname = get_spectrum(T, logg, metallicity, alpha) f = fits.getdata(specname) w = fits.getdata(wavename) return (w / 10.0, f / phoenix_factor) #Implicit unit conversion here. else: print( 'Error: Provided combination of T, log(g), Fe/H and a/M is out of bounds.' ) print('T = %s, log(g) = %s, Fe/H = %s, a/M = %s' % (T, logg, metallicity, alpha)) print('The following values are accepted:') print('T:') print(T_a) print('Log(g):') print(logg_a) print('Fe/H:') print(FeH_a) print('a/M:') print(alpha_a) print( 'Alpha element enhancement != 0 only available for -3.0<[Fe/H]<0.0' ) sys.exit()
def get_spectrum(T, logg, Z, a): """Querying a PHOENIX photosphere model, either from disk or from the PHOENIX website if the spectrum hasn't been downloaded before, and returning the names of the files in the data subfolder. These may then be read by the wrapper function read_spectrum() below. However, if limbs is set to a positive value, the code is switched to a mode in which the CLV effect is taken into account. In this event, PHOENIX spectra cannot be used, and the SPECTRUM package will be used to calculate spectra. For this functionality to work, the user needs to have built the SPECTRUM source code (included in this distribution). See the documentation for instructions. Parameters ---------- T : int, float The model photosphere temperature. Acceptable values are: 2300 - 7000 in steps of 100, and 7200 - 12000 in steps of 200. logg : int, float The model log(g) value. Acceptable values are: 0.0 - 6.0 in steps of 0.5. Z : int, float The model metallicity [Fe/H] value. Acceptable values are: -4.0, -3.0, -2.0 and -1.5 to +1.0 in steps of 0.5. a : int, float The model alpha element enhancement [alpha/M]. Acceptable values are: -0.2 to 1.2 in steps of 0.2, but only for Fe/H of -3.0 to 0.0. Returns ------- wlname, specname: str, str The names of the files containing the wavelength and flux axes of the requested spectrum. """ import requests import shutil import urllib.request as request from contextlib import closing import sys import os.path import lib.test as test #First run standard tests on the input test.typetest(T, [int, float], varname='T in get_spectrum') test.typetest(logg, [int, float], varname='logg in get_spectrum') test.typetest(Z, [int, float], varname='metallicity in get_spectrum') test.typetest(a, [int, float], varname='alpha in get_spectrum') test.nantest(T, varname='T in get_spectrum') test.nantest(logg, varname='logg in get_spectrum') test.nantest(Z, varname='Z in get_spectrum') test.nantest(a, varname='a in get_spectrum') test.notnegativetest(T, varname='T in get_spectrum') #This is where phoenix spectra are located. root = 'ftp://phoenix.astro.physik.uni-goettingen.de/HiResFITS/' #We assemble a combination of strings to parse the user input into the URL, z_string = '{:.1f}'.format(float(Z)) if Z > 0: z_string = '+' + z_string else: z_string = '-' + z_string a_string = '' if a > 0: a_string = '.Alpha=+' + '{:.2f}'.format(float(a)) if a < 0: a_string = '.Alpha=' + '{:.2f}'.format(float(a)) t_string = str(int(T)) if T < 10000: t_string = '0' + t_string g_string = '-' + '{:.2f}'.format(float(logg)) #These are URLS for the input files. waveurl = root + 'WAVE_PHOENIX-ACES-AGSS-COND-2011.fits' specurl = root + 'PHOENIX-ACES-AGSS-COND-2011/Z' + z_string + a_string + '/lte' + t_string + g_string + z_string + a_string + '.PHOENIX-ACES-AGSS-COND-2011-HiRes.fits' #These are the output filenames, they will also be returned so that the wrapper #of this function can take them in. wavename = 'data/PHOENIX/WAVE.fits' if os.path.isdir('data/PHOENIX/') == False: os.makedirs('data/PHOENIX/') specname = 'data/PHOENIX/lte' + t_string + g_string + z_string + a_string + '.PHOENIX-ACES-AGSS-COND-2011-HiRes.fits' #If it doesn't already exists, we download them, otherwise, we just pass them on: if os.path.exists(wavename) == False: with closing(request.urlopen(waveurl)) as r: with open(wavename, 'wb') as f: shutil.copyfileobj(r, f) if os.path.exists(specname) == False: print(specurl) with closing(request.urlopen(specurl)) as r: with open(specname, 'wb') as f: shutil.copyfileobj(r, f) return (wavename, specname)