Beispiel #1
0
def sum_spectra(sims,thin=True,Tex=None,Tbg=None,res=None,name='sum'):

	'''
	Adds all the spectra in the simulations list and returns a spectrum object.  By default,
	it assumes the emission is optically thin and will simply co-add the existing profiles.
	If thin is set to False, it will co-add and re-calculate based on the excitation/bg
	temperatures provided.  Currently, molsim can only handle single-excitation temperature
	co-adds with optically thick transmission, as it is not a full non-LTE radiative
	transfer program.  If a resolution is not specified, the highest resolution of the
	input datasets will be used.
	'''

		
	#first figure out what the resolution needs to be if it wasn't set
	if res is None:
		res = min([x.res for x in sims])
		
	#first we find out the limits of the total frequency coverage so we can make an
	#appropriate array to resample onto
	total_freq = np.concatenate([x.spectrum.freq_profile for x in sims])
	total_freq.sort()
	lls,uls = find_limits(total_freq,spacing_tolerance=2,padding=0)
		
	#now make a resampled array
	freq_arr = np.concatenate([np.arange(ll,ul,res) for ll,ul in zip(lls,uls)])	
	int_arr = np.zeros_like(freq_arr)	
	
	#make a spectrum to output
	sum_spectrum = Spectrum(name=name)
	sum_spectrum.freq_profile = freq_arr	

	if thin is True:		
		#loop through the stored simulations, resample them onto freq_arr, add them up
		for x in sims:
			int_arr0 = np.interp(freq_arr,x.spectrum.freq_profile,x.spectrum.int_profile,left=0.,right=0.)
			int_arr += int_arr0				
		sum_spectrum.int_profile = int_arr
		
	if thin is False:
		#if it's not gonna be thin, then we add up all the taus and apply the corrections
		for x in sims:
			int_arr0 = np.interp(freq_arr,x.spectrum.freq_profile,x.spectrum.tau_profile,left=0.,right=0.)
			int_arr += int_arr0
			
		#now we apply the corrections at the specified Tex
		J_T = ((h*freq_arr*10**6/k)*
			  (np.exp(((h*freq_arr*10**6)/
			  (k*Tex))) -1)**-1
			  )
		J_Tbg = ((h*freq_arr*10**6/k)*
			  (np.exp(((h*freq_arr*10**6)/
			  (k*Tbg))) -1)**-1
			  )			  
			
		int_arr = (J_T - J_Tbg)*(1 - np.exp(-int_arr))
		sum_spectrum.int_profile = int_arr

	return sum_spectrum
Beispiel #2
0
def resample_obs(x_arr,y_arr,res,return_spectrum=False):
	'''
	Resamples x_arr and y_arr to a resolution of 'res' in whatever units x_arr is and returns
	them as numpy arrays if return_spectrum is False or as a Spectrum object if return_spectrum
	is True.
	'''	
	
	lls,uls = find_limits(x_arr)
	new_x = np.array([])
	
	for x,y in zip(lls,uls):
		new_x = np.concatenate((new_x,np.arange(x,y,res)))
		
	new_y = np.interp(new_x,x_arr,y_arr,left=np.nan,right=np.nan)
	
	if return_spectrum is False:
		return new_x,new_y
	else:
		return Spectrum(frequency=new_x,Tb=new_y)	
Beispiel #3
0
    def simulate_spectrum(self,
                          parameters: np.ndarray,
                          scale: float = 3.0) -> np.ndarray:
        """
        Wraps `molsim` functionality to simulate the spectrum, given a set
        of input parameters as a NumPy 1D array. On the first pass, this generates
        a `Simulation` instance and stores it, which has some overhead associated
        with figuring out which catalog entries to simulate. After the first
        pass, the instance is re-used with the `Source` object updated with
        the new parameters.

        The nuance in this function is with `scale`: during the preprocess
        step, we assume that the observation frequency is not shifted to the
        source reference. To simulate with molsim, we identify where the catalog
        overlaps with our frequency windows, and because it is unshifted this
        causes molsim to potentially ignore a lot of lines (particularly
        high frequency ones). The `scale` parameter scales the input VLSR
        as to make sure that we cover everything as best as we can.

        Parameters
        ----------
        parameters : np.ndarray
            NumPy 1D array containing parameters for the simulation.
        scale : float, optional
            Modifies the window to consider catalog overlap, by default 3.

        Returns
        -------
        np.ndarray
            NumPy 1D array corresponding to the simulated spectrum
        """
        size, vlsr, ncol, Tex, dV = parameters
        # Assume that the value is in log space, if it's below 1000
        if ncol <= 1e3:
            ncol = 10**ncol
        source = Source("", vlsr, size, column=ncol, Tex=Tex, dV=dV)
        if not hasattr(self, "simulation"):
            min_freq, max_freq = find_limits(
                self.observation.spectrum.frequency)
            # there's a buffer here just to make sure we don't go out of bounds
            # and suddenly stop simulating lines
            min_offsets = compute.calculate_dopplerwidth_frequency(
                min_freq, vlsr * scale)
            max_offsets = compute.calculate_dopplerwidth_frequency(
                max_freq, vlsr * scale)
            min_freq -= min_offsets
            max_freq += max_offsets
            self.simulation = Simulation(
                mol=self.molecule,
                ll=min_freq,
                ul=max_freq,
                observation=self.observation,
                source=source,
                line_profile="gaussian",
                use_obs=True,
            )
        else:
            self.simulation.source = source
            self.simulation._apply_voffset()
            self.simulation._calc_tau()
            self.simulation._make_lines()
            self.simulation._beam_correct()
        intensity = self.simulation.spectrum.int_profile
        return intensity
