Source code for pyspeckit.spectrum.models.hyperfine

"""
======================================
Generalized hyperfine component fitter
======================================
.. moduleauthor:: Adam Ginsburg <adam.g.ginsburg@gmail.com>

Module API
^^^^^^^^^^
"""
import numpy as np
from astropy import units as u
from astropy import constants
import copy

from . import model
from . import fitter

# should be imported in the future
ckms = 2.99792458e5
hoverk = (constants.h.cgs/constants.k_B.cgs).value

[docs]class hyperfinemodel(object): """ Wrapper for the hyperfine model class. Specify the offsets and relative strengths when initializing, then you've got yourself a hyperfine modeler. There are a wide variety of different fitter attributes, each designed to free a different subset of the parameters. Their purposes should be evident from their names. """ def __init__(self, line_names, voff_lines_dict, freq_dict, line_strength_dict, relative_strength_total_degeneracy): """ Initialize the various parameters defining the hyperfine transitions Parameters ---------- line_names: list list of the line names to be used as indices for the dictionaries voff_lines_dict: dict a linename:v_off dictionary of velocity offsets for the hyperfine components. Technically, this is redundant with freq_dict freq_dict: dict frequencies of the indvidual transitions line_strength_dict: dict Relative strengths of the hyperfine components, usually determined by their degeneracy and Einstein A coefficients """ self.line_names = tuple(line_names) self.voff_lines_dict = voff_lines_dict self.freq_dict = freq_dict self.line_strength_dict = line_strength_dict self.relative_strength_total_degeneracy = relative_strength_total_degeneracy self.fitter = model.SpectralModel(self,4, parnames=['Tex','tau','center','width'], # T_ex = 0 results in an infinity parlimited=[(True,False), (True,False), (False,False), (True,False)], parlimits=[(1e-5,0), (0,0), (0,0), (0,0)], # specify the parameter names (LaTeX is OK) shortvarnames=("T_{ex}","\\tau","v","\\sigma"), guess_types=['amplitude+2.73', 1.0, 'center', 'width'], fitunit='Hz') self.nlines = len(line_names) self.varyhf_fitter = model.SpectralModel(self.hyperfine_varyhf,3+self.nlines, parnames=['Tex','center','width']+['tau%s' % k for k in self.line_names], parlimited=[(True,False), (False,False), (True,False)] + [(True,False),]*self.nlines, parlimits=[(1e-5,0), (0,0), (0,0)]+[(0,0),]*self.nlines, shortvarnames=("T_{ex}","v","\\sigma") + tuple(("\\tau(\\mathrm{%s})" % k for k in self.line_names)), fitunit='Hz') self.varyhf_amp_fitter = model.SpectralModel(self.hyperfine_varyhf_amp, 2+self.nlines, parnames=['center','width']+['amp%s' % k for k in self.line_names], parlimited=[(False,False), (True,False)] + [(True,False),]*self.nlines, parlimits=[(0,0), (0,0)]+[(0,0),]*self.nlines, shortvarnames=("v","\\sigma") + tuple(("amp(\\mathrm{%s})" % k for k in self.line_names)), fitunit='Hz') self.varyhf_amp_width_fitter = model.SpectralModel(self.hyperfine_varyhf_amp_width,1+self.nlines*2, parnames=['center']+['amp%s' % k for k in self.line_names]+['width%s' % k for k in self.line_names], parlimited=[(False,False)] + [(True,False),]*self.nlines + [(True,False)]*self.nlines, parlimits=[(0,0)]+[(0,0),]*self.nlines*2, shortvarnames=(("v",) + tuple(("amp(\\mathrm{%s})" % k for k in self.line_names)) + tuple(("\\sigma(\\mathrm{%s})" % k for k in self.line_names))), # specify the parameter names (TeX is OK) fitunit='Hz' ) self.vheight_fitter = model.SpectralModel(fitter.vheightmodel(self),5, parnames=['height','Tex','tau','center','width'], parlimited=[(False,False), (True,False), (True,False), (False,False), (True,False)], parlimits=[(0,0), (1e-5,0), (0,0), (0,0), (0,0)], shortvarnames=("H","T_{ex}","\\tau","v","\\sigma"), # specify the parameter names (TeX is OK) guess_types=[0.0, 'amplitude+2.73', 1.0, 'center', 'width'], fitunit='Hz' ) self.background_fitter = model.SpectralModel(self.hyperfine_addbackground,5, parnames=['Tbackground','Tex','tau','center','width'], parlimited=[(True,False), (True,False), (False,False), (True,False), (False,False), (True,False)], parlimits=[(0,0), (1e-5,0), (0,0), (0,0), (0,0), (0,0)], shortvarnames=('T_{BG}',"T_{ex}","\\tau","v","\\sigma"), # specify the parameter names (TeX is OK) guess_types=[2.73, 'amplitude+2.73', 1.0, 'center', 'width'], fitunit='Hz') self.background_contsub_fitter = model.SpectralModel(self.hyperfine_background,5, parnames=['Tbackground','Tex','tau','center','width'], parlimited=[(True,False), (True,False), (False,False), (True,False), (False,False), (True,False)], parlimits=[(0,0), (1e-5,0), (0,0), (0,0), (0,0), (0,0)], shortvarnames=('T_{BG}',"T_{ex}","\\tau","v","\\sigma"), # specify the parameter names (TeX is OK) guess_types=[0.0, 'amplitude+2.73', 1.0, 'center', 'width'], fitunit='Hz') self.ampfitter = model.SpectralModel(self.hyperfine_amp,3, parnames=['amp','center','width'], parlimited=[(False,False), (False,False), (True,False)], parlimits=[(0,0), (0,0), (0,0)], shortvarnames=("amp","v","\\sigma"), # specify the parameter names (TeX is OK) fitunit='Hz' ) self.taufitter = model.SpectralModel(self.hyperfine_tau,3, parnames=['tau','center','width'], parlimited=[(True,False), (False,False), (True,False)], parlimits=[(0,0), (0,0), (0,0)], shortvarnames=(r'\tau',"v","\\sigma"), # specify the parameter names (TeX is OK) guess_types=[1.0, 'center', 'width'], fitunit='Hz') self.totaltaufitter = model.SpectralModel(self.hyperfine_tau_total,3, parnames=['tau','center','width'], parlimited=[(True,False), (False,False), (True,False)], parlimits=[(0,0), (0,0), (0,0)], shortvarnames=(r'\tau',"v","\\sigma"), # specify the parameter names (TeX is OK) guess_types=[1.0, 'center', 'width'], fitunit='Hz') def __copy__(self): # http://stackoverflow.com/questions/1500718/what-is-the-right-way-to-override-the-copy-deepcopy-operations-on-an-object-in-p cls = self.__class__ result = cls.__new__(cls) result.__dict__.update(self.__dict__) return result def __deepcopy__(self, memo): # A deep copy of the hyperfine model is OK to just do regular copies of # all attributes, since none of them are meant to be modified cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): setattr(result, k, copy.deepcopy(v, memo)) return result def __call__(self, *args, **kwargs): """ Generate a model spectrum given an excitation temperature, optical depth, offset velocity, and velocity width. """ return self.hyperfine(*args,**kwargs)
[docs] def hyperfine_amp(self, xarr, amp=None, xoff_v=0.0, width=1.0, return_hyperfine_components=False, Tbackground=2.73, Tex=5.0, tau=0.1): """ wrapper of self.hyperfine with order of arguments changed """ return self.hyperfine(xarr, amp=amp, Tex=Tex, tau=tau, xoff_v=xoff_v, width=width, return_hyperfine_components=return_hyperfine_components, Tbackground=Tbackground)
[docs] def hyperfine_tau(self, xarr, tau, xoff_v, width, **kwargs): """ same as hyperfine, but with arguments in a different order, AND tau is returned instead of exp(-tau)""" return self.hyperfine(xarr, tau=tau, xoff_v=xoff_v, width=width, return_tau=True, **kwargs)
[docs] def hyperfine_tau_total(self, xarr, tau_total, xoff_v, width, **kwargs): """ same as hyperfine, but with arguments in a different order, AND tau is returned instead of exp(-tau), AND the *peak* tau is used""" return self.hyperfine(xarr, tau_total=tau_total, xoff_v=xoff_v, width=width, return_tau=True, **kwargs)
[docs] def hyperfine_varyhf(self, xarr, Tex, xoff_v, width, *args, **kwargs): """ Wrapper of hyperfine for using a variable number of peaks with specified tau """ return self.hyperfine(xarr, Tex=Tex, xoff_v=xoff_v, width=width, tau=dict(zip(self.line_names,args)), vary_hyperfine_tau=True, **kwargs)
[docs] def hyperfine_varyhf_amp(self, xarr, xoff_v, width, *args, **kwargs): """ Wrapper of hyperfine for using a variable number of peaks with specified amplitude (rather than tau). Uses some opaque tricks: Tex is basically ignored, and return_tau means you're actually returning the amplitude, which is just passed in as tau""" return self.hyperfine(xarr, xoff_v=xoff_v, width=width, tau=dict(zip(self.line_names,args)), vary_hyperfine_tau=True, return_tau=True, **kwargs)
[docs] def hyperfine_varyhf_amp_width(self, xarr, xoff_v, *args, **kwargs): """ Wrapper of hyperfine for using a variable number of peaks with specified amplitude (rather than tau). Uses some opaque tricks: Tex is basically ignored, and return_tau means you're actually returning the amplitude, which is just passed in as tau""" if len(args) % 2 != 0: raise ValueError("Incorrect number of arguments for varying amplitude" " and width. Need N amplitudes, N widths.") nargs = int(len(args)/2) return self.hyperfine(xarr, xoff_v=xoff_v, tau=dict(zip(self.line_names,args[:nargs])), width=dict(zip(self.line_names,args[nargs:])), vary_hyperfine_tau=True, vary_hyperfine_width=True, return_tau=True, **kwargs)
[docs] def hyperfine_addbackground(self, xarr, Tbackground=2.73, Tex=5.0, tau=0.1, xoff_v=0.0, width=1.0, return_tau=False, **kwargs): """ Identical to hyperfine, but adds Tbackground as a constant continuum level """ if return_tau: raise ValueError("Cannot return tau when adding a continuum background.") return (self.hyperfine(xarr, Tbackground=Tbackground, Tex=Tex, tau=tau, xoff_v=xoff_v, width=width, return_tau=False, **kwargs) + Tbackground)
[docs] def hyperfine_background(self, xarr, Tbackground=2.73, Tex=5.0, tau=0.1, xoff_v=0.0, width=1.0, return_tau=False, **kwargs): """ Identical to hyperfine, but with Tbackground free. Assumes already background-subtracted """ if return_tau: raise ValueError("Cannot return tau when adding a continuum background.") return self.hyperfine(xarr, Tbackground=Tbackground, Tex=Tex, tau=tau, xoff_v=xoff_v, width=width, return_tau=False, **kwargs)
[docs] def hyperfine(self, xarr, Tex=5.0, tau=0.1, xoff_v=0.0, width=1.0, return_hyperfine_components=False, Tbackground=2.73, amp=None, return_tau=False, tau_total=None, vary_hyperfine_tau=False, vary_hyperfine_width=False): """ Generate a model spectrum given an excitation temperature, optical depth, offset velocity, and velocity width. Parameters ---------- return_tau : bool If specified, return just the tau spectrum, ignoring Tex tau_total : bool If specified, use this *instead of tau*, and it tries to normalize to the *peak of the line* vary_hyperfine_tau : bool If set to true, allows the hyperfine transition amplitudes to vary and does not use the line_strength_dict. If set, `tau` must be a dict """ # Convert X-units to frequency in Hz try: xarr = xarr.as_unit('Hz').value except AttributeError: xarr = xarr.to('Hz').value # Ensure parameters are scalar / have no extra dims if not np.isscalar(Tex): Tex = Tex.squeeze() if not np.isscalar(xoff_v): xoff_v = xoff_v.squeeze() if vary_hyperfine_width: if not isinstance(width, dict): raise TypeError("If varying the amplitude of the hyperfine lines, must specify tau as a dict") else: if not np.isscalar(width): width = width.squeeze() if vary_hyperfine_tau: if not isinstance(tau, dict): raise TypeError("If varying the amplitude of the hyperfine lines, must specify tau as a dict") else: if not np.isscalar(tau): tau = tau.squeeze() # Generate an optical depth spectrum as a function of the X-axis tau_nu_cumul = np.zeros(len(xarr)) # Error check: inputing NANs results in meaningless output - return without computing a model if (np.any(np.isnan((Tex,xoff_v))) or ((not vary_hyperfine_tau) and np.isnan(tau)) or ((not vary_hyperfine_width) and np.isnan(width))): if return_hyperfine_components: return [tau_nu_cumul] * len(self.line_names) else: return tau_nu_cumul if tau_total is not None: tau = 1 components =[] for linename in self.line_names: voff_lines = np.array(self.voff_lines_dict[linename]) lines = (1-voff_lines/ckms)*self.freq_dict[linename] if not vary_hyperfine_width and width == 0: tau_nu = xarr*0 else: if vary_hyperfine_width: nuwidth = np.abs(width[linename]/ckms*lines) else: nuwidth = np.abs(width/ckms*lines) nuoff = xoff_v/ckms*lines if vary_hyperfine_tau: tau_line = tau[linename] else: # the total optical depth, which is being fitted, should be the sum of the components tau_line = (tau * np.array(self.line_strength_dict[linename])/ np.array(self.relative_strength_total_degeneracy[linename])) tau_nu = np.array(tau_line * np.exp(-(xarr+nuoff-self.freq_dict[linename])**2 / (2.0*nuwidth**2))) tau_nu[tau_nu!=tau_nu] = 0 # avoid nans components.append(tau_nu) tau_nu_cumul += tau_nu # add a list of the individual 'component' spectra to the total components... if tau_total is not None: tau_max = tau_nu_cumul.max() # danger of undersampling... tau_nu_cumul *= tau_total/tau_max for c in components: c *= tau_total/tau_max if return_hyperfine_components: if return_tau: return components elif amp is None: return (1.0-np.exp(-np.array(components)))*(Tex-Tbackground) else: comps = (1.0-np.exp(-np.array(components)))*(Tex-Tbackground) return comps/comps.max() * amp if return_tau: return tau_nu_cumul else: # This is not the full equation of radiative transfer, but a # background-subtracted version # With "background" function B_nu = CMB, S_nu = absorber, and I_nu = received: # I_nu = B_nu * exp(-tau) + (1-exp(-tau)) * S_nu # This is a very good approximation for Rohlfs & Wilson eqn 15.29: #spec = (1.0-np.exp(-np.array(tau_nu_cumul)))*(Tex-Tbackground) # this is the exact version of 15.29 T0 = hoverk * xarr # division by zero should raise an exception, but zero-background # is allowed and just turns this term to zero=(exp(-inf)) if Tbackground > 0: background_term = 1/(np.exp(T0/Tbackground)-1) else: background_term = 0 with np.errstate(divide='raise'): # division by zero is disallowed and should raise an exception spec = (1.0-np.exp(-np.array(tau_nu_cumul)))*T0*(1/(np.exp(T0/Tex)-1) - background_term) # This is the equation of radiative transfer using the RJ definitions # (eqn 1.37 in Rohlfs) # It is identical, except without T_background subtracted # spec = Tex+(np.exp(-np.array(tau_nu_cumul)))*(Tbackground-Tex) if amp is None: return spec else: return spec/spec.max() * amp