rm-tables 0.1.0__py3-none-any.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.
- rm_tables/__init__.py +25 -0
- rm_tables/coverage.py +216 -0
- rm_tables/data/ferguson_f05_g93.npz +0 -0
- rm_tables/data/kR_h2001.dat +722 -0
- rm_tables/data/opal_type1.npz +0 -0
- rm_tables/defaults.py +93 -0
- rm_tables/io.py +265 -0
- rm_tables/lookup.py +323 -0
- rm_tables/sources/__init__.py +9 -0
- rm_tables/sources/_compiled.py +219 -0
- rm_tables/sources/_composition.py +114 -0
- rm_tables/sources/_semenov_fit.py +163 -0
- rm_tables/sources/ferguson.py +154 -0
- rm_tables/sources/opal.py +240 -0
- rm_tables/sources/semenov.py +130 -0
- rm_tables/tables.py +528 -0
- rm_tables-0.1.0.dist-info/METADATA +208 -0
- rm_tables-0.1.0.dist-info/RECORD +21 -0
- rm_tables-0.1.0.dist-info/WHEEL +5 -0
- rm_tables-0.1.0.dist-info/licenses/LICENSE +21 -0
- rm_tables-0.1.0.dist-info/top_level.txt +1 -0
rm_tables/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Rosseland mean opacity for gas and dust, at any composition.
|
|
2
|
+
|
|
3
|
+
Two ways in. `opacity` returns a callable and builds no table: it evaluates the
|
|
4
|
+
sources at the point requested. `build` returns a tabulated grid for anyone who
|
|
5
|
+
wants to fit their own interpolant or write the result to a file.
|
|
6
|
+
|
|
7
|
+
Sources are used as their authors published them. OPAL supplies the ionised gas
|
|
8
|
+
above 5623 K; either Semenov 2003 or Ferguson 2005 supplies dust and molecules
|
|
9
|
+
below. The two cold sources are alternatives and are never combined; the README
|
|
10
|
+
says which to choose. Both entry points take a `dataset` naming the OPAL file,
|
|
11
|
+
which sets the metal mixture, and the excess carbon and oxygen that file
|
|
12
|
+
tabulates.
|
|
13
|
+
|
|
14
|
+
`build` raises `rm_tables.coverage.CoverageError` for a range the chosen sources
|
|
15
|
+
do not hold. A callable from `opacity` raises it for a temperature no source
|
|
16
|
+
reaches, and holds the nearest value it has in density. `CoverageError`
|
|
17
|
+
subclasses `ValueError`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import coverage, defaults
|
|
21
|
+
from .lookup import opacity
|
|
22
|
+
from .tables import build, load
|
|
23
|
+
|
|
24
|
+
__all__ = ["opacity", "build", "load"]
|
|
25
|
+
__version__ = "0.1.0"
|
rm_tables/coverage.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""What each source covers, and the error raised when a request leaves it.
|
|
2
|
+
|
|
3
|
+
Coverage is not always a rectangle. OPAL and Ferguson are tabulated on a grid of
|
|
4
|
+
``log10 T`` and ``log10 R``, so theirs are. Semenov is a function of temperature
|
|
5
|
+
and density, and its gas branch is bounded in density, so its coverage narrows
|
|
6
|
+
with temperature above 1000 K.
|
|
7
|
+
|
|
8
|
+
A request outside coverage raises and names a source that would answer it.
|
|
9
|
+
Nothing here fills, holds or extrapolates.
|
|
10
|
+
"""
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
__all__ = ["Coverage", "CoverageError", "OPAL", "FERGUSON", "SEMENOV",
|
|
14
|
+
"covers", "check_composition"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CoverageError(ValueError):
|
|
18
|
+
"""A requested temperature or density lies outside every chosen source."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Coverage:
|
|
22
|
+
"""The region of ``(log10 T, log10 R)`` one source can answer in.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
name : str
|
|
27
|
+
The source, as it appears in an error message.
|
|
28
|
+
log_T : tuple of float
|
|
29
|
+
Lowest and highest ``log10`` of temperature in K.
|
|
30
|
+
log_R : tuple of float or callable
|
|
31
|
+
Lowest and highest ``log10 R``, where ``R = rho / (T / 1e6)**3`` in
|
|
32
|
+
g/cm^3. A callable takes ``log10 T`` and returns the pair, for a source
|
|
33
|
+
whose density range depends on temperature.
|
|
34
|
+
reference : str
|
|
35
|
+
Author, year and the journal reference, for the provenance record.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, name, log_T, log_R, reference):
|
|
39
|
+
self.name = name
|
|
40
|
+
self.log_T = tuple(log_T)
|
|
41
|
+
self._log_R = log_R
|
|
42
|
+
self.reference = reference
|
|
43
|
+
|
|
44
|
+
def log_R_at(self, log_T):
|
|
45
|
+
"""Lowest and highest ``log10 R`` this source answers at one temperature.
|
|
46
|
+
|
|
47
|
+
Both are NaN at a temperature the source answers at no density.
|
|
48
|
+
"""
|
|
49
|
+
if callable(self._log_R):
|
|
50
|
+
return self._log_R(log_T)
|
|
51
|
+
return self._log_R
|
|
52
|
+
|
|
53
|
+
def covers(self, log_T, log_R):
|
|
54
|
+
"""True where this source has a value. Broadcasts over arrays."""
|
|
55
|
+
log_T = np.asarray(log_T, float)
|
|
56
|
+
log_R = np.asarray(log_R, float)
|
|
57
|
+
inside = (log_T >= self.log_T[0]) & (log_T <= self.log_T[1])
|
|
58
|
+
lo, hi = np.vectorize(self.log_R_at)(log_T)
|
|
59
|
+
return inside & (log_R >= lo) & (log_R <= hi)
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return (f"Coverage({self.name!r}, log_T={self.log_T}, "
|
|
63
|
+
f"log_R={'variable' if callable(self._log_R) else self._log_R})")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _f32(x):
|
|
67
|
+
"""The bound as the stored axis holds it.
|
|
68
|
+
|
|
69
|
+
Both tabulated sources ship their axes in 32-bit floats, so an axis written
|
|
70
|
+
from 2.70 reads back as 2.700000047683716. A bound declared at the nominal
|
|
71
|
+
value claims a sliver the data does not have, and a request landing in that
|
|
72
|
+
sliver passes the coverage check and then finds no value.
|
|
73
|
+
"""
|
|
74
|
+
return float(np.float32(x))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Iglesias & Rogers 1996, ApJ 464, 943. A rectangle, as tabulated.
|
|
78
|
+
OPAL = Coverage("opal", (_f32(3.75), _f32(8.70)), (_f32(-8.0), _f32(1.0)),
|
|
79
|
+
"Iglesias & Rogers 1996, ApJ 464, 943")
|
|
80
|
+
|
|
81
|
+
# Ferguson et al. 2005, ApJ 623, 585. Also a rectangle, on the same density axis.
|
|
82
|
+
FERGUSON = Coverage("ferguson", (_f32(2.70), _f32(4.50)),
|
|
83
|
+
(_f32(-8.0), _f32(1.0)),
|
|
84
|
+
"Ferguson et al. 2005, ApJ 623, 585")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _semenov_log_R(log_T):
|
|
88
|
+
"""Semenov's density range at one temperature, in the table's coordinates.
|
|
89
|
+
|
|
90
|
+
Semenov is bounded in density, not in the density parameter. Its gas grid
|
|
91
|
+
runs 1e-19 to 1e-7 g/cm^3. Since ``R = rho / (T / 1e6)**3``, a constant
|
|
92
|
+
density bound is a straight line of slope -3 in ``log10 R``:
|
|
93
|
+
|
|
94
|
+
log10 T 3.30 3.40 3.70 4.00
|
|
95
|
+
lower -10.90 -11.20 -12.10 -13.00
|
|
96
|
+
upper 1.10 0.80 -0.10 -1.00
|
|
97
|
+
|
|
98
|
+
Below the dust-to-gas transition the polynomial has no density dependence
|
|
99
|
+
and answers at any density. The transition sits 100 K above the interpolated
|
|
100
|
+
evaporation temperature of iron, olivine and orthopyroxene, so it moves with
|
|
101
|
+
density: 1001.21 K in the dilute limit, 1001.29 K at 1e-19 g/cm^3 and
|
|
102
|
+
1514.73 K at 1e-7 g/cm^3. The two bounds therefore begin binding at
|
|
103
|
+
different temperatures, ``log10 T`` of 3.0005 and 3.1803.
|
|
104
|
+
|
|
105
|
+
Past the dense bound the routine returns to the dust polynomial once the
|
|
106
|
+
density is high enough to carry the transition temperature above ``T``, at
|
|
107
|
+
``log10 R`` of 2.08 at ``log10 T = 3.20`` and 4.30 at 3.30. That second
|
|
108
|
+
region is not claimed, so the pair returned is a subset of where the source
|
|
109
|
+
has a value rather than all of it.
|
|
110
|
+
"""
|
|
111
|
+
if log_T > 4.0:
|
|
112
|
+
return (np.nan, np.nan) # above 10,000 K Semenov stops
|
|
113
|
+
lo = -1.0 - 3.0 * log_T if log_T >= 3.0005 else -24.0
|
|
114
|
+
hi = 11.0 - 3.0 * log_T if log_T >= 3.1803 else 4.0
|
|
115
|
+
return (lo, hi)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Semenov et al. 2003, A&A 410, 611. NOT a rectangle.
|
|
119
|
+
SEMENOV = Coverage("semenov", (np.log10(5.0), 4.0), _semenov_log_R,
|
|
120
|
+
"Semenov et al. 2003, A&A 410, 611")
|
|
121
|
+
|
|
122
|
+
_BY_NAME = {c.name: c for c in (OPAL, FERGUSON, SEMENOV)}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def covers(sources, log_T, log_R, what="the requested range"):
|
|
126
|
+
"""Raise unless every point is answered by at least one source.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
sources : sequence of str
|
|
131
|
+
Source names, from ``'opal'``, ``'ferguson'`` and ``'semenov'``.
|
|
132
|
+
log_T, log_R : ndarray
|
|
133
|
+
The grid to check, broadcast against each other.
|
|
134
|
+
what : str, optional
|
|
135
|
+
How the range is described in the error message.
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
CoverageError
|
|
140
|
+
Naming the worst uncovered corner, and any source that would cover it.
|
|
141
|
+
"""
|
|
142
|
+
chosen = [_BY_NAME[s] for s in sources]
|
|
143
|
+
log_T, log_R = np.broadcast_arrays(np.asarray(log_T, float),
|
|
144
|
+
np.asarray(log_R, float))
|
|
145
|
+
ok = np.zeros(log_T.shape, bool)
|
|
146
|
+
for c in chosen:
|
|
147
|
+
ok |= c.covers(log_T, log_R)
|
|
148
|
+
if ok.all():
|
|
149
|
+
return
|
|
150
|
+
i = int(np.argmin(ok.ravel()))
|
|
151
|
+
bad_T, bad_R = log_T.ravel()[i], log_R.ravel()[i]
|
|
152
|
+
rescue = [c.name for c in _BY_NAME.values()
|
|
153
|
+
if c.name not in sources and bool(c.covers(bad_T, bad_R))]
|
|
154
|
+
hint = (f" Add {' or '.join(rescue)} to cover it."
|
|
155
|
+
if rescue else " No source covers it.")
|
|
156
|
+
raise CoverageError(
|
|
157
|
+
f"{what} is not covered by {', '.join(sources)}: "
|
|
158
|
+
f"{int(np.size(ok) - ok.sum())} of {np.size(ok)} points fall outside, "
|
|
159
|
+
f"the first at log10 T = {bad_T:.3f} ({10 ** bad_T:.4g} K), "
|
|
160
|
+
f"log10 R = {bad_R:.3f}.{hint}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_composition(X, Z, dataset):
|
|
164
|
+
"""Refuse a composition the physics or the tables cannot hold.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
X : float
|
|
169
|
+
Hydrogen mass fraction.
|
|
170
|
+
Z : float
|
|
171
|
+
Metal mass fraction.
|
|
172
|
+
dataset : str
|
|
173
|
+
The OPAL set the hot opacity comes from, from
|
|
174
|
+
`rm_tables.sources.opal.sets`.
|
|
175
|
+
|
|
176
|
+
Raises
|
|
177
|
+
------
|
|
178
|
+
CoverageError
|
|
179
|
+
If helium would be negative, if either fraction is negative, or if the
|
|
180
|
+
pair lies outside what the chosen set tabulates. OPAL interpolates
|
|
181
|
+
linearly in hydrogen and in the logarithm of the metallicity, and
|
|
182
|
+
continues that straight line past its own edge, so a request outside
|
|
183
|
+
returns a number with nothing behind it. Hydrogen of 1.5 returns
|
|
184
|
+
5.5e-136 cm^2/g at 1e5 K.
|
|
185
|
+
"""
|
|
186
|
+
from .sources import opal
|
|
187
|
+
X, Z = float(X), float(Z)
|
|
188
|
+
if not (np.isfinite(X) and np.isfinite(Z)):
|
|
189
|
+
raise CoverageError(
|
|
190
|
+
f"mass fractions must be finite, and X={X:g}, Z={Z:g} was "
|
|
191
|
+
f"requested.")
|
|
192
|
+
if X < 0.0 or Z < 0.0:
|
|
193
|
+
raise CoverageError(
|
|
194
|
+
f"mass fractions cannot be negative, and X={X:g}, Z={Z:g} "
|
|
195
|
+
f"was requested.")
|
|
196
|
+
if X + Z > 1.0:
|
|
197
|
+
raise CoverageError(
|
|
198
|
+
f"X={X:g} and Z={Z:g} leave helium at {1.0 - X - Z:g}. "
|
|
199
|
+
f"Helium is the remainder, so X + Z cannot exceed 1.")
|
|
200
|
+
xs, zs = opal.compositions(dataset)
|
|
201
|
+
x_hi, z_hi = float(np.max(xs)), float(np.max(zs))
|
|
202
|
+
x_lo = float(np.min(xs))
|
|
203
|
+
# The tabulated fractions are stored in 32-bit floats, so 0.7 reads back as
|
|
204
|
+
# 0.699999988079071. Compare at that precision or an exactly tabulated
|
|
205
|
+
# value falls outside its own set.
|
|
206
|
+
Xc, Zc = float(np.float32(X)), float(np.float32(Z))
|
|
207
|
+
if not x_lo <= Xc <= x_hi:
|
|
208
|
+
raise CoverageError(
|
|
209
|
+
f"set {dataset!r} tabulates hydrogen from {x_lo:g} to {x_hi:g}, "
|
|
210
|
+
f"and X={X:g} was requested."
|
|
211
|
+
+ (f" That set holds one hydrogen fraction only."
|
|
212
|
+
if x_lo == x_hi else ""))
|
|
213
|
+
if Zc > z_hi:
|
|
214
|
+
raise CoverageError(
|
|
215
|
+
f"set {dataset!r} tabulates metals up to {z_hi:g}, and Z={Z:g} "
|
|
216
|
+
f"was requested. rm_tables.sources.opal.sets() lists the others.")
|
|
Binary file
|