pythonradex 2.0.2__py3-none-any.whl → 3.0.1__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 CHANGED
@@ -4,19 +4,20 @@ Created on Sun Nov 12 17:05:01 2017
4
4
 
5
5
  @author: gianni
6
6
  """
7
+
7
8
  from pythonradex import atomic_transition
8
9
  from scipy import constants
9
10
  import numpy as np
10
11
 
11
12
 
12
13
  def is_comment(line):
13
- if line.replace(" ", "")[0] == "!":
14
+ if line.lstrip().startswith("!"):
14
15
  return True
15
16
  else:
16
17
  return False
17
18
 
18
19
 
19
- def read(datafilepath, read_frequencies, read_quantum_numbers=False):
20
+ def read(datafilepath):
20
21
  """
21
22
  Read a data file that follows the LAMDA format (https://home.strw.leidenuniv.nl/~moldata/molformat.html).
22
23
  Such files can for example be downloaded from the EMAA (https://emaa.osug.fr/)
@@ -24,15 +25,6 @@ def read(datafilepath, read_frequencies, read_quantum_numbers=False):
24
25
 
25
26
  Args:
26
27
  datafilepath (:obj:`str`): The filepath to the LAMDA file.
27
- read_frequencies (:obj:`bool`): Whether to read the frequencies of the
28
- radiative transitions from the file or not. If False, calculates the
29
- frequencies from the level energies given in the file. Setting this to
30
- True can be useful since frequencies are sometimes given with more
31
- significant digits than level energies. However, the LAMDA format does not
32
- force a file to contain the frequencies, so they might not be present.
33
- read_quantum_numbers (:obj:`bool`): Whether to read the quantum numbers
34
- from the file. Defaults to False. The LAMDA format does not force a
35
- file to list quantum numbers, so they might not be present.
36
28
 
37
29
  Returns:
38
30
  dict: A dictionary containing the data read from the file.
@@ -43,8 +35,9 @@ def read(datafilepath, read_frequencies, read_quantum_numbers=False):
43
35
  pythonradex.atomic_transitions.RadiativeTransition objects);
44
36
  'collisional transitions' (a list of
45
37
  pythonradex.atomic_transition.CollisionalTransition objects);
46
- 'quantum numbers' (list of str, empty if read_quantum_numbers
47
- was set to False)
38
+ 'frequencies' (list of float containing the frequencies of the
39
+ radiative transitions form the file; empty
40
+ if file does not contain frequencies)
48
41
  """
49
42
  # identifiers used in the LAMDA database files:
50
43
  LAMDA_coll_ID = {
@@ -59,11 +52,12 @@ def read(datafilepath, read_frequencies, read_quantum_numbers=False):
59
52
  datafile = open(datafilepath, "r")
60
53
  levels = []
61
54
  rad_transitions = []
55
+ frequencies = []
62
56
  coll_transitions = {}
63
- quantum_numbers = []
64
57
  comment_offset = 0
65
58
  for i, line in enumerate(datafile):
66
- if is_comment(line) or line == "" or line == "\n":
59
+ line = line.lstrip()
60
+ if is_comment(line) or line == "":
67
61
  comment_offset += 1
68
62
  continue
69
63
  if i == 0 + comment_offset:
@@ -76,9 +70,8 @@ def read(datafilepath, read_frequencies, read_quantum_numbers=False):
76
70
  if 3 + comment_offset <= i < 3 + comment_offset + n_levels:
77
71
  line_entries = line.split()
78
72
  leveldata = [float(string) for string in line_entries[:3]]
79
- assert (
80
- int(leveldata[0]) == i - 2 - comment_offset
81
- ), "level numeration not consistent"
73
+ if int(leveldata[0]) != i - 2 - comment_offset:
74
+ raise RuntimeError("level numeration not consistent")
82
75
  # transforming energy from cm-1 to J; level indices starting from 0:
83
76
  lev = atomic_transition.Level(
84
77
  g=leveldata[2],
@@ -86,8 +79,6 @@ def read(datafilepath, read_frequencies, read_quantum_numbers=False):
86
79
  index=int(leveldata[0]) - 1,
87
80
  )
88
81
  levels.append(lev)
89
- if read_quantum_numbers:
90
- quantum_numbers.append(line_entries[3])
91
82
  continue
92
83
  if i == 3 + comment_offset + n_levels:
93
84
  n_rad_transitions = int(line)
@@ -101,15 +92,23 @@ def read(datafilepath, read_frequencies, read_quantum_numbers=False):
101
92
  up = next(level for level in levels if level.index == radtransdata[1] - 1)
102
93
  low = next(level for level in levels if level.index == radtransdata[2] - 1)
103
94
  rad_trans_kwargs = {"up": up, "low": low, "A21": radtransdata[3]}
104
- if read_frequencies:
105
- nu0 = radtransdata[4] * constants.giga
106
- Delta_E = up.E - low.E
107
- assert np.isclose(
108
- nu0, Delta_E / constants.h, atol=0, rtol=1e-3
109
- ), "read frequency is inconsistent with level energies"
110
- rad_trans_kwargs["nu0"] = nu0
111
95
  rad_trans = atomic_transition.RadiativeTransition(**rad_trans_kwargs)
112
96
  rad_transitions.append(rad_trans)
97
+ if len(radtransdata) >= 5:
98
+ # frequencies are there, so let's read them
99
+ frequencies.append(radtransdata[4] * constants.giga)
100
+ # Delta_E = up.E - low.E
101
+ # if not helpers.DeltaE_nu0_consistent(Delta_E=Delta_E,nu0=nu0):
102
+ # filename = Path(datafilepath).name
103
+ # nu0_from_Delta_E = Delta_E/constants.h
104
+ # mean_nu0 = (nu0+nu0_from_Delta_E)/2
105
+ # relative_diff = abs(nu0-nu0_from_Delta_E)/mean_nu0
106
+ # warnings.warn(f"{filename}: transition from level {up.index+1} to {low.index+1}: "
107
+ # f"frequency ({nu0/constants.giga} GHz) inconsistent "
108
+ # +"with level energies (DeltaE corresponds "
109
+ # +f"to {nu0_from_Delta_E/constants.giga} GHz). "
110
+ # +f"relative difference: {relative_diff*100:.2g}%")
111
+ # rad_trans_kwargs["nu0"] = nu0
113
112
  continue
114
113
  if i == 4 + comment_offset + n_levels + n_rad_transitions:
115
114
  coll_partner_offset = 0
@@ -160,5 +159,5 @@ def read(datafilepath, read_frequencies, read_quantum_numbers=False):
160
159
  "levels": levels,
161
160
  "radiative transitions": rad_transitions,
162
161
  "collisional transitions": coll_transitions,
163
- "quantum numbers": quantum_numbers,
162
+ "frequencies": frequencies,
164
163
  }
@@ -5,10 +5,22 @@ Created on Sun Nov 12 17:03:55 2017
5
5
  @author: gianni
6
6
  """
7
7
 
8
+ # note about frequencies:
9
+ # in LAMDA files, the listed frequencies are only for "informational pruposes"
10
+ # indeed, RADEX does not use them for any calculation, but just for the final
11
+ # output to help the user to identify the transition
12
+ # on the other hand, the frequencies have sometimes better accuracy than DeltaE/h
13
+ # (e.g. for CO)
14
+ # here, I adopt a similar approach as RADEX: I use DeltaE/h for cases where the position
15
+ # of the line is not important, but I use the specific nu0 for cases where
16
+ # the spectral position of the line matters (e.g. spectra etc.)
17
+
18
+
8
19
  from pythonradex import helpers
9
20
  import numpy as np
10
21
  from scipy import constants
11
22
  import numba as nb
23
+ import warnings
12
24
 
13
25
 
14
26
  @nb.jit(nopython=True, cache=True)
@@ -71,7 +83,7 @@ class LineProfile:
71
83
  v (float or numpy.ndarray): velocity in [m/s]
72
84
 
73
85
  Returns:
74
- float or numpy.ndarra: The line profile in [(m/s)\ :sup:`-1`]
86
+ float or numpy.ndarray: The line profile in [(m/s)\ :sup:`-1`]
75
87
  """
76
88
  nu = self.nu0 * (1 - v / constants.c)
77
89
  return self.phi_nu(nu) * self.nu0 / constants.c
@@ -245,26 +257,35 @@ class RadiativeTransition(Transition):
245
257
  low (pythonradex.atomic_transition.Level): the lower level of the transition
246
258
  Delta_E (:obj:`float`): the energy difference between the upper and lower level
247
259
  name (:obj:`str`): name of the transition
248
- nu0 (:obj:`float`): rest frequency in [Hz]
260
+ Delta_E_over_h (:obj:`float`): Delta_E/h, corresponding to rest frequency in [Hz]
249
261
  A21 (:obj:`float`): Einstein A21 coefficient in [s\ :sup:`-1`]
250
262
  B21 (:obj:`float`): Einstein B21 coefficient in [sr m\ :sup:`2` Hz / Jy]
251
263
  B12 (:obj:`float`): Einstein B12 coefficient in [sr m\ :sup:`2` Hz / Jy]
252
264
  """
253
265
 
254
- def __init__(self, up, low, A21, nu0=None):
255
- Transition.__init__(self, up=up, low=low)
256
- assert self.Delta_E > 0, "non-positive Delta_E for radiative transition"
266
+ def __init__(self, up, low, A21):
267
+ super().__init__(up=up, low=low)
268
+ if self.Delta_E <= 0:
269
+ raise RuntimeError("non-positive Delta_E for radiative transition")
257
270
  self.A21 = A21
258
- nu0_from_Delta_E = self.Delta_E / constants.h
259
- if nu0 is None:
260
- self.nu0 = nu0_from_Delta_E
261
- else:
262
- # when reading LAMDA files, it is useful to specify nu0 directly, since
263
- # it is sometimes given with more significant digits
264
- assert np.isclose(nu0_from_Delta_E, nu0, atol=0, rtol=1e-3)
265
- self.nu0 = nu0
266
- self.B21 = helpers.B21(A21=self.A21, nu=self.nu0)
267
- self.B12 = helpers.B12(A21=self.A21, nu=self.nu0, g1=self.low.g, g2=self.up.g)
271
+ # rest frequency, but I do want to distinguish it from the frequency listed
272
+ # in the LAMDA file, because sometimes those are not consistent with DeltaE/h
273
+ self.Delta_E_over_h = self.Delta_E / constants.h
274
+ # nu0_from_Delta_E = self.Delta_E / constants.h
275
+ # if nu0 is None:
276
+ # self.nu0 = nu0_from_Delta_E
277
+ # else:
278
+ # # when reading LAMDA files, it is useful to read nu0 rather than calculate
279
+ # #from level energies, since nu0 is sometimes given with more significant
280
+ # #digits than the level energies
281
+ # if not helpers.DeltaE_nu0_consistent(Delta_E=self.Delta_E,nu0=nu0):
282
+ # warnings.warn(f"Delta_E and nu0 are inconsistent (up.index = {up.index}, "
283
+ # +f"low.index={low.index}, A21={A21} s-1, nu0={nu0/constants.giga} GHz)")
284
+ # self.nu0 = nu0
285
+ self.B21 = helpers.B21(A21=self.A21, nu=self.Delta_E_over_h)
286
+ self.B12 = helpers.B12(
287
+ A21=self.A21, nu=self.Delta_E_over_h, g1=self.low.g, g2=self.up.g
288
+ )
268
289
 
269
290
  def source_function(self, x1, x2):
270
291
  S = np.where(
@@ -286,7 +307,6 @@ class EmissionLine(RadiativeTransition):
286
307
  Delta_E (:obj:`float`): the energy difference between the upper and lower level
287
308
  in [J]
288
309
  name (:obj:`str`): name of the transition
289
- nu0 (:obj:`float`): rest frequency in [Hz]
290
310
  A21 (:obj:`float`): Einstein A21 coefficient in [s\ :sup:`-1`]
291
311
  B21 (:obj:`float`): Einstein B21 coefficient in [sr m\ :sup:`2` Hz / Jy]
292
312
  B12 (:obj:`float`): Einstein B12 coefficient in [sr m\ :sup:`2` Hz / Jy]
@@ -295,27 +315,42 @@ class EmissionLine(RadiativeTransition):
295
315
  """
296
316
 
297
317
  def __init__(self, up, low, A21, line_profile_type, width_v, nu0=None):
298
- RadiativeTransition.__init__(self, up=up, low=low, A21=A21, nu0=nu0)
299
- self.line_profile = line_profiles[line_profile_type](
300
- nu0=self.nu0, width_v=width_v
301
- )
318
+ super().__init__(up=up, low=low, A21=A21)
319
+ if nu0 is None:
320
+ nu0 = self.Delta_E_over_h
321
+ else:
322
+ self.check_DeltaE_nu0_consistency(nu0)
323
+ self.line_profile = line_profiles[line_profile_type](nu0=nu0, width_v=width_v)
302
324
  self.tau_kwargs = {"A21": self.A21, "g_up": self.up.g, "g_low": self.low.g}
303
325
 
304
326
  @classmethod
305
327
  def from_radiative_transition(
306
- cls, radiative_transition, line_profile_type, width_v
328
+ cls, radiative_transition, line_profile_type, width_v, nu0=None
307
329
  ):
308
330
  """Alternative constructor, taking an instance of RadiativeTransition,
309
- a line profile class and the width of the line"""
331
+ a line profile class, the width of the line and optionally the rest
332
+ frequency"""
310
333
  return cls(
311
334
  up=radiative_transition.up,
312
335
  low=radiative_transition.low,
313
336
  A21=radiative_transition.A21,
314
337
  line_profile_type=line_profile_type,
315
338
  width_v=width_v,
316
- nu0=radiative_transition.nu0,
339
+ nu0=nu0,
317
340
  )
318
341
 
342
+ def check_DeltaE_nu0_consistency(self, nu0):
343
+ if not np.isclose(self.Delta_E_over_h, nu0, atol=0, rtol=1e-2):
344
+ mean = (self.Delta_E_over_h + nu0) / 2
345
+ relative_diff = abs(self.Delta_E_over_h - nu0) / mean
346
+ warnings.warn(
347
+ "inconsistency between DeltaE/h and the frequency listed in LAMDA file"
348
+ + f" for transition {self.name}. "
349
+ + f" DeltaE/h={self.Delta_E_over_h/constants.giga} GHz, "
350
+ + f"listed frequency = {nu0/constants.giga}, relative difference ="
351
+ + f"{relative_diff*100:.2g}%"
352
+ )
353
+
319
354
  def tau(self, N1, N2, nu):
320
355
  r"""Computes the optical depth
321
356
 
@@ -351,7 +386,7 @@ class EmissionLine(RadiativeTransition):
351
386
  phi_nu=self.line_profile.phi_nu0,
352
387
  N1=N1,
353
388
  N2=N2,
354
- nu=self.nu0,
389
+ nu=self.line_profile.nu0,
355
390
  **self.tau_kwargs,
356
391
  )
357
392
 
@@ -368,8 +403,9 @@ class CollisionalTransition(Transition):
368
403
  """
369
404
 
370
405
  def __init__(self, up, low, K21_data, Tkin_data):
371
- Transition.__init__(self, up=up, low=low)
372
- assert np.all(K21_data >= 0)
406
+ super().__init__(up=up, low=low)
407
+ if np.any(K21_data < 0):
408
+ raise ValueError("negative K21 values")
373
409
  self.K21_data = K21_data
374
410
  self.Tkin_data = Tkin_data
375
411
  self.Tkin_data_limits = np.min(self.Tkin_data), np.max(self.Tkin_data)
@@ -386,15 +422,13 @@ class CollisionalTransition(Transition):
386
422
  at the requested temperature(s)
387
423
 
388
424
  Raises:
389
- AssertionError: If Tkin is outside the available temperature range.
425
+ ValueError: If Tkin is outside the available temperature range.
390
426
  """
391
427
  Tmin, Tmax = self.Tkin_data_limits
392
- assert np.all(
393
- Tmin <= Tkin
394
- ), "requested temperature below minimum collider temperature"
395
- assert np.all(
396
- Tkin <= Tmax
397
- ), "requested temperature above maximum collider temperature"
428
+ if np.any(Tkin < Tmin):
429
+ raise ValueError("requested temperature below minimum collider temperature")
430
+ if np.any(Tkin > Tmax):
431
+ raise ValueError("requested temperature above maximum collider temperature")
398
432
  K21 = np.interp(Tkin, self.Tkin_data, self.K21_data)
399
433
  K12 = compute_K12(
400
434
  K21=K21, g_up=self.up.g, g_low=self.low.g, Delta_E=self.Delta_E, Tkin=Tkin
@@ -4,6 +4,7 @@ Created on Sun Nov 12 17:31:02 2017
4
4
 
5
5
  @author: gianni
6
6
  """
7
+
7
8
  import numpy as np
8
9
  import numba as nb
9
10
  from pythonradex import escape_probability_functions
@@ -44,7 +45,7 @@ class StaticSphere(EscapeProbabilityStaticSphere):
44
45
  # for small tau, the Osterbrock formula becomes numerically unstable,
45
46
  # so we use a Taylor expansion
46
47
  min_tau = 1e-2
47
- stable_region = tau > min_tau
48
+ stable_region = np.abs(tau) > min_tau
48
49
  flux_nu = (
49
50
  2
50
51
  * np.pi
@@ -59,7 +60,8 @@ class StaticSphere(EscapeProbabilityStaticSphere):
59
60
  * (tau / 3 - tau**2 / 8 + tau**3 / 30 - tau**4 / 144)
60
61
  ) # from Wolfram Alpha
61
62
  flux_nu = np.where(stable_region, flux_nu, flux_nu_Taylor)
62
- assert np.all(np.isfinite(flux_nu))
63
+ if not np.all(np.isfinite(flux_nu)):
64
+ raise RuntimeError
63
65
  return flux_nu / np.pi
64
66
 
65
67
 
@@ -67,7 +69,7 @@ class StaticSphere(EscapeProbabilityStaticSphere):
67
69
  def exp_tau_factor(tau):
68
70
  # for very small tau, the exp can introduce numerical errors, so we
69
71
  # take care of that when computing (1-exp(-tau))
70
- return np.where(tau < 1e-5, tau, 1 - np.exp(-tau))
72
+ return np.where(np.abs(tau) < 1e-5, tau, 1 - np.exp(-tau))
71
73
 
72
74
 
73
75
  class Flux1D:
@@ -9,7 +9,6 @@ Created on Thu May 23 11:10:38 2024
9
9
  import numba as nb
10
10
  import numpy as np
11
11
 
12
-
13
12
  # It would be much more elegant to put these functions directly into the
14
13
  # classes defined in escape_probability.py. But I want to compile the beta functions
15
14
  # with numba, so I have to put these functions outside the class definitions
@@ -41,7 +40,8 @@ def identify_tau_regions(tau):
41
40
  + negative_tau_region.sum()
42
41
  + unreliable_negative_region.sum()
43
42
  )
44
- assert total_region_points == len(tau), "selection needs to cover all points of tau"
43
+ if total_region_points != len(tau):
44
+ raise RuntimeError("selection needs to cover all points of tau")
45
45
  return (
46
46
  normal_tau_region,
47
47
  small_tau_region,
@@ -130,7 +130,8 @@ def generate_Taylor_beta(beta_ana, beta_Taylor):
130
130
  prob[negative] = beta_ana(tau[negative])
131
131
  # here I just use abs(tau) to stabilize the code
132
132
  prob[unreliable] = beta_ana(np.abs(tau[unreliable]))
133
- assert np.all(np.isfinite(prob))
133
+ if not np.all(np.isfinite(prob)):
134
+ raise RuntimeError("negative escape probability")
134
135
  return clip_prob(prob)
135
136
 
136
137
  return beta
@@ -185,7 +186,8 @@ def beta_static_slab(tau):
185
186
  # should be ignored for tau < -1
186
187
  int_term = interpolated_integral_term(tau=tau)
187
188
  prob = np.where(tau < min_grid_tau, 1, int_term / tau)
188
- assert np.all(np.isfinite(prob))
189
+ if not np.all(np.isfinite(prob)):
190
+ raise RuntimeError("negative escape probability")
189
191
  return clip_prob(prob)
190
192
 
191
193
 
@@ -213,14 +215,15 @@ def beta_LVG_sphere_RADEX(tau):
213
215
  # I use the same formulae as RADEX, except that I take care of negative tau
214
216
  # handling negative tau turns out to be important, e.g. for CO, Tkin=200,
215
217
  # width_v=3 km/s, Ntot=1e20 cm-2, RADEX gives an invalid solution
216
- assert -7 < min_reliable_tau < -0.01
218
+ if not (-7 < min_reliable_tau < -0.01):
219
+ raise RuntimeError("unexpected value for min_reliable_tau")
217
220
  gtr7 = 7 <= tau
218
221
  less7 = (0.001 <= tau) & (tau < 7)
219
222
  small = (-0.001 <= tau) & (tau < 0.001)
220
223
  negative = (min_reliable_tau <= tau) & (tau < -0.001)
221
224
  unreliable_less7 = (-7 <= tau) & (tau < min_reliable_tau)
222
225
  unreliable_gtr7 = tau < -7
223
- assert (
226
+ if not (
224
227
  gtr7.sum()
225
228
  + less7.sum()
226
229
  + small.sum()
@@ -228,7 +231,8 @@ def beta_LVG_sphere_RADEX(tau):
228
231
  + unreliable_less7.sum()
229
232
  + unreliable_gtr7.sum()
230
233
  == tau.size
231
- )
234
+ ):
235
+ raise RuntimeError
232
236
  beta = np.empty_like(tau)
233
237
  beta[gtr7] = beta_LVG_sphere_RADEX_gtr7(tau[gtr7])
234
238
  beta[less7] = beta_LVG_sphere_RADEX_less7(tau[less7])
pythonradex/helpers.py CHANGED
@@ -74,7 +74,8 @@ def relative_difference(a, b):
74
74
  a=0 and b!=0: return 1"""
75
75
  abs_diff = np.abs(a - b)
76
76
  rel_diff = np.where((a == 0) & (b == 0), 0, np.where(a == 0, 1, abs_diff / a))
77
- assert not np.any(np.isnan(rel_diff))
77
+ if np.any(np.isnan(rel_diff)):
78
+ raise RuntimeError("relative difference calculation returned nan values")
78
79
  return np.abs(rel_diff)
79
80
 
80
81
 
pythonradex/intensity.py CHANGED
@@ -46,9 +46,10 @@ class IntensityCalculator:
46
46
  if self.is_LVG_sphere
47
47
  else self.n_nu_elements["regular"]
48
48
  )
49
- assert (
50
- n_nu_elements % 2 == 1
51
- ), "need odd number of elements to include nu0 in the array"
49
+ if n_nu_elements % 2 != 1:
50
+ raise RuntimeError(
51
+ "need odd number of elements to include nu0 in the array"
52
+ )
52
53
  middle_index = n_nu_elements // 2
53
54
  nu0 = self.emitting_molecule.nu0[transitions]
54
55
  width_nu = self.emitting_molecule.width_v / constants.c * nu0
@@ -168,7 +169,7 @@ class IntensityCalculator:
168
169
 
169
170
  def construct_tau_individual_line(self, line, nu, tau_nu0):
170
171
  # tau is prop to phi_nu/nu**2
171
- normalisation = line.line_profile.phi_nu0 / line.nu0**2 / tau_nu0
172
+ normalisation = line.line_profile.phi_nu0 / line.line_profile.nu0**2 / tau_nu0
172
173
  return line.line_profile.phi_nu(nu) / nu**2 / normalisation
173
174
 
174
175
  def set_tau_lines(self):
@@ -224,14 +225,15 @@ class IntensityCalculator:
224
225
  + " are optically thin!"
225
226
  )
226
227
  for dust_func in (self.S_dust, self.tau_dust):
227
- assert np.all(dust_func(self.nu) == 0), "LVG does not support dust"
228
+ if not np.all(dust_func(self.nu) == 0):
229
+ raise RuntimeError("LVG does not support dust")
228
230
  I = np.zeros_like(self.nu)
229
231
  for line_index, line, tau_line in zip(
230
232
  self.nu_selected_line_indices, self.nu_selected_lines, self.tau_lines
231
233
  ):
232
234
  LVG_sphere_kwargs = {
233
235
  "nu": self.nu,
234
- "nu0": line.nu0,
236
+ "nu0": line.line_profile.nu0,
235
237
  "V": self.V_LVG_sphere,
236
238
  }
237
239
  source_function = helpers.B_nu(T=self.Tex[line_index], nu=self.nu)
@@ -251,7 +253,8 @@ class IntensityCalculator:
251
253
  # this function works only if there are no overlapping lines
252
254
  # faster than calling the spectrum function, since I don't need to loop
253
255
  # over the lines
254
- assert not self.emitting_molecule.any_line_has_overlap(transitions)
256
+ if self.emitting_molecule.any_line_has_overlap(transitions):
257
+ raise ValueError("overlapping lines not allowed in this function")
255
258
  nu = self.emitting_molecule.nu0[transitions]
256
259
  tau_lines = self.tau_nu0_individual_transitions[transitions]
257
260
  Tex = self.Tex[transitions]
@@ -265,7 +268,8 @@ class IntensityCalculator:
265
268
  kwargs = {"tau": tau_tot, "source_function": source_function}
266
269
  if self.is_LVG_sphere:
267
270
  for dust in (S_dust, tau_dust):
268
- assert np.all(dust == 0), "LVG does not support dust"
271
+ if not np.all(dust == 0):
272
+ raise RuntimeError("LVG does not support dust")
269
273
  return escape_probability.specific_intensity_nu0_lvg_sphere(**kwargs)
270
274
  else:
271
275
  return self.specific_intensity(**kwargs)
pythonradex/molecule.py CHANGED
@@ -4,6 +4,7 @@ Created on Sun Nov 12 16:48:00 2017
4
4
 
5
5
  @author: gianni
6
6
  """
7
+
7
8
  from scipy import constants
8
9
  import numpy as np
9
10
  from pythonradex import LAMDA_file, atomic_transition
@@ -38,19 +39,12 @@ class Molecule:
38
39
  datafilepath (:obj:`str`): The filepath to the LAMDA file.
39
40
  partition_function (func): A user-supplied partition function of one argument
40
41
  (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
+ will be calculated by using the data from the LAMDA file.
42
43
  """
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, read_frequencies=True)
49
- except:
50
- data = LAMDA_file.read(datafilepath=datafilepath, read_frequencies=False)
51
- self.levels = data["levels"]
52
- self.rad_transitions = data["radiative transitions"]
53
- self.coll_transitions = data["collisional transitions"]
44
+ self.lamda_data = LAMDA_file.read(datafilepath=datafilepath)
45
+ self.levels = self.lamda_data["levels"]
46
+ self.rad_transitions = self.lamda_data["radiative transitions"]
47
+ self.coll_transitions = self.lamda_data["collisional transitions"]
54
48
  self.n_levels = len(self.levels)
55
49
  self.n_rad_transitions = len(self.rad_transitions)
56
50
  self.set_partition_function(partition_function=partition_function)
@@ -58,7 +52,9 @@ class Molecule:
58
52
  self.A21 = np.array([line.A21 for line in self.rad_transitions])
59
53
  self.B21 = np.array([line.B21 for line in self.rad_transitions])
60
54
  self.B12 = np.array([line.B12 for line in self.rad_transitions])
61
- self.nu0 = np.array([line.nu0 for line in self.rad_transitions])
55
+ self.Delta_E_over_h = np.array(
56
+ [line.Delta_E_over_h for line in self.rad_transitions]
57
+ )
62
58
  self.gup_rad_transitions = np.array(
63
59
  [line.up.g for line in self.rad_transitions]
64
60
  )
@@ -148,7 +144,8 @@ class Molecule:
148
144
  for i, line in enumerate(self.rad_transitions)
149
145
  if line.name == transition_name
150
146
  ]
151
- assert len(candidate_numbers) == 1
147
+ if len(candidate_numbers) != 1:
148
+ raise RuntimeError("unable to get unique transition number")
152
149
  return candidate_numbers[0]
153
150
 
154
151
 
@@ -174,8 +171,8 @@ class EmittingMolecule(Molecule):
174
171
  datafilepath,
175
172
  line_profile_type,
176
173
  width_v,
177
- read_frequencies=False,
178
174
  partition_function=None,
175
+ use_datafile_frequencies=True,
179
176
  ):
180
177
  """Constructs a new instance of the EmittingMolecule class using a LAMDA datafile
181
178
 
@@ -185,30 +182,33 @@ class EmittingMolecule(Molecule):
185
182
  'rectangular' or 'Gaussian'
186
183
  width_v (:obj:`float`): The width of the line profile in [m/s]. For a Gaussian
187
184
  line profile, this corresponds to the FWHM.
188
- read_frequencies (:obj:`bool`): Whether to read the frequencies of the
189
- radiative transitions from the file or not. If False, calculates the
190
- frequencies from the level energies given in the file. Setting this to
191
- True can be useful since frequencies are sometimes given with more
192
- significant digits than level energies. However, the LAMDA format does not
193
- force a file to contain the frequencies, so they might not be present.
194
185
  partition_function (func): A user-supplied partition function of one argument
195
186
  (temperature). Defaults to None. If equal to None, the partition function
196
187
  will be calculated by using the data from the provided LAMDA file.
188
+ use_datafile_frequencies (bool): whether to use the frequencies read from the LAMDA
189
+ file (if False, will use DeltaE/h).
197
190
  """
198
- Molecule.__init__(
199
- self, datafilepath=datafilepath, partition_function=partition_function
191
+ super().__init__(
192
+ datafilepath=datafilepath, partition_function=partition_function
200
193
  )
201
194
  self.width_v = width_v
202
195
  self.line_profile_type = line_profile_type
203
- # convert radiative transitions to emission lines (but keep the same attribute name)
204
- self.rad_transitions = [
205
- atomic_transition.EmissionLine.from_radiative_transition(
206
- radiative_transition=rad_trans,
196
+ # convert RadiativeTransition to EmissionLine (but keep the same attribute name)
197
+ converted_transitions = []
198
+ for i, line in enumerate(self.rad_transitions):
199
+ if use_datafile_frequencies:
200
+ nu0 = self.lamda_data["frequencies"][i]
201
+ else:
202
+ nu0 = None
203
+ converted = atomic_transition.EmissionLine.from_radiative_transition(
204
+ radiative_transition=line,
207
205
  line_profile_type=line_profile_type,
208
206
  width_v=width_v,
207
+ nu0=nu0,
209
208
  )
210
- for rad_trans in self.rad_transitions
211
- ]
209
+ converted_transitions.append(converted)
210
+ self.rad_transitions = converted_transitions
211
+ self.nu0 = np.array([line.line_profile.nu0 for line in self.rad_transitions])
212
212
  self.identify_overlapping_lines()
213
213
  if self.any_line_has_overlap(line_indices=range(self.n_rad_transitions)):
214
214
  self.has_overlapping_lines = True
@@ -218,16 +218,18 @@ class EmittingMolecule(Molecule):
218
218
  self.Tkin_data_limits = {}
219
219
  for collider, coll_transitions in self.coll_transitions.items():
220
220
  self.Tkin_data[collider] = coll_transitions[0].Tkin_data
221
- assert np.all(self.Tkin_data[collider][:-1] < self.Tkin_data[collider][1:])
221
+ if np.any(self.Tkin_data[collider][:-1] >= self.Tkin_data[collider][1:]):
222
+ raise RuntimeError
222
223
  for coll_trans in coll_transitions:
223
- assert np.all(coll_trans.Tkin_data == self.Tkin_data[collider])
224
+ if np.any(coll_trans.Tkin_data != self.Tkin_data[collider]):
225
+ raise RuntimeError
224
226
  self.Tkin_data_limits[collider] = np.min(self.Tkin_data[collider]), np.max(
225
227
  self.Tkin_data[collider]
226
228
  )
227
229
  self.set_K21_matrix()
228
230
  # collecting phi_nu0 and coll parameters, needed for numba-compiled functions:
229
231
  self.phi_nu0 = np.array(
230
- [line.line_profile.phi_nu(line.nu0) for line in self.rad_transitions]
232
+ [line.line_profile.phi_nu0 for line in self.rad_transitions]
231
233
  )
232
234
  self.coll_gups = {}
233
235
  self.coll_glows = {}
@@ -364,7 +366,8 @@ class EmittingMolecule(Molecule):
364
366
 
365
367
  def interpolate_K(self, Tkin, collider):
366
368
  Tlimits = self.Tkin_data_limits[collider]
367
- assert Tlimits[0] <= Tkin <= Tlimits[1]
369
+ if not (Tlimits[0] <= Tkin <= Tlimits[1]):
370
+ raise ValueError("Tkin outside bounds")
368
371
  output = {}
369
372
  Tkin_data = self.Tkin_data[collider]
370
373
  j = np.searchsorted(Tkin_data, Tkin, side="left")
@@ -66,6 +66,7 @@ class Source:
66
66
  geometry,
67
67
  line_profile_type,
68
68
  width_v,
69
+ use_datafile_frequencies=True,
69
70
  use_Ng_acceleration=True,
70
71
  treat_line_overlap=False,
71
72
  warn_negative_tau=True,
@@ -99,6 +100,14 @@ class Source:
99
100
  we can also say that width_v=dv/dr*2*R. For "LVG slab", width_v=dv/dz*Z
100
101
  where Z is the depth of the slab and dv/dz the constant velocity
101
102
  gradient of the slab.
103
+ use_datafile_frequencies (bool): whether to use the frequencies read from the LAMDA
104
+ file (if False, will use Delta_E/h). Defaults to True. In principle, the
105
+ frequencies in a LAMDA-formatted file are only for informational purposes.
106
+ However, they are sometimes more accurate than Delta_E/h, where Delta_E is
107
+ the energy difference between the two levels of the transition, as computed
108
+ from the level energies in the file. Using the frequencies can have
109
+ advantages especially if one models spectra. But the frequencies and level
110
+ energies in the files are not always fully consistent.
102
111
  use_Ng_acceleration (:obj:`bool`): Whether to use Ng acceleration. Defaults
103
112
  to True.
104
113
  treat_line_overlap (:obj:`bool`): Whether to treat the overlap of emission
@@ -116,13 +125,15 @@ class Source:
116
125
  by general user. Defaults to False.
117
126
 
118
127
  """
119
- assert (
120
- width_v < 10000 * constants.kilo
121
- ), "assumption of small nu0/Delta_nu for flux calculation not satisfied"
128
+ if width_v >= 10000 * constants.kilo:
129
+ raise ValueError(
130
+ "assumption of small nu0/Delta_nu for flux calculation not satisfied"
131
+ )
122
132
  self.emitting_molecule = EmittingMolecule(
123
133
  datafilepath=datafilepath,
124
134
  line_profile_type=line_profile_type,
125
135
  width_v=width_v,
136
+ use_datafile_frequencies=use_datafile_frequencies,
126
137
  )
127
138
  self.treat_line_overlap = treat_line_overlap
128
139
  self.geometry_name = geometry
@@ -264,9 +275,10 @@ class Source:
264
275
  "tau_dust": tau_dust,
265
276
  }
266
277
  for p_name, p in params.items():
267
- assert (
268
- p is not None
269
- ), f"for initial setup, all params need to be defined; {p_name} missing"
278
+ if p is None:
279
+ raise ValueError(
280
+ f"for initial setup, all params need to be defined; {p_name} missing"
281
+ )
270
282
  self.rate_equations = rate_equations.RateEquations(
271
283
  molecule=self.emitting_molecule,
272
284
  treat_line_overlap=self.treat_line_overlap,
@@ -418,13 +430,13 @@ class Source:
418
430
  self.emitting_molecule.rad_transitions[i]
419
431
  for i in negative_tau_transition_indices
420
432
  ]
421
- warnings.warn("negative optical depth!")
433
+ warn_message = "negative optical depth!"
422
434
  for i, trans in zip(
423
435
  negative_tau_transition_indices, negative_tau_transitions
424
436
  ):
425
- print(
426
- f"{trans.name}: tau_nu0 = {self.tau_nu0_individual_transitions[i]:.3g}"
427
- )
437
+ warn_message += f" {trans.name}: tau_nu0 = {self.tau_nu0_individual_transitions[i]:.3g};"
438
+ warn_message = warn_message[:-1]
439
+ warnings.warn(warn_message)
428
440
  self.level_pop = level_pop
429
441
  self.Tex = self.emitting_molecule.get_Tex(level_pop)
430
442
  self.intensity_calculator = intensity.IntensityCalculator(
@@ -684,7 +696,7 @@ class Source:
684
696
  for i, line in enumerate(self.emitting_molecule.rad_transitions):
685
697
  output = (
686
698
  f"{line.up.index:>4d} {line.low.index:>4d} "
687
- + f"{line.nu0/constants.giga:>14.6f} {self.Tex[i]:>10.2f} "
699
+ + f"{line.line_profile.nu0/constants.giga:>14.6f} {self.Tex[i]:>10.2f} "
688
700
  + f"{self.level_pop[line.low.index]:>14g} "
689
701
  + f"{self.level_pop[line.up.index]:>14g} "
690
702
  + f"{self.tau_nu0_individual_transitions[i]:>14g}"
@@ -65,9 +65,10 @@ class RateEquations:
65
65
 
66
66
  def assign_func_nu(self, func_name, argument):
67
67
  if isinstance(argument, numbers.Number):
68
- assert (
69
- argument >= 0
70
- ), "ext_background, T_dust or tau_dust cannot be negative"
68
+ if argument < 0:
69
+ raise ValueError(
70
+ "ext_background, T_dust or tau_dust cannot be negative"
71
+ )
71
72
  setattr(self, func_name, self.generate_constant_func(const_value=argument))
72
73
  else:
73
74
  setattr(self, func_name, argument)
@@ -390,8 +391,9 @@ class RateEquations:
390
391
  Gamma = self.GammaR(level_population=level_population) + self.GammaC
391
392
  Gamma[0, :] = np.ones(self.molecule.n_levels)
392
393
  fractional_population = np.linalg.solve(Gamma, b=self.b)
393
- assert np.all(fractional_population >= 0), (
394
- "negative level population, potentially due to high column"
395
- + "density and/or low collider density"
396
- )
394
+ if np.any(fractional_population < 0):
395
+ raise RuntimeError(
396
+ "negative level population, potentially due to high column"
397
+ + "density and/or low collider density"
398
+ )
397
399
  return fractional_population
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pythonradex
3
- Version: 2.0.2
3
+ Version: 3.0.1
4
4
  Summary: 1D non-LTE radiative transfer
5
5
  Project-URL: Documentation, https://pythonradex.readthedocs.io/en/latest/
6
6
  Project-URL: Issues, https://github.com/gica3618/pythonradex/issues
@@ -9,12 +9,14 @@ Author-email: Gianni Cataldi <cataldi.gia@gmail.com>
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE.txt
11
11
  Keywords: non-LTE,radex,radiative transfer
12
- Classifier: Development Status :: 4 - Beta
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Programming Language :: Python
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
18
20
  Requires-Python: >=3.10
19
21
  Requires-Dist: numba>=0.61.0
20
22
  Requires-Dist: numpy>=2.0
@@ -0,0 +1,14 @@
1
+ pythonradex/LAMDA_file.py,sha256=oVUVYNjuJl3ZMBd_T8dknWb0WExL12KuZbYsh1ZMalo,6716
2
+ pythonradex/__init__.py,sha256=iLQfJx9dbUoHmti29jtlTNopYzTdS7O1mBbZfb0Kin0,223
3
+ pythonradex/atomic_transition.py,sha256=1-l8PJYcbwutzrM5QxLOi0fc3pz-13u4pYvCYdJp1HM,16313
4
+ pythonradex/escape_probability.py,sha256=UCs0Ec4AGEN92uN9A79cuFsapa7fUFGKD8tNtODqrV0,5608
5
+ pythonradex/escape_probability_functions.py,sha256=oLoY8h8zXqpdz0cYq3Cc7TFKVV1Lqa6Si6pGd4xopE4,9419
6
+ pythonradex/helpers.py,sha256=IY_EUtvMA12uuQxwFz-CZNEPuQqPBLl7xl7FVb5-w6k,3285
7
+ pythonradex/intensity.py,sha256=6oOltX6-h_6QsIr5jvscN0yCN_JsTpwXm-3uTmsTX2M,12199
8
+ pythonradex/molecule.py,sha256=hMZqidSXIIGYOx_zqE7Ccylsuo6718GOESkI3Mbk5B8,17489
9
+ pythonradex/radiative_transfer.py,sha256=DvKyRss9ByDX4sXG5PSV112ACOK4GYmc9gpTWmST-Es,34012
10
+ pythonradex/rate_equations.py,sha256=x8CYB6O7ErYD65JiBiBQe4XqTMySSowqAijcCCQU9bk,15410
11
+ pythonradex-3.0.1.dist-info/METADATA,sha256=lbbXRE9JlomsGm9_vc3vhEsDDdFInoepo885rxtckqU,2706
12
+ pythonradex-3.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
13
+ pythonradex-3.0.1.dist-info/licenses/LICENSE.txt,sha256=biAf-Gv0h8Xr2r-CuwVZJsJKnj6WCL6KJfof8mz-M0U,1071
14
+ pythonradex-3.0.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,14 +0,0 @@
1
- pythonradex/LAMDA_file.py,sha256=Mk_CM5MZTpd-TsirfhkGpv_XvXbPk25iAMB0yeyGcxU,6816
2
- pythonradex/__init__.py,sha256=iLQfJx9dbUoHmti29jtlTNopYzTdS7O1mBbZfb0Kin0,223
3
- pythonradex/atomic_transition.py,sha256=8fTmDZ4uSiHT5fHa_ANVqsdZDMNVn7q9eJUxGdr1qOE,14394
4
- pythonradex/escape_probability.py,sha256=FaK-m2rQUccsr7zwnupPPWBfHItCW_Sj3I61TE1ZgJU,5559
5
- pythonradex/escape_probability_functions.py,sha256=WNFgF66N8e8SajfJJkwRHxtMDhq7aGC_lOD5kcl0Q0A,9175
6
- pythonradex/helpers.py,sha256=kBSdLgsoh7wx80veLBtPx-nUyeRWMDdlaTm_LB7CW-I,3210
7
- pythonradex/intensity.py,sha256=poBcfRqtVURE-UGUI0x-9aIWbWhaH1xpQC0q5ET2Skk,11987
8
- pythonradex/molecule.py,sha256=4e2-1L8QHr4TavCUnNwolhwjMi6-AO98G6J_tFnSt5o,17658
9
- pythonradex/radiative_transfer.py,sha256=3tyC_e4CiLIYbaX3IKG1IdclAcKGYT7DyJZaMp_VNJ0,33065
10
- pythonradex/rate_equations.py,sha256=mFLO6GCT6lADg6niGcVwTamN9gPJXAwgs75o86zjnYE,15338
11
- pythonradex-2.0.2.dist-info/METADATA,sha256=g_ieHy-bFHDd0KmZGWByqOZkMynAOLWw09fEwFMf2dU,2591
12
- pythonradex-2.0.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
13
- pythonradex-2.0.2.dist-info/licenses/LICENSE.txt,sha256=biAf-Gv0h8Xr2r-CuwVZJsJKnj6WCL6KJfof8mz-M0U,1071
14
- pythonradex-2.0.2.dist-info/RECORD,,