eclipse 0.13.0__tar.gz → 0.14.0__tar.gz
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-0.13.0/src/eclipse.egg-info → eclipse-0.14.0}/PKG-INFO +1 -3
- {eclipse-0.13.0 → eclipse-0.14.0}/README.md +0 -2
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/__init__.py +1 -1
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/fitting.py +106 -6
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/main.py +6 -2
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/results_file.py +2 -1
- {eclipse-0.13.0 → eclipse-0.14.0/src/eclipse.egg-info}/PKG-INFO +1 -3
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse.egg-info/SOURCES.txt +1 -0
- eclipse-0.14.0/tests/test_fit_rest_start_and_limits.py +243 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_raster.py +3 -1
- {eclipse-0.13.0 → eclipse-0.14.0}/LICENSE +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/MANIFEST.in +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/pyproject.toml +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/setup.cfg +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/analysis.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/atmosphere.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/band.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/cli.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/config.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/continuum.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/eis_response/EIS_EffArea_A.004 +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/eis_response/EIS_EffArea_B.004 +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/eis_response/EIS_EffArea_B.005 +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/eis_response/README_dz2025 +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/eis_response/fit_eis_ea_2023-05-04_smooth.sav +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/eis_response/source.txt +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/science_cases.yaml +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/grating_reflection_efficiency.dat +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/primary_mirror_coating_reflectance.dat +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/source.txt +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/throughput_aluminium_1000_angstrom.dat +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/throughput_aluminium_oxide_1000_angstrom.dat +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/throughput_carbon_1000_angstrom.dat +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data_processing.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/deprecated.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/eis_calibration.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/extern/__init__.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/extern/mpfit.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/frame.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/full_frame.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/monte_carlo.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/pinhole_diffraction.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/psf.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/radiometric.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/raster.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/readout.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/resolution_budget.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/sampling.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/science_cases.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/synthesis.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/synthesis_cli.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/synthesis_file.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/utils.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse.egg-info/dependency_links.txt +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse.egg-info/entry_points.txt +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse.egg-info/requires.txt +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse.egg-info/top_level.txt +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/src/euvst_response/__init__.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_analysis_lookups.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_atmosphere_input.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_axis_conventions.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_band_lines.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_common_random_numbers.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_config_checks.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_config_validation.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_continuum.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_distributions.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_doppler_sign.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_downsample.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_eis_effective_area.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_emission_bins.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_conditioning.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_config.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_constraints.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_finite.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_multi_start.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_quality.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_start.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_summaries.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_fit_weighting.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_focal_plane_dispersion.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_frame.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_full_frame.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_goft_temperature_chunks.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_ground_truth_pixel_mean.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_instrument_settings.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_instrument_sources.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_line_cube_coordinates.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_line_matching.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_map_metadata.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_mpi_ranks.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_noise_toggle.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_old_package_name.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_photon_energy.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_pinhole_config.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_pixel_sampling.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_psf_boundary.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_radiometric_chain.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_readout.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_readout_dark_current.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_resolution_budget.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_results_file.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_results_file_runs.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_science_cases.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_slit_mask.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_slit_psf.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_small_fixes.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_spectral_rebinning.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_synthesis_file.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_synthesis_grids.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_uniform_intensity.py +0 -0
- {eclipse-0.13.0 → eclipse-0.14.0}/tests/test_untested_numbers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eclipse
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
|
|
5
5
|
Author-email: James McKevitt <jm2@mssl.ucl.ac.uk>
|
|
6
6
|
Project-URL: Homepage, https://github.com/jamesmckevitt/eclipse
|
|
@@ -61,8 +61,6 @@ The instrument model will be updated as EUVST is built, tested and commissioned.
|
|
|
61
61
|
[](https://pypi.org/project/eclipse/)
|
|
62
62
|
[](https://doi.org/10.5281/zenodo.17543844)
|
|
63
63
|
|
|
64
|
-
ECLIPSE is installed with `pip install eclipse`. ECLIPSE 0.12 and earlier were on PyPI as `solarc-eclipse`.
|
|
65
|
-
|
|
66
64
|
The documentation, with installation and a first run, guides to synthesising spectra, simulating the instrument and analysing the results, and a worked example, is at **[solarc-eclipse.readthedocs.io](https://solarc-eclipse.readthedocs.io)**.
|
|
67
65
|
|
|
68
66
|
## Acknowledgements
|
|
@@ -11,8 +11,6 @@ The instrument model will be updated as EUVST is built, tested and commissioned.
|
|
|
11
11
|
[](https://pypi.org/project/eclipse/)
|
|
12
12
|
[](https://doi.org/10.5281/zenodo.17543844)
|
|
13
13
|
|
|
14
|
-
ECLIPSE is installed with `pip install eclipse`. ECLIPSE 0.12 and earlier were on PyPI as `solarc-eclipse`.
|
|
15
|
-
|
|
16
14
|
The documentation, with installation and a first run, guides to synthesising spectra, simulating the instrument and analysing the results, and a worked example, is at **[solarc-eclipse.readthedocs.io](https://solarc-eclipse.readthedocs.io)**.
|
|
17
15
|
|
|
18
16
|
## Acknowledgements
|
|
@@ -4,7 +4,7 @@ ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
|
|
|
4
4
|
Tools for modeling the performance of EUVST, the EUV spectrograph on SOLAR-C.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.14.0"
|
|
8
8
|
__author__ = "James McKevitt"
|
|
9
9
|
__email__ = "jm2@mssl.ucl.ac.uk"
|
|
10
10
|
|
|
@@ -71,6 +71,15 @@ class FitComponent:
|
|
|
71
71
|
name : str, optional
|
|
72
72
|
Name in the results. Default the rest wavelength, e.g.
|
|
73
73
|
`"195.1190 Angstrom"`.
|
|
74
|
+
velocity_limits : tuple of u.Quantity, optional
|
|
75
|
+
Lowest and highest velocity the fit may give this line, measured
|
|
76
|
+
from its rest wavelength, such as `(-60 * u.km / u.s, 60 * u.km / u.s)`.
|
|
77
|
+
Not for a line whose centre is tied to another's. Default None, for
|
|
78
|
+
no limits.
|
|
79
|
+
width_limits : tuple of u.Quantity, optional
|
|
80
|
+
Smallest and largest Gaussian sigma the fit may give this line, such
|
|
81
|
+
as `(0.019 * u.AA, 0.051 * u.AA)`. Not for a line whose width is tied
|
|
82
|
+
to another's. Default None, for no limits.
|
|
74
83
|
"""
|
|
75
84
|
wavelength: u.Quantity
|
|
76
85
|
tie_center: Optional[int] = None # index of component whose centre this is tied to
|
|
@@ -79,6 +88,8 @@ class FitComponent:
|
|
|
79
88
|
# Label for this component in the results. Defaults to its rest
|
|
80
89
|
# wavelength, e.g. "195.1190 Angstrom".
|
|
81
90
|
name: Optional[str] = None
|
|
91
|
+
velocity_limits: Optional[tuple] = None
|
|
92
|
+
width_limits: Optional[tuple] = None
|
|
82
93
|
|
|
83
94
|
def __post_init__(self):
|
|
84
95
|
# Catch a wavelength with no unit here rather than in the fit.
|
|
@@ -87,6 +98,38 @@ class FitComponent:
|
|
|
87
98
|
and np.isfinite(self.wavelength.value) and self.wavelength.value > 0):
|
|
88
99
|
raise ValueError(f"A fitting component's wavelength must be the wavelength of a "
|
|
89
100
|
f"line, such as '195.119 AA'; got {self.wavelength!r}.")
|
|
101
|
+
for key, kind, example in (("velocity_limits", u.km / u.s, "[-60 km/s, 60 km/s]"),
|
|
102
|
+
("width_limits", u.AA, "[0.019 AA, 0.051 AA]")):
|
|
103
|
+
value = getattr(self, key)
|
|
104
|
+
if value is None:
|
|
105
|
+
continue
|
|
106
|
+
try:
|
|
107
|
+
low, high = (u.Quantity(v) for v in value)
|
|
108
|
+
except (TypeError, ValueError):
|
|
109
|
+
low = high = None
|
|
110
|
+
if (low is None or not low.unit.is_equivalent(kind)
|
|
111
|
+
or not high.unit.is_equivalent(kind)
|
|
112
|
+
or not (np.isfinite(low.value) and np.isfinite(high.value))
|
|
113
|
+
or not low < high or (key == "width_limits" and low <= 0)):
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"A fitting component's {key} must be two {kind.physical_type}s, the "
|
|
116
|
+
f"lower first{', both above zero' if key == 'width_limits' else ''}, "
|
|
117
|
+
f"such as {example}; got {value!r}.")
|
|
118
|
+
setattr(self, key, (low.to(kind), high.to(kind)))
|
|
119
|
+
|
|
120
|
+
def centre_limits(self) -> Optional[tuple]:
|
|
121
|
+
"""Lowest and highest centre in cm that `velocity_limits` allow, or None."""
|
|
122
|
+
if self.velocity_limits is None:
|
|
123
|
+
return None
|
|
124
|
+
rest = self.wavelength.to_value(u.cm)
|
|
125
|
+
c = const.c.to_value(u.km / u.s)
|
|
126
|
+
return tuple(rest * (1 + v.to_value(u.km / u.s) / c) for v in self.velocity_limits)
|
|
127
|
+
|
|
128
|
+
def sigma_limits(self) -> Optional[tuple]:
|
|
129
|
+
"""Smallest and largest sigma in cm that `width_limits` allow, or None."""
|
|
130
|
+
if self.width_limits is None:
|
|
131
|
+
return None
|
|
132
|
+
return tuple(w.to_value(u.cm) for w in self.width_limits)
|
|
90
133
|
|
|
91
134
|
|
|
92
135
|
def default_component_name(wavelength: u.Quantity) -> str:
|
|
@@ -123,6 +166,11 @@ class FitConfig:
|
|
|
123
166
|
Weight each pixel of the DN spectra by one over its uncertainty
|
|
124
167
|
squared, estimated from its own signal as eispac does, and fit the
|
|
125
168
|
photon spectra by their Poisson likelihood. Default True.
|
|
169
|
+
start : str
|
|
170
|
+
Where each fit of a blend starts: `"search"` (default) slides the
|
|
171
|
+
components together along the spectrum to where they best match it;
|
|
172
|
+
`"rest"` starts every component at its rest wavelength, as eispac's
|
|
173
|
+
templates do.
|
|
126
174
|
"""
|
|
127
175
|
components: List[FitComponent] = field(default_factory=list)
|
|
128
176
|
primary_component: int = 0
|
|
@@ -146,6 +194,10 @@ class FitConfig:
|
|
|
146
194
|
# its position count no more than its noisier peak. Results files from
|
|
147
195
|
# before this setting are unweighted.
|
|
148
196
|
weighted: bool = True
|
|
197
|
+
# Where a blend's fit starts. A search can start a faint blend a whole
|
|
198
|
+
# component spacing from the truth; starting at rest, as eispac does,
|
|
199
|
+
# keeps it on the line that is there.
|
|
200
|
+
start: str = "search"
|
|
149
201
|
|
|
150
202
|
def __post_init__(self):
|
|
151
203
|
if len(self.components) == 1:
|
|
@@ -154,6 +206,8 @@ class FitConfig:
|
|
|
154
206
|
"needs at least two; for a single Gaussian leave components "
|
|
155
207
|
"out, and the other fitting settings still apply."
|
|
156
208
|
)
|
|
209
|
+
if self.start not in ("search", "rest"):
|
|
210
|
+
raise ValueError(f"fitting.start must be 'search' or 'rest', got {self.start!r}.")
|
|
157
211
|
if self.backend not in (None, "scipy", "mpfit"):
|
|
158
212
|
raise ValueError(
|
|
159
213
|
f"Unknown fitting backend '{self.backend}'. "
|
|
@@ -194,6 +248,12 @@ class FitConfig:
|
|
|
194
248
|
f"has to be the index of another component, from "
|
|
195
249
|
f"0 to {n - 1}."
|
|
196
250
|
)
|
|
251
|
+
for key, tie in (("velocity_limits", "tie_center"), ("width_limits", "tie_width")):
|
|
252
|
+
if getattr(comp, key) is not None and getattr(comp, tie) is not None:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"fitting.components[{idx}] has {key} and {tie}. A tied "
|
|
255
|
+
f"component follows the one it is tied to, so give the limits "
|
|
256
|
+
f"to that one.")
|
|
197
257
|
if comp.name is not None and (
|
|
198
258
|
not isinstance(comp.name, str) or not comp.name.strip()):
|
|
199
259
|
raise ValueError(
|
|
@@ -229,7 +289,7 @@ class FitConfig:
|
|
|
229
289
|
f"the components a 'name'."
|
|
230
290
|
)
|
|
231
291
|
else:
|
|
232
|
-
#
|
|
292
|
+
# These settings need components; without them they would
|
|
233
293
|
# silently do nothing.
|
|
234
294
|
if self.primary_component != 0:
|
|
235
295
|
raise ValueError(
|
|
@@ -241,6 +301,11 @@ class FitConfig:
|
|
|
241
301
|
"fitting.constrain_positive_intensity is only applied to "
|
|
242
302
|
"multi-component fits, and there are no components."
|
|
243
303
|
)
|
|
304
|
+
if self.start != "search":
|
|
305
|
+
raise ValueError(
|
|
306
|
+
"fitting.start is only applied to multi-component fits, and there are "
|
|
307
|
+
"no components."
|
|
308
|
+
)
|
|
244
309
|
|
|
245
310
|
@property
|
|
246
311
|
def n_components(self) -> int:
|
|
@@ -678,7 +743,16 @@ def _build_scipy_multi(fit_config: FitConfig, pixel_angstrom: float | None = Non
|
|
|
678
743
|
for pos, fi in enumerate(free_indices):
|
|
679
744
|
if fi % 3 == 0 and fi < 3 * nc and pos not in ratio_spec:
|
|
680
745
|
lower[pos] = 0.0
|
|
681
|
-
|
|
746
|
+
# Limits on each component's centre and width, in Angstrom.
|
|
747
|
+
limited = False
|
|
748
|
+
for pos, fi in enumerate(free_indices):
|
|
749
|
+
if fi < 3 * nc and fi % 3 in (1, 2):
|
|
750
|
+
comp = fit_config.components[fi // 3]
|
|
751
|
+
limits = comp.centre_limits() if fi % 3 == 1 else comp.sigma_limits()
|
|
752
|
+
if limits is not None:
|
|
753
|
+
lower[pos], upper[pos] = (value * 1e8 for value in limits)
|
|
754
|
+
limited = True
|
|
755
|
+
has_bounds = ratio_spec or fit_config.constrain_positive_intensity or limited
|
|
682
756
|
bounds = (lower, upper) if has_bounds else (-np.inf, np.inf)
|
|
683
757
|
|
|
684
758
|
# Order the ratios so parents come before their children.
|
|
@@ -881,6 +955,9 @@ def _build_parinfo(fit_config: FitConfig, p0: np.ndarray,
|
|
|
881
955
|
/ fit_config.components[src].wavelength.to(u.cm).value)
|
|
882
956
|
# mpfit tie expression references the parameter array p
|
|
883
957
|
centre_info["tied"] = f"p[{3 * src + 1}] * {factor!r}"
|
|
958
|
+
elif (centre_limits := comp.centre_limits()) is not None:
|
|
959
|
+
centre_info["limited"] = [1, 1]
|
|
960
|
+
centre_info["limits"] = list(centre_limits)
|
|
884
961
|
parinfo.append(centre_info)
|
|
885
962
|
|
|
886
963
|
# --- sigma (width) ---
|
|
@@ -888,6 +965,9 @@ def _build_parinfo(fit_config: FitConfig, p0: np.ndarray,
|
|
|
888
965
|
# Width must be strictly positive (avoid divide-by-zero in Gaussian)
|
|
889
966
|
sigma_info["limited"] = [1, 0]
|
|
890
967
|
sigma_info["limits"] = [1e-30, 0.0]
|
|
968
|
+
if (sigma_limits := comp.sigma_limits()) is not None:
|
|
969
|
+
sigma_info["limited"] = [1, 1]
|
|
970
|
+
sigma_info["limits"] = list(sigma_limits)
|
|
891
971
|
if comp.tie_width is not None:
|
|
892
972
|
src = _tie_root(fit_config, "tie_width", i)
|
|
893
973
|
sigma_info["tied"] = f"p[{3 * src + 2}]"
|
|
@@ -982,16 +1062,28 @@ def _guess_multi_params(wv: np.ndarray, prof: np.ndarray,
|
|
|
982
1062
|
rest_wl = np.array([c.wavelength.to(u.cm).value
|
|
983
1063
|
for c in fit_config.components])
|
|
984
1064
|
ref_wl = float(rest_wl[fit_config.primary_component])
|
|
985
|
-
|
|
1065
|
+
# Shifts of the whole comb that keep each component within its velocity
|
|
1066
|
+
# limits.
|
|
1067
|
+
allowed = [-np.inf, np.inf]
|
|
1068
|
+
for comp, rest in zip(fit_config.components, rest_wl):
|
|
1069
|
+
limits = comp.centre_limits()
|
|
1070
|
+
if limits is not None:
|
|
1071
|
+
low_centre, high_centre = limits
|
|
1072
|
+
allowed[0] = max(allowed[0], ref_wl * low_centre / rest - ref_wl)
|
|
1073
|
+
allowed[1] = min(allowed[1], ref_wl * high_centre / rest - ref_wl)
|
|
1074
|
+
# With start "rest", every component starts at its rest wavelength, as
|
|
1075
|
+
# eispac's templates do.
|
|
1076
|
+
if fit_config.start == "search" and peak > 0:
|
|
986
1077
|
# A trial shift moves the primary centre to ref_wl + shift and every
|
|
987
1078
|
# other component to its rest wavelength times the same factor. The
|
|
988
1079
|
# range keeps the whole comb inside the observed window; with all
|
|
989
1080
|
# components at one wavelength it reduces to shifting the comb bodily.
|
|
990
1081
|
lo = float(wv.min()) * ref_wl / float(rest_wl.min()) - ref_wl
|
|
991
1082
|
hi = float(wv.max()) * ref_wl / float(rest_wl.max()) - ref_wl
|
|
1083
|
+
lo, hi = max(lo, allowed[0]), min(hi, allowed[1])
|
|
992
1084
|
step = float(np.median(np.diff(wv))) if len(wv) > 1 else 0.0
|
|
993
1085
|
if hi > lo and step > 0:
|
|
994
|
-
trial = np.arange(lo, hi + 0.5 * step, step)
|
|
1086
|
+
trial = np.minimum(np.arange(lo, hi + 0.5 * step, step), hi)
|
|
995
1087
|
score = np.zeros(trial.size)
|
|
996
1088
|
for r in rest_wl:
|
|
997
1089
|
score += np.interp(r * (ref_wl + trial) / ref_wl, wv, prof_c,
|
|
@@ -1006,8 +1098,8 @@ def _guess_multi_params(wv: np.ndarray, prof: np.ndarray,
|
|
|
1006
1098
|
# nothing to recover.
|
|
1007
1099
|
shift = float(trial[int(np.argmax(score))])
|
|
1008
1100
|
else:
|
|
1009
|
-
#
|
|
1010
|
-
#
|
|
1101
|
+
# No shift keeps the comb inside the window and within the
|
|
1102
|
+
# limits. Fall back to the brightest pixel and the component
|
|
1011
1103
|
# nearest to it, read as a Doppler factor.
|
|
1012
1104
|
peak_wl = float(wv[np.nanargmax(prof_c)])
|
|
1013
1105
|
nearest = float(rest_wl[np.argmin(np.abs(rest_wl - peak_wl))])
|
|
@@ -1015,6 +1107,8 @@ def _guess_multi_params(wv: np.ndarray, prof: np.ndarray,
|
|
|
1015
1107
|
else:
|
|
1016
1108
|
shift = 0.0
|
|
1017
1109
|
|
|
1110
|
+
if allowed[0] <= allowed[1]:
|
|
1111
|
+
shift = float(np.clip(shift, *allowed))
|
|
1018
1112
|
doppler = (ref_wl + shift) / ref_wl
|
|
1019
1113
|
full_guess = np.zeros(fit_config.n_full_params)
|
|
1020
1114
|
for i, comp in enumerate(fit_config.components):
|
|
@@ -1022,6 +1116,12 @@ def _guess_multi_params(wv: np.ndarray, prof: np.ndarray,
|
|
|
1022
1116
|
full_guess[base] = peak if i == fit_config.primary_component else peak * 0.15
|
|
1023
1117
|
full_guess[base + 1] = rest_wl[i] * doppler
|
|
1024
1118
|
full_guess[base + 2] = sigma
|
|
1119
|
+
# A start outside a component's limits is moved to the nearest limit, as
|
|
1120
|
+
# the optimisers need one inside them.
|
|
1121
|
+
for i, comp in enumerate(fit_config.components):
|
|
1122
|
+
for offset, limits in ((1, comp.centre_limits()), (2, comp.sigma_limits())):
|
|
1123
|
+
if limits is not None:
|
|
1124
|
+
full_guess[3 * i + offset] = np.clip(full_guess[3 * i + offset], *limits)
|
|
1025
1125
|
full_guess[-1] = back
|
|
1026
1126
|
return full_guess
|
|
1027
1127
|
|
|
@@ -137,9 +137,10 @@ _SIMULATION_KEYS = {"slit_width", "expos", "vis_sl", "psf", "psf_boundary",
|
|
|
137
137
|
|
|
138
138
|
_FITTING_KEYS = {"components", "primary_component",
|
|
139
139
|
"constrain_positive_intensity", "backend", "max_iter",
|
|
140
|
-
"bessel_correction", "save_iterations", "weighted"}
|
|
140
|
+
"bessel_correction", "save_iterations", "weighted", "start"}
|
|
141
141
|
_FITTING_COMPONENT_KEYS = {"wavelength", "tie_center", "tie_width",
|
|
142
|
-
"amplitude_greater_than", "name"
|
|
142
|
+
"amplitude_greater_than", "name", "velocity_limits",
|
|
143
|
+
"width_limits"}
|
|
143
144
|
|
|
144
145
|
|
|
145
146
|
def _dataclass_keys(cls) -> set:
|
|
@@ -472,6 +473,8 @@ def _parse_fitting_config(config: dict) -> FitConfig | None:
|
|
|
472
473
|
tie_width=comp_dict.get("tie_width", None),
|
|
473
474
|
amplitude_greater_than=comp_dict.get("amplitude_greater_than", None),
|
|
474
475
|
name=comp_dict.get("name", None),
|
|
476
|
+
velocity_limits=comp_dict.get("velocity_limits"),
|
|
477
|
+
width_limits=comp_dict.get("width_limits"),
|
|
475
478
|
))
|
|
476
479
|
|
|
477
480
|
return FitConfig(
|
|
@@ -484,6 +487,7 @@ def _parse_fitting_config(config: dict) -> FitConfig | None:
|
|
|
484
487
|
bessel_correction=fitting_cfg.get("bessel_correction", False),
|
|
485
488
|
save_iterations=fitting_cfg.get("save_iterations", False),
|
|
486
489
|
weighted=fitting_cfg.get("weighted", True),
|
|
490
|
+
start=fitting_cfg.get("start", "search"),
|
|
487
491
|
)
|
|
488
492
|
|
|
489
493
|
|
|
@@ -353,7 +353,8 @@ def _jsonable(value):
|
|
|
353
353
|
# FitConfig.weighted, and None is the detector material's value.
|
|
354
354
|
_MATERIAL = {"fano_factor": None, "pair_energy_300k": None, "pair_energy_slope": None}
|
|
355
355
|
_EARLIER_VALUES = {
|
|
356
|
-
"FitConfig": {"weighted": False},
|
|
356
|
+
"FitConfig": {"weighted": False, "start": "search"},
|
|
357
|
+
"FitComponent": {"velocity_limits": None, "width_limits": None},
|
|
357
358
|
"Telescope_EUVST": {"resolution_budget": "none"},
|
|
358
359
|
"Detector_SWC": {**_MATERIAL,
|
|
359
360
|
"dispersion": (198.885944, 1.2516577, -1.361114e-4),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eclipse
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
|
|
5
5
|
Author-email: James McKevitt <jm2@mssl.ucl.ac.uk>
|
|
6
6
|
Project-URL: Homepage, https://github.com/jamesmckevitt/eclipse
|
|
@@ -61,8 +61,6 @@ The instrument model will be updated as EUVST is built, tested and commissioned.
|
|
|
61
61
|
[](https://pypi.org/project/eclipse/)
|
|
62
62
|
[](https://doi.org/10.5281/zenodo.17543844)
|
|
63
63
|
|
|
64
|
-
ECLIPSE is installed with `pip install eclipse`. ECLIPSE 0.12 and earlier were on PyPI as `solarc-eclipse`.
|
|
65
|
-
|
|
66
64
|
The documentation, with installation and a first run, guides to synthesising spectra, simulating the instrument and analysing the results, and a worked example, is at **[solarc-eclipse.readthedocs.io](https://solarc-eclipse.readthedocs.io)**.
|
|
67
65
|
|
|
68
66
|
## Acknowledgements
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""A blend started at its rest wavelengths, and limits on each component's velocity and width.
|
|
2
|
+
|
|
3
|
+
Started this way, as eispac's templates are, a faint blend stays on its line.
|
|
4
|
+
"""
|
|
5
|
+
import astropy.constants as const
|
|
6
|
+
import astropy.units as u
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
import yaml
|
|
10
|
+
from astropy.wcs import WCS
|
|
11
|
+
from ndcube import NDCube
|
|
12
|
+
|
|
13
|
+
from eclipse.fitting import FitComponent, FitConfig, _guess_multi_params, fit_cube_gauss
|
|
14
|
+
from eclipse.results_file import load_results, save_results
|
|
15
|
+
|
|
16
|
+
STEP = 0.0169 * u.Angstrom
|
|
17
|
+
N_WAVE = 31
|
|
18
|
+
CENTRE = 195.13 * u.Angstrom
|
|
19
|
+
STRONG, FAINT = 195.119 * u.Angstrom, 195.179 * u.Angstrom
|
|
20
|
+
SIGMA = 0.027 * u.Angstrom
|
|
21
|
+
C_KM_S = const.c.to_value(u.km / u.s)
|
|
22
|
+
# eispac's fe_12_195_119.2c template: the centre within 0.04 Angstrom of rest
|
|
23
|
+
# and the width between 0.0191 and 0.0510 Angstrom.
|
|
24
|
+
EISPAC = dict(velocity_limits=(-61.5 * u.km / u.s, 61.5 * u.km / u.s),
|
|
25
|
+
width_limits=(0.0191 * u.AA, 0.0510 * u.AA))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _wave():
|
|
29
|
+
return CENTRE + (np.arange(N_WAVE) - (N_WAVE - 1) / 2) * STEP
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cube(spectra):
|
|
33
|
+
"""Spectra, shaped (n, N_WAVE), as a (1, n, N_WAVE) cube."""
|
|
34
|
+
wcs = WCS(naxis=3)
|
|
35
|
+
wcs.wcs.ctype = ["WAVE", "HPLN-TAN", "HPLT-TAN"]
|
|
36
|
+
wcs.wcs.cunit = ["cm", "arcsec", "arcsec"]
|
|
37
|
+
wcs.wcs.cdelt = [STEP.to_value(u.cm), 0.4, 0.16]
|
|
38
|
+
wcs.wcs.crpix = [(N_WAVE + 1) / 2, 1.0, 1.0]
|
|
39
|
+
wcs.wcs.crval = [CENTRE.to_value(u.cm), 0.0, 0.0]
|
|
40
|
+
return NDCube(np.asarray(spectra)[np.newaxis], wcs=wcs, unit=u.DN / u.pix)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _blend(velocity=0.0, strong=100.0, ratio=0.06, background=5.0):
|
|
44
|
+
"""Fe XII 195.119 and a 195.179 `ratio` as bright, at `velocity` km/s."""
|
|
45
|
+
wave = _wave()
|
|
46
|
+
return background + sum(
|
|
47
|
+
peak * np.exp(-0.5 * ((wave - rest * (1 + velocity / C_KM_S)) / SIGMA)
|
|
48
|
+
.decompose().value ** 2)
|
|
49
|
+
for peak, rest in ((strong, STRONG), (strong * ratio, FAINT)))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _components(primary=None, **limits):
|
|
53
|
+
return [FitComponent(STRONG, **(primary or {}), **limits),
|
|
54
|
+
FitComponent(FAINT, tie_center=0, tie_width=0)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _velocity(params):
|
|
58
|
+
return (params[..., 1] * u.cm / STRONG.to(u.cm) - 1).value * C_KM_S
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize("backend", ["scipy", "mpfit"])
|
|
62
|
+
def test_a_noisy_faint_blend_started_at_rest_and_limited_stays_on_its_line(backend):
|
|
63
|
+
"""
|
|
64
|
+
With a few dozen counts in the line, a search start lands some fits a
|
|
65
|
+
line spacing, 92 km/s, from the truth; at rest within eispac's limits
|
|
66
|
+
none do.
|
|
67
|
+
"""
|
|
68
|
+
rng = np.random.default_rng(3)
|
|
69
|
+
truth = _blend(strong=30.0)
|
|
70
|
+
spectra = rng.poisson(np.tile(truth, (400, 1))).astype(float)
|
|
71
|
+
cube = _cube(spectra)
|
|
72
|
+
|
|
73
|
+
def far(**settings):
|
|
74
|
+
config = FitConfig(components=_components(**settings.pop("limits", {})),
|
|
75
|
+
constrain_positive_intensity=True, backend=backend, **settings)
|
|
76
|
+
data, _, failed = fit_cube_gauss(cube, n_jobs=1, fit_config=config, return_failed=True)
|
|
77
|
+
velocity = _velocity(data[0])[~failed[0]]
|
|
78
|
+
return np.mean(np.abs(velocity) > 45)
|
|
79
|
+
|
|
80
|
+
searched = far()
|
|
81
|
+
rested = far(start="rest", limits=EISPAC)
|
|
82
|
+
assert searched > 0.02
|
|
83
|
+
assert rested < searched / 4
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.parametrize("backend", ["scipy", "mpfit"])
|
|
87
|
+
def test_a_line_beyond_its_velocity_limit_is_fitted_at_the_limit(backend):
|
|
88
|
+
"""
|
|
89
|
+
The line is to the blue. To the red, the faint component, 92 km/s to the
|
|
90
|
+
red of the other, could reach it from inside the limits.
|
|
91
|
+
"""
|
|
92
|
+
config = FitConfig(components=_components(**EISPAC), start="rest", backend=backend)
|
|
93
|
+
data, _, failed = fit_cube_gauss(_cube([_blend(velocity=-80.0)]), n_jobs=1,
|
|
94
|
+
fit_config=config, return_failed=True)
|
|
95
|
+
assert _velocity(data[0, 0]) == pytest.approx(-61.5, abs=0.05)
|
|
96
|
+
width = (data[0, 0, 2] * u.cm).to_value(u.AA)
|
|
97
|
+
assert 0.0191 - 1e-9 <= width <= 0.0510 + 1e-9
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.parametrize("backend", ["scipy", "mpfit"])
|
|
101
|
+
def test_limits_that_hold_the_truth_change_nothing_in_a_noiseless_fit(backend):
|
|
102
|
+
spectrum = _cube([_blend(velocity=20.0, ratio=0.2)])
|
|
103
|
+
fitted = {}
|
|
104
|
+
for name, settings in (("free", {}), ("limited", {"start": "rest"})):
|
|
105
|
+
limits = EISPAC if name == "limited" else {}
|
|
106
|
+
config = FitConfig(components=_components(**limits), backend=backend, **settings)
|
|
107
|
+
fitted[name] = fit_cube_gauss(spectrum, n_jobs=1, fit_config=config)[0][0, 0]
|
|
108
|
+
assert _velocity(fitted["limited"]) == pytest.approx(20.0, abs=1e-3)
|
|
109
|
+
assert np.allclose(fitted["limited"], fitted["free"], rtol=1e-5, atol=0.0)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_a_rest_start_puts_each_component_at_its_rest_wavelength():
|
|
113
|
+
"""A search finds the shifted blend; a rest start does not look, but stays within limits."""
|
|
114
|
+
spectrum = _blend(velocity=40.0, ratio=0.3)
|
|
115
|
+
wave = _wave().to_value(u.cm)
|
|
116
|
+
searched = _guess_multi_params(wave, spectrum, FitConfig(components=_components()))
|
|
117
|
+
rested = _guess_multi_params(wave, spectrum, FitConfig(components=_components(),
|
|
118
|
+
start="rest"))
|
|
119
|
+
assert _velocity(searched) == pytest.approx(40.0, abs=30.0)
|
|
120
|
+
assert rested[1] == pytest.approx(STRONG.to_value(u.cm), rel=1e-12)
|
|
121
|
+
assert rested[4] == pytest.approx(FAINT.to_value(u.cm), rel=1e-12)
|
|
122
|
+
|
|
123
|
+
narrow = FitConfig(components=_components(width_limits=(0.05 * u.AA, 0.06 * u.AA),
|
|
124
|
+
velocity_limits=(10 * u.km / u.s,
|
|
125
|
+
20 * u.km / u.s)),
|
|
126
|
+
start="rest")
|
|
127
|
+
clipped = _guess_multi_params(wave, spectrum, narrow)
|
|
128
|
+
assert _velocity(clipped) == pytest.approx(10.0, rel=1e-9)
|
|
129
|
+
assert (clipped[2] * u.cm).to_value(u.AA) == pytest.approx(0.05, rel=1e-12)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.parametrize("settings, message", [
|
|
133
|
+
({"start": "middle"}, "fitting.start must be 'search' or 'rest'"),
|
|
134
|
+
({"components": [FitComponent(STRONG), FitComponent(
|
|
135
|
+
FAINT, tie_center=0, velocity_limits=(-60 * u.km / u.s, 60 * u.km / u.s))]},
|
|
136
|
+
"velocity_limits and tie_center"),
|
|
137
|
+
({"components": [FitComponent(STRONG), FitComponent(
|
|
138
|
+
FAINT, tie_width=0, width_limits=(0.02 * u.AA, 0.05 * u.AA))]},
|
|
139
|
+
"width_limits and tie_width"),
|
|
140
|
+
({"start": "rest", "components": []}, "only applied to multi-component fits"),
|
|
141
|
+
])
|
|
142
|
+
def test_what_cannot_apply_is_refused(settings, message):
|
|
143
|
+
with pytest.raises(ValueError, match=message):
|
|
144
|
+
FitConfig(**{"components": _components(), **settings})
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@pytest.mark.parametrize("limits", [
|
|
148
|
+
{"velocity_limits": (60 * u.km / u.s, -60 * u.km / u.s)},
|
|
149
|
+
{"velocity_limits": (-60 * u.AA, 60 * u.AA)},
|
|
150
|
+
{"velocity_limits": (-60 * u.km / u.s,)},
|
|
151
|
+
{"width_limits": (0 * u.AA, 0.05 * u.AA)},
|
|
152
|
+
{"width_limits": (0.02 * u.km / u.s, 0.05 * u.km / u.s)},
|
|
153
|
+
])
|
|
154
|
+
def test_limits_that_are_not_two_ordered_values_of_the_right_kind_are_refused(limits):
|
|
155
|
+
with pytest.raises(ValueError, match="must be two"):
|
|
156
|
+
FitComponent(STRONG, **limits)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_a_configuration_file_gives_them_with_units(tmp_path):
|
|
160
|
+
from eclipse.main import _parse_fitting_config
|
|
161
|
+
|
|
162
|
+
text = """
|
|
163
|
+
fitting:
|
|
164
|
+
start: rest
|
|
165
|
+
constrain_positive_intensity: true
|
|
166
|
+
components:
|
|
167
|
+
- wavelength: 195.119 angstrom
|
|
168
|
+
velocity_limits: [-60 km/s, 60 km/s]
|
|
169
|
+
width_limits: [0.019 angstrom, 0.051 angstrom]
|
|
170
|
+
- wavelength: 195.179 angstrom
|
|
171
|
+
tie_center: 0
|
|
172
|
+
tie_width: 0
|
|
173
|
+
"""
|
|
174
|
+
config = _parse_fitting_config(yaml.safe_load(text))
|
|
175
|
+
assert config.start == "rest"
|
|
176
|
+
low, high = config.components[0].velocity_limits
|
|
177
|
+
assert low == -60 * u.km / u.s and high == 60 * u.km / u.s
|
|
178
|
+
assert config.components[0].width_limits[1] == 0.051 * u.AA
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_the_settings_are_kept_in_a_results_file(tmp_path):
|
|
182
|
+
config = FitConfig(components=_components(**EISPAC), start="rest")
|
|
183
|
+
save_results(tmp_path / "run.h5", {"config_objects": {"fit": config}})
|
|
184
|
+
restored = load_results(tmp_path / "run.h5")["config_objects"]["fit"]
|
|
185
|
+
assert restored.start == "rest"
|
|
186
|
+
assert restored.components[0].velocity_limits == EISPAC["velocity_limits"]
|
|
187
|
+
assert restored.components[0].width_limits == EISPAC["width_limits"]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_a_results_file_from_before_the_settings_loads_as_it_was_run(tmp_path):
|
|
191
|
+
"""Earlier runs searched and had no limits, which is what the settings give, without a warning."""
|
|
192
|
+
import json
|
|
193
|
+
import warnings
|
|
194
|
+
|
|
195
|
+
import h5py
|
|
196
|
+
|
|
197
|
+
from eclipse import results_file
|
|
198
|
+
|
|
199
|
+
config = FitConfig(components=_components())
|
|
200
|
+
path = save_results(tmp_path / "old.h5", {"instrument": "SWC"})
|
|
201
|
+
encoded = results_file._jsonable(config)
|
|
202
|
+
del encoded["fields"]["start"]
|
|
203
|
+
for component in encoded["fields"]["components"]:
|
|
204
|
+
del component["fields"]["velocity_limits"], component["fields"]["width_limits"]
|
|
205
|
+
with h5py.File(path, "r+") as f:
|
|
206
|
+
f.attrs["fit"] = json.dumps(encoded)
|
|
207
|
+
f.attrs["eclipse_order"] = json.dumps(["instrument", "fit"])
|
|
208
|
+
with warnings.catch_warnings(record=True) as seen:
|
|
209
|
+
warnings.simplefilter("always")
|
|
210
|
+
restored = load_results(path)["fit"]
|
|
211
|
+
assert not seen, [str(w.message) for w in seen]
|
|
212
|
+
assert repr(restored) == repr(config)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_a_search_within_limits_stays_within_them():
|
|
216
|
+
"""The comb search tries only shifts that keep a limited component within its limits."""
|
|
217
|
+
spectrum = _blend(velocity=-40.0, ratio=0.06, strong=1.0) + _blend(velocity=52.0, ratio=0.0,
|
|
218
|
+
strong=1.2, background=0)
|
|
219
|
+
config = FitConfig(components=_components(velocity_limits=(-60 * u.km / u.s,
|
|
220
|
+
0 * u.km / u.s)))
|
|
221
|
+
guess = _guess_multi_params(_wave().to_value(u.cm), spectrum, config)
|
|
222
|
+
assert -60.0 <= _velocity(guess) <= 0.0
|
|
223
|
+
assert _velocity(guess) == pytest.approx(-40.0, abs=10.0)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@pytest.mark.parametrize("value", ["60 km/s", 60, ["-60 km/s", "60 kms"]])
|
|
227
|
+
def test_a_mistyped_limit_in_a_configuration_file_says_what_it_should_be(value):
|
|
228
|
+
from eclipse.main import _parse_fitting_config
|
|
229
|
+
|
|
230
|
+
fitting = {"components": [{"wavelength": "195.119 angstrom", "velocity_limits": value},
|
|
231
|
+
{"wavelength": "195.179 angstrom", "tie_center": 0}]}
|
|
232
|
+
with pytest.raises(ValueError, match="velocity_limits must be two"):
|
|
233
|
+
_parse_fitting_config({"fitting": fitting})
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_limits_set_after_a_component_is_made_are_read():
|
|
237
|
+
component = FitComponent(STRONG)
|
|
238
|
+
component.velocity_limits = (-10 * u.km / u.s, 10 * u.km / u.s)
|
|
239
|
+
component.width_limits = (0.02 * u.AA, 0.03 * u.AA)
|
|
240
|
+
rest = STRONG.to_value(u.cm)
|
|
241
|
+
assert component.centre_limits() == pytest.approx(
|
|
242
|
+
(rest * (1 - 10 / C_KM_S), rest * (1 + 10 / C_KM_S)), rel=1e-12)
|
|
243
|
+
assert component.sigma_limits() == pytest.approx((0.02e-8, 0.03e-8), rel=1e-12)
|
|
@@ -134,7 +134,9 @@ def test_an_exposure_is_shared_between_the_snapshots_it_spans(tmp_path):
|
|
|
134
134
|
# Within one snapshot's span.
|
|
135
135
|
assert series.coverage(2 * u.s, 8 * u.s) == [(0, 1.0)]
|
|
136
136
|
# A quarter in the first, three quarters in the second.
|
|
137
|
-
|
|
137
|
+
shares = series.coverage(5 * u.s, 25 * u.s)
|
|
138
|
+
assert [index for index, _ in shares] == [0, 1]
|
|
139
|
+
assert [fraction for _, fraction in shares] == pytest.approx([0.25, 0.75])
|
|
138
140
|
# The last snapshot lasts as long as the gap before it.
|
|
139
141
|
assert series.coverage(40 * u.s, 50 * u.s) == [(2, 1.0)]
|
|
140
142
|
for start, end in ((-1, 5), (45, 51), (5, 5)):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/eis_response/fit_eis_ea_2023-05-04_smooth.sav
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/grating_reflection_efficiency.dat
RENAMED
|
File without changes
|
{eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/primary_mirror_coating_reflectance.dat
RENAMED
|
File without changes
|
|
File without changes
|
{eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/throughput_aluminium_1000_angstrom.dat
RENAMED
|
File without changes
|
|
File without changes
|
{eclipse-0.13.0 → eclipse-0.14.0}/src/eclipse/data/throughput/throughput_carbon_1000_angstrom.dat
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|