pythonradex 1.0.8__py3-none-any.whl → 2.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.
@@ -11,74 +11,89 @@ from scipy import constants
11
11
  import numba as nb
12
12
 
13
13
 
14
- @nb.jit(nopython=True,cache=True)
15
- def tau_nu(A21,phi_nu,g_low,g_up,N1,N2,nu):
16
- return constants.c**2/(8*np.pi*nu**2)*A21*phi_nu*(g_up/g_low*N1-N2)
14
+ @nb.jit(nopython=True, cache=True)
15
+ def tau(A21, phi_nu, g_low, g_up, N1, N2, nu):
16
+ return (
17
+ constants.c**2 / (8 * np.pi * nu**2) * A21 * phi_nu * (g_up / g_low * N1 - N2)
18
+ )
17
19
 
18
- @nb.jit(nopython=True,cache=True,error_model='numpy')
19
- def Tex(Delta_E,g_low,g_up,x1,x2):
20
- return np.where((x1==0) & (x2==0),0,
21
- -Delta_E/(constants.k*np.log(g_low*x2/(g_up*x1))))
22
20
 
23
- @nb.jit(nopython=True,cache=True)
24
- def compute_K12(K21,g_up,g_low,Delta_E,Tkin):
25
- #see RADEX manual
26
- return (g_up/g_low*K21*np.exp(-Delta_E/(constants.k*Tkin)))
21
+ @nb.jit(nopython=True, cache=True, error_model="numpy")
22
+ def Tex(Delta_E, g_low, g_up, x1, x2):
23
+ return np.where(
24
+ (x1 == 0) & (x2 == 0),
25
+ 0,
26
+ -Delta_E / (constants.k * np.log(g_low * x2 / (g_up * x1))),
27
+ )
27
28
 
28
29
 
29
- class LineProfile():
30
-
31
- '''Abstract class representing a general, normalised line profile.'''
30
+ @nb.jit(nopython=True, cache=True)
31
+ def compute_K12(K21, g_up, g_low, Delta_E, Tkin):
32
+ # see RADEX manual
33
+ return g_up / g_low * K21 * np.exp(-Delta_E / (constants.k * Tkin))
34
+
35
+
36
+ class LineProfile:
37
+ """Abstract class representing a general, normalised line profile."""
32
38
 
33
39
  n_nu_elements_for_coarse_array = 10
34
40
 
35
- def __init__(self,nu0,width_v):
41
+ def __init__(self, nu0, width_v):
36
42
  self.nu0 = nu0
37
43
  self.width_v = width_v
38
- self.width_nu = helpers.Delta_nu(Delta_v=self.width_v,nu0=self.nu0)
44
+ self.width_nu = helpers.Delta_nu(Delta_v=self.width_v, nu0=self.nu0)
39
45
  self.initialise_phi_nu_params()
40
- # self.initialise_coarse_nu_array()
41
- # self.coarse_phi_nu_array = self.phi_nu(nu=self.coarse_nu_array)
46
+ self.set_nu_extent()
42
47
  self.phi_nu0 = self.phi_nu(nu=self.nu0)
43
48
 
44
49
  def initialise_phi_nu_params(self):
45
50
  raise NotImplementedError
46
-
47
- def phi_nu(self,nu):
48
- r'''The value of the line profile in frequency space.
49
-
51
+
52
+ def phi_nu(self, nu):
53
+ r"""The value of the line profile in frequency space.
54
+
50
55
  The line profile is normalised such that its integral over frequency equals 1.
51
56
 
52
57
  Args:
53
58
  nu (float or numpy.ndarray): frequency in [Hz]
54
-
59
+
55
60
  Returns:
56
61
  float or numpy.ndarray: The line profile in [Hz\ :sup:`-1`]
57
- '''
62
+ """
58
63
  raise NotImplementedError
59
64
 
60
- def phi_v(self,v):
61
- r'''The value of the line profile in velocity space.
62
-
65
+ def phi_v(self, v):
66
+ r"""The value of the line profile in velocity space.
67
+
63
68
  The line profile is normalised such that its integral over velocity equals 1.
64
69
 
