Source code for pyspeckit.spectrum.models.lte_molecule

"""
LTE Molecule Modeling Tool
==========================

Uses astroquery & vamdclib to obtain molecular parameters.
http://astroquery.readthedocs.io/en/latest/splatalogue/splatalogue.html

Equations are based on Mangum & Shirley 2015 (2015PASP..127..266M)

Module API
^^^^^^^^^^
"""
from __future__ import print_function
import numpy as np
from astropy import units as u
from astropy import constants
from .model import SpectralModel

kb_cgs = constants.k_B.cgs.value
h_cgs = constants.h.cgs.value
eightpicubed = 8 * np.pi**3
threehc = 3 * constants.h.cgs * constants.c.cgs
hoverk_cgs = (h_cgs/kb_cgs)
c_cgs = constants.c.cgs.value

[docs]def line_tau(tex, total_column, partition_function, degeneracy, frequency, energy_upper, einstein_A=None): """ Given the excitation temperature of the state, total column density of the molecule, the partition function, the degeneracy of the state, the frequency of the state, and the upper-state energy level, return the optical depth of that transition. This is a helper function for the LTE molecule calculations. It implements the equations .. math:: \\tau_\\nu = \\frac{c^2}{8 \pi \\nu^2} A_{ij} N_u \\exp\left( \\frac{h \\nu}{k_B T_{ex}}\\right) .. math:: N_{u} = N_{tot} \\frac{g_u}{Q} \\exp\left(\\frac{-E_u}{k_B T_{ex}} \\right) based on Equation 29 of Mangum & Shirley 2015 (2015PASP..127..266M) """ # don't use dipole moment, because there are extra hidden dependencies assert frequency.unit.is_equivalent(u.Hz) assert energy_upper.unit.is_equivalent(u.erg) assert total_column.unit.is_equivalent(u.cm**-2) assert tex.unit.is_equivalent(u.K) N_upper = (total_column * degeneracy / partition_function * np.exp(-energy_upper / (constants.k_B * tex))) # equation 29 in Mangum 2015 #taudnu = (eightpicubed * frequency * dipole_moment**2 / threehc * # (np.exp(frequency*h_cgs/(kb_cgs*tex))-1) * N_upper) assert einstein_A.unit.is_equivalent(u.Hz) taudnu = ((constants.c**2/(8*np.pi*frequency**2) * einstein_A * N_upper)* (np.exp(frequency*constants.h/(constants.k_B*tex))-1)) return taudnu.decompose()
# Deprecated version of the above # def line_tau_nonquantum(tex, total_column, partition_function, degeneracy, # frequency, energy_upper, SijMu2=None, molwt=None): # # assert frequency.unit.is_equivalent(u.Hz) # assert energy_upper.unit.is_equivalent(u.erg) # assert total_column.unit.is_equivalent(u.cm**-2) # assert tex.unit.is_equivalent(u.K) # # energy_lower = energy_upper - frequency*constants.h # #N_lower = (total_column * degeneracy / partition_function * # # np.exp(-energy_lower / (constants.k_B * tex))) # # # http://www.cv.nrao.edu/php/splat/OSU_Splat.html # assert SijMu2.unit.is_equivalent(u.debye**2) # amu = u.Da # assert molwt.unit.is_equivalent(amu) # C1 = 54.5953 * u.nm**2 * u.K**0.5 / amu**0.5 / u.debye**2 # C2 = 4.799237e-5 * u.K / u.MHz # C3 = (1.43877506 * u.K / ((1*u.cm).to(u.Hz, u.spectral()) * constants.h)).to(u.K/u.erg) # #C3 = (constants.h / constants.k_B).to(u.K/u.erg) # tau = total_column/partition_function * C1 * (molwt/tex)**0.5 * (1-np.exp(-C2*frequency/tex)) * SijMu2 * np.exp(-C3*energy_lower/tex) # # return tau.decompose()
[docs]def line_tau_cgs(tex, total_column, partition_function, degeneracy, frequency, energy_upper, einstein_A): """ Given the excitation temperature of the state, total column density of the molecule, the partition function, the degeneracy of the state, the frequency of the state, and the upper-state energy level, return the optical depth of that transition. Unlike :func:`line_tau`, this function requires inputs in CGS units. This is a helper function for the LTE molecule calculations. It implements the equations .. math:: \\tau_\\nu = \\frac{c^2}{8 \pi \\nu^2} A_{ij} N_u \\exp\left( \\frac{h \\nu}{k_B T_{ex}}\\right) .. math:: N_{u} = N_{tot} \\frac{g_u}{Q} \\exp\left(\\frac{-E_u}{k_B T_{ex}} \\right) based on Equations 11 and 29 of Mangum & Shirley 2015 (2015PASP..127..266M) """ N_upper = (total_column * degeneracy / partition_function * np.exp(-energy_upper / (kb_cgs * tex))) # equation 29 in Mangum 2015 #taudnu = (eightpicubed * frequency * dipole_moment**2 / threehc * # (np.exp(frequency*h_cgs/(kb_cgs*tex))-1) * N_upper) # substitute eqn 11: # Aij = 64 pi^4 nu^3 / (3 h c^3) |mu ul|^2 # becomes # dipole_moment^2 = 3 h c^3 Aij / ( 64 pi^4 nu^3 ) taudnu = ((c_cgs**2/(8*np.pi*frequency**2) * einstein_A * N_upper)* (np.exp(frequency*h_cgs/(kb_cgs*tex))-1)) return taudnu
[docs]def Jnu(nu, T): """RJ equivalent temperature (MS15 eqn 24)""" return constants.h*nu/constants.k_B / (np.exp(constants.h*nu/(constants.k_B*T))-1)
[docs]def Jnu_cgs(nu, T): """RJ equivalent temperature (MS15 eqn 24) (use cgs constants for speed) """ return hoverk_cgs*nu / (np.exp(hoverk_cgs*nu/T)-1)
def line_brightness(tex, dnu, frequency, tbg=2.73*u.K, *args, **kwargs): tau = line_tau(tex=tex, frequency=frequency, *args, **kwargs) / dnu tau = tau.decompose() assert tau.unit == u.dimensionless_unscaled return (Jnu(frequency, tex)-Jnu(frequency, tbg)).decompose() * (1 - np.exp(-tau)) def line_brightness_cgs(tex, dnu, frequency, tbg=2.73, *args, **kwargs): tau = line_tau(tex=tex, frequency=frequency, *args, **kwargs) / dnu return (Jnu(frequency, tex)-Jnu(frequency, tbg)) * (1 - np.exp(-tau)) # requires vamdc branch of astroquery
[docs]def get_molecular_parameters(molecule_name, molecule_name_vamdc=None, tex=50, fmin=1*u.GHz, fmax=1*u.THz, line_lists=['SLAIM'], chem_re_flags=0, **kwargs): """ Get the molecular parameters for a molecule from the CDMS database using vamdclib Parameters ---------- molecule_name : string The string name of the molecule (normal name, like CH3OH or CH3CH2OH) molecule_name_vamdc : string or None If specified, gives this name to vamdc instead of the normal name. Needed for some molecules, like CH3CH2OH -> C2H5OH. tex : float Optional excitation temperature (basically checks if the partition function calculator works) fmin : quantity with frequency units fmax : quantity with frequency units The minimum and maximum frequency to search over line_lists : list A list of Splatalogue line list catalogs to search. Valid options include SLAIM, CDMS, JPL. Only a single catalog should be used to avoid repetition of transitions and species chem_re_flags : int An integer flag to be passed to splatalogue's chemical name matching tool Examples -------- >>> freqs, aij, deg, EU, partfunc = get_molecular_parameters(molecule_name='CH2CHCN', ... fmin=220*u.GHz, ... fmax=222*u.GHz, ... molecule_name_vamdc='C2H3CN') >>> freqs, aij, deg, EU, partfunc = get_molecular_parameters('CH3OH', ... fmin=90*u.GHz, ... fmax=100*u.GHz) """ from astroquery.vamdc import load_species_table from astroquery.splatalogue import Splatalogue from vamdclib import nodes from vamdclib import request from vamdclib import specmodel lut = load_species_table.species_lookuptable() species_id_dict = lut.find(molecule_name_vamdc or molecule_name, flags=chem_re_flags) if len(species_id_dict) == 1: species_id = list(species_id_dict.values())[0] elif len(species_id_dict) == 0: raise ValueError("No matches for {0}".format(molecule_name)) else: raise ValueError("Too many species matched: {0}" .format(species_id_dict)) # do this here, before trying to compute the partition function, because # this query could fail tbl = Splatalogue.query_lines(fmin, fmax, chemical_name=molecule_name, line_lists=line_lists, show_upper_degeneracy=True, **kwargs) nl = nodes.Nodelist() nl.findnode('cdms') cdms = nl.findnode('cdms') request = request.Request(node=cdms) query_string = "SELECT ALL WHERE VAMDCSpeciesID='%s'" % species_id request.setquery(query_string) result = request.dorequest() # run it once to test that it works Q = list(specmodel.calculate_partitionfunction(result.data['States'], temperature=tex).values())[0] def partfunc(tem): Q = list(specmodel.calculate_partitionfunction(result.data['States'], temperature=tem).values())[0] return Q freqs = (np.array(tbl['Freq-GHz'])*u.GHz if 'Freq-GHz' in tbl else np.array(tbl['Freq-GHz(rest frame,redshifted)'])*u.GHz) aij = tbl['Log<sub>10</sub> (A<sub>ij</sub>)'] deg = tbl['Upper State Degeneracy'] EU = (np.array(tbl['E_U (K)'])*u.K*constants.k_B).to(u.erg).value return freqs, aij, deg, EU, partfunc
[docs]def generate_model(xarr, vcen, width, tex, column, freqs, aij, deg, EU, partfunc, background=None, tbg=2.73, ): """ Model Generator """ if hasattr(tex,'unit'): tex = tex.value if hasattr(tbg,'unit'): tbg = tbg.value if hasattr(column, 'unit'): column = column.value if column < 25: column = 10**column if hasattr(vcen, 'unit'): vcen = vcen.value if hasattr(width, 'unit'): width = width.value ckms = constants.c.to(u.km/u.s).value # assume equal-width channels #kwargs = dict(rest=ref_freq) #equiv = u.doppler_radio(**kwargs) # channelwidth array, with last element approximated channelwidth = np.empty_like(xarr.value) channelwidth[:-1] = np.abs(np.diff(xarr.to(u.Hz))).value channelwidth[-1] = channelwidth[-2] #velo = xarr.to(u.km/u.s, equiv).value freq = xarr.to(u.Hz).value # same unit as nu below model = np.zeros_like(xarr).value # splatalogue can report bad frequencies as zero OK = freqs.value != 0 freqs_ = freqs.to(u.Hz).value Q = partfunc(tex) for A, g, nu, eu in zip(aij[OK], deg[OK], freqs_[OK], EU[OK]): tau_per_dnu = line_tau_cgs(tex, column, Q, g, nu, eu, 10**A) width_dnu = width / ckms * nu s = np.exp(-(freq-(1-vcen/ckms)*nu)**2/(2*width_dnu**2))*tau_per_dnu/channelwidth jnu = (Jnu_cgs(nu, tex)-Jnu_cgs(nu, tbg)) model = model + jnu*(1-np.exp(-s)) if background is not None: return background-model return model
""" Example case to produce a model: freqs, aij, deg, EU, partfunc = get_molecular_parameters('CH3OH') def modfunc(xarr, vcen, width, tex, column): return generate_model(xarr, vcen, width, tex, column, freqs=freqs, aij=aij, deg=deg, EU=EU, partfunc=partfunc) fitter = generate_fitter(modfunc, name="CH3OH") """
[docs]def generate_fitter(model_func, name): """ Generator for hnco fitter class """ myclass = SpectralModel(model_func, 4, parnames=['shift','width','tex','column'], parlimited=[(False,False),(True,False),(True,False),(True,False)], parlimits=[(0,0), (0,0), (0,0),(0,0)], shortvarnames=(r'\Delta x',r'\sigma','T_{ex}','N'), centroid_par='shift', ) myclass.__name__ = name return myclass
# url = 'http://cdms.ph1.uni-koeln.de/cdms/tap/' # rslt = requests.post(url+"/sync", data={'REQUEST':"doQuery", 'LANG': 'VSS2', 'FORMAT':'XSAMS', 'QUERY':"SELECT SPECIES WHERE MoleculeStoichiometricFormula='CH2O'"}) if __name__ == "__main__": # example # 303 J = 3 gI = 0.25 gJ = 2*J+1 gK = 1 # 321 has same parameters for g ph2co = {'tex':18.75*u.K, 'total_column': 1e12*u.cm**-2, 'partition_function': 44.6812, # splatalogue's 18.75 'degeneracy': gI*gJ*gK, #'dipole_moment': 2.331e-18*u.esu*u.cm, #2.331*u.debye, } ph2co_303 = { 'frequency': 218.22219*u.GHz, 'energy_upper': kb_cgs*20.95582*u.K, 'einstein_A': 10**-3.55007/u.s, } ph2co_303.update(ph2co) ph2co_303['dnu'] = (1*u.km/u.s/constants.c * ph2co_303['frequency']) ph2co_321 = { 'frequency': 218.76007*u.GHz, 'energy_upper': kb_cgs*68.11081*u.K, 'einstein_A': 10**-3.80235/u.s, } ph2co_321.update(ph2co) ph2co_321['dnu'] = (1*u.km/u.s/constants.c * ph2co_321['frequency']) ph2co_322 = { 'frequency': 218.47563*u.GHz, 'energy_upper': kb_cgs*68.0937*u.K, 'einstein_A': 10**-3.80373/u.s, } ph2co_322.update(ph2co) ph2co_322['dnu'] = (1*u.km/u.s/constants.c * ph2co_322['frequency']) print(("tau303 = {0}".format(line_tau(**ph2co_303)))) print(("tau321 = {0}".format(line_tau(**ph2co_321)))) print(("tau322 = {0}".format(line_tau(**ph2co_322)))) print(("r303/r321 = {0}".format(line_brightness(**ph2co_321)/line_brightness(**ph2co_303)))) print(("r303/r322 = {0}".format(line_brightness(**ph2co_322)/line_brightness(**ph2co_303)))) # CDMS Q import requests import bs4 url = 'http://cdms.ph1.uni-koeln.de/cdms/tap/' rslt = requests.post(url+"/sync", data={'REQUEST':"doQuery", 'LANG': 'VSS2', 'FORMAT':'XSAMS', 'QUERY':"SELECT SPECIES WHERE MoleculeStoichiometricFormula='CH2O'"}) bb = bs4.BeautifulSoup(rslt.content, 'html5lib') h = [x for x in bb.findAll('molecule') if x.ordinarystructuralformula.value.text=='H2CO'][0] tem_, Q_ = h.partitionfunction.findAll('datalist') tem = [float(x) for x in tem_.text.split()] Q = [float(x) for x in Q_.text.split()] del ph2co_303['tex'] del ph2co_303['partition_function'] T_303 = np.array([line_brightness(tex=tex*u.K, partition_function=pf, **ph2co_303).value for tex,pf in zip(tem,Q)]) del ph2co_321['tex'] del ph2co_321['partition_function'] T_321 = np.array([line_brightness(tex=tex*u.K, partition_function=pf, **ph2co_321).value for tex,pf in zip(tem,Q)]) del ph2co_322['tex'] del ph2co_322['partition_function'] T_322 = np.array([line_brightness(tex=tex*u.K, partition_function=pf, **ph2co_322).value for tex,pf in zip(tem,Q)]) import pylab as pl pl.clf() pl.subplot(2,1,1) pl.plot(tem, T_321, label='$3_{2,1}-2_{2,0}$') pl.plot(tem, T_322, label='$3_{2,2}-2_{2,1}$') pl.plot(tem, T_303, label='$3_{0,3}-2_{0,2}$') pl.xlim(0,200) pl.subplot(2,1,2) pl.plot(tem, T_321/T_303, label='321/303') pl.plot(tem, T_322/T_303, label='322/303') pl.xlim(0,200) pl.draw(); pl.show()