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.
- 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 +1640 -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 +504 -0
- lightweaver/version.py +34 -0
- lightweaver/wittmann.py +1375 -0
- lightweaver/zeeman.py +157 -0
- lightweaver-0.15.0.dist-info/METADATA +81 -0
- lightweaver-0.15.0.dist-info/RECORD +69 -0
- lightweaver-0.15.0.dist-info/WHEEL +6 -0
- lightweaver-0.15.0.dist-info/licenses/LICENSE +21 -0
- lightweaver-0.15.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import TYPE_CHECKING, List, Tuple, Union
|
|
4
|
+
try:
|
|
5
|
+
from xdrlib import Unpacker
|
|
6
|
+
except ImportError:
|
|
7
|
+
from mda_xdrlib.xdrlib import Unpacker
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
import lightweaver.constants as Const
|
|
12
|
+
from .utils import get_data_path
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .atmosphere import Atmosphere
|
|
16
|
+
|
|
17
|
+
class Element:
|
|
18
|
+
'''
|
|
19
|
+
A simple value comparable description of an element (just proton number
|
|
20
|
+
Z), that can be quickly and easily compared, whilst allowing access to
|
|
21
|
+
things from the periodic table.
|
|
22
|
+
'''
|
|
23
|
+
def __init__(self, Z: int):
|
|
24
|
+
self.Z = Z
|
|
25
|
+
|
|
26
|
+
def __hash__(self):
|
|
27
|
+
return hash(self.Z)
|
|
28
|
+
|
|
29
|
+
def __eq__(self, other):
|
|
30
|
+
return self.Z == other.Z
|
|
31
|
+
|
|
32
|
+
def __lt__(self, other):
|
|
33
|
+
if isinstance(other, Isotope):
|
|
34
|
+
return self.Z <= other.Z
|
|
35
|
+
return self.Z < other.Z
|
|
36
|
+
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
return 'Element(Z=%d)' % self.Z
|
|
39
|
+
|
|
40
|
+
def __str__(self):
|
|
41
|
+
return '%s(Z=%d)' % (self.name, self.Z)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def mass(self):
|
|
45
|
+
'''
|
|
46
|
+
Returns the mass of the element in AMU.
|
|
47
|
+
'''
|
|
48
|
+
return PeriodicTable.massData[self.Z]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def name(self):
|
|
52
|
+
'''
|
|
53
|
+
Returns the name of the element as a string.
|
|
54
|
+
'''
|
|
55
|
+
return PeriodicTable.nameMapping[self.Z]
|
|
56
|
+
|
|
57
|
+
class Isotope(Element):
|
|
58
|
+
'''
|
|
59
|
+
A simple value comparable isotope description, inheriting from Element.
|
|
60
|
+
'''
|
|
61
|
+
def __init__(self, N, Z):
|
|
62
|
+
super().__init__(Z)
|
|
63
|
+
self.N = N
|
|
64
|
+
|
|
65
|
+
def __hash__(self):
|
|
66
|
+
return hash((self.N, self.Z))
|
|
67
|
+
|
|
68
|
+
def __eq__(self, other):
|
|
69
|
+
if type(other) is Isotope:
|
|
70
|
+
return self.N == other.N and self.Z == other.Z
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def __lt__(self, other):
|
|
74
|
+
if isinstance(other, Isotope):
|
|
75
|
+
return (self.Z, self.N) < (other.Z, other.N)
|
|
76
|
+
elif type(other) is Element:
|
|
77
|
+
return self.Z < other.Z
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def __repr__(self):
|
|
81
|
+
return 'Isotope(N=%d, Z=%d)' % (self.N, self.Z)
|
|
82
|
+
|
|
83
|
+
def __str__(self):
|
|
84
|
+
return '%s(N=%d, Z=%d)' % (self.element_name, self.N, self.Z)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def mass(self):
|
|
88
|
+
'''
|
|
89
|
+
Returns the mass of the isotope in AMU.
|
|
90
|
+
'''
|
|
91
|
+
return PeriodicTable.massData[(self.N, self.Z)]
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def name(self):
|
|
95
|
+
'''
|
|
96
|
+
Returns the name of the isotope as a string.
|
|
97
|
+
'''
|
|
98
|
+
# NOTE(cmo): Handle H isotopes
|
|
99
|
+
if self.Z == 1 and self.N != 1:
|
|
100
|
+
return PeriodicTable.nameMapping[(self.N, self.Z)]
|
|
101
|
+
|
|
102
|
+
baseName = self.element_name
|
|
103
|
+
return '^%d_%s' % (self.N, baseName)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def element(self):
|
|
107
|
+
'''
|
|
108
|
+
Returns the underlying Element of which this isotope is a family
|
|
109
|
+
member.
|
|
110
|
+
'''
|
|
111
|
+
return PeriodicTable[self.Z]
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def element_mass(self):
|
|
115
|
+
'''
|
|
116
|
+
Returns the average mass of the element.
|
|
117
|
+
'''
|
|
118
|
+
return super().mass
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def element_name(self):
|
|
122
|
+
'''
|
|
123
|
+
Returns the name of the Element as a string.
|
|
124
|
+
'''
|
|
125
|
+
return super().name
|
|
126
|
+
|
|
127
|
+
def load_periodic_table_data():
|
|
128
|
+
'''
|
|
129
|
+
Internal use function to load data from the AtomicMassesNames.pickle data
|
|
130
|
+
file.
|
|
131
|
+
'''
|
|
132
|
+
path = get_data_path() + 'AtomicMassesNames.pickle'
|
|
133
|
+
with open(path, 'rb') as pkl:
|
|
134
|
+
massData, nameMapping = pickle.load(pkl)
|
|
135
|
+
|
|
136
|
+
# NOTE(cmo): Manually change names to D and T for H isotopes
|
|
137
|
+
nameMapping[(2, 1)] = 'D'
|
|
138
|
+
nameMapping['D'] = (2, 1)
|
|
139
|
+
nameMapping[(3, 1)] = 'T'
|
|
140
|
+
nameMapping['T'] = (3, 1)
|
|
141
|
+
|
|
142
|
+
# NOTE(cmo): Compute list of isotopes for each eleemnt.
|
|
143
|
+
# This can definitely be done more efficiently, but it probably doesn't
|
|
144
|
+
# matter. (~5 ms, done once, on startup, from timeit)
|
|
145
|
+
isotopes = {}
|
|
146
|
+
elements = {}
|
|
147
|
+
for target in massData:
|
|
148
|
+
if isinstance(target, tuple):
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
element = Element(Z=target)
|
|
152
|
+
elements[target] = element
|
|
153
|
+
isotopes[element] = []
|
|
154
|
+
for key in massData:
|
|
155
|
+
if isinstance(key, int):
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
if key[1] == target:
|
|
159
|
+
N, Z = key[0], element.Z
|
|
160
|
+
iso = Isotope(N=N, Z=Z)
|
|
161
|
+
isotopes[element].append(iso)
|
|
162
|
+
elements[(N, Z)] = iso
|
|
163
|
+
return massData, nameMapping, isotopes, elements
|
|
164
|
+
|
|
165
|
+
def normalise_atom_name(n: str) -> str:
|
|
166
|
+
'''
|
|
167
|
+
Normalises Element names to be two characters long with an uppercase
|
|
168
|
+
first letter, and lower case second, or a space in the case of single
|
|
169
|
+
character names.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
n : str
|
|
174
|
+
The name to normalise.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
result : str
|
|
179
|
+
The normalised name.
|
|
180
|
+
'''
|
|
181
|
+
strlen = len(n)
|
|
182
|
+
if strlen > 2 or strlen == 0:
|
|
183
|
+
raise ValueError('%s does not represent valid Element name' % n)
|
|
184
|
+
elif strlen == 1:
|
|
185
|
+
return n[0].upper()
|
|
186
|
+
else:
|
|
187
|
+
return n[0].upper() + n[1].lower()
|
|
188
|
+
|
|
189
|
+
class PeriodicTableData:
|
|
190
|
+
'''
|
|
191
|
+
Container and accessor for the periodic table data. Not intended to be
|
|
192
|
+
instantiated by users, instead use the pre-instantiated PeriodicTable
|
|
193
|
+
instance.
|
|
194
|
+
'''
|
|
195
|
+
massData, nameMapping, isos, elems = load_periodic_table_data()
|
|
196
|
+
|
|
197
|
+
def __getitem__(self, x: Union[str, int, Tuple[int, int], Element]) -> Element:
|
|
198
|
+
'''
|
|
199
|
+
Allows access to the associated Element or Isotope via a variety of means.
|
|
200
|
+
If input is
|
|
201
|
+
- an Element or Isotope, then this function returns it.
|
|
202
|
+
- an int, then this function returns the element with associated
|
|
203
|
+
proton number.
|
|
204
|
+
- a tuple of ints, then this function returns the element with
|
|
205
|
+
associated (Z, N)
|
|
206
|
+
- a str starting with '^' then the str is parsed as an isotope in
|
|
207
|
+
the form ^N_AtomName' and the associated Isotope is returned.
|
|
208
|
+
- any other str, then the str is parsed as a one or two character
|
|
209
|
+
atom identifier (e.g. H or Ca) and the associated Element is
|
|
210
|
+
returned.
|
|
211
|
+
'''
|
|
212
|
+
if isinstance(x, Element):
|
|
213
|
+
return x
|
|
214
|
+
|
|
215
|
+
if isinstance(x, int):
|
|
216
|
+
try:
|
|
217
|
+
return self.elems[x]
|
|
218
|
+
except KeyError:
|
|
219
|
+
raise KeyError('Unable to find Element with Z=%d' % x)
|
|
220
|
+
|
|
221
|
+
if isinstance(x, tuple) and all(isinstance(y, int) for y in x):
|
|
222
|
+
try:
|
|
223
|
+
return self.elems[x]
|
|
224
|
+
except KeyError:
|
|
225
|
+
raise KeyError('Unable to find Isotope with (N=%d, Z=%d)' % x)
|
|
226
|
+
|
|
227
|
+
if isinstance(x, str):
|
|
228
|
+
x = x.strip()
|
|
229
|
+
if x.startswith('^'):
|
|
230
|
+
terms = x[1:].split('_')
|
|
231
|
+
if len(terms) != 2:
|
|
232
|
+
raise KeyError('Unable to parse Isotope string %s' % x)
|
|
233
|
+
N = int(terms[0])
|
|
234
|
+
name = normalise_atom_name(terms[1])
|
|
235
|
+
try:
|
|
236
|
+
Z = self.nameMapping[name]
|
|
237
|
+
return self.elems[(N, Z)]
|
|
238
|
+
except KeyError:
|
|
239
|
+
raise KeyError('Unable to find Isotope from string %s' % x)
|
|
240
|
+
else:
|
|
241
|
+
name = normalise_atom_name(x)
|
|
242
|
+
try:
|
|
243
|
+
Z = self.nameMapping[name]
|
|
244
|
+
return self.elems[Z]
|
|
245
|
+
except KeyError:
|
|
246
|
+
raise KeyError('Unable to find Element with name %s' % name)
|
|
247
|
+
|
|
248
|
+
raise KeyError('Cannot find element from %s' % repr(x))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def get_isotopes(cls, e: Element) -> List[Isotope]:
|
|
253
|
+
'''
|
|
254
|
+
Get all isotopes associated with a certain Element.
|
|
255
|
+
'''
|
|
256
|
+
if not isinstance(e, Element):
|
|
257
|
+
raise ValueError('Requires Element as first argument, got %s' % repr(e))
|
|
258
|
+
|
|
259
|
+
if isinstance(e, Isotope):
|
|
260
|
+
return cls.isos[e.element]
|
|
261
|
+
|
|
262
|
+
return cls.isos[e]
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def elements(self):
|
|
266
|
+
'''
|
|
267
|
+
Return a sorted list of Elements by proton number Z.
|
|
268
|
+
'''
|
|
269
|
+
return sorted([e for _, e in self.elems.items() if type(e) is Element])
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def isotopes(self):
|
|
273
|
+
'''
|
|
274
|
+
Return a sorted list of Isotopes by proton number Z.
|
|
275
|
+
'''
|
|
276
|
+
return sorted([e for _, e in self.elems.items() if type(e) is Isotope])
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def nuclides(self):
|
|
280
|
+
'''
|
|
281
|
+
Return a list of all nuclides (Elements and Isotopes).
|
|
282
|
+
'''
|
|
283
|
+
return self.elements + self.isotopes
|
|
284
|
+
|
|
285
|
+
PeriodicTable = PeriodicTableData()
|
|
286
|
+
|
|
287
|
+
class AtomicAbundance:
|
|
288
|
+
'''
|
|
289
|
+
Container and accessor for atomic abundance data. This can be
|
|
290
|
+
instantiated with a subset of atomic abundances, which will be used in
|
|
291
|
+
conjunction with the the default values for non-specified elements.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
abundanceData : dict, optional
|
|
296
|
+
Contains the abundance data to override. For elements this should be
|
|
297
|
+
dict[Element] = abundance, and for isotopes dict[Isotope] = isotope
|
|
298
|
+
fraction.
|
|
299
|
+
abundDex : bool, optional
|
|
300
|
+
Whether the supplied abundance is in dex (with Hydrogen abundance of
|
|
301
|
+
12.0) or in relative Hydrogen abundance (default: True i.e. in dex).
|
|
302
|
+
metallicity : float, optional
|
|
303
|
+
Enhance the metallic abundance by a factor of 10**metallicity,
|
|
304
|
+
(default: 0.0).
|
|
305
|
+
'''
|
|
306
|
+
def __init__(self, abundanceData: dict=None, abundDex=True, metallicity: float=0.0):
|
|
307
|
+
self.abundance = self.load_default_abundance_data()
|
|
308
|
+
# NOTE(cmo): Default abundances always in dex
|
|
309
|
+
self.dex_to_decimal(self.abundance)
|
|
310
|
+
|
|
311
|
+
if abundanceData is not None:
|
|
312
|
+
if abundDex:
|
|
313
|
+
self.dex_to_decimal(abundanceData)
|
|
314
|
+
self.abundance.update(abundanceData)
|
|
315
|
+
|
|
316
|
+
self.metallicity = metallicity
|
|
317
|
+
if metallicity != 0.0:
|
|
318
|
+
self.apply_metallicity(self.abundance, metallicity)
|
|
319
|
+
|
|
320
|
+
self.isotopeProportions = {iso: v for iso, v in self.abundance.items()
|
|
321
|
+
if type(iso) is Isotope}
|
|
322
|
+
|
|
323
|
+
self.convert_isotopes_to_abundances()
|
|
324
|
+
self.compute_stats()
|
|
325
|
+
|
|
326
|
+
def convert_isotopes_to_abundances(self):
|
|
327
|
+
'''
|
|
328
|
+
Converts the isotope fractions to relative Hydrogen abundance.
|
|
329
|
+
'''
|
|
330
|
+
for e in PeriodicTable.elements:
|
|
331
|
+
totalProp = 0.0
|
|
332
|
+
isos = PeriodicTable.get_isotopes(e)
|
|
333
|
+
for iso in isos:
|
|
334
|
+
totalProp += self.abundance[iso]
|
|
335
|
+
for iso in isos:
|
|
336
|
+
if totalProp != 0.0:
|
|
337
|
+
self.abundance[iso] /= totalProp
|
|
338
|
+
self.abundance[iso] *= self.abundance[e]
|
|
339
|
+
|
|
340
|
+
def compute_stats(self):
|
|
341
|
+
'''
|
|
342
|
+
Compute the total abundance (totalAbundance), mass per H atom
|
|
343
|
+
(massPerH), and average mass per Element (avgMass).
|
|
344
|
+
'''
|
|
345
|
+
totalAbund = 0.0
|
|
346
|
+
avgMass = 0.0
|
|
347
|
+
for e in PeriodicTable.elements:
|
|
348
|
+
totalAbund += self.abundance[e]
|
|
349
|
+
avgMass += self.abundance[e] * e.mass
|
|
350
|
+
|
|
351
|
+
self.totalAbundance = totalAbund
|
|
352
|
+
self.massPerH = avgMass
|
|
353
|
+
self.avgMass = avgMass / totalAbund
|
|
354
|
+
|
|
355
|
+
def __getitem__(self, x: Union[str, int, Tuple[int, int], Element]) -> float:
|
|
356
|
+
'''
|
|
357
|
+
Returns the abundance of the requested Element or Isotope. All forms
|
|
358
|
+
of describing these are accepted as the PeriodicTable is invoked.
|
|
359
|
+
'''
|
|
360
|
+
return self.abundance[PeriodicTable[x]]
|
|
361
|
+
|
|
362
|
+
def get_primary_isotope(self, x: Element) -> Isotope:
|
|
363
|
+
'''
|
|
364
|
+
Returns the Isotope with the highest abundance of a particular
|
|
365
|
+
Element.
|
|
366
|
+
'''
|
|
367
|
+
isos = PeriodicTable.get_isotopes(x)
|
|
368
|
+
maxIso = isos[0]
|
|
369
|
+
maxAbund = self[maxIso]
|
|
370
|
+
for iso in isos[1:]:
|
|
371
|
+
if (abund := self[iso]) > maxAbund:
|
|
372
|
+
maxAbund = abund
|
|
373
|
+
maxIso = iso
|
|
374
|
+
return iso
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def dex_to_decimal(abunds):
|
|
378
|
+
'''
|
|
379
|
+
Used to convert from absolute abundance in dex to relative fractional
|
|
380
|
+
abundance.
|
|
381
|
+
'''
|
|
382
|
+
for e, v in abunds.items():
|
|
383
|
+
if type(e) is Element:
|
|
384
|
+
abunds[e] = 10**(v - 12.0)
|
|
385
|
+
|
|
386
|
+
@staticmethod
|
|
387
|
+
def apply_metallicity(abunds, metallicity):
|
|
388
|
+
'''
|
|
389
|
+
Used to adjust the metallicity of the abundances, by a fraction
|
|
390
|
+
10**metallicity.
|
|
391
|
+
'''
|
|
392
|
+
m = 10**metallicity
|
|
393
|
+
for e, v in abunds.items():
|
|
394
|
+
if type(e) is Element and e.Z > 2:
|
|
395
|
+
abunds[e] *= m
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def load_default_abundance_data() -> dict:
|
|
399
|
+
"""
|
|
400
|
+
Load the default abundances and convert to required format for
|
|
401
|
+
AtomicAbundance class (i.e. dict where dict[Element] = abundance, and
|
|
402
|
+
dict[Isotope] = isotope fraction).
|
|
403
|
+
"""
|
|
404
|
+
with open(get_data_path() + 'AbundancesAsplund09.pickle', 'rb') as pkl:
|
|
405
|
+
abundances = pickle.load(pkl)
|
|
406
|
+
|
|
407
|
+
lwAbundances = {}
|
|
408
|
+
for ele in abundances:
|
|
409
|
+
Z = ele['elem']['elem']['Z']
|
|
410
|
+
abund = ele['elem']['abundance']
|
|
411
|
+
lwAbundances[PeriodicTable[Z]] = abund
|
|
412
|
+
|
|
413
|
+
for iso in ele['isotopes']:
|
|
414
|
+
N = iso['N']
|
|
415
|
+
prop = iso['proportion']
|
|
416
|
+
lwAbundances[PeriodicTable[(N, Z)]] = prop
|
|
417
|
+
|
|
418
|
+
for e in PeriodicTable.nuclides:
|
|
419
|
+
if e not in lwAbundances:
|
|
420
|
+
lwAbundances[e] = 0.0
|
|
421
|
+
|
|
422
|
+
return lwAbundances
|
|
423
|
+
|
|
424
|
+
DefaultAtomicAbundance = AtomicAbundance()
|
|
425
|
+
|
|
426
|
+
@dataclass
|
|
427
|
+
class KuruczPf:
|
|
428
|
+
'''
|
|
429
|
+
Storage and functions relating to Bob Kurucz's partition functions.
|
|
430
|
+
Based on the data used in RH.
|
|
431
|
+
|
|
432
|
+
Attributes
|
|
433
|
+
----------
|
|
434
|
+
element : Element
|
|
435
|
+
The element associated with this partition funciton.
|
|
436
|
+
abundance : float
|
|
437
|
+
The abundance of this element.
|
|
438
|
+
Tpf : np.ndarray
|
|
439
|
+
The temperature grid on which the partition function is defined.
|
|
440
|
+
pf : np.ndarray
|
|
441
|
+
The partition function data.
|
|
442
|
+
ionPot : np.ndarray
|
|
443
|
+
The ionisation potential of each level.
|
|
444
|
+
'''
|
|
445
|
+
element: Element
|
|
446
|
+
abundance: float
|
|
447
|
+
Tpf: np.ndarray
|
|
448
|
+
pf: np.ndarray
|
|
449
|
+
ionPot: np.ndarray
|
|
450
|
+
|
|
451
|
+
def lte_ionisation(self, atmos: 'Atmosphere') -> np.ndarray:
|
|
452
|
+
'''
|
|
453
|
+
Compute the population of the species in each ionisation
|
|
454
|
+
stage in a given atmosphere.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
atmos : Atmosphere
|
|
459
|
+
The atmosphere in which to compute the populations.
|
|
460
|
+
|
|
461
|
+
Returns
|
|
462
|
+
-------
|
|
463
|
+
pops : np.ndarray
|
|
464
|
+
The LTE ionisation populations [Nstage x Nspace].
|
|
465
|
+
'''
|
|
466
|
+
Nstage = self.ionPot.shape[0]
|
|
467
|
+
Nspace = atmos.Nspace
|
|
468
|
+
|
|
469
|
+
C1 = (Const.HPlanck / (2.0 * np.pi * Const.MElectron)) * Const.HPlanck / Const.KBoltzmann
|
|
470
|
+
|
|
471
|
+
CtNe = 2.0 * (C1/atmos.temperature)**(-1.5) / atmos.ne
|
|
472
|
+
total = np.ones(Nspace)
|
|
473
|
+
pops = np.zeros((Nstage, Nspace))
|
|
474
|
+
pops[0, :] = 1.0
|
|
475
|
+
|
|
476
|
+
Uk = np.interp(atmos.temperature, self.Tpf, self.pf[0, :])
|
|
477
|
+
|
|
478
|
+
for i in range(1, Nstage):
|
|
479
|
+
Ukp1 = np.interp(atmos.temperature, self.Tpf, self.pf[i, :])
|
|
480
|
+
|
|
481
|
+
pops[i, :] = pops[i-1, :] * CtNe * np.exp(Ukp1 - Uk - self.ionPot[i-1] \
|
|
482
|
+
/ (Const.KBoltzmann * atmos.temperature))
|
|
483
|
+
total += pops[i]
|
|
484
|
+
|
|
485
|
+
Ukp1, Uk = Uk, Ukp1
|
|
486
|
+
|
|
487
|
+
pops[0, :] = self.abundance * atmos.nHTot / total
|
|
488
|
+
pops[1:,:] *= pops[0, :]
|
|
489
|
+
|
|
490
|
+
return pops
|
|
491
|
+
|
|
492
|
+
def fjk(self, atmos: 'Atmosphere', k: int) -> Tuple[np.ndarray, np.ndarray]:
|
|
493
|
+
'''
|
|
494
|
+
Compute the fractional population of the species in each ionisation
|
|
495
|
+
stage and partial derivative wrt n_e at one point in a given
|
|
496
|
+
atmosphere.
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
atmos : Atmosphere
|
|
501
|
+
The atmosphere in which to compute the populations.
|
|
502
|
+
k : int
|
|
503
|
+
The spatial index at which to compute the populations
|
|
504
|
+
|
|
505
|
+
Returns
|
|
506
|
+
-------
|
|
507
|
+
fj : np.ndarray
|
|
508
|
+
The fractional populations [Nstage].
|
|
509
|
+
dfj : np.ndarray
|
|
510
|
+
The derivatives of the fractional populations [Nstage].
|
|
511
|
+
'''
|
|
512
|
+
Nspace: int = atmos.Nspace
|
|
513
|
+
T: float = atmos.temperature[k]
|
|
514
|
+
ne: float = atmos.ne[k]
|
|
515
|
+
|
|
516
|
+
C1 = ((Const.HPlanck / (2.0 * np.pi * Const.MElectron))
|
|
517
|
+
* Const.HPlanck / Const.KBoltzmann)
|
|
518
|
+
|
|
519
|
+
CtNe = 2.0 * (C1/T)**(-1.5) / ne
|
|
520
|
+
Nstage: int = self.ionPot.shape[0]
|
|
521
|
+
fjk = np.zeros(Nstage)
|
|
522
|
+
fjk[0] = 1.0
|
|
523
|
+
dfjk = np.zeros(Nstage)
|
|
524
|
+
|
|
525
|
+
# fjk: fractional population of stage j, at atmospheric index k
|
|
526
|
+
# The first stage starts with a "population" of 1, then via Saha we
|
|
527
|
+
# compute the relative populations of the other stages, before dividing
|
|
528
|
+
# by the sum across these
|
|
529
|
+
|
|
530
|
+
Uk: float = np.interp(T, self.Tpf, self.pf[0, :])
|
|
531
|
+
|
|
532
|
+
for j in range(1, Nstage):
|
|
533
|
+
Ukp1: float = np.interp(T, self.Tpf, self.pf[j, :])
|
|
534
|
+
|
|
535
|
+
fjk[j] = fjk[j-1] * CtNe * np.exp(Ukp1 - Uk - self.ionPot[j-1]
|
|
536
|
+
/ (Const.KBoltzmann * T))
|
|
537
|
+
dfjk[j] = -j * fjk[j] / ne
|
|
538
|
+
|
|
539
|
+
Uk = Ukp1
|
|
540
|
+
|
|
541
|
+
sumF = np.sum(fjk)
|
|
542
|
+
sumDf = np.sum(dfjk)
|
|
543
|
+
fjk /= sumF
|
|
544
|
+
dfjk = (dfjk - fjk * sumDf) / sumF
|
|
545
|
+
return fjk, dfjk
|
|
546
|
+
|
|
547
|
+
def fj(self, atmos: 'Atmosphere') -> Tuple[np.ndarray, np.ndarray]:
|
|
548
|
+
'''
|
|
549
|
+
Compute the fractional population of the species in each ionisation
|
|
550
|
+
stage and partial derivative wrt n_e for each location in a given
|
|
551
|
+
atmosphere.
|
|
552
|
+
|
|
553
|
+
Parameters
|
|
554
|
+
----------
|
|
555
|
+
atmos : Atmosphere
|
|
556
|
+
The atmosphere in which to compute the populations.
|
|
557
|
+
|
|
558
|
+
Returns
|
|
559
|
+
-------
|
|
560
|
+
fj : np.ndarray
|
|
561
|
+
The fractional populations [Nstage x Nspace].
|
|
562
|
+
dfj : np.ndarray
|
|
563
|
+
The derivatives of the fractional populations [Nstage x Nspace].
|
|
564
|
+
'''
|
|
565
|
+
Nspace: int = atmos.Nspace
|
|
566
|
+
T = atmos.temperature
|
|
567
|
+
ne = atmos.ne
|
|
568
|
+
|
|
569
|
+
C1 = ((Const.HPlanck / (2.0 * np.pi * Const.MElectron))
|
|
570
|
+
* Const.HPlanck / Const.KBoltzmann)
|
|
571
|
+
|
|
572
|
+
CtNe = 2.0 * (C1/T)**(-1.5) / ne
|
|
573
|
+
Nstage: int = self.ionPot.shape[0]
|
|
574
|
+
fj = np.zeros((Nstage, Nspace))
|
|
575
|
+
fj[0, :] = 1.0
|
|
576
|
+
dfj = np.zeros((Nstage, Nspace))
|
|
577
|
+
|
|
578
|
+
# fjk: fractional population of stage j, at atmospheric index k
|
|
579
|
+
# The first stage starts with a "population" of 1, then via Saha we
|
|
580
|
+
# compute the relative populations of the other stages, before dividing
|
|
581
|
+
# by the sum across these
|
|
582
|
+
|
|
583
|
+
Uk = np.interp(T, self.Tpf, self.pf[0, :])
|
|
584
|
+
|
|
585
|
+
for j in range(1, Nstage):
|
|
586
|
+
Ukp1 = np.interp(T, self.Tpf, self.pf[j, 0])
|
|
587
|
+
|
|
588
|
+
fj[j] = fj[j-1] * CtNe * np.exp(Ukp1 - Uk - self.ionPot[j-1]
|
|
589
|
+
/ (Const.KBoltzmann * T))
|
|
590
|
+
dfj[j] = -j * fj[j] / ne
|
|
591
|
+
|
|
592
|
+
Uk[:] = Ukp1
|
|
593
|
+
|
|
594
|
+
sumF = np.sum(fj, axis=0)
|
|
595
|
+
sumDf = np.sum(dfj, axis=0)
|
|
596
|
+
fj /= sumF
|
|
597
|
+
dfj = (dfj - fj * sumDf) / sumF
|
|
598
|
+
return fj, dfj
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class KuruczPfTable:
|
|
602
|
+
'''
|
|
603
|
+
Container for all of the Kurucz partition function data, allowing
|
|
604
|
+
different paths and AtomicAbundances to be used. Serves to construct the
|
|
605
|
+
KuruczPf objects if used.
|
|
606
|
+
|
|
607
|
+
Parameters
|
|
608
|
+
----------
|
|
609
|
+
atomicAbundance : AtomicAbundance, optional
|
|
610
|
+
The abundance data to use, if non-standard.
|
|
611
|
+
kuruczPfPath : str
|
|
612
|
+
The path to the Kurucz parition function data in RH's XDR format, if
|
|
613
|
+
non-standard.
|
|
614
|
+
'''
|
|
615
|
+
def __init__(self, atomicAbundance: AtomicAbundance=None, kuruczPfPath: str=None):
|
|
616
|
+
if atomicAbundance is None:
|
|
617
|
+
atomicAbundance = DefaultAtomicAbundance
|
|
618
|
+
self.atomicAbundance = atomicAbundance
|
|
619
|
+
kuruczPfPath = get_data_path() + 'pf_Kurucz.input' if kuruczPfPath is None \
|
|
620
|
+
else kuruczPfPath
|
|
621
|
+
with open(kuruczPfPath, 'rb') as f:
|
|
622
|
+
s = f.read()
|
|
623
|
+
u = Unpacker(s)
|
|
624
|
+
|
|
625
|
+
# NOTE(cmo): Each of these terms is simply in flat lists indexed by Atomic Number Z-1
|
|
626
|
+
self.Tpf = np.array(u.unpack_array(u.unpack_double))
|
|
627
|
+
stages = []
|
|
628
|
+
pf = []
|
|
629
|
+
ionpot = []
|
|
630
|
+
for i in range(99):
|
|
631
|
+
z = u.unpack_int()
|
|
632
|
+
stages.append(u.unpack_int())
|
|
633
|
+
pf.append(np.array(u.unpack_farray(stages[-1] * self.Tpf.shape[0],
|
|
634
|
+
u.unpack_double)).reshape(stages[-1],
|
|
635
|
+
self.Tpf.shape[0]))
|
|
636
|
+
ionpot.append(np.array(u.unpack_farray(stages[-1], u.unpack_double)))
|
|
637
|
+
|
|
638
|
+
ionpot = [i * Const.HC_CM for i in ionpot]
|
|
639
|
+
pf = [np.log(p) for p in pf]
|
|
640
|
+
self.pf = pf
|
|
641
|
+
self.ionpot = ionpot
|
|
642
|
+
|
|
643
|
+
def __getitem__(self, x: Element) -> KuruczPf:
|
|
644
|
+
'''
|
|
645
|
+
Used to construct the partition function object for the requested
|
|
646
|
+
element.
|
|
647
|
+
'''
|
|
648
|
+
if type(x) is Isotope:
|
|
649
|
+
raise ValueError('Isotopes not supported by KuruczPf')
|
|
650
|
+
|
|
651
|
+
zm = x.Z - 1
|
|
652
|
+
return KuruczPf(element=x, abundance=self.atomicAbundance[x],
|
|
653
|
+
Tpf=self.Tpf, pf=self.pf[zm], ionPot=self.ionpot[zm])
|