65
70
  Args:
66
71
  v (float or numpy.ndarray): velocity in [m/s]
67
-
72
+
68
73
  Returns:
69
74
  float or numpy.ndarra: The line profile in [(m/s)\ :sup:`-1`]
70
- '''
71
- nu = self.nu0*(1-v/constants.c)
72
- return self.phi_nu(nu)*self.nu0/constants.c
75
+ """
76
+ nu = self.nu0 * (1 - v / constants.c)
77
+ return self.phi_nu(nu) * self.nu0 / constants.c
73
78
 
74
- def average_over_phi_nu(self,func):
79
+ def average_over_phi_nu(self, func):
75
80
  raise NotImplementedError
76
81
 
82
+ def set_nu_extent(self):
83
+ # this function should calculate the min and max frequency covered by the
84
+ # profile; this is used to calculate overlapping lines
85
+ raise NotImplementedError
77
86
 
78
- class GaussianLineProfile(LineProfile):
87
+ def covers_frequency(self, nu):
88
+ # check if the line profile covers a given frequency
89
+ # don't do self.nu_min <= nu <= self.nu_max, otherwise it does not work
90
+ # if nu is an array
91
+ return (self.nu_min <= nu) & (nu <= self.nu_max)
79
92
 
93
+
94
+ class GaussianLineProfile(LineProfile):
80
95
  """Represents a Gaussian line profile
81
-
96
+
82
97
  Attributes:
83
98
  nu0 (:obj:`float`): rest frequency in [Hz]
84
99
  width_nu (:obj:`float`): FWHM in [Hz]
@@ -87,125 +102,143 @@ class GaussianLineProfile(LineProfile):
87
102
 
88
103
  def initialise_phi_nu_params(self):
89
104
  self.sigma_nu = helpers.FWHM2sigma(self.width_nu)
90
- self.normalisation = 1/(self.sigma_nu*np.sqrt(2*np.pi))
91
-
92
- def phi_nu(self,nu):
93
- return self.normalisation*np.exp(-(nu-self.nu0)**2/(2*self.sigma_nu**2))
94
-
95
- def average_over_phi_nu(self,func):
96
- #choose +- 1 FWHM initially, where the Gaussian is at ~6% of the peak
97
- #note that at least two iterations are done, so the final width is
98
- #at least +-1.65 FWHM
99
- width = 2 #units of width_nu
105
+ self.normalisation = 1 / (self.sigma_nu * np.sqrt(2 * np.pi))
106
+
107
+ def phi_nu(self, nu):
108
+ return self.normalisation * np.exp(
109
+ -((nu - self.nu0) ** 2) / (2 * self.sigma_nu**2)
110
+ )
111
+
112
+ def average_over_phi_nu(self, func):
113
+ # choose +- 1 FWHM initially, where the Gaussian is at ~6% of the peak
114
+ # note that at least two iterations are done, so the final width is
115
+ # at least +-1.65 FWHM
116
+ width = 2 # units of width_nu
100
117
  elements_per_width = 20
101
118
  old_average = np.inf
102
119
  residual = np.inf
103
120
  niter = 0
104
121
  while residual > 1e-2:
105
- n_elements = int(elements_per_width*width)
106
- nu = np.linspace(self.nu0-width/2*self.width_nu,
107
- self.nu0+width/2*self.width_nu,n_elements)
122
+ n_elements = int(elements_per_width * width)
123
+ nu = np.linspace(
124
+ self.nu0 - width / 2 * self.width_nu,
125
+ self.nu0 + width / 2 * self.width_nu,
126
+ n_elements,
127
+ )
108
128
  phi_nu = self.phi_nu(nu)
109
- norm = np.trapezoid(y=phi_nu,x=nu)
110
- new_average = np.trapezoid(y=func(nu)*phi_nu,x=nu)/norm
111
- residual = helpers.relative_difference(a=np.array((new_average,)),
112
- b=np.array((old_average,)))
129
+ norm = np.trapezoid(y=phi_nu, x=nu)
130
+ new_average = np.trapezoid(y=func(nu) * phi_nu, x=nu) / norm
131
+ residual = helpers.relative_difference(
132
+ a=np.array((new_average,)), b=np.array((old_average,))
133
+ )
113
134
  old_average = new_average
114
- #I want to cover at least the width corresponding to the definition of
115
- #overlapping lines, which is 3.14; so add 1.3 to make it 2+1.3=3.3
135
+ # I want to cover at least the width corresponding to the definition of
136
+ # overlapping lines, which is 3.14; so add 1.3 to make it 2+1.3=3.3
116
137
  width += 1.3
117
138
  elements_per_width += 15
118
139
  niter += 1
119
140
  return new_average
120
141
 
142
+ def set_nu_extent(self):
143
+ # a Gaussian is never zero, so I choose +- 1.57 FWHM, where the Gaussian
144
+ # is 0.1% of the peak
145
+ self.nu_min = self.nu0 - 1.57 * self.width_nu
146
+ self.nu_max = self.nu0 + 1.57 * self.width_nu
121
147
 
122
- class RectangularLineProfile(LineProfile):
123
148
 
124
- '''Represents a rectangular line profile, i.e. constant over width_v, 0 outside'''
149
+ class RectangularLineProfile(LineProfile):
150
+ """Represents a rectangular line profile, i.e. constant over width_v, 0 outside"""
125
151
 
126
152
  def initialise_phi_nu_params(self):
127
- self.normalisation = 1/self.width_nu
153
+ self.normalisation = 1 / self.width_nu
128
154
 
129
- def phi_nu(self,nu):
130
- inside_line = (nu>=self.nu0-self.width_nu/2) & (nu<=self.nu0+self.width_nu/2)
131
- return np.where(inside_line,self.normalisation,0)
155
+ def phi_nu(self, nu):
156
+ inside_line = (nu >= self.nu_min) & (nu <= self.nu_max)
157
+ return np.where(inside_line, self.normalisation, 0)
132
158
 
133
- def average_over_phi_nu(self,func):
134
- #easier than Gaussian, just have to iterate over n_elements
135
- #note that the func doesn't need to be constant over the line profile,
136
- #so I need to have a sufficient number of elements
159
+ def average_over_phi_nu(self, func):
160
+ # easier than Gaussian, just have to iterate over n_elements
161
+ # note that the func doesn't need to be constant over the line profile,
162
+ # so I need to have a sufficient number of elements
137
163
  n_elements = 20
138
164
  old_average = np.inf
139
165
  residual = np.inf
140
166
  while residual > 1e-2:
141
- nu = np.linspace(self.nu0-self.width_nu/2,self.nu0+self.width_nu/2,
142
- n_elements)
167
+ nu = np.linspace(self.nu_min, self.nu_max, n_elements)
143
168
  phi_nu = self.phi_nu(nu)
144
- norm = np.trapezoid(y=phi_nu,x=nu)
145
- new_average = np.trapezoid(y=func(nu)*phi_nu,x=nu)/norm
146
- residual = helpers.relative_difference(a=np.array((new_average,)),
147
- b=np.array((old_average,)))
169
+ norm = np.trapezoid(y=phi_nu, x=nu)
170
+ new_average = np.trapezoid(y=func(nu) * phi_nu, x=nu) / norm
171
+ residual = helpers.relative_difference(
172
+ a=np.array((new_average,)), b=np.array((old_average,))
173
+ )
148
174
  old_average = new_average
149
175
  n_elements += 15
150
176
  return new_average
151
177
 
152
- line_profiles = {'Gaussian':GaussianLineProfile,'rectangular':RectangularLineProfile}
178
+ def set_nu_extent(self):
179
+ self.nu_min = self.nu0 - self.width_nu / 2
180
+ self.nu_max = self.nu0 + self.width_nu / 2
181
+
153
182
 
183
+ line_profiles = {"Gaussian": GaussianLineProfile, "rectangular": RectangularLineProfile}
154
184
 
155
- class Level():
156
- '''Represents an atomic / molecular level.
185
+
186
+ class Level:
187
+ """Represents an atomic / molecular level.
157
188
 
158
189
  Attributes:
159
190
  g (:obj:`float`): the statistical weight of the level
160
191
  E (:obj:`float`): the energy of the level in [J]
161
- number (:obj:`int`): the number of the level
162
- '''
192
+ index (:obj:`int`): the index of the level; if constructed from a
193
+ LAMDA-formatted file, this index indicates the postiion in the file,
194
+ with the first level listed in the file having index 0.
195
+ """
163
196
 
