eclipse 0.13.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.
- eclipse/__init__.py +95 -0
- eclipse/analysis.py +853 -0
- eclipse/atmosphere.py +859 -0
- eclipse/band.py +477 -0
- eclipse/cli.py +138 -0
- eclipse/config.py +1710 -0
- eclipse/continuum.py +413 -0
- eclipse/data/eis_response/EIS_EffArea_A.004 +61 -0
- eclipse/data/eis_response/EIS_EffArea_B.004 +60 -0
- eclipse/data/eis_response/EIS_EffArea_B.005 +64 -0
- eclipse/data/eis_response/README_dz2025 +50 -0
- eclipse/data/eis_response/fit_eis_ea_2023-05-04_smooth.sav +0 -0
- eclipse/data/eis_response/source.txt +33 -0
- eclipse/data/science_cases.yaml +365 -0
- eclipse/data/throughput/grating_reflection_efficiency.dat +25 -0
- eclipse/data/throughput/primary_mirror_coating_reflectance.dat +25 -0
- eclipse/data/throughput/source.txt +6 -0
- eclipse/data/throughput/throughput_aluminium_1000_angstrom.dat +503 -0
- eclipse/data/throughput/throughput_aluminium_oxide_1000_angstrom.dat +503 -0
- eclipse/data/throughput/throughput_carbon_1000_angstrom.dat +503 -0
- eclipse/data_processing.py +904 -0
- eclipse/deprecated.py +161 -0
- eclipse/eis_calibration.py +573 -0
- eclipse/extern/__init__.py +0 -0
- eclipse/extern/mpfit.py +2388 -0
- eclipse/fitting.py +1734 -0
- eclipse/frame.py +594 -0
- eclipse/full_frame.py +944 -0
- eclipse/main.py +1646 -0
- eclipse/monte_carlo.py +660 -0
- eclipse/pinhole_diffraction.py +409 -0
- eclipse/psf.py +6 -0
- eclipse/radiometric.py +1328 -0
- eclipse/raster.py +869 -0
- eclipse/readout.py +1002 -0
- eclipse/resolution_budget.py +108 -0
- eclipse/results_file.py +1226 -0
- eclipse/sampling.py +351 -0
- eclipse/science_cases.py +364 -0
- eclipse/synthesis.py +2492 -0
- eclipse/synthesis_cli.py +12 -0
- eclipse/synthesis_file.py +882 -0
- eclipse/utils.py +1145 -0
- eclipse-0.13.0.dist-info/METADATA +70 -0
- eclipse-0.13.0.dist-info/RECORD +50 -0
- eclipse-0.13.0.dist-info/WHEEL +5 -0
- eclipse-0.13.0.dist-info/entry_points.txt +7 -0
- eclipse-0.13.0.dist-info/licenses/LICENSE +3 -0
- eclipse-0.13.0.dist-info/top_level.txt +2 -0
- euvst_response/__init__.py +129 -0
eclipse/continuum.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Continuum under the lines: free-free, free-bound and two-photon emission.
|
|
3
|
+
|
|
4
|
+
Computed from CHIANTI through fiasco for every element, with the same
|
|
5
|
+
abundances and ionisation fractions as the lines. Like a contribution
|
|
6
|
+
function, it multiplies an emission measure of n_e^2 dh, so fiasco's values
|
|
7
|
+
are multiplied by n_H / n_e and divided by 4 pi. Only two-photon emission
|
|
8
|
+
depends on density. In a synthesis file, each group of overlapping windows
|
|
9
|
+
gets one continuum entry, which the instrument run adds to every window it
|
|
10
|
+
reaches.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import astropy.units as u
|
|
20
|
+
|
|
21
|
+
# Name prefix of a continuum entry in a synthesis file, followed by the
|
|
22
|
+
# wavelength range of its window group in Angstrom.
|
|
23
|
+
CONTINUUM_PREFIX = "continuum"
|
|
24
|
+
# Unit of the continuum per emission measure: spectral radiance per
|
|
25
|
+
# n_e^2 dh, with wavelengths in cm, as for ECLIPSE's line spectra.
|
|
26
|
+
EMISSIVITY_UNIT = u.erg * u.cm**3 / (u.s * u.sr * u.cm)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_continuum(name: str) -> bool:
|
|
30
|
+
"""Whether synthesis file entry `name` is the continuum rather than a line."""
|
|
31
|
+
return name.startswith(CONTINUUM_PREFIX + "_")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def continuum_windows(wavelengths: Sequence[u.Quantity]) -> Dict[str, u.Quantity]:
|
|
35
|
+
"""
|
|
36
|
+
Wavelengths of the continuum entries covering a set of spectral windows.
|
|
37
|
+
|
|
38
|
+
Each window wavelength covers the bin halfway to its neighbours. The
|
|
39
|
+
bins of the end wavelengths extend the same distance outward. Windows
|
|
40
|
+
whose bins overlap are grouped. Each group gets one evenly spaced grid,
|
|
41
|
+
at the smallest spacing of its windows, whose bins cover all of theirs.
|
|
42
|
+
Groups do not overlap, so no wavelength is in two entries.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
wavelengths : sequence of u.Quantity
|
|
47
|
+
Wavelengths of each line, increasing, as in `wl_grid` from
|
|
48
|
+
`synthesise_spectra`.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
dict
|
|
53
|
+
Wavelengths in cm for each group, keyed by entry name such as
|
|
54
|
+
`"continuum_194.9248-195.3757"`.
|
|
55
|
+
"""
|
|
56
|
+
windows = []
|
|
57
|
+
for grid in wavelengths:
|
|
58
|
+
values = np.asarray(u.Quantity(grid).to_value(u.cm), dtype=float)
|
|
59
|
+
if values.size < 2:
|
|
60
|
+
raise ValueError("A spectral window needs two or more wavelengths.")
|
|
61
|
+
spacing = np.diff(values)
|
|
62
|
+
if not np.all(spacing > 0):
|
|
63
|
+
raise ValueError("A spectral window's wavelengths must increase.")
|
|
64
|
+
# The window's bins, from half a spacing before its first wavelength
|
|
65
|
+
# to half a spacing after its last.
|
|
66
|
+
windows.append((float(values[0] - spacing[0] / 2), float(values[-1] + spacing[-1] / 2),
|
|
67
|
+
float(np.min(spacing))))
|
|
68
|
+
|
|
69
|
+
def bins(low, high, step):
|
|
70
|
+
"""Number of bins of width `step` needed to reach from `low` to `high`."""
|
|
71
|
+
return int(np.ceil((high - low) / step * (1 - 1e-12)))
|
|
72
|
+
|
|
73
|
+
windows.sort()
|
|
74
|
+
groups: List[List[float]] = []
|
|
75
|
+
for low, high, step in windows:
|
|
76
|
+
# A group's bins can reach past its windows' last bin, so a window is
|
|
77
|
+
# grouped with it if it begins before the group's bins end.
|
|
78
|
+
if groups:
|
|
79
|
+
first, last, spacing = groups[-1]
|
|
80
|
+
if low < first + bins(first, last, spacing) * spacing:
|
|
81
|
+
groups[-1] = [first, max(last, high), min(spacing, step)]
|
|
82
|
+
continue
|
|
83
|
+
groups.append([low, high, step])
|
|
84
|
+
grids = {}
|
|
85
|
+
for low, high, step in groups:
|
|
86
|
+
# Bins of this spacing from the group's first bin edge to past its last.
|
|
87
|
+
grid = low + (np.arange(bins(low, high, step)) + 0.5) * step
|
|
88
|
+
name = f"{CONTINUUM_PREFIX}_{grid[0] * 1e8:.4f}-{grid[-1] * 1e8:.4f}"
|
|
89
|
+
# Windows far narrower than any line's could round to the same name.
|
|
90
|
+
if name in grids:
|
|
91
|
+
name = f"{name}_{sum(key.startswith(name) for key in grids) + 1}"
|
|
92
|
+
grids[name] = grid * u.cm
|
|
93
|
+
return grids
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _element_continuum(args):
|
|
97
|
+
"""
|
|
98
|
+
One element's continuum emission, for a worker process.
|
|
99
|
+
|
|
100
|
+
Free-free plus free-bound emission, shape `(nT, n_wavelength)`, and
|
|
101
|
+
two-photon emission, shape `(nT, nN, n_wavelength)`. Both are in
|
|
102
|
+
erg cm3 / (s Angstrom), per n_e n_H and over all directions, as fiasco
|
|
103
|
+
gives them.
|
|
104
|
+
"""
|
|
105
|
+
symbol, temperature_K, wavelength_aa, densities_cm3, abundance, dbase_root = args
|
|
106
|
+
import logging
|
|
107
|
+
|
|
108
|
+
import fiasco
|
|
109
|
+
from fiasco.util.exceptions import MissingDatasetException
|
|
110
|
+
|
|
111
|
+
# fiasco skips and logs each ion lacking data for a continuum; collect
|
|
112
|
+
# them to report once, rather than a line each.
|
|
113
|
+
left_out = []
|
|
114
|
+
|
|
115
|
+
class _LeftOut(logging.Handler):
|
|
116
|
+
def emit(self, record):
|
|
117
|
+
message = record.getMessage()
|
|
118
|
+
if " not included in " in message:
|
|
119
|
+
ion, rest = message.split(" not included in ", 1)
|
|
120
|
+
# An ion with no electrons has no bound states, and so no
|
|
121
|
+
# free-bound emission to leave out.
|
|
122
|
+
if ion.split()[-1] == str(atomic_number + 1):
|
|
123
|
+
return
|
|
124
|
+
left_out.append(f"{ion} ({rest.split(' emission')[0]})")
|
|
125
|
+
|
|
126
|
+
from .utils import element_data
|
|
127
|
+
|
|
128
|
+
atomic_number, _ = element_data(symbol)
|
|
129
|
+
logger = logging.getLogger("fiasco")
|
|
130
|
+
handler, propagate = _LeftOut(), logger.propagate
|
|
131
|
+
logger.addHandler(handler)
|
|
132
|
+
logger.propagate = False
|
|
133
|
+
single_ion = []
|
|
134
|
+
try:
|
|
135
|
+
kwargs = {} if dbase_root is None else {"hdf5_dbase_root": dbase_root}
|
|
136
|
+
element = fiasco.Element(symbol, temperature_K * u.K, abundance=abundance, **kwargs)
|
|
137
|
+
wavelength = wavelength_aa * u.AA
|
|
138
|
+
unit = u.erg * u.cm**3 / (u.s * u.AA)
|
|
139
|
+
free = (element.free_free(wavelength) + element.free_bound(wavelength)).to_value(unit)
|
|
140
|
+
density = densities_cm3 * u.cm**-3
|
|
141
|
+
two_photon = np.zeros((temperature_K.size, densities_cm3.size, wavelength_aa.size))
|
|
142
|
+
for ion in element:
|
|
143
|
+
# Only hydrogen- and helium-like ions give two-photon emission.
|
|
144
|
+
if not (ion.hydrogenic or ion.helium_like):
|
|
145
|
+
continue
|
|
146
|
+
try:
|
|
147
|
+
emission = _two_photon(ion, wavelength, density, unit, single_ion, kwargs,
|
|
148
|
+
abundance)
|
|
149
|
+
except MissingDatasetException:
|
|
150
|
+
left_out.append(f"{ion.ion_name} (two-photon)")
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
abundance_of = u.Quantity(ion.abundance).to_value(u.dimensionless_unscaled)
|
|
154
|
+
except MissingDatasetException:
|
|
155
|
+
# An element missing from the abundance set has zero abundance.
|
|
156
|
+
continue
|
|
157
|
+
share = (abundance_of
|
|
158
|
+
* u.Quantity(ion.ionization_fraction).to_value(u.dimensionless_unscaled))
|
|
159
|
+
two_photon += emission * share[:, np.newaxis, np.newaxis]
|
|
160
|
+
finally:
|
|
161
|
+
logger.removeHandler(handler)
|
|
162
|
+
logger.propagate = propagate
|
|
163
|
+
return (symbol, free, two_photon, str(element[0].hdf5_dbase_root), left_out,
|
|
164
|
+
single_ion)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _beyond_the_rates(error: ValueError) -> bool:
|
|
168
|
+
"""Whether fiasco stopped at a temperature beyond the rates CHIANTI tabulates."""
|
|
169
|
+
return "interpolation range" in str(error)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _two_photon(ion, wavelength, density, unit, single_ion: list, ion_kwargs: dict,
|
|
173
|
+
abundance: str) -> np.ndarray:
|
|
174
|
+
"""
|
|
175
|
+
One ion's two-photon emission, shape (nT, nN, n_wavelength), in `unit`.
|
|
176
|
+
|
|
177
|
+
fiasco is called one density at a time, since version 0.8 fails to
|
|
178
|
+
arrange its result for several. Its level populations use level-resolved
|
|
179
|
+
recombination and ionisation rates, which for some ions CHIANTI
|
|
180
|
+
tabulates only up to some temperature. Above that, fiasco's single-ion
|
|
181
|
+
model is used, as for ions with no such rates at all, and the ion and
|
|
182
|
+
temperatures are added to `single_ion`.
|
|
183
|
+
"""
|
|
184
|
+
import fiasco
|
|
185
|
+
|
|
186
|
+
def at(one_density, ions):
|
|
187
|
+
parts = []
|
|
188
|
+
for part, single in ions:
|
|
189
|
+
kwargs = {"use_two_ion_model": False} if single else {}
|
|
190
|
+
parts.append(part.two_photon(wavelength, one_density, **kwargs).to_value(unit)[:, 0])
|
|
191
|
+
return np.concatenate(parts)
|
|
192
|
+
|
|
193
|
+
ions = [(ion, False)]
|
|
194
|
+
columns = []
|
|
195
|
+
for one_density in density:
|
|
196
|
+
one_density = one_density[np.newaxis]
|
|
197
|
+
try:
|
|
198
|
+
columns.append(at(one_density, ions))
|
|
199
|
+
continue
|
|
200
|
+
except ValueError as error:
|
|
201
|
+
if not _beyond_the_rates(error) or len(ions) > 1:
|
|
202
|
+
raise
|
|
203
|
+
# First temperature beyond the rates, found once.
|
|
204
|
+
first = None
|
|
205
|
+
for index, temperature in enumerate(ion.temperature):
|
|
206
|
+
one = fiasco.Ion(ion.ion_name, temperature[np.newaxis], abundance=abundance,
|
|
207
|
+
**ion_kwargs)
|
|
208
|
+
try:
|
|
209
|
+
one.two_photon(wavelength, one_density)
|
|
210
|
+
except ValueError as error:
|
|
211
|
+
if not _beyond_the_rates(error):
|
|
212
|
+
raise
|
|
213
|
+
first = index
|
|
214
|
+
break
|
|
215
|
+
if first is None:
|
|
216
|
+
raise RuntimeError(f"fiasco could not work out the two-photon emission of "
|
|
217
|
+
f"{ion.ion_name} at its temperatures together, but could at "
|
|
218
|
+
f"each on its own.")
|
|
219
|
+
below, above = ion.temperature[:first], ion.temperature[first:]
|
|
220
|
+
ions = ([(fiasco.Ion(ion.ion_name, below, abundance=abundance, **ion_kwargs), False)]
|
|
221
|
+
if below.size else [])
|
|
222
|
+
ions.append((fiasco.Ion(ion.ion_name, above, abundance=abundance, **ion_kwargs), True))
|
|
223
|
+
single_ion.append(f"{ion.ion_name} from log T "
|
|
224
|
+
f"{np.log10(above[0].to_value(u.K)):.2f}")
|
|
225
|
+
columns.append(at(one_density, ions))
|
|
226
|
+
return np.stack(columns, axis=1)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def compute_continuum_fiasco(
|
|
230
|
+
wavelength: u.Quantity,
|
|
231
|
+
logT_grid: np.ndarray,
|
|
232
|
+
logN_grid: np.ndarray,
|
|
233
|
+
abundance: str = "sun_coronal_2021_chianti",
|
|
234
|
+
n_workers: int = 0,
|
|
235
|
+
hdf5_dbase_root=None,
|
|
236
|
+
elements: Optional[Sequence[str]] = None,
|
|
237
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
238
|
+
"""
|
|
239
|
+
Continuum per emission measure, from CHIANTI through fiasco.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
wavelength : u.Quantity
|
|
244
|
+
Wavelengths to compute it at.
|
|
245
|
+
logT_grid : np.ndarray
|
|
246
|
+
Temperatures as log10(T / K), as for the lines' contribution
|
|
247
|
+
functions.
|
|
248
|
+
logN_grid : np.ndarray
|
|
249
|
+
Densities as log10(n_e / cm-3), as for the lines' contribution
|
|
250
|
+
functions, for the two-photon emission.
|
|
251
|
+
abundance : str, optional
|
|
252
|
+
CHIANTI abundance set, as for the lines. Default
|
|
253
|
+
`"sun_coronal_2021_chianti"`.
|
|
254
|
+
n_workers : int, optional
|
|
255
|
+
Number of processes for the elements. Default 0 uses every CPU
|
|
256
|
+
available to this process, up to one per element. The workers are
|
|
257
|
+
spawned, so a calling script needs an `if __name__ == "__main__":`
|
|
258
|
+
guard.
|
|
259
|
+
hdf5_dbase_root : str or Path, optional
|
|
260
|
+
CHIANTI database to read. Default fiasco's own.
|
|
261
|
+
elements : sequence of str, optional
|
|
262
|
+
Elements by symbol. Default every element in CHIANTI.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
free : np.ndarray
|
|
267
|
+
Free-free and free-bound emission, shape (nT, n_wavelength), in
|
|
268
|
+
erg cm3 / (s sr cm): spectral radiance per n_e^2 dh.
|
|
269
|
+
two_photon : np.ndarray
|
|
270
|
+
Two-photon emission, shape (nN, nT, n_wavelength), in the same unit,
|
|
271
|
+
on the `logN_grid` densities, like a line's `g_tn`.
|
|
272
|
+
"""
|
|
273
|
+
import fiasco
|
|
274
|
+
|
|
275
|
+
from .atmosphere import _offer_database_build
|
|
276
|
+
|
|
277
|
+
dbase_root = None if hdf5_dbase_root is None else str(hdf5_dbase_root)
|
|
278
|
+
_offer_database_build(dbase_root)
|
|
279
|
+
temperature_K = 10.0 ** np.asarray(logT_grid, dtype=float)
|
|
280
|
+
densities_cm3 = 10.0 ** np.asarray(logN_grid, dtype=float)
|
|
281
|
+
wavelength_aa = np.atleast_1d(u.Quantity(wavelength).to_value(u.AA))
|
|
282
|
+
if elements is None:
|
|
283
|
+
elements = fiasco.list_elements(dbase_root)
|
|
284
|
+
worker_args = [(symbol, temperature_K, wavelength_aa, densities_cm3, abundance, dbase_root)
|
|
285
|
+
for symbol in elements]
|
|
286
|
+
|
|
287
|
+
if n_workers <= 0:
|
|
288
|
+
n_workers = (len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity")
|
|
289
|
+
else os.cpu_count() or 1)
|
|
290
|
+
if n_workers > 1 and len(worker_args) > 1:
|
|
291
|
+
import multiprocessing as mp
|
|
292
|
+
|
|
293
|
+
with mp.get_context("spawn").Pool(min(n_workers, len(worker_args))) as pool:
|
|
294
|
+
results = pool.map(_element_continuum, worker_args)
|
|
295
|
+
else:
|
|
296
|
+
results = [_element_continuum(args) for args in worker_args]
|
|
297
|
+
|
|
298
|
+
free = np.zeros((temperature_K.size, wavelength_aa.size))
|
|
299
|
+
two_photon = np.zeros((temperature_K.size, densities_cm3.size, wavelength_aa.size))
|
|
300
|
+
left_out = {ion for *_, ions, _ in results for ion in ions}
|
|
301
|
+
single_ion = [entry for *_, entries in results for entry in entries]
|
|
302
|
+
if single_ion:
|
|
303
|
+
print(f" CHIANTI's recombination and ionisation rates of each level stop short of "
|
|
304
|
+
f"the hottest temperatures for {len(single_ion)} ions, whose two-photon "
|
|
305
|
+
f"emission there is worked out with fiasco's single-ion model, as for ions "
|
|
306
|
+
f"that have no such rates: {', '.join(single_ion)}")
|
|
307
|
+
for kind in ("free-bound", "two-photon"):
|
|
308
|
+
ions = [entry.rsplit(" (", 1)[0] for entry in left_out if entry.endswith(f"({kind})")]
|
|
309
|
+
if ions:
|
|
310
|
+
print(f" CHIANTI has no {kind} data for {len(ions)} ions, which are left out of "
|
|
311
|
+
f"that part of the continuum: {_ion_list(ions)}")
|
|
312
|
+
for symbol, element_free, element_two_photon, used, _, _ in results:
|
|
313
|
+
if dbase_root is not None and used != dbase_root:
|
|
314
|
+
raise RuntimeError(
|
|
315
|
+
f"The continuum of {symbol} was worked out from the CHIANTI database at "
|
|
316
|
+
f"{used} instead of the requested {dbase_root}.")
|
|
317
|
+
if not (np.all(np.isfinite(element_free)) and np.all(np.isfinite(element_two_photon))):
|
|
318
|
+
raise RuntimeError(f"fiasco gave a continuum for {symbol} that is not finite "
|
|
319
|
+
f"everywhere.")
|
|
320
|
+
free += element_free
|
|
321
|
+
two_photon += element_two_photon
|
|
322
|
+
|
|
323
|
+
# Convert fiasco's continuum, per n_e n_H, over all directions and per
|
|
324
|
+
# Angstrom, to per n_e^2, per steradian and per cm.
|
|
325
|
+
kwargs = {} if dbase_root is None else {"hdf5_dbase_root": dbase_root}
|
|
326
|
+
hydrogen = fiasco.Ion("H 1", temperature_K * u.K, abundance=abundance, **kwargs)
|
|
327
|
+
ratio = np.asarray(hydrogen.proton_electron_ratio.to_value(u.dimensionless_unscaled))
|
|
328
|
+
per_cm = (1.0 / u.AA).to_value(1.0 / u.cm)
|
|
329
|
+
scale = ratio / (4.0 * np.pi) * per_cm
|
|
330
|
+
free = free * scale[:, np.newaxis]
|
|
331
|
+
two_photon = np.moveaxis(two_photon * scale[:, np.newaxis, np.newaxis], 1, 0)
|
|
332
|
+
return free, two_photon
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def continuum_spectra(
|
|
336
|
+
emission_measure: np.ndarray,
|
|
337
|
+
electron_density: np.ndarray,
|
|
338
|
+
logN_grid: np.ndarray,
|
|
339
|
+
free: np.ndarray,
|
|
340
|
+
two_photon: np.ndarray,
|
|
341
|
+
) -> np.ndarray:
|
|
342
|
+
"""
|
|
343
|
+
Continuum spectral radiance in every pixel.
|
|
344
|
+
|
|
345
|
+
Parameters
|
|
346
|
+
----------
|
|
347
|
+
emission_measure : np.ndarray
|
|
348
|
+
Emission measure n_e^2 dh in each temperature bin of each pixel, in
|
|
349
|
+
cm^-5, shape (rows, columns, nT): a DEM times its bin widths.
|
|
350
|
+
ECLIPSE's synthesis uses its own DEM, which, unlike `em_tv`, keeps
|
|
351
|
+
plasma moving faster than the velocity grid reaches.
|
|
352
|
+
electron_density : np.ndarray or float
|
|
353
|
+
Electron density in cm^-3 at each pixel and temperature, same shape
|
|
354
|
+
as `emission_measure`, or one density for all. ECLIPSE's own
|
|
355
|
+
synthesis gives the mean weighted by emission measure.
|
|
356
|
+
logN_grid : np.ndarray
|
|
357
|
+
Densities `two_photon` is tabulated on, as log10(n_e / cm-3).
|
|
358
|
+
free, two_photon : np.ndarray
|
|
359
|
+
From `compute_continuum_fiasco`, on the temperatures of the emission
|
|
360
|
+
measure.
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
np.ndarray
|
|
365
|
+
Spectral radiance, shape (rows, columns, n_wavelength), in
|
|
366
|
+
erg / (s cm2 sr cm). Two-photon emission is interpolated linearly in
|
|
367
|
+
log10 n_e and is zero at densities off the grid, as for the lines'
|
|
368
|
+
contribution functions.
|
|
369
|
+
"""
|
|
370
|
+
em = np.asarray(emission_measure, dtype=float)
|
|
371
|
+
rows, columns, n_t = em.shape
|
|
372
|
+
if free.shape[0] != n_t or two_photon.shape[1] != n_t:
|
|
373
|
+
raise ValueError(f"The emission measure has {n_t} temperatures, but the continuum "
|
|
374
|
+
f"has {free.shape[0]}.")
|
|
375
|
+
density = np.broadcast_to(np.asarray(electron_density, dtype=float), em.shape)
|
|
376
|
+
flat = em.reshape(-1, n_t)
|
|
377
|
+
spectra = flat @ free
|
|
378
|
+
log_n = np.log10(np.where(density > 0, density, np.nan)).reshape(-1, n_t)
|
|
379
|
+
grid = np.asarray(logN_grid, dtype=float)
|
|
380
|
+
for t in range(n_t):
|
|
381
|
+
weight = flat[:, t]
|
|
382
|
+
lit = (weight > 0) & np.isfinite(log_n[:, t])
|
|
383
|
+
lit &= (log_n[:, t] >= grid[0]) & (log_n[:, t] <= grid[-1])
|
|
384
|
+
if not lit.any():
|
|
385
|
+
continue
|
|
386
|
+
position = np.interp(log_n[lit, t], grid, np.arange(grid.size, dtype=float))
|
|
387
|
+
lower = np.floor(position).astype(int)
|
|
388
|
+
upper = np.minimum(lower + 1, grid.size - 1)
|
|
389
|
+
fraction = position - lower
|
|
390
|
+
table = two_photon[:, t, :]
|
|
391
|
+
spectra[lit] += weight[lit, np.newaxis] * (
|
|
392
|
+
(1.0 - fraction)[:, np.newaxis] * table[lower] + fraction[:, np.newaxis] * table[upper])
|
|
393
|
+
return spectra.reshape(rows, columns, -1)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _ion_list(ions: Sequence[str]) -> str:
|
|
397
|
+
"""Ions such as `["Fe 1", "Fe 2", "Fe 27"]` written as `"Fe 1-2, 27"`, element by element."""
|
|
398
|
+
stages: Dict[str, List[int]] = {}
|
|
399
|
+
for ion in ions:
|
|
400
|
+
symbol, stage = ion.split()
|
|
401
|
+
stages.setdefault(symbol, []).append(int(stage))
|
|
402
|
+
parts = []
|
|
403
|
+
from .utils import element_data
|
|
404
|
+
|
|
405
|
+
for symbol in sorted(stages, key=lambda s: element_data(s)[0]):
|
|
406
|
+
numbers = sorted(set(stages[symbol]))
|
|
407
|
+
runs, start = [], numbers[0]
|
|
408
|
+
for previous, number in zip(numbers, numbers[1:] + [None]):
|
|
409
|
+
if number is None or number != previous + 1:
|
|
410
|
+
runs.append(str(start) if start == previous else f"{start}-{previous}")
|
|
411
|
+
start = number
|
|
412
|
+
parts.append(f"{symbol} {', '.join(runs)}")
|
|
413
|
+
return "; ".join(parts)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# EIS_EFFArea_A.004
|
|
2
|
+
# EIS Effective Area
|
|
3
|
+
# Mon Apr 23 10:40:45 2007
|
|
4
|
+
# Front filter data file: EIS_FFA_A_Trans.003
|
|
5
|
+
# SEF data file: EIS_SEFTrans.002
|
|
6
|
+
# Mirror data file: EIS_MirrorRefl_A.003
|
|
7
|
+
# Grating data file: EIS_GratingRefl_A.003
|
|
8
|
+
# Vignetting data file: EIS_vignetting.001
|
|
9
|
+
# Mirror area: 88.4
|
|
10
|
+
# Groove efficiency: 1.00
|
|
11
|
+
# CCD qe: 0.64
|
|
12
|
+
# Spider trans: 0.80
|
|
13
|
+
# Mesh trans: 1.00
|
|
14
|
+
245.000 0.0226728
|
|
15
|
+
246.000 0.0243236
|
|
16
|
+
247.000 0.0262798
|
|
17
|
+
248.000 0.0281824
|
|
18
|
+
249.000 0.0296339
|
|
19
|
+
250.000 0.0315844
|
|
20
|
+
251.000 0.0351500
|
|
21
|
+
252.000 0.0390812
|
|
22
|
+
253.000 0.0428331
|
|
23
|
+
254.000 0.0466468
|
|
24
|
+
255.000 0.0506535
|
|
25
|
+
256.000 0.0546878
|
|
26
|
+
257.000 0.0587970
|
|
27
|
+
258.000 0.0630460
|
|
28
|
+
259.000 0.0673784
|
|
29
|
+
260.000 0.0718699
|
|
30
|
+
261.000 0.0765660
|
|
31
|
+
262.000 0.0813246
|
|
32
|
+
263.000 0.0860998
|
|
33
|
+
264.000 0.0909178
|
|
34
|
+
265.000 0.0955089
|
|
35
|
+
266.000 0.0998424
|
|
36
|
+
267.000 0.103622
|
|
37
|
+
268.000 0.106984
|
|
38
|
+
269.000 0.109509
|
|
39
|
+
270.000 0.110764
|
|
40
|
+
271.000 0.110875
|
|
41
|
+
272.000 0.109445
|
|
42
|
+
273.000 0.106640
|
|
43
|
+
274.000 0.102602
|
|
44
|
+
275.000 0.0974887
|
|
45
|
+
276.000 0.0914836
|
|
46
|
+
277.000 0.0847751
|
|
47
|
+
278.000 0.0777747
|
|
48
|
+
279.000 0.0705505
|
|
49
|
+
280.000 0.0635321
|
|
50
|
+
281.000 0.0571789
|
|
51
|
+
282.000 0.0513891
|
|
52
|
+
283.000 0.0460830
|
|
53
|
+
284.000 0.0413185
|
|
54
|
+
285.000 0.0371190
|
|
55
|
+
286.000 0.0333321
|
|
56
|
+
287.000 0.0298378
|
|
57
|
+
288.000 0.0266719
|
|
58
|
+
289.000 0.0238613
|
|
59
|
+
290.000 0.0213016
|
|
60
|
+
291.000 0.0189506
|
|
61
|
+
292.000 0.0167953
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# EIS_EFFArea_B.004
|
|
2
|
+
# EIS Effective Area
|
|
3
|
+
# Mon Apr 23 10:40:45 2007
|
|
4
|
+
# Front filter data file: EIS_FFA_B_Trans.003
|
|
5
|
+
# SEF data file: EIS_SEFTrans.002
|
|
6
|
+
# Mirror data file: EIS_MirrorRefl_B.003
|
|
7
|
+
# Grating data file: EIS_GratingRefl_B.003
|
|
8
|
+
# Mirror area: 88.4
|
|
9
|
+
# Groove efficiency: 1.00
|
|
10
|
+
# CCD qe: 0.64
|
|
11
|
+
# Spider trans: 0.80
|
|
12
|
+
# Mesh trans: 1.00
|
|
13
|
+
165.000 0.000174973
|
|
14
|
+
166.000 0.000232704
|
|
15
|
+
167.000 0.000248308
|
|
16
|
+
168.000 0.000241170
|
|
17
|
+
169.000 0.000265181
|
|
18
|
+
170.000 0.000379874
|
|
19
|
+
171.000 0.000255772
|
|
20
|
+
172.000 0.000621409
|
|
21
|
+
173.000 0.00109901
|
|
22
|
+
174.000 0.00140740
|
|
23
|
+
175.000 0.00184703
|
|
24
|
+
176.000 0.00275733
|
|
25
|
+
177.000 0.00436359
|
|
26
|
+
178.000 0.00677930
|
|
27
|
+
179.000 0.0100962
|
|
28
|
+
180.000 0.0146472
|
|
29
|
+
181.000 0.0209196
|
|
30
|
+
182.000 0.0296101
|
|
31
|
+
183.000 0.0412875
|
|
32
|
+
184.000 0.0562303
|
|
33
|
+
185.000 0.0740649
|
|
34
|
+
186.000 0.0945498
|
|
35
|
+
187.000 0.117659
|
|
36
|
+
188.000 0.142528
|
|
37
|
+
189.000 0.168455
|
|
38
|
+
190.000 0.194897
|
|
39
|
+
191.000 0.221390
|
|
40
|
+
192.000 0.246519
|
|
41
|
+
193.000 0.269192
|
|
42
|
+
194.000 0.288602
|
|
43
|
+
195.000 0.301803
|
|
44
|
+
196.000 0.305428
|
|
45
|
+
197.000 0.295828
|
|
46
|
+
198.000 0.269425
|
|
47
|
+
199.000 0.226404
|
|
48
|
+
200.000 0.174608
|
|
49
|
+
201.000 0.124217
|
|
50
|
+
202.000 0.0838537
|
|
51
|
+
203.000 0.0570717
|
|
52
|
+
204.000 0.0416032
|
|
53
|
+
205.000 0.0325555
|
|
54
|
+
206.000 0.0267497
|
|
55
|
+
207.000 0.0224701
|
|
56
|
+
208.000 0.0189209
|
|
57
|
+
209.000 0.0157723
|
|
58
|
+
210.000 0.0131231
|
|
59
|
+
211.000 0.0110494
|
|
60
|
+
212.000 0.00951424
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# EIS_EFFArea_B.005
|
|
2
|
+
# EIS Effective Area
|
|
3
|
+
# Mon Apr 23 10:40:45 2007
|
|
4
|
+
# Front filter data file: EIS_FFA_B_Trans.003
|
|
5
|
+
# SEF data file: EIS_SEFTrans.002
|
|
6
|
+
# Mirror data file: EIS_MirrorRefl_B.003
|
|
7
|
+
# Grating data file: EIS_GratingRefl_B.003
|
|
8
|
+
# Mirror area: 88.4
|
|
9
|
+
# Groove efficiency: 1.00
|
|
10
|
+
# CCD qe: 0.64
|
|
11
|
+
# Spider trans: 0.80
|
|
12
|
+
# Mesh trans: 1.00
|
|
13
|
+
# Thu Apr 14 08:08:21 EDT 2011
|
|
14
|
+
# Extrapolated [linear extrapolation in log(y)]
|
|
15
|
+
# one new point at 213 Angstroms (JTM)
|
|
16
|
+
165.000 0.000174973
|
|
17
|
+
166.000 0.000232704
|
|
18
|
+
167.000 0.000248308
|
|
19
|
+
168.000 0.000241170
|
|
20
|
+
169.000 0.000265181
|
|
21
|
+
170.000 0.000379874
|
|
22
|
+
171.000 0.000255772
|
|
23
|
+
172.000 0.000621409
|
|
24
|
+
173.000 0.00109901
|
|
25
|
+
174.000 0.00140740
|
|
26
|
+
175.000 0.00184703
|
|
27
|
+
176.000 0.00275733
|
|
28
|
+
177.000 0.00436359
|
|
29
|
+
178.000 0.00677930
|
|
30
|
+
179.000 0.0100962
|
|
31
|
+
180.000 0.0146472
|
|
32
|
+
181.000 0.0209196
|
|
33
|
+
182.000 0.0296101
|
|
34
|
+
183.000 0.0412875
|
|
35
|
+
184.000 0.0562303
|
|
36
|
+
185.000 0.0740649
|
|
37
|
+
186.000 0.0945498
|
|
38
|
+
187.000 0.117659
|
|
39
|
+
188.000 0.142528
|
|
40
|
+
189.000 0.168455
|
|
41
|
+
190.000 0.194897
|
|
42
|
+
191.000 0.221390
|
|
43
|
+
192.000 0.246519
|
|
44
|
+
193.000 0.269192
|
|
45
|
+
194.000 0.288602
|
|
46
|
+
195.000 0.301803
|
|
47
|
+
196.000 0.305428
|
|
48
|
+
197.000 0.295828
|
|
49
|
+
198.000 0.269425
|
|
50
|
+
199.000 0.226404
|
|
51
|
+
200.000 0.174608
|
|
52
|
+
201.000 0.124217
|
|
53
|
+
202.000 0.0838537
|
|
54
|
+
203.000 0.0570717
|
|
55
|
+
204.000 0.0416032
|
|
56
|
+
205.000 0.0325555
|
|
57
|
+
206.000 0.0267497
|
|
58
|
+
207.000 0.0224701
|
|
59
|
+
208.000 0.0189209
|
|
60
|
+
209.000 0.0157723
|
|
61
|
+
210.000 0.0131231
|
|
62
|
+
211.000 0.0110494
|
|
63
|
+
212.000 0.00951424
|
|
64
|
+
213.000 0.00819236
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
The files are associated with the paper
|
|
3
|
+
describing the in-flight radiometric calibration of the
|
|
4
|
+
Hinode EIS instrument
|
|
5
|
+
|
|
6
|
+
see doi: 10.48550/arXiv.2308.06609
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
The IDL save files contain a structure RETURN_STR with the results of the
|
|
11
|
+
fits of the single spectra, merged.
|
|
12
|
+
|
|
13
|
+
The main array is return_str.total_counts which contains the
|
|
14
|
+
total counts in the lines for all the dates, in units of
|
|
15
|
+
DN/s/1arc sec
|
|
16
|
+
|
|
17
|
+
The array return_str.list_lambda has the expected wavelengths of the lines.
|
|
18
|
+
|
|
19
|
+
The arrays in data_active_sfit_2023.save
|
|
20
|
+
return_str.eis_sw_ar_spectra and return_str.eis_lw_ar_spectra
|
|
21
|
+
contain the active region averaged spectra.
|
|
22
|
+
|
|
23
|
+
The arrays in data_quiet_sfit_2023.save
|
|
24
|
+
return_str.eis_sw_qs_spectra and return_str.eis_lw_qs_spectra
|
|
25
|
+
contain the quiet Sun averaged spectra.
|
|
26
|
+
|
|
27
|
+
The wavelengths can be obtained with
|
|
28
|
+
eis_pixel_to_wavelength(indgen(2048),1,2)
|
|
29
|
+
and
|
|
30
|
+
eis_pixel_to_wavelength(indgen(2048),2,2)
|
|
31
|
+
for the 2 arcsecond slit.
|
|
32
|
+
|
|
33
|
+
Note that the spectra have not been wavelength calibrated.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
They are the result of the processing of a large number of EIS data
|
|
37
|
+
for the in-flight calibration from 2007 to 2022.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
The routine used to process Hinode EIS full spectral raster atlases
|
|
42
|
+
is process_atlas.pro
|
|
43
|
+
|
|
44
|
+
The routine interpol_eis_ea.pro is to be used together with
|
|
45
|
+
the save file fit_eis_ea_2023-05-04_smooth.sav to obtain the
|
|
46
|
+
time and wavelength-dependent effective area.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
written by Giulio Del Zanna (GDZ), December 15 2023
|
|
50
|
+
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Hinode/EIS effective-area data, used by eclipse/eis_calibration.py.
|
|
2
|
+
|
|
3
|
+
EIS_EffArea_A.004 (long-wavelength channel, 245-292 Angstrom)
|
|
4
|
+
EIS_EffArea_B.004 (short-wavelength channel, 165-212 Angstrom)
|
|
5
|
+
EIS_EffArea_B.005 (short-wavelength channel, 165-213 Angstrom)
|
|
6
|
+
|
|
7
|
+
Pre-flight ground-calibration effective areas, produced at MSSL in
|
|
8
|
+
April 2007. Copied verbatim from SolarSoft:
|
|
9
|
+
$SSW/hinode/eis/response/. IDL eis_ea.pro reads the latest of each,
|
|
10
|
+
A.004 and B.005, which is B.004 with a point at 213 Angstrom added;
|
|
11
|
+
eis_ltds.pro names A.004 and B.004. Column 1 is wavelength in Angstrom,
|
|
12
|
+
column 2 is effective area in cm^2 with the CCD quantum efficiency
|
|
13
|
+
already included.
|
|
14
|
+
|
|
15
|
+
Lang, J. et al. 2006, Applied Optics, 45, 8689
|
|
16
|
+
EIS Software Note 2, "Radiometric Calibration".
|
|
17
|
+
|
|
18
|
+
fit_eis_ea_2023-05-04_smooth.sav
|
|
19
|
+
|
|
20
|
+
Fitted time- and wavelength-dependent effective areas from the Del
|
|
21
|
+
Zanna et al. (2025) in-flight radiometric calibration, covering 2007
|
|
22
|
+
to 2022. interpol_eis_ea.pro reads the unsmoothed
|
|
23
|
+
fit_eis_ea_2023-05-04.sav unless given another; this is the smoothed
|
|
24
|
+
one, which the accompanying README_dz2025 (kept alongside it here,
|
|
25
|
+
verbatim) says to use. Distributed by Giulio Del Zanna.
|
|
26
|
+
|
|
27
|
+
doi: 10.48550/arXiv.2308.06609
|
|
28
|
+
|
|
29
|
+
The Del Zanna (2013) spline points and the Warren et al. (2014) exponential
|
|
30
|
+
decay coefficients are small enough to live in the source rather than in a
|
|
31
|
+
data file; their provenance is recorded in eis_calibration.py.
|
|
32
|
+
|
|
33
|
+
13/08/2026, B.005 added 28/09/2026
|