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.
- 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-310-x86_64-linux-gnu.so +0 -0
- lightweaver/DefaultIterSchemes/SimdImpl_AVX512.cpython-310-x86_64-linux-gnu.so +0 -0
- lightweaver/DefaultIterSchemes/SimdImpl_SSE2.cpython-310-x86_64-linux-gnu.so +0 -0
- lightweaver/LwCompiled.cpython-310-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,1286 @@
|
|
|
1
|
+
from copy import copy
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Dict, Iterable, List, Optional, Set, Tuple, Union, cast
|
|
4
|
+
|
|
5
|
+
import astropy.units as u
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numba import njit
|
|
8
|
+
from scipy.linalg import solve
|
|
9
|
+
from scipy.optimize import newton_krylov
|
|
10
|
+
|
|
11
|
+
import lightweaver.constants as Const
|
|
12
|
+
from .atmosphere import Atmosphere
|
|
13
|
+
from .atomic_model import AtomicModel, LineType, element_sort
|
|
14
|
+
from .atomic_table import (AtomicAbundance, DefaultAtomicAbundance, Element,
|
|
15
|
+
KuruczPf, KuruczPfTable, PeriodicTable)
|
|
16
|
+
from .molecule import MolecularTable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@njit(cache=True)
|
|
20
|
+
def lte_pops_impl(temperature, ne, nTotal, stages, energies,
|
|
21
|
+
gs, nStar=None, debye=True, computeDiff=False):
|
|
22
|
+
Nlevel = stages.shape[0]
|
|
23
|
+
Nspace = ne.shape[0]
|
|
24
|
+
c1 = ((Const.HPlanck / (2.0 * np.pi * Const.MElectron))
|
|
25
|
+
* (Const.HPlanck / Const.KBoltzmann))
|
|
26
|
+
|
|
27
|
+
c2 = 0.0
|
|
28
|
+
nDebye = np.zeros(Nlevel)
|
|
29
|
+
if debye:
|
|
30
|
+
c2 = (np.sqrt(8.0 * np.pi / Const.KBoltzmann)
|
|
31
|
+
* (Const.QElectron**2 / (4.0 * np.pi * Const.Epsilon0))**1.5)
|
|
32
|
+
for i in range(1, Nlevel):
|
|
33
|
+
stage = stages[i]
|
|
34
|
+
Z = stage
|
|
35
|
+
for m in range(1, stage - stages[0] + 1):
|
|
36
|
+
nDebye[i] += Z
|
|
37
|
+
Z += 1
|
|
38
|
+
|
|
39
|
+
if nStar is None:
|
|
40
|
+
nStar = np.empty((Nlevel, Nspace))
|
|
41
|
+
|
|
42
|
+
if computeDiff:
|
|
43
|
+
prev = np.empty(Nlevel)
|
|
44
|
+
# NOTE(cmo): Will remain 0 and be returned as second return value if
|
|
45
|
+
# computeDiff is not set to True
|
|
46
|
+
maxDiff = 0.0
|
|
47
|
+
|
|
48
|
+
# NOTE(cmo): For some reason this is consistently faster with these hoisted
|
|
49
|
+
# from below, despite the allocations this causes
|
|
50
|
+
dE = energies - energies[0]
|
|
51
|
+
gi0 = gs / gs[0]
|
|
52
|
+
dZ = stages - stages[0]
|
|
53
|
+
|
|
54
|
+
for k in range(Nspace):
|
|
55
|
+
if debye:
|
|
56
|
+
dEion = c2 * np.sqrt(ne[k] / temperature[k])
|
|
57
|
+
else:
|
|
58
|
+
dEion = 0.0
|
|
59
|
+
cNe_T = 0.5 * ne[k] * (c1 / temperature[k])**1.5
|
|
60
|
+
total = 1.0
|
|
61
|
+
if computeDiff:
|
|
62
|
+
for i in range(Nlevel):
|
|
63
|
+
prev[i] = nStar[i, k]
|
|
64
|
+
for i in range(1, Nlevel):
|
|
65
|
+
dE_kT = (dE[i] - nDebye[i] * dEion) / (Const.KBoltzmann * temperature[k])
|
|
66
|
+
neFactor = cNe_T**dZ[i]
|
|
67
|
+
|
|
68
|
+
nst = gi0[i] * np.exp(-dE_kT)
|
|
69
|
+
nStar[i, k] = nst
|
|
70
|
+
nStar[i, k] /= neFactor
|
|
71
|
+
total += nStar[i, k]
|
|
72
|
+
nStar[0, k] = nTotal[k] / total
|
|
73
|
+
|
|
74
|
+
for i in range(1, Nlevel):
|
|
75
|
+
nStar[i, k] *= nStar[0, k]
|
|
76
|
+
|
|
77
|
+
if computeDiff:
|
|
78
|
+
for i in range(Nlevel):
|
|
79
|
+
maxDiff = max((nStar[i, k] - prev[i]) / nStar[i, k], maxDiff)
|
|
80
|
+
|
|
81
|
+
return nStar, maxDiff
|
|
82
|
+
|
|
83
|
+
def lte_pops(atomicModel: AtomicModel, temperature: np.ndarray,
|
|
84
|
+
ne: np.ndarray, nTotal: np.ndarray, nStar=None,
|
|
85
|
+
debye: bool=True) -> np.ndarray:
|
|
86
|
+
'''
|
|
87
|
+
Compute the LTE populations for a given atomic model under given
|
|
88
|
+
thermodynamic conditions.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
atomicModel : AtomicModel
|
|
93
|
+
The atomic model to consider.
|
|
94
|
+
temperature : np.ndarray
|
|
95
|
+
The temperature structure in the atmosphere.
|
|
96
|
+
ne : np.ndarray
|
|
97
|
+
The electron density in the atmosphere.
|
|
98
|
+
nTotal : np.ndarray
|
|
99
|
+
The total population of the species at each point in the atmosphere.
|
|
100
|
+
nStar : np.ndarray, optional
|
|
101
|
+
An optional array to store the result in.
|
|
102
|
+
debye : bool, optional
|
|
103
|
+
Whether to consider Debye shielding (default: True).1
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
ltePops : np.ndarray
|
|
108
|
+
The ltePops for the species.
|
|
109
|
+
'''
|
|
110
|
+
stages = np.array([l.stage for l in atomicModel.levels])
|
|
111
|
+
energies = np.array([l.E_SI for l in atomicModel.levels])
|
|
112
|
+
gs = np.array([l.g for l in atomicModel.levels])
|
|
113
|
+
return lte_pops_impl(temperature, ne, nTotal, stages,
|
|
114
|
+
energies, gs, nStar=nStar, debye=debye)[0]
|
|
115
|
+
|
|
116
|
+
def update_lte_pops_inplace(atomicModel: AtomicModel, temperature: np.ndarray,
|
|
117
|
+
ne: np.ndarray, nTotal: np.ndarray, nStar: np.ndarray,
|
|
118
|
+
debye: bool=True) -> Tuple[np.ndarray, float]:
|
|
119
|
+
stages = np.array([l.stage for l in atomicModel.levels])
|
|
120
|
+
energies = np.array([l.E_SI for l in atomicModel.levels])
|
|
121
|
+
gs = np.array([l.g for l in atomicModel.levels])
|
|
122
|
+
return lte_pops_impl(temperature, ne, nTotal, stages, energies, gs,
|
|
123
|
+
debye=debye, nStar=nStar, computeDiff=True)
|
|
124
|
+
|
|
125
|
+
class LteNeIterator:
|
|
126
|
+
def __init__(self, atoms: Iterable[AtomicModel], temperature: np.ndarray,
|
|
127
|
+
nHTot: np.ndarray, abundance: AtomicAbundance,
|
|
128
|
+
nlteStartingPops: Dict[Element, np.ndarray]):
|
|
129
|
+
sortedAtoms = sorted(atoms, key=element_sort)
|
|
130
|
+
self.nTotal = [abundance[a.element] * nHTot
|
|
131
|
+
for a in sortedAtoms]
|
|
132
|
+
self.stages = [np.array([l.stage for l in a.levels])
|
|
133
|
+
for a in sortedAtoms]
|
|
134
|
+
self.temperature = temperature
|
|
135
|
+
self.nHTot = nHTot
|
|
136
|
+
self.sortedAtoms = sortedAtoms
|
|
137
|
+
self.abundances = [abundance[a.element] for a in sortedAtoms]
|
|
138
|
+
self.nlteStartingPops = nlteStartingPops
|
|
139
|
+
|
|
140
|
+
def __call__(self, prevNeRatio: np.ndarray) -> np.ndarray:
|
|
141
|
+
atomicPops = []
|
|
142
|
+
ne = np.zeros_like(prevNeRatio)
|
|
143
|
+
prevNe = prevNeRatio * self.nHTot
|
|
144
|
+
|
|
145
|
+
for i, a in enumerate(self.sortedAtoms):
|
|
146
|
+
nStar = lte_pops(a, self.temperature, prevNe,
|
|
147
|
+
self.nTotal[i], debye=True)
|
|
148
|
+
atomicPops.append(AtomicState(model=a, abundance=self.abundances[i],
|
|
149
|
+
nStar=nStar, nTotal=self.nTotal[i]))
|
|
150
|
+
# NOTE(cmo): Take into account NLTE pops if provided
|
|
151
|
+
if a.element in self.nlteStartingPops:
|
|
152
|
+
if self.nlteStartingPops[a.element].shape != nStar.shape:
|
|
153
|
+
raise ValueError(('Starting populations provided for %s '
|
|
154
|
+
'do not match model.') % a.element)
|
|
155
|
+
nStar = self.nlteStartingPops[a.element]
|
|
156
|
+
|
|
157
|
+
ne += np.sum(nStar * self.stages[i][:, None], axis=0)
|
|
158
|
+
|
|
159
|
+
self.atomicPops = atomicPops
|
|
160
|
+
diff = (ne - prevNe) / self.nHTot
|
|
161
|
+
return diff
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class SpectrumConfiguration:
|
|
166
|
+
'''
|
|
167
|
+
Container for the configuration of common wavelength grid and species
|
|
168
|
+
active at each wavelength.
|
|
169
|
+
|
|
170
|
+
Attributes
|
|
171
|
+
----------
|
|
172
|
+
radSet : RadiativeSet
|
|
173
|
+
The set of atoms involved in the creation of this simulation.
|
|
174
|
+
wavelength : np.ndarray
|
|
175
|
+
The common wavelength array used for this simulation.
|
|
176
|
+
models : list of AtomicModel
|
|
177
|
+
The models for the active and detailed static atoms present in this
|
|
178
|
+
simulation.
|
|
179
|
+
transWavelengths : Dict[(Element, i, j), np.ndarray]
|
|
180
|
+
The local wavelength grid for each transition stored in a dictionary
|
|
181
|
+
by transition ID.
|
|
182
|
+
blueIdx : Dict[(Element, i, j), int]
|
|
183
|
+
The index at which each local grid starts in the global wavelength
|
|
184
|
+
array.
|
|
185
|
+
redIdx : Dict[(Element, i, j), int]
|
|
186
|
+
The index at which each local grid has ended in the global wavelength
|
|
187
|
+
array (exclusive,
|
|
188
|
+
i.e. transWavelength = globalWavelength[blueIdx:redIdx]).
|
|
189
|
+
activeTrans : Dict[(Element, i, j), bool]
|
|
190
|
+
Whether this transition is ever active (contributing in either an
|
|
191
|
+
active or detailed static sense) over the range of wavelength.
|
|
192
|
+
activeWavelengths : Dict[(Element, i, j), np.ndarray]
|
|
193
|
+
A mask of the wavelengths at which this transition is active.
|
|
194
|
+
|
|
195
|
+
Properties
|
|
196
|
+
----------
|
|
197
|
+
NprdTrans : int
|
|
198
|
+
The number of PRD transitions present on the active transitions.
|
|
199
|
+
'''
|
|
200
|
+
radSet: 'RadiativeSet'
|
|
201
|
+
wavelength: np.ndarray
|
|
202
|
+
models: List[AtomicModel]
|
|
203
|
+
transWavelengths: Dict[Tuple[Element, int, int], np.ndarray]
|
|
204
|
+
blueIdx: Dict[Tuple[Element, int, int], int]
|
|
205
|
+
redIdx: Dict[Tuple[Element, int, int], int]
|
|
206
|
+
activeTrans: Dict[Tuple[Element, int, int], bool]
|
|
207
|
+
activeWavelengths: Dict[Tuple[Element, int, int], np.ndarray]
|
|
208
|
+
|
|
209
|
+
def subset_configuration(self, wavelengths) -> 'SpectrumConfiguration':
|
|
210
|
+
'''
|
|
211
|
+
Computes a SpectrumConfiguration for a sub-region of the global wavelength array.
|
|
212
|
+
|
|
213
|
+
This is typically used for computing a final formal solution on a single
|
|
214
|
+
ray through the atmosphere. In this situation all lines are set to
|
|
215
|
+
contribute throughout the entire grid, to avoid situations where small
|
|
216
|
+
jumps in intensity occur from lines being cut off.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
wavelengths : np.ndarray
|
|
221
|
+
The grid on which to produce the new SpectrumConfiguration.
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
spectrumConfig : SpectrumConfiguration
|
|
226
|
+
The subset spectrum configuration.
|
|
227
|
+
'''
|
|
228
|
+
Nblue = np.searchsorted(self.wavelength, wavelengths[0])
|
|
229
|
+
Nred = min(np.searchsorted(self.wavelength, wavelengths[-1])+1,
|
|
230
|
+
self.wavelength.shape[0])
|
|
231
|
+
Nwavelengths = wavelengths.shape[0]
|
|
232
|
+
|
|
233
|
+
activeTrans = {k: bool(np.any(v[Nblue:Nred]))
|
|
234
|
+
for k, v in self.activeWavelengths.items()}
|
|
235
|
+
transGrids = {k: np.copy(wavelengths) for k, active in activeTrans.items()
|
|
236
|
+
if active}
|
|
237
|
+
activeWavelengths = {k: np.ones_like(wavelengths, dtype=bool)
|
|
238
|
+
for k in transGrids}
|
|
239
|
+
blueIdx = {k: 0 for k in transGrids}
|
|
240
|
+
redIdx = {k: Nwavelengths for k in transGrids}
|
|
241
|
+
|
|
242
|
+
def test_atom_active(atom: AtomicModel) -> bool:
|
|
243
|
+
for t in atom.transitions:
|
|
244
|
+
if activeTrans[t.transId]:
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
models = []
|
|
249
|
+
for atom in self.models:
|
|
250
|
+
if test_atom_active(atom):
|
|
251
|
+
models.append(atom)
|
|
252
|
+
|
|
253
|
+
return SpectrumConfiguration(radSet=self.radSet, wavelength=wavelengths,
|
|
254
|
+
models=models, transWavelengths=transGrids,
|
|
255
|
+
blueIdx=blueIdx, redIdx=redIdx,
|
|
256
|
+
activeTrans=activeTrans,
|
|
257
|
+
activeWavelengths=activeWavelengths)
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def NprdTrans(self):
|
|
261
|
+
try:
|
|
262
|
+
return self._NprdTrans
|
|
263
|
+
except AttributeError:
|
|
264
|
+
count = 0
|
|
265
|
+
for element in self.radSet.activeSet:
|
|
266
|
+
atom = self.radSet.atoms[element]
|
|
267
|
+
for l in atom.lines:
|
|
268
|
+
if l.type == LineType.PRD:
|
|
269
|
+
count += 1
|
|
270
|
+
self._NprdTrans = count
|
|
271
|
+
return count
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass
|
|
275
|
+
class AtomicState:
|
|
276
|
+
'''
|
|
277
|
+
Container for the state of an atomic model during a simulation.
|
|
278
|
+
|
|
279
|
+
This hold both the model, as well as the simulations properties such as
|
|
280
|
+
abundance, populations and radiative rates.
|
|
281
|
+
|
|
282
|
+
Attributes
|
|
283
|
+
----------
|
|
284
|
+
model : AtomicModel
|
|
285
|
+
The python model of the atom.
|
|
286
|
+
abundance : float
|
|
287
|
+
The abundance of the species as a fraction of H abundance.
|
|
288
|
+
nStar : np.ndarray
|
|
289
|
+
The LTE populations of the species.
|
|
290
|
+
nTotal : np.ndarray
|
|
291
|
+
The total species population at each point in the atmosphere.
|
|
292
|
+
detailed : bool
|
|
293
|
+
Whether the species has detailed populations.
|
|
294
|
+
pops : np.ndarray, optional
|
|
295
|
+
The NLTE populations for the species, if detailed is True.
|
|
296
|
+
radiativeRates: Dict[(int, int), np.ndarray], optional
|
|
297
|
+
If detailed the radiative rates for the species will be present here,
|
|
298
|
+
stored under (i, j) and (j, i) for each transition.
|
|
299
|
+
'''
|
|
300
|
+
model: AtomicModel
|
|
301
|
+
abundance: float
|
|
302
|
+
nStar: np.ndarray
|
|
303
|
+
nTotal: np.ndarray
|
|
304
|
+
detailed: bool = False
|
|
305
|
+
pops: Optional[np.ndarray] = None
|
|
306
|
+
radiativeRates: Optional[Dict[Tuple[int, int], np.ndarray]] = None
|
|
307
|
+
|
|
308
|
+
def __post_init__(self):
|
|
309
|
+
if self.detailed:
|
|
310
|
+
self.radiativeRates = {}
|
|
311
|
+
ratesShape = self.nStar.shape[1:]
|
|
312
|
+
for t in self.model.transitions:
|
|
313
|
+
self.radiativeRates[(t.i, t.j)] = np.zeros(ratesShape)
|
|
314
|
+
self.radiativeRates[(t.j, t.i)] = np.zeros(ratesShape)
|
|
315
|
+
|
|
316
|
+
def __str__(self):
|
|
317
|
+
s = 'AtomicState(%s)' % self.element
|
|
318
|
+
return s
|
|
319
|
+
|
|
320
|
+
def __hash__(self):
|
|
321
|
+
# return hash(repr(self))
|
|
322
|
+
raise NotImplementedError
|
|
323
|
+
|
|
324
|
+
def dimensioned_view(self, shape):
|
|
325
|
+
'''
|
|
326
|
+
Returns a view over the contents of AtomicState reshaped so all data
|
|
327
|
+
has the correct (1/2/3D) dimensionality for the atmospheric model, as
|
|
328
|
+
these are all stored under a flat scheme.
|
|
329
|
+
|
|
330
|
+
Parameters
|
|
331
|
+
----------
|
|
332
|
+
shape : tuple
|
|
333
|
+
The shape to reshape to, this can be obtained from
|
|
334
|
+
Atmosphere.structure.dimensioned_shape
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
state : AtomicState
|
|
339
|
+
An instance of self with the arrays reshaped to the appropriate
|
|
340
|
+
dimensionality.
|
|
341
|
+
'''
|
|
342
|
+
state = copy(self)
|
|
343
|
+
state.nStar = self.nStar.reshape(-1, *shape)
|
|
344
|
+
state.nTotal = self.nTotal.reshape(shape)
|
|
345
|
+
if self.pops is not None:
|
|
346
|
+
state.pops = self.pops.reshape(-1, *shape)
|
|
347
|
+
state.radiativeRates = {k: v.reshape(shape) for k, v in
|
|
348
|
+
self.radiativeRates.items()}
|
|
349
|
+
return state
|
|
350
|
+
|
|
351
|
+
def unit_view(self):
|
|
352
|
+
'''
|
|
353
|
+
Returns a view over the contents of the AtomicState with the correct
|
|
354
|
+
`astropy.units`.
|
|
355
|
+
'''
|
|
356
|
+
state = copy(self)
|
|
357
|
+
m3 = u.m**(-3)
|
|
358
|
+
state.nStar = self.nStar << m3
|
|
359
|
+
state.nTotal = self.nTotal << m3
|
|
360
|
+
if self.pops is not None:
|
|
361
|
+
state.pops = self.pops << m3
|
|
362
|
+
state.radiativeRates = {k: v << u.s**-1 for k, v in self.radiativeRates.items()}
|
|
363
|
+
return state
|
|
364
|
+
|
|
365
|
+
def dimensioned_unit_view(self, shape):
|
|
366
|
+
'''
|
|
367
|
+
Returns a view over the contents of AtomicState reshaped so all data
|
|
368
|
+
has the correct (1/2/3D) dimensionality for the atmospheric model,
|
|
369
|
+
and the correct `astropy.units`.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
shape : tuple
|
|
374
|
+
The shape to reshape to, this can be obtained from
|
|
375
|
+
Atmosphere.structure.dimensioned_shape
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
state : AtomicState
|
|
380
|
+
An instance of self with the arrays reshaped to the appropriate
|
|
381
|
+
dimensionality.
|
|
382
|
+
'''
|
|
383
|
+
state = self.dimensioned_view(shape)
|
|
384
|
+
return state.unit_view()
|
|
385
|
+
|
|
386
|
+
def update_nTotal(self, atmos: Atmosphere):
|
|
387
|
+
'''
|
|
388
|
+
Update nTotal assuming either the abundance or nHTot have changed.
|
|
389
|
+
'''
|
|
390
|
+
self.nTotal[:] = self.abundance * atmos.nHTot # type: ignore
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def element(self) -> Element:
|
|
394
|
+
'''
|
|
395
|
+
The element associated with this model.
|
|
396
|
+
'''
|
|
397
|
+
return self.model.element
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def mass(self) -> float:
|
|
401
|
+
'''
|
|
402
|
+
The mass of the element associated with this model.
|
|
403
|
+
'''
|
|
404
|
+
return self.element.mass
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def n(self) -> np.ndarray:
|
|
408
|
+
'''
|
|
409
|
+
The NLTE populations, if present, or the LTE populations.
|
|
410
|
+
'''
|
|
411
|
+
if self.pops is None:
|
|
412
|
+
return self.nStar
|
|
413
|
+
return self.pops
|
|
414
|
+
|
|
415
|
+
@n.setter
|
|
416
|
+
def n(self, val: np.ndarray):
|
|
417
|
+
if val.shape != self.nStar.shape:
|
|
418
|
+
raise ValueError(('Incorrect dimensions for population array, '
|
|
419
|
+
'expected %s') % self.nStar.shape)
|
|
420
|
+
|
|
421
|
+
self.pops = val
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def name(self) -> str:
|
|
425
|
+
'''
|
|
426
|
+
The name of the element associated with this model.
|
|
427
|
+
'''
|
|
428
|
+
return self.model.element.name
|
|
429
|
+
|
|
430
|
+
def fjk(self, atmos, k):
|
|
431
|
+
# Nstage: int = (self.model.levels[-1].stage - self.model.levels[0].stage) + 1
|
|
432
|
+
Nstage: int = self.model.levels[-1].stage + 1
|
|
433
|
+
|
|
434
|
+
fjk = np.zeros(Nstage)
|
|
435
|
+
# TODO(cmo): Proper derivative treatment
|
|
436
|
+
dfjk = np.zeros(Nstage)
|
|
437
|
+
|
|
438
|
+
for i, l in enumerate(self.model.levels):
|
|
439
|
+
fjk[l.stage] += self.n[i, k]
|
|
440
|
+
|
|
441
|
+
fjk /= self.nTotal[k]
|
|
442
|
+
|
|
443
|
+
return fjk, dfjk
|
|
444
|
+
|
|
445
|
+
def fj(self, atmos):
|
|
446
|
+
Nstage: int = self.model.levels[-1].stage + 1
|
|
447
|
+
Nspace: int = atmos.Nspace
|
|
448
|
+
|
|
449
|
+
fj = np.zeros((Nstage, Nspace))
|
|
450
|
+
# TODO(cmo): Proper derivative treatment
|
|
451
|
+
dfj = np.zeros((Nstage, Nspace))
|
|
452
|
+
|
|
453
|
+
for i, l in enumerate(self.model.levels):
|
|
454
|
+
fj[l.stage] += self.n[i]
|
|
455
|
+
|
|
456
|
+
fj /= self.nTotal
|
|
457
|
+
|
|
458
|
+
return fj, dfj
|
|
459
|
+
|
|
460
|
+
def set_n_to_lte(self):
|
|
461
|
+
'''
|
|
462
|
+
Reset the NLTE populations to LTE.
|
|
463
|
+
'''
|
|
464
|
+
if self.pops is not None:
|
|
465
|
+
self.pops[:] = self.nStar
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class AtomicStateTable:
|
|
469
|
+
'''
|
|
470
|
+
Container for AtomicStates.
|
|
471
|
+
|
|
472
|
+
The __getitem__ on this class is intended to be smart, and should work
|
|
473
|
+
correctly with ints, strings, or Elements and return the associated
|
|
474
|
+
AtomicState.
|
|
475
|
+
This object is not normally constructed directly by the user, but will
|
|
476
|
+
instead be interacted with as a means of transporting information to and
|
|
477
|
+
from the backend.
|
|
478
|
+
|
|
479
|
+
'''
|
|
480
|
+
def __init__(self, atoms: List[AtomicState]):
|
|
481
|
+
self.atoms = {a.element: a for a in atoms}
|
|
482
|
+
|
|
483
|
+
def __contains__(self, name: Union[int, Tuple[int, int], str, Element]) -> bool:
|
|
484
|
+
try:
|
|
485
|
+
x = PeriodicTable[name]
|
|
486
|
+
return x in self.atoms
|
|
487
|
+
except KeyError:
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
def __len__(self) -> int:
|
|
491
|
+
return len(self.atoms)
|
|
492
|
+
|
|
493
|
+
def __getitem__(self, name: Union[int, Tuple[int, int], str, Element]) -> AtomicState:
|
|
494
|
+
x = PeriodicTable[name]
|
|
495
|
+
return self.atoms[x]
|
|
496
|
+
|
|
497
|
+
def __iter__(self):
|
|
498
|
+
return iter(sorted(self.atoms.values(), key=element_sort))
|
|
499
|
+
|
|
500
|
+
def dimensioned_view(self, shape):
|
|
501
|
+
'''
|
|
502
|
+
Returns a view over the contents of AtomicStateTable reshaped so all data
|
|
503
|
+
has the correct (1/2/3D) dimensionality for the atmospheric model, as
|
|
504
|
+
these are all stored under a flat scheme.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
shape : tuple
|
|
509
|
+
The shape to reshape to, this can be obtained from
|
|
510
|
+
Atmosphere.structure.dimensioned_shape
|
|
511
|
+
|
|
512
|
+
Returns
|
|
513
|
+
-------
|
|
514
|
+
state : AtomicStateTable
|
|
515
|
+
An instance of self with the arrays reshaped to the appropriate
|
|
516
|
+
dimensionality.
|
|
517
|
+
'''
|
|
518
|
+
table = copy(self)
|
|
519
|
+
table.atoms = {k: a.dimensioned_view(shape) for k, a in self.atoms.items()}
|
|
520
|
+
return table
|
|
521
|
+
|
|
522
|
+
def unit_view(self):
|
|
523
|
+
'''
|
|
524
|
+
Returns a view over the contents of the AtomicStateTable with the correct
|
|
525
|
+
`astropy.units`.
|
|
526
|
+
'''
|
|
527
|
+
table = copy(self)
|
|
528
|
+
table.atoms = {k: a.unit_view() for k, a in self.atoms.items()}
|
|
529
|
+
return table
|
|
530
|
+
|
|
531
|
+
def dimensioned_unit_view(self, shape):
|
|
532
|
+
'''
|
|
533
|
+
Returns a view over the contents of AtomicStateTable reshaped so all data
|
|
534
|
+
has the correct (1/2/3D) dimensionality for the atmospheric model,
|
|
535
|
+
and the correct `astropy.units`.
|
|
536
|
+
|
|
537
|
+
Parameters
|
|
538
|
+
----------
|
|
539
|
+
shape : tuple
|
|
540
|
+
The shape to reshape to, this can be obtained from
|
|
541
|
+
Atmosphere.structure.dimensioned_shape
|
|
542
|
+
|
|
543
|
+
Returns
|
|
544
|
+
-------
|
|
545
|
+
state : AtomicStateTable
|
|
546
|
+
An instance of self with the arrays reshaped to the appropriate
|
|
547
|
+
dimensionality.
|
|
548
|
+
'''
|
|
549
|
+
table = self.dimensioned_view(shape)
|
|
550
|
+
return table.unit_view()
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@dataclass
|
|
554
|
+
class SpeciesStateTable:
|
|
555
|
+
'''
|
|
556
|
+
Container for the species populations in the simulation. Similar to
|
|
557
|
+
AtomicStateTable but also holding the molecular populations and the
|
|
558
|
+
atmosphere object.
|
|
559
|
+
|
|
560
|
+
The __getitem__ is intended to be smart, returning in order of priority
|
|
561
|
+
on the name match (int, str, Element), H- populations, molecular
|
|
562
|
+
populations, NLTE atomic populations, LTE atomic populations.
|
|
563
|
+
This object is not normally constructed directly by the user, but will
|
|
564
|
+
instead be interacted with as a means of transporting information to and
|
|
565
|
+
from the backend.
|
|
566
|
+
|
|
567
|
+
Attributes
|
|
568
|
+
----------
|
|
569
|
+
atmosphere : Atmosphere
|
|
570
|
+
The atmosphere object.
|
|
571
|
+
abundance : AtomicAbundance
|
|
572
|
+
The abundance of all species present in the atmosphere.
|
|
573
|
+
atomicPops : AtomicStateTable
|
|
574
|
+
The atomic populations state container.
|
|
575
|
+
molecularTable : MolecularTable
|
|
576
|
+
The molecules present in the simulation.
|
|
577
|
+
molecularPops : list of np.ndarray
|
|
578
|
+
The populations of each molecule in the molecularTable
|
|
579
|
+
HminPops : np.ndarray
|
|
580
|
+
H- ion populations throughout the atmosphere.
|
|
581
|
+
'''
|
|
582
|
+
atmosphere: Atmosphere
|
|
583
|
+
abundance: AtomicAbundance
|
|
584
|
+
atomicPops: AtomicStateTable
|
|
585
|
+
molecularTable: MolecularTable
|
|
586
|
+
molecularPops: List[np.ndarray]
|
|
587
|
+
HminPops: np.ndarray
|
|
588
|
+
|
|
589
|
+
def dimensioned_view(self):
|
|
590
|
+
'''
|
|
591
|
+
Returns a view over the contents of SpeciesStateTable reshaped so all data
|
|
592
|
+
has the correct (1/2/3D) dimensionality for the atmospheric model, as
|
|
593
|
+
these are all stored under a flat scheme.
|
|
594
|
+
'''
|
|
595
|
+
shape = self.atmosphere.structure.dimensioned_shape
|
|
596
|
+
table = copy(self)
|
|
597
|
+
table.atmosphere = self.atmosphere.dimensioned_view()
|
|
598
|
+
table.atomicPops = self.atomicPops.dimensioned_view(shape)
|
|
599
|
+
table.molecularPops = [m.reshape(shape) for m in self.molecularPops]
|
|
600
|
+
table.HminPops = self.HminPops.reshape(shape)
|
|
601
|
+
return table
|
|
602
|
+
|
|
603
|
+
def unit_view(self):
|
|
604
|
+
'''
|
|
605
|
+
Returns a view over the contents of the SpeciesStateTable with the correct
|
|
606
|
+
`astropy.units`.
|
|
607
|
+
'''
|
|
608
|
+
table = copy(self)
|
|
609
|
+
table.atmosphere = self.atmosphere.unit_view()
|
|
610
|
+
table.atomicPops = self.atomicPops.unit_view()
|
|
611
|
+
table.molecularPops = [(m << u.m**(-3)) for m in self.molecularPops]
|
|
612
|
+
table.HminPops = self.HminPops << u.m**(-3)
|
|
613
|
+
return table
|
|
614
|
+
|
|
615
|
+
def dimensioned_unit_view(self):
|
|
616
|
+
'''
|
|
617
|
+
Returns a view over the contents of SpeciesStateTable reshaped so all data
|
|
618
|
+
has the correct (1/2/3D) dimensionality for the atmospheric model,
|
|
619
|
+
and the correct `astropy.units`.
|
|
620
|
+
'''
|
|
621
|
+
table = self.dimensioned_view()
|
|
622
|
+
return table.unit_view()
|
|
623
|
+
|
|
624
|
+
def __getitem__(self, name: Union[int, Tuple[int, int], str, Element]) -> np.ndarray:
|
|
625
|
+
if isinstance(name, str) and name == 'H-':
|
|
626
|
+
return self.HminPops
|
|
627
|
+
|
|
628
|
+
if name in self.molecularTable:
|
|
629
|
+
name = cast(str, name)
|
|
630
|
+
key = self.molecularTable.indices[name]
|
|
631
|
+
return self.molecularPops[key]
|
|
632
|
+
|
|
633
|
+
if name in self.atomicPops:
|
|
634
|
+
return self.atomicPops[name].n
|
|
635
|
+
|
|
636
|
+
raise LookupError(f'Element defined by "{name}" not found.')
|
|
637
|
+
|
|
638
|
+
def __contains__(self, name: Union[int, Tuple[int, int], str, Element]) -> bool:
|
|
639
|
+
if name == 'H-':
|
|
640
|
+
return True
|
|
641
|
+
|
|
642
|
+
if name in self.molecularTable:
|
|
643
|
+
return True
|
|
644
|
+
|
|
645
|
+
if name in self.atomicPops:
|
|
646
|
+
return True
|
|
647
|
+
|
|
648
|
+
return False
|
|
649
|
+
|
|
650
|
+
def update_lte_atoms_Hmin_pops(self, atmos: Atmosphere, conserveCharge=False,
|
|
651
|
+
updateTotals=False, maxIter=2000, quiet=False, tol=1e-3):
|
|
652
|
+
'''
|
|
653
|
+
Under the assumption that the atmosphere has changed, update the LTE
|
|
654
|
+
atomic populations and the H- populations.
|
|
655
|
+
|
|
656
|
+
Parameters
|
|
657
|
+
----------
|
|
658
|
+
atmos : Atmosphere
|
|
659
|
+
The atmosphere object.
|
|
660
|
+
conserveCharge : bool
|
|
661
|
+
Whether to conserveCharge and adjust the electron density in
|
|
662
|
+
atmos based on the change in ionisation of the non-detailed
|
|
663
|
+
species (default: False).
|
|
664
|
+
updateTotals : bool, optional
|
|
665
|
+
Whether to update the totals of each species from the abundance
|
|
666
|
+
and total hydrogen density (default: False).
|
|
667
|
+
maxIter : int, optional
|
|
668
|
+
The maximum number of iterations to take looking for a stable
|
|
669
|
+
solution (default: 2000).
|
|
670
|
+
quiet : bool, optional
|
|
671
|
+
Whether to print information about the update (default: False)
|
|
672
|
+
tol : float, optional
|
|
673
|
+
The tolerance of relative change at which to consider the
|
|
674
|
+
populations converged (default: 1e-3)
|
|
675
|
+
'''
|
|
676
|
+
if updateTotals:
|
|
677
|
+
for atom in self.atomicPops:
|
|
678
|
+
atom.update_nTotal(atmos)
|
|
679
|
+
for i in range(maxIter):
|
|
680
|
+
maxDiff = 0.0
|
|
681
|
+
maxName = '--'
|
|
682
|
+
ne = np.zeros_like(atmos.ne)
|
|
683
|
+
diffs = [update_lte_pops_inplace(atom.model, atmos.temperature,
|
|
684
|
+
atmos.ne, atom.nTotal, atom.nStar,
|
|
685
|
+
debye=True)[1] for atom in self.atomicPops]
|
|
686
|
+
|
|
687
|
+
for j, atom in enumerate(self.atomicPops):
|
|
688
|
+
if conserveCharge:
|
|
689
|
+
stages = np.array([l.stage for l in atom.model.levels])
|
|
690
|
+
if atom.pops is None:
|
|
691
|
+
ne += np.sum(atom.nStar * stages[:, None], axis=0)
|
|
692
|
+
else:
|
|
693
|
+
ne += np.sum(atom.n * stages[:, None], axis=0)
|
|
694
|
+
|
|
695
|
+
diff = diffs[j]
|
|
696
|
+
if diff > maxDiff:
|
|
697
|
+
maxDiff = diff
|
|
698
|
+
maxName = atom.name
|
|
699
|
+
if conserveCharge:
|
|
700
|
+
ne[ne < 1e6] = 1e6
|
|
701
|
+
atmos.ne[:] = ne
|
|
702
|
+
if maxDiff < tol:
|
|
703
|
+
if not quiet:
|
|
704
|
+
print('LTE Iterations %d (%s slowest convergence)' % (i+1, maxName))
|
|
705
|
+
break
|
|
706
|
+
|
|
707
|
+
else:
|
|
708
|
+
raise ValueError('No convergence in LTE update')
|
|
709
|
+
|
|
710
|
+
self.HminPops[:] = hminus_pops(atmos, self.atomicPops['H'])
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
class RadiativeSet:
|
|
714
|
+
'''
|
|
715
|
+
Used to configure the atomic models present in the simulation and then
|
|
716
|
+
set up the global wavelength grid and initial populations.
|
|
717
|
+
All atoms start passive.
|
|
718
|
+
|
|
719
|
+
Parameters
|
|
720
|
+
----------
|
|
721
|
+
atoms : list of AtomicModel
|
|
722
|
+
The atomic models to be used in the simulation (active, detailed, and
|
|
723
|
+
background).
|
|
724
|
+
abundance : AtomicAbundance, optional
|
|
725
|
+
The abundance to be used for each species.
|
|
726
|
+
|
|
727
|
+
Attributes
|
|
728
|
+
----------
|
|
729
|
+
abundance : AtomicAbundance
|
|
730
|
+
The abundances in use.
|
|
731
|
+
elements : list of Elements
|
|
732
|
+
The elements present in the simulation.
|
|
733
|
+
atoms : Dict[Element, AtomicModel]
|
|
734
|
+
Mapping from Element to associated model.
|
|
735
|
+
passiveSet : set of Elements
|
|
736
|
+
Set of atoms (designmated by their Elements) set to passive in the
|
|
737
|
+
simulation.
|
|
738
|
+
detailedStaticSet : set of Elements
|
|
739
|
+
Set of atoms (designmated by their Elements) set to "detailed static" in the
|
|
740
|
+
simulation.
|
|
741
|
+
activeSet : set of Elements
|
|
742
|
+
Set of atoms (designmated by their Elements) set to active in the
|
|
743
|
+
simulation.
|
|
744
|
+
'''
|
|
745
|
+
def __init__(self, atoms: List[AtomicModel],
|
|
746
|
+
abundance: AtomicAbundance=DefaultAtomicAbundance):
|
|
747
|
+
self.abundance = abundance
|
|
748
|
+
self.elements = [a.element for a in atoms]
|
|
749
|
+
self.atoms = {k: v for k, v in zip(self.elements, atoms)}
|
|
750
|
+
self.passiveSet = set(self.elements)
|
|
751
|
+
self.detailedStaticSet: Set[Element] = set()
|
|
752
|
+
self.activeSet: Set[Element] = set()
|
|
753
|
+
|
|
754
|
+
if len(self.passiveSet) > len(self.elements):
|
|
755
|
+
raise ValueError('Multiple entries for an atom: %s' % self.atoms)
|
|
756
|
+
|
|
757
|
+
def __contains__(self, x: Union[int, Tuple[int, int], str, Element]) -> bool:
|
|
758
|
+
return PeriodicTable[x] in self.elements
|
|
759
|
+
|
|
760
|
+
def is_active(self, name: Union[int, Tuple[int, int], str, Element]) -> bool:
|
|
761
|
+
'''
|
|
762
|
+
Check if an atom (designated by int, (int, int), str, or Element) is
|
|
763
|
+
active.
|
|
764
|
+
'''
|
|
765
|
+
x = PeriodicTable[name]
|
|
766
|
+
return x in self.activeSet
|
|
767
|
+
|
|
768
|
+
def is_passive(self, name: Union[int, Tuple[int, int], str, Element]) -> bool:
|
|
769
|
+
'''
|
|
770
|
+
Check if an atom (designated by int, (int, int), str, or Element) is
|
|
771
|
+
passive.
|
|
772
|
+
'''
|
|
773
|
+
x = PeriodicTable[name]
|
|
774
|
+
return x in self.passiveSet
|
|
775
|
+
|
|
776
|
+
def is_detailed(self, name: Union[int, Tuple[int, int], str, Element]) -> bool:
|
|
777
|
+
'''
|
|
778
|
+
Check if an atom (designated by int, (int, int), str, or Element) is
|
|
779
|
+
passive.
|
|
780
|
+
'''
|
|
781
|
+
x = PeriodicTable[name]
|
|
782
|
+
return x in self.detailedStaticSet
|
|
783
|
+
|
|
784
|
+
@property
|
|
785
|
+
def activeAtoms(self) -> List[AtomicModel]:
|
|
786
|
+
'''
|
|
787
|
+
List of AtomicModels set to active.
|
|
788
|
+
'''
|
|
789
|
+
activeAtoms : List[AtomicModel] = [self.atoms[e] for e in self.activeSet]
|
|
790
|
+
activeAtoms = sorted(activeAtoms, key=element_sort)
|
|
791
|
+
return activeAtoms
|
|
792
|
+
|
|
793
|
+
@property
|
|
794
|
+
def detailedAtoms(self) -> List[AtomicModel]:
|
|
795
|
+
'''
|
|
796
|
+
List of AtomicModels set to detailed static.
|
|
797
|
+
'''
|
|
798
|
+
detailedAtoms : List[AtomicModel] = [self.atoms[e] for e in self.detailedStaticSet]
|
|
799
|
+
detailedAtoms = sorted(detailedAtoms, key=element_sort)
|
|
800
|
+
return detailedAtoms
|
|
801
|
+
|
|
802
|
+
@property
|
|
803
|
+
def passiveAtoms(self) -> List[AtomicModel]:
|
|
804
|
+
'''
|
|
805
|
+
List of AtomicModels set to passive.
|
|
806
|
+
'''
|
|
807
|
+
passiveAtoms : List[AtomicModel] = [self.atoms[e] for e in self.passiveSet]
|
|
808
|
+
passiveAtoms = sorted(passiveAtoms, key=element_sort)
|
|
809
|
+
return passiveAtoms
|
|
810
|
+
|
|
811
|
+
def __getitem__(self, name: Union[int, Tuple[int, int], str, Element]) -> AtomicModel:
|
|
812
|
+
x = PeriodicTable[name]
|
|
813
|
+
return self.atoms[x]
|
|
814
|
+
|
|
815
|
+
def __iter__(self):
|
|
816
|
+
return iter(self.atoms.values())
|
|
817
|
+
|
|
818
|
+
def set_active(self, *args: str):
|
|
819
|
+
'''
|
|
820
|
+
Set one (or multiple) atoms active.
|
|
821
|
+
'''
|
|
822
|
+
names = set(args)
|
|
823
|
+
xs = [PeriodicTable[name] for name in names]
|
|
824
|
+
for x in xs:
|
|
825
|
+
self.activeSet.add(x)
|
|
826
|
+
self.detailedStaticSet.discard(x)
|
|
827
|
+
self.passiveSet.discard(x)
|
|
828
|
+
|
|
829
|
+
def set_detailed_static(self, *args: str):
|
|
830
|
+
'''
|
|
831
|
+
Set one (or multiple) atoms to detailed static
|
|
832
|
+
'''
|
|
833
|
+
names = set(args)
|
|
834
|
+
xs = [PeriodicTable[name] for name in names]
|
|
835
|
+
for x in xs:
|
|
836
|
+
self.detailedStaticSet.add(x)
|
|
837
|
+
self.activeSet.discard(x)
|
|
838
|
+
self.passiveSet.discard(x)
|
|
839
|
+
|
|
840
|
+
def set_passive(self, *args: str):
|
|
841
|
+
'''
|
|
842
|
+
Set one (or multiple) atoms passive.
|
|
843
|
+
'''
|
|
844
|
+
names = set(args)
|
|
845
|
+
xs = [PeriodicTable[name] for name in names]
|
|
846
|
+
for x in xs:
|
|
847
|
+
self.passiveSet.add(x)
|
|
848
|
+
self.activeSet.discard(x)
|
|
849
|
+
self.detailedStaticSet.discard(x)
|
|
850
|
+
|
|
851
|
+
def iterate_lte_ne_eq_pops(self, atmos: Atmosphere,
|
|
852
|
+
mols: Optional[MolecularTable]=None,
|
|
853
|
+
nlteStartingPops: Optional[Dict[Element, np.ndarray]]=None,
|
|
854
|
+
direct: bool=True, quiet: bool=True) -> SpeciesStateTable:
|
|
855
|
+
'''
|
|
856
|
+
Compute the starting populations for the simulation with all NLTE
|
|
857
|
+
atoms in LTE or otherwise using the provided populations.
|
|
858
|
+
Additionally computes a self-consistent LTE electron density.
|
|
859
|
+
|
|
860
|
+
Parameters
|
|
861
|
+
----------
|
|
862
|
+
atmos : Atmosphere
|
|
863
|
+
The atmosphere for which to compute the populations.
|
|
864
|
+
mols : MolecularTable, optional
|
|
865
|
+
Molecules to be included in the populations (default: None)
|
|
866
|
+
nlteStartingPops : Dict[Element, np.ndarray], optional
|
|
867
|
+
Starting population override for any active or detailed static
|
|
868
|
+
species.
|
|
869
|
+
direct : bool
|
|
870
|
+
Whether to use the direct electron density solver (essentially,
|
|
871
|
+
damped Lambda iteration for the fixpoint). With the new damping this
|
|
872
|
+
appears to converge very quickly with little need for the
|
|
873
|
+
Newton-Krylov selected if this is set to False. Default: True
|
|
874
|
+
quiet : bool, optional
|
|
875
|
+
Whether to print convergence info (default: True, i.e. don't print).
|
|
876
|
+
|
|
877
|
+
Returns
|
|
878
|
+
-------
|
|
879
|
+
eqPops : SpeciesStatTable
|
|
880
|
+
The configured initial populations.
|
|
881
|
+
'''
|
|
882
|
+
if mols is None:
|
|
883
|
+
mols = MolecularTable([])
|
|
884
|
+
|
|
885
|
+
if nlteStartingPops is None:
|
|
886
|
+
nlteStartingPops = {}
|
|
887
|
+
else:
|
|
888
|
+
for e in nlteStartingPops:
|
|
889
|
+
if (e not in self.activeSet) \
|
|
890
|
+
and (e not in self.detailedStaticSet):
|
|
891
|
+
raise ValueError(('Provided NLTE Populations for %s assumed LTE. '
|
|
892
|
+
'Ensure these are indexed by `Element` '
|
|
893
|
+
'rather than str.') % e)
|
|
894
|
+
|
|
895
|
+
if direct:
|
|
896
|
+
maxIter = 3000
|
|
897
|
+
prevNe = np.copy(atmos.ne)
|
|
898
|
+
ne = np.copy(atmos.ne)
|
|
899
|
+
atoms = sorted(self.atoms.values(), key=element_sort)
|
|
900
|
+
for it in range(maxIter):
|
|
901
|
+
atomicPops = []
|
|
902
|
+
prevNe[:] = ne
|
|
903
|
+
ne.fill(0.0)
|
|
904
|
+
for a in atoms:
|
|
905
|
+
abund = self.abundance[a.element]
|
|
906
|
+
nTotal = abund * atmos.nHTot
|
|
907
|
+
nStar = lte_pops(a, atmos.temperature, atmos.ne, nTotal, debye=True)
|
|
908
|
+
atomicPops.append(AtomicState(model=a, abundance=abund,
|
|
909
|
+
nStar=nStar, nTotal=nTotal))
|
|
910
|
+
|
|
911
|
+
# NOTE(cmo): Take into account NLTE pops if provided
|
|
912
|
+
if a.element in nlteStartingPops:
|
|
913
|
+
if nlteStartingPops[a.element].shape != nStar.shape:
|
|
914
|
+
raise ValueError(('Starting populations provided for %s '
|
|
915
|
+
'do not match model.') % a.element)
|
|
916
|
+
nStar = nlteStartingPops[a.element]
|
|
917
|
+
|
|
918
|
+
stages = np.array([l.stage for l in a.levels])
|
|
919
|
+
ne += np.sum(nStar * stages[:, None], axis=0)
|
|
920
|
+
# NOTE(cmo): Damp correction: dramatically improves convergence.
|
|
921
|
+
atmos.ne[:] = 0.55 * ne + 0.45 * prevNe
|
|
922
|
+
|
|
923
|
+
max_err = np.nanmax(np.abs(1.0 - prevNe / atmos.ne))
|
|
924
|
+
if max_err < 1e-5:
|
|
925
|
+
if not quiet:
|
|
926
|
+
print("Iterate LTE: %d iterations" % it)
|
|
927
|
+
break
|
|
928
|
+
else:
|
|
929
|
+
raise ValueError("LTE ne failed to converge")
|
|
930
|
+
else:
|
|
931
|
+
neRatio = np.copy(atmos.ne) / atmos.nHTot
|
|
932
|
+
iterator = LteNeIterator(self.atoms.values(), atmos.temperature,
|
|
933
|
+
atmos.nHTot, self.abundance, nlteStartingPops)
|
|
934
|
+
neRatio += iterator(neRatio)
|
|
935
|
+
newNeRatio = newton_krylov(iterator, neRatio)
|
|
936
|
+
atmos.ne[:] = newNeRatio * atmos.nHTot
|
|
937
|
+
|
|
938
|
+
atomicPops = iterator.atomicPops
|
|
939
|
+
|
|
940
|
+
detailedAtomicPops = []
|
|
941
|
+
for pop in atomicPops:
|
|
942
|
+
ele = pop.model.element
|
|
943
|
+
if ele in self.passiveSet:
|
|
944
|
+
if ele in nlteStartingPops:
|
|
945
|
+
# NOTE(cmo): I don't believe this is possible; it would need
|
|
946
|
+
# to be detailed_static as per the contract on passive atoms
|
|
947
|
+
# being "true" LTE. Leaving for now for safety.
|
|
948
|
+
pop.n = np.copy(nlteStartingPops[ele])
|
|
949
|
+
detailedAtomicPops.append(pop)
|
|
950
|
+
else:
|
|
951
|
+
nltePops = np.copy(nlteStartingPops[ele]) if ele in nlteStartingPops \
|
|
952
|
+
else np.copy(pop.nStar)
|
|
953
|
+
detailedAtomicPops.append(AtomicState(model=pop.model,
|
|
954
|
+
abundance=self.abundance[ele],
|
|
955
|
+
nStar=pop.nStar, nTotal=pop.nTotal,
|
|
956
|
+
detailed=True, pops=nltePops))
|
|
957
|
+
|
|
958
|
+
table = AtomicStateTable(detailedAtomicPops)
|
|
959
|
+
eqPops = chemical_equilibrium_fixed_ne(atmos, mols, table, self.abundance, quiet=quiet)
|
|
960
|
+
# NOTE(cmo): This is technically not quite correct, because we adjust
|
|
961
|
+
# nTotal and the atomic populations to account for the atoms bound up
|
|
962
|
+
# in molecules, but not n_e, this is unlikely to make much difference
|
|
963
|
+
# in reality, other than in very cool atmospheres with a lot of
|
|
964
|
+
# molecules (even then it should be pretty tiny)
|
|
965
|
+
return eqPops
|
|
966
|
+
|
|
967
|
+
def compute_eq_pops(self, atmos: Atmosphere,
|
|
968
|
+
mols: Optional[MolecularTable]=None,
|
|
969
|
+
nlteStartingPops: Optional[Dict[Element, np.ndarray]]=None):
|
|
970
|
+
'''
|
|
971
|
+
Compute the starting populations for the simulation with all NLTE
|
|
972
|
+
atoms in LTE or otherwise using the provided populations.
|
|
973
|
+
|
|
974
|
+
Parameters
|
|
975
|
+
----------
|
|
976
|
+
atmos : Atmosphere
|
|
977
|
+
The atmosphere for which to compute the populations.
|
|
978
|
+
mols : MolecularTable, optional
|
|
979
|
+
Molecules to be included in the populations (default: None)
|
|
980
|
+
nlteStartingPops : Dict[Element, np.ndarray], optional
|
|
981
|
+
Starting population override for any active or detailed static
|
|
982
|
+
species.
|
|
983
|
+
|
|
984
|
+
Returns
|
|
985
|
+
-------
|
|
986
|
+
eqPops : SpeciesStatTable
|
|
987
|
+
The configured initial populations.
|
|
988
|
+
'''
|
|
989
|
+
if mols is None:
|
|
990
|
+
mols = MolecularTable([])
|
|
991
|
+
|
|
992
|
+
if nlteStartingPops is None:
|
|
993
|
+
nlteStartingPops = {}
|
|
994
|
+
else:
|
|
995
|
+
for e in nlteStartingPops:
|
|
996
|
+
if (e not in self.activeSet) \
|
|
997
|
+
and (e not in self.detailedStaticSet):
|
|
998
|
+
raise ValueError(('Provided NLTE Populations for %s assumed LTE. '
|
|
999
|
+
'Ensure these are indexed by `Element` '
|
|
1000
|
+
'rather than str.') % e)
|
|
1001
|
+
|
|
1002
|
+
atomicPops = []
|
|
1003
|
+
atoms = sorted(self.atoms.values(), key=element_sort)
|
|
1004
|
+
for a in atoms:
|
|
1005
|
+
nTotal = self.abundance[a.element] * atmos.nHTot
|
|
1006
|
+
nStar = lte_pops(a, atmos.temperature, atmos.ne, nTotal, debye=True)
|
|
1007
|
+
|
|
1008
|
+
ele = a.element
|
|
1009
|
+
if ele in self.passiveSet:
|
|
1010
|
+
n = None
|
|
1011
|
+
atomicPops.append(AtomicState(model=a, abundance=self.abundance[ele], nStar=nStar,
|
|
1012
|
+
nTotal=nTotal, pops=n))
|
|
1013
|
+
else:
|
|
1014
|
+
nltePops = np.copy(nlteStartingPops[ele]) if ele in nlteStartingPops \
|
|
1015
|
+
else np.copy(nStar)
|
|
1016
|
+
atomicPops.append(AtomicState(model=a, abundance=self.abundance[ele],
|
|
1017
|
+
nStar=nStar, nTotal=nTotal, detailed=True,
|
|
1018
|
+
pops=nltePops))
|
|
1019
|
+
|
|
1020
|
+
table = AtomicStateTable(atomicPops)
|
|
1021
|
+
eqPops = chemical_equilibrium_fixed_ne(atmos, mols, table, self.abundance)
|
|
1022
|
+
# NOTE(cmo): This is technically not quite correct, because we adjust
|
|
1023
|
+
# nTotal and the atomic populations to account for the atoms bound up
|
|
1024
|
+
# in molecules, but not n_e, this is unlikely to make much difference
|
|
1025
|
+
# in reality, other than in very cool atmospheres with a lot of
|
|
1026
|
+
# molecules (even then it should be pretty tiny)
|
|
1027
|
+
return eqPops
|
|
1028
|
+
|
|
1029
|
+
def compute_wavelength_grid(self, extraWavelengths: Optional[np.ndarray]=None,
|
|
1030
|
+
lambdaReference=500.0) -> SpectrumConfiguration:
|
|
1031
|
+
'''
|
|
1032
|
+
Compute the global wavelength grid from the current configuration of
|
|
1033
|
+
the RadiativeSet.
|
|
1034
|
+
|
|
1035
|
+
Parameters
|
|
1036
|
+
----------
|
|
1037
|
+
extraWavelengths : np.ndarray, optional
|
|
1038
|
+
Extra wavelengths to add to the global array [nm].
|
|
1039
|
+
lambdaReference : float, optional
|
|
1040
|
+
If a difference reference wavelength is to be used then it should
|
|
1041
|
+
be specified here to ensure it is in the global array.
|
|
1042
|
+
|
|
1043
|
+
Returns
|
|
1044
|
+
-------
|
|
1045
|
+
spect : SpectrumConfiguration
|
|
1046
|
+
The configured wavelength grids needed to set up the backend.
|
|
1047
|
+
'''
|
|
1048
|
+
if len(self.activeSet) == 0 and len(self.detailedStaticSet) == 0:
|
|
1049
|
+
raise ValueError('Need at least one atom active or in detailed'
|
|
1050
|
+
' calculation with static populations.')
|
|
1051
|
+
extraGrids = []
|
|
1052
|
+
if extraWavelengths is not None:
|
|
1053
|
+
extraGrids.append(extraWavelengths)
|
|
1054
|
+
extraGrids.append(np.array([lambdaReference]))
|
|
1055
|
+
|
|
1056
|
+
models: List[AtomicModel] = []
|
|
1057
|
+
ids: List[Tuple[Element, int, int]] = []
|
|
1058
|
+
grids = []
|
|
1059
|
+
|
|
1060
|
+
for ele in (self.activeSet | self.detailedStaticSet):
|
|
1061
|
+
atom = self.atoms[ele]
|
|
1062
|
+
models.append(atom)
|
|
1063
|
+
for trans in atom.transitions:
|
|
1064
|
+
grids.append(trans.wavelength())
|
|
1065
|
+
ids.append(trans.transId)
|
|
1066
|
+
|
|
1067
|
+
grid = np.concatenate(grids + extraGrids)
|
|
1068
|
+
grid = np.sort(grid)
|
|
1069
|
+
grid = np.unique(grid)
|
|
1070
|
+
# grid = np.unique(np.floor(1e10*grid)) / 1e10
|
|
1071
|
+
blueIdx = {}
|
|
1072
|
+
redIdx = {}
|
|
1073
|
+
|
|
1074
|
+
for i, g in enumerate(grids):
|
|
1075
|
+
ident = ids[i]
|
|
1076
|
+
blueIdx[ident] = np.searchsorted(grid, g[0])
|
|
1077
|
+
redIdx[ident] = np.searchsorted(grid, g[-1])+1
|
|
1078
|
+
|
|
1079
|
+
transGrids: Dict[Tuple[Element, int, int], np.ndarray] = {}
|
|
1080
|
+
for ident in ids:
|
|
1081
|
+
transGrids[ident] = np.copy(grid[blueIdx[ident]:redIdx[ident]])
|
|
1082
|
+
|
|
1083
|
+
activeWavelengths = {k: ((grid >= v[0]) & (grid <= v[-1])) for k, v in transGrids.items()}
|
|
1084
|
+
activeTrans = {k: True for k in transGrids}
|
|
1085
|
+
|
|
1086
|
+
return SpectrumConfiguration(radSet=self, wavelength=grid, models=models,
|
|
1087
|
+
transWavelengths=transGrids,
|
|
1088
|
+
blueIdx=blueIdx, redIdx=redIdx,
|
|
1089
|
+
activeTrans=activeTrans,
|
|
1090
|
+
activeWavelengths=activeWavelengths)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def hminus_pops(atmos: Atmosphere, hPops: AtomicState) -> np.ndarray:
|
|
1094
|
+
'''
|
|
1095
|
+
Compute the H- ion populations for a given atmosphere
|
|
1096
|
+
|
|
1097
|
+
Parameters
|
|
1098
|
+
----------
|
|
1099
|
+
atmos : Atmosphere
|
|
1100
|
+
The atmosphere object.
|
|
1101
|
+
|
|
1102
|
+
hPops : AtomicState
|
|
1103
|
+
The hydrogen populations state associated with atmos.
|
|
1104
|
+
|
|
1105
|
+
Returns
|
|
1106
|
+
-------
|
|
1107
|
+
HminPops : np.ndarray
|
|
1108
|
+
The H- populations.
|
|
1109
|
+
'''
|
|
1110
|
+
CI = (Const.HPlanck / (2.0 * np.pi * Const.MElectron)) * (Const.HPlanck / Const.KBoltzmann)
|
|
1111
|
+
Nspace = atmos.Nspace
|
|
1112
|
+
|
|
1113
|
+
PhiHmin = 0.25 * (CI / atmos.temperature)**1.5 \
|
|
1114
|
+
* np.exp(Const.E_ION_HMIN / (Const.KBoltzmann * atmos.temperature))
|
|
1115
|
+
HminPops = atmos.ne * np.sum(hPops.n, axis=0) * PhiHmin
|
|
1116
|
+
|
|
1117
|
+
return HminPops
|
|
1118
|
+
|
|
1119
|
+
def chemical_equilibrium_fixed_ne(atmos: Atmosphere, molecules: MolecularTable,
|
|
1120
|
+
atomicPops: AtomicStateTable,
|
|
1121
|
+
abundance: AtomicAbundance,
|
|
1122
|
+
quiet: bool = False) -> SpeciesStateTable:
|
|
1123
|
+
'''
|
|
1124
|
+
Compute the molecular populations from the current atmospheric model and
|
|
1125
|
+
atomic populations.
|
|
1126
|
+
|
|
1127
|
+
This method assumes that the number of electrons bound in molecules is
|
|
1128
|
+
insignificant, and neglects this.
|
|
1129
|
+
Intended for internal use.
|
|
1130
|
+
|
|
1131
|
+
Parameters
|
|
1132
|
+
----------
|
|
1133
|
+
atmos : Atmosphere
|
|
1134
|
+
The model atmosphere of the simulation.
|
|
1135
|
+
molecules : MolecularTable
|
|
1136
|
+
The molecules to consider.
|
|
1137
|
+
atomicPops : AtomicStateTable
|
|
1138
|
+
The atomic populations.
|
|
1139
|
+
abundance : AtomicAbundance
|
|
1140
|
+
The abundance of each species in the simulation.
|
|
1141
|
+
quiet : bool, optional
|
|
1142
|
+
Whether to print convergence info (default: True).
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
Returns
|
|
1146
|
+
-------
|
|
1147
|
+
state : SpeciesState
|
|
1148
|
+
The combined state object of atomic and molecular populations.
|
|
1149
|
+
'''
|
|
1150
|
+
nucleiSet: Set[Element] = set()
|
|
1151
|
+
for mol in molecules:
|
|
1152
|
+
nucleiSet |= set(mol.elements)
|
|
1153
|
+
nuclei: List[Element] = list(nucleiSet)
|
|
1154
|
+
nuclei = sorted(nuclei)
|
|
1155
|
+
|
|
1156
|
+
if len(nuclei) == 0:
|
|
1157
|
+
HminPops = hminus_pops(atmos, atomicPops['H'])
|
|
1158
|
+
result = SpeciesStateTable(atmos, abundance, atomicPops, molecules, [], HminPops)
|
|
1159
|
+
return result
|
|
1160
|
+
|
|
1161
|
+
if nuclei[0] != PeriodicTable[1]:
|
|
1162
|
+
raise ValueError('H not list of nuclei -- check H2 molecule')
|
|
1163
|
+
# print([n.name for n in nuclei])
|
|
1164
|
+
|
|
1165
|
+
nuclIndex = [[nuclei.index(ele) for ele in mol.elements] for mol in molecules]
|
|
1166
|
+
|
|
1167
|
+
# Replace basic elements with full Models if present
|
|
1168
|
+
kuruczTable = KuruczPfTable(atomicAbundance=abundance)
|
|
1169
|
+
nucData: Dict[Element, Union[KuruczPf, AtomicState]] = {}
|
|
1170
|
+
for nuc in nuclei:
|
|
1171
|
+
if nuc in atomicPops:
|
|
1172
|
+
nucData[nuc] = atomicPops[nuc]
|
|
1173
|
+
else:
|
|
1174
|
+
nucData[nuc] = kuruczTable[nuc]
|
|
1175
|
+
|
|
1176
|
+
Nnuclei = len(nuclei)
|
|
1177
|
+
|
|
1178
|
+
Neqn = Nnuclei + len(molecules)
|
|
1179
|
+
f = np.zeros(Neqn)
|
|
1180
|
+
n = np.zeros(Neqn)
|
|
1181
|
+
df = np.zeros((Neqn, Neqn))
|
|
1182
|
+
a = np.zeros(Neqn)
|
|
1183
|
+
|
|
1184
|
+
# Equilibrium constant per molecule
|
|
1185
|
+
Phi = np.zeros(len(molecules))
|
|
1186
|
+
# Neutral fraction
|
|
1187
|
+
fn0 = np.zeros(Nnuclei)
|
|
1188
|
+
|
|
1189
|
+
CI = (Const.HPlanck / (2.0 * np.pi * Const.MElectron)) * (Const.HPlanck / Const.KBoltzmann)
|
|
1190
|
+
Nspace = atmos.Nspace
|
|
1191
|
+
HminPops = np.zeros(Nspace)
|
|
1192
|
+
molPops = [np.zeros(Nspace) for mol in molecules]
|
|
1193
|
+
maxIter = 0
|
|
1194
|
+
for k in range(Nspace):
|
|
1195
|
+
for i, nuc in enumerate(nuclei):
|
|
1196
|
+
nucleus = nucData[nuc]
|
|
1197
|
+
a[i] = nucleus.abundance * atmos.nHTot[k]
|
|
1198
|
+
fjk, dfjk = nucleus.fjk(atmos, k)
|
|
1199
|
+
fn0[i] = fjk[0]
|
|
1200
|
+
|
|
1201
|
+
PhiHmin = 0.25 * (CI / atmos.temperature[k])**1.5 \
|
|
1202
|
+
* np.exp(Const.E_ION_HMIN / (Const.KBoltzmann * atmos.temperature[k]))
|
|
1203
|
+
fHmin = atmos.ne[k] * fn0[0] * PhiHmin
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
# Eq constant for each molecule at this location
|
|
1207
|
+
for i, mol in enumerate(molecules):
|
|
1208
|
+
Phi[i] = mol.equilibrium_constant(atmos.temperature[k])
|
|
1209
|
+
|
|
1210
|
+
# Setup initial solution. Everything dissociated
|
|
1211
|
+
# n[:Nnuclei] = a[:Nnuclei]
|
|
1212
|
+
# n[Nnuclei:] = 0.0
|
|
1213
|
+
n[:] = a[:]
|
|
1214
|
+
# print('a', a)
|
|
1215
|
+
|
|
1216
|
+
nIter = 1
|
|
1217
|
+
NmaxIter = 50
|
|
1218
|
+
IterLimit = 1e-3
|
|
1219
|
+
prevN = n.copy()
|
|
1220
|
+
while nIter < NmaxIter:
|
|
1221
|
+
# print(k, ',', nIter)
|
|
1222
|
+
# Save previous solution
|
|
1223
|
+
prevN[:] = n[:]
|
|
1224
|
+
|
|
1225
|
+
# Set up iteration
|
|
1226
|
+
f[:] = n - a
|
|
1227
|
+
df[:, :] = 0.0
|
|
1228
|
+
np.fill_diagonal(df, 1.0)
|
|
1229
|
+
|
|
1230
|
+
# Add nHmin to number conservation for H
|
|
1231
|
+
f[0] += fHmin * n[0]
|
|
1232
|
+
df[0, 0] += fHmin
|
|
1233
|
+
|
|
1234
|
+
# Fill population vector f and derivative matrix df
|
|
1235
|
+
for i, mol in enumerate(molecules):
|
|
1236
|
+
saha = Phi[i]
|
|
1237
|
+
for j, ele in enumerate(mol.elements):
|
|
1238
|
+
nu = nuclIndex[i][j]
|
|
1239
|
+
saha *= (fn0[nu] * n[nu])**mol.elementCount[j]
|
|
1240
|
+
# Contribution to conservation for each nucleus in this molecule
|
|
1241
|
+
f[nu] += mol.elementCount[j] * n[Nnuclei + i]
|
|
1242
|
+
|
|
1243
|
+
saha /= atmos.ne[k]**mol.charge
|
|
1244
|
+
f[Nnuclei + i] -= saha
|
|
1245
|
+
# if Nnuclei + i == f.shape[0]-1:
|
|
1246
|
+
# print(i)
|
|
1247
|
+
# print(saha)
|
|
1248
|
+
|
|
1249
|
+
# Compute derivative matrix
|
|
1250
|
+
for j, ele in enumerate(mol.elements):
|
|
1251
|
+
nu = nuclIndex[i][j]
|
|
1252
|
+
df[nu, Nnuclei + i] += mol.elementCount[j]
|
|
1253
|
+
df[Nnuclei + i, nu] = -saha * (mol.elementCount[j] / n[nu])
|
|
1254
|
+
|
|
1255
|
+
correction = solve(df, f)
|
|
1256
|
+
n -= correction
|
|
1257
|
+
|
|
1258
|
+
dnMax = np.nanmax(np.abs(1.0 - prevN / n))
|
|
1259
|
+
if dnMax <= IterLimit:
|
|
1260
|
+
maxIter = max(maxIter, nIter)
|
|
1261
|
+
break
|
|
1262
|
+
|
|
1263
|
+
nIter += 1
|
|
1264
|
+
if dnMax > IterLimit:
|
|
1265
|
+
raise ValueError(('ChemEq iteration not converged: T: %e [K],'
|
|
1266
|
+
' density %e [m^-3], dnmax %e') % (atmos.temperature[k],
|
|
1267
|
+
atmos.nHTot[k], dnMax))
|
|
1268
|
+
|
|
1269
|
+
for i, ele in enumerate(nuclei):
|
|
1270
|
+
if ele in atomicPops:
|
|
1271
|
+
atomPop = atomicPops[ele]
|
|
1272
|
+
fraction = n[i] / atomPop.nTotal[k]
|
|
1273
|
+
atomPop.nStar[:, k] *= fraction
|
|
1274
|
+
atomPop.nTotal[k] *= fraction
|
|
1275
|
+
if atomPop.pops is not None:
|
|
1276
|
+
atomPop.pops[:, k] *= fraction
|
|
1277
|
+
|
|
1278
|
+
HminPops[k] = atmos.ne[k] * n[0] * PhiHmin
|
|
1279
|
+
|
|
1280
|
+
for i, pop in enumerate(molPops):
|
|
1281
|
+
pop[k] = n[Nnuclei + i]
|
|
1282
|
+
|
|
1283
|
+
result = SpeciesStateTable(atmos, abundance, atomicPops, molecules, molPops, HminPops)
|
|
1284
|
+
if not quiet:
|
|
1285
|
+
print("chem_eq: maximum number of iterations taken: %d" % maxIter)
|
|
1286
|
+
return result
|