164
- def __init__(self,g,E,number):
197
+ def __init__(self, g, E, index):
165
198
  self.g = g
166
199
  self.E = E
167
- self.number = number
200
+ self.index = index
168
201
 
169
- def LTE_level_pop(self,Z,T):
170
- '''Calculates the fractional population of the level in LTE.
202
+ def Boltzmann_level_population(self, Z, T):
203
+ """Calculates the fractional population of the level using a Boltzmann
204
+ distribution.
171
205
 
172
206
  Args:
173
207
  Z (:obj:`float`): The partition function.
174
208
  T (:obj:`float`): The temperature in [K].
175
-
209
+
176
210
  Returns:
177
- float: The fractional population of the level in LTE.
178
- '''
179
- #this is a convenience function that is not used when solving the non-LTE
180
- #radiative transfer
181
- return self.g*np.exp(-self.E/(constants.k*T))/Z
211
+ float: The fractional population of the level using a Boltzmann
212
+ distribution.
213
+ """
214
+ # this is a convenience function that is not used when solving the non-LTE
215
+ # radiative transfer
216
+ return self.g * np.exp(-self.E / (constants.k * T)) / Z
182
217
 
183
218
 
184
- class Transition():
219
+ class Transition:
185
220
 
186
- def __init__(self,up,low):
221
+ def __init__(self, up, low):
187
222
  self.up = up
188
223
  self.low = low
189
- self.Delta_E = self.up.E-self.low.E
190
- self.name = f'{self.up.number}-{self.low.number}'
224
+ self.Delta_E = self.up.E - self.low.E
225
+ self.name = f"{self.up.index}-{self.low.index}"
226
+
227
+ def Tex(self, x1, x2):
228
+ """Computes the excitation temperature.
191
229
 
