lightweaver 0.15.0__cp310-cp310-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-310-x86_64-linux-gnu.so +0 -0
  37. lightweaver/DefaultIterSchemes/SimdImpl_AVX512.cpython-310-x86_64-linux-gnu.so +0 -0
  38. lightweaver/DefaultIterSchemes/SimdImpl_SSE2.cpython-310-x86_64-linux-gnu.so +0 -0
  39. lightweaver/LwCompiled.cpython-310-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,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])