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
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
from scipy import constants
|
|
2
|
+
import numpy as np
|
|
3
|
+
from pythonradex import helpers,escape_probability,atomic_transition,flux,rate_equations
|
|
4
|
+
from pythonradex.molecule import EmittingMolecule
|
|
5
|
+
import warnings
|
|
6
|
+
import numba as nb
|
|
7
|
+
import numbers
|
|
8
|
+
import traceback
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Cloud():
|
|
12
|
+
|
|
13
|
+
'''
|
|
14
|
+
Solving the non-LTE radiative transfer using escape probabilities.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
|
|
18
|
+
emitting_molecule (pythonradex.molecule.EmittingMolecule): object
|
|
19
|
+
containing all the information about the emitting atom or molecule
|
|
20
|
+
tau_nu0_individual_transitions (numpy.ndarray): optical depth of each individual
|
|
21
|
+
transition at the central frequency (contributions of dust or overlapping lines
|
|
22
|
+
to the optical depth are not included)
|
|
23
|
+
level_pop (numpy.ndarray): fractional population of each level
|
|
24
|
+
Tex (numpy.ndarray): excitation temperature of each transition
|
|
25
|
+
|
|
26
|
+
Note:
|
|
27
|
+
The attributes tau_nu0_individual_transitions, level_pop and Tex are available
|
|
28
|
+
only after solving the radiative transfer by calling solve_radiative_transfer
|
|
29
|
+
|
|
30
|
+
'''
|
|
31
|
+
relative_convergence = 1e-6
|
|
32
|
+
min_iter = 10
|
|
33
|
+
max_iter = 1000
|
|
34
|
+
underrelaxation = 0.3 #RADEX uses 0.3
|
|
35
|
+
min_tau_considered_for_convergence = 1e-2
|
|
36
|
+
min_iter_before_ng_acceleration = 4
|
|
37
|
+
ng_acceleration_interval = 4
|
|
38
|
+
slow_variation_limit = 0.1
|
|
39
|
+
geometries = {'uniform sphere':escape_probability.UniformSphere,
|
|
40
|
+
'uniform sphere RADEX':escape_probability.UniformSphereRADEX,
|
|
41
|
+
'uniform slab':escape_probability.UniformSlab,
|
|
42
|
+
'LVG slab':escape_probability.UniformLVGSlab,
|
|
43
|
+
'LVG sphere':escape_probability.UniformLVGSphere,
|
|
44
|
+
'LVG sphere RADEX':escape_probability.LVGSphereRADEX}
|
|
45
|
+
line_profiles = {'Gaussian':atomic_transition.GaussianLineProfile,
|
|
46
|
+
'rectangular':atomic_transition.RectangularLineProfile}
|
|
47
|
+
|
|
48
|
+
def __init__(self,datafilepath,geometry,line_profile_type,width_v,
|
|
49
|
+
use_Ng_acceleration=True,treat_line_overlap=False,
|
|
50
|
+
warn_negative_tau=True,verbose=False,test_mode=False):
|
|
51
|
+
'''Initialises a new instance of the Cloud class.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
datafilepath (:obj:`str`): filepath to the LAMDA file that contains
|
|
55
|
+
the atomic / molecular data
|
|
56
|
+
geometry (:obj:`str`): determines how the escape probability
|
|
57
|
+
and flux are calculated. Available options: "uniform sphere",
|
|
58
|
+
"uniform sphere RADEX", "uniform slab", "LVG slab", "LVG sphere" and
|
|
59
|
+
"LVG sphere RADEX". The options containing "RADEX" are meant to
|
|
60
|
+
mimic the behaviour of the original RADEX code by using the same
|
|
61
|
+
equations as RADEX.
|
|
62
|
+
line_profile_type (:obj:`str`): The type of the line profile.
|
|
63
|
+
Available options: "rectangular" or "Gaussian". Note that for
|
|
64
|
+
LVG geometries, only "rectangular" is allowed.
|
|
65
|
+
width_v (:obj:`float`): The width of the line profile in [m/s]. For a Gaussian
|
|
66
|
+
profile, this is interpreted as the FWHM. Note that the interpretation
|
|
67
|
+
of this parameter depends on the adopted geometry. For the static
|
|
68
|
+
geometries, width_v is the width of the intrinsic emission profile.
|
|
69
|
+
On the other hand, for LVG geometries (for which the line profile
|
|
70
|
+
is rectangular), width_v corresponds to the total velocity width
|
|
71
|
+
of the cloud. So, for "LVG sphere", width_v=2*V, where V is the
|
|
72
|
+
velocity at the surface of the sphere. In terms of the constant
|
|
73
|
+
velocity gradient dv/dr=V/R (with R the radius of the sphere),
|
|
74
|
+
we can also say width_v=dv/dr*2*R. For "LVG slab", width_v=dv/dz*Z
|
|
75
|
+
where Z is the depth of the slab and dv/dz the constant velocity
|
|
76
|
+
gradient of the slab.
|
|
77
|
+
use_Ng_acceleration (:obj:`bool`): Whether to use Ng acceleration. Defaults
|
|
78
|
+
to True.
|
|
79
|
+
treat_line_overlap (:obj:`bool`): Whether to treat the overlap of emission
|
|
80
|
+
lines by averaging over the line profile. If False, all calculations
|
|
81
|
+
are done at the rest frequency (same as RADEX), and overlap effects
|
|
82
|
+
are neglected. If True, overlapping lines are treated correctly by
|
|
83
|
+
performing averages over frequency. This slows down the calculation
|
|
84
|
+
considerably. Defaults to False. Can only be used in combination
|
|
85
|
+
with static geometries (i.e. not with LVG geometries).
|
|
86
|
+
warn_negative_tau (:obj:`bool`): Whether the raise a warning when negative
|
|
87
|
+
optical depth is encountered. Defaults to True. Setting this to False
|
|
88
|
+
is useful when calculating a grid of models.
|
|
89
|
+
verbose (:obj:`bool`): Whether to print additional information. Defaults to False.
|
|
90
|
+
test_mode (:obj:`bool`): Enter test mode. Only for developer, should not be used
|
|
91
|
+
by general user. Defaults to False.
|
|
92
|
+
|
|
93
|
+
'''
|
|
94
|
+
assert width_v < 10000*constants.kilo,\
|
|
95
|
+
'assumption of small nu0/Delta_nu for flux calculation not satisfied'
|
|
96
|
+
self.emitting_molecule = EmittingMolecule(
|
|
97
|
+
datafilepath=datafilepath,
|
|
98
|
+
line_profile_type=line_profile_type,
|
|
99
|
+
width_v=width_v)
|
|
100
|
+
self.treat_line_overlap = treat_line_overlap
|
|
101
|
+
self.geometry_name = geometry
|
|
102
|
+
if self.treat_line_overlap:
|
|
103
|
+
if 'LVG' in self.geometry_name:
|
|
104
|
+
#this is because in LVG, the assumption is that once the radiation
|
|
105
|
+
#escapes from a local slab, it also escapes the cloud. With overlapping
|
|
106
|
+
#lines this assumption breaks down
|
|
107
|
+
raise ValueError('treatment of overlapping lines currently not'
|
|
108
|
+
+' supported for LVG geometries')
|
|
109
|
+
if self.emitting_molecule.has_overlapping_lines and not self.treat_line_overlap:
|
|
110
|
+
warnings.warn('some lines are overlapping, but treatement of'
|
|
111
|
+
+' overlapping lines not activated')
|
|
112
|
+
if verbose:
|
|
113
|
+
for i in range(len(self.emitting_molecule.overlapping_lines)):
|
|
114
|
+
if len(self.emitting_molecule.overlapping_lines[i]) > 0:
|
|
115
|
+
print(f'identified overlaps for line {i}:'
|
|
116
|
+
+f' {self.emitting_molecule.overlapping_lines[i]}')
|
|
117
|
+
if 'LVG' in self.geometry_name and not test_mode:
|
|
118
|
+
if not self.emitting_molecule.line_profile_type == 'rectangular':
|
|
119
|
+
#this is to get the right optical depth which is independent of
|
|
120
|
+
#velocity in this model (e.g. Eq. 13 in de Jong et al. 1975)
|
|
121
|
+
raise ValueError(f'{self.geometry_name} requires a rectangular profile')
|
|
122
|
+
#velocity at the surface of a LVG sphere
|
|
123
|
+
self.V_LVG_sphere = self.emitting_molecule.width_v/2
|
|
124
|
+
self.geometry = self.geometries[geometry]()
|
|
125
|
+
self.use_Ng_acceleration = use_Ng_acceleration
|
|
126
|
+
self.warn_negative_tau = warn_negative_tau
|
|
127
|
+
self.verbose = verbose
|
|
128
|
+
self.Einstein_kwargs = {'B12':self.emitting_molecule.B12,
|
|
129
|
+
'B21':self.emitting_molecule.B21,
|
|
130
|
+
'A21':self.emitting_molecule.A21}
|
|
131
|
+
|
|
132
|
+
def check_parameters(self,collider_densities,T_dust,tau_dust,ext_background):
|
|
133
|
+
if collider_densities is not None:
|
|
134
|
+
for collider in collider_densities.keys():
|
|
135
|
+
if collider not in self.emitting_molecule.coll_transitions:
|
|
136
|
+
raise ValueError(f'no data for collider "{collider}" available')
|
|
137
|
+
if 'LVG' in self.geometry_name:
|
|
138
|
+
if not (T_dust in (None,0) and tau_dust in (None,0)):
|
|
139
|
+
#this is because for LVG, it is assumed that radiation escaping
|
|
140
|
+
#the local slab will escape the entire cloud, which is not true
|
|
141
|
+
#if there is dust
|
|
142
|
+
raise ValueError('including dust continuum is currently not'
|
|
143
|
+
+' supported for LVG geometries')
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def should_be_updated(new_func_or_number,old):
|
|
147
|
+
#if it is a number, I can decide whether to update or not; if it is a function,
|
|
148
|
+
#I always update
|
|
149
|
+
if new_func_or_number is None:
|
|
150
|
+
return False
|
|
151
|
+
if isinstance(new_func_or_number,numbers.Number):
|
|
152
|
+
return new_func_or_number != old
|
|
153
|
+
else:
|
|
154
|
+
#if it's not a number and not None, then always update
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
def update_parameters(self,N=None,Tkin=None,collider_densities=None,
|
|
158
|
+
ext_background=None,T_dust=None,tau_dust=None):
|
|
159
|
+
r'''Set or update the parameters for a radiative transfer calculation.
|
|
160
|
+
Any of the parameters can be set to None if no update of that
|
|
161
|
+
parameter is wished.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
ext_background (func, number or None): A function taking the
|
|
165
|
+
frequency in Hz as input and returning the background radiation
|
|
166
|
+
field in [W/m\ :sup:`2`/Hz/sr]. A single number is interpreted as
|
|
167
|
+
a constant value for all frequencies. Defaults to None
|
|
168
|
+
(i.e. do not update).
|
|
169
|
+
N (:obj:`float` or None): The column density in [m\ :sup:`-2`].
|
|
170
|
+
Defaults to None (i.e. do not update).
|
|
171
|
+
Tkin (:obj:`float` or None): The kinetic temperature of the gas in [K].
|
|
172
|
+
Defaults to None (i.e. do not update).
|
|
173
|
+
collider_densities (dict or None): A dictionary of the number densities of
|
|
174
|
+
each collider that should be considered, in units of [m\ :sup:`-3`]. The
|
|
175
|
+
following keys are recognised: "H2", "para-H2", "ortho-H2", "e",
|
|
176
|
+
"H", "He", "H+". Defaults to None (i.e. do not update).
|
|
177
|
+
T_dust (func, number or None): The dust temperature in [K] as a
|
|
178
|
+
function of frequency. It is assumed that the source function
|
|
179
|
+
of the dust is a black body at temperature T_dust. A single number
|
|
180
|
+
is interpreted as a constant temperature for all frequencies.
|
|
181
|
+
Can only be used with static geometries (i.e. not with LVG geometries).
|
|
182
|
+
Defaults to None (i.e. do not update). For a model without dust,
|
|
183
|
+
put this parameter to 0.
|
|
184
|
+
tau_dust (func, number or None): optical depth of the dust as a function of
|
|
185
|
+
frequency. A single number is interpreted as a constant optical
|
|
186
|
+
depth for all frequencies. Can only be used with static geometries
|
|
187
|
+
(i.e. not with LVG geometries). Defaults to None (i.e. do not update).
|
|
188
|
+
For a model without dust, put this parameter to 0.
|
|
189
|
+
|
|
190
|
+
'''
|
|
191
|
+
#why do I put this into a seperate method rather than __init__? The reason
|
|
192
|
+
#is that the stuff in __init__ is expensive (in particular reading the
|
|
193
|
+
#LAMDA file). So if I explore e.g. a grid of N, the stuff in __init__
|
|
194
|
+
#should only be done once to save computation time. Then this method
|
|
195
|
+
#can be called inside the loop over e.g. the N values
|
|
196
|
+
#note that setting up the rate equations is expensive, so I only do it if
|
|
197
|
+
#necessary
|
|
198
|
+
#unfortunately a fairly complicated function, couldn't find any easier way...
|
|
199
|
+
self.check_parameters(collider_densities=collider_densities,T_dust=T_dust,
|
|
200
|
+
tau_dust=tau_dust,ext_background=ext_background)
|
|
201
|
+
if not hasattr(self,'rate_equations'):
|
|
202
|
+
params = {'N':N,'Tkin':Tkin,'collider_densities':collider_densities,
|
|
203
|
+
'ext_background':ext_background,'T_dust':T_dust,
|
|
204
|
+
'tau_dust':tau_dust}
|
|
205
|
+
for p_name,p in params.items():
|
|
206
|
+
assert p is not None,\
|
|
207
|
+
f'for initial setup, all params need to be defined; {p_name} missing'
|
|
208
|
+
self.rate_equations = rate_equations.RateEquations(
|
|
209
|
+
molecule=self.emitting_molecule,
|
|
210
|
+
treat_line_overlap=self.treat_line_overlap,
|
|
211
|
+
geometry=self.geometry,**params)
|
|
212
|
+
else:
|
|
213
|
+
if N is not None:
|
|
214
|
+
#updating N should be very cheap, so I don't care if I really need
|
|
215
|
+
#to update or not...
|
|
216
|
+
self.rate_equations.set_N(N)
|
|
217
|
+
update_Tkin = self.should_be_updated(
|
|
218
|
+
new_func_or_number=Tkin,old=self.rate_equations.Tkin)
|
|
219
|
+
update_coll_densities = self.should_be_updated(
|
|
220
|
+
new_func_or_number=collider_densities,
|
|
221
|
+
old=self.rate_equations.collider_densities)
|
|
222
|
+
if update_Tkin or update_coll_densities:
|
|
223
|
+
Tkin_updated = Tkin if update_Tkin else self.rate_equations.Tkin
|
|
224
|
+
coll_dens_updated = collider_densities if update_coll_densities else\
|
|
225
|
+
self.rate_equations.collider_densities
|
|
226
|
+
self.rate_equations.set_collision_rates(
|
|
227
|
+
Tkin=Tkin_updated,collider_densities=coll_dens_updated)
|
|
228
|
+
if self.should_be_updated(new_func_or_number=ext_background,
|
|
229
|
+
old=self.rate_equations.ext_background):
|
|
230
|
+
self.rate_equations.set_ext_background(ext_background=ext_background)
|
|
231
|
+
update_T_dust = self.should_be_updated(
|
|
232
|
+
new_func_or_number=T_dust,old=self.rate_equations.T_dust)
|
|
233
|
+
update_tau_dust = self.should_be_updated(
|
|
234
|
+
new_func_or_number=tau_dust,
|
|
235
|
+
old=self.rate_equations.tau_dust)
|
|
236
|
+
if update_T_dust or update_tau_dust:
|
|
237
|
+
Td = T_dust if update_T_dust else self.rate_equations.T_dust
|
|
238
|
+
taud = tau_dust if update_tau_dust else self.rate_equations.tau_dust
|
|
239
|
+
self.rate_equations.set_dust(T_dust=Td,tau_dust=taud)
|
|
240
|
+
|
|
241
|
+
def get_initial_level_pop(self):
|
|
242
|
+
#assume LTE for the first iteration
|
|
243
|
+
LTE_level_pop = self.emitting_molecule.LTE_level_pop(T=self.rate_equations.Tkin)
|
|
244
|
+
return self.rate_equations.solve(level_population=LTE_level_pop)
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
@nb.jit(nopython=True,cache=True)
|
|
248
|
+
def compute_residual(Tex_residual,tau,min_tau):
|
|
249
|
+
#same as RADEX: consider only lines above a minimum tau for convergence
|
|
250
|
+
selection = tau > min_tau
|
|
251
|
+
n_selected = selection.sum()
|
|
252
|
+
if n_selected > 0:
|
|
253
|
+
return np.sum(Tex_residual[selection]) / n_selected
|
|
254
|
+
else:
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
@nb.jit(nopython=True,cache=True)
|
|
259
|
+
def ng_accelerate(level_pop,old_level_pops):
|
|
260
|
+
#implemented in the same way as JADEX
|
|
261
|
+
#https://github.com/autocorr/Jadex.jl/blob/main/src/solver.jl
|
|
262
|
+
#see Dullemond lecture notes (ALI_basics_and_NGAcceleratin.pdf) for the
|
|
263
|
+
#formulas
|
|
264
|
+
#the first element of old_level_pops is the oldest level population
|
|
265
|
+
x1 = level_pop
|
|
266
|
+
x2 = old_level_pops[2]
|
|
267
|
+
x3 = old_level_pops[1]
|
|
268
|
+
x4 = old_level_pops[0]
|
|
269
|
+
q1 = x1 - 2*x2 + x3
|
|
270
|
+
q2 = x1 - x2 - x3 + x4
|
|
271
|
+
q3 = x1 - x2
|
|
272
|
+
A1 = np.sum(q1*q1)
|
|
273
|
+
A2 = np.sum(q2*q1)
|
|
274
|
+
B1 = A2 # q1*q2
|
|
275
|
+
B2 = np.sum(q2*q2)
|
|
276
|
+
C1 = np.sum(q1*q3)
|
|
277
|
+
C2 = np.sum(q2*q3)
|
|
278
|
+
denom = A1 * B2 - A2 * B1
|
|
279
|
+
if denom > 0:
|
|
280
|
+
a = (C1 * B2 - C2 * B1) / denom
|
|
281
|
+
b = (C2 * A1 - C1 * A2) / denom
|
|
282
|
+
accelerated_level_pop = (1 - a - b) * x1 + a * x2 + b * x3
|
|
283
|
+
else:
|
|
284
|
+
accelerated_level_pop = level_pop
|
|
285
|
+
return accelerated_level_pop
|
|
286
|
+
|
|
287
|
+
def solve_radiative_transfer(self):
|
|
288
|
+
"""Solves the radiative transfer."""
|
|
289
|
+
level_pop = self.get_initial_level_pop()
|
|
290
|
+
#typed list because this will be used by compiled function ng_accelerate:
|
|
291
|
+
old_level_pops = nb.typed.List([])
|
|
292
|
+
Tex_residual = np.ones(self.emitting_molecule.n_rad_transitions) * np.inf
|
|
293
|
+
old_Tex = 0
|
|
294
|
+
counter = 0
|
|
295
|
+
residual = np.inf
|
|
296
|
+
while residual>self.relative_convergence or counter < self.min_iter:
|
|
297
|
+
counter += 1
|
|
298
|
+
if counter%10 == 0 and self.verbose:
|
|
299
|
+
print(f'iteration {counter}')
|
|
300
|
+
if counter > self.max_iter:
|
|
301
|
+
raise RuntimeError('maximum number of iterations reached')
|
|
302
|
+
new_level_pop = self.rate_equations.solve(level_population=level_pop)
|
|
303
|
+
Tex = self.emitting_molecule.get_Tex(new_level_pop)
|
|
304
|
+
Tex_residual = helpers.relative_difference(Tex,old_Tex)
|
|
305
|
+
if self.verbose:
|
|
306
|
+
print(f'max relative Tex residual: {np.max(Tex_residual):.3g}')
|
|
307
|
+
tau_nu0 = self.emitting_molecule.get_tau_nu0_lines(
|
|
308
|
+
N=self.rate_equations.N,level_population=new_level_pop)
|
|
309
|
+
residual = self.compute_residual(
|
|
310
|
+
Tex_residual=Tex_residual,tau=tau_nu0,
|
|
311
|
+
min_tau=self.min_tau_considered_for_convergence)
|
|
312
|
+
old_Tex = Tex.copy()
|
|
313
|
+
old_level_pops.append(level_pop.copy())
|
|
314
|
+
if len(old_level_pops) > 3:
|
|
315
|
+
old_level_pops.pop(0)
|
|
316
|
+
level_pop = self.underrelaxation*new_level_pop\
|
|
317
|
+
+ (1-self.underrelaxation)*level_pop
|
|
318
|
+
if self.use_Ng_acceleration\
|
|
319
|
+
and counter > self.min_iter_before_ng_acceleration\
|
|
320
|
+
and counter%self.ng_acceleration_interval == 0:
|
|
321
|
+
level_pop = self.ng_accelerate(level_pop=level_pop,
|
|
322
|
+
old_level_pops=old_level_pops)
|
|
323
|
+
if self.verbose:
|
|
324
|
+
print(f'converged in {counter} iterations')
|
|
325
|
+
self.n_iter_convergence = counter
|
|
326
|
+
self.tau_nu0_individual_transitions = self.emitting_molecule.get_tau_nu0_lines(
|
|
327
|
+
N=self.rate_equations.N,level_population=level_pop)
|
|
328
|
+
if self.warn_negative_tau:
|
|
329
|
+
if np.any(self.tau_nu0_individual_transitions < 0):
|
|
330
|
+
negative_tau_transition_indices = np.where(self.tau_nu0_individual_transitions < 0)[0]
|
|
331
|
+
negative_tau_transitions = [self.emitting_molecule.rad_transitions[i]
|
|
332
|
+
for i in negative_tau_transition_indices]
|
|
333
|
+
warnings.warn('negative optical depth!')
|
|
334
|
+
for i,trans in zip(negative_tau_transition_indices,
|
|
335
|
+
negative_tau_transitions):
|
|
336
|
+
print(f'{trans.name}: tau_nu0 = {self.tau_nu0_individual_transitions[i]:.3g}')
|
|
337
|
+
self.level_pop = level_pop
|
|
338
|
+
self.Tex = self.emitting_molecule.get_Tex(level_pop)
|
|
339
|
+
self.flux_calculator = flux.FluxCalculator(
|
|
340
|
+
emitting_molecule=self.emitting_molecule,
|
|
341
|
+
level_population=self.level_pop,
|
|
342
|
+
geometry_name=self.geometry_name,
|
|
343
|
+
V_LVG_sphere=self.V_LVG_sphere,
|
|
344
|
+
compute_flux_nu=self.geometry.compute_flux_nu,
|
|
345
|
+
tau_nu0_individual_transitions=self.tau_nu0_individual_transitions,
|
|
346
|
+
tau_dust=self.rate_equations.tau_dust,
|
|
347
|
+
S_dust=self.rate_equations.S_dust)
|
|
348
|
+
|
|
349
|
+
def fluxes_of_individual_transitions(self,solid_angle,transitions):
|
|
350
|
+
r''' Calculate the fluxes of individual lines.
|
|
351
|
+
The flux of individual lines is the amount of
|
|
352
|
+
energy per time reaching the telescope via photons emitted by the molecule.
|
|
353
|
+
This calculation is only easily possible if the dust is optically thin (i.e.
|
|
354
|
+
the dust does not hinder line photons from escaping the cloud). Thus, this
|
|
355
|
+
function throws an error if the dust is not optically thin. Similarly,
|
|
356
|
+
the calculation is not possible when lines are overlapping and are not
|
|
357
|
+
optically thin. It is the responsibility of the user to choose an
|
|
358
|
+
appropriate observational quantity to be compared to the line fluxes
|
|
359
|
+
calculated here. In particular, for optically thin lines, the
|
|
360
|
+
continuum-subtracted observation might be appropriate. On the other
|
|
361
|
+
hand, for optically thick lines, the non-continuum-subtracted
|
|
362
|
+
observations might be more appropriate (because the optically thick
|
|
363
|
+
line blocks the continuum at the line enter; see e.g. Weaver et al. 2018)
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
solid_angle (:obj:`float`): The solid angle of the source in [sr].
|
|
367
|
+
transitions (:obj:`list` of :obj:`int` or None): The indices of the
|
|
368
|
+
transitions for which to calculate the fluxes. If None, then the
|
|
369
|
+
fluxes of all transitions are calculated. Defaults to None. The
|
|
370
|
+
indices correspond to the list of transitions in the LAMDA file,
|
|
371
|
+
starting with 0.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
list: The list of fluxes in [W/m\ :sup:`2`] corresponding to the
|
|
375
|
+
input list of requested transitions. If not specific transitions
|
|
376
|
+
where requested (transitions=None), then the list of fluxes corresponds
|
|
377
|
+
to the transitions as listed in the LAMDA file.
|
|
378
|
+
'''
|
|
379
|
+
return self.flux_calculator.fluxes_of_individual_transitions(
|
|
380
|
+
solid_angle=solid_angle,transitions=transitions)
|
|
381
|
+
|
|
382
|
+
def tau_nu(self,nu):
|
|
383
|
+
r''' Calculate the total optical depth (all lines plus dust) at each
|
|
384
|
+
input frequencies
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
nu (numpy.ndarray): The frequencies in [Hz] for which the optical depth
|
|
388
|
+
should be calculated
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
np.ndarray: The total optical depth at the input frequencies.
|
|
392
|
+
'''
|
|
393
|
+
self.flux_calculator.set_nu(nu=nu)
|
|
394
|
+
return self.flux_calculator.tau_nu_tot
|
|
395
|
+
|
|
396
|
+
def spectrum(self,solid_angle,nu):
|
|
397
|
+
r''' Calculate the total flux (lines + dust) at each input frequency
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
solid_angle (:obj:`float`): The solid angle of the source in [sr].
|
|
401
|
+
nu (numpy.ndarray): The frequencies in [Hz] for which the optical depth
|
|
402
|
+
should be calculated
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
np.ndarray: The flux in [W/m2/Hz] for each input frequency.
|
|
406
|
+
'''
|
|
407
|
+
self.flux_calculator.set_nu(nu=nu)
|
|
408
|
+
return self.flux_calculator.spectrum(solid_angle=solid_angle)
|
|
409
|
+
|
|
410
|
+
def model_grid(self,ext_backgrounds,N_values,Tkin_values,collider_densities_values,
|
|
411
|
+
requested_output,T_dust=0,tau_dust=0,solid_angle=None,
|
|
412
|
+
transitions=None,nu=None):
|
|
413
|
+
r'''Iterator over a grid of models.
|
|
414
|
+
Models are calculated for all possible combinations of the input
|
|
415
|
+
parameters ext_backgrounds, N_values, Tkin_values and
|
|
416
|
+
collider_densities_values.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
ext_backgrounds (:obj:`dict`): A dictionary, one entry for
|
|
420
|
+
each background that should be used. Each entry can be a function
|
|
421
|
+
of frequency, or a single number (interpreted as a radiation field
|
|
422
|
+
independent of frequency). The units are W/m2/Hz/sr. The keys
|
|
423
|
+
of the dictionary are used in the output to identify which
|
|
424
|
+
background was used.
|
|
425
|
+
N_values (:obj:`list` or numpy.ndarray): The list of column densities to
|
|
426
|
+
compute models for, in [m\ :sup:`-2`].
|
|
427
|
+
Tkin_values (:obj:`list` or numpy.ndarray): The list of kinetic temperatures
|
|
428
|
+
to compute models for, in [K].
|
|
429
|
+
collider_densities_values (:obj:`dict`): A dictionary with one entry for each
|
|
430
|
+
collider. Each entry is a list of densities for which models should be
|
|
431
|
+
computed for, using a "zip" logic (i.e. calculate a model for the first
|
|
432
|
+
entries of each list, for the second entries of each list, etc).
|
|
433
|
+
Units are [m\ :sup:`-3`]. Example:
|
|
434
|
+
collider_densities_values={'para-H2':[400,600],'ortho-H2':[700,1000]}
|
|
435
|
+
requested_output (:obj:`list`): The list of requested outputs. Possible
|
|
436
|
+
entries are 'level_pop', 'Tex', 'tau_nu0_individual_transitions',
|
|
437
|
+
'fluxes_of_individual_transitions', 'tau_nu', and 'spectrum'
|
|
438
|
+
T_dust (func or number): The dust temperature in [K] as a function of frequency.
|
|
439
|
+
It is assumed that the source function of the dust is a black body
|
|
440
|
+
at temperature T_dust. A single number is interpreted as a constant value
|
|
441
|
+
for all frequencies. Defaults to 0 (i.e. no internal dust
|
|
442
|
+
radiation field).
|
|
443
|
+
tau_dust (func or number): optical depth of the dust as a function of frequency.
|
|
444
|
+
A single number is interpreted as a constant value
|
|
445
|
+
for all frequencies. Defaults to 0 (i.e. no internal dust
|
|
446
|
+
radiation field).
|
|
447
|
+
solid_angle (:obj:`float`): The solid angle of the source in [sr].
|
|
448
|
+
Defaults to None. Compulsory if 'fluxes_of_individual_transitions'
|
|
449
|
+
or 'spectrum' are requested.
|
|
450
|
+
transitions (:obj:`list`): The indices of the transitions for which
|
|
451
|
+
to calculate Tex, tau_nu and fluxes. If None, then
|
|
452
|
+
values for all transitions are calculated. Defaults to None. The
|
|
453
|
+
indices are relative to the list of transitions in the LAMDA file,
|
|
454
|
+
starting with 0.
|
|
455
|
+
nu (numpy.ndarray): The frequencies in [Hz]. Compulsory if 'tau_nu'
|
|
456
|
+
or 'spectrum' is requested. Defaults to None.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
dict: dictionary representing the model
|
|
460
|
+
The dictionary fields are 'ext_background', 'N', 'Tkin' and
|
|
461
|
+
'collider_densities' to identify the input parameters of the model,
|
|
462
|
+
as well as any requested output. Units of outputs:
|
|
463
|
+
'level_pop': no units; 'Tex': [K]; 'tau_nu0': no units;
|
|
464
|
+
'fluxes_of_individual_transitions': [W/m\ :sup:`2`];
|
|
465
|
+
'tau_nu': no units; 'spectrum': [W/m\ :sup:`2`/Hz]. If the model
|
|
466
|
+
for a specific set of parameters could not be calculated, the output
|
|
467
|
+
fields are None.
|
|
468
|
+
'''
|
|
469
|
+
#it is expensive to update Tkin and collider densities, so those should be in
|
|
470
|
+
#the outermost loops
|
|
471
|
+
allowed_outputs = ('level_pop','Tex','tau_nu0_individual_transitions',
|
|
472
|
+
'fluxes_of_individual_transitions','tau_nu','spectrum')
|
|
473
|
+
for request in requested_output:
|
|
474
|
+
assert request in allowed_outputs,f'requested output "{request}" is invalid'
|
|
475
|
+
if 'fluxes_of_individual_transitions' in requested_output\
|
|
476
|
+
or 'spectrum' in requested_output:
|
|
477
|
+
assert solid_angle is not None
|
|
478
|
+
if 'tau_nu' in requested_output or 'spectrum' in requested_output:
|
|
479
|
+
assert nu is not None
|
|
480
|
+
if transitions is None:
|
|
481
|
+
transitions = [i for i in range(self.emitting_molecule.n_rad_transitions)]
|
|
482
|
+
n_coll_values = np.array([len(coll_values) for coll_values in
|
|
483
|
+
collider_densities_values.values()])
|
|
484
|
+
assert np.all(n_coll_values==n_coll_values[0]),\
|
|
485
|
+
'please provide the same number of collider densities for each collider'
|
|
486
|
+
n_coll_values = n_coll_values[0]
|
|
487
|
+
#set T_dust and tau_dust, other values are not important because they are
|
|
488
|
+
#going to change in the loop
|
|
489
|
+
initial_ext_bg = list(ext_backgrounds.values())[0]
|
|
490
|
+
initial_coll_dens = {coll_name:coll_values[0] for coll_name,coll_values
|
|
491
|
+
in collider_densities_values.items()}
|
|
492
|
+
self.update_parameters(
|
|
493
|
+
ext_background=initial_ext_bg,N=N_values[0],Tkin=Tkin_values[0],
|
|
494
|
+
collider_densities=initial_coll_dens,T_dust=T_dust,
|
|
495
|
+
tau_dust=tau_dust)
|
|
496
|
+
for Tkin in Tkin_values:
|
|
497
|
+
for i in range(n_coll_values):
|
|
498
|
+
collider_densities = {collider:values[i] for collider,values
|
|
499
|
+
in collider_densities_values.items()}
|
|
500
|
+
#note updating Tkin and coll dens together to avoid
|
|
501
|
+
#unnecessary overhead
|
|
502
|
+
self.update_parameters(Tkin=Tkin,collider_densities=collider_densities)
|
|
503
|
+
for ext_background_name,ext_background in ext_backgrounds.items():
|
|
504
|
+
self.update_parameters(ext_background=ext_background)
|
|
505
|
+
for N in N_values:
|
|
506
|
+
output = {'ext_background':ext_background_name,'N':N,
|
|
507
|
+
'Tkin':Tkin,'collider_densities':collider_densities}
|
|
508
|
+
try:
|
|
509
|
+
self.update_parameters(N=N)
|
|
510
|
+
self.solve_radiative_transfer()
|
|
511
|
+
if 'level_pop' in requested_output:
|
|
512
|
+
output['level_pop'] = self.level_pop
|
|
513
|
+
if 'Tex' in requested_output:
|
|
514
|
+
output['Tex'] = self.Tex[transitions]
|
|
515
|
+
if 'tau_nu0_individual_transitions' in requested_output:
|
|
516
|
+
output['tau_nu0_individual_transitions']\
|
|
517
|
+
= self.tau_nu0_individual_transitions[transitions]
|
|
518
|
+
if 'fluxes_of_individual_transitions' in requested_output:
|
|
519
|
+
output['fluxes_of_individual_transitions']\
|
|
520
|
+
= self.fluxes_of_individual_transitions(
|
|
521
|
+
solid_angle=solid_angle,transitions=transitions)
|
|
522
|
+
if 'tau_nu' in requested_output:
|
|
523
|
+
output['tau_nu'] = self.tau_nu(nu=nu)
|
|
524
|
+
if 'spectrum' in requested_output:
|
|
525
|
+
output['spectrum'] = self.spectrum(
|
|
526
|
+
solid_angle=solid_angle,nu=nu)
|
|
527
|
+
yield output
|
|
528
|
+
except:
|
|
529
|
+
print('Error during calculation of model with following'
|
|
530
|
+
+f' parameters: {output}')
|
|
531
|
+
traceback.print_exc()
|
|
532
|
+
for out in requested_output:
|
|
533
|
+
output[out] = None
|
|
534
|
+
yield output
|
|
535
|
+
|
|
536
|
+
def print_results(self):
|
|
537
|
+
'''Prints the results from the radiative transfer computation.'''
|
|
538
|
+
print('\n')
|
|
539
|
+
print(' up low nu0 [GHz] T_ex [K] poplow popup'\
|
|
540
|
+
+' tau_nu0')
|
|
541
|
+
for i,line in enumerate(self.emitting_molecule.rad_transitions):
|
|
542
|
+
output = f'{line.up.number:>4d} {line.low.number:>4d} '\
|
|
543
|
+
+f'{line.nu0/constants.giga:>14.6f} {self.Tex[i]:>10.2f} '\
|
|
544
|
+
+f'{self.level_pop[line.low.number]:>14g} '\
|
|
545
|
+
+f'{self.level_pop[line.up.number]:>14g} '\
|
|
546
|
+
+f'{self.tau_nu0_individual_transitions[i]:>14g}'
|
|
547
|
+
print(output)
|
|
548
|
+
print('\n')
|