192
- def Tex(self,x1,x2):
193
- '''Computes the excitation temperature.
194
-
195
230
  Args:
196
231
  x1: (numpy.ndarray): fractional population of the lower level
197
232
  x2: (numpy.ndarray): fractional population of the upper level
198
233
 
199
234
  Returns:
200
235
  numpy.ndarray: excitation temperature in [K]
201
- '''
202
- return Tex(Delta_E=self.Delta_E,g_up=self.up.g,g_low=self.low.g,
203
- x1=x1,x2=x2)
236
+ """
237
+ return Tex(Delta_E=self.Delta_E, g_up=self.up.g, g_low=self.low.g, x1=x1, x2=x2)
204
238
 
205
239
 
206
240
  class RadiativeTransition(Transition):
207
-
208
- r'''Represents the radiative transition between two energy levels.
241
+ r"""Represents the radiative transition between two energy levels.
209
242
 
210
243
  Attributes:
211
244
  up (pythonradex.atomic_transition.Level): the upper level of the transition
@@ -216,25 +249,27 @@ class RadiativeTransition(Transition):
216
249
  A21 (:obj:`float`): Einstein A21 coefficient in [s\ :sup:`-1`]
217
250
  B21 (:obj:`float`): Einstein B21 coefficient in [sr m\ :sup:`2` Hz / Jy]
218
251
  B12 (:obj:`float`): Einstein B12 coefficient in [sr m\ :sup:`2` Hz / Jy]
