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,1640 @@
|
|
|
1
|
+
import numbers
|
|
2
|
+
import pickle
|
|
3
|
+
from copy import copy
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
from typing import TYPE_CHECKING, Optional, Sequence, Union, cast
|
|
7
|
+
|
|
8
|
+
import astropy.units as u
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.polynomial.legendre import leggauss
|
|
11
|
+
|
|
12
|
+
import lightweaver.constants as Const
|
|
13
|
+
from .atomic_table import (AtomicAbundance, DefaultAtomicAbundance,
|
|
14
|
+
PeriodicTable)
|
|
15
|
+
from .utils import (ConvergenceError, check_shape_exception, get_data_path,
|
|
16
|
+
view_flatten)
|
|
17
|
+
from .wittmann import Wittmann, cgs
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .LwCompiled import LwSpectrum
|
|
21
|
+
|
|
22
|
+
class ScaleType(Enum):
|
|
23
|
+
'''
|
|
24
|
+
Atmospheric scales used in the definition of 1D atmospheres to allow the
|
|
25
|
+
correct conversion to a height based system.
|
|
26
|
+
Options:
|
|
27
|
+
|
|
28
|
+
- `Geometric`
|
|
29
|
+
|
|
30
|
+
- `ColumnMass`
|
|
31
|
+
|
|
32
|
+
- `Tau500`
|
|
33
|
+
|
|
34
|
+
'''
|
|
35
|
+
Geometric = 0
|
|
36
|
+
ColumnMass = auto()
|
|
37
|
+
Tau500 = auto()
|
|
38
|
+
|
|
39
|
+
class BoundaryCondition:
|
|
40
|
+
'''
|
|
41
|
+
Base class for boundary conditions.
|
|
42
|
+
|
|
43
|
+
Defines the interface; do not use directly.
|
|
44
|
+
|
|
45
|
+
Attributes
|
|
46
|
+
----------
|
|
47
|
+
These attributes are only available after set_required_angles has been called.
|
|
48
|
+
|
|
49
|
+
mux : np.ndarray
|
|
50
|
+
The mu_x to return from compute_bc (in order).
|
|
51
|
+
muy : np.ndarray
|
|
52
|
+
The mu_y to return from compute_bc (in order).
|
|
53
|
+
muz : np.ndarray
|
|
54
|
+
The mu_z to return from compute_bc (in order).
|
|
55
|
+
indexVector : np.ndarray
|
|
56
|
+
A 2D array of integer shape (mu, toObs) - where mu is the mu index on
|
|
57
|
+
the associated atmosphere - relating each index of the second (Nrays)
|
|
58
|
+
axis of a pair of (mu, toObs). Used to construct and destructure this
|
|
59
|
+
array.
|
|
60
|
+
|
|
61
|
+
'''
|
|
62
|
+
def compute_bc(self, atmos: 'Atmosphere', spect: 'LwSpectrum') -> np.ndarray:
|
|
63
|
+
'''
|
|
64
|
+
Called when the radiation boundary condition is needed by the backend.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
atmos : Atmosphere
|
|
69
|
+
The atmospheric object in which to compute the radiation.
|
|
70
|
+
spect : LwSpectrum
|
|
71
|
+
The computational spectrum object provided by the Context.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
result : np.ndarray
|
|
76
|
+
This function needs to return a contiguous array of shape [Nwave,
|
|
77
|
+
Nrays, Nbc], where Nwave is the number of wavelengths in the
|
|
78
|
+
wavelength grid, Nrays is the number of rays in the angular
|
|
79
|
+
quadrature (also including up/down directions) ordered as
|
|
80
|
+
specified by the mux/y/z and indexVector variables on this
|
|
81
|
+
object, Nbc is the number of spatial positions the boundary
|
|
82
|
+
condition needs to be defined at ordered in a flattened [Nz, Ny,
|
|
83
|
+
Nx] fashion. (dtype: <f8)
|
|
84
|
+
|
|
85
|
+
'''
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
def set_required_angles(self, mux, muy, muz, indexVector):
|
|
89
|
+
'''
|
|
90
|
+
The angles (and their ordering) to be used for this boundary
|
|
91
|
+
condition (in the case of a callable)
|
|
92
|
+
'''
|
|
93
|
+
self.mux = mux
|
|
94
|
+
self.muy = muy
|
|
95
|
+
self.muz = muz
|
|
96
|
+
self.indexVector = indexVector
|
|
97
|
+
|
|
98
|
+
class NoBc(BoundaryCondition):
|
|
99
|
+
'''
|
|
100
|
+
Indicates no boundary condition on the axis because it is invalid for the
|
|
101
|
+
current simulation.
|
|
102
|
+
Used only by the backend.
|
|
103
|
+
'''
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
class ZeroRadiation(BoundaryCondition):
|
|
107
|
+
'''
|
|
108
|
+
Zero radiation boundary condition.
|
|
109
|
+
Commonly used for coronal situations.
|
|
110
|
+
'''
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
class ThermalisedRadiation(BoundaryCondition):
|
|
114
|
+
'''
|
|
115
|
+
Thermalised radiation (blackbody) boundary condition.
|
|
116
|
+
Commonly used for photospheric situations.
|
|
117
|
+
'''
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
class PeriodicRadiation(BoundaryCondition):
|
|
121
|
+
'''
|
|
122
|
+
Periodic boundary condition.
|
|
123
|
+
Commonly used on the x-axis in 2D simulations.
|
|
124
|
+
'''
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
def get_top_pressure(eos: Wittmann, temp, ne=None, rho=None):
|
|
128
|
+
'''
|
|
129
|
+
Return a pressure for the top of atmosphere.
|
|
130
|
+
For internal use.
|
|
131
|
+
|
|
132
|
+
In order this is deduced from:
|
|
133
|
+
- the electron density `ne` [m-3], if provided
|
|
134
|
+
- the mass density `rho` [kg m-3], if provided
|
|
135
|
+
- the electron pressure present in FALC
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
pressure : float
|
|
140
|
+
pressure IN CGS [dyn cm-2]
|
|
141
|
+
|
|
142
|
+
'''
|
|
143
|
+
if ne is not None:
|
|
144
|
+
pe = (ne << u.Unit('m-3')).to('cm-3').value * cgs.BK * temp
|
|
145
|
+
return eos.pg_from_pe(temp, pe)
|
|
146
|
+
elif rho is not None:
|
|
147
|
+
return eos.pg_from_rho(temp, (rho << u.Unit('kg m-3')).to('g cm-3').value)
|
|
148
|
+
|
|
149
|
+
pgasCgs = np.array([0.70575286, 0.59018545, 0.51286639, 0.43719268, 0.37731009,
|
|
150
|
+
0.33516886, 0.31342915, 0.30604891, 0.30059491, 0.29207645,
|
|
151
|
+
0.2859011 , 0.28119224, 0.27893046, 0.27949676, 0.28299726,
|
|
152
|
+
0.28644693, 0.28825946, 0.29061192, 0.29340255, 0.29563072,
|
|
153
|
+
0.29864548, 0.30776456, 0.31825915, 0.32137574, 0.3239401 ,
|
|
154
|
+
0.32622212, 0.32792196, 0.3292243 , 0.33025437, 0.33146736,
|
|
155
|
+
0.3319676 , 0.33217821, 0.3322355 , 0.33217166, 0.33210297,
|
|
156
|
+
0.33203833, 0.33198508])
|
|
157
|
+
tempCoord = np.array([ 7600., 7780., 7970., 8273., 8635., 8988., 9228.,
|
|
158
|
+
9358., 9458., 9587., 9735., 9983., 10340., 10850.,
|
|
159
|
+
11440., 12190., 13080., 14520., 16280., 17930., 20420.,
|
|
160
|
+
24060., 27970., 32150., 36590., 41180., 45420., 49390.,
|
|
161
|
+
53280., 60170., 66150., 71340., 75930., 83890., 90820.,
|
|
162
|
+
95600., 100000.])
|
|
163
|
+
|
|
164
|
+
ptop = np.interp(temp, tempCoord, pgasCgs)
|
|
165
|
+
return ptop
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class Stratifications:
|
|
169
|
+
'''
|
|
170
|
+
Stores the optional derived z-stratifications of an atmospheric model.
|
|
171
|
+
|
|
172
|
+
Attributes
|
|
173
|
+
----------
|
|
174
|
+
cmass : np.ndarray
|
|
175
|
+
Column mass [kg m-2].
|
|
176
|
+
tauRef : np.ndarray
|
|
177
|
+
Reference optical depth at 500 nm.
|
|
178
|
+
'''
|
|
179
|
+
cmass: np.ndarray
|
|
180
|
+
tauRef: np.ndarray
|
|
181
|
+
|
|
182
|
+
def dimensioned_view(self, shape) -> 'Stratifications':
|
|
183
|
+
'''
|
|
184
|
+
Makes an instance of `Stratifications` reshaped to the provided
|
|
185
|
+
shape for multi-dimensional atmospheres.
|
|
186
|
+
For internal use.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
shape : tuple
|
|
191
|
+
Shape to reform the stratifications, provided by
|
|
192
|
+
`Layout.dimensioned_shape`.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
stratifications : Stratifications
|
|
197
|
+
Reshaped stratifications.
|
|
198
|
+
'''
|
|
199
|
+
strat = copy(self)
|
|
200
|
+
strat.cmass = self.cmass.reshape(shape)
|
|
201
|
+
strat.tauRef = self.tauRef.reshape(shape)
|
|
202
|
+
return strat
|
|
203
|
+
|
|
204
|
+
def unit_view(self) -> 'Stratifications':
|
|
205
|
+
'''
|
|
206
|
+
Makes an instance of `Stratifications` with the correct `astropy.units`
|
|
207
|
+
For internal use.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
stratifications : Stratifications
|
|
212
|
+
The same data with units applied.
|
|
213
|
+
'''
|
|
214
|
+
strat = copy(self)
|
|
215
|
+
strat.cmass = self.cmass << u.kg / u.m**2
|
|
216
|
+
strat.tauRef = self.tauRef << u.dimensionless_unscaled
|
|
217
|
+
return strat
|
|
218
|
+
|
|
219
|
+
def dimensioned_unit_view(self, shape) -> 'Stratifications':
|
|
220
|
+
'''
|
|
221
|
+
Makes an instance of `Stratifications` reshaped to the provided shape
|
|
222
|
+
with the correct `astropy.units` for multi-dimensional atmospheres.
|
|
223
|
+
For internal use.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
shape : tuple
|
|
228
|
+
Shape to reform the stratifications, provided by
|
|
229
|
+
`Layout.dimensioned_shape`.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
stratifications : Stratifications
|
|
234
|
+
Reshaped stratifications with units.
|
|
235
|
+
'''
|
|
236
|
+
strat = self.dimensioned_view(shape)
|
|
237
|
+
return strat.unit_view()
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class Layout:
|
|
241
|
+
'''
|
|
242
|
+
Storage for basic atmospheric parameters whose presence is determined by
|
|
243
|
+
problem dimensionality, boundary conditions and optional stratifications.
|
|
244
|
+
|
|
245
|
+
Attributes
|
|
246
|
+
----------
|
|
247
|
+
Ndim : int
|
|
248
|
+
Number of dimensions in model.
|
|
249
|
+
|
|
250
|
+
x : np.ndarray
|
|
251
|
+
Ordinates of grid points along the x-axis (present for Ndim >= 2) [m].
|
|
252
|
+
y : np.ndarray
|
|
253
|
+
Ordinates of grid points along the y-axis (present for Ndim == 3) [m].
|
|
254
|
+
z : np.ndarray
|
|
255
|
+
Ordinates of grid points along the z-axis (present for all Ndim) [m].
|
|
256
|
+
vx : np.ndarray
|
|
257
|
+
x component of plasma velocity (present for Ndim >= 2) [m/s].
|
|
258
|
+
vy : np.ndarray
|
|
259
|
+
y component of plasma velocity (present for Ndim == 3) [m/s].
|
|
260
|
+
vz : np.ndarray
|
|
261
|
+
z component of plasma velocity (present for all Ndim) [m/s]. Aliased to
|
|
262
|
+
`vlos` when `Ndim==1`
|
|
263
|
+
xLowerBc : BoundaryCondition
|
|
264
|
+
Boundary condition for the plane of minimal x-coordinate.
|
|
265
|
+
xUpperBc : BoundaryCondition
|
|
266
|
+
Boundary condition for the plane of maximal x-coordinate.
|
|
267
|
+
yLowerBc : BoundaryCondition
|
|
268
|
+
Boundary condition for the plane of minimal y-coordinate.
|
|
269
|
+
yUpperBc : BoundaryCondition
|
|
270
|
+
Boundary condition for the plane of maximal y-coordinate.
|
|
271
|
+
zLowerBc : BoundaryCondition
|
|
272
|
+
Boundary condition for the plane of minimal z-coordinate.
|
|
273
|
+
zUpperBc : BoundaryCondition
|
|
274
|
+
Boundary condition for the plane of maximal z-coordinate.
|
|
275
|
+
'''
|
|
276
|
+
|
|
277
|
+
Ndim: int
|
|
278
|
+
x: np.ndarray
|
|
279
|
+
y: np.ndarray
|
|
280
|
+
z: np.ndarray
|
|
281
|
+
vx: np.ndarray
|
|
282
|
+
vy: np.ndarray
|
|
283
|
+
vz: np.ndarray
|
|
284
|
+
xLowerBc: BoundaryCondition
|
|
285
|
+
xUpperBc: BoundaryCondition
|
|
286
|
+
yLowerBc: BoundaryCondition
|
|
287
|
+
yUpperBc: BoundaryCondition
|
|
288
|
+
zLowerBc: BoundaryCondition
|
|
289
|
+
zUpperBc: BoundaryCondition
|
|
290
|
+
stratifications: Optional[Stratifications] = None
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def make_1d(cls, z: np.ndarray, vz: np.ndarray,
|
|
294
|
+
lowerBc: BoundaryCondition, upperBc: BoundaryCondition,
|
|
295
|
+
stratifications: Optional[Stratifications]=None) -> 'Layout':
|
|
296
|
+
'''
|
|
297
|
+
Construct 1D Layout.
|
|
298
|
+
'''
|
|
299
|
+
|
|
300
|
+
return cls(Ndim=1, x=np.array(()), y=np.array(()),
|
|
301
|
+
z=z, vx=np.array(()), vy=np.array(()),
|
|
302
|
+
vz=vz, xLowerBc=NoBc(), xUpperBc=NoBc(),
|
|
303
|
+
yLowerBc=NoBc(), yUpperBc=NoBc(),
|
|
304
|
+
zLowerBc=lowerBc, zUpperBc=upperBc,
|
|
305
|
+
stratifications=stratifications)
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
def make_2d(cls, x: np.ndarray, z: np.ndarray,
|
|
309
|
+
vx: np.ndarray, vz: np.ndarray,
|
|
310
|
+
xLowerBc: BoundaryCondition, xUpperBc: BoundaryCondition,
|
|
311
|
+
zLowerBc: BoundaryCondition, zUpperBc: BoundaryCondition,
|
|
312
|
+
stratifications: Optional[Stratifications]=None) -> 'Layout':
|
|
313
|
+
'''
|
|
314
|
+
Construct 2D Layout.
|
|
315
|
+
'''
|
|
316
|
+
|
|
317
|
+
Bc = BoundaryCondition
|
|
318
|
+
return cls(Ndim=2, x=x, y=np.array(()), z=z,
|
|
319
|
+
vx=vx, vy=np.array(()), vz=vz,
|
|
320
|
+
xLowerBc=xLowerBc, xUpperBc=xUpperBc,
|
|
321
|
+
yLowerBc=NoBc(), yUpperBc=NoBc(),
|
|
322
|
+
zLowerBc=zLowerBc, zUpperBc=zUpperBc,
|
|
323
|
+
stratifications=stratifications)
|
|
324
|
+
|
|
325
|
+
@classmethod
|
|
326
|
+
def make_3d(cls, x: np.ndarray, y: np.ndarray, z: np.ndarray,
|
|
327
|
+
vx: np.ndarray, vy: np.ndarray, vz: np.ndarray,
|
|
328
|
+
xLowerBc: BoundaryCondition, xUpperBc: BoundaryCondition,
|
|
329
|
+
yLowerBc: BoundaryCondition, yUpperBc: BoundaryCondition,
|
|
330
|
+
zLowerBc: BoundaryCondition, zUpperBc: BoundaryCondition,
|
|
331
|
+
stratifications: Optional[Stratifications]=None) -> 'Layout':
|
|
332
|
+
'''
|
|
333
|
+
Construct 3D Layout.
|
|
334
|
+
'''
|
|
335
|
+
|
|
336
|
+
return cls(Ndim=3, x=x, y=y, z=z,
|
|
337
|
+
vx=vx, vy=vy, vz=vz,
|
|
338
|
+
xLowerBc=xLowerBc, xUpperBc=xUpperBc,
|
|
339
|
+
yLowerBc=yLowerBc, yUpperBc=yUpperBc,
|
|
340
|
+
zLowerBc=zLowerBc, zUpperBc=zUpperBc,
|
|
341
|
+
stratifications=stratifications)
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def Nx(self) -> int:
|
|
345
|
+
'''
|
|
346
|
+
Number of grid points along the x-axis.
|
|
347
|
+
'''
|
|
348
|
+
return self.x.shape[0]
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def Ny(self) -> int:
|
|
352
|
+
'''
|
|
353
|
+
Number of grid points along the y-axis.
|
|
354
|
+
'''
|
|
355
|
+
return self.y.shape[0]
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def Nz(self) -> int:
|
|
359
|
+
'''
|
|
360
|
+
Number of grid points along the z-axis.
|
|
361
|
+
'''
|
|
362
|
+
return self.z.shape[0]
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def Noutgoing(self) -> int:
|
|
366
|
+
'''
|
|
367
|
+
Number of grid points at which the outgoing radiation is computed.
|
|
368
|
+
'''
|
|
369
|
+
return max(1, self.Nx, self.Nx * self.Ny)
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def vlos(self) -> np.ndarray:
|
|
373
|
+
if self.Ndim > 1:
|
|
374
|
+
raise ValueError('vlos is ambiguous when Ndim > 1, use vx, vy, or vz instead.')
|
|
375
|
+
return self.vz
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def Nspace(self) -> int:
|
|
379
|
+
'''
|
|
380
|
+
Number of spatial points present in the grid.
|
|
381
|
+
'''
|
|
382
|
+
if self.Ndim == 1:
|
|
383
|
+
return self.Nz
|
|
384
|
+
elif self.Ndim == 2:
|
|
385
|
+
return self.Nx * self.Nz
|
|
386
|
+
elif self.Ndim == 3:
|
|
387
|
+
return self.Nx * self.Ny * self.Nz
|
|
388
|
+
else:
|
|
389
|
+
raise ValueError('Invalid Ndim: %d, check geometry initialisation' % self.Ndim)
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def tauRef(self):
|
|
393
|
+
'''
|
|
394
|
+
Alias to `self.stratifications.tauRef`, if computed.
|
|
395
|
+
'''
|
|
396
|
+
if self.stratifications is not None:
|
|
397
|
+
return self.stratifications.tauRef
|
|
398
|
+
else:
|
|
399
|
+
raise ValueError('tauRef not computed for this Atmosphere')
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def cmass(self):
|
|
403
|
+
'''
|
|
404
|
+
Alias to `self.stratifications.cmass`, if computed.
|
|
405
|
+
'''
|
|
406
|
+
if self.stratifications is not None:
|
|
407
|
+
return self.stratifications.cmass
|
|
408
|
+
else:
|
|
409
|
+
raise ValueError('tauRef not computed for this Atmosphere')
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def dimensioned_shape(self):
|
|
413
|
+
'''
|
|
414
|
+
Tuple defining the shape to which the arrays of atmospheric paramters
|
|
415
|
+
can be reshaped to be indexed in a 1/2/3D fashion.
|
|
416
|
+
'''
|
|
417
|
+
if self.Ndim == 1:
|
|
418
|
+
shape = (self.Nz,)
|
|
419
|
+
elif self.Ndim == 2:
|
|
420
|
+
shape = (self.Nz, self.Nx)
|
|
421
|
+
elif self.Ndim == 3:
|
|
422
|
+
shape = (self.Nz, self.Ny, self.Nx)
|
|
423
|
+
else:
|
|
424
|
+
raise ValueError('Unreasonable Ndim (%d)' % self.Ndim)
|
|
425
|
+
return shape
|
|
426
|
+
|
|
427
|
+
def dimensioned_view(self) -> 'Layout':
|
|
428
|
+
'''
|
|
429
|
+
Returns a view over the contents of Layout reshaped so all data has
|
|
430
|
+
the correct (1/2/3D) dimensionality for the atmospheric model, as
|
|
431
|
+
these are all stored under a flat scheme.
|
|
432
|
+
'''
|
|
433
|
+
layout = copy(self)
|
|
434
|
+
shape = self.dimensioned_shape
|
|
435
|
+
if self.stratifications is not None:
|
|
436
|
+
layout.stratifications = self.stratifications.dimensioned_view(shape)
|
|
437
|
+
if self.vx.size > 0:
|
|
438
|
+
layout.vx = self.vx.reshape(shape)
|
|
439
|
+
if self.vy.size > 0:
|
|
440
|
+
layout.vy = self.vy.reshape(shape)
|
|
441
|
+
if self.vz.size > 0:
|
|
442
|
+
layout.vz = self.vz.reshape(shape)
|
|
443
|
+
return layout
|
|
444
|
+
|
|
445
|
+
def unit_view(self) -> 'Layout':
|
|
446
|
+
'''
|
|
447
|
+
Returns a view over the contents of the Layout with the correct
|
|
448
|
+
`astropy.units`.
|
|
449
|
+
'''
|
|
450
|
+
layout = copy(self)
|
|
451
|
+
layout.x = self.x << u.m
|
|
452
|
+
layout.y = self.y << u.m
|
|
453
|
+
layout.z = self.z << u.m
|
|
454
|
+
layout.vx = self.vx << u.m / u.s
|
|
455
|
+
layout.vy = self.vy << u.m / u.s
|
|
456
|
+
layout.vz = self.vz << u.m / u.s
|
|
457
|
+
if self.stratifications is not None:
|
|
458
|
+
layout.stratifications = self.stratifications.unit_view()
|
|
459
|
+
return layout
|
|
460
|
+
|
|
461
|
+
def dimensioned_unit_view(self) -> 'Layout':
|
|
462
|
+
'''
|
|
463
|
+
Returns a view over the contents of Layout reshaped so all data has
|
|
464
|
+
the correct (1/2/3D) dimensionality for the atmospheric model, and
|
|
465
|
+
the correct `astropy.units`.
|
|
466
|
+
'''
|
|
467
|
+
layout = self.dimensioned_view()
|
|
468
|
+
return layout.unit_view()
|
|
469
|
+
|
|
470
|
+
@dataclass
|
|
471
|
+
class Atmosphere:
|
|
472
|
+
'''
|
|
473
|
+
Storage for all atmospheric data. These arrays will be shared directly
|
|
474
|
+
with the backend, so a modification here also modifies the data seen by
|
|
475
|
+
the backend. Be careful to modify these arrays *in place*, as their data
|
|
476
|
+
is shared by direct memory reference. Use the class methods to construct
|
|
477
|
+
atmospheres of different dimensionality.
|
|
478
|
+
|
|
479
|
+
Attributes
|
|
480
|
+
----------
|
|
481
|
+
structure : Layout
|
|
482
|
+
A layout structure holding the atmospheric stratification, and
|
|
483
|
+
velocity description.
|
|
484
|
+
temperature : np.ndarray
|
|
485
|
+
The atmospheric temperature structure.
|
|
486
|
+
vturb : np.ndarray
|
|
487
|
+
The atmospheric microturbulent velocity structure.
|
|
488
|
+
ne : np.ndarray
|
|
489
|
+
The electron density structure in the atmosphere.
|
|
490
|
+
nHTot : np.ndarray
|
|
491
|
+
The total hydrogen number density distribution throughout the
|
|
492
|
+
atmosphere.
|
|
493
|
+
B : np.ndarray, optional
|
|
494
|
+
The magnitude of the stratified magnetic field throughout the
|
|
495
|
+
atmosphere (Tesla).
|
|
496
|
+
gammaB : np.ndarray, optional
|
|
497
|
+
Co-altitude (latitude) of magnetic field vector (radians) throughout the
|
|
498
|
+
atmosphere from the local vertical.
|
|
499
|
+
chiB : np.ndarray, optional
|
|
500
|
+
Azimuth of magnetic field vector (radians) in the x-y plane, measured
|
|
501
|
+
from the x-axis.
|
|
502
|
+
'''
|
|
503
|
+
|
|
504
|
+
structure: Layout
|
|
505
|
+
temperature: np.ndarray
|
|
506
|
+
vturb: np.ndarray
|
|
507
|
+
ne: np.ndarray
|
|
508
|
+
nHTot: np.ndarray
|
|
509
|
+
B: Optional[np.ndarray] = None
|
|
510
|
+
gammaB: Optional[np.ndarray] = None
|
|
511
|
+
chiB: Optional[np.ndarray] = None
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def Ndim(self) -> int:
|
|
515
|
+
'''
|
|
516
|
+
Ndim : int
|
|
517
|
+
The dimensionality (1, 2, or 3) of the atmospheric model.
|
|
518
|
+
'''
|
|
519
|
+
return self.structure.Ndim
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def Nx(self) -> int:
|
|
523
|
+
'''
|
|
524
|
+
Nx : int
|
|
525
|
+
The number of points in the x-direction discretisation.
|
|
526
|
+
'''
|
|
527
|
+
return self.structure.Nx
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def Ny(self) -> int:
|
|
531
|
+
'''
|
|
532
|
+
Ny : int
|
|
533
|
+
The number of points in the y-direction discretisation.
|
|
534
|
+
'''
|
|
535
|
+
return self.structure.Ny
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def Nz(self) -> int:
|
|
539
|
+
'''
|
|
540
|
+
Nz : int
|
|
541
|
+
The number of points in the y-direction discretisation.
|
|
542
|
+
'''
|
|
543
|
+
return self.structure.Nz
|
|
544
|
+
|
|
545
|
+
@property
|
|
546
|
+
def Noutgoing(self) -> int:
|
|
547
|
+
'''
|
|
548
|
+
Noutgoing : int
|
|
549
|
+
The number of cells at the top of the atmosphere (that each produce a
|
|
550
|
+
spectrum).
|
|
551
|
+
'''
|
|
552
|
+
return self.structure.Noutgoing
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def vx(self) -> np.ndarray:
|
|
556
|
+
'''
|
|
557
|
+
vx : np.ndarray
|
|
558
|
+
x component of plasma velocity (present for Ndim >= 2) [m/s].
|
|
559
|
+
'''
|
|
560
|
+
return self.structure.vx
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def vy(self) -> np.ndarray:
|
|
564
|
+
'''
|
|
565
|
+
vy : np.ndarray
|
|
566
|
+
y component of plasma velocity (present for Ndim == 3) [m/s].
|
|
567
|
+
'''
|
|
568
|
+
return self.structure.vy
|
|
569
|
+
|
|
570
|
+
@property
|
|
571
|
+
def vz(self) -> np.ndarray:
|
|
572
|
+
'''
|
|
573
|
+
vz : np.ndarray
|
|
574
|
+
z component of plasma velocity (present for all Ndim) [m/s]. Aliased
|
|
575
|
+
to `vlos` when `Ndim==1`
|
|
576
|
+
'''
|
|
577
|
+
return self.structure.vz
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def vlos(self) -> np.ndarray:
|
|
581
|
+
'''
|
|
582
|
+
vz : np.ndarray
|
|
583
|
+
z component of plasma velocity (present for all Ndim) [m/s]. Only
|
|
584
|
+
available when Ndim==1`.
|
|
585
|
+
'''
|
|
586
|
+
return self.structure.vlos
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
def cmass(self) -> np.ndarray:
|
|
590
|
+
'''
|
|
591
|
+
cmass : np.ndarray
|
|
592
|
+
Column mass [kg m-2].
|
|
593
|
+
'''
|
|
594
|
+
return self.structure.cmass
|
|
595
|
+
|
|
596
|
+
@property
|
|
597
|
+
def tauRef(self) -> np.ndarray:
|
|
598
|
+
'''
|
|
599
|
+
tauRef : np.ndarray
|
|
600
|
+
Reference optical depth at 500 nm.
|
|
601
|
+
'''
|
|
602
|
+
return self.structure.tauRef
|
|
603
|
+
|
|
604
|
+
@property
|
|
605
|
+
def height(self) -> np.ndarray:
|
|
606
|
+
return self.structure.z
|
|
607
|
+
|
|
608
|
+
@property
|
|
609
|
+
def x(self) -> np.ndarray:
|
|
610
|
+
'''
|
|
611
|
+
x : np.ndarray
|
|
612
|
+
Ordinates of grid points along the x-axis (present for Ndim >= 2) [m].
|
|
613
|
+
'''
|
|
614
|
+
return self.structure.x
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def y(self) -> np.ndarray:
|
|
618
|
+
'''
|
|
619
|
+
y : np.ndarray
|
|
620
|
+
Ordinates of grid points along the y-axis (present for Ndim == 3) [m].
|
|
621
|
+
'''
|
|
622
|
+
return self.structure.y
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def z(self) -> np.ndarray:
|
|
626
|
+
'''
|
|
627
|
+
z : np.ndarray
|
|
628
|
+
Ordinates of grid points along the z-axis (present for all Ndim) [m].
|
|
629
|
+
'''
|
|
630
|
+
return self.structure.z
|
|
631
|
+
|
|
632
|
+
@property
|
|
633
|
+
def zLowerBc(self) -> BoundaryCondition:
|
|
634
|
+
'''
|
|
635
|
+
zLowerBc : BoundaryCondition
|
|
636
|
+
Boundary condition for the plane of minimal z-coordinate.
|
|
637
|
+
'''
|
|
638
|
+
return self.structure.zLowerBc
|
|
639
|
+
|
|
640
|
+
@property
|
|
641
|
+
def zUpperBc(self) -> BoundaryCondition:
|
|
642
|
+
'''
|
|
643
|
+
zUpperBc : BoundaryCondition
|
|
644
|
+
Boundary condition for the plane of maximal z-coordinate.
|
|
645
|
+
'''
|
|
646
|
+
return self.structure.zUpperBc
|
|
647
|
+
|
|
648
|
+
@property
|
|
649
|
+
def yLowerBc(self) -> BoundaryCondition:
|
|
650
|
+
'''
|
|
651
|
+
yLowerBc : BoundaryCondition
|
|
652
|
+
Boundary condition for the plane of minimal y-coordinate.
|
|
653
|
+
'''
|
|
654
|
+
return self.structure.yLowerBc
|
|
655
|
+
|
|
656
|
+
@property
|
|
657
|
+
def yUpperBc(self) -> BoundaryCondition:
|
|
658
|
+
'''
|
|
659
|
+
yUpperBc : BoundaryCondition
|
|
660
|
+
Boundary condition for the plane of maximal y-coordinate.
|
|
661
|
+
'''
|
|
662
|
+
return self.structure.yUpperBc
|
|
663
|
+
|
|
664
|
+
@property
|
|
665
|
+
def xLowerBc(self) -> BoundaryCondition:
|
|
666
|
+
'''
|
|
667
|
+
xLowerBc : BoundaryCondition
|
|
668
|
+
Boundary condition for the plane of minimal x-coordinate.
|
|
669
|
+
'''
|
|
670
|
+
return self.structure.xLowerBc
|
|
671
|
+
|
|
672
|
+
@property
|
|
673
|
+
def xUpperBc(self) -> BoundaryCondition:
|
|
674
|
+
'''
|
|
675
|
+
xUpperBc : BoundaryCondition
|
|
676
|
+
Boundary condition for the plane of maximal x-coordinate.
|
|
677
|
+
'''
|
|
678
|
+
return self.structure.xUpperBc
|
|
679
|
+
|
|
680
|
+
@property
|
|
681
|
+
def Nspace(self):
|
|
682
|
+
'''
|
|
683
|
+
Nspace : int
|
|
684
|
+
Total number of points in the atmospheric spatial discretistaion.
|
|
685
|
+
'''
|
|
686
|
+
return self.structure.Nspace
|
|
687
|
+
|
|
688
|
+
@property
|
|
689
|
+
def Nrays(self):
|
|
690
|
+
'''
|
|
691
|
+
Nrays : int
|
|
692
|
+
Number of rays in angular discretisation used.
|
|
693
|
+
'''
|
|
694
|
+
try:
|
|
695
|
+
if self.muz is None:
|
|
696
|
+
raise AttributeError('Nrays not set, call atmos.rays or .quadrature first')
|
|
697
|
+
except AttributeError:
|
|
698
|
+
raise AttributeError('Nrays not set, call atmos.rays or .quadrature first')
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
return self.muz.shape[0]
|
|
702
|
+
|
|
703
|
+
def dimensioned_view(self):
|
|
704
|
+
'''
|
|
705
|
+
Returns a view over the contents of Layout reshaped so all data has
|
|
706
|
+
the correct (1/2/3D) dimensionality for the atmospheric model, as
|
|
707
|
+
these are all stored under a flat scheme.
|
|
708
|
+
'''
|
|
709
|
+
shape = self.structure.dimensioned_shape
|
|
710
|
+
atmos = copy(self)
|
|
711
|
+
atmos.structure = self.structure.dimensioned_view()
|
|
712
|
+
atmos.temperature = self.temperature.reshape(shape)
|
|
713
|
+
atmos.vturb = self.vturb.reshape(shape)
|
|
714
|
+
atmos.ne = self.ne.reshape(shape)
|
|
715
|
+
atmos.nHTot = self.nHTot.reshape(shape)
|
|
716
|
+
if self.B is not None:
|
|
717
|
+
atmos.B = self.B.reshape(shape)
|
|
718
|
+
atmos.chiB = self.chiB.reshape(shape)
|
|
719
|
+
atmos.gammaB = self.gammaB.reshape(shape)
|
|
720
|
+
return atmos
|
|
721
|
+
|
|
722
|
+
def unit_view(self):
|
|
723
|
+
'''
|
|
724
|
+
Returns a view over the contents of the Layout with the correct
|
|
725
|
+
`astropy.units`.
|
|
726
|
+
'''
|
|
727
|
+
atmos = copy(self)
|
|
728
|
+
atmos.structure = self.structure.unit_view()
|
|
729
|
+
atmos.temperature = self.temperature << u.K
|
|
730
|
+
atmos.vturb = self.vturb << u.m / u.s
|
|
731
|
+
atmos.ne = self.ne << u.m**(-3)
|
|
732
|
+
atmos.nHTot = self.nHTot << u.m**(-3)
|
|
733
|
+
if self.B is not None:
|
|
734
|
+
atmos.B = self.B << u.T
|
|
735
|
+
atmos.chiB = self.chiB << u.rad
|
|
736
|
+
atmos.gammaB = self.gammaB << u.rad
|
|
737
|
+
return atmos
|
|
738
|
+
|
|
739
|
+
def dimensioned_unit_view(self):
|
|
740
|
+
'''
|
|
741
|
+
Returns a view over the contents of Layout reshaped so all data has
|
|
742
|
+
the correct (1/2/3D) dimensionality for the atmospheric model, and
|
|
743
|
+
the correct `astropy.units`.
|
|
744
|
+
'''
|
|
745
|
+
atmos = self.dimensioned_view()
|
|
746
|
+
return atmos.unit_view()
|
|
747
|
+
|
|
748
|
+
@classmethod
|
|
749
|
+
def make_1d(cls, scale: ScaleType, depthScale: np.ndarray,
|
|
750
|
+
temperature: np.ndarray, vlos: np.ndarray,
|
|
751
|
+
vturb: np.ndarray, ne: Optional[np.ndarray]=None,
|
|
752
|
+
hydrogenPops: Optional[np.ndarray]=None,
|
|
753
|
+
nHTot: Optional[np.ndarray]=None,
|
|
754
|
+
B: Optional[np.ndarray]=None,
|
|
755
|
+
gammaB: Optional[np.ndarray]=None,
|
|
756
|
+
chiB: Optional[np.ndarray]=None,
|
|
757
|
+
lowerBc: Optional[BoundaryCondition]=None,
|
|
758
|
+
upperBc: Optional[BoundaryCondition]=None,
|
|
759
|
+
convertScales: bool=True,
|
|
760
|
+
abundance: Optional[AtomicAbundance]=None,
|
|
761
|
+
logG: float=2.44,
|
|
762
|
+
Pgas: Optional[np.ndarray]=None,
|
|
763
|
+
Pe: Optional[np.ndarray]=None,
|
|
764
|
+
Ptop: Optional[float]=None,
|
|
765
|
+
PeTop: Optional[float]=None,
|
|
766
|
+
verbose: bool=False):
|
|
767
|
+
'''
|
|
768
|
+
Constructor for 1D Atmosphere objects. Optionally will use an
|
|
769
|
+
equation of state (EOS) to estimate missing parameters.
|
|
770
|
+
|
|
771
|
+
If sufficient information is provided (i.e. all required parameters
|
|
772
|
+
and ne and (hydrogenPops or nHTot)) then the EOS is not invoked to
|
|
773
|
+
estimate any thermodynamic properties. If both of nHTot and
|
|
774
|
+
hydrogenPops are omitted, then the electron pressure will be used
|
|
775
|
+
with the Wittmann equation of state to estimate the mass density, and
|
|
776
|
+
the hydrogen number density will be inferred from this and the
|
|
777
|
+
abundances. If, instead, ne is omitted, then the mass density will be
|
|
778
|
+
used with the Wittmann EOS to estimate the electron pressure.
|
|
779
|
+
If both of these are omitted then the EOS will be used to estimate
|
|
780
|
+
both. If:
|
|
781
|
+
|
|
782
|
+
- Pgas is provided, then this gas pressure will define the
|
|
783
|
+
atmospheric stratification and will be used with the EOS.
|
|
784
|
+
|
|
785
|
+
- Pe is provided, then this electron pressure will define the
|
|
786
|
+
atmospheric stratification and will be used with the EOS.
|
|
787
|
+
|
|
788
|
+
- Ptop is provided, then this gas pressure at the top of the
|
|
789
|
+
atmosphere will be used with the log gravitational acceleration
|
|
790
|
+
logG, and the EOS to estimate the missing parameters assuming
|
|
791
|
+
hydrostatic equilibrium.
|
|
792
|
+
|
|
793
|
+
- PeTop is provided, then this electron pressure at the top of
|
|
794
|
+
the atmosphere will be used with the log gravitational
|
|
795
|
+
acceleration logG, and the EOS to estimate the missing parameters
|
|
796
|
+
assuming hydrostatic equilibrium.
|
|
797
|
+
|
|
798
|
+
- If all of Pgas, Pe, Ptop, PeTop are omitted then Ptop will be
|
|
799
|
+
estimated from the gas pressure in the FALC model at the
|
|
800
|
+
temperature at the top boundary. The hydrostatic reconstruction
|
|
801
|
+
will then continue as usual.
|
|
802
|
+
|
|
803
|
+
convertScales will substantially slow down this function due to the
|
|
804
|
+
slow calculation of background opacities used to compute tauRef. If
|
|
805
|
+
an atmosphere is constructed with a Geometric stratification, and an
|
|
806
|
+
estimate of tauRef is not required before running the main RT module,
|
|
807
|
+
then this can be set to False.
|
|
808
|
+
All of these parameters can be provided as astropy Quantities, and
|
|
809
|
+
will be converted in the constructor.
|
|
810
|
+
|
|
811
|
+
Parameters
|
|
812
|
+
----------
|
|
813
|
+
scale : ScaleType
|
|
814
|
+
The type of stratification used along the z-axis.
|
|
815
|
+
depthScale : np.ndarray
|
|
816
|
+
The z-coordinates used along the chosen stratification. The
|
|
817
|
+
stratification is expected to start at the top of the atmosphere
|
|
818
|
+
(closest to the observer), and descend along the observer's line
|
|
819
|
+
of sight.
|
|
820
|
+
temperature : np.ndarray
|
|
821
|
+
Temperature structure of the atmosphere [K].
|
|
822
|
+
vlos : np.ndarray
|
|
823
|
+
Velocity structure of the atmosphere along z [m/s].
|
|
824
|
+
vturb : np.ndarray
|
|
825
|
+
Microturbulent velocity structure of the atmosphere [m/s].
|
|
826
|
+
ne : np.ndarray
|
|
827
|
+
Electron density structure of the atmosphere [m-3].
|
|
828
|
+
hydrogenPops : np.ndarray, optional
|
|
829
|
+
Detailed (per level) hydrogen number density structure of the
|
|
830
|
+
atmosphere [m-3], 2D array [Nlevel, Nspace].
|
|
831
|
+
nHTot : np.ndarray, optional
|
|
832
|
+
Total hydrogen number density structure of the atmosphere [m-3]
|
|
833
|
+
B : np.ndarray, optional.
|
|
834
|
+
Magnetic field strength [T].
|
|
835
|
+
gammaB : np.ndarray, optional
|
|
836
|
+
Co-altitude of magnetic field vector [radians].
|
|
837
|
+
chiB : np.ndarray, optional
|
|
838
|
+
Azimuth of magnetic field vector (in x-y plane, from x) [radians].
|
|
839
|
+
lowerBc : BoundaryCondition, optional
|
|
840
|
+
Boundary condition for incoming radiation at the minimal z
|
|
841
|
+
coordinate (default: ThermalisedRadiation).
|
|
842
|
+
upperBc : BoundaryCondition, optional
|
|
843
|
+
Boundary condition for incoming radiation at the maximal z
|
|
844
|
+
coordinate (default: ZeroRadiation).
|
|
845
|
+
convertScales : bool, optional
|
|
846
|
+
Whether to automatically compute tauRef and cmass for an
|
|
847
|
+
atmosphere given in a stratification of m (default: True).
|
|
848
|
+
abundance: AtomicAbundance, optional
|
|
849
|
+
An instance of AtomicAbundance giving the abundances of each
|
|
850
|
+
atomic species in the given atmosphere, only used if the EOS is
|
|
851
|
+
invoked. (default: DefaultAtomicAbundance)
|
|
852
|
+
logG: float, optional
|
|
853
|
+
The log10 of the magnitude of gravitational acceleration [m/s2]
|
|
854
|
+
(default: 2.44).
|
|
855
|
+
Pgas: np.ndarray, optional
|
|
856
|
+
The gas pressure stratification of the atmosphere [Pa],
|
|
857
|
+
optionally used by the EOS.
|
|
858
|
+
Pe: np.ndarray, optional
|
|
859
|
+
The electron pressure stratification of the atmosphere [Pa],
|
|
860
|
+
optionally used by the EOS.
|
|
861
|
+
Ptop: np.ndarray, optional
|
|
862
|
+
The gas pressure at the top of the atmosphere [Pa], optionally
|
|
863
|
+
used by the EOS for a hydrostatic reconstruction.
|
|
864
|
+
Petop: np.ndarray, optional
|
|
865
|
+
The electron pressure at the top of the atmosphere [Pa],
|
|
866
|
+
optionally used by the EOS for a hydrostatic reconstruction.
|
|
867
|
+
verbose: bool, optional
|
|
868
|
+
Explain decisions made with the EOS to estimate missing
|
|
869
|
+
parameters (if invoked) through print calls (default: False).
|
|
870
|
+
|
|
871
|
+
Raises
|
|
872
|
+
------
|
|
873
|
+
ValueError
|
|
874
|
+
if incorrect arguments or unable to construct estimate missing
|
|
875
|
+
parameters.
|
|
876
|
+
'''
|
|
877
|
+
if scale == ScaleType.Geometric:
|
|
878
|
+
depthScale = (depthScale << u.m).value
|
|
879
|
+
if np.any((depthScale[:-1] - depthScale[1:]) < 0.0):
|
|
880
|
+
raise ValueError("Geometric depth scale should be provided in decreasing height.")
|
|
881
|
+
elif scale == ScaleType.ColumnMass:
|
|
882
|
+
depthScale = (depthScale << u.kg / u.m**2).value
|
|
883
|
+
if np.any((depthScale[1:] - depthScale[:-1]) < 0.0):
|
|
884
|
+
raise ValueError("Column mass depth scale should be provided in increasing column mass.")
|
|
885
|
+
|
|
886
|
+
check_shape = lambda x, xName: check_shape_exception(x,
|
|
887
|
+
depthScale.shape[0], 1, xName)
|
|
888
|
+
temperature = (temperature << u.K).value
|
|
889
|
+
check_shape(temperature, 'temperature')
|
|
890
|
+
vlos = (vlos << u.m / u.s).value
|
|
891
|
+
check_shape(vlos, 'vlos')
|
|
892
|
+
vturb = (vturb << u.m / u.s).value
|
|
893
|
+
check_shape(vturb, 'vturb')
|
|
894
|
+
if ne is not None:
|
|
895
|
+
ne = (ne << u.m**(-3)).value
|
|
896
|
+
check_shape(ne, 'ne')
|
|
897
|
+
if hydrogenPops is not None:
|
|
898
|
+
hydrogenPops = (hydrogenPops << u.m**(-3)).value
|
|
899
|
+
hydrogenPops = cast(np.ndarray, hydrogenPops)
|
|
900
|
+
if hydrogenPops.shape[1] != depthScale.shape[0]:
|
|
901
|
+
raise ValueError(f'Array hydrogenPops does not have the expected'
|
|
902
|
+
f' second dimension: {depthScale.shape[0]}'
|
|
903
|
+
f' (got: {hydrogenPops.shape[1]}).')
|
|
904
|
+
if nHTot is not None:
|
|
905
|
+
nHTot = (nHTot << u.m**(-3)).value
|
|
906
|
+
check_shape(nHTot, 'nHTot')
|
|
907
|
+
if B is not None:
|
|
908
|
+
B = (B << u.T).value
|
|
909
|
+
check_shape(B, 'B')
|
|
910
|
+
if gammaB is None or chiB is None:
|
|
911
|
+
raise ValueError('B is set, both gammaB and chiB must be also.')
|
|
912
|
+
if gammaB is not None:
|
|
913
|
+
gammaB = (gammaB << u.rad).value
|
|
914
|
+
check_shape(gammaB, 'gammaB')
|
|
915
|
+
if B is None or chiB is None:
|
|
916
|
+
raise ValueError('gammaB is set, both B and chiB must be also.')
|
|
917
|
+
if chiB is not None:
|
|
918
|
+
chiB = (chiB << u.rad).value
|
|
919
|
+
check_shape(chiB, 'chiB')
|
|
920
|
+
if gammaB is None or B is None:
|
|
921
|
+
raise ValueError('chiB is set, both B and gammaB must be also.')
|
|
922
|
+
|
|
923
|
+
if lowerBc is None:
|
|
924
|
+
lowerBc = ThermalisedRadiation()
|
|
925
|
+
elif isinstance(lowerBc, PeriodicRadiation):
|
|
926
|
+
raise ValueError('Cannot set periodic boundary conditions for 1D atmosphere')
|
|
927
|
+
if upperBc is None:
|
|
928
|
+
upperBc = ZeroRadiation()
|
|
929
|
+
elif isinstance(upperBc, PeriodicRadiation):
|
|
930
|
+
raise ValueError('Cannot set periodic boundary conditions for 1D atmosphere')
|
|
931
|
+
|
|
932
|
+
if scale != ScaleType.Geometric and not convertScales:
|
|
933
|
+
raise ValueError('Height scale must be provided if scale conversion is not applied')
|
|
934
|
+
|
|
935
|
+
if nHTot is None and hydrogenPops is not None:
|
|
936
|
+
nHTot = np.sum(hydrogenPops, axis=0)
|
|
937
|
+
|
|
938
|
+
if np.any(temperature < 2000):
|
|
939
|
+
# NOTE(cmo): Minimum value was decreased in NICOLE so should be safe
|
|
940
|
+
raise ValueError('Minimum temperature too low for EOS (< 2000 K)')
|
|
941
|
+
|
|
942
|
+
if abundance is None:
|
|
943
|
+
abundance = DefaultAtomicAbundance
|
|
944
|
+
|
|
945
|
+
wittAbundances = np.array([abundance[e] for e in PeriodicTable.elements])
|
|
946
|
+
eos = Wittmann(abund_init=wittAbundances)
|
|
947
|
+
|
|
948
|
+
Nspace = depthScale.shape[0]
|
|
949
|
+
if nHTot is None and ne is not None:
|
|
950
|
+
if verbose:
|
|
951
|
+
print('Setting nHTot from electron pressure.')
|
|
952
|
+
pe = (ne << u.Unit('m-3')).to('cm-3').value * cgs.BK * temperature
|
|
953
|
+
rho = np.zeros(Nspace)
|
|
954
|
+
for k in range(Nspace):
|
|
955
|
+
rho[k] = eos.rho_from_pe(temperature[k], pe[k])
|
|
956
|
+
nHTot = np.copy((rho << u.Unit('g cm-3')).to('kg m-3').value
|
|
957
|
+
/ (Const.Amu * abundance.massPerH))
|
|
958
|
+
elif ne is None and nHTot is not None:
|
|
959
|
+
if verbose:
|
|
960
|
+
print('Setting ne from mass density.')
|
|
961
|
+
rho = ((Const.Amu * abundance.massPerH * nHTot) << u.Unit('kg m-3')).to('g cm-3').value
|
|
962
|
+
pe = np.zeros(Nspace)
|
|
963
|
+
for k in range(Nspace):
|
|
964
|
+
pe[k] = eos.pe_from_rho(temperature[k], rho[k])
|
|
965
|
+
ne = np.copy(((pe / (cgs.BK * temperature)) << u.Unit('cm-3')).to('m-3').value)
|
|
966
|
+
elif ne is None and nHTot is None:
|
|
967
|
+
if Pgas is not None and Pgas.shape[0] != Nspace:
|
|
968
|
+
raise ValueError('Dimensions of Pgas do not match atmospheric depth')
|
|
969
|
+
if Pe is not None and Pe.shape[0] != Nspace:
|
|
970
|
+
raise ValueError('Dimensions of Pe do not match atmospheric depth')
|
|
971
|
+
|
|
972
|
+
if Pgas is not None and Pe is None:
|
|
973
|
+
if verbose:
|
|
974
|
+
print('Setting ne, nHTot from provided gas pressure.')
|
|
975
|
+
# Convert to cgs for eos
|
|
976
|
+
pgas = (Pgas << u.Unit('Pa')).to('dyn cm-2').value
|
|
977
|
+
pe = np.zeros(Nspace)
|
|
978
|
+
rho = np.zeros(Nspace)
|
|
979
|
+
for k in range(Nspace):
|
|
980
|
+
pe[k] = eos.pe_from_pg(temperature[k], pgas[k])
|
|
981
|
+
rho[k] = eos.rho_from_pg(temperature[k], pgas[k])
|
|
982
|
+
elif Pe is not None and Pgas is None:
|
|
983
|
+
if verbose:
|
|
984
|
+
print('Setting ne, nHTot from provided electron pressure.')
|
|
985
|
+
# Convert to cgs for eos
|
|
986
|
+
pe = (Pe << u.Unit('Pa')).to('dyn cm-2').value
|
|
987
|
+
pgas = np.zeros(Nspace)
|
|
988
|
+
rho = np.zeros(Nspace)
|
|
989
|
+
for k in range(Nspace):
|
|
990
|
+
pgas[k] = eos.pg_from_pe(temperature[k], pe[k])
|
|
991
|
+
rho[k] = eos.rho_from_pe(temperature[k], pe[k])
|
|
992
|
+
elif Pgas is None and Pe is None:
|
|
993
|
+
# Doing Hydrostatic Eq. based here on NICOLE implementation
|
|
994
|
+
gravAcc = ((10**logG) << u.Unit('m s-2')).to('cm s-2').value
|
|
995
|
+
Avog = 6.022045e23 # Avogadro's Number
|
|
996
|
+
if Ptop is None and PeTop is not None:
|
|
997
|
+
if verbose:
|
|
998
|
+
print(('Setting ne, nHTot to hydrostatic equilibrium (logG=%f)'
|
|
999
|
+
' from provided top electron pressure.') % logG)
|
|
1000
|
+
PeTop = (PeTop << u.Unit("Pa")).to('dyn cm-2').value
|
|
1001
|
+
Ptop = eos.pg_from_pe(temperature[0], PeTop)
|
|
1002
|
+
elif Ptop is not None and PeTop is None:
|
|
1003
|
+
if verbose:
|
|
1004
|
+
print(('Setting ne, nHTot to hydrostatic equilibrium (logG=%f)'
|
|
1005
|
+
' from provided top gas pressure.') % logG)
|
|
1006
|
+
Ptop = (Ptop << u.Unit("Pa")).to('dyn cm-2').value
|
|
1007
|
+
PeTop = eos.pe_from_pg(temperature[0], Ptop)
|
|
1008
|
+
elif Ptop is None and PeTop is None:
|
|
1009
|
+
if verbose:
|
|
1010
|
+
print(('Setting ne, nHTot to hydrostatic equilibrium (logG=%f)'
|
|
1011
|
+
' from FALC gas pressure at upper boundary temperature.') % logG)
|
|
1012
|
+
Ptop = get_top_pressure(eos, temperature[0])
|
|
1013
|
+
PeTop = eos.pe_from_pg(temperature[0], Ptop)
|
|
1014
|
+
else:
|
|
1015
|
+
raise ValueError("Cannot set both Ptop and PeTop")
|
|
1016
|
+
|
|
1017
|
+
if scale == ScaleType.Tau500:
|
|
1018
|
+
tau = depthScale
|
|
1019
|
+
elif scale == ScaleType.Geometric:
|
|
1020
|
+
height = (depthScale << u.Unit('m')).to('cm').value
|
|
1021
|
+
else:
|
|
1022
|
+
cmass = (depthScale << u.Unit('kg m-2')).to('g cm-2').value
|
|
1023
|
+
|
|
1024
|
+
# NOTE(cmo): Compute HSE following the NICOLE method.
|
|
1025
|
+
rho = np.zeros(Nspace)
|
|
1026
|
+
chi_c = np.zeros(Nspace)
|
|
1027
|
+
pgas = np.zeros(Nspace)
|
|
1028
|
+
pe = np.zeros(Nspace)
|
|
1029
|
+
pgas[0] = Ptop
|
|
1030
|
+
pe[0] = PeTop
|
|
1031
|
+
chi_c[0] = eos.cont_opacity(temperature[0], pgas[0], pe[0],
|
|
1032
|
+
np.array([5000.0])).item()
|
|
1033
|
+
avg_mol_weight = lambda k: abundance.massPerH / (abundance.totalAbundance
|
|
1034
|
+
+ pe[k] / pgas[k])
|
|
1035
|
+
rho[0] = Ptop * avg_mol_weight(0) / Avog / cgs.BK / temperature[0]
|
|
1036
|
+
chi_c[0] /= rho[0]
|
|
1037
|
+
|
|
1038
|
+
for k in range(1, Nspace):
|
|
1039
|
+
chi_c[k] = chi_c[k-1]
|
|
1040
|
+
rho[k] = rho[k-1]
|
|
1041
|
+
for it in range(200):
|
|
1042
|
+
if scale == ScaleType.Tau500:
|
|
1043
|
+
dtau = tau[k] - tau[k-1]
|
|
1044
|
+
pgas[k] = (pgas[k-1] + gravAcc * dtau
|
|
1045
|
+
/ (0.5 * (chi_c[k-1] + chi_c[k])))
|
|
1046
|
+
elif scale == ScaleType.Geometric:
|
|
1047
|
+
pgas[k] = pgas[k-1] * np.exp(-gravAcc / Avog /
|
|
1048
|
+
cgs.BK * avg_mol_weight(k-1)
|
|
1049
|
+
* 0.5 * (1.0 / temperature[k-1]
|
|
1050
|
+
+ 1.0 / temperature[k]) *
|
|
1051
|
+
(height[k] - height[k-1]))
|
|
1052
|
+
else:
|
|
1053
|
+
pgas[k] = gravAcc * cmass[k]
|
|
1054
|
+
|
|
1055
|
+
pe[k] = eos.pe_from_pg(temperature[k], pgas[k])
|
|
1056
|
+
prevChi = chi_c[k]
|
|
1057
|
+
chi_c[k] = eos.cont_opacity(temperature[k], pgas[k], pe[k],
|
|
1058
|
+
np.array([5000.0])).item()
|
|
1059
|
+
rho[k] = (pgas[k] * avg_mol_weight(k) / Avog /
|
|
1060
|
+
cgs.BK / temperature[k])
|
|
1061
|
+
chi_c[k] /= rho[k]
|
|
1062
|
+
|
|
1063
|
+
change = np.abs(prevChi - chi_c[k]) / (prevChi + chi_c[k])
|
|
1064
|
+
if change < 1e-5:
|
|
1065
|
+
break
|
|
1066
|
+
else:
|
|
1067
|
+
raise ConvergenceError(('No convergence in HSE at depth point %d, '
|
|
1068
|
+
'last change %2.4e') % (k, change))
|
|
1069
|
+
nHTot = np.copy((rho << u.Unit('g cm-3')).to('kg m-3').value
|
|
1070
|
+
/ (Const.Amu * abundance.massPerH))
|
|
1071
|
+
ne = np.copy(((pe / (cgs.BK * temperature)) << u.Unit('cm-3')).to('m-3').value)
|
|
1072
|
+
|
|
1073
|
+
# NOTE(cmo): Compute final pgas, pe from EOS that will be used for
|
|
1074
|
+
# background opacity.
|
|
1075
|
+
rhoSI = Const.Amu * abundance.massPerH * nHTot
|
|
1076
|
+
rho = (rhoSI << u.Unit('kg m-3')).to('g cm-3').value
|
|
1077
|
+
pgas = np.zeros_like(depthScale)
|
|
1078
|
+
pe = np.zeros_like(depthScale)
|
|
1079
|
+
for k in range(Nspace):
|
|
1080
|
+
pgas[k] = eos.pg_from_rho(temperature[k], rho[k])
|
|
1081
|
+
pe[k] = eos.pe_from_rho(temperature[k], rho[k])
|
|
1082
|
+
|
|
1083
|
+
chi_c = np.zeros_like(depthScale)
|
|
1084
|
+
for k in range(depthScale.shape[0]):
|
|
1085
|
+
chi_c[k] = eos.cont_opacity(temperature[k], pgas[k], pe[k],
|
|
1086
|
+
np.array([5000.0])).item()
|
|
1087
|
+
chi_c = (chi_c << u.Unit('cm')).to('m').value
|
|
1088
|
+
|
|
1089
|
+
# NOTE(cmo): We should now have a uniform minimum set of data (other
|
|
1090
|
+
# than the scale type), allowing us to simply convert between the
|
|
1091
|
+
# scales we do have!
|
|
1092
|
+
if convertScales:
|
|
1093
|
+
if scale == ScaleType.ColumnMass:
|
|
1094
|
+
height = np.zeros_like(depthScale)
|
|
1095
|
+
tau_ref = np.zeros_like(depthScale)
|
|
1096
|
+
cmass = depthScale
|
|
1097
|
+
|
|
1098
|
+
height[0] = 0.0
|
|
1099
|
+
tau_ref[0] = chi_c[0] / rhoSI[0] * cmass[0]
|
|
1100
|
+
for k in range(1, cmass.shape[0]):
|
|
1101
|
+
height[k] = height[k-1] - 2.0 * ((cmass[k] - cmass[k-1])
|
|
1102
|
+
/ (rhoSI[k-1] + rhoSI[k]))
|
|
1103
|
+
tau_ref[k] = tau_ref[k-1] + 0.5 * ((chi_c[k-1] + chi_c[k])
|
|
1104
|
+
* (height[k-1] - height[k]))
|
|
1105
|
+
|
|
1106
|
+
hTau1 = np.interp(1.0, tau_ref, height)
|
|
1107
|
+
height -= hTau1
|
|
1108
|
+
elif scale == ScaleType.Geometric:
|
|
1109
|
+
cmass = np.zeros(Nspace)
|
|
1110
|
+
tau_ref = np.zeros(Nspace)
|
|
1111
|
+
height = depthScale
|
|
1112
|
+
nHTot = cast(np.ndarray, nHTot)
|
|
1113
|
+
ne = cast(np.ndarray, ne)
|
|
1114
|
+
|
|
1115
|
+
cmass[0] = ((nHTot[0] * abundance.massPerH + ne[0])
|
|
1116
|
+
* (Const.KBoltzmann * temperature[0] / 10**logG))
|
|
1117
|
+
tau_ref[0] = 0.5 * chi_c[0] * (height[0] - height[1])
|
|
1118
|
+
if tau_ref[0] > 1.0:
|
|
1119
|
+
tau_ref[0] = 0.0
|
|
1120
|
+
|
|
1121
|
+
for k in range(1, Nspace):
|
|
1122
|
+
cmass[k] = cmass[k-1] + 0.5 * ((rhoSI[k-1] + rhoSI[k])
|
|
1123
|
+
* (height[k-1] - height[k]))
|
|
1124
|
+
tau_ref[k] = tau_ref[k-1] + 0.5 * ((chi_c[k-1] + chi_c[k])
|
|
1125
|
+
* (height[k-1] - height[k]))
|
|
1126
|
+
elif scale == ScaleType.Tau500:
|
|
1127
|
+
cmass = np.zeros(Nspace)
|
|
1128
|
+
height = np.zeros(Nspace)
|
|
1129
|
+
tau_ref = depthScale
|
|
1130
|
+
|
|
1131
|
+
cmass[0] = (tau_ref[0] / chi_c[0]) * rhoSI[0]
|
|
1132
|
+
for k in range(1, Nspace):
|
|
1133
|
+
height[k] = height[k-1] - 2.0 * ((tau_ref[k] - tau_ref[k-1])
|
|
1134
|
+
/ (chi_c[k-1] + chi_c[k]))
|
|
1135
|
+
cmass[k] = cmass[k-1] + 0.5 * ((chi_c[k-1] + chi_c[k])
|
|
1136
|
+
* (height[k-1] - height[k]))
|
|
1137
|
+
|
|
1138
|
+
hTau1 = np.interp(1.0, tau_ref, height)
|
|
1139
|
+
height -= hTau1
|
|
1140
|
+
else:
|
|
1141
|
+
raise ValueError("Other scales not handled yet")
|
|
1142
|
+
|
|
1143
|
+
stratifications: Optional[Stratifications] = Stratifications(
|
|
1144
|
+
cmass=cmass,
|
|
1145
|
+
tauRef=tau_ref)
|
|
1146
|
+
|
|
1147
|
+
else:
|
|
1148
|
+
stratifications = None
|
|
1149
|
+
height = depthScale
|
|
1150
|
+
|
|
1151
|
+
layout = Layout.make_1d(z=height, vz=vlos,
|
|
1152
|
+
lowerBc=lowerBc, upperBc=upperBc,
|
|
1153
|
+
stratifications=stratifications)
|
|
1154
|
+
ne = cast(np.ndarray, ne)
|
|
1155
|
+
nHTot = cast(np.ndarray, nHTot)
|
|
1156
|
+
atmos = cls(structure=layout, temperature=temperature, vturb=vturb,
|
|
1157
|
+
ne=ne, nHTot=nHTot, B=B, gammaB=gammaB, chiB=chiB)
|
|
1158
|
+
|
|
1159
|
+
return atmos
|
|
1160
|
+
|
|
1161
|
+
@classmethod
|
|
1162
|
+
def make_2d(cls, height: np.ndarray, x: np.ndarray,
|
|
1163
|
+
temperature: np.ndarray, vx: np.ndarray,
|
|
1164
|
+
vz: np.ndarray, vturb: np.ndarray,
|
|
1165
|
+
ne: Optional[np.ndarray]=None,
|
|
1166
|
+
nHTot: Optional[np.ndarray]=None,
|
|
1167
|
+
B: Optional[np.ndarray]=None,
|
|
1168
|
+
gammaB: Optional[np.ndarray]=None,
|
|
1169
|
+
chiB: Optional[np.ndarray]=None,
|
|
1170
|
+
xUpperBc: Optional[BoundaryCondition]=None,
|
|
1171
|
+
xLowerBc: Optional[BoundaryCondition]=None,
|
|
1172
|
+
zUpperBc: Optional[BoundaryCondition]=None,
|
|
1173
|
+
zLowerBc: Optional[BoundaryCondition]=None,
|
|
1174
|
+
abundance: Optional[AtomicAbundance]=None,
|
|
1175
|
+
verbose=False):
|
|
1176
|
+
'''
|
|
1177
|
+
Constructor for 2D Atmosphere objects.
|
|
1178
|
+
|
|
1179
|
+
No provision for estimating parameters using hydrostatic equilibrium
|
|
1180
|
+
is provided, but one of ne, or nHTot can be omitted and inferred by
|
|
1181
|
+
use of the Wittmann equation of state.
|
|
1182
|
+
The atmosphere must be defined on a geometric stratification.
|
|
1183
|
+
All atmospheric parameters are expected in a 2D [z, x] array.
|
|
1184
|
+
|
|
1185
|
+
Parameters
|
|
1186
|
+
----------
|
|
1187
|
+
height : np.ndarray
|
|
1188
|
+
The z-coordinates of the atmospheric grid. The stratification is
|
|
1189
|
+
expected to start at the top of the atmosphere (closest to the
|
|
1190
|
+
observer), and descend along the observer's line of sight.
|
|
1191
|
+
x : np.ndarray
|
|
1192
|
+
The (horizontal) x-coordinates of the atmospheric grid.
|
|
1193
|
+
temperature : np.ndarray
|
|
1194
|
+
Temperature structure of the atmosphere [K].
|
|
1195
|
+
vx : np.ndarray
|
|
1196
|
+
x-component of the atmospheric velocity [m/s].
|
|
1197
|
+
vz : np.ndarray
|
|
1198
|
+
z-component of the atmospheric velocity [m/s].
|
|
1199
|
+
vturb : np.ndarray
|
|
1200
|
+
Microturbulent velocity structure [m/s].
|
|
1201
|
+
ne : np.ndarray
|
|
1202
|
+
Electron density structure of the atmosphere [m-3].
|
|
1203
|
+
nHTot : np.ndarray, optional
|
|
1204
|
+
Total hydrogen number density structure of the atmosphere [m-3].
|
|
1205
|
+
B : np.ndarray, optional.
|
|
1206
|
+
Magnetic field strength [T].
|
|
1207
|
+
gammaB : np.ndarray, optional
|
|
1208
|
+
Inclination (co-altitude) of magnetic field vector to the z-axis
|
|
1209
|
+
[radians].
|
|
1210
|
+
chiB : np.ndarray, optional
|
|
1211
|
+
Azimuth of magnetic field vector (in x-y plane, from x) [radians].
|
|
1212
|
+
xLowerBc : BoundaryCondition, optional
|
|
1213
|
+
Boundary condition for incoming radiation at the minimal x
|
|
1214
|
+
coordinate (default: PeriodicRadiation).
|
|
1215
|
+
xUpperBc : BoundaryCondition, optional
|
|
1216
|
+
Boundary condition for incoming radiation at the maximal x
|
|
1217
|
+
coordinate (default: PeriodicRadiation).
|
|
1218
|
+
zLowerBc : BoundaryCondition, optional
|
|
1219
|
+
Boundary condition for incoming radiation at the minimal z
|
|
1220
|
+
coordinate (default: ThermalisedRadiation).
|
|
1221
|
+
zUpperBc : BoundaryCondition, optional
|
|
1222
|
+
Boundary condition for incoming radiation at the maximal z
|
|
1223
|
+
coordinate (default: ZeroRadiation).
|
|
1224
|
+
convertScales : bool, optional
|
|
1225
|
+
Whether to automatically compute tauRef and cmass for an
|
|
1226
|
+
atmosphere given in a stratification of m (default: True).
|
|
1227
|
+
abundance: AtomicAbundance, optional
|
|
1228
|
+
An instance of AtomicAbundance giving the abundances of each
|
|
1229
|
+
atomic species in the given atmosphere, only used if the EOS is
|
|
1230
|
+
invoked. (default: DefaultAtomicAbundance)
|
|
1231
|
+
verbose: bool, optional
|
|
1232
|
+
Explain decisions made with the EOS to estimate missing
|
|
1233
|
+
parameters (if invoked) through print calls (default: False).
|
|
1234
|
+
|
|
1235
|
+
Raises
|
|
1236
|
+
------
|
|
1237
|
+
ValueError
|
|
1238
|
+
if incorrect arguments or unable to construct estimate missing
|
|
1239
|
+
parameters.
|
|
1240
|
+
'''
|
|
1241
|
+
|
|
1242
|
+
x = (x << u.m).value
|
|
1243
|
+
if np.any((x[1:] - x[:-1]) < 0.0):
|
|
1244
|
+
raise ValueError("x should be increasing with index (left -> right).")
|
|
1245
|
+
height = (height << u.m).value
|
|
1246
|
+
if np.any((height[:-1] - height[1:]) < 0.0):
|
|
1247
|
+
raise ValueError("Height should be decreasing with index (top -> bottom).")
|
|
1248
|
+
temperature = (temperature << u.K).value
|
|
1249
|
+
vx = (vx << u.m / u.s).value
|
|
1250
|
+
vz = (vz << u.m / u.s).value
|
|
1251
|
+
vturb = (vturb << u.m / u.s).value
|
|
1252
|
+
if ne is not None:
|
|
1253
|
+
ne = (ne << u.m**(-3)).value
|
|
1254
|
+
if nHTot is not None:
|
|
1255
|
+
nHTot = (nHTot << u.m**(-3)).value
|
|
1256
|
+
if B is not None:
|
|
1257
|
+
B = (B << u.T).value
|
|
1258
|
+
B = cast(np.ndarray, B)
|
|
1259
|
+
flatB = view_flatten(B)
|
|
1260
|
+
else:
|
|
1261
|
+
flatB = None
|
|
1262
|
+
|
|
1263
|
+
if gammaB is not None:
|
|
1264
|
+
gammaB = (gammaB << u.rad).value
|
|
1265
|
+
gammaB = cast(np.ndarray, gammaB)
|
|
1266
|
+
flatGammaB = view_flatten(gammaB)
|
|
1267
|
+
else:
|
|
1268
|
+
flatGammaB = None
|
|
1269
|
+
|
|
1270
|
+
if chiB is not None:
|
|
1271
|
+
chiB = (chiB << u.rad).value
|
|
1272
|
+
chiB = cast(np.ndarray, chiB)
|
|
1273
|
+
flatChiB = view_flatten(chiB)
|
|
1274
|
+
else:
|
|
1275
|
+
flatChiB = None
|
|
1276
|
+
|
|
1277
|
+
if zLowerBc is None:
|
|
1278
|
+
zLowerBc = ThermalisedRadiation()
|
|
1279
|
+
elif isinstance(zLowerBc, PeriodicRadiation):
|
|
1280
|
+
raise ValueError('Cannot set periodic boundary conditions for z-axis.')
|
|
1281
|
+
if zUpperBc is None:
|
|
1282
|
+
zUpperBc = ZeroRadiation()
|
|
1283
|
+
elif isinstance(zUpperBc, PeriodicRadiation):
|
|
1284
|
+
raise ValueError('Cannot set periodic boundary conditions for z-axis.')
|
|
1285
|
+
if xUpperBc is None:
|
|
1286
|
+
xUpperBc = PeriodicRadiation()
|
|
1287
|
+
if xLowerBc is None:
|
|
1288
|
+
xLowerBc = PeriodicRadiation()
|
|
1289
|
+
if abundance is None:
|
|
1290
|
+
|
|
1291
|
+
abundance = DefaultAtomicAbundance
|
|
1292
|
+
|
|
1293
|
+
wittAbundances = np.array([abundance[e] for e in PeriodicTable.elements])
|
|
1294
|
+
eos = Wittmann(abund_init=wittAbundances)
|
|
1295
|
+
|
|
1296
|
+
flatHeight = view_flatten(height)
|
|
1297
|
+
flatTemperature = view_flatten(temperature)
|
|
1298
|
+
Nspace = flatHeight.shape[0]
|
|
1299
|
+
if nHTot is None and ne is not None:
|
|
1300
|
+
if verbose:
|
|
1301
|
+
print('Setting nHTot from electron pressure.')
|
|
1302
|
+
flatNe = view_flatten(ne)
|
|
1303
|
+
pe = (flatNe << u.Unit('m-3')).to('cm-3').value * cgs.BK * flatTemperature
|
|
1304
|
+
rho = np.zeros(Nspace)
|
|
1305
|
+
for k in range(Nspace):
|
|
1306
|
+
rho[k] = eos.rho_from_pe(flatTemperature[k], pe[k])
|
|
1307
|
+
nHTot = np.ascontiguousarray(
|
|
1308
|
+
(rho << u.Unit('g cm-3')).to('kg m-3').value
|
|
1309
|
+
/ (Const.Amu * abundance.massPerH)
|
|
1310
|
+
)
|
|
1311
|
+
elif ne is None and nHTot is not None:
|
|
1312
|
+
if verbose:
|
|
1313
|
+
print('Setting ne from mass density.')
|
|
1314
|
+
flatNHTot = view_flatten(nHTot)
|
|
1315
|
+
rho = ((Const.Amu * abundance.massPerH * flatNHTot) << u.Unit('kg m-3')).to('g cm-3').value
|
|
1316
|
+
pe = np.zeros(Nspace)
|
|
1317
|
+
for k in range(Nspace):
|
|
1318
|
+
pe[k] = eos.pe_from_rho(flatTemperature[k], rho[k])
|
|
1319
|
+
ne = np.ascontiguousarray(((pe / (cgs.BK * flatTemperature)) << u.Unit('cm-3')).to('m-3').value)
|
|
1320
|
+
elif ne is None and nHTot is None:
|
|
1321
|
+
raise ValueError('Cannot omit both ne and nHTot (currently).')
|
|
1322
|
+
flatX = view_flatten(x)
|
|
1323
|
+
nHTot = cast(np.ndarray, nHTot)
|
|
1324
|
+
flatNHTot = view_flatten(nHTot)
|
|
1325
|
+
ne = cast(np.ndarray, ne)
|
|
1326
|
+
flatNe = view_flatten(ne)
|
|
1327
|
+
flatVx = view_flatten(vx)
|
|
1328
|
+
flatVz = view_flatten(vz)
|
|
1329
|
+
flatVturb = view_flatten(vturb)
|
|
1330
|
+
|
|
1331
|
+
layout = Layout.make_2d(x=flatX, z=flatHeight, vx=flatVx, vz=flatVz,
|
|
1332
|
+
xLowerBc=xLowerBc, xUpperBc=xUpperBc,
|
|
1333
|
+
zLowerBc=zLowerBc, zUpperBc=zUpperBc,
|
|
1334
|
+
stratifications=None)
|
|
1335
|
+
|
|
1336
|
+
atmos = cls(structure=layout, temperature=flatTemperature,
|
|
1337
|
+
vturb=flatVturb, ne=flatNe, nHTot=flatNHTot, B=flatB,
|
|
1338
|
+
gammaB=flatGammaB, chiB=flatChiB)
|
|
1339
|
+
return atmos
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def quadrature(self, Nrays: Optional[int]=None,
|
|
1343
|
+
mu: Optional[Sequence[float]]=None,
|
|
1344
|
+
wmu: Optional[Sequence[float]]=None):
|
|
1345
|
+
'''
|
|
1346
|
+
Compute the angular quadrature for solving the RTE and Kinetic
|
|
1347
|
+
Equilibrium in a given atmosphere.
|
|
1348
|
+
|
|
1349
|
+
Procedure varies with dimensionality.
|
|
1350
|
+
|
|
1351
|
+
By convention muz is always positive, as the direction on this axis
|
|
1352
|
+
is determined by the toObs term that is used internally to the formal
|
|
1353
|
+
solver.
|
|
1354
|
+
|
|
1355
|
+
1D:
|
|
1356
|
+
If a number of rays is given (typically 3 or 5), then the
|
|
1357
|
+
Gauss-Legendre quadrature for this set is used.
|
|
1358
|
+
If mu and wmu are instead given then these will be validated and
|
|
1359
|
+
used.
|
|
1360
|
+
|
|
1361
|
+
2+D:
|
|
1362
|
+
If the number of rays selected is in the list of near optimal
|
|
1363
|
+
quadratures for unpolarised radiation provided by Stepan et al
|
|
1364
|
+
2020 (A&A, 646 A24), then this is used. Otherwise an exception is
|
|
1365
|
+
raised.
|
|
1366
|
+
|
|
1367
|
+
The available quadratures are:
|
|
1368
|
+
|
|
1369
|
+
+--------+-------+
|
|
1370
|
+
| Points | Order |
|
|
1371
|
+
+========+=======+
|
|
1372
|
+
| 1 | 3 |
|
|
1373
|
+
+--------+-------+
|
|
1374
|
+
| 3 | 7 |
|
|
1375
|
+
+--------+-------+
|
|
1376
|
+
| 6 | 9 |
|
|
1377
|
+
+--------+-------+
|
|
1378
|
+
| 7 | 11 |
|
|
1379
|
+
+--------+-------+
|
|
1380
|
+
| 10 | 13 |
|
|
1381
|
+
+--------+-------+
|
|
1382
|
+
| 11 | 15 |
|
|
1383
|
+
+--------+-------+
|
|
1384
|
+
|
|
1385
|
+
Parameters
|
|
1386
|
+
----------
|
|
1387
|
+
Nrays : int, optional
|
|
1388
|
+
The number of rays to use in the quadrature. See notes above.
|
|
1389
|
+
mu : sequence of float, optional
|
|
1390
|
+
The cosine of the angle made between the between each of the set
|
|
1391
|
+
of rays and the z axis, only used in 1D.
|
|
1392
|
+
wmu : sequence of float, optional
|
|
1393
|
+
The integration weights for each mu, must be provided if mu is provided.
|
|
1394
|
+
|
|
1395
|
+
Raises
|
|
1396
|
+
------
|
|
1397
|
+
ValueError
|
|
1398
|
+
on incorrect input.
|
|
1399
|
+
'''
|
|
1400
|
+
|
|
1401
|
+
if self.Ndim == 1:
|
|
1402
|
+
if Nrays is not None and mu is None:
|
|
1403
|
+
if Nrays >= 1:
|
|
1404
|
+
x, w = leggauss(Nrays)
|
|
1405
|
+
mid, halfWidth = 0.5, 0.5
|
|
1406
|
+
x = mid + halfWidth * x
|
|
1407
|
+
w *= halfWidth
|
|
1408
|
+
|
|
1409
|
+
self.muz = x
|
|
1410
|
+
self.wmu = w
|
|
1411
|
+
else:
|
|
1412
|
+
raise ValueError('Unsupported Nrays=%d' % Nrays)
|
|
1413
|
+
elif Nrays is not None and mu is not None:
|
|
1414
|
+
if wmu is None:
|
|
1415
|
+
raise ValueError('Must provide wmu when providing mu')
|
|
1416
|
+
if Nrays != len(mu):
|
|
1417
|
+
raise ValueError('mu must be Nrays long if Nrays is provided')
|
|
1418
|
+
if len(mu) != len(wmu):
|
|
1419
|
+
raise ValueError('mu and wmu must be the same shape')
|
|
1420
|
+
|
|
1421
|
+
self.muz = np.array(mu, dtype=np.float64)
|
|
1422
|
+
self.wmu = np.array(wmu, dtype=np.float64)
|
|
1423
|
+
|
|
1424
|
+
self.muy = np.zeros_like(self.muz)
|
|
1425
|
+
self.mux = np.sqrt(1.0 - self.muz**2)
|
|
1426
|
+
else:
|
|
1427
|
+
with open(get_data_path() + 'Quadratures.pickle', 'rb') as pkl:
|
|
1428
|
+
quads = pickle.load(pkl)
|
|
1429
|
+
|
|
1430
|
+
rays = {int(q.split('n')[1]): q for q in quads}
|
|
1431
|
+
if Nrays not in rays:
|
|
1432
|
+
raise ValueError('For multidimensional cases Nrays must be in %s' % repr(rays))
|
|
1433
|
+
|
|
1434
|
+
quad = quads[rays[Nrays]]
|
|
1435
|
+
|
|
1436
|
+
if self.Ndim == 2:
|
|
1437
|
+
Nrays *= 2
|
|
1438
|
+
theta = np.deg2rad(quad[:, 1])
|
|
1439
|
+
chi = np.deg2rad(quad[:, 2])
|
|
1440
|
+
# polar coords:
|
|
1441
|
+
# x = sin theta cos chi
|
|
1442
|
+
# y = sin theta sin chi
|
|
1443
|
+
# z = cos theta
|
|
1444
|
+
self.mux = np.zeros(Nrays)
|
|
1445
|
+
self.mux[:Nrays // 2] = np.sin(theta) * np.cos(chi)
|
|
1446
|
+
self.mux[Nrays // 2:] = -np.sin(theta) * np.cos(chi)
|
|
1447
|
+
self.muz = np.zeros(Nrays)
|
|
1448
|
+
self.muz[:Nrays // 2] = np.cos(theta)
|
|
1449
|
+
self.muz[Nrays // 2:] = np.cos(theta)
|
|
1450
|
+
self.wmu = np.zeros(Nrays)
|
|
1451
|
+
self.wmu[:Nrays // 2] = quad[:, 0]
|
|
1452
|
+
self.wmu[Nrays // 2:] = quad[:, 0]
|
|
1453
|
+
self.wmu /= np.sum(self.wmu)
|
|
1454
|
+
self.muy = np.sqrt(1.0 - (self.mux**2 + self.muz**2))
|
|
1455
|
+
|
|
1456
|
+
else:
|
|
1457
|
+
raise NotImplementedError()
|
|
1458
|
+
|
|
1459
|
+
self.configure_bcs()
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def rays(self, muz: Union[float, Sequence[float]],
|
|
1463
|
+
mux: Optional[Union[float, Sequence[float]]]=None,
|
|
1464
|
+
muy: Optional[Union[float, Sequence[float]]]=None,
|
|
1465
|
+
wmu: Optional[Union[float, Sequence[float]]]=None,
|
|
1466
|
+
upOnly: bool=False):
|
|
1467
|
+
'''
|
|
1468
|
+
Set up the rays on the Atmosphere for computing the intensity in a
|
|
1469
|
+
particular direction (or set of directions).
|
|
1470
|
+
|
|
1471
|
+
If only the z angle is set then the ray is assumed in the x-z plane.
|
|
1472
|
+
If either muz or muy is omitted then this angle is inferred by
|
|
1473
|
+
normalisation of the projection.
|
|
1474
|
+
|
|
1475
|
+
By convention muz is always positive, as the direction on this axis
|
|
1476
|
+
is determined by the toObs term that is used internally to the formal
|
|
1477
|
+
solver.
|
|
1478
|
+
|
|
1479
|
+
Parameters
|
|
1480
|
+
----------
|
|
1481
|
+
muz : float or sequence of float, optional
|
|
1482
|
+
The angular projections along the z axis.
|
|
1483
|
+
mux : float or sequence of float, optional
|
|
1484
|
+
The angular projections along the x axis.
|
|
1485
|
+
muy : float or sequence of float, optional
|
|
1486
|
+
The angular projections along the y axis.
|
|
1487
|
+
wmu : float or sequence of float, optional
|
|
1488
|
+
The integration weights for the given ray if J is to be
|
|
1489
|
+
integrated for angle set.
|
|
1490
|
+
upOnly : bool, optional
|
|
1491
|
+
Whether to only configure boundary conditions for up-only rays.
|
|
1492
|
+
(default: False)
|
|
1493
|
+
|
|
1494
|
+
Raises
|
|
1495
|
+
------
|
|
1496
|
+
ValueError
|
|
1497
|
+
if the angular projections or integration weights are incorrectly
|
|
1498
|
+
normalised.
|
|
1499
|
+
'''
|
|
1500
|
+
|
|
1501
|
+
if isinstance(muz, numbers.Real):
|
|
1502
|
+
muz = [float(muz)]
|
|
1503
|
+
if isinstance(mux, numbers.Real):
|
|
1504
|
+
mux = [float(mux)]
|
|
1505
|
+
if isinstance(muy, numbers.Real):
|
|
1506
|
+
muy = [float(muy)]
|
|
1507
|
+
if isinstance(wmu, numbers.Real):
|
|
1508
|
+
wmu = [float(wmu)]
|
|
1509
|
+
|
|
1510
|
+
if mux is None and muy is None:
|
|
1511
|
+
self.muz = np.array(muz, dtype=np.float64)
|
|
1512
|
+
self.wmu = np.zeros_like(self.muz)
|
|
1513
|
+
self.muy = np.zeros_like(self.muz)
|
|
1514
|
+
self.mux = np.sqrt(1.0 - self.muz**2)
|
|
1515
|
+
elif muy is None:
|
|
1516
|
+
self.muz = np.array(muz, dtype=np.float64)
|
|
1517
|
+
self.wmu = np.zeros_like(self.muz)
|
|
1518
|
+
self.mux = np.array(mux, dtype=np.float64)
|
|
1519
|
+
self.muy = np.sqrt(1.0 - (self.muz**2 + self.mux**2))
|
|
1520
|
+
elif mux is None:
|
|
1521
|
+
self.muz = np.array(muz, dtype=np.float64)
|
|
1522
|
+
self.wmu = np.zeros_like(self.muz)
|
|
1523
|
+
self.muy = np.array(muy, dtype=np.float64)
|
|
1524
|
+
self.mux = np.sqrt(1.0 - (self.muz**2 + self.muy**2))
|
|
1525
|
+
else:
|
|
1526
|
+
self.muz = np.array(muz, dtype=np.float64)
|
|
1527
|
+
self.mux = np.array(mux, dtype=np.float64)
|
|
1528
|
+
self.muy = np.array(muy, dtype=np.float64)
|
|
1529
|
+
self.wmu = np.zeros_like(muz)
|
|
1530
|
+
|
|
1531
|
+
if not np.allclose(self.muz**2 + self.mux**2 + self.muy**2, 1):
|
|
1532
|
+
raise ValueError('mux**2 + muy**2 + muz**2 != 1.0')
|
|
1533
|
+
|
|
1534
|
+
if not np.all(self.muz > 0):
|
|
1535
|
+
raise ValueError('muz must be > 0')
|
|
1536
|
+
|
|
1537
|
+
if wmu is not None:
|
|
1538
|
+
self.wmu = np.array(wmu, dtype=np.float64)
|
|
1539
|
+
|
|
1540
|
+
if not np.isclose(self.wmu.sum(), 1.0):
|
|
1541
|
+
raise ValueError('sum of wmus is not 1.0')
|
|
1542
|
+
|
|
1543
|
+
self.configure_bcs(upOnly=upOnly)
|
|
1544
|
+
|
|
1545
|
+
def configure_bcs(self, upOnly: bool=False):
|
|
1546
|
+
'''
|
|
1547
|
+
Configure the required angular information for all boundary
|
|
1548
|
+
conditions on the model.
|
|
1549
|
+
|
|
1550
|
+
Parameters
|
|
1551
|
+
----------
|
|
1552
|
+
upOnly : bool, optional
|
|
1553
|
+
Whether to only configure boundary conditions for up-going rays.
|
|
1554
|
+
(default: False)
|
|
1555
|
+
'''
|
|
1556
|
+
|
|
1557
|
+
# NOTE(cmo): We always have z-bcs
|
|
1558
|
+
# For zLowerBc, muz is positive, and we have all mux, muz
|
|
1559
|
+
mux, muy, muz = self.mux, self.muy, self.muz
|
|
1560
|
+
# NOTE(cmo): indexVector is of shape (mu, toObs) to allow the core to
|
|
1561
|
+
# easily destructure the blob that will be handed to it from
|
|
1562
|
+
# compute_bc.
|
|
1563
|
+
indexVector = np.ones((self.mux.shape[0], 2), dtype=np.int32) * -1
|
|
1564
|
+
indexVector[:, 1] = np.arange(mux.shape[0])
|
|
1565
|
+
self.zLowerBc.set_required_angles(mux, muy, muz, indexVector)
|
|
1566
|
+
|
|
1567
|
+
indexVector = np.ones((mux.shape[0], 2), dtype=np.int32) * -1
|
|
1568
|
+
if not upOnly:
|
|
1569
|
+
indexVector[:, 0] = np.arange(mux.shape[0])
|
|
1570
|
+
self.zUpperBc.set_required_angles(-mux, -muy, -muz, indexVector)
|
|
1571
|
+
|
|
1572
|
+
toObsRange = [0, 1]
|
|
1573
|
+
if upOnly:
|
|
1574
|
+
toObsRange = [1]
|
|
1575
|
+
|
|
1576
|
+
# NOTE(cmo): If 2+D we have x-bcs too
|
|
1577
|
+
# xLowerBc has all muz and all mux > 0
|
|
1578
|
+
mux, muy, muz = [], [], []
|
|
1579
|
+
indexVector = np.ones((self.mux.shape[0], 2), dtype=np.int32) * -1
|
|
1580
|
+
count = 0
|
|
1581
|
+
musDone = np.zeros(self.muz.shape[0], dtype=np.bool_)
|
|
1582
|
+
for mu in range(self.muz.shape[0]):
|
|
1583
|
+
for equalMu in np.argwhere(np.abs(self.muz) == self.muz[mu]).reshape(-1)[::-1]:
|
|
1584
|
+
if musDone[equalMu]:
|
|
1585
|
+
continue
|
|
1586
|
+
musDone[equalMu] = True
|
|
1587
|
+
|
|
1588
|
+
for toObsI in toObsRange:
|
|
1589
|
+
sign = [-1, 1][toObsI]
|
|
1590
|
+
sMux = sign * self.mux[equalMu]
|
|
1591
|
+
if sMux > 0:
|
|
1592
|
+
mux.append(sMux)
|
|
1593
|
+
muy.append(sign * self.muy[equalMu])
|
|
1594
|
+
muz.append(sign * self.muz[equalMu])
|
|
1595
|
+
indexVector[equalMu, toObsI] = count
|
|
1596
|
+
count += 1
|
|
1597
|
+
if np.all(musDone):
|
|
1598
|
+
break
|
|
1599
|
+
|
|
1600
|
+
mux = np.array(mux)
|
|
1601
|
+
muy = np.array(muy)
|
|
1602
|
+
muz = np.array(muz)
|
|
1603
|
+
self.xLowerBc.set_required_angles(mux, muy, muz, indexVector)
|
|
1604
|
+
|
|
1605
|
+
mux, muy, muz = [], [], []
|
|
1606
|
+
indexVector = np.ones((self.mux.shape[0], 2), dtype=np.int32) * -1
|
|
1607
|
+
count = 0
|
|
1608
|
+
musDone = np.zeros(self.muz.shape[0], dtype=np.bool_)
|
|
1609
|
+
for mu in range(self.muz.shape[0]):
|
|
1610
|
+
for equalMu in np.argwhere(np.abs(self.muz) == self.muz[mu]).reshape(-1):
|
|
1611
|
+
if musDone[equalMu]:
|
|
1612
|
+
continue
|
|
1613
|
+
musDone[equalMu] = True
|
|
1614
|
+
|
|
1615
|
+
for toObsI in toObsRange:
|
|
1616
|
+
sign = [-1, 1][toObsI]
|
|
1617
|
+
sMux = sign * self.mux[equalMu]
|
|
1618
|
+
if sMux < 0:
|
|
1619
|
+
mux.append(sMux)
|
|
1620
|
+
muy.append(sign * self.muy[equalMu])
|
|
1621
|
+
muz.append(sign * self.muz[equalMu])
|
|
1622
|
+
indexVector[equalMu, toObsI] = count
|
|
1623
|
+
count += 1
|
|
1624
|
+
if np.all(musDone):
|
|
1625
|
+
break
|
|
1626
|
+
|
|
1627
|
+
mux = np.array(mux)
|
|
1628
|
+
muy = np.array(muy)
|
|
1629
|
+
muz = np.array(muz)
|
|
1630
|
+
self.xUpperBc.set_required_angles(mux, muy, muz, indexVector)
|
|
1631
|
+
|
|
1632
|
+
self.yLowerBc.set_required_angles(np.zeros((0)), np.zeros((0)), np.zeros((0)),
|
|
1633
|
+
np.ones((self.mux.shape[0], 2),
|
|
1634
|
+
dtype=np.int32) * -1)
|
|
1635
|
+
self.yUpperBc.set_required_angles(np.zeros((0)), np.zeros((0)), np.zeros((0)),
|
|
1636
|
+
np.ones((self.mux.shape[0], 2),
|
|
1637
|
+
dtype=np.int32) * -1)
|
|
1638
|
+
|
|
1639
|
+
if self.Ndim > 2:
|
|
1640
|
+
raise ValueError('Only <= 2D atmospheres supported currently.')
|