lightweaver 0.15.0__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.

Potentially problematic release.


This version of lightweaver might be problematic. Click here for more details.

Files changed (69) hide show
  1. lightweaver/Data/AbundancesAsplund09.pickle +0 -0
  2. lightweaver/Data/AtomicMassesNames.pickle +0 -0
  3. lightweaver/Data/Barklem_dfdata.dat +41 -0
  4. lightweaver/Data/Barklem_pddata.dat +40 -0
  5. lightweaver/Data/Barklem_spdata.dat +46 -0
  6. lightweaver/Data/DefaultMolecules/C2.molecule +27 -0
  7. lightweaver/Data/DefaultMolecules/CH/CH_X-A.asc +46409 -0
  8. lightweaver/Data/DefaultMolecules/CH/CH_X-A_12.asc +28322 -0
  9. lightweaver/Data/DefaultMolecules/CH/CH_X-B.asc +4272 -0
  10. lightweaver/Data/DefaultMolecules/CH/CH_X-B_12.asc +2583 -0
  11. lightweaver/Data/DefaultMolecules/CH/CH_X-C.asc +20916 -0
  12. lightweaver/Data/DefaultMolecules/CH/CH_X-C_12.asc +13106 -0
  13. lightweaver/Data/DefaultMolecules/CH.molecule +35 -0
  14. lightweaver/Data/DefaultMolecules/CN.molecule +30 -0
  15. lightweaver/Data/DefaultMolecules/CO/vmax=3_Jmax=49_dv=1_26 +296 -0
  16. lightweaver/Data/DefaultMolecules/CO/vmax=9_Jmax=120_dv=1_26 +2162 -0
  17. lightweaver/Data/DefaultMolecules/CO.molecule +30 -0
  18. lightweaver/Data/DefaultMolecules/CO_NLTE.molecule +29 -0
  19. lightweaver/Data/DefaultMolecules/CaH.molecule +29 -0
  20. lightweaver/Data/DefaultMolecules/H2+.molecule +27 -0
  21. lightweaver/Data/DefaultMolecules/H2.molecule +27 -0
  22. lightweaver/Data/DefaultMolecules/H2O.molecule +27 -0
  23. lightweaver/Data/DefaultMolecules/HF.molecule +29 -0
  24. lightweaver/Data/DefaultMolecules/LiH.molecule +27 -0
  25. lightweaver/Data/DefaultMolecules/MgH.molecule +34 -0
  26. lightweaver/Data/DefaultMolecules/N2.molecule +28 -0
  27. lightweaver/Data/DefaultMolecules/NH.molecule +27 -0
  28. lightweaver/Data/DefaultMolecules/NO.molecule +27 -0
  29. lightweaver/Data/DefaultMolecules/O2.molecule +27 -0
  30. lightweaver/Data/DefaultMolecules/OH.molecule +27 -0
  31. lightweaver/Data/DefaultMolecules/SiO.molecule +26 -0
  32. lightweaver/Data/DefaultMolecules/TiO.molecule +30 -0
  33. lightweaver/Data/Quadratures.pickle +0 -0
  34. lightweaver/Data/pf_Kurucz.input +0 -0
  35. lightweaver/DefaultIterSchemes/.placeholder +0 -0
  36. lightweaver/DefaultIterSchemes/SimdImpl_AVX2FMA.cpython-312-x86_64-linux-gnu.so +0 -0
  37. lightweaver/DefaultIterSchemes/SimdImpl_AVX512.cpython-312-x86_64-linux-gnu.so +0 -0
  38. lightweaver/DefaultIterSchemes/SimdImpl_SSE2.cpython-312-x86_64-linux-gnu.so +0 -0
  39. lightweaver/LwCompiled.cpython-312-x86_64-linux-gnu.so +0 -0
  40. lightweaver/__init__.py +33 -0
  41. lightweaver/atmosphere.py +1640 -0
  42. lightweaver/atomic_model.py +852 -0
  43. lightweaver/atomic_set.py +1286 -0
  44. lightweaver/atomic_table.py +653 -0
  45. lightweaver/barklem.py +151 -0
  46. lightweaver/benchmark.py +113 -0
  47. lightweaver/broadening.py +605 -0
  48. lightweaver/collisional_rates.py +337 -0
  49. lightweaver/config.py +106 -0
  50. lightweaver/constants.py +22 -0
  51. lightweaver/crtaf.py +197 -0
  52. lightweaver/fal.py +440 -0
  53. lightweaver/iterate_ctx.py +241 -0
  54. lightweaver/iteration_update.py +134 -0
  55. lightweaver/libenkiTS.so +0 -0
  56. lightweaver/molecule.py +225 -0
  57. lightweaver/multi.py +113 -0
  58. lightweaver/nr_update.py +106 -0
  59. lightweaver/rh_atoms.py +19743 -0
  60. lightweaver/simd_management.py +42 -0
  61. lightweaver/utils.py +504 -0
  62. lightweaver/version.py +34 -0
  63. lightweaver/wittmann.py +1375 -0
  64. lightweaver/zeeman.py +157 -0
  65. lightweaver-0.15.0.dist-info/METADATA +81 -0
  66. lightweaver-0.15.0.dist-info/RECORD +69 -0
  67. lightweaver-0.15.0.dist-info/WHEEL +6 -0
  68. lightweaver-0.15.0.dist-info/licenses/LICENSE +21 -0
  69. lightweaver-0.15.0.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