219
- '''
252
+ """
220
253
 
221
- def __init__(self,up,low,A21,nu0=None):
222
- Transition.__init__(self,up=up,low=low)
223
- assert self.Delta_E > 0, 'non-positive Delta_E for radiative transition'
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"
224
257
  self.A21 = A21
225
- nu0_from_Delta_E = self.Delta_E/constants.h
258
+ nu0_from_Delta_E = self.Delta_E / constants.h
226
259
  if nu0 is None:
227
260
  self.nu0 = nu0_from_Delta_E
228
261
  else:
229
- #when reading LAMDA files, it is useful to specify nu0 directly, since
230
- #it is sometimes given with more significant digits
231
- assert np.isclose(nu0_from_Delta_E,nu0,atol=0,rtol=1e-3)
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)
232
265
  self.nu0 = nu0
233
- self.B21 = helpers.B21(A21=self.A21,nu=self.nu0)
234
- self.B12 = helpers.B12(A21=self.A21,nu=self.nu0,g1=self.low.g,g2=self.up.g)
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)
235
268
 
236
- def source_function(self,x1,x2):
237
- S = np.where((x1==0) & (x2==0),0,self.A21*x2/(x1*self.B12-x2*self.B21))
269
+ def source_function(self, x1, x2):
270
+ S = np.where(
271
+ (x1 == 0) & (x2 == 0), 0, self.A21 * x2 / (x1 * self.B12 - x2 * self.B21)
272
+ )
238
273
  # if np.any(S < 0):
239
274
  # raise RuntimeError('negative source function, potentially due to '
240
275
  # +'high columndensity and/or low collider density')
@@ -242,10 +277,9 @@ class RadiativeTransition(Transition):
242
277
 
243
278
 
244
279
  class EmissionLine(RadiativeTransition):
245
-
246
- r'''Represents an emission line arising from the radiative transition between
280
+ r"""Represents an emission line arising from the radiative transition between
247
281
  two levels
248
-
282
+
249
283
  Attributes:
250
284
  up (pythonradex.atomic_transition.Level): the upper level of the transition
251
285
  low (pythonradex.atomic_transition.Level): the lower level of the transition
@@ -258,24 +292,33 @@ class EmissionLine(RadiativeTransition):
258
292
  B12 (:obj:`float`): Einstein B12 coefficient in [sr m\ :sup:`2` Hz / Jy]
259
293
  line_profile (pythonradex.atomic_transition.LineProfile): object representing
260
294
  the shape of the line profile
261
- '''
262
- def __init__(self,up,low,A21,line_profile_type,width_v,nu0=None):
263
- RadiativeTransition.__init__(self,up=up,low=low,A21=A21,nu0=nu0)
264
- self.line_profile = line_profiles[line_profile_type](nu0=self.nu0,width_v=width_v)
265
- self.tau_kwargs = {'A21':self.A21,'g_up':self.up.g,'g_low':self.low.g}
295
+ """
296
+
297
+ 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
+ )
302
+ self.tau_kwargs = {"A21": self.A21, "g_up": self.up.g, "g_low": self.low.g}
266
303
 
267
304
  @classmethod
268
- def from_radiative_transition(cls,radiative_transition,line_profile_type,
269
- width_v):
270
- '''Alternative constructor, taking an instance of RadiativeTransition,
271
- a line profile class and the width of the line'''
272
- return cls(up=radiative_transition.up,low=radiative_transition.low,
273
- A21=radiative_transition.A21,line_profile_type=line_profile_type,
274
- width_v=width_v,nu0=radiative_transition.nu0)
275
-
276
- def tau_nu(self,N1,N2,nu):
277
- r'''Computes the optical depth
278
-
305
+ def from_radiative_transition(
306
+ cls, radiative_transition, line_profile_type, width_v
307
+ ):
308
+ """Alternative constructor, taking an instance of RadiativeTransition,
309
+ a line profile class and the width of the line"""
310
+ return cls(
311
+ up=radiative_transition.up,
312
+ low=radiative_transition.low,
313
+ A21=radiative_transition.A21,
314
+ line_profile_type=line_profile_type,
315
+ width_v=width_v,
316
+ nu0=radiative_transition.nu0,
317
+ )
318
+
319
+ def tau(self, N1, N2, nu):
320
+ r"""Computes the optical depth
321
+
279
322
  Args:
280
323
  N1: (:obj:`float`): column density of molecules in the lower level
281
324
  in [m\ :sup:`-2`]
@@ -285,14 +328,15 @@ class EmissionLine(RadiativeTransition):
285
328
 
286
329
  Returns:
287
330
  numpy.ndarray: the optical depth at the requested frequencies
288
-
289
- '''
290
- return tau_nu(phi_nu=self.line_profile.phi_nu(nu),N1=N1,N2=N2,nu=nu,
291
- **self.tau_kwargs)
292
-
293
- def tau_nu0(self,N1,N2):
294
- r'''Computes the optical depth at the rest frequency
295
-
331
+
332
+ """
333
+ return tau(
334
+ phi_nu=self.line_profile.phi_nu(nu), N1=N1, N2=N2, nu=nu, **self.tau_kwargs
335
+ )
336
+
337
+ def tau_nu0(self, N1, N2):
338
+ r"""Computes the optical depth at the rest frequency
339
+
296
340
  Args:
297
341
  N1: (:obj:`float` or numpy.ndarray): column density of molecules
298
342
  in the lower level in [m\ :sup:`-2`]
@@ -301,33 +345,37 @@ class EmissionLine(RadiativeTransition):
301
345
 
302
346
  Returns:
303
347
  float or numpy.ndarray: the optical depth at the rest frequency
304
-
305
- '''
306
- return tau_nu(phi_nu=self.line_profile.phi_nu0,N1=N1,N2=N2,nu=self.nu0,
307
- **self.tau_kwargs)
308
348
 
