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,129 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Sun Nov 12 17:05:01 2017
4
+
5
+ @author: gianni
6
+ """
7
+ from pythonradex import atomic_transition
8
+ from scipy import constants
9
+ import numpy as np
10
+
11
+ def is_comment(line):
12
+ if line.replace(' ','')[0] == '!':
13
+ return True
14
+ else:
15
+ return False
16
+
17
+ def read(datafilepath,read_frequencies,read_quantum_numbers=False):
18
+ '''
19
+ Read a LAMDA data file. LAMDA files can be downloaded from the LAMDA database
20
+ at http://home.strw.leidenuniv.nl/~moldata
21
+
22
+ Args:
23
+ datafilepath (:obj:`str`): The filepath to the LAMDA file.
24
+ read_frequencies (:obj:`bool`): Whether to read the frequencies of the
25
+ radiative transitions from the file or not. If False, calculates the
26
+ frequencies from the level energies given in the file. Setting this to
27
+ True can be useful since frequencies are sometimes given with more
28
+ significant digits than level energies. However, the LAMDA format does not
29
+ force a file to contain the frequencies, so they might not be present.
30
+ read_quantum_numbers (:obj:`bool`): Whether to read the quantum numbers
31
+ from the file. Defaults to False. The LAMDA format does not force a
32
+ file to list quantum numbers, so they might not be present.
33
+
34
+ Returns:
35
+ dict: A dictionary containing the data read from the file.
36
+ The following keys give acces to lists containing the data.
37
+ The elements of these lists are in the order they appear in the file.
38
+ 'levels' (a list of pythonradex.atomic_transition.Level objects);
39
+ 'radiative transitions' (a list of
40
+ pythonradex.atomic_transitions.RadiativeTransition objects);
41
+ 'collisional transitions' (a list of
42
+ pythonradex.atomic_transition.CollisionalTransition objects);
43
+ 'quantum numbers' (list of str, empty if read_quantum_numbers
44
+ was set to False)
45
+ '''
46
+ #identifiers used in the LAMDA database files:
47
+ LAMDA_coll_ID = {'1':'H2','2':'para-H2','3':'ortho-H2','4':'e',
48
+ '5':'H','6':'He','7':'H+'}
49
+ datafile = open(datafilepath,'r')
50
+ levels = []
51
+ rad_transitions = []
52
+ coll_transitions = {}
53
+ quantum_numbers = []
54
+ comment_offset = 0
55
+ for i,line in enumerate(datafile):
56
+ if is_comment(line) or line=='' or line=='\n':
57
+ comment_offset += 1
58
+ continue
59
+ if i == 0+comment_offset:
60
+ continue
61
+ if i == 1+comment_offset:
62
+ continue
63
+ if i == 2+comment_offset:
64
+ n_levels = int(line)
65
+ continue
66
+ if 3+comment_offset <= i < 3+comment_offset+n_levels:
67
+ line_entries = line.split()
68
+ leveldata = [float(string) for string in line_entries[:3]]
69
+ assert int(leveldata[0]) == i-2-comment_offset,\
70
+ 'level numeration not consistent'
71
+ #transforming energy from cm-1 to J; level numbers starting from 0:
72
+ lev = atomic_transition.Level(
73
+ g=leveldata[2],
74
+ E=constants.c*constants.h*leveldata[1]/constants.centi,
75
+ number=int(leveldata[0])-1)
76
+ levels.append(lev)
77
+ if read_quantum_numbers:
78
+ quantum_numbers.append(line_entries[3])
79
+ continue
80
+ if i == 3+comment_offset+n_levels:
81
+ n_rad_transitions = int(line)
82
+ continue
83
+ if 4+comment_offset+n_levels <= i < 4+comment_offset+n_levels+n_rad_transitions:
84
+ radtransdata = [float(string) for string in line.split()]
85
+ up = next(level for level in levels if level.number==radtransdata[1]-1)
86
+ low = next(level for level in levels if level.number==radtransdata[2]-1)
87
+ rad_trans_kwargs = {'up':up,'low':low,'A21':radtransdata[3]}
88
+ if read_frequencies:
89
+ nu0 = radtransdata[4]*constants.giga
90
+ Delta_E = up.E-low.E
91
+ assert np.isclose(nu0,Delta_E/constants.h,atol=0,rtol=1e-3),\
92
+ 'read frequency is inconsistent with level energies'
93
+ rad_trans_kwargs['nu0'] = nu0
94
+ rad_trans = atomic_transition.RadiativeTransition(**rad_trans_kwargs)
95
+ rad_transitions.append(rad_trans)
96
+ continue
97
+ if i == 4+comment_offset+n_levels+n_rad_transitions:
98
+ coll_partner_offset = 0
99
+ continue
100
+ if i == 5+comment_offset+n_levels+n_rad_transitions + coll_partner_offset:
101
+ coll_ID = LAMDA_coll_ID[line[0]]
102
+ coll_transitions[coll_ID] = []
103
+ continue
104
+ if i == 6+comment_offset+n_levels+n_rad_transitions + coll_partner_offset:
105
+ n_coll_transitions = int(line)
106
+ continue
107
+ if i == 7+comment_offset+n_levels+n_rad_transitions + coll_partner_offset:
108
+ continue #this lines contains the number of temperature elements
109
+ if i == 8+comment_offset+n_levels+n_rad_transitions + coll_partner_offset:
110
+ coll_temperatures = np.array([float(string) for string in line.split()])
111
+ continue
112
+ last_coll_trans_i = 9+comment_offset+n_levels+n_rad_transitions\
113
+ +coll_partner_offset+n_coll_transitions-1
114
+ if 9+comment_offset+n_levels+n_rad_transitions+coll_partner_offset <= i <=\
115
+ last_coll_trans_i:
116
+ coll_trans_data = [float(string) for string in line.split()]
117
+ up = next(level for level in levels if level.number==coll_trans_data[1]-1)
118
+ low = next(level for level in levels if level.number==coll_trans_data[2]-1)
119
+ K21_data = np.array(coll_trans_data[3:])*constants.centi**3
120
+ coll_trans = atomic_transition.CollisionalTransition(
121
+ up=up,low=low,K21_data=K21_data,
122
+ Tkin_data=coll_temperatures)
123
+ coll_transitions[coll_ID].append(coll_trans)
124
+ if i == last_coll_trans_i:
125
+ coll_partner_offset += 4+n_coll_transitions
126
+ continue
127
+ datafile.close()
128
+ return {'levels':levels,'radiative transitions':rad_transitions,
129
+ 'collisional transitions':coll_transitions,'quantum numbers':quantum_numbers}
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2025-present Gianni Cataldi <cataldi.gia@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.0.1"
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Python implementation of the RADEX radiative transfer code
4
+ """
@@ -0,0 +1,351 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Sun Nov 12 17:03:55 2017
4
+
5
+ @author: gianni
6
+ """
7
+
8
+ from pythonradex import helpers
9
+ import numpy as np
10
+ from scipy import constants
11
+ import numba as nb
12
+
13
+
14
+ @nb.jit(nopython=True,cache=True)
15
+ def tau_nu(A21,phi_nu,g_low,g_up,N1,N2,nu):
16
+ return constants.c**2/(8*np.pi*nu**2)*A21*phi_nu*(g_up/g_low*N1-N2)
17
+
18
+ @nb.jit(nopython=True,cache=True,error_model='numpy')
19
+ def Tex(Delta_E,g_low,g_up,x1,x2):
20
+ return np.where((x1==0) & (x2==0),0,
21
+ -Delta_E/(constants.k*np.log(g_low*x2/(g_up*x1))))
22
+
23
+ @nb.jit(nopython=True,cache=True)
24
+ def compute_K12(K21,g_up,g_low,Delta_E,Tkin):
25
+ #see RADEX manual
26
+ return (g_up/g_low*K21*np.exp(-Delta_E/(constants.k*Tkin)))
27
+
28
+
29
+ class LineProfile():
30
+
31
+ '''Abstract class representing a general, normalised line profile.'''
32
+
33
+ n_nu_elements_for_coarse_array = 10
34
+
35
+ def __init__(self,nu0,width_v):
36
+ self.nu0 = nu0
37
+ self.width_v = width_v
38
+ self.width_nu = helpers.Delta_nu(Delta_v=self.width_v,nu0=self.nu0)
39
+ self.initialise_phi_nu_params()
40
+ # self.initialise_coarse_nu_array()
41
+ # self.coarse_phi_nu_array = self.phi_nu(nu=self.coarse_nu_array)
42
+ self.phi_nu0 = self.phi_nu(nu=self.nu0)
43
+
44
+ def initialise_phi_nu_params(self):
45
+ raise NotImplementedError
46
+
47
+ def phi_nu(self,nu):
48
+ r'''The value of the line profile in frequency space.
49
+
50
+ The line profile is normalised such that its integral over frequency equals 1.
51
+
52
+ Args:
53
+ nu (float or numpy.ndarray): frequency in [Hz]
54
+
55
+ Returns:
56
+ float or numpy.ndarray: The line profile in [Hz\ :sup:`-1`]
57
+ '''
58
+ raise NotImplementedError
59
+
60
+ def phi_v(self,v):
61
+ r'''The value of the line profile in velocity space.
62
+
63
+ The line profile is normalised such that its integral over velocity equals 1.
64
+
65
+ Args:
66
+ v (float or numpy.ndarray): velocity in [m/s]
67
+
68
+ Returns:
69
+ float or numpy.ndarra: The line profile in [(m/s)\ :sup:`-1`]
70
+ '''
71
+ nu = self.nu0*(1-v/constants.c)
72
+ return self.phi_nu(nu)*self.nu0/constants.c
73
+
74
+ def average_over_phi_nu(self,func):
75
+ raise NotImplementedError
76
+
77
+
78
+ class GaussianLineProfile(LineProfile):
79
+
80
+ """Represents a Gaussian line profile
81
+
82
+ Attributes:
83
+ nu0 (:obj:`float`): rest frequency in [Hz]
84
+ width_nu (:obj:`float`): FWHM in [Hz]
85
+ width_v (:obj:`float`): FWHM in [m/s]
86
+ """
87
+
88
+ def initialise_phi_nu_params(self):
89
+ self.sigma_nu = helpers.FWHM2sigma(self.width_nu)
90
+ self.normalisation = 1/(self.sigma_nu*np.sqrt(2*np.pi))
91
+
92
+ def phi_nu(self,nu):
93
+ return self.normalisation*np.exp(-(nu-self.nu0)**2/(2*self.sigma_nu**2))
94
+
95
+ def average_over_phi_nu(self,func):
96
+ #choose +- 1 FWHM initially, where the Gaussian is at ~6% of the peak
97
+ #note that at least two iterations are done, so the final width is
98
+ #at least +-1.65 FWHM
99
+ width = 2 #units of width_nu
100
+ elements_per_width = 20
101
+ old_average = np.inf
102
+ residual = np.inf
103
+ niter = 0
104
+ while residual > 1e-2:
105
+ n_elements = int(elements_per_width*width)
106
+ nu = np.linspace(self.nu0-width/2*self.width_nu,
107
+ self.nu0+width/2*self.width_nu,n_elements)
108
+ phi_nu = self.phi_nu(nu)
109
+ norm = np.trapezoid(y=phi_nu,x=nu)
110
+ new_average = np.trapezoid(y=func(nu)*phi_nu,x=nu)/norm
111
+ residual = helpers.relative_difference(a=np.array((new_average,)),
112
+ b=np.array((old_average,)))
113
+ old_average = new_average
114
+ #I want to cover at least the width corresponding to the definition of
115
+ #overlapping lines, which is 3.14; so add 1.3 to make it 2+1.3=3.3
116
+ width += 1.3
117
+ elements_per_width += 15
118
+ niter += 1
119
+ return new_average
120
+
121
+
122
+ class RectangularLineProfile(LineProfile):
123
+
124
+ '''Represents a rectangular line profile, i.e. constant over width_v, 0 outside'''
125
+
126
+ def initialise_phi_nu_params(self):
127
+ self.normalisation = 1/self.width_nu
128
+
129
+ def phi_nu(self,nu):
130
+ inside_line = (nu>=self.nu0-self.width_nu/2) & (nu<=self.nu0+self.width_nu/2)
131
+ return np.where(inside_line,self.normalisation,0)
132
+
133
+ def average_over_phi_nu(self,func):
134
+ #easier than Gaussian, just have to iterate over n_elements
135
+ #note that the func doesn't need to be constant over the line profile,
136
+ #so I need to have a sufficient number of elements
137
+ n_elements = 20
138
+ old_average = np.inf
139
+ residual = np.inf
140
+ while residual > 1e-2:
141
+ nu = np.linspace(self.nu0-self.width_nu/2,self.nu0+self.width_nu/2,
142
+ n_elements)
143
+ phi_nu = self.phi_nu(nu)
144
+ norm = np.trapezoid(y=phi_nu,x=nu)
145
+ new_average = np.trapezoid(y=func(nu)*phi_nu,x=nu)/norm
146
+ residual = helpers.relative_difference(a=np.array((new_average,)),
147
+ b=np.array((old_average,)))
148
+ old_average = new_average
149
+ n_elements += 15
150
+ return new_average
151
+
152
+ line_profiles = {'Gaussian':GaussianLineProfile,'rectangular':RectangularLineProfile}
153
+
154
+
155
+ class Level():
156
+ '''Represents an atomic / molecular level.
157
+
158
+ Attributes:
159
+ g (:obj:`float`): the statistical weight of the level
160
+ E (:obj:`float`): the energy of the level in [J]
161
+ number (:obj:`int`): the number of the level
162
+ '''
163
+
164
+ def __init__(self,g,E,number):
165
+ self.g = g
166
+ self.E = E
167
+ self.number = number
168
+
169
+ def LTE_level_pop(self,Z,T):
170
+ '''Calculates the fractional population of the level in LTE.
171
+
172
+ Args:
173
+ Z (:obj:`float`): The partition function.
174
+ T (:obj:`float`): The temperature in [K].
175
+
176
+ Returns:
177
+ float: The fractional population of the level in LTE.
178
+ '''
179
+ #this is a convenience function that is not used when solving the non-LTE
180
+ #radiative transfer
181
+ return self.g*np.exp(-self.E/(constants.k*T))/Z
182
+
183
+
184
+ class Transition():
185
+
186
+ def __init__(self,up,low):
187
+ self.up = up
188
+ self.low = low
189
+ self.Delta_E = self.up.E-self.low.E
190
+ self.name = f'{self.up.number}-{self.low.number}'
191
+
192
+ def Tex(self,x1,x2):
193
+ '''Computes the excitation temperature.
194
+
195
+ Args:
196
+ x1: (numpy.ndarray): fractional population of the lower level
197
+ x2: (numpy.ndarray): fractional population of the upper level
198
+
199
+ Returns:
200
+ numpy.ndarray: excitation temperature in [K]
201
+ '''
202
+ return Tex(Delta_E=self.Delta_E,g_up=self.up.g,g_low=self.low.g,
203
+ x1=x1,x2=x2)
204
+
205
+
206
+ class RadiativeTransition(Transition):
207
+
208
+ r'''Represents the radiative transition between two energy levels.
209
+
210
+ Attributes:
211
+ up (pythonradex.atomic_transition.Level): the upper level of the transition
212
+ low (pythonradex.atomic_transition.Level): the lower level of the transition
213
+ Delta_E (:obj:`float`): the energy difference between the upper and lower level
214
+ name (:obj:`str`): name of the transition
215
+ nu0 (:obj:`float`): rest frequency in [Hz]
216
+ A21 (:obj:`float`): Einstein A21 coefficient in [s\ :sup:`-1`]
217
+ B21 (:obj:`float`): Einstein B21 coefficient in [sr m\ :sup:`2` Hz / Jy]
218
+ B12 (:obj:`float`): Einstein B12 coefficient in [sr m\ :sup:`2` Hz / Jy]
219
+ '''
220
+
221
+ def __init__(self,up,low,A21,nu0=None):
222
+ Transition.__init__(self,up=up,low=low)
223
+ assert self.Delta_E > 0, 'non-positive Delta_E for radiative transition'
224
+ self.A21 = A21
225
+ nu0_from_Delta_E = self.Delta_E/constants.h
226
+ if nu0 is None:
227
+ self.nu0 = nu0_from_Delta_E
228
+ else:
229
+ #when reading LAMDA files, it is useful to specify nu0 directly, since
230
+ #it is sometimes given with more significant digits
231
+ assert np.isclose(nu0_from_Delta_E,nu0,atol=0,rtol=1e-3)
232
+ self.nu0 = nu0
233
+ self.B21 = helpers.B21(A21=self.A21,nu=self.nu0)
234
+ self.B12 = helpers.B12(A21=self.A21,nu=self.nu0,g1=self.low.g,g2=self.up.g)
235
+
236
+ def source_function(self,x1,x2):
237
+ S = np.where((x1==0) & (x2==0),0,self.A21*x2/(x1*self.B12-x2*self.B21))
238
+ # if np.any(S < 0):
239
+ # raise RuntimeError('negative source function, potentially due to '
240
+ # +'high columndensity and/or low collider density')
241
+ return S
242
+
243
+
244
+ class EmissionLine(RadiativeTransition):
245
+
246
+ r'''Represents an emission line arising from the radiative transition between
247
+ two levels
248
+
249
+ Attributes:
250
+ up (pythonradex.atomic_transition.Level): the upper level of the transition
251
+ low (pythonradex.atomic_transition.Level): the lower level of the transition
252
+ Delta_E (:obj:`float`): the energy difference between the upper and lower level
253
+ in [J]
254
+ name (:obj:`str`): name of the transition
255
+ nu0 (:obj:`float`): rest frequency in [Hz]
256
+ A21 (:obj:`float`): Einstein A21 coefficient in [s\ :sup:`-1`]
257
+ B21 (:obj:`float`): Einstein B21 coefficient in [sr m\ :sup:`2` Hz / Jy]
258
+ B12 (:obj:`float`): Einstein B12 coefficient in [sr m\ :sup:`2` Hz / Jy]
259
+ line_profile (pythonradex.atomic_transition.LineProfile): object representing
260
+ the shape of the line profile
261
+ '''
262
+ def __init__(self,up,low,A21,line_profile_type,width_v,nu0=None):
263
+ RadiativeTransition.__init__(self,up=up,low=low,A21=A21,nu0=nu0)
264
+ self.line_profile = line_profiles[line_profile_type](nu0=self.nu0,width_v=width_v)
265
+ self.tau_kwargs = {'A21':self.A21,'g_up':self.up.g,'g_low':self.low.g}
266
+
267
+ @classmethod
268
+ def from_radiative_transition(cls,radiative_transition,line_profile_type,
269
+ width_v):
270
+ '''Alternative constructor, taking an instance of RadiativeTransition,
271
+ a line profile class and the width of the line'''
272
+ return cls(up=radiative_transition.up,low=radiative_transition.low,
273
+ A21=radiative_transition.A21,line_profile_type=line_profile_type,
274
+ width_v=width_v,nu0=radiative_transition.nu0)
275
+
276
+ def tau_nu(self,N1,N2,nu):
277
+ r'''Computes the optical depth
278
+
279
+ Args:
280
+ N1: (:obj:`float`): column density of molecules in the lower level
281
+ in [m\ :sup:`-2`]
282
+ N2: (:obj:`float`): column density of molecules in the upper level
283
+ in [m\ :sup:`-2`]
284
+ nu: (numpy.ndarray): frequencies in [Hz]
285
+
286
+ Returns:
287
+ numpy.ndarray: the optical depth at the requested frequencies
288
+
289
+ '''
290
+ return tau_nu(phi_nu=self.line_profile.phi_nu(nu),N1=N1,N2=N2,nu=nu,
291
+ **self.tau_kwargs)
292
+
293
+ def tau_nu0(self,N1,N2):
294
+ r'''Computes the optical depth at the rest frequency
295
+
296
+ Args:
297
+ N1: (:obj:`float` or numpy.ndarray): column density of molecules
298
+ in the lower level in [m\ :sup:`-2`]
299
+ N2: (:obj:`float` or numpy.ndarray): column density of molecules
300
+ in the upper level in [m\ :sup:`-2`]
301
+
302
+ Returns:
303
+ float or numpy.ndarray: the optical depth at the rest frequency
304
+
305
+ '''
306
+ return tau_nu(phi_nu=self.line_profile.phi_nu0,N1=N1,N2=N2,nu=self.nu0,
307
+ **self.tau_kwargs)
308
+
309
+
310
+ class CollisionalTransition(Transition):
311
+
312
+ '''Represent the collisional transition between two energy levels
313
+
314
+ Attributes:
315
+ up (pythonradex.atomic_transition.Level): the upper level of the transition
316
+ low (pythonradex.atomic_transition.Level): the lower level of the transition
317
+ Delta_E (:obj:`float`): the energy difference between the upper and lower level
318
+ in [J]
319
+ name (:obj:`str`): name of the transition
320
+ '''
321
+
322
+ def __init__(self,up,low,K21_data,Tkin_data):
323
+ Transition.__init__(self,up=up,low=low)
324
+ assert np.all(K21_data >= 0)
325
+ self.K21_data = K21_data
326
+ self.Tkin_data = Tkin_data
327
+ self.Tkin_data_limits = np.min(self.Tkin_data),np.max(self.Tkin_data)
328
+
329
+ def coeffs(self,Tkin):
330
+ r'''
331
+ Computes the collisional coefficients
332
+
333
+ Args:
334
+ Tkin (float or numpy.ndarray): kinetic temperature in [K]
335
+
336
+ Returns:
337
+ tuple: The collision coefficients K12 and K21 in [m\ :sup:`3`/s]
338
+ at the requested temperature(s)
339
+
340
+ Raises:
341
+ AssertionError: If Tkin is outside the available temperature range.
342
+ '''
343
+ Tmin,Tmax = self.Tkin_data_limits
344
+ assert np.all(Tmin <= Tkin),\
345
+ 'requested temperature below minimum collider temperature'
346
+ assert np.all(Tkin <= Tmax),\
347
+ 'requested temperature above maximum collider temperature'
348
+ K21 = np.interp(Tkin,self.Tkin_data,self.K21_data)
349
+ K12 = compute_K12(K21=K21,g_up=self.up.g,g_low=self.low.g,Delta_E=self.Delta_E,
350
+ Tkin=Tkin)
351
+ return [K12,K21]
@@ -0,0 +1,119 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Sun Nov 12 17:31:02 2017
4
+
5
+ @author: gianni
6
+ """
7
+ import numpy as np
8
+ import numba as nb
9
+ from pythonradex import escape_probability_functions
10
+ from scipy import constants
11
+
12
+
13
+ class EscapeProbabilityUniformSphere():
14
+
15
+ '''Represents the escape probability from a uniform spherical medium.'''
16
+
17
+ def __init__(self):
18
+ self.beta = escape_probability_functions.beta_uniform_sphere
19
+
20
+
21
+ class UniformSphere(EscapeProbabilityUniformSphere):
22
+
23
+ '''Represents the escape probability and emerging flux from a uniform
24
+ spherical medium'''
25
+
26
+ @staticmethod
27
+ @nb.jit(nopython=True,cache=True)
28
+ def compute_flux_nu(tau_nu,source_function,solid_angle):
29
+ '''Computes the observed flux in [W/m2/Hz], given the optical depth
30
+ tau_nu, the source function in [W/m2/Hz/sr] and the solid angle in [sr].'''
31
+ #see old Osterbrock book for this formula, appendix 2
32
+ #this is the flux per surface of the emitting region
33
+ #for lower tau_nu, the Osterbrock formula becomes numerically unstable,
34
+ #so we use a Taylor expansion
35
+ min_tau_nu = 1e-2
36
+ stable_region = tau_nu > min_tau_nu
37
+ flux_nu = 2*np.pi*source_function/tau_nu**2\
38
+ *(tau_nu**2/2-1+(tau_nu+1)*np.exp(-tau_nu))
39
+ flux_nu_Taylor = 2*np.pi*source_function*(tau_nu/3-tau_nu**2/8+tau_nu**3/30
40
+ -tau_nu**4/144) #from Wolfram Alpha
41
+ flux_nu = np.where(stable_region,flux_nu,flux_nu_Taylor)
42
+ assert np.all(np.isfinite(flux_nu))
43
+ #the observed flux is (flux at sphere surface)*4*pi*r**2/(4*pi*d**2)
44
+ #=F_surface*Omega*4/(4*pi)
45
+ return flux_nu*solid_angle/np.pi
46
+
47
+ @nb.jit(nopython=True,cache=True)
48
+ def exp_tau_factor(tau_nu):
49
+ #for very small tau, the exp can introduce numerical errors, so we
50
+ #take care of that when computing (1-exp(-tau))
51
+ return np.where(tau_nu<1e-5,tau_nu,1-np.exp(-tau_nu))
52
+
53
+
54
+ class Flux1D():
55
+
56
+ '''Represents the computation of the flux when considering only a single direction'''
57
+
58
+ @staticmethod
59
+ @nb.jit(nopython=True,cache=True)
60
+ def compute_flux_nu(tau_nu,source_function,solid_angle):
61
+ '''Computes the observed flux in [W/m2/Hz], given the optical depth
62
+ tau_nu, the source function in [W/m2/Hz/sr] and the solid angle of
63
+ the source (in [sr]) seen by the observer.'''
64
+ tau_factor = exp_tau_factor(tau_nu=tau_nu)
65
+ return source_function*tau_factor*solid_angle
66
+
67
+
68
+ class UniformSphereRADEX(EscapeProbabilityUniformSphere,Flux1D):
69
+ """Represents the escape probability from a uniform sphere, but uses the
70
+ single direction assumption to compute the emerging flux. This is what is
71
+ done in the original RADEX code."""
72
+ #see line 288 and following in io.f of RADEX
73
+
74
+ pass
75
+
76
+
77
+ class UniformSlab(Flux1D):
78
+ #Since I assume the source is in the far field, it is ok to calculate the flux
79
+ #with the 1D formula
80
+ '''Represents the escape probability and emerging flux from a uniform
81
+ slab'''
82
+
83
+ def __init__(self):
84
+ self.beta = escape_probability_functions.beta_uniform_slab
85
+
86
+
87
+ class UniformLVGSlab(Flux1D):
88
+ """The escape probability and flux for a uniform large velocity gradient (LVG)
89
+ slab"""
90
+
91
+ def __init__(self):
92
+ self.beta = escape_probability_functions.beta_LVG_slab
93
+
94
+
95
+ class UniformLVGSphere():
96
+ """The escape probability and flux for a uniform large velocity gradient (LVG)
97
+ sphere"""
98
+
99
+ def __init__(self):
100
+ self.beta = escape_probability_functions.beta_LVG_sphere
101
+
102
+ @staticmethod
103
+ @nb.jit(nopython=True,cache=True)
104
+ def compute_flux_nu(tau_nu,source_function,solid_angle,nu,nu0,V):
105
+ #V is the velocity at the surface of the sphere
106
+ #this formula can be derived by using an approach similar to
107
+ #de Jong et al. (1975, Fig. 3)
108
+ tau_factor = exp_tau_factor(tau_nu=tau_nu)
109
+ v = constants.c*(1-nu/nu0)
110
+ return source_function*tau_factor*solid_angle*(1-(v/V)**2)
111
+
112
+
113
+ class LVGSphereRADEX(Flux1D):
114
+ """The escape probability and flux for a large velocity gradient (LVG)
115
+ sphere (Expanding sphere in RADEX terminology), using the same
116
+ formula for the escape probability as the RADEX code"""
117
+
118
+ def __init__(self):
119
+ self.beta = escape_probability_functions.beta_LVG_sphere_RADEX