lightweaver 0.16.1__cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.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.
- lightweaver/Data/AbundancesAsplund09.pickle +0 -0
- lightweaver/Data/AtomicMassesNames.pickle +0 -0
- lightweaver/Data/Barklem_dfdata.dat +41 -0
- lightweaver/Data/Barklem_pddata.dat +40 -0
- lightweaver/Data/Barklem_spdata.dat +46 -0
- lightweaver/Data/DefaultMolecules/C2.molecule +27 -0
- lightweaver/Data/DefaultMolecules/CH/CH_X-A.asc +46409 -0
- lightweaver/Data/DefaultMolecules/CH/CH_X-A_12.asc +28322 -0
- lightweaver/Data/DefaultMolecules/CH/CH_X-B.asc +4272 -0
- lightweaver/Data/DefaultMolecules/CH/CH_X-B_12.asc +2583 -0
- lightweaver/Data/DefaultMolecules/CH/CH_X-C.asc +20916 -0
- lightweaver/Data/DefaultMolecules/CH/CH_X-C_12.asc +13106 -0
- lightweaver/Data/DefaultMolecules/CH.molecule +35 -0
- lightweaver/Data/DefaultMolecules/CN.molecule +30 -0
- lightweaver/Data/DefaultMolecules/CO/vmax=3_Jmax=49_dv=1_26 +296 -0
- lightweaver/Data/DefaultMolecules/CO/vmax=9_Jmax=120_dv=1_26 +2162 -0
- lightweaver/Data/DefaultMolecules/CO.molecule +30 -0
- lightweaver/Data/DefaultMolecules/CO_NLTE.molecule +29 -0
- lightweaver/Data/DefaultMolecules/CaH.molecule +29 -0
- lightweaver/Data/DefaultMolecules/H2+.molecule +27 -0
- lightweaver/Data/DefaultMolecules/H2.molecule +27 -0
- lightweaver/Data/DefaultMolecules/H2O.molecule +27 -0
- lightweaver/Data/DefaultMolecules/HF.molecule +29 -0
- lightweaver/Data/DefaultMolecules/LiH.molecule +27 -0
- lightweaver/Data/DefaultMolecules/MgH.molecule +34 -0
- lightweaver/Data/DefaultMolecules/N2.molecule +28 -0
- lightweaver/Data/DefaultMolecules/NH.molecule +27 -0
- lightweaver/Data/DefaultMolecules/NO.molecule +27 -0
- lightweaver/Data/DefaultMolecules/O2.molecule +27 -0
- lightweaver/Data/DefaultMolecules/OH.molecule +27 -0
- lightweaver/Data/DefaultMolecules/SiO.molecule +26 -0
- lightweaver/Data/DefaultMolecules/TiO.molecule +30 -0
- lightweaver/Data/Quadratures.pickle +0 -0
- lightweaver/Data/pf_Kurucz.input +0 -0
- lightweaver/DefaultIterSchemes/.placeholder +0 -0
- lightweaver/DefaultIterSchemes/SimdImpl_AVX2FMA.cpython-312-x86_64-linux-gnu.so +0 -0
- lightweaver/DefaultIterSchemes/SimdImpl_AVX512.cpython-312-x86_64-linux-gnu.so +0 -0
- lightweaver/DefaultIterSchemes/SimdImpl_SSE2.cpython-312-x86_64-linux-gnu.so +0 -0
- lightweaver/LwCompiled.cpython-312-x86_64-linux-gnu.so +0 -0
- lightweaver/__init__.py +33 -0
- lightweaver/atmosphere.py +1767 -0
- lightweaver/atomic_model.py +852 -0
- lightweaver/atomic_set.py +1286 -0
- lightweaver/atomic_table.py +653 -0
- lightweaver/barklem.py +151 -0
- lightweaver/benchmark.py +113 -0
- lightweaver/broadening.py +605 -0
- lightweaver/collisional_rates.py +337 -0
- lightweaver/config.py +106 -0
- lightweaver/constants.py +22 -0
- lightweaver/crtaf.py +197 -0
- lightweaver/fal.py +440 -0
- lightweaver/iterate_ctx.py +241 -0
- lightweaver/iteration_update.py +134 -0
- lightweaver/libenkiTS.so +0 -0
- lightweaver/molecule.py +225 -0
- lightweaver/multi.py +113 -0
- lightweaver/nr_update.py +106 -0
- lightweaver/rh_atoms.py +19743 -0
- lightweaver/simd_management.py +42 -0
- lightweaver/utils.py +509 -0
- lightweaver/version.py +34 -0
- lightweaver/wittmann.py +1375 -0
- lightweaver/zeeman.py +157 -0
- lightweaver-0.16.1.dist-info/METADATA +81 -0
- lightweaver-0.16.1.dist-info/RECORD +69 -0
- lightweaver-0.16.1.dist-info/WHEEL +6 -0
- lightweaver-0.16.1.dist-info/licenses/LICENSE +21 -0
- lightweaver-0.16.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from fractions import Fraction
|
|
4
|
+
from typing import TYPE_CHECKING, Callable, Optional, Sequence, Tuple, cast
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from weno4 import weno4
|
|
8
|
+
|
|
9
|
+
import lightweaver.constants as Const
|
|
10
|
+
from lightweaver.constants import VMICRO_CHAR
|
|
11
|
+
from .atomic_table import Element, PeriodicTable
|
|
12
|
+
from .broadening import LineBroadening
|
|
13
|
+
from .utils import gaunt_bf, sequence_repr
|
|
14
|
+
from .zeeman import ZeemanComponents, compute_zeeman_components
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .atmosphere import Atmosphere
|
|
18
|
+
from .atomic_set import SpeciesStateTable
|
|
19
|
+
from .collisional_rates import CollisionalRates
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AtomicModel:
|
|
24
|
+
'''
|
|
25
|
+
Container class for the complete description of a model atom.
|
|
26
|
+
|
|
27
|
+
Attributes
|
|
28
|
+
----------
|
|
29
|
+
element : Element
|
|
30
|
+
The element or ion represented by this model.
|
|
31
|
+
levels : list of AtomicLevel
|
|
32
|
+
The levels in use in this model.
|
|
33
|
+
lines : list of AtomicLine
|
|
34
|
+
The atomic lines present in this model.
|
|
35
|
+
continua : list of AtomicContinuum
|
|
36
|
+
The atomic continua present in this model.
|
|
37
|
+
collisions : list of CollisionalRates
|
|
38
|
+
The collisional rates present in this model.
|
|
39
|
+
'''
|
|
40
|
+
element: Element
|
|
41
|
+
levels: Sequence['AtomicLevel']
|
|
42
|
+
lines: Sequence['AtomicLine']
|
|
43
|
+
continua: Sequence['AtomicContinuum']
|
|
44
|
+
collisions: Sequence['CollisionalRates']
|
|
45
|
+
|
|
46
|
+
# @profile
|
|
47
|
+
def __post_init__(self):
|
|
48
|
+
for l in self.levels:
|
|
49
|
+
l.setup(self)
|
|
50
|
+
|
|
51
|
+
for l in self.lines:
|
|
52
|
+
l.setup(self)
|
|
53
|
+
|
|
54
|
+
for c in self.continua:
|
|
55
|
+
c.setup(self)
|
|
56
|
+
|
|
57
|
+
for c in self.collisions:
|
|
58
|
+
c.setup(self)
|
|
59
|
+
|
|
60
|
+
def __repr__(self):
|
|
61
|
+
s = 'AtomicModel(element=%s,\n\tlevels=[\n' % repr(self.element)
|
|
62
|
+
for l in self.levels:
|
|
63
|
+
s += '\t\t' + repr(l) + ',\n'
|
|
64
|
+
s += '\t],\n\tlines=[\n'
|
|
65
|
+
for l in self.lines:
|
|
66
|
+
s += '\t\t' + repr(l) + ',\n'
|
|
67
|
+
s += '\t],\n\tcontinua=[\n'
|
|
68
|
+
for c in self.continua:
|
|
69
|
+
s += '\t\t' + repr(c) + ',\n'
|
|
70
|
+
s += '\t],\n\tcollisions=[\n'
|
|
71
|
+
for c in self.collisions:
|
|
72
|
+
s += '\t\t' + repr(c) + ',\n'
|
|
73
|
+
s += '])\n'
|
|
74
|
+
return s
|
|
75
|
+
|
|
76
|
+
# def __hash__(self):
|
|
77
|
+
# return hash(repr(self))
|
|
78
|
+
|
|
79
|
+
def vBroad(self, atmos: 'Atmosphere') -> np.ndarray:
|
|
80
|
+
'''
|
|
81
|
+
Computes the atomic broadening velocity structure for a given
|
|
82
|
+
atmosphere from the thermal motions and microturbulent velocity.
|
|
83
|
+
'''
|
|
84
|
+
vTherm = 2.0 * Const.KBoltzmann / (Const.Amu * PeriodicTable[self.element].mass)
|
|
85
|
+
vBroad = np.sqrt(vTherm * atmos.temperature + atmos.vturb**2)
|
|
86
|
+
return vBroad
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def transitions(self) -> Sequence['AtomicTransition']:
|
|
90
|
+
'''
|
|
91
|
+
List of all atomic transitions present on the model.
|
|
92
|
+
'''
|
|
93
|
+
return self.lines + self.continua # type: ignore
|
|
94
|
+
|
|
95
|
+
def reconfigure_atom(atom: AtomicModel):
|
|
96
|
+
'''
|
|
97
|
+
Re-perform all atomic set up after modifying parameters.
|
|
98
|
+
'''
|
|
99
|
+
atom.__post_init__()
|
|
100
|
+
|
|
101
|
+
def element_sort(atom: AtomicModel):
|
|
102
|
+
return atom.element
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class AtomicLevel:
|
|
106
|
+
'''
|
|
107
|
+
Description of atomic level in model atom.
|
|
108
|
+
|
|
109
|
+
Attributes
|
|
110
|
+
----------
|
|
111
|
+
E : float
|
|
112
|
+
Energy above ground state [cm-1]
|
|
113
|
+
g : float
|
|
114
|
+
Statistical weight of level
|
|
115
|
+
label : str
|
|
116
|
+
Name for level
|
|
117
|
+
stage : int
|
|
118
|
+
Ionisation of level with 0 being neutral
|
|
119
|
+
atom : AtomicModel
|
|
120
|
+
AtomicModel that holds this level, will be initialised by the atom.
|
|
121
|
+
J : Fraction, optional
|
|
122
|
+
Total quantum angular momentum.
|
|
123
|
+
L : int, optional
|
|
124
|
+
Orbital angular momentum.
|
|
125
|
+
S : Fraction, optional
|
|
126
|
+
Spin.
|
|
127
|
+
'''
|
|
128
|
+
E: float
|
|
129
|
+
g: float
|
|
130
|
+
label: str
|
|
131
|
+
stage: int
|
|
132
|
+
atom: AtomicModel = field(init=False)
|
|
133
|
+
J: Optional[Fraction] = None
|
|
134
|
+
L: Optional[int] = None
|
|
135
|
+
S: Optional[Fraction] = None
|
|
136
|
+
|
|
137
|
+
def setup(self, atom):
|
|
138
|
+
self.atom = atom
|
|
139
|
+
|
|
140
|
+
def __hash__(self):
|
|
141
|
+
return hash((self.E, self.g, self.label, self.stage, self.J, self.L, self.S))
|
|
142
|
+
|
|
143
|
+
def __eq__(self, other: object) -> bool:
|
|
144
|
+
if isinstance(other, AtomicLevel):
|
|
145
|
+
return hash(self) == hash(other)
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def lsCoupling(self) -> bool:
|
|
150
|
+
'''
|
|
151
|
+
Returns whether the L-S coupling formalism can be applied to this
|
|
152
|
+
level.
|
|
153
|
+
'''
|
|
154
|
+
if all(x is not None for x in (self.J, self.L, self.S)):
|
|
155
|
+
J = cast(Fraction, self.J)
|
|
156
|
+
L = cast(int, self.L)
|
|
157
|
+
S = cast(Fraction, self.S)
|
|
158
|
+
if J <= L + S:
|
|
159
|
+
return True
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def E_SI(self):
|
|
164
|
+
'''
|
|
165
|
+
Returns E in Joule.
|
|
166
|
+
'''
|
|
167
|
+
return self.E * Const.HC_CM
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def E_eV(self):
|
|
171
|
+
'''
|
|
172
|
+
Returns E in electron volt.
|
|
173
|
+
'''
|
|
174
|
+
return self.E_SI / Const.EV
|
|
175
|
+
|
|
176
|
+
def __repr__(self):
|
|
177
|
+
s = ('AtomicLevel(E=%10.3f, g=%g, label="%s", stage=%d, '
|
|
178
|
+
'J=%s, L=%s, S=%s)') % (self.E, self.g, self.label, self.stage,
|
|
179
|
+
repr(self.J), repr(self.L), repr(self.S))
|
|
180
|
+
return s
|
|
181
|
+
|
|
182
|
+
class LineType(Enum):
|
|
183
|
+
'''
|
|
184
|
+
Enum to show if the line should be treated in CRD or PRD.
|
|
185
|
+
'''
|
|
186
|
+
CRD = 0
|
|
187
|
+
PRD = auto()
|
|
188
|
+
|
|
189
|
+
def __repr__(self):
|
|
190
|
+
if self == LineType.CRD:
|
|
191
|
+
return 'LineType.CRD'
|
|
192
|
+
elif self == LineType.PRD:
|
|
193
|
+
return 'LineType.PRD'
|
|
194
|
+
else:
|
|
195
|
+
raise ValueError('Unknown LineType in LineType.__repr__')
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class LineQuadrature:
|
|
200
|
+
'''
|
|
201
|
+
Describes the wavelength quadrature to be used for integrating properties
|
|
202
|
+
associated with a line.
|
|
203
|
+
'''
|
|
204
|
+
def setup(self, line: 'AtomicLine'):
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
def doppler_units(self, line: 'AtomicLine') -> np.ndarray:
|
|
208
|
+
'''
|
|
209
|
+
Return the quadrature in Doppler units.
|
|
210
|
+
'''
|
|
211
|
+
raise NotImplementedError
|
|
212
|
+
|
|
213
|
+
def wavelength(self, line: 'AtomicLine', vMicroChar: float=Const.VMICRO_CHAR) -> np.ndarray:
|
|
214
|
+
'''
|
|
215
|
+
Return the quadrature in nm.
|
|
216
|
+
'''
|
|
217
|
+
raise NotImplementedError
|
|
218
|
+
|
|
219
|
+
def __repr__(self):
|
|
220
|
+
raise NotImplementedError
|
|
221
|
+
|
|
222
|
+
def __hash__(self):
|
|
223
|
+
raise NotImplementedError
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class LinearQuadrature(LineQuadrature):
|
|
227
|
+
"""
|
|
228
|
+
Simple linearly spaced wavelength grid. Primarily provided for CRTAF
|
|
229
|
+
interaction.
|
|
230
|
+
|
|
231
|
+
Nlambda : int
|
|
232
|
+
The number of wavelength points in the wavelength grid (typically odd).
|
|
233
|
+
deltaLambda : int
|
|
234
|
+
The half-width of the grid (i.e. from core to one edge) [nm].
|
|
235
|
+
"""
|
|
236
|
+
Nlambda: int
|
|
237
|
+
deltaLambda: float
|
|
238
|
+
|
|
239
|
+
def __repr__(self):
|
|
240
|
+
s = '%s(Nlambda=%d, deltaLambda=%g)' % (type(self).__name__,
|
|
241
|
+
self.Nlambda, self.deltaLambda)
|
|
242
|
+
return s
|
|
243
|
+
|
|
244
|
+
def wavelength(self, line: "AtomicLine", vMicroChar: float = Const.VMICRO_CHAR) -> np.ndarray:
|
|
245
|
+
return np.linspace(line.lambda0 - self.deltaLambda, line.lambda0 + self.deltaLambda, self.Nlambda)
|
|
246
|
+
|
|
247
|
+
def doppler_units(self, line: "AtomicLine") -> np.ndarray:
|
|
248
|
+
wavelength_grid = self.wavelength(line)
|
|
249
|
+
vMicroChar = VMICRO_CHAR
|
|
250
|
+
qToLambda = line.lambda0 * (vMicroChar / Const.CLight)
|
|
251
|
+
return (wavelength_grid - line.lambda0) / qToLambda
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class TabulatedQuadrature(LineQuadrature):
|
|
256
|
+
"""
|
|
257
|
+
Tabulated wavelength quadrature. Primarily provided for CRTAF interaction.
|
|
258
|
+
|
|
259
|
+
wavelengthGrid : Sequence[float]
|
|
260
|
+
The wavelength sample points [nm].
|
|
261
|
+
"""
|
|
262
|
+
wavelengthGrid: Sequence[float]
|
|
263
|
+
|
|
264
|
+
def __repr__(self):
|
|
265
|
+
s = '%s(wavelengthGrid=%s)' % (type(self).__name__, sequence_repr(self.wavelengthGrid))
|
|
266
|
+
return s
|
|
267
|
+
|
|
268
|
+
def wavelength(self, line: "AtomicLine", vMicroChar: float = Const.VMICRO_CHAR) -> np.ndarray:
|
|
269
|
+
return np.ascontiguousarray(self.wavelengthGrid) + line.lambda0
|
|
270
|
+
|
|
271
|
+
def doppler_units(self, line: "AtomicLine") -> np.ndarray:
|
|
272
|
+
wavelength_grid = self.wavelength(line)
|
|
273
|
+
vMicroChar = VMICRO_CHAR
|
|
274
|
+
qToLambda = line.lambda0 * (vMicroChar / Const.CLight)
|
|
275
|
+
return (wavelength_grid - line.lambda0) / qToLambda
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclass
|
|
280
|
+
class LinearCoreExpWings(LineQuadrature):
|
|
281
|
+
"""
|
|
282
|
+
RH-Style line quadrature, with approximately linear core spacing and
|
|
283
|
+
exponential wing spacing, by using a function of the form
|
|
284
|
+
q(n) = a*(n + (exp(b*n)-1))
|
|
285
|
+
with n in [0, N) satisfying the following conditions:
|
|
286
|
+
|
|
287
|
+
- q[0] = 0
|
|
288
|
+
|
|
289
|
+
- q[(N-1)/2] = qcore
|
|
290
|
+
|
|
291
|
+
- q[N-1] = qwing.
|
|
292
|
+
|
|
293
|
+
If qWing <= 2 * qCore, linear grid spacing will be used for this transition.
|
|
294
|
+
"""
|
|
295
|
+
qCore: float
|
|
296
|
+
qWing: float
|
|
297
|
+
Nlambda: int
|
|
298
|
+
beta: float = field(init=False)
|
|
299
|
+
|
|
300
|
+
def __repr__(self):
|
|
301
|
+
s = '%s(qCore=%g, qWing=%g, Nlambda=%d)' % (type(self).__name__,
|
|
302
|
+
self.qCore, self.qWing, self.Nlambda)
|
|
303
|
+
return s
|
|
304
|
+
|
|
305
|
+
def __hash__(self):
|
|
306
|
+
return hash((self.qCore, self.qWing, self.Nlambda))
|
|
307
|
+
|
|
308
|
+
def setup(self, line: 'AtomicLine'):
|
|
309
|
+
if self.qWing <= 2.0 * self.qCore:
|
|
310
|
+
# Use linear scale to qWing
|
|
311
|
+
self.beta = 1.0
|
|
312
|
+
else:
|
|
313
|
+
self.beta = self.qWing / (2.0 * self.qCore)
|
|
314
|
+
|
|
315
|
+
def doppler_units(self, line: 'AtomicLine') -> np.ndarray:
|
|
316
|
+
Nlambda = self.Nlambda // 2 if self.Nlambda % 2 == 1 else (self.Nlambda - 1) // 2
|
|
317
|
+
Nlambda += 1
|
|
318
|
+
beta = self.beta
|
|
319
|
+
|
|
320
|
+
y = beta + np.sqrt(beta**2 + (beta - 1.0) * Nlambda + 2.0 - 3.0 * beta)
|
|
321
|
+
b = 2.0 * np.log(y) / (Nlambda - 1)
|
|
322
|
+
a = self.qWing / (Nlambda - 2.0 + y**2)
|
|
323
|
+
nl = np.arange(Nlambda)
|
|
324
|
+
q: np.ndarray = a * (nl + (np.exp(b * nl) - 1.0))
|
|
325
|
+
|
|
326
|
+
NlambdaFull = 2 * Nlambda - 1
|
|
327
|
+
result = np.zeros(NlambdaFull)
|
|
328
|
+
Nmid = Nlambda - 1
|
|
329
|
+
|
|
330
|
+
result[:Nmid][::-1] = -q[1:]
|
|
331
|
+
result[Nmid+1:] = q[1:]
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
def wavelength(self, line: 'AtomicLine', vMicroChar=Const.VMICRO_CHAR) -> np.ndarray:
|
|
335
|
+
qToLambda = line.lambda0 * (vMicroChar / Const.CLight)
|
|
336
|
+
result = self.doppler_units(line)
|
|
337
|
+
result *= qToLambda
|
|
338
|
+
result += line.lambda0
|
|
339
|
+
return result
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass
|
|
343
|
+
class AtomicTransition:
|
|
344
|
+
'''
|
|
345
|
+
Basic storage class for atomic transitions. Both lines and continua are
|
|
346
|
+
derived from this.
|
|
347
|
+
'''
|
|
348
|
+
j: int
|
|
349
|
+
i: int
|
|
350
|
+
atom: AtomicModel = field(init=False)
|
|
351
|
+
jLevel: AtomicLevel = field(init=False)
|
|
352
|
+
iLevel: AtomicLevel = field(init=False)
|
|
353
|
+
|
|
354
|
+
def setup(self, atom: AtomicModel):
|
|
355
|
+
if self.j < self.i:
|
|
356
|
+
self.i, self.j = self.j, self.i
|
|
357
|
+
self.atom = atom
|
|
358
|
+
self.jLevel: AtomicLevel = self.atom.levels[self.j]
|
|
359
|
+
self.iLevel: AtomicLevel = self.atom.levels[self.i]
|
|
360
|
+
|
|
361
|
+
def __hash__(self):
|
|
362
|
+
raise NotImplementedError
|
|
363
|
+
|
|
364
|
+
def __eq__(self, other: object) -> bool:
|
|
365
|
+
if other is self:
|
|
366
|
+
return True
|
|
367
|
+
|
|
368
|
+
return repr(self) == repr(other)
|
|
369
|
+
|
|
370
|
+
def wavelength(self) -> np.ndarray:
|
|
371
|
+
raise NotImplementedError
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def lambda0(self) -> float:
|
|
375
|
+
raise NotImplementedError
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def lambda0_m(self) -> float:
|
|
379
|
+
raise NotImplementedError
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def transId(self) -> Tuple[Element, int, int]:
|
|
383
|
+
'''
|
|
384
|
+
Unique identifier (transition ID) for transition (assuming one copy
|
|
385
|
+
of each Element), used in creating a SpectrumConfiguration etc.
|
|
386
|
+
'''
|
|
387
|
+
return (self.atom.element, self.i, self.j)
|
|
388
|
+
|
|
389
|
+
@dataclass
|
|
390
|
+
class LineProfileState:
|
|
391
|
+
'''
|
|
392
|
+
Dataclass used to communicate line profile calculations from the backend
|
|
393
|
+
to the frontend whilst allowing the backend to provide an overrideable
|
|
394
|
+
optimised voigt implementation for the default case.
|
|
395
|
+
|
|
396
|
+
Attributes
|
|
397
|
+
----------
|
|
398
|
+
wavelength : np.ndarray
|
|
399
|
+
Wavelengths at which to compute the line profile [nm]
|
|
400
|
+
vlosMu : np.ndarray
|
|
401
|
+
Bulk velocity projected onto each ray in the angular integration scheme
|
|
402
|
+
[m/s] in an array of [Nmu, Nspace].
|
|
403
|
+
atmos : Atmosphere
|
|
404
|
+
The associated atmosphere.
|
|
405
|
+
eqPops : SpeciesStateTable
|
|
406
|
+
The associated populations for each species present in the simulation.
|
|
407
|
+
default_voigt_callback : callable
|
|
408
|
+
Computes the Voigt profile for the default case, takes the damping
|
|
409
|
+
parameter aDamp and broadening velocity vBroad as arguments, and
|
|
410
|
+
returns the line profile phi (in this case phi_num in the tech report).
|
|
411
|
+
vBroad : np.ndarray, optional
|
|
412
|
+
Cache to avoid recomputing vBroad every time. May be None.
|
|
413
|
+
'''
|
|
414
|
+
|
|
415
|
+
wavelength: np.ndarray
|
|
416
|
+
vlosMu: np.ndarray
|
|
417
|
+
atmos: 'Atmosphere'
|
|
418
|
+
eqPops: 'SpeciesStateTable'
|
|
419
|
+
default_voigt_callback: Callable[[np.ndarray, np.ndarray], np.ndarray]
|
|
420
|
+
vBroad: Optional[np.ndarray]=None
|
|
421
|
+
|
|
422
|
+
@dataclass
|
|
423
|
+
class LineProfileResult:
|
|
424
|
+
'''
|
|
425
|
+
Dataclass for returning the line profile and associated data that needs
|
|
426
|
+
to be saved (damping parameter and elastic collision rate) from the
|
|
427
|
+
frontend to the backend.
|
|
428
|
+
'''
|
|
429
|
+
phi: np.ndarray
|
|
430
|
+
aDamp: np.ndarray
|
|
431
|
+
Qelast: np.ndarray
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@dataclass(eq=False)
|
|
435
|
+
class AtomicLine(AtomicTransition):
|
|
436
|
+
'''
|
|
437
|
+
Base class for atomic lines, holding their specialised information over
|
|
438
|
+
transitions.
|
|
439
|
+
|
|
440
|
+
Attributes
|
|
441
|
+
----------
|
|
442
|
+
f : float
|
|
443
|
+
Oscillator strength.
|
|
444
|
+
type : LineType
|
|
445
|
+
Should the line be treated in PRD or CRD.
|
|
446
|
+
quadrature : LineQuadrature
|
|
447
|
+
Wavelength quadrature for integrating line properties over.
|
|
448
|
+
broadening : LineBroadening
|
|
449
|
+
Object describing the broadening processes to be used in conjunction
|
|
450
|
+
with the quadrature to generate the line profile.
|
|
451
|
+
gLandeEff : float, optional
|
|
452
|
+
Optionally override LS-coupling (if available for this transition),
|
|
453
|
+
and just directly set the effective Lande g factor (if it isn't).
|
|
454
|
+
'''
|
|
455
|
+
f: float
|
|
456
|
+
type: LineType
|
|
457
|
+
quadrature: LineQuadrature
|
|
458
|
+
broadening: LineBroadening
|
|
459
|
+
gLandeEff: Optional[float] = None
|
|
460
|
+
|
|
461
|
+
def setup(self, atom: AtomicModel):
|
|
462
|
+
super().setup(atom)
|
|
463
|
+
self.quadrature.setup(self)
|
|
464
|
+
self.broadening.setup(self)
|
|
465
|
+
|
|
466
|
+
def __repr__(self):
|
|
467
|
+
s = '%s(j=%d, i=%d, f=%9.3e, type=%s, quadrature=%s, broadening=%s' % (
|
|
468
|
+
type(self).__name__,
|
|
469
|
+
self.j, self.i, self.f, repr(self.type),
|
|
470
|
+
repr(self.quadrature), repr(self.broadening))
|
|
471
|
+
if self.gLandeEff is not None:
|
|
472
|
+
s += ', gLandeEff=%e' % self.gLandeEff
|
|
473
|
+
s += ')'
|
|
474
|
+
return s
|
|
475
|
+
|
|
476
|
+
def __hash__(self):
|
|
477
|
+
return hash(repr(self))
|
|
478
|
+
|
|
479
|
+
def wavelength(self, vMicroChar=Const.VMICRO_CHAR) -> np.ndarray:
|
|
480
|
+
'''
|
|
481
|
+
Returns the wavelength grid for this transition based on the
|
|
482
|
+
LineQuadrature.
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
vMicroChar : float, optional
|
|
487
|
+
Characterisitc microturbulent velocity to assume when computing
|
|
488
|
+
the line quadrature (default 3e3 m/s).
|
|
489
|
+
'''
|
|
490
|
+
return self.quadrature.wavelength(self, vMicroChar=vMicroChar)
|
|
491
|
+
|
|
492
|
+
def zeeman_components(self) -> Optional[ZeemanComponents]:
|
|
493
|
+
'''
|
|
494
|
+
Returns the Zeeman components of a line, if possible or None.
|
|
495
|
+
'''
|
|
496
|
+
return compute_zeeman_components(self)
|
|
497
|
+
|
|
498
|
+
def compute_phi(self, state: LineProfileState) -> LineProfileResult:
|
|
499
|
+
'''
|
|
500
|
+
Compute the line profile, intended to be called from the backend.
|
|
501
|
+
'''
|
|
502
|
+
raise NotImplementedError
|
|
503
|
+
|
|
504
|
+
@property
|
|
505
|
+
def overlyingContinuumLevel(self) -> AtomicLevel:
|
|
506
|
+
'''
|
|
507
|
+
Find the first overlying continuum level.
|
|
508
|
+
'''
|
|
509
|
+
Z = self.jLevel.stage + 1
|
|
510
|
+
j = self.j
|
|
511
|
+
ic = j + 1
|
|
512
|
+
try:
|
|
513
|
+
while self.atom.levels[ic].stage < Z:
|
|
514
|
+
ic += 1
|
|
515
|
+
cont = self.atom.levels[ic]
|
|
516
|
+
return cont
|
|
517
|
+
except IndexError:
|
|
518
|
+
raise ValueError('No overlying continuum level found for line %s' % repr(self))
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def lambda0(self) -> float:
|
|
522
|
+
'''
|
|
523
|
+
Return the line rest wavelength [nm].
|
|
524
|
+
'''
|
|
525
|
+
return self.lambda0_m / Const.NM_TO_M
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def lambda0_m(self) -> float:
|
|
529
|
+
'''
|
|
530
|
+
Return the line rest wavelength [m].
|
|
531
|
+
'''
|
|
532
|
+
deltaE = self.jLevel.E_SI - self.iLevel.E_SI
|
|
533
|
+
return Const.HC / deltaE
|
|
534
|
+
|
|
535
|
+
@property
|
|
536
|
+
def Aji(self) -> float:
|
|
537
|
+
'''
|
|
538
|
+
Return the Einstein A coefficient for this line.
|
|
539
|
+
'''
|
|
540
|
+
gRatio = self.iLevel.g / self.jLevel.g
|
|
541
|
+
C: float = 2 * np.pi * (Const.QElectron / Const.Epsilon0) \
|
|
542
|
+
* (Const.QElectron / Const.MElectron) / Const.CLight
|
|
543
|
+
return C / self.lambda0_m**2 * gRatio * self.f
|
|
544
|
+
|
|
545
|
+
@property
|
|
546
|
+
def Bji(self) -> float:
|
|
547
|
+
'''
|
|
548
|
+
Return the Einstein B_{ji} coefficient for this line.
|
|
549
|
+
'''
|
|
550
|
+
return self.lambda0_m**3 / (2.0 * Const.HC) * self.Aji
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def Bij(self) -> float:
|
|
554
|
+
'''
|
|
555
|
+
Return the Einstein B_{ij} coefficient for this line.
|
|
556
|
+
'''
|
|
557
|
+
return self.jLevel.g / self.iLevel.g * self.Bji
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def polarisable(self) -> bool:
|
|
561
|
+
'''
|
|
562
|
+
Return whether sufficient information is available to compute full
|
|
563
|
+
Stokes solutions for this line.
|
|
564
|
+
'''
|
|
565
|
+
return (self.iLevel.lsCoupling and self.jLevel.lsCoupling) or (self.gLandeEff is not None)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@dataclass(eq=False, repr=False)
|
|
569
|
+
class VoigtLine(AtomicLine):
|
|
570
|
+
'''
|
|
571
|
+
Specialised line profile for the default case of a Voigt profile.
|
|
572
|
+
'''
|
|
573
|
+
|
|
574
|
+
def damping(self, atmos: 'Atmosphere', eqPops: 'SpeciesStateTable',
|
|
575
|
+
vBroad: Optional[np.ndarray]=None):
|
|
576
|
+
'''
|
|
577
|
+
Computes the damping parameter and elastic collision rate.
|
|
578
|
+
|
|
579
|
+
Parameters
|
|
580
|
+
----------
|
|
581
|
+
atmos : Atmosphere
|
|
582
|
+
The atmosphere to consider.
|
|
583
|
+
eqPops : SpeciesStateTable
|
|
584
|
+
The populations in this atmosphere.
|
|
585
|
+
vBroad : np.ndarray, optional
|
|
586
|
+
The broadening velocity, will be used if passed, or computed
|
|
587
|
+
using atom.vBroad if not.
|
|
588
|
+
|
|
589
|
+
Returns
|
|
590
|
+
-------
|
|
591
|
+
aDamp : np.ndarray
|
|
592
|
+
The Voigt damping parameter.
|
|
593
|
+
Qelast : np.ndarray
|
|
594
|
+
The rate of elastic collisions broadening the line -- needed for PRD.
|
|
595
|
+
'''
|
|
596
|
+
Qs = self.broadening.broaden(atmos, eqPops)
|
|
597
|
+
|
|
598
|
+
if vBroad is None:
|
|
599
|
+
vBroad = self.atom.vBroad(atmos)
|
|
600
|
+
|
|
601
|
+
cDop = self.lambda0_m / (4.0 * np.pi)
|
|
602
|
+
aDamp = (Qs.natural + Qs.Qelast) * cDop / vBroad
|
|
603
|
+
return aDamp, Qs.Qelast
|
|
604
|
+
|
|
605
|
+
def compute_phi(self, state: LineProfileState) -> LineProfileResult:
|
|
606
|
+
'''
|
|
607
|
+
Computes the line profile.
|
|
608
|
+
|
|
609
|
+
In the case of a VoigtLine the line profile simply uses the
|
|
610
|
+
default_voigt_callback from the backend.
|
|
611
|
+
|
|
612
|
+
Parameters
|
|
613
|
+
----------
|
|
614
|
+
state : LineProfileState
|
|
615
|
+
The information from the backend
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
result : LineProfileResult
|
|
620
|
+
The line profile, as well as the damping parameter 'a' and and
|
|
621
|
+
the broadening velocity.
|
|
622
|
+
'''
|
|
623
|
+
vBroad = self.atom.vBroad(state.atmos) if state.vBroad is None else state.vBroad
|
|
624
|
+
aDamp, Qelast = self.damping(state.atmos, state.eqPops, vBroad=vBroad)
|
|
625
|
+
cb = state.default_voigt_callback
|
|
626
|
+
# NOTE(cmo): This is affected by mypy #5485, so we ignore typing for now
|
|
627
|
+
phi = state.default_voigt_callback(aDamp, vBroad) # type: ignore
|
|
628
|
+
|
|
629
|
+
return LineProfileResult(phi=phi, aDamp=aDamp, Qelast=Qelast)
|
|
630
|
+
|
|
631
|
+
@dataclass(eq=False)
|
|
632
|
+
class AtomicContinuum(AtomicTransition):
|
|
633
|
+
'''
|
|
634
|
+
Base class for atomic continua.
|
|
635
|
+
'''
|
|
636
|
+
|
|
637
|
+
def setup(self, atom: AtomicModel):
|
|
638
|
+
super().setup(atom)
|
|
639
|
+
|
|
640
|
+
def __repr__(self):
|
|
641
|
+
s = 'AtomicContinuum(j=%d, i=%d)' % (self.j, self.i)
|
|
642
|
+
return s
|
|
643
|
+
|
|
644
|
+
def __hash__(self):
|
|
645
|
+
return hash(repr(self))
|
|
646
|
+
|
|
647
|
+
def alpha(self, wavelength: np.ndarray) -> np.ndarray:
|
|
648
|
+
'''
|
|
649
|
+
Returns the cross-section as a function of wavelength
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
wavelength : np.ndarray
|
|
654
|
+
The wavelengths at which to compute the cross-section
|
|
655
|
+
|
|
656
|
+
Returns
|
|
657
|
+
-------
|
|
658
|
+
alpha : np.ndarray
|
|
659
|
+
The cross-section for each wavelength
|
|
660
|
+
'''
|
|
661
|
+
raise NotImplementedError
|
|
662
|
+
|
|
663
|
+
def wavelength(self) -> np.ndarray:
|
|
664
|
+
'''
|
|
665
|
+
The wavelength grid on which this continuum's cross section is defined.
|
|
666
|
+
'''
|
|
667
|
+
raise NotImplementedError
|
|
668
|
+
|
|
669
|
+
@property
|
|
670
|
+
def minLambda(self) -> float:
|
|
671
|
+
'''
|
|
672
|
+
The minimum wavelength at which this transition contributes.
|
|
673
|
+
'''
|
|
674
|
+
raise NotImplementedError
|
|
675
|
+
|
|
676
|
+
@property
|
|
677
|
+
def lambda0(self) -> float:
|
|
678
|
+
'''
|
|
679
|
+
The maximum (edge) wavelength at which this transition contributes [nm].
|
|
680
|
+
'''
|
|
681
|
+
return self.lambda0_m / Const.NM_TO_M
|
|
682
|
+
|
|
683
|
+
@property
|
|
684
|
+
def lambdaEdge(self) -> float:
|
|
685
|
+
'''
|
|
686
|
+
The maximum (edge) wavelength at which this transition contributes [nm].
|
|
687
|
+
'''
|
|
688
|
+
return self.lambda0
|
|
689
|
+
|
|
690
|
+
@property
|
|
691
|
+
def lambda0_m(self) -> float:
|
|
692
|
+
'''
|
|
693
|
+
The maximum (edge) wavelength at which this transition contributes [m].
|
|
694
|
+
'''
|
|
695
|
+
deltaE = self.jLevel.E_SI - self.iLevel.E_SI
|
|
696
|
+
return Const.HC / deltaE
|
|
697
|
+
|
|
698
|
+
@property
|
|
699
|
+
def polarisable(self) -> bool:
|
|
700
|
+
'''
|
|
701
|
+
Returns whether this continuum is polarisable, always False.
|
|
702
|
+
'''
|
|
703
|
+
return False
|
|
704
|
+
|
|
705
|
+
@dataclass(eq=False)
|
|
706
|
+
class ExplicitContinuum(AtomicContinuum):
|
|
707
|
+
'''
|
|
708
|
+
Specific version of atomic continuum with tabulated cross-section against
|
|
709
|
+
wavelength. Interpolated using weno4.
|
|
710
|
+
Attributes
|
|
711
|
+
----------
|
|
712
|
+
wavelengthGrid : list of float
|
|
713
|
+
Wavelengths at which cross-section is tabulated [nm].
|
|
714
|
+
alphaGrid : list of float
|
|
715
|
+
Tabulated cross-sections [m2].
|
|
716
|
+
'''
|
|
717
|
+
wavelengthGrid: Sequence[float]
|
|
718
|
+
alphaGrid: Sequence[float]
|
|
719
|
+
|
|
720
|
+
def setup(self, atom: AtomicModel):
|
|
721
|
+
super().setup(atom)
|
|
722
|
+
self.wavelengthGrid = np.asarray(self.wavelengthGrid) # type: ignore
|
|
723
|
+
if not np.all(np.diff(self.wavelengthGrid) > 0.0):
|
|
724
|
+
raise ValueError(('Wavelength array not monotonically'
|
|
725
|
+
' increasing in continuum %s') % repr(self))
|
|
726
|
+
self.alphaGrid = np.asarray(self.alphaGrid) # type: ignore
|
|
727
|
+
if self.lambdaEdge - self.wavelengthGrid[-1] > 0.01:
|
|
728
|
+
wav = np.concatenate((self.wavelengthGrid, np.array([self.lambdaEdge])))
|
|
729
|
+
self.wavelengthGrid = wav
|
|
730
|
+
self.alphaGrid = np.concatenate((self.alphaGrid, np.array([self.alphaGrid[-1]])))
|
|
731
|
+
|
|
732
|
+
def __repr__(self):
|
|
733
|
+
s = 'ExplicitContinuum(j=%d, i=%d, wavelengthGrid=%s, alphaGrid=%s)' % (self.j, self.i,
|
|
734
|
+
sequence_repr(self.wavelengthGrid), sequence_repr(self.alphaGrid))
|
|
735
|
+
return s
|
|
736
|
+
|
|
737
|
+
def alpha(self, wavelength: np.ndarray) -> np.ndarray:
|
|
738
|
+
'''
|
|
739
|
+
Computes cross-section as a function of wavelength.
|
|
740
|
+
|
|
741
|
+
Parameters
|
|
742
|
+
----------
|
|
743
|
+
wavelength : np.ndarray
|
|
744
|
+
Wavelengths at which to compute the cross-section [nm].
|
|
745
|
+
|
|
746
|
+
Returns
|
|
747
|
+
-------
|
|
748
|
+
alpha : np.ndarray
|
|
749
|
+
Cross-section at associated wavelength.
|
|
750
|
+
'''
|
|
751
|
+
alpha = weno4(wavelength, self.wavelengthGrid, self.alphaGrid, left=0.0, right=0.0)
|
|
752
|
+
alpha[wavelength < self.minLambda] = 0.0
|
|
753
|
+
alpha[wavelength > self.lambdaEdge] = 0.0
|
|
754
|
+
if np.any(alpha < 0.0):
|
|
755
|
+
# NOTE(cmo): If weno4 has exploded to the extent that there are negatives, something has gone very wrong (e.g. overly sampled verticals in the cross-section resonances), so switch to linear interpolation.
|
|
756
|
+
alpha = np.interp(wavelength, self.wavelengthGrid, self.alphaGrid, left=0.0, right=0.0)
|
|
757
|
+
alpha[wavelength < self.minLambda] = 0.0
|
|
758
|
+
alpha[wavelength > self.lambdaEdge] = 0.0
|
|
759
|
+
alpha[alpha < 0.0] = 0.0
|
|
760
|
+
return alpha
|
|
761
|
+
|
|
762
|
+
def wavelength(self) -> np.ndarray:
|
|
763
|
+
'''
|
|
764
|
+
Returns the wavelength grid at which this transition needs to be
|
|
765
|
+
computed to be correctly integrated. Specific handling is added to
|
|
766
|
+
ensure that it is treated properly close to the edge.
|
|
767
|
+
'''
|
|
768
|
+
grid = cast(np.ndarray, self.wavelengthGrid)
|
|
769
|
+
edge = self.lambdaEdge
|
|
770
|
+
result = np.copy(grid[(grid >= self.minLambda) & (grid <= edge)])
|
|
771
|
+
# NOTE(cmo): If the last value before the edge is more than 0.1 nm away
|
|
772
|
+
# then put the edge in.
|
|
773
|
+
if edge - result[-1] > 0.1:
|
|
774
|
+
result = np.concatenate((result, (edge,)))
|
|
775
|
+
return result
|
|
776
|
+
|
|
777
|
+
@property
|
|
778
|
+
def minLambda(self) -> float:
|
|
779
|
+
'''
|
|
780
|
+
The minimum wavelength at which this transition contributes.
|
|
781
|
+
'''
|
|
782
|
+
return self.wavelengthGrid[0]
|
|
783
|
+
|
|
784
|
+
@dataclass(eq=False)
|
|
785
|
+
class HydrogenicContinuum(AtomicContinuum):
|
|
786
|
+
'''
|
|
787
|
+
Specific case of a Hydrogenic continuum, approximately falling off as
|
|
788
|
+
1/nu**3 towards higher frequencies (additional effects from Gaunt
|
|
789
|
+
factor).
|
|
790
|
+
|
|
791
|
+
Attributes
|
|
792
|
+
----------
|
|
793
|
+
NlambaGen : int
|
|
794
|
+
The number of points to generate for the wavelength grid.
|
|
795
|
+
alpha0 : float
|
|
796
|
+
The cross-section at the edge wavelength [m2].
|
|
797
|
+
minWavelength : float
|
|
798
|
+
The minimum wavelength below which this transition is assumed to no
|
|
799
|
+
longer contribute [nm].
|
|
800
|
+
'''
|
|
801
|
+
NlambdaGen: int
|
|
802
|
+
alpha0: float
|
|
803
|
+
minWavelength: float
|
|
804
|
+
|
|
805
|
+
def __repr__(self):
|
|
806
|
+
s = ('HydrogenicContinuum(j=%d, i=%d, NlambdaGen=%d, alpha0=%g,'
|
|
807
|
+
' minWavelength=%g)') % (self.j, self.i, self.NlambdaGen, self.alpha0,
|
|
808
|
+
self.minWavelength)
|
|
809
|
+
return s
|
|
810
|
+
|
|
811
|
+
def setup(self, atom):
|
|
812
|
+
super().setup(atom)
|
|
813
|
+
if self.minLambda >= self.lambda0:
|
|
814
|
+
raise ValueError(('Minimum wavelength is larger than continuum edge '
|
|
815
|
+
'at %g [nm] in continuum %s') % (self.lambda0, repr(self)))
|
|
816
|
+
|
|
817
|
+
def alpha(self, wavelength: np.ndarray) -> np.ndarray:
|
|
818
|
+
'''
|
|
819
|
+
Computes cross-section as a function of wavelength.
|
|
820
|
+
|
|
821
|
+
Parameters
|
|
822
|
+
----------
|
|
823
|
+
wavelength : np.ndarray
|
|
824
|
+
Wavelengths at which to compute the cross-section [nm].
|
|
825
|
+
|
|
826
|
+
Returns
|
|
827
|
+
-------
|
|
828
|
+
alpha : np.ndarray
|
|
829
|
+
Cross-section at associated wavelength.
|
|
830
|
+
'''
|
|
831
|
+
Z = self.jLevel.stage
|
|
832
|
+
nEff = Z * np.sqrt(Const.ERydberg / (self.jLevel.E_SI - self.iLevel.E_SI))
|
|
833
|
+
gbf0 = gaunt_bf(self.lambda0, nEff, Z)
|
|
834
|
+
gbf = gaunt_bf(wavelength, nEff, Z)
|
|
835
|
+
alpha = self.alpha0 * gbf / gbf0 * (wavelength / self.lambda0)**3
|
|
836
|
+
alpha[wavelength < self.minLambda] = 0.0
|
|
837
|
+
alpha[wavelength > self.lambdaEdge] = 0.0
|
|
838
|
+
return alpha
|
|
839
|
+
|
|
840
|
+
def wavelength(self) -> np.ndarray:
|
|
841
|
+
'''
|
|
842
|
+
Returns the wavelength grid at which this transition needs to be
|
|
843
|
+
computed to be correctly integrated.
|
|
844
|
+
'''
|
|
845
|
+
return np.linspace(self.minLambda, self.lambdaEdge, self.NlambdaGen)
|
|
846
|
+
|
|
847
|
+
@property
|
|
848
|
+
def minLambda(self) -> float:
|
|
849
|
+
'''
|
|
850
|
+
The minimum wavelength at which this transition contributes.
|
|
851
|
+
'''
|
|
852
|
+
return self.minWavelength
|