309
-
349
+ """
350
+ return tau(
351
+ phi_nu=self.line_profile.phi_nu0,
352
+ N1=N1,
353
+ N2=N2,
354
+ nu=self.nu0,
355
+ **self.tau_kwargs,
356
+ )
357
+
358
+
310
359
  class CollisionalTransition(Transition):
360
+ """Represents the collisional transition between two energy levels
311
361
 
312
- '''Represents the collisional transition between two energy levels
313
-
314
362
  Attributes:
315
363
  up (pythonradex.atomic_transition.Level): the upper level of the transition
316
364
  low (pythonradex.atomic_transition.Level): the lower level of the transition
317
365
  Delta_E (:obj:`float`): the energy difference between the upper and lower level
318
366
  in [J]
319
367
  name (:obj:`str`): name of the transition
320
- '''
368
+ """
321
369
 
322
- def __init__(self,up,low,K21_data,Tkin_data):
323
- Transition.__init__(self,up=up,low=low)
370
+ def __init__(self, up, low, K21_data, Tkin_data):
371
+ Transition.__init__(self, up=up, low=low)
324
372
  assert np.all(K21_data >= 0)
325
373
  self.K21_data = K21_data
326
374
  self.Tkin_data = Tkin_data
327
- self.Tkin_data_limits = np.min(self.Tkin_data),np.max(self.Tkin_data)
375
+ self.Tkin_data_limits = np.min(self.Tkin_data), np.max(self.Tkin_data)
328
376
 
329
- def coeffs(self,Tkin):
330
- r'''
377
+ def coeffs(self, Tkin):
378
+ r"""
331
379
  Computes the collisional coefficients
332
380
 
333
381
  Args:
@@ -339,13 +387,16 @@ class CollisionalTransition(Transition):
339
387
 
340
388
  Raises:
341
389
  AssertionError: If Tkin is outside the available temperature range.
342
- '''
343
- Tmin,Tmax = self.Tkin_data_limits
344
- assert np.all(Tmin <= Tkin),\
345
- 'requested temperature below minimum collider temperature'
346
- assert np.all(Tkin <= Tmax),\
347
- 'requested temperature above maximum collider temperature'
348
- K21 = np.interp(Tkin,self.Tkin_data,self.K21_data)
349
- K12 = compute_K12(K21=K21,g_up=self.up.g,g_low=self.low.g,Delta_E=self.Delta_E,
350
- Tkin=Tkin)
351
- return [K12,K21]
390
+ """
391
+ 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"
398
+ K21 = np.interp(Tkin, self.Tkin_data, self.K21_data)
399
+ K12 = compute_K12(
400
+ K21=K21, g_up=self.up.g, g_low=self.low.g, Delta_E=self.Delta_E, Tkin=Tkin
401
+ )
402
+ return [K12, K21]