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/science_cases.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Science cases from the Concept Study Report, and ECLIPSE configurations for them.
|
|
3
|
+
|
|
4
|
+
`data/science_cases.yaml` holds every case with all its lines. The functions
|
|
5
|
+
here turn the lines ECLIPSE can currently simulate into uniform-intensity
|
|
6
|
+
configurations, and the `eclipse-science-cases` command writes them to files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import copy
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
from importlib.resources import files
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import astropy.constants as const
|
|
20
|
+
import astropy.units as u
|
|
21
|
+
import numpy as np
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
from .config import Telescope_EUVST, _load_throughput_table
|
|
25
|
+
from .utils import element_data, load_yaml_config
|
|
26
|
+
|
|
27
|
+
# Defaults for every configuration unless the base settings override them:
|
|
28
|
+
# the settings the cases were run with for the NASA PDR analysis.
|
|
29
|
+
DEFAULT_SETTINGS = {
|
|
30
|
+
"n_iter": 512,
|
|
31
|
+
"ncpu": -1,
|
|
32
|
+
"offchip_bin_slit": [1, 2],
|
|
33
|
+
"simulation": {"psf": True},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Keys that each configuration takes from its line, so the base settings
|
|
37
|
+
# cannot set them.
|
|
38
|
+
_LINE_KEYS = ("instrument", "uniform_intensity", "rest_wavelength",
|
|
39
|
+
"thermal_width", "synthesis_file", "reference_line")
|
|
40
|
+
|
|
41
|
+
# Time-series keys, not allowed in a configuration observing one line's
|
|
42
|
+
# intensity.
|
|
43
|
+
_SERIES_KEYS = ("atmosphere_series", "synthesis_series", "raster", "synthesis")
|
|
44
|
+
|
|
45
|
+
# Two table entries for the same line can be written to different precision,
|
|
46
|
+
# e.g. 195.119 and 195.12, so a wavelength matches lines within this.
|
|
47
|
+
_WAVELENGTH_MATCH = 0.01 * u.AA
|
|
48
|
+
|
|
49
|
+
_ROMAN = {numeral: value for value, numeral in enumerate(
|
|
50
|
+
"I II III IV V VI VII VIII IX X XI XII XIII XIV XV XVI XVII XVIII XIX XX "
|
|
51
|
+
"XXI XXII XXIII XXIV XXV XXVI XXVII XXVIII XXIX XXX".split(), start=1)}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ScienceCaseConfig:
|
|
56
|
+
"""ECLIPSE configuration for one line of one science case."""
|
|
57
|
+
name: str
|
|
58
|
+
case: dict
|
|
59
|
+
line: dict
|
|
60
|
+
config: dict
|
|
61
|
+
|
|
62
|
+
def to_yaml(self) -> str:
|
|
63
|
+
"""Configuration as YAML text, with a header giving its source."""
|
|
64
|
+
header = (
|
|
65
|
+
f"# Science case {self.case['task']} ({self.case['name']}): "
|
|
66
|
+
f"{self.case['description']}\n"
|
|
67
|
+
f"# Line: {line_label(self.line)}, log T_max = {float(self.line['log_t_max']):.2f}, "
|
|
68
|
+
f"intensity {self.line['intensity']} times filling factor "
|
|
69
|
+
f"{self.case['filling_factor']}\n"
|
|
70
|
+
f"# Written by eclipse-science-cases from the science case table\n\n"
|
|
71
|
+
)
|
|
72
|
+
return header + yaml.safe_dump(self.config, sort_keys=False,
|
|
73
|
+
default_flow_style=None)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_science_cases(path: str | Path | None = None) -> list[dict]:
|
|
77
|
+
"""
|
|
78
|
+
Read the science case table.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
path : str or Path, optional
|
|
83
|
+
Table to read instead of the one included with ECLIPSE.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
list of dict
|
|
88
|
+
One entry per case, as written in the table.
|
|
89
|
+
"""
|
|
90
|
+
if path is None:
|
|
91
|
+
source = files("eclipse") / "data" / "science_cases.yaml"
|
|
92
|
+
else:
|
|
93
|
+
source = Path(path)
|
|
94
|
+
return yaml.safe_load(source.read_text())
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def line_label(line: dict) -> str:
|
|
98
|
+
"""A line as written in the table, e.g. `Fe XII 195.119`."""
|
|
99
|
+
return f"{line['ion']} {line['wavelength'].split()[0]}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def line_id(line: dict) -> str:
|
|
103
|
+
"""A line as used in file names, e.g. `fe12_195119`."""
|
|
104
|
+
symbol, numeral = line["ion"].split()
|
|
105
|
+
wavelength = u.Quantity(line["wavelength"]).to_value(u.AA)
|
|
106
|
+
return f"{symbol.lower()}{_ROMAN[numeral]:02d}_{wavelength:.3f}".replace(".", "")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def thermal_width(line: dict) -> u.Quantity:
|
|
110
|
+
"""1-sigma thermal velocity, sqrt(k T / m), at the line's T_max."""
|
|
111
|
+
mass = element_data(line["ion"].split()[0])[1] * const.u
|
|
112
|
+
temperature = 10.0 ** float(line["log_t_max"]) * u.K
|
|
113
|
+
return np.sqrt(const.k_B * temperature / mass).to(u.km / u.s)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@lru_cache(maxsize=None)
|
|
117
|
+
def short_wavelength_range() -> tuple[u.Quantity, u.Quantity]:
|
|
118
|
+
"""
|
|
119
|
+
Wavelength range ECLIPSE can simulate in the short wavelength channel.
|
|
120
|
+
|
|
121
|
+
This is where both the primary mirror and grating efficiency tables have
|
|
122
|
+
values; the effective area is undefined outside them.
|
|
123
|
+
"""
|
|
124
|
+
telescope = Telescope_EUVST()
|
|
125
|
+
tables = [_load_throughput_table(table)[0]
|
|
126
|
+
for table in (telescope.pm_table, telescope.grating_table)]
|
|
127
|
+
return (max(t.min() for t in tables).to(u.AA),
|
|
128
|
+
min(t.max() for t in tables).to(u.AA))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def instrument_for(line: dict) -> str | None:
|
|
132
|
+
"""ECLIPSE instrument that can simulate `line`, or None if there is none yet."""
|
|
133
|
+
low, high = short_wavelength_range()
|
|
134
|
+
return "SWC" if low <= u.Quantity(line["wavelength"]) <= high else None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _line_matches(line: dict, selector: str) -> bool:
|
|
138
|
+
"""Whether `selector`, an ion or an ion and wavelength, matches `line`."""
|
|
139
|
+
tokens = selector.split()
|
|
140
|
+
if len(tokens) not in (2, 3):
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"Choose a line by ion, e.g. 'Fe XII', or by ion and wavelength in "
|
|
143
|
+
f"Angstrom, e.g. 'Fe XII 195.119'; got {selector!r}."
|
|
144
|
+
)
|
|
145
|
+
if line["ion"] != " ".join(tokens[:2]):
|
|
146
|
+
return False
|
|
147
|
+
if len(tokens) == 2:
|
|
148
|
+
return True
|
|
149
|
+
wavelength = u.Quantity(line["wavelength"])
|
|
150
|
+
return abs(wavelength - float(tokens[2]) * u.AA) <= _WAVELENGTH_MATCH
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _select_cases(table: list[dict], selectors: list[str] | None) -> list[dict]:
|
|
154
|
+
if not selectors:
|
|
155
|
+
return table
|
|
156
|
+
unknown = [s for s in selectors
|
|
157
|
+
if not any(s in (case["task"], case["name"]) for case in table)]
|
|
158
|
+
if unknown:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"No science case is called {unknown}. The cases are "
|
|
161
|
+
f"{[case['task'] for case in table]}, or their names, e.g. "
|
|
162
|
+
f"{table[0]['name']!r}."
|
|
163
|
+
)
|
|
164
|
+
return [case for case in table
|
|
165
|
+
if any(s in (case["task"], case["name"]) for s in selectors)]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _chosen_lines(table: list[dict], cases: list[str] | None,
|
|
169
|
+
lines: list[str] | None) -> list[tuple[dict, dict]]:
|
|
170
|
+
"""Chosen `(case, line)` pairs; a line selector that matches none raises ValueError."""
|
|
171
|
+
chosen, matched = [], set()
|
|
172
|
+
for case in _select_cases(table, cases):
|
|
173
|
+
for line in case["lines"]:
|
|
174
|
+
if lines:
|
|
175
|
+
picked = {s for s in lines if _line_matches(line, s)}
|
|
176
|
+
if not picked:
|
|
177
|
+
continue
|
|
178
|
+
matched |= picked
|
|
179
|
+
chosen.append((case, line))
|
|
180
|
+
unmatched = [s for s in (lines or []) if s not in matched]
|
|
181
|
+
if unmatched:
|
|
182
|
+
raise ValueError(f"No line in the chosen cases matches {unmatched}.")
|
|
183
|
+
return chosen
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _config(case: dict, line: dict, instrument: str, base: dict) -> dict:
|
|
187
|
+
"""One line's configuration: defaults, then the case, then the base settings."""
|
|
188
|
+
value, unit = line["intensity"].split(maxsplit=1)
|
|
189
|
+
# Decimal keeps every digit of the table without binary rounding,
|
|
190
|
+
# e.g. 823 x 0.2 is 164.6, not 164.60000000000002.
|
|
191
|
+
intensity = Decimal(value) * Decimal(str(case["filling_factor"]))
|
|
192
|
+
|
|
193
|
+
settings = {**DEFAULT_SETTINGS, **base}
|
|
194
|
+
config = {"instrument": instrument}
|
|
195
|
+
config.update({k: v for k, v in settings.items() if not isinstance(v, dict)})
|
|
196
|
+
config["uniform_intensity"] = f"{intensity.normalize():f} {unit}"
|
|
197
|
+
config["rest_wavelength"] = line["wavelength"]
|
|
198
|
+
config["thermal_width"] = f"{thermal_width(line).to_value(u.km / u.s):.2f} km/s"
|
|
199
|
+
config["simulation"] = {
|
|
200
|
+
**DEFAULT_SETTINGS["simulation"],
|
|
201
|
+
"expos": case["exposure"],
|
|
202
|
+
"slit_width": case["slit_width"],
|
|
203
|
+
**base.get("simulation", {}),
|
|
204
|
+
}
|
|
205
|
+
config.update({k: v for k, v in settings.items()
|
|
206
|
+
if isinstance(v, dict) and k != "simulation"})
|
|
207
|
+
# Deep copy, so no list or section is shared with the defaults or the
|
|
208
|
+
# other configurations.
|
|
209
|
+
return copy.deepcopy(config)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def science_case_configs(cases: list[str] | None = None,
|
|
213
|
+
lines: list[str] | None = None,
|
|
214
|
+
base: dict | None = None,
|
|
215
|
+
table: list[dict] | None = None,
|
|
216
|
+
) -> tuple[list[ScienceCaseConfig], list[str]]:
|
|
217
|
+
"""
|
|
218
|
+
ECLIPSE configurations for the science case lines ECLIPSE can simulate.
|
|
219
|
+
|
|
220
|
+
Each uses uniform intensity mode: the line's intensity times the case's
|
|
221
|
+
filling factor, the thermal width at the line's T_max, and the case's
|
|
222
|
+
exposure time and slit width.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
cases : list of str, optional
|
|
227
|
+
Cases to include, by task (`I-1-1`) or name
|
|
228
|
+
(`1.1.1-nanoflares_events`). Default: every case.
|
|
229
|
+
lines : list of str, optional
|
|
230
|
+
Lines to include, by ion (`Fe XII`) or by ion and wavelength in
|
|
231
|
+
Angstrom (`Fe XII 195.119`, matched to 0.01 Angstrom). Default:
|
|
232
|
+
every line.
|
|
233
|
+
base : dict, optional
|
|
234
|
+
Settings for every configuration, as in a configuration file. These
|
|
235
|
+
override the defaults (512 iterations on every CPU, PSF on, off-chip
|
|
236
|
+
binning of 1 and 2) and, where given, the case's exposure time and
|
|
237
|
+
slit width. They cannot set the instrument or the line.
|
|
238
|
+
table : list of dict, optional
|
|
239
|
+
Science cases to use instead of the table included with ECLIPSE.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
configs : list of ScienceCaseConfig
|
|
244
|
+
One per chosen line that ECLIPSE can simulate.
|
|
245
|
+
skipped : list of str
|
|
246
|
+
Chosen lines ECLIPSE cannot simulate yet, as `<task> <line>`.
|
|
247
|
+
"""
|
|
248
|
+
from .main import _require_mapping, _validate_config_keys
|
|
249
|
+
|
|
250
|
+
table = load_science_cases() if table is None else table
|
|
251
|
+
base = {} if base is None else base
|
|
252
|
+
for key in _LINE_KEYS:
|
|
253
|
+
if key in base:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"The base settings cannot set '{key}': every configuration "
|
|
256
|
+
f"takes it from its line."
|
|
257
|
+
)
|
|
258
|
+
for key in _SERIES_KEYS:
|
|
259
|
+
if key in base:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
f"The base settings cannot set '{key}': every configuration "
|
|
262
|
+
f"observes its line's intensity, not a time series."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# The simulation section is merged with each case's settings, so check it
|
|
266
|
+
# here; the other sections are checked with the whole configuration.
|
|
267
|
+
if "simulation" in base:
|
|
268
|
+
_require_mapping(base["simulation"], "The base settings' 'simulation:' section")
|
|
269
|
+
|
|
270
|
+
configs, skipped = [], []
|
|
271
|
+
for case, line in _chosen_lines(table, cases, lines):
|
|
272
|
+
instrument = instrument_for(line)
|
|
273
|
+
if instrument is None:
|
|
274
|
+
skipped.append(f"{case['task']} {line_label(line)}")
|
|
275
|
+
continue
|
|
276
|
+
config = _config(case, line, instrument, base)
|
|
277
|
+
_validate_config_keys(config, instrument)
|
|
278
|
+
configs.append(ScienceCaseConfig(
|
|
279
|
+
name=f"{case['name']}_{line_id(line)}",
|
|
280
|
+
case=case, line=line, config=config))
|
|
281
|
+
return configs, skipped
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def write_science_case_configs(out_dir: str | Path, **kwargs) -> tuple[list[Path], list[str]]:
|
|
285
|
+
"""
|
|
286
|
+
Write the configurations from :func:`science_case_configs` to files.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
out_dir : str or Path
|
|
291
|
+
Folder to write to. Each file is named after its case and line, and
|
|
292
|
+
so are the results of running it.
|
|
293
|
+
**kwargs
|
|
294
|
+
Passed to :func:`science_case_configs`.
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
paths : list of Path
|
|
299
|
+
Files written.
|
|
300
|
+
skipped : list of str
|
|
301
|
+
Chosen lines ECLIPSE cannot simulate yet.
|
|
302
|
+
"""
|
|
303
|
+
configs, skipped = science_case_configs(**kwargs)
|
|
304
|
+
out_dir = Path(out_dir)
|
|
305
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
paths = []
|
|
307
|
+
for item in configs:
|
|
308
|
+
path = out_dir / f"{item.name}.yaml"
|
|
309
|
+
path.write_text(item.to_yaml())
|
|
310
|
+
paths.append(path)
|
|
311
|
+
return paths, skipped
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def main(argv: list[str] | None = None) -> None:
|
|
315
|
+
"""Command line entry point for `eclipse-science-cases`."""
|
|
316
|
+
parser = argparse.ArgumentParser(
|
|
317
|
+
prog="eclipse-science-cases",
|
|
318
|
+
description="Write ECLIPSE configurations for the science cases in "
|
|
319
|
+
"the Concept Study Report.",
|
|
320
|
+
)
|
|
321
|
+
parser.add_argument("--out", default="run/input",
|
|
322
|
+
help="folder to write the configurations to "
|
|
323
|
+
"(default: run/input)")
|
|
324
|
+
parser.add_argument("--case", action="append", dest="cases", metavar="CASE",
|
|
325
|
+
help="a case to include, by task (I-1-1) or name "
|
|
326
|
+
"(1.1.1-nanoflares_events); repeat for more "
|
|
327
|
+
"(default: every case)")
|
|
328
|
+
parser.add_argument("--line", action="append", dest="lines", metavar="LINE",
|
|
329
|
+
help="a line to include, by ion ('Fe XII') or by ion "
|
|
330
|
+
"and wavelength in Angstrom ('Fe XII 195.119'); "
|
|
331
|
+
"repeat for more (default: every line)")
|
|
332
|
+
parser.add_argument("--base", metavar="YAML",
|
|
333
|
+
help="a configuration file whose settings go into every "
|
|
334
|
+
"configuration, e.g. a detector section or n_iter")
|
|
335
|
+
parser.add_argument("--list", action="store_true",
|
|
336
|
+
help="list the chosen cases and lines instead of "
|
|
337
|
+
"writing anything")
|
|
338
|
+
args = parser.parse_args(argv)
|
|
339
|
+
|
|
340
|
+
if args.list:
|
|
341
|
+
shown = None
|
|
342
|
+
for case, line in _chosen_lines(load_science_cases(), args.cases, args.lines):
|
|
343
|
+
if case is not shown:
|
|
344
|
+
print(f"{case['task']} {case['name']}: {case['description']}")
|
|
345
|
+
shown = case
|
|
346
|
+
instrument = instrument_for(line) or "not simulated yet"
|
|
347
|
+
print(f" {line_label(line):<22} {instrument}")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
base = {}
|
|
351
|
+
if args.base:
|
|
352
|
+
base = load_yaml_config(Path(args.base).read_text()) or {}
|
|
353
|
+
if not isinstance(base, dict):
|
|
354
|
+
raise ValueError(f"{args.base} has to hold a mapping of settings.")
|
|
355
|
+
|
|
356
|
+
paths, skipped = write_science_case_configs(
|
|
357
|
+
args.out, cases=args.cases, lines=args.lines, base=base)
|
|
358
|
+
low, high = short_wavelength_range()
|
|
359
|
+
print(f"Wrote {len(paths)} configuration{'' if len(paths) == 1 else 's'} "
|
|
360
|
+
f"to {args.out}")
|
|
361
|
+
if skipped:
|
|
362
|
+
print(f"Skipped {len(skipped)} line{'' if len(skipped) == 1 else 's'} "
|
|
363
|
+
f"outside the {low.value:g} to {high.value:g} Angstrom ECLIPSE can "
|
|
364
|
+
f"simulate yet; --list shows which")
|