lightweaver 0.15.0__cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

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