pythonradex 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Thu May 23 11:10:38 2024
5
+
6
+ @author: gianni
7
+ """
8
+
9
+ import numba as nb
10
+ import numpy as np
11
+
12
+
13
+ #It would be much more elegant to put these functions directly into the
14
+ #classes defined in escape_probability.py. But I want to compile the beta functions
15
+ #with numba, so I have to put these functions outside the class definitions
16
+ #(defining them as staticmethod does not work here)
17
+
18
+ min_reliable_tau = -1
19
+
20
+ @nb.jit(nopython=True,cache=True)
21
+ def clip_prob(prob):
22
+ return np.where(prob>1,1,np.where(prob<0,0,prob))
23
+
24
+ ####### functions for escape probability using Taylor expansion ######
25
+
26
+ @nb.jit(nopython=True,cache=True)
27
+ def identify_tau_regions(tau_nu):
28
+ tau_epsilon = 0.05 #important that this is not too small, to avoid the unstable region
29
+ normal_tau_region = tau_nu > tau_epsilon
30
+ small_tau_region = np.abs(tau_nu) <= tau_epsilon
31
+ negative_tau_region = (tau_nu >= min_reliable_tau) & (tau_nu<-tau_epsilon)
32
+ unreliable_negative_region = tau_nu < min_reliable_tau
33
+ total_region_points = normal_tau_region.sum()+small_tau_region.sum()\
34
+ +negative_tau_region.sum()+unreliable_negative_region.sum()
35
+ assert total_region_points==len(tau_nu),\
36
+ 'selection needs to cover all points of tau_nu'
37
+ return normal_tau_region,small_tau_region,negative_tau_region,\
38
+ unreliable_negative_region
39
+
40
+ @nb.jit(nopython=True,cache=True)
41
+ def beta_analytical_uniform_sphere(tau_nu):
42
+ '''Computes the escape probability for a uniform sphere analytically,
43
+ given the optical depth tau_nu'''
44
+ #see the RADEX manual for this formula; derivation is found in the old
45
+ #Osterbrock (1974) book, appendix 2. Note that Osterbrock uses tau for
46
+ #radius, while I use it for diameter
47
+ return 1.5/tau_nu*(1-2/tau_nu**2+(2/tau_nu+2/tau_nu**2)*np.exp(-tau_nu))
48
+
49
+ @nb.jit(nopython=True,cache=True)
50
+ def beta_Taylor_uniform_sphere(tau_nu):
51
+ '''Computes the escape probability of a uniform sphere using a Taylor expansion,
52
+ given the optical depth tau_nu'''
53
+ #Taylor expansion of beta for uniform sphere is easier to evaluate numerically
54
+ #(for small tau_nu)
55
+ #Series calculated using Wolfram Alpha; not so easy analytically, to
56
+ #calculate the limit as tau->0, use rule of L'Hopital
57
+ return 1 - 0.375*tau_nu + 0.1*tau_nu**2 - 0.0208333*tau_nu**3
58
+
59
+ @nb.jit(nopython=True,cache=True)
60
+ def beta_analytical_LVG_slab(tau_nu):
61
+ #see line 357 in matrix.f and RADEX paper, or Scoville & Solomon (1974)
62
+ return (1-np.exp(-3*tau_nu))/(3*tau_nu)
63
+
64
+ @nb.jit(nopython=True,cache=True)
65
+ def beta_Taylor_LVG_slab(tau_nu):
66
+ return 1 - (3*tau_nu)/2 + (3*tau_nu**2)/2 - (9*tau_nu**3)/8
67
+
68
+ #about the LVG sphere
69
+ #we need to assume that the LVG sphere is homogeneous, because from formula 2.6.36
70
+ #in Elitzur92, it becomes clear that the optical depth in a velocity-
71
+ #coherent element depends on the local number density. But since pythonradex assumes
72
+ #uniform excitation temperature and a single optical depth characerising the source,
73
+ #the local number density must be the same everywhere
74
+ #Thus, we need to assume that the LVG sphere is uniform, otherwise we cannot handle it
75
+ #in pythonradex.
76
+ #We are indeed considering the model by Goldreich & Kwan (1974) which has uniform density.
77
+ #in fact, same argument also implies that LVG slab needs to be uniform...
78
+
79
+ @nb.jit(nopython=True,cache=True)
80
+ def beta_analytical_LVG_sphere(tau_nu):
81
+ #e.g Elitzur p. 44, or Ramos & Elitzur 2018, eq. 14
82
+ return (1-np.exp(-tau_nu))/tau_nu
83
+
84
+ @nb.jit(nopython=True,cache=True)
85
+ def beta_Taylor_LVG_sphere(tau_nu):
86
+ #from Wolfram Alpha
87
+ return 1 - tau_nu/2 + tau_nu**2/6 - tau_nu**3/24
88
+
89
+ def generate_Taylor_beta(beta_ana,beta_Taylor):
90
+ @nb.jit(nopython=True,cache=True)
91
+ def beta(tau_nu):
92
+ #use Taylor expansion if tau is below epsilon; this is to avoid numerical
93
+ #problems with the anlytical formula to compute the escape probability.
94
+ #Concerning negative optical depth, the RADEX paper advises that
95
+ #negativ tau (inverted population) cannot be treated correctly by a
96
+ #non-local code like RADEX, and that results for lines with tau <~ -1
97
+ #should be ignored. Moreover, negative tau can make the code crash:
98
+ #very negative tau leads to very large transition rates, which makes the
99
+ #matrix of the rate equations ill-conditioned.
100
+ #thus, for tau < -1, I just take abs(tau). Note that this is different
101
+ #from RADEX: they make something quite strange: they have different
102
+ #approximations for positive large, normal and small tau. But then they
103
+ #use abs(tau) to decide which function to use, but then use tau in that
104
+ #function (line 333 in matrix.f)
105
+ normal,small,negative,unreliable = identify_tau_regions(tau_nu=tau_nu)
106
+ prob = np.empty_like(tau_nu)
107
+ prob[normal] = beta_ana(tau_nu[normal])
108
+ #here I use tau even if tau < 0:
109
+ prob[small] = beta_Taylor(tau_nu[small])
110
+ prob[negative] = beta_ana(tau_nu[negative])
111
+ #here I just use abs(tau) to stabilize the code
112
+ prob[unreliable] = beta_ana(np.abs(tau_nu[unreliable]))
113
+ assert np.all(np.isfinite(prob))
114
+ return clip_prob(prob)
115
+ return beta
116
+
117
+ beta_uniform_sphere = generate_Taylor_beta(beta_ana=beta_analytical_uniform_sphere,
118
+ beta_Taylor=beta_Taylor_uniform_sphere)
119
+ beta_LVG_slab = generate_Taylor_beta(beta_ana=beta_analytical_LVG_slab,
120
+ beta_Taylor=beta_Taylor_LVG_slab)
121
+ beta_LVG_sphere = generate_Taylor_beta(beta_ana=beta_analytical_LVG_sphere,
122
+ beta_Taylor=beta_Taylor_LVG_sphere)
123
+
124
+ ######### functions for uniform slab ##################
125
+ #see Elitzur92 (https://ui.adsabs.harvard.edu/abs/1992ASSL..170.....E/abstract,
126
+ #Problem 2.12) for confirmation of the formulas used here
127
+
128
+ @nb.jit(nopython=True,cache=True)
129
+ def integral_term_for_uniform_slab(tau):
130
+ mu = np.linspace(1e-5,1,200)
131
+ return np.trapezoid((1-np.exp(-tau/mu))*mu,mu)
132
+
133
+ tau_grid_for_UniformSlab = np.logspace(-5,2,1000)
134
+ min_grid_tau = np.min(tau_grid_for_UniformSlab)
135
+ max_grid_tau = np.max(tau_grid_for_UniformSlab)
136
+ #the expression for the flux contains an integral term;
137
+ #here I pre-compute this term so it can be interpolated to speed up the code
138
+ integral_term_grid = np.array([integral_term_for_uniform_slab(tau)
139
+ for tau in tau_grid_for_UniformSlab])
140
+
141
+ @nb.jit(nopython=True,cache=True)
142
+ def interpolated_integral_term(tau):
143
+ interp = np.interp(x=tau,xp=tau_grid_for_UniformSlab,fp=integral_term_grid)
144
+ #limiting values:
145
+ # -very large tau: integral term goes to 0.5
146
+ # -very small tau: integral term goes to tau
147
+ #note that I cannot use the left and right keywords of np.interp because
148
+ #I want to compile this function; so I use np.where instead
149
+ return np.where(tau>max_grid_tau,0.5,np.where(tau<min_grid_tau,tau,interp))
150
+
151
+ @nb.jit(nopython=True,cache=True)
152
+ def beta_uniform_slab(tau_nu):
153
+ #for negative tau, this function will also return 1, which is fine if tau
154
+ #is close to 0, but not correct for very negative tau; in general, results
155
+ #should be ignored for tau < -1
156
+ int_term = interpolated_integral_term(tau=tau_nu)
157
+ prob = np.where(tau_nu<min_grid_tau,1,int_term/tau_nu)
158
+ assert np.all(np.isfinite(prob))
159
+ return clip_prob(prob)
160
+
161
+ ######### LVG sphere as implemented by RADEX ##################
162
+
163
+ @nb.jit(nopython=True,cache=True)
164
+ def beta_LVG_sphere_RADEX_gtr7(tau_nu):
165
+ tau_nu_r = tau_nu/2
166
+ return 2/(tau_nu_r*4*(np.sqrt(np.log(tau_nu_r/np.sqrt(np.pi)))))
167
+
168
+ @nb.jit(nopython=True,cache=True)
169
+ def beta_LVG_sphere_RADEX_less7(tau_nu):
170
+ tau_nu_r = tau_nu/2
171
+ return 2*(1-np.exp(-2.34*tau_nu_r))/(4.68*tau_nu_r)
172
+
173
+ @nb.jit(nopython=True,cache=True)
174
+ def beta_LVG_sphere_RADEX(tau_nu):
175
+ #RADEX paper, eq. 18, claims that they use beta_analytical_LVG_sphere
176
+ #(beta= (1-exp(-tau))/tau
177
+ #but in fact they use a different formula from de Jong+80
178
+ #see line 346 in matrix.f
179
+ #I use the same formulae as RADEX, except that I take care of negative tau
180
+ #handling negative tau turns out to be important, e.g. for CO, Tkin=200,
181
+ #width_v=3 km/s, Ntot=1e20 cm-2, RADEX gives an invalid solution
182
+ assert -7 < min_reliable_tau < -0.01
183
+ gtr7 = 7 <= tau_nu
184
+ less7 = (0.001 <= tau_nu) & (tau_nu < 7)
185
+ small = (-0.001 <= tau_nu) & (tau_nu < 0.001)
186
+ negative = (min_reliable_tau <= tau_nu) & (tau_nu < -0.001)
187
+ unreliable_less7 = (-7 <= tau_nu) & (tau_nu < min_reliable_tau)
188
+ unreliable_gtr7 = tau_nu < -7
189
+ assert gtr7.sum()+less7.sum()+small.sum()+negative.sum()+unreliable_less7.sum()\
190
+ +unreliable_gtr7.sum() == tau_nu.size
191
+ beta = np.empty_like(tau_nu)
192
+ beta[gtr7] = beta_LVG_sphere_RADEX_gtr7(tau_nu[gtr7])
193
+ beta[less7] = beta_LVG_sphere_RADEX_less7(tau_nu[less7])
194
+ beta[small] = 1.0
195
+ beta[negative] = beta_LVG_sphere_RADEX_less7(tau_nu[negative])
196
+ beta[unreliable_less7] = beta_LVG_sphere_RADEX_less7(np.abs(tau_nu[unreliable_less7]))
197
+ beta[unreliable_gtr7] = beta_LVG_sphere_RADEX_gtr7(np.abs(tau_nu[unreliable_gtr7]))
198
+ return clip_prob(beta)
pythonradex/flux.py ADDED
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Fri Aug 2 10:32:03 2024
5
+
6
+ @author: gianni
7
+ """
8
+
9
+ from scipy import constants
10
+ from pythonradex import helpers
11
+ import numpy as np
12
+ import warnings
13
+
14
+
15
+ class FluxCalculator():
16
+
17
+ tau_peak_fraction = 1e-2
18
+ nu_per_FHWM = 20
19
+ n_nu_elements = {'regular':15,'LVG sphere':51}
20
+
21
+ def __init__(self,emitting_molecule,level_population,geometry_name,
22
+ V_LVG_sphere,compute_flux_nu,tau_nu0_individual_transitions,
23
+ tau_dust,S_dust):
24
+ self.emitting_molecule = emitting_molecule
25
+ self.level_population = level_population
26
+ self.geometry_name = geometry_name
27
+ self.V_LVG_sphere = V_LVG_sphere
28
+ self.Tex = self.emitting_molecule.get_Tex(level_population=level_population)
29
+ self.compute_flux_nu = compute_flux_nu
30
+ self.tau_nu0_individual_transitions = tau_nu0_individual_transitions
31
+ self.tau_dust = tau_dust
32
+ self.S_dust = S_dust
33
+ self.is_LVG_sphere = self.geometry_name == 'LVG sphere'
34
+
35
+ def get_flux_parameters_for_rectangular_flux(self,transitions):
36
+ n_nu_elements = self.n_nu_elements['LVG sphere'] if self.is_LVG_sphere\
37
+ else self.n_nu_elements['regular']
38
+ assert n_nu_elements%2 == 1,\
39
+ 'need odd number of elements to include nu0 in the array'
40
+ middle_index = n_nu_elements//2
41
+ nu0 = self.emitting_molecule.nu0[transitions]
42
+ width_nu = self.emitting_molecule.width_v/constants.c*nu0
43
+ # a matrix where first index is transition, second index is nu
44
+ nu = np.linspace(nu0-width_nu/2,nu0+width_nu/2,n_nu_elements,axis=-1)
45
+ tau_nu = 1/nu**2
46
+ tau_nu0 = self.tau_nu0_individual_transitions[transitions]
47
+ tau_nu *= tau_nu0[:,None]/tau_nu[:,middle_index][:,None]
48
+ source_function = helpers.B_nu(T=self.Tex[transitions][:,None],nu=nu)
49
+ return nu,tau_nu,source_function
50
+
51
+ def fast_line_fluxes_rectangular_without_overlap(self,solid_angle,transitions):
52
+ #note that integrated fluxes only make sense for non-overlapping lines,
53
+ #so this function can only be used for non-overlapping lines
54
+ nu,tau_nu,source_function = self.get_flux_parameters_for_rectangular_flux(
55
+ transitions=transitions)
56
+ kwargs = {'tau_nu':tau_nu,'source_function':source_function,
57
+ 'solid_angle':solid_angle}
58
+ if self.is_LVG_sphere:
59
+ kwargs['nu'] = nu
60
+ kwargs['nu0'] = self.emitting_molecule.nu0[transitions][:,None]
61
+ kwargs['V'] = self.V_LVG_sphere
62
+ flux_nu = self.compute_flux_nu(**kwargs)
63
+ flux = np.trapezoid(flux_nu,nu,axis=1)
64
+ return flux
65
+
66
+ def fast_line_fluxes_Gaussian_without_overlap(self,solid_angle,transitions):
67
+ nu0 = self.emitting_molecule.nu0[transitions]
68
+ FWHM_nu = self.emitting_molecule.width_v/constants.c*nu0
69
+ sigma_nu = helpers.FWHM2sigma(FWHM_nu)
70
+ #for thin emission, I just want to include out to a certain fraction
71
+ #of the peak
72
+ #but for thick emission, the spectrum is saturated, so a fraction of the
73
+ #peak is not useful; in those cases, I need to set an absolute value
74
+ #for the minimum tau to include
75
+ tau_nu0 = self.tau_nu0_individual_transitions[transitions]
76
+ peak_frac = self.tau_peak_fraction*tau_nu0
77
+ min_tau = np.where(peak_frac>0.01,0.01,peak_frac)
78
+ #calculate the distance in freq spac between tau_nu0 and min_tau:
79
+ #(min_tau = tau_nu0*exp(-(nu-nu0)**2/(2*sigma_nu**2))
80
+ Delta_nu = sigma_nu*np.sqrt(-2*np.log(min_tau/tau_nu0))
81
+ n_nu = np.max((2*Delta_nu/FWHM_nu * self.nu_per_FHWM).astype(int))
82
+ #first index transition, second index frequency:
83
+ nu = np.linspace(nu0-Delta_nu,nu0+Delta_nu,n_nu,axis=-1)
84
+ #doing an approximation: in principle, tau has an additional 1/nu**2 dependence,
85
+ #but if Delta_nu is small compared to nu0, that dependence is negligible
86
+ phi_nu_shape = np.exp(-(nu-nu0[:,None])**2/(2*sigma_nu[:,None]**2))
87
+ tau_nu = tau_nu0[:,None]*phi_nu_shape
88
+ source_function = helpers.B_nu(T=self.Tex[:,None][transitions],nu=nu)
89
+ spectrum = self.compute_flux_nu(tau_nu=tau_nu,source_function=source_function,
90
+ solid_angle=solid_angle)
91
+ flux = np.trapezoid(spectrum,nu,axis=1)
92
+ return flux
93
+
94
+ def all_transitions(self):
95
+ return list(range(self.emitting_molecule.n_rad_transitions))
96
+
97
+ def determine_tau_of_overlapping_lines(self,transitions):
98
+ total_tau = []
99
+ for i in transitions:
100
+ tau = 0
101
+ for j in self.emitting_molecule.overlapping_lines[i]:
102
+ tau += self.tau_nu0_individual_transitions[j]
103
+ total_tau.append(tau)
104
+ return np.array(total_tau)
105
+
106
+ def fluxes_of_individual_transitions(self,solid_angle,transitions=None):
107
+ max_acceptable_tau = 0.1 #for dust and overlapping lines
108
+ if transitions is None:
109
+ transitions = self.all_transitions()
110
+ if self.emitting_molecule.any_line_has_overlap(transitions):
111
+ tau_overlapping_lines = self.determine_tau_of_overlapping_lines(
112
+ transitions=transitions)
113
+ if np.any(tau_overlapping_lines>max_acceptable_tau):
114
+ raise ValueError('fluxes of individual lines can only be calculated'
115
+ +' for non-overlapping lines or thin overlapping lines')
116
+ if np.any(self.tau_dust(self.emitting_molecule.nu0) > max_acceptable_tau):
117
+ raise ValueError('dust is not optically thin, cannot calculate'+
118
+ ' fluxes of individual lines')
119
+ if self.emitting_molecule.line_profile_type == 'Gaussian':
120
+ fast_flux = self.fast_line_fluxes_Gaussian_without_overlap(
121
+ solid_angle=solid_angle,transitions=transitions)
122
+ elif self.emitting_molecule.line_profile_type == 'rectangular':
123
+ fast_flux = self.fast_line_fluxes_rectangular_without_overlap(
124
+ solid_angle=solid_angle,transitions=transitions)
125
+ else:
126
+ raise ValueError(f'line profile {self.emitting_molecule.line_profile_type} '
127
+ +'unknown')
128
+ return np.squeeze(fast_flux)
129
+
130
+ def set_nu(self,nu):
131
+ self.nu = nu
132
+ self.nu_selected_lines,self.nu_selected_line_indices = self.identify_lines()
133
+ self.tau_dust_nu = self.tau_dust(self.nu)
134
+ self.set_tau_nu_lines()
135
+ self.set_tau_nu_tot()
136
+
137
+ def identify_lines(self):
138
+ nu_min,nu_max = np.min(self.nu),np.max(self.nu)
139
+ selected_lines = []
140
+ selected_line_indices = []
141
+ for i,line in enumerate(self.emitting_molecule.rad_transitions):
142
+ if nu_min <= line.nu0 and line.nu0 <= nu_max:
143
+ selected_line_indices.append(i)
144
+ selected_lines.append(line)
145
+ return selected_lines,selected_line_indices
146
+
147
+ def construct_tau_nu_individual_line(self,line,nu,tau_nu0):
148
+ #tau is prop to phi_nu/nu**2
149
+ normalisation = line.line_profile.phi_nu0/line.nu0**2 / tau_nu0
150
+ return line.line_profile.phi_nu(nu)/nu**2 / normalisation
151
+
152
+ def set_tau_nu_lines(self):
153
+ self.tau_nu_lines = []
154
+ for line_index,line in zip(self.nu_selected_line_indices,self.nu_selected_lines):
155
+ tau = self.construct_tau_nu_individual_line(
156
+ line=line,nu=self.nu,
157
+ tau_nu0=self.tau_nu0_individual_transitions[line_index])
158
+ self.tau_nu_lines.append(tau)
159
+
160
+ def set_tau_nu_tot(self):
161
+ #note that this function automatically takes overlapping lines into account
162
+ #note also that I cannot use the get_tau_tot_nu method of the
163
+ #emitting_molecule, because that method only considers a single line
164
+ #and its overlaps
165
+ self.tau_nu_tot = np.sum(self.tau_nu_lines,axis=0) + self.tau_dust_nu
166
+
167
+ def get_S_tot(self):
168
+ S_nu = np.zeros_like(self.nu)
169
+ for i,line in enumerate(self.nu_selected_lines):
170
+ x1 = self.level_population[line.low.number]
171
+ x2 = self.level_population[line.up.number]
172
+ S_line = line.source_function(x1=x1,x2=x2)
173
+ S_nu += self.tau_nu_lines[i]*S_line
174
+ S_nu += self.S_dust(self.nu)*self.tau_dust_nu
175
+ S_tot = np.where(self.tau_nu_tot==0,0,S_nu/self.tau_nu_tot)
176
+ return S_tot
177
+
178
+ def spectrum(self,solid_angle):
179
+ #note that this function gives the right answer also in cases where
180
+ #line overlap treatment is not necessary
181
+ if self.is_LVG_sphere:
182
+ #assumption for LVG sphere: lines do not overlap and there is no dust,
183
+ #so I can just sum them up
184
+ #this also works for overlapping lines in the optically thin regime
185
+ #reason for this special case: the formula to compute the flux for an LVG
186
+ #sphere depends on nu0, which is not well defined if there are several
187
+ #lines
188
+ if self.emitting_molecule.any_line_has_overlap(
189
+ line_indices=self.nu_selected_line_indices):
190
+ warnings.warn('LVG sphere geometry: lines are overlapping, '
191
+ +'output spectrum will only be correct if all lines'
192
+ +' are optically thin!')
193
+ for dust_func in (self.S_dust,self.tau_dust):
194
+ assert np.all(dust_func(self.nu)==0),'LVG does not support dust'
195
+ spectrum = np.zeros_like(self.nu)
196
+ for line_index,line,tau_nu_line in zip(self.nu_selected_line_indices,
197
+ self.nu_selected_lines,
198
+ self.tau_nu_lines):
199
+ LVG_sphere_kwargs = {'nu':self.nu,'nu0':line.nu0,'V':self.V_LVG_sphere}
200
+ source_function = helpers.B_nu(T=self.Tex[line_index],nu=self.nu)
201
+ spectrum += self.compute_flux_nu(
202
+ tau_nu=tau_nu_line,source_function=source_function,
203
+ solid_angle=solid_angle,**LVG_sphere_kwargs)
204
+ else:
205
+ source_function = self.get_S_tot()
206
+ spectrum = self.compute_flux_nu(
207
+ tau_nu=self.tau_nu_tot,source_function=source_function,
208
+ solid_angle=solid_angle)
209
+ return spectrum
pythonradex/helpers.py ADDED
@@ -0,0 +1,69 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import numpy as np
4
+ from scipy import constants
5
+ import numba as nb
6
+
7
+
8
+ @nb.jit(nopython=True,cache=True)
9
+ def B21(A21,nu):
10
+ return constants.c**2/(2*constants.h*nu**3)*A21
11
+
12
+ @nb.jit(nopython=True,cache=True)
13
+ def B12(A21,nu,g1,g2):
14
+ return g2/g1*B21(A21=A21,nu=nu)
15
+
16
+ @nb.jit(nopython=True,cache=True)
17
+ def B_nu(nu,T):
18
+ r"""Planck function (black body)
19
+
20
+ Args:
21
+ nu (:obj:`float` or numpy.ndarray): frequency in [Hz]
22
+ T (:obj:`float` or numpy.ndarray): temperature in [K]
23
+
24
+ Returns:
25
+ float or numpy.ndarray: value of Planck function in [W/m\ :sup:`2`/Hz/sr]
26
+ """
27
+ return (2*constants.h*nu**3/constants.c**2
28
+ *(np.exp(constants.h*nu/(constants.k*T))-1)**-1)
29
+
30
+ def generate_CMB_background(z=0):
31
+ r'''generates a function that gives the CMB background at redshift z
32
+
33
+ Args:
34
+ z: (:obj:`float`): redshift
35
+
36
+ Returns:
37
+ func: a function giving CMB background in [W/m\ :sup:`2`/Hz/sr] for an input frequency in [Hz]
38
+ '''
39
+ T_CMB = 2.73*(1+z)
40
+ def CMB_background(nu):
41
+ return B_nu(nu=nu,T=T_CMB)
42
+ return CMB_background
43
+
44
+ @nb.jit(nopython=True,cache=True)
45
+ def FWHM2sigma(FWHM):
46
+ """Convert FWHM of a Gaussian to standard deviation.
47
+
48
+ Args:
49
+ FWHM (:obj:`float` or numpy.ndarray): FWHM of the Gaussian
50
+
51
+ Returns:
52
+ float or numpy.ndarray: the standard deviation corresponding to the input FWHM"""
53
+ return FWHM/(2*np.sqrt(2*np.log(2)))
54
+
55
+ @nb.jit(nopython=True,cache=True)
56
+ def relative_difference(a,b):
57
+ """Computes the elementwise relative difference between a and b.
58
+ In general, return |a-b|/a.
59
+ Special cases:
60
+ a=0 and b=0: return 0
61
+ a=0 and b!=0: return 1"""
62
+ abs_diff = np.abs(a-b)
63
+ rel_diff = np.where((a==0) & (b==0),0,np.where(a==0,1,abs_diff/a))
64
+ assert not np.any(np.isnan(rel_diff))
65
+ return np.abs(rel_diff)
66
+
67
+ @nb.jit(nopython=True,cache=True)
68
+ def Delta_nu(Delta_v,nu0):
69
+ return nu0 * Delta_v / constants.c