Beispiel #4
0
def sum_spectra(sims,
                thin=True,
                Tex=None,
                Tbg=None,
                res=None,
                noise=None,
                override_freqs=None,
                planck=False,
                name='sum'):
    '''
	Adds all the spectra in the simulations list and returns a spectrum object.  By default,
	it assumes the emission is optically thin and will simply co-add the existing profiles.
	If thin is set to False, it will co-add and re-calculate based on the excitation/bg
	temperatures provided.  Currently, molsim can only handle single-excitation temperature
	co-adds with optically thick transmission, as it is not a full non-LTE radiative
	transfer program.  If a resolution is not specified, the highest resolution of the
	input datasets will be used.
	
	If the user wants back the summed spectra on an exact set of frequencies, they can specify
	that array (a numpy array) as override_freqs.
	
	
	'''

    #first figure out what the resolution needs to be if it wasn't set
    if res is None:
        res = min([x.res for x in sims])

    #check if override_freqs has been specified, and if so, use that.
    if override_freqs is not None:
        freq_arr = override_freqs

    else:
        #first we find out the limits of the total frequency coverage so we can make an
        #appropriate array to resample onto
        total_freq = np.concatenate([x.spectrum.freq_profile for x in sims])
        #eliminate all duplicate entries
        total_freq = np.array(list(set(total_freq)))
        total_freq.sort()
        lls, uls = find_limits(total_freq, spacing_tolerance=2, padding=0)

        #now make a resampled array
        freq_arr = np.concatenate(
            [np.arange(ll, ul, res) for ll, ul in zip(lls, uls)])

    #now make an identical array to hold intensities
    int_arr = np.zeros_like(freq_arr)

    #make a spectrum to output
    sum_spectrum = Spectrum(name=name)
    sum_spectrum.freq_profile = freq_arr

    if thin is True:
        #loop through the stored simulations, resample them onto freq_arr, add them up
        for x in sims:
            int_arr0 = np.interp(freq_arr,
                                 x.spectrum.freq_profile,
                                 x.spectrum.int_profile,
                                 left=0.,
                                 right=0.)
            int_arr += int_arr0
        sum_spectrum.int_profile = int_arr

    if thin is False:

        #Check to see if the user has specified a Tbg
        if Tbg is None:
            print(
                'If summing for the optically thick condition, either a constant Tbg or an appropriate Continuum object must be provided.  Operation aborted.'
            )
            return
        #Otherwise if we have a continuum object, we use that to calculate the Tbg at each point in freq_arr generated above
        if isinstance(Tbg, Continuum):
            sum_Tbg = Tbg.Tbg(freq_arr)
        else:
            sum_Tbg = Tbg

        #if it's not gonna be thin, then we add up all the taus and apply the corrections
        for x in sims:
            int_arr0 = np.interp(freq_arr,
                                 x.spectrum.freq_profile,
                                 x.spectrum.tau_profile,
                                 left=0.,
                                 right=0.)
            int_arr += int_arr0

        #now we apply the corrections at the specified Tex
        J_T = ((h * freq_arr * 10**6 / k) * (np.exp(
            ((h * freq_arr * 10**6) / (k * Tex))) - 1)**-1)
        J_Tbg = ((h * freq_arr * 10**6 / k) * (np.exp(
            ((h * freq_arr * 10**6) / (k * sum_Tbg))) - 1)**-1)

        int_arr = (J_T - J_Tbg) * (1 - np.exp(-int_arr))

        ##########################
        # For Spectra in Jy/Beam #
        ##########################

        if planck is True:

            #Collect the ranges over which different beam sizes are in play.
            omegas = []  #solid angle
            omegas_lls = [
            ]  #lower limits of frequency ranges covered by a solid angle
            omegas_uls = [
            ]  #upper limits of frequency ranges covered by a solid angle

            #loop through the simulations and extract the lls, uls, and omegas (from the synthesized beams)
            for sim in sims:
                for ll in sim.ll:
                    omegas_lls.append(ll)
                    omegas.append(sim.observation.observatory.synth_beam[0] *
                                  sim.observation.observatory.synth_beam[1])
                for ul in sim.ul:
                    omegas_uls.append(ul)

            #now we need an array that is identical to freq_arr, but holds the omega values at each point
            omega_arr = np.zeros_like(freq_arr)

            #and now we have to fill it, given the ranges we have data for.
            #we start by making arrays to hold all possible omega values
            tmp_omegas = []
            for omega, ll, ul in zip(omegas, omegas_lls, omegas_uls):
                tmp_omega = np.zeros_like(freq_arr)
                tmp_omega[np.where(
                    np.logical_and(freq_arr >= ll, freq_arr <= ul))] = omega
                tmp_omegas.append(np.array(tmp_omega))

            #now go through and flatten it into a single array, keeping the biggest omega value at each frequency
            #this is arbitrary. It's not possible to sum spectra at a point with more than one omega value.  The user has to make
            #sure they aren't doing this.  We keep just the largest.
            omega_arr = np.maximum.reduce(tmp_omegas)

            #now we can do the actual conversion to Planck scale Jy/beam.  We can only operate on non-zero values.
            mask = np.where(int_arr != 0)[0]
            int_arr[mask] = (
                3.92E-8 * (freq_arr[mask] * 1E-3)**3 * omega_arr[mask] /
                (np.exp(0.048 * freq_arr[mask] * 1E-3 / int_arr[mask]) - 1))

        sum_spectrum.int_profile = int_arr

    #add in noise, if requested
    if noise is not None:
        #initiate the random number generator
        rng = np.random.default_rng()

        #generate a noise array the same length as the simulation,
        noise_arr = rng.normal(0, noise, len(sum_spectrum.int_profile))

        #add it in
        sum_spectrum.int_profile += noise_arr

    return sum_spectrum