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,308 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Fri Sep 13 09:03:13 2024
5
+
6
+ @author: gianni
7
+ """
8
+
9
+ import numba as nb
10
+ import numpy as np
11
+ from pythonradex import atomic_transition,helpers
12
+ import numbers
13
+
14
+
15
+ class RateEquations():
16
+
17
+ def __init__(self,geometry,molecule,collider_densities,Tkin,N,
18
+ treat_line_overlap,ext_background,T_dust,tau_dust):
19
+ self.geometry = geometry
20
+ self.molecule = molecule
21
+ self.treat_line_overlap = treat_line_overlap
22
+ self.N = N
23
+ self.set_collision_rates(Tkin=Tkin,collider_densities=collider_densities)
24
+ self.set_ext_background(ext_background=ext_background)
25
+ self.set_dust(T_dust=T_dust,tau_dust=tau_dust)
26
+ self.compute_Unu0_Vnu0()
27
+ #equation of steady state: A*x=b, x=fractional population that we search
28
+ self.b = np.zeros(self.molecule.n_levels)
29
+ # the system of equations is not linearly independent
30
+ #thus, I replace one equation by the normalisation condition,
31
+ #i.e. x1+...+xn=1, where xi is the fractional population of level i
32
+ #I replace the first equation (arbitrary choice):
33
+ self.b[0] = 1
34
+ if not treat_line_overlap:
35
+ self.GammaR = self.GammaR_nu0
36
+ else:
37
+ self.GammaR = self.GammaR_averaged
38
+
39
+ def set_N(self,N):
40
+ self.N = N
41
+
42
+ def set_collision_rates(self,Tkin,collider_densities):
43
+ self.Tkin = Tkin
44
+ self.collider_densities = collider_densities
45
+ self.GammaC = self.molecule.get_GammaC(
46
+ Tkin=self.Tkin,collider_densities=collider_densities)
47
+
48
+ @staticmethod
49
+ def generate_constant_func(const_value):
50
+ def const_func(nu):
51
+ return np.ones_like(nu)*const_value
52
+ return const_func
53
+
54
+ def assign_func_nu(self,func_name,argument):
55
+ if isinstance(argument, numbers.Number):
56
+ assert argument >= 0, 'ext_background, T_dust or tau_dust cannot be negative'
57
+ setattr(self,func_name,self.generate_constant_func(const_value=argument))
58
+ else:
59
+ setattr(self,func_name,argument)
60
+
61
+ def set_ext_background(self,ext_background):
62
+ self.assign_func_nu(func_name='ext_background',argument=ext_background)
63
+ if not self.treat_line_overlap:
64
+ if isinstance(ext_background,numbers.Number):
65
+ #faster than evaluating the function at each nu0
66
+ self.Iext_nu0 = np.full(shape=self.molecule.nu0.shape,
67
+ fill_value=ext_background)
68
+ else:
69
+ self.Iext_nu0 = np.array([self.ext_background(nu0) for nu0 in
70
+ self.molecule.nu0])
71
+
72
+ def set_dust(self,T_dust,tau_dust):
73
+ self.no_dust = (T_dust == 0 and tau_dust == 0)
74
+ for func_name,func in {'T_dust':T_dust,'tau_dust':tau_dust}.items():
75
+ self.assign_func_nu(func_name=func_name,argument=func)
76
+ if not self.treat_line_overlap:
77
+ if isinstance(tau_dust,numbers.Number):
78
+ #fast:
79
+ self.tau_dust_nu0 = np.full(shape=self.molecule.nu0.shape,
80
+ fill_value=tau_dust)
81
+ else:
82
+ self.tau_dust_nu0 = self.tau_dust(self.molecule.nu0)
83
+ self.S_dust_nu0 = self.S_dust(nu=self.molecule.nu0)
84
+
85
+ def S_dust(self,nu):
86
+ T = np.atleast_1d(self.T_dust(nu))
87
+ return helpers.B_nu(nu=nu,T=T)
88
+
89
+ # We need to consider three cases:
90
+ #1. no averaging over line profile (i.e. evaluate everything at nu0);
91
+ #this cannot be used for overlapping lines, but dust is ok
92
+ #2. with averaging, no treatment of overlapping lines
93
+ #3. with averaging, with treatement of overlapping lines
94
+ #all three cases need to include dust continuum
95
+
96
+
97
+ #### case 1: "nu0 case" (everything evaluated at nu0, no overlapping lines allowed
98
+ # (because no averaging))
99
+ #basically, instead of averaging over the line profile, everything evaluated
100
+ #at nu0, so the averaging function is a delta func instead of line profile
101
+
102
+ def U_matrix_nu0(self):
103
+ #already multiply by 4*pi/h*nu0 here
104
+ U = np.zeros((self.molecule.n_levels,)*2)
105
+ n_low = self.molecule.nlow_rad_transitions
106
+ n_up = self.molecule.nup_rad_transitions
107
+ U[n_up,n_low] = self.molecule.A21
108
+ return U
109
+
110
+ def V_matrix_nu0(self):
111
+ #already multiply by 4*pi/h*nu0 here
112
+ V = np.zeros((self.molecule.n_levels,)*2)
113
+ n_low = self.molecule.nlow_rad_transitions
114
+ n_up = self.molecule.nup_rad_transitions
115
+ V[n_up,n_low] = self.molecule.B21
116
+ V[n_low,n_up] = self.molecule.B12
117
+ return V
118
+
119
+ def compute_Unu0_Vnu0(self):
120
+ self.U_nu0 = self.U_matrix_nu0()
121
+ self.V_nu0 = self.V_matrix_nu0()
122
+
123
+ @staticmethod
124
+ @nb.jit(nopython=True,cache=True)
125
+ def tau_line_nu0(level_population,N,trans_low_number,trans_up_number,
126
+ A21,phi_nu0,glow_rad_transitions,gup_rad_transitions,nu0):
127
+ #careful, don't use empty_like with an array that is made of integers...
128
+ tau_nu0 = np.empty(len(trans_low_number))
129
+ for i in range(len(trans_low_number)):
130
+ N1 = N * level_population[trans_low_number[i]]
131
+ N2 = N * level_population[trans_up_number[i]]
132
+ tau_nu0[i] = atomic_transition.tau_nu(
133
+ A21=A21[i],phi_nu=phi_nu0[i],
134
+ g_low=glow_rad_transitions[i],
135
+ g_up=gup_rad_transitions[i],N1=N1,N2=N2,nu=nu0[i])
136
+ return tau_nu0
137
+
138
+ @staticmethod
139
+ @nb.jit(nopython=True,cache=True)
140
+ def Ieff_nu0(beta_nu0,tau_tot_nu0,no_dust,S_dust_nu0,tau_dust_nu0,
141
+ n_levels,trans_low_number,trans_up_number,Iext_nu0):
142
+ Ieff = np.zeros((n_levels,n_levels))
143
+ for i in range(len(trans_low_number)):
144
+ tau_tot = tau_tot_nu0[i]
145
+ if no_dust or tau_tot==0:
146
+ psistar_eta_c = 0
147
+ else:
148
+ psistar_eta_c = (1-beta_nu0[i])*S_dust_nu0[i]*tau_dust_nu0[i]/tau_tot
149
+ nlow = trans_low_number[i]
150
+ nup = trans_up_number[i]
151
+ I = beta_nu0[i]*Iext_nu0[i] + psistar_eta_c
152
+ Ieff[nlow,nup] = I
153
+ Ieff[nup,nlow] = I
154
+ return Ieff
155
+
156
+ @staticmethod
157
+ @nb.jit(nopython=True,cache=True)
158
+ def mixed_term_nu0(beta_nu0,tau_tot_nu0,tau_line_nu0,n_levels,
159
+ trans_low_number,trans_up_number,A21):
160
+ #this is the term (Sum_l'' Chi^{dagger}_{l''l}) * Psi^*_nu * (Sum_l''' U_{l'l'''})
161
+ #if there is no overlap, only the term l''=l' and l'''=l survives:
162
+ #Psi^*_nu * Chi^{dagger}_l'l * U_l'l
163
+ #note that U_l'l is 0 if l'<l
164
+ term = np.zeros((n_levels,n_levels))
165
+ #already multiply by 4*pi/h*nu0 here
166
+ for i in range(len(trans_low_number)):
167
+ n_low = trans_low_number[i]
168
+ n_up = trans_up_number[i]
169
+ tau_tot = tau_tot_nu0[i]
170
+ if tau_tot == 0:
171
+ term[n_up,n_low] = 0
172
+ else:
173
+ term[n_up,n_low] = (1-beta_nu0[i])*tau_line_nu0[i]/tau_tot*A21[i]
174
+ return term
175
+
176
+ @staticmethod
177
+ @nb.jit(nopython=True,cache=True)
178
+ def transposed_GammaR(U_nu0,V_nu0,Ieff_nu0,mixed_term_nu0):
179
+ return np.transpose(U_nu0+V_nu0*Ieff_nu0-mixed_term_nu0)
180
+
181
+ @staticmethod
182
+ @nb.jit(nopython=True,cache=True)
183
+ def get_diagonal_GammaR(GammaR):
184
+ #the diagonal of GammaR is the negative row sum over the non-diagonal terms
185
+ return -(np.sum(GammaR,axis=0)-np.diag(GammaR))
186
+
187
+ def GammaR_nu0(self,level_population):
188
+ tau_line_nu0 = self.tau_line_nu0(
189
+ level_population=level_population,N=self.N,
190
+ trans_low_number=self.molecule.nlow_rad_transitions,
191
+ trans_up_number=self.molecule.nup_rad_transitions,
192
+ A21=self.molecule.A21,phi_nu0=self.molecule.phi_nu0,
193
+ glow_rad_transitions=self.molecule.glow_rad_transitions,
194
+ gup_rad_transitions=self.molecule.gup_rad_transitions,
195
+ nu0=self.molecule.nu0)
196
+ tau_tot_nu0 = tau_line_nu0 + self.tau_dust_nu0
197
+ beta_nu0 = self.geometry.beta(tau_tot_nu0)
198
+ Ieff_nu0 = self.Ieff_nu0(
199
+ beta_nu0=beta_nu0,tau_tot_nu0=tau_tot_nu0,
200
+ no_dust=self.no_dust,
201
+ S_dust_nu0=self.S_dust_nu0,tau_dust_nu0=self.tau_dust_nu0,
202
+ n_levels=self.molecule.n_levels,
203
+ trans_low_number=self.molecule.nlow_rad_transitions,
204
+ trans_up_number=self.molecule.nup_rad_transitions,
205
+ Iext_nu0=self.Iext_nu0)
206
+ mixed_term_nu0 = self.mixed_term_nu0(
207
+ beta_nu0=beta_nu0,tau_tot_nu0=tau_tot_nu0,
208
+ tau_line_nu0=tau_line_nu0,n_levels=self.molecule.n_levels,
209
+ trans_low_number=self.molecule.nlow_rad_transitions,
210
+ trans_up_number=self.molecule.nup_rad_transitions,
211
+ A21=self.molecule.A21)
212
+ #GammaR_ll' = U_l'l+... i.e. indices are interchanged, so I have to transpose
213
+ #see eq. 2.19 in Rybicki & Hummer (1992)
214
+ #note that the factor h*nu/4pi is already taken into account in all terms
215
+ GammaR = self.transposed_GammaR(
216
+ U_nu0=self.U_nu0,V_nu0=self.V_nu0,Ieff_nu0=Ieff_nu0,
217
+ mixed_term_nu0=mixed_term_nu0)
218
+ diag = self.get_diagonal_GammaR(GammaR=GammaR)
219
+ np.fill_diagonal(a=GammaR,val=diag)
220
+ return GammaR
221
+
222
+ #### cases 2: averaging over line profile
223
+ #note that U is proportional to phi_nu (when divided by h*nu), so we don't
224
+ #need to average the U term
225
+
226
+ def get_tau_tot_functions(self,level_population):
227
+ kwargs = {'level_population':level_population,'N':self.N,
228
+ 'tau_dust':self.tau_dust}
229
+ return [self.molecule.get_tau_tot_nu(line_index=i,**kwargs)
230
+ for i in range(self.molecule.n_rad_transitions)]
231
+
232
+ def get_tau_line_functions(self,level_population):
233
+ kwargs = {'level_population':level_population,'N':self.N}
234
+ return [self.molecule.get_tau_line_nu(line_index=i,**kwargs) for i in
235
+ range(self.molecule.n_rad_transitions)]
236
+
237
+ def V_Ieff_averaged(self,tau_tot_functions):
238
+ V_Ieff = np.zeros((self.molecule.n_levels,)*2)
239
+ for i,trans in enumerate(self.molecule.rad_transitions):
240
+ def Ieff(nu):
241
+ tau_tot = tau_tot_functions[i](nu)
242
+ beta = self.geometry.beta(tau_tot)
243
+ betaIext = beta*self.ext_background(nu)
244
+ return np.where(tau_tot==0,betaIext,betaIext
245
+ +(1-beta)*self.S_dust(nu)*self.tau_dust(nu)/tau_tot)
246
+ Ieff_averaged = trans.line_profile.average_over_phi_nu(Ieff)
247
+ n_low = trans.low.number
248
+ n_up = trans.up.number
249
+ V_Ieff[n_low,n_up] = self.V_nu0[n_low,n_up]*Ieff_averaged
250
+ V_Ieff[n_up,n_low] = self.V_nu0[n_up,n_low]*Ieff_averaged
251
+ return V_Ieff
252
+
253
+ def mixed_term_averaged(self,tau_tot_functions,tau_line_functions):
254
+ mixed_term = np.zeros((self.molecule.n_levels,)*2)
255
+ #I choose that trans corresponds to the term U_lprime_lprimeprimeprime
256
+ #while the pairing transition corresponds to chi_lprimeprime_l. This makes
257
+ #it easier because I know that U_ab is zero if a<b, so I can simply iterate
258
+ #over all transitions to have all posibilities for the U term covered
259
+ for i,trans in enumerate(self.molecule.rad_transitions):
260
+ #add the transition itself to the list of pairing transitions
261
+ #(this is for the term lprimeprime=lprime and lprimeprimeprime=l):
262
+ pairing_transitions = self.molecule.overlapping_lines[i] + [i,]
263
+ for j in pairing_transitions:
264
+ def mixed(nu):
265
+ #for tau_tot it doesn't matter which transition I take,
266
+ #because they are overlapping
267
+ tau_tot = tau_tot_functions[i](nu)
268
+ beta = self.geometry.beta(tau_tot)
269
+ #need to be careful here: need to take tau_line of the
270
+ #lprimeprime -> l transition
271
+ tau_line = tau_line_functions[j](nu)
272
+ #A21 (U matrix) needs to be from the lprime -> lprimeprimeprime
273
+ #transition
274
+ A21 = trans.A21
275
+ return np.where(tau_tot==0,0,(1-beta)*tau_line/tau_tot*A21)
276
+ #need to average over the line profile of the
277
+ #lprime -> lprimeprimeprime transition!
278
+ mixed_averaged = trans.line_profile.average_over_phi_nu(mixed)
279
+ pairing_trans = self.molecule.rad_transitions[j]
280
+ #only non-diagonal terms need to be calculated, as the diagonal
281
+ #of Gamma will be calculated from the non-diagonal terms directly
282
+ if trans.up.number != pairing_trans.low.number:
283
+ mixed_term[trans.up.number,pairing_trans.low.number] += mixed_averaged
284
+ if trans.up.number != pairing_trans.up.number:
285
+ mixed_term[trans.up.number,pairing_trans.up.number] += -mixed_averaged
286
+ return mixed_term
287
+
288
+ def GammaR_averaged(self,level_population):
289
+ tau_line_functions = self.get_tau_line_functions(
290
+ level_population=level_population)
291
+ tau_tot_functions = self.get_tau_tot_functions(
292
+ level_population=level_population)
293
+ V_Ieff = self.V_Ieff_averaged(tau_tot_functions=tau_tot_functions)
294
+ mixed_term = self.mixed_term_averaged(tau_tot_functions=tau_tot_functions,
295
+ tau_line_functions=tau_line_functions)
296
+ GammaR = np.transpose(self.U_nu0+V_Ieff-mixed_term)
297
+ diag = self.get_diagonal_GammaR(GammaR=GammaR)
298
+ np.fill_diagonal(a=GammaR,val=diag)
299
+ return GammaR
300
+
301
+ def solve(self,level_population):
302
+ Gamma = self.GammaR(level_population=level_population) + self.GammaC
303
+ Gamma[0,:] = np.ones(self.molecule.n_levels)
304
+ fractional_population = np.linalg.solve(Gamma,b=self.b)
305
+ assert np.all(fractional_population >= 0),\
306
+ 'negative level population, potentially due to high column'\
307
+ +'density and/or low collider density'
308
+ return fractional_population
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythonradex
3
+ Version: 1.0.0
4
+ Summary: 1D non-LTE radiative transfer
5
+ Project-URL: Documentation, https://pythonradex.readthedocs.io/en/latest/
6
+ Project-URL: Issues, https://github.com/gica3618/pythonradex/issues
7
+ Project-URL: Source, https://github.com/gica3618/pythonradex
8
+ Author-email: Gianni Cataldi <cataldi.gia@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE.txt
11
+ Keywords: non-LTE,radex,radiative transfer
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: numba>=0.61.0
20
+ Requires-Dist: numpy>=2.0
21
+ Requires-Dist: scipy>=1.14
22
+ Description-Content-Type: text/plain
23
+
24
+ pythonradex solves the 1D non-LTE radiative transfer for a homogeneous medium. It is a tool to quickly estimate the radiation from an astrophysical gas. It also provides a convenient function to read data files from the LAMDA database. pythonradex is a python re-implementation of the RADEX code, with additional functionality not included in RADEX (treatment of overlapping lines, treatment of dust continuum, output of spectra,...)
25
+
26
+ For more details, please refer to the documentation: http://pythonradex.readthedocs.io/
27
+
28
+ pythonradex is written and maintained by Gianni Cataldi
@@ -0,0 +1,15 @@
1
+ pythonradex/LAMDA_file.py,sha256=K-lIJa-5Qj_fwQm2iFJP99eDNOW2fQUB9IsRh_EaEVE,6304
2
+ pythonradex/__about__.py,sha256=VtFVQjqY6htZKsKHWnMX6fPxVvmPA5My0o-1h-RVj5E,133
3
+ pythonradex/__init__.py,sha256=8AAxjGySDAcXBqMpDwMRGDOGByrmmvw5tA-ZcJ3CaMo,90
4
+ pythonradex/atomic_transition.py,sha256=NsH_ngC1WqdJ2BZU80qa4BZVeyaBKq3wx53ojgFjj0E,13216
5
+ pythonradex/escape_probability.py,sha256=8gcXYdlOrSLk0x0rFXhUCNQCM7bKMKB0zSxgOPEBLeI,4317
6
+ pythonradex/escape_probability_functions.py,sha256=Px4peWRd5LsWmjEuqCJoljPNFq3sj0WZsn8Eo5nc3CM,9229
7
+ pythonradex/flux.py,sha256=q_yHAVn0VO6b20-lCNropHPBrM4vQMIrsc5P57spr5s,10589
8
+ pythonradex/helpers.py,sha256=JfoporFSdOBBn8kAYJQaAmSxGIFWm_nkSCpe85JRLS8,1985
9
+ pythonradex/molecule.py,sha256=6nuHkxt3TuPWuwr-BRGBf4llzrRycVoVmfb8GDC3_tE,17647
10
+ pythonradex/radiative_transfer.py,sha256=IV5lThKw44qsMWzSnwFmCoxSAMhzX0_2xn-tKBVzFas,31598
11
+ pythonradex/rate_equations.py,sha256=HRbSR4JtPy1Z8H14RFnamG9nqCL9ScYdrnPqPNzWkxA,14699
12
+ pythonradex-1.0.0.dist-info/METADATA,sha256=milpT8jfkaCPPXZ2_rv3fwlVIfwF4xIDL5WzENyZhTo,1457
13
+ pythonradex-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ pythonradex-1.0.0.dist-info/licenses/LICENSE.txt,sha256=biAf-Gv0h8Xr2r-CuwVZJsJKnj6WCL6KJfof8mz-M0U,1071
15
+ pythonradex-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Gianni Cataldi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.