proposal-simulation-tool / simulating_spectra.py
simulating_spectra.py
Raw
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Thu Jul 23 17:31:30 2020

@author: jonty/sascha
"""

import numpy as np
import matplotlib.pyplot as plt
from astropy.io import ascii
from extinction import apply, fitzpatrick99
from scipy import interpolate
import math
import glob
from astropy.io import fits
from spectres import spectres

# constants
pc  = 3.086e16 # m
rsol = 696340e3 # m

#Define stellar spectrum

#
# Here we want a generalised function to read in a stellar spectrum from the subfolder.
#
#B0V T = 30000 K, log g = 4, Rstar = 10.0 Rsol
#B5V T = 15200 K, log g = 4, Rstar = 3.9 Rsol
#B9V T = 10600 K, log g = 4, Rstar = 2.7 Rsol
#B2V T = 20600 K, log g = 4, Rstar = 3.85 Rsol
#O8V T = 35000 K, log g = 4, Rstar = 8.75 Rsol


def read_stellar_spectrum(dstar=1000,spectrum='./stellar_spectra/BT-NEXTGEN_Photosphere_Teff_15000_logg_4.0_Rstar_3.9.spec'):
    spec_trim = spectrum[:-5]
    spec_split = spec_trim.split('_')
    #print(spec_split)
    tstar = float(spec_split[4])
    logg  = float(spec_split[6])
    rstar = float(spec_split[8])

    data = ascii.read(spectrum,comment='#',names=['Wave','Flux'])

    wavelengths =  data['Wave'].data #Angstroms
    model_spect =  data['Flux'].data #Ergs/cm**2/s/A
    if spec_split[0] == 'BOSZ':
        model_spect *= np.pi #factor of pi *only* as already scaled by 4x

    wav_um = wavelengths / 1e4
    flx_mjy = model_spect *  1e3 * wavelengths**2  * 3.33564095E+04 * ((rstar*rsol)**2 / (dstar*pc)**2) #coversion to mJy

    return wav_um,flx_mjy


# get list of stellar spectra filenames from subfolder

spectra_list = glob.glob("./stellar_spectra/*.spec")

# print list of filenames

for i in range(len(spectra_list)):
    print(i,spectra_list[i])

# ask user for desired spectrum, distance

spec_index  = int(input("Please enter index of desired stellar spectrum:"))
spec = spectra_list[spec_index]

dstar = float(input("Please enter the distance (in pc) of the star:"))

# read in stellar spectrum
wav_um, flx_mjy = read_stellar_spectrum(dstar,spec)
wav_aa = wav_um*1e4 # wavelengths in angstroms for the extinction calculation

spectrum_total_int = 0

# Set the depletion of silicon
depletion_si = float(input("Please enter the depletion of silicon:"))

# Set the depletion of carbon
depletion_carbon = float(input("Please enter the depletion of carbon:"))

# get list of stellar spectra filenames from subfolder

ice_list = glob.glob("./water_ice/*.q")
print(ice_list)
# print list of filenames
for i in range(len(ice_list)):
    print(i,ice_list[i])

water_ice_yesno = float(input("Please enter 0 for no water ice and 1 for including water ice (10 ppm abundance):"))
if water_ice_yesno==1:
    ice_index = input("Please enter index of desired ice component:")

    # Set the abundance of ices in the diffuse ISM

    ice_species=[]
    ice_species_string = ""
    ice_species.append(ice_list[int(ice_index)])
    ice_species_string = ice_species[0]#[12:]+"_"


# Set the fraction of small silicates
small_silicates_fraction = float(input("Please enter the fraction of silicates in nano silicates (recommended 0.03-0.1):"))

#
# Here we want a function to select the dust component(s) we want to add to the extincted stellar model
#
def read_dust_qext(species='mg05fe05sio3_am_cde01.q'):
    # read in the Q values
    data_dust=np.loadtxt(species)

    wavelengths_q=data_dust[:,0]
    Qext=data_dust[:,1]

    return wavelengths_q, Qext

# Here we read the properties, e.g. density and molmass of the dust species

def read_dust_prop(species='mg05fe05sio3_am_cde01.q'):
    # read in the Q values
    data_dust=np.loadtxt(species)
    sample = open('./dust_qext/silicate_sample.txt','r')
    linessample=sample.readlines()
    sample_column_number = 0
    resultsample=[]
    for x in linessample:
        resultsample.append(x.split()[sample_column_number])
    sample.close()
    #print(resultsample)

    index = resultsample.index(species)
    name,density,molmass=linessample[index].split()

    return density, molmass

def read_ice_prop(ice_species='water_ice_cde01.q'):
    # read in the Q values
    data_dust=np.loadtxt(ice_species)
    ice_sample = open('./water_ice/ice_sample.txt','r')
    linessample=ice_sample.readlines()
    sample_column_number = 0
    resultsample=[]
    for x in linessample:
        resultsample.append(x.split()[sample_column_number])
    ice_sample.close()
    #print(resultsample)

    index = resultsample.index(ice_species)
    name,ice_density, ice_grain_radius =linessample[index].split()

    return ice_density, ice_grain_radius

#
# Calculate number of atoms along line of sight based on extinction
#
def calculate_Nd(Av,rho,gmol,fraction, depletion_si):
    NH=1.9e21*Av
    si_abundance=3.4e-5
    # total amount of silicon atoms available
    si_los=NH*si_abundance*depletion_si*(1.0-small_silicates_fraction)
    si_los_specie=si_los*fraction
    # calculate the silicon atoms per particle
    #rho=3.2 # check per composition
    rho=rho
    V=4./3.*math.pi*(0.1*1e-4)**3
    grams=V*rho
    gmol=gmol
    #gmol=100. # check per composition
    mol=grams/gmol
    Avogadro=6.0221409e+23
    si_atoms_per_particle=mol*Avogadro
    Nd=si_los/si_atoms_per_particle

    # this needs to be adapted, because it will be different for different species
    # also has the assumption that all the silicon is locked up in dust

    print('Nd',Nd)
    return Nd

def water_ice(Av, ice_grain_radius, rho_ice):#grain radius can be where this code breaks! not yet added
    NH=1.9e21*Av
    water_ice_abundance=10e-6  # water molecules per
    ice_los=NH*water_ice_abundance
    # 0.1 micron grains
    rho_ice=float(rho_ice) # check per composition
    V=4./3.*math.pi*(float(ice_grain_radius)*1e-4)**3
    grams=V*rho_ice
    gmol=18.015281
    mol=grams/gmol
    Avogadro=6.0221409e+23
    water_molecules_per_particle=mol*Avogadro
    N_waterice=ice_los/water_molecules_per_particle
    return N_waterice

def calculate_hydrocarbon_spectrum(Av, depletion_carbon):
    NH=1.9e21*Av
    carbon_abundance=3.91e-4 # from tielens book
    # total amount of silicon atoms available
    carbon_dust_col=carbon_abundance*depletion_carbon*NH
    abs_strength=[24.3, 15.2, 23.7, 14.8, 2.58,0.275,0.275,0.984629]         # from paper Jean Chiar
    # fractions for hydrocarbon
    fractions=np.loadtxt('./hydrocarbon/fractions_chx2.txt')
    wavelength_carbon=np.arange(2,40.0,0.001)
    # read fits file with all the gaussians
    hdulist = fits.open('./hydrocarbon/spectrum_34_micron2.fits')
    hdu = hdulist[0]
    data_gauss34 = fits.getdata('./hydrocarbon/spectrum_34_micron2.fits')
    print(data_gauss34.shape)
    spectrum=np.empty(data_gauss34.shape)

    for i in range(len(fractions)):

        spectrum[:,i]=data_gauss34[:,i]*abs_strength[i]*1e-18*fractions[i]*carbon_dust_col

    tau_total=np.sum(spectrum, axis=1)

    # this needs to be adapted, because it will be different for different species
    # also has the assumption that all the silicon is locked up in dust

    #plt.plot(wavelength_carbon,tau_total)
    return wavelength_carbon, tau_total

def small_silicates(Av,depletion_si,small_silicates_fraction):
    # this returns the optical depth from small silicates
    # the silicates are a mix between olivine and pyroxene grains, we will assume it is 50/50 for now
    NH=1.9e21*Av
    si_abundance=3.9e-5
    si_los=NH*si_abundance*depletion_si*small_silicates_fraction

    si_atoms_per_unit_py=271

    Nd_py=(si_los)/si_atoms_per_unit_py

    data_smallsil_py=np.loadtxt('./nanosil_cross_sec/pyro_0_cross.txt')

    #tau_small_sil_ol=data_smallsil_ol[:,1]*Nd_ol
    tau_small_sil_py=data_smallsil_py[:,1]*Nd_py

    tau_total_sms=tau_small_sil_py

    return data_smallsil_py[:,0], tau_total_sms

#generate noise for a spectrum
def spec_noise(spectrum,noise_para=[0.0,1.0],noise_type='Poisson'):

    if noise_type == 'Poisson':
        noise = np.zeros((len(spectrum)))

        for k in range(len(noise)):
            noise[k] = 1e-3*np.sqrt(1e3*spectrum[k])*np.random.uniform(-1,1)

    if noise_type == 'Gaussian':
        mu=noise_para[0]
        sigma = noise_para[1]
        noise = np.zeros((len(spectrum)))

        for k in range(len(noise)):
           noise[k] = spectrum[k]*np.random.normal(mu,sigma)

    return noise

# get list of stellar spectra filenames from subfolder

dust_list = glob.glob("./dust_qext/*.q")
print(dust_list)
# print list of filenames
for i in range(len(dust_list)):
    print(i,dust_list[i])

# ask user for desired spectrum, distance

dust_index  = input("Please enter index(s) of desired dust component(s), separated by commas (no spaces e.g. '1,2,3'):")

dust_split = dust_index.split(',')

species=[]
species_string = ""
for i in range(len(dust_split)):
    species.append(dust_list[int(dust_split[i])])
    species_string += species[i][12:]+"_"
Nspecies = len(species)

print("Using ",Nspecies," dust species to generate features on reddened stellar spectrum.")

##################################
dust_fraction  = input("Please enter the fractions(s) of the desired dust component(s), separated by commas (no spaces e.g. '0.2,0.2,0.6', must add up to 1):")

dust_split_fraction = dust_fraction.split(',')
fraction=np.empty(len(dust_split_fraction))

for i in range(len(dust_split_fraction)):
    fraction[i]=float(dust_split_fraction[i])

# Here we define the extinction properties of the ISM
#Avs = [1.0,3.0,5.0,6.5,8.0,10.0,15.0] # in mags
#Avs = [4.0,5.5,7.0] # in mags
Avs = [7.0] # in mags


Rv = 3.1 # for MW

# create list of filenames based on material composition and extinction
filename_array = []
for i in range(len(Avs)):
    filename_array.append("extinction_"+species_string+"av"+str(Avs[i])+"_"+str(int(dstar/1000))+"kpc.txt")

#filename_array=["extinction_enstatite_av1_1kpc.txt","extinction_enstatite_av3_1kpc.txt","extinction_enstatite_av5_1kpc.txt","extinction_enstatite_av8_1kpc.txt","extinction_enstatite_av10_1kpc.txt","extinction_enstatite_av15_1kpc.txt"]

#Apply extinction models
for i in range(len(Avs)):
    Av = Avs[i]
    spectrum = apply(fitzpatrick99(wav_aa,Av,Rv),flx_mjy)

    fig = plt.figure(figsize=(8,6))
    ax = fig.add_subplot(1, 1, 1)
    ax.set_xlabel(r'Wavelength [$\mu$m]',fontsize=16)
    ax.set_ylabel(r'Flux density [Jy]',fontsize=16)
    ax.set_ylim(1e-5,1e3)
    ax.set_xlim(1e-2,1e3)
    ax.loglog(wav_um,spectrum,color='blue')
    ax.loglog(wav_um,flx_mjy,color='red')
    plt.show()
    plt.close()
    print(np.min(wav_um),np.max(wav_um))
    # wavelength for interpolation
    wavelength_int=np.arange(0.1,40.0,0.001)
    spectrum3_int = spectres(wavelength_int,wav_um,spectrum)
    #interpfunc_spectrum=interpolate.interp1d(wav_um,spectrum)
    #spectrum3_int=interpfunc_spectrum(wavelength_int)

    #interpfunc_model=interpolate.interp1d(wav_um,flx_mjy)
    #model_int=interpfunc_model(wavelength_int)

    model_int=spectres(wavelength_int,wav_um,flx_mjy)

    # optical depth array

    tau_cont=np.log(spectrum3_int/model_int)*-1.

    wavelength_int_q=np.arange(2,40.0,0.001)
    tau_int=spectres(wavelength_int_q,wavelength_int,tau_cont)
    model_int2=spectres(wavelength_int_q,wav_um,flx_mjy)

    #tau_cont=np.log(spectrum3_int/model_int)*-1.
    #wavelength_int_q=np.arange(2,40.0,0.001)
    #tau_int_func=interpolate.interp1d(wavelength_int,tau_cont)
    #tau_int=tau_int_func(wavelength_int_q)
    #model_int2=interpfunc_model(wavelength_int_q)

    tau_lab=np.empty([len(species),len(wavelength_int_q)])

    teller=0

    for specie in species:

        wavelengths_q, Qext = read_dust_qext(specie)

        rho, molmass = read_dust_prop(specie)
        rho=float(rho)
        molmass=float(molmass)
        # interpolating lab data
        interpfunc_Qext=interpolate.interp1d(wavelengths_q,Qext)
        Qext_int=interpfunc_Qext(wavelength_int_q)

        # How many dust particles do we have along the line of sight for a certain Av value?
        #Nd = calculate_Nd(Av)
        Nd=calculate_Nd(Av, rho, molmass, fraction, depletion_si)

        # Now we need the lab spectrum part of the intensity
        tau_lab[teller,:]=Qext_int*math.pi*(0.1*1e-4)**2*Nd*fraction[teller]

        teller=teller+1

    tau_lab=np.sum(tau_lab, axis=0)

    # Here we should add the carbon features
    # how did we integrate things ... , perhaps just expand 2-3 to -40 micron
    wavelength_carbon, tau_carbon=calculate_hydrocarbon_spectrum(Av,depletion_carbon)

    # water ice
    # here we add tau from water ice
    # THIS NEEDS THE OPTIONS TO CHOOSE DIFFERENT COMPOSITIONS!!!!!
    if water_ice_yesno==1:
        data_waterice = np.loadtxt(ice_species[0])
        wavelengths_q_waterice=data_waterice[:,0]
        Qext_waterice=data_waterice[:,1]
        # interpolating lab data
        interpfunc_Qext_waterice=interpolate.interp1d(wavelengths_q_waterice,Qext_waterice)
        Qext_int_waterice=interpfunc_Qext_waterice(wavelength_int_q)

        rho_ice, ice_grain_radius = read_ice_prop(ice_species[0])
        N_waterice=water_ice(Av, ice_grain_radius,rho_ice)
        tau_waterice=Qext_int_waterice*math.pi*(float(ice_grain_radius)*1.0e-4)**2*N_waterice
    elif water_ice_yesno==0:
        tau_waterice=0

    # small silicates
    wavelength_small_silicates, tau_small_silicates=small_silicates(Av,depletion_si,small_silicates_fraction)

    tau_smallsil = spectres(wavelength_int_q,wavelength_small_silicates,tau_small_silicates)
    tau_smallsil[np.isnan(tau_smallsil)] = 0

    # interpolating lab data

# now we need to bring back the original stellar spectrum
    tau=tau_int+tau_lab+tau_carbon+tau_waterice+tau_smallsil
    intensity=model_int2*np.exp(-1*(tau))
    wavelength_int_where=np.where(wavelength_int<2)
    wavelength_int_again=wavelength_int[wavelength_int_where]
    spectrum_again=spectrum3_int[wavelength_int_where]

    spectrum_total=np.concatenate((spectrum_again,intensity))
    wavelength_total=np.concatenate((wavelength_int_again,wavelength_int_q))

    spectrum_total_func=interpolate.interp1d(wavelength_total,spectrum_total,fill_value="extrapolate")
    spectrum_total_int = spectrum_total_func(wavelength_int)
    # Adding noise to the spectrum
    noise = spec_noise(spectrum_total_int,noise_para=None,noise_type='Poisson') #Noise = sqrt(Signal)
    #noise = spec_noise(spectrum_total_int,noise_para=[0.0,0.10],noise_type='Gaussian') #Noise = 10% Gaussian uncertainty

    spectrum_total_int_noisy = spectrum_total_int + noise

    # we now also need to consider binning and the
    # rebinning the spectrum
    #JWST MIRSPEC resolutions by filter:
    #https://jwst-docs.stsci.edu/mid-infrared-instrument/miri-observing-modes/miri-medium-resolution-spectroscopy#MIRIMediumResolutionSpectroscopy-wavelengthMRSwavelengthcoverage

    #lam_min, lam_max, avg. spec. resln.
    spectral_resolution = {'nirspec_prism' :[0.6,5.0,100],
                           'nirspec_lores' :[0.6,5.0,1000],
                           'nirspec_hires' :[0.6,5.0,2700],
                           'mirispec_ch1_s':[4.88,5.75,3515],
                           'mirispec_ch1_m':[5.63,6.63,3470],
                           'mirispec_ch1_l':[6.41,7.52,3355],
                           'mirispec_ch2_s':[7.48,8.76,3050],
                           'mirispec_ch2_m':[8.71 ,10.23,2960],
                           'mirispec_ch2_l':[10.02,11.75,3080],
                           'mirispec_ch3_s':[11.52,13.49,2705],
                           'mirispec_ch3_m':[13.36,15.65,2215],
                           'mirispec_ch3_l':[15.43,18.08,2385],
                           'mirispec_ch4_s':[17.65,20.94,1695],
                           'mirispec_ch4_m':[20.41,24.22,1725],
                           'mirispec_ch4_l':[23.88,28.34,1495]}

    #empty arrays for the final spectrum
    jwst_spec_wave = np.asarray([])
    jwst_spec_flux = np.asarray([])

    for key in spectral_resolution:
        channel = spectral_resolution[key]

        new_waves = np.arange(channel[0],channel[1],
                                (channel[1]-0.5*(channel[1]-channel[0]))/channel[2])
        new_waves = np.append(new_waves,channel[1])
        new_fluxs = spectres(new_waves,wavelength_int,spectrum_total_int_noisy)

        jwst_spec_wave = np.append(jwst_spec_wave,new_waves)
        jwst_spec_flux = np.append(jwst_spec_flux,new_fluxs)

    #plot figures

    fig = plt.figure(figsize=(8,6))
    ax = fig.add_subplot(1, 1, 1)
    ax.set_xlabel(r'Wavelength [$\mu$m]',fontsize=16)
    ax.set_ylabel(r'Flux density [mJy]',fontsize=16)
    ax.set_ylim(1e-1,1e3)
    ax.set_xlim(1,40)
    ax.loglog(wavelength_int,spectrum_total_int,color='blue')
    ax.loglog(wav_um,flx_mjy,color='red')
    ax.text(20,5e2,r'$A_{V}$ = '+str(Av),fontsize=16)

    fig = plt.figure(figsize=(8,6))
    ax = fig.add_subplot(1, 1, 1)
    ax.set_xlabel(r'Wavelength [$\mu$m]',fontsize=16)
    ax.set_ylabel(r'Relative intenstity (compared to photosphere)',fontsize=16)
    ax.set_ylim(1e-1,1.1e0)
    ax.set_xlim(1,40)
    ax.loglog(wavelength_int,spectrum_total_int_noisy/spectrum3_int,color='green')
    ax.loglog(wavelength_int,spectrum_total_int/spectrum3_int,color='blue')
    ax.loglog(wavelength_int,model_int/model_int,color='red')
    ax.text(20,0.7,r'$A_{V}$ = '+str(Av),fontsize=16)
    plt.show()
    plt.close()

    fig = plt.figure(figsize=(8,6))
    ax = fig.add_subplot(1, 1, 1)
    ax.set_xlabel(r'Wavelength [$\mu$m]',fontsize=16)
    ax.set_ylabel(r'Flux density [mJy]',fontsize=16)
    ax.set_ylim(1e-1,1e3)
    ax.set_xlim(1,40)
    ax.set_xscale('log')
    ax.set_yscale('log')
    ax.loglog(wavelength_int,spectrum3_int,color='black')
    cval = np.arange(0,1,1/len(spectral_resolution))
    nk = 0
    for keys in spectral_resolution:
        #cval = (nk+1)/len(spectral_resolution)
        lam_lo = spectral_resolution[keys][0]
        lam_hi = spectral_resolution[keys][1]
        filt = np.where((jwst_spec_wave >= lam_lo) & (jwst_spec_wave <= lam_hi))
        ax.scatter(jwst_spec_wave[filt],jwst_spec_flux[filt], cmap='rainbow',linestyle='-',marker='.',linewidth=0.5)
        nk += 1
    ax.text(20,5e2,r'$A_{V}$ = '+str(Av),fontsize=16)

    plt.show()
    plt.close()


    np.savetxt('./results/'+filename_array[i], np.c_[wavelength_int,spectrum_total_int/spectrum3_int], delimiter=' ')