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.
- pythonradex/LAMDA_file.py +129 -0
- pythonradex/__about__.py +4 -0
- pythonradex/__init__.py +4 -0
- pythonradex/atomic_transition.py +351 -0
- pythonradex/escape_probability.py +119 -0
- pythonradex/escape_probability_functions.py +198 -0
- pythonradex/flux.py +209 -0
- pythonradex/helpers.py +69 -0
- pythonradex/molecule.py +360 -0
- pythonradex/radiative_transfer.py +548 -0
- pythonradex/rate_equations.py +308 -0
- pythonradex-1.0.0.dist-info/METADATA +28 -0
- pythonradex-1.0.0.dist-info/RECORD +15 -0
- pythonradex-1.0.0.dist-info/WHEEL +4 -0
- pythonradex-1.0.0.dist-info/licenses/LICENSE.txt +21 -0
pythonradex/molecule.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created on Sun Nov 12 16:48:00 2017
|
|
4
|
+
|
|
5
|
+
@author: gianni
|
|
6
|
+
"""
|
|
7
|
+
from scipy import constants
|
|
8
|
+
import numpy as np
|
|
9
|
+
from pythonradex import LAMDA_file,atomic_transition
|
|
10
|
+
import numba as nb
|
|
11
|
+
|
|
12
|
+
#Note: the partition function is just for convenience when using the Molecule class
|
|
13
|
+
#directly; it is not used to solve the non-LTE radiative transfer
|
|
14
|
+
|
|
15
|
+
class Molecule():
|
|
16
|
+
|
|
17
|
+
'''Represents an atom or molecule with data read from a LAMDA file
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
levels (list of pythonradex.atomic_transition.Level): The energy levels
|
|
21
|
+
of the molecule, in the same order as in the LAMDA file.
|
|
22
|
+
rad_transitions (list of pythonradex.atomic_transition.RadiativeTransition):
|
|
23
|
+
The radiative transitions of the molecule, in the same order as in the
|
|
24
|
+
LAMDA file.
|
|
25
|
+
coll_transitions (dict): The collisional transitions of the molecule. The
|
|
26
|
+
dictionary keys correspond to the different colliders. Each entry of
|
|
27
|
+
the dictionary is a list of pythonradex.atomic_transition.CollisionalTransition
|
|
28
|
+
objects, in the same order as in the LAMDA file.
|
|
29
|
+
n_levels (:obj:`int`): the total number of levels
|
|
30
|
+
n_rad_transitions (:obj:`int`): the total number of radiative transitions
|
|
31
|
+
|
|
32
|
+
'''
|
|
33
|
+
|
|
34
|
+
def __init__(self,datafilepath,partition_function=None):
|
|
35
|
+
"""Constructs a new instance of the Molecule class using a LAMDA datafile
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
datafilepath (:obj:`str`): The filepath to the LAMDA file.
|
|
39
|
+
partition_function (func): A user-supplied partition function of one argument
|
|
40
|
+
(temperature). Defaults to None. If equal to None, the partition function
|
|
41
|
+
will be calculated by using the data from the provided LAMDA file.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
#reading frequencies can be useful since frequencies are sometimes
|
|
45
|
+
#given with more significant digits than level energies. However,
|
|
46
|
+
#the LAMDA format does not orce a file to contain the frequencies,
|
|
47
|
+
#so they might not be present.
|
|
48
|
+
data = LAMDA_file.read(datafilepath=datafilepath,
|
|
49
|
+
read_frequencies=True)
|
|
50
|
+
except:
|
|
51
|
+
data = LAMDA_file.read(datafilepath=datafilepath,
|
|
52
|
+
read_frequencies=False)
|
|
53
|
+
self.levels = data['levels']
|
|
54
|
+
self.rad_transitions = data['radiative transitions']
|
|
55
|
+
self.coll_transitions = data['collisional transitions']
|
|
56
|
+
self.n_levels = len(self.levels)
|
|
57
|
+
self.n_rad_transitions = len(self.rad_transitions)
|
|
58
|
+
self.set_partition_function(partition_function=partition_function)
|
|
59
|
+
|
|
60
|
+
def set_partition_function(self,partition_function):
|
|
61
|
+
if partition_function is None:
|
|
62
|
+
self.Z = self.Z_from_atomic_data
|
|
63
|
+
else:
|
|
64
|
+
self.Z = partition_function
|
|
65
|
+
|
|
66
|
+
def Z_from_atomic_data(self,T):
|
|
67
|
+
T = np.array(T)
|
|
68
|
+
weights = np.array([l.g for l in self.levels])
|
|
69
|
+
energies = np.array([l.E for l in self.levels])
|
|
70
|
+
if T.ndim > 0:
|
|
71
|
+
shape = [self.n_levels,]+[1 for i in range(T.ndim)] #needs to come before T is modified
|
|
72
|
+
T = np.expand_dims(T,axis=0) #insert new axis at first position (axis=0)
|
|
73
|
+
weights = weights.reshape(shape)
|
|
74
|
+
energies = energies.reshape(shape)
|
|
75
|
+
return np.sum(weights*np.exp(-energies/(constants.k*T)),axis=0)
|
|
76
|
+
|
|
77
|
+
def LTE_level_pop(self,T):
|
|
78
|
+
'''Computes the level populations in LTE for all levels
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
T (:obj:`float` or numpy.ndarray): The temperature.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
numpy.ndarray: The fractional level population. If only one temperature
|
|
85
|
+
was given as input, the output array is one-dimensional, with the level
|
|
86
|
+
populations corresponding to the levels in the order of the LAMDA file.
|
|
87
|
+
If an array of temperatures was given as input, the output array has
|
|
88
|
+
two dimensions, with the second corresponding to the different temperatures.
|
|
89
|
+
|
|
90
|
+
'''
|
|
91
|
+
T = np.array(T)
|
|
92
|
+
Z = self.Z(T)
|
|
93
|
+
pops = [l.LTE_level_pop(T=T,Z=Z) for l in self.levels]
|
|
94
|
+
if T.ndim > 0:
|
|
95
|
+
shape = [1,]+list(T.shape)
|
|
96
|
+
return np.concatenate([p.reshape(shape) for p in pops],axis=0)
|
|
97
|
+
else:
|
|
98
|
+
return np.array(pops)
|
|
99
|
+
|
|
100
|
+
def get_rad_transition_number(self,transition_name):
|
|
101
|
+
'''Returns the transition number for a given transition name'''
|
|
102
|
+
candidate_numbers = [i for i,line in enumerate(self.rad_transitions) if
|
|
103
|
+
line.name==transition_name]
|
|
104
|
+
assert len(candidate_numbers) == 1
|
|
105
|
+
return candidate_numbers[0]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class EmittingMolecule(Molecule):
|
|
109
|
+
|
|
110
|
+
'''Represents a an emitting molecule, i.e. a molecule with a specified line profile
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
levels (list of pythonradex.atomic_transition.Level): The energy levels
|
|
114
|
+
of the molecule, in the same order as in the LAMDA file.
|
|
115
|
+
rad_transitions (list of pythonradex.atomic_transition.EmissionLine):
|
|
116
|
+
The radiative transitions of the molecule, in the same order as in the
|
|
117
|
+
LAMDA file.
|
|
118
|
+
coll_transitions (dict): The collisional transitions of the molecule. The
|
|
119
|
+
dictionary keys correspond to the different colliders. Each entry of
|
|
120
|
+
the dictionary is a list of pythonradex.atomic_transition.CollisionalTransition
|
|
121
|
+
objects, in the same order as in the LAMDA file.
|
|
122
|
+
n_levels (:obj:`int`): the total number of levels
|
|
123
|
+
n_rad_transitions (:obj:`int`): the total number of radiative transitions
|
|
124
|
+
'''
|
|
125
|
+
|
|
126
|
+
def __init__(self,datafilepath,line_profile_type,width_v,read_frequencies=False,
|
|
127
|
+
partition_function=None):
|
|
128
|
+
"""Constructs a new instance of the EmittingMolecule class using a LAMDA datafile
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
datafilepath (:obj:`str`): The filepath to the LAMDA file.
|
|
132
|
+
line_profile_type (:obj:`str`): The type of the line profile. Either
|
|
133
|
+
'rectangular' or 'Gaussian'
|
|
134
|
+
width_v (:obj:`float`): The width of the line profile in [m/s]. For a Gaussian
|
|
135
|
+
line profile, this corresponds to the FWHM.
|
|
136
|
+
read_frequencies (:obj:`bool`): Whether to read the frequencies of the
|
|
137
|
+
radiative transitions from the file or not. If False, calculates the
|
|
138
|
+
frequencies from the level energies given in the file. Setting this to
|
|
139
|
+
True can be useful since frequencies are sometimes given with more
|
|
140
|
+
significant digits than level energies. However, the LAMDA format does not
|
|
141
|
+
force a file to contain the frequencies, so they might not be present.
|
|
142
|
+
partition_function (func): A user-supplied partition function of one argument
|
|
143
|
+
(temperature). Defaults to None. If equal to None, the partition function
|
|
144
|
+
will be calculated by using the data from the provided LAMDA file.
|
|
145
|
+
"""
|
|
146
|
+
Molecule.__init__(self,datafilepath=datafilepath,
|
|
147
|
+
partition_function=partition_function)
|
|
148
|
+
self.width_v = width_v
|
|
149
|
+
self.line_profile_type = line_profile_type
|
|
150
|
+
#convert radiative transitions to emission lines (but keep the same attribute name)
|
|
151
|
+
self.rad_transitions = [atomic_transition.EmissionLine.from_radiative_transition(
|
|
152
|
+
radiative_transition=rad_trans,
|
|
153
|
+
line_profile_type=line_profile_type,width_v=width_v)
|
|
154
|
+
for rad_trans in self.rad_transitions]
|
|
155
|
+
self.identify_overlapping_lines()
|
|
156
|
+
if self.any_line_has_overlap(line_indices=range(self.n_rad_transitions)):
|
|
157
|
+
self.has_overlapping_lines = True
|
|
158
|
+
else:
|
|
159
|
+
self.has_overlapping_lines = False
|
|
160
|
+
self.Tkin_data = {}
|
|
161
|
+
self.Tkin_data_limits = {}
|
|
162
|
+
for collider,coll_transitions in self.coll_transitions.items():
|
|
163
|
+
self.Tkin_data[collider] = coll_transitions[0].Tkin_data
|
|
164
|
+
assert np.all(self.Tkin_data[collider][:-1] < self.Tkin_data[collider][1:])
|
|
165
|
+
for coll_trans in coll_transitions:
|
|
166
|
+
assert np.all(coll_trans.Tkin_data==self.Tkin_data[collider])
|
|
167
|
+
self.Tkin_data_limits[collider] = np.min(self.Tkin_data[collider]),\
|
|
168
|
+
np.max(self.Tkin_data[collider])
|
|
169
|
+
self.set_K21_matrix()
|
|
170
|
+
#collecting parameters, needed for numba-compiled functions:
|
|
171
|
+
self.A21 = np.array([line.A21 for line in self.rad_transitions])
|
|
172
|
+
self.B21 = np.array([line.B21 for line in self.rad_transitions])
|
|
173
|
+
self.B12 = np.array([line.B12 for line in self.rad_transitions])
|
|
174
|
+
self.nu0 = np.array([line.nu0 for line in self.rad_transitions])
|
|
175
|
+
self.phi_nu0 = np.array([line.line_profile.phi_nu(line.nu0) for line
|
|
176
|
+
in self.rad_transitions])
|
|
177
|
+
self.gup_rad_transitions = np.array([line.up.g for line in self.rad_transitions])
|
|
178
|
+
self.glow_rad_transitions = np.array([line.low.g for line in self.rad_transitions])
|
|
179
|
+
self.nlow_rad_transitions = np.array([line.low.number for line in
|
|
180
|
+
self.rad_transitions])
|
|
181
|
+
self.nup_rad_transitions = np.array([line.up.number for line in
|
|
182
|
+
self.rad_transitions])
|
|
183
|
+
self.Delta_E_rad_transitions = np.array([line.Delta_E for line in
|
|
184
|
+
self.rad_transitions])
|
|
185
|
+
self.coll_gups = {}
|
|
186
|
+
self.coll_glows = {}
|
|
187
|
+
self.coll_DeltaEs = {}
|
|
188
|
+
self.coll_nup = {}
|
|
189
|
+
self.coll_nlow = {}
|
|
190
|
+
for collider,coll_transitions in self.coll_transitions.items():
|
|
191
|
+
self.coll_glows[collider] = np.array([c.low.g for c in coll_transitions])
|
|
192
|
+
self.coll_gups[collider] = np.array([c.up.g for c in coll_transitions])
|
|
193
|
+
self.coll_DeltaEs[collider] = np.array([c.Delta_E for c in
|
|
194
|
+
coll_transitions])
|
|
195
|
+
self.coll_nlow[collider] = np.array([c.low.number for c in coll_transitions])
|
|
196
|
+
self.coll_nup[collider] = np.array([c.up.number for c in coll_transitions])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_tau_nu0_lines(self,N,level_population):
|
|
200
|
+
r'''Compute the optical depth at the rest frequency of all lines (dust
|
|
201
|
+
and overlapping lines are ignored)
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
N (:obj:`float`): the column density in [m\ :sup:`-2`]
|
|
205
|
+
level_population (numpy.ndarray): the fractional population of each level,
|
|
206
|
+
where the levels are in the order of the LAMDA file
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
numpy.ndarray: the optical depth at the rest frequency
|
|
210
|
+
'''
|
|
211
|
+
N1 = level_population[self.nlow_rad_transitions]*N
|
|
212
|
+
N2 = level_population[self.nup_rad_transitions]*N
|
|
213
|
+
tau_nu0 = atomic_transition.tau_nu(
|
|
214
|
+
A21=self.A21,phi_nu=self.phi_nu0,
|
|
215
|
+
g_up=self.gup_rad_transitions,g_low=self.glow_rad_transitions,
|
|
216
|
+
N1=N1,N2=N2,nu=self.nu0)
|
|
217
|
+
return tau_nu0
|
|
218
|
+
|
|
219
|
+
def get_tau_nu0_lines_LTE(self,N,T):
|
|
220
|
+
r'''Compute the optical depth at the rest frequency in LTE for all lines
|
|
221
|
+
(dust and overlapping lines are ignored)
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
N (:obj:`float`): the column density in [m\ :sup:`-2`]
|
|
225
|
+
T (:obj:`float`): the temperature in [K]
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
numpy.ndarray: the optical depth at the rest frequency assuming LTE
|
|
229
|
+
'''
|
|
230
|
+
level_population = self.LTE_level_pop(T=T)
|
|
231
|
+
return self.get_tau_nu0_lines(N=N,level_population=level_population)
|
|
232
|
+
|
|
233
|
+
def get_Tex(self,level_population):
|
|
234
|
+
r'''Compute the excitation temperature for all radiative transitions
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
level_population (numpy.ndarray): the fractional population of each level,
|
|
238
|
+
where the levels are in the order of the LAMDA file
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
numpy.ndarray: the excitation temperature for each radiative transition,
|
|
242
|
+
in the order as in the LAMDA file
|
|
243
|
+
'''
|
|
244
|
+
Tex = atomic_transition.Tex(
|
|
245
|
+
Delta_E=self.Delta_E_rad_transitions,g_up=self.gup_rad_transitions,
|
|
246
|
+
g_low=self.glow_rad_transitions,
|
|
247
|
+
x1=level_population[self.nlow_rad_transitions],
|
|
248
|
+
x2=level_population[self.nup_rad_transitions])
|
|
249
|
+
return Tex
|
|
250
|
+
|
|
251
|
+
def identify_overlapping_lines(self):
|
|
252
|
+
self.overlapping_lines = []
|
|
253
|
+
for i,line in enumerate(self.rad_transitions):
|
|
254
|
+
width_nu = self.width_v/constants.c*line.nu0
|
|
255
|
+
overlapping_lines = []
|
|
256
|
+
for j,overlap_line in enumerate(self.rad_transitions):
|
|
257
|
+
if j == i:
|
|
258
|
+
continue
|
|
259
|
+
nu0_overlap_line = overlap_line.nu0
|
|
260
|
+
width_nu_overlap_line = self.width_v/constants.c*nu0_overlap_line
|
|
261
|
+
if self.line_profile_type == 'rectangular':
|
|
262
|
+
if np.abs(line.nu0-nu0_overlap_line)\
|
|
263
|
+
<= width_nu/2 + width_nu_overlap_line/2:
|
|
264
|
+
overlapping_lines.append(j)
|
|
265
|
+
elif self.line_profile_type == 'Gaussian':
|
|
266
|
+
#here I need to be conservative because the Gaussian profile can,
|
|
267
|
+
#in theory, be arbitrarily broad (for arbitrarily high optical depth)
|
|
268
|
+
#take +- 1.57 FWHM, where the Gaussian is 0.1% of the peak
|
|
269
|
+
if np.abs(line.nu0-nu0_overlap_line)\
|
|
270
|
+
<= 1.57*width_nu + 1.57*width_nu_overlap_line:
|
|
271
|
+
overlapping_lines.append(j)
|
|
272
|
+
self.overlapping_lines.append(overlapping_lines)
|
|
273
|
+
|
|
274
|
+
def any_line_has_overlap(self,line_indices):
|
|
275
|
+
for index in line_indices:
|
|
276
|
+
if len(self.overlapping_lines[index]) > 0:
|
|
277
|
+
return True
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
def get_tau_line_nu(self,line_index,level_population,N):
|
|
281
|
+
line = self.rad_transitions[line_index]
|
|
282
|
+
def tau_line_nu(nu):
|
|
283
|
+
return line.tau_nu(N1=N*level_population[line.low.number],
|
|
284
|
+
N2=N*level_population[line.up.number],nu=nu)
|
|
285
|
+
return tau_line_nu
|
|
286
|
+
|
|
287
|
+
def get_tau_tot_nu(self,line_index,level_population,N,tau_dust):
|
|
288
|
+
overlapping_indices = self.overlapping_lines[line_index]
|
|
289
|
+
line = self.rad_transitions[line_index]
|
|
290
|
+
overlapping_lines = [self.rad_transitions[i] for i in overlapping_indices]
|
|
291
|
+
def tau_tot_nu(nu):
|
|
292
|
+
tau_tot = line.tau_nu(N1=N*level_population[line.low.number],
|
|
293
|
+
N2=N*level_population[line.up.number],nu=nu)
|
|
294
|
+
for ovl_line in overlapping_lines:
|
|
295
|
+
x1 = level_population[ovl_line.low.number]
|
|
296
|
+
x2 = level_population[ovl_line.up.number]
|
|
297
|
+
tau_tot += ovl_line.tau_nu(N1=x1*N,N2=x2*N,nu=nu)
|
|
298
|
+
tau_tot += tau_dust(nu)
|
|
299
|
+
return tau_tot
|
|
300
|
+
return tau_tot_nu
|
|
301
|
+
|
|
302
|
+
def set_K21_matrix(self):
|
|
303
|
+
self.K21_matrix = {collider:np.zeros((len(coll_transitions),
|
|
304
|
+
self.Tkin_data[collider].size))
|
|
305
|
+
for collider,coll_transitions in
|
|
306
|
+
self.coll_transitions.items()}
|
|
307
|
+
for collider,coll_transitions in self.coll_transitions.items():
|
|
308
|
+
for i,coll_trans in enumerate(coll_transitions):
|
|
309
|
+
self.K21_matrix[collider][i,:] = coll_trans.K21_data
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
@nb.jit(nopython=True,cache=True)
|
|
313
|
+
def construct_K_matrix(n_levels,K12,K21,nlow,nup):
|
|
314
|
+
K = np.zeros((n_levels,n_levels))
|
|
315
|
+
for i in range(len(K12)):
|
|
316
|
+
nl = nlow[i]
|
|
317
|
+
nu = nup[i]
|
|
318
|
+
#production of upper level from lower level:
|
|
319
|
+
K[nu,nl] += K12[i]
|
|
320
|
+
#destruction of lower level by transitions to upper level:
|
|
321
|
+
K[nl,nl] += -K12[i]
|
|
322
|
+
#production lower level from upper level:
|
|
323
|
+
K[nl,nu] += K21[i]
|
|
324
|
+
#destruction of upper level by transition to lower level:
|
|
325
|
+
K[nu,nu] += -K21[i]
|
|
326
|
+
return K
|
|
327
|
+
|
|
328
|
+
def interpolate_K(self,Tkin,collider):
|
|
329
|
+
Tlimits = self.Tkin_data_limits[collider]
|
|
330
|
+
assert Tlimits[0] <= Tkin <= Tlimits[1]
|
|
331
|
+
output = {}
|
|
332
|
+
Tkin_data = self.Tkin_data[collider]
|
|
333
|
+
j = np.searchsorted(Tkin_data,Tkin,side='left')
|
|
334
|
+
if j == 0:
|
|
335
|
+
K21 = self.K21_matrix[collider][:,0]
|
|
336
|
+
else:
|
|
337
|
+
i = j-1
|
|
338
|
+
x0 = Tkin_data[i]
|
|
339
|
+
y0 = self.K21_matrix[collider][:,i]
|
|
340
|
+
x1 = Tkin_data[j]
|
|
341
|
+
y1 = self.K21_matrix[collider][:,j]
|
|
342
|
+
x = Tkin
|
|
343
|
+
K21 = (y0*(x1-x) + y1*(x-x0)) / (x1-x0)
|
|
344
|
+
K12 = atomic_transition.compute_K12(
|
|
345
|
+
K21=K21,g_up=self.coll_gups[collider],
|
|
346
|
+
g_low=self.coll_glows[collider],
|
|
347
|
+
Delta_E=self.coll_DeltaEs[collider],Tkin=Tkin)
|
|
348
|
+
output['K21'] = K21
|
|
349
|
+
output['K12'] = K12
|
|
350
|
+
return output
|
|
351
|
+
|
|
352
|
+
def get_GammaC(self,Tkin,collider_densities):
|
|
353
|
+
GammaC = np.zeros((self.n_levels,)*2)
|
|
354
|
+
for collider,coll_dens in collider_densities.items():
|
|
355
|
+
interpK = self.interpolate_K(Tkin=Tkin,collider=collider)
|
|
356
|
+
K = self.construct_K_matrix(
|
|
357
|
+
n_levels=self.n_levels,K12=interpK['K12'],K21=interpK['K21'],
|
|
358
|
+
nlow=self.coll_nlow[collider],nup=self.coll_nup[collider])
|
|
359
|
+
GammaC += K*coll_dens
|
|
360
|
+
return GammaC
|