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
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)
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
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