enchase 2.2.0__tar.gz → 2.3.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.
- {enchase-2.2.0/enchase.egg-info → enchase-2.3.0}/PKG-INFO +20 -10
- {enchase-2.2.0 → enchase-2.3.0}/README.md +16 -6
- {enchase-2.2.0 → enchase-2.3.0}/chase/__init__.py +8 -1
- {enchase-2.2.0 → enchase-2.3.0}/chase/calib/__init__.py +14 -1
- enchase-2.3.0/chase/calib/straylight.py +271 -0
- {enchase-2.2.0 → enchase-2.3.0/enchase.egg-info}/PKG-INFO +20 -10
- {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/SOURCES.txt +2 -0
- {enchase-2.2.0 → enchase-2.3.0}/pyproject.toml +4 -4
- enchase-2.3.0/tests/test_straylight.py +120 -0
- {enchase-2.2.0 → enchase-2.3.0}/LICENSE +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/NOTICE +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/_compat.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/__init__.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/contrast.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/systematics.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/temperature.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/validation.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/calib/align.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/calib/intensity.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/calib/pointing.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/calib/spatial.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/calib/wavelength.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/cli.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/config.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/core.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/io/__init__.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/io/download.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/io/fits_io.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/io/loader.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/pipeline.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/portal.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/shifts_cache.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/tui.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/viz/__init__.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/viz/animate.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/chase/viz/diagnostics.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/enchase/__init__.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/dependency_links.txt +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/entry_points.txt +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/requires.txt +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/top_level.txt +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/setup.cfg +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/tests/test_analysis.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/tests/test_calib.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/tests/test_io.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/tests/test_limb_clv.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/tests/test_pipeline.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/tests/test_pointing.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/tests/test_systematics.py +0 -0
- {enchase-2.2.0 → enchase-2.3.0}/tests/test_validation.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: enchase
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: enCHASE: calibration and analysis pipeline for CHASE/HIS solar spectroscopy (TUI + SDK + CLI)
|
|
5
5
|
Author: Waleed Ajmal
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
9
|
-
Project-URL: Changelog, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/waleedsworld/enchase
|
|
8
|
+
Project-URL: Repository, https://github.com/waleedsworld/enchase
|
|
9
|
+
Project-URL: Changelog, https://github.com/waleedsworld/enchase/blob/main/CHANGELOG.md
|
|
10
10
|
Keywords: solar,CHASE,HIS,spectroscopy,flare,calibration,astronomy
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -45,13 +45,13 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
Calibration and flare analysis for CHASE/HIS solar spectroscopy. Install one package, point it at data, get aligned cubes, animations, contrast profiles, and temperature maps.
|
|
47
47
|
|
|
48
|
-
[](https://github.com/waleedsworld/enchase/actions/workflows/test.yml)
|
|
49
49
|
[](https://pypi.org/project/enchase/)
|
|
50
50
|

|
|
51
51
|

|
|
52
52
|
|
|
53
53
|
<p align="center">
|
|
54
|
-
<img src="https://raw.githubusercontent.com/
|
|
54
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/flare.gif" width="760" alt="Stabilised 2-panel animation of the 2023-03-29 X1.2 flare"><br>
|
|
55
55
|
<em>The 2023-03-29 X1.2 flare, stabilised by this pipeline. Photosphere left, chromosphere right.</em>
|
|
56
56
|
</p>
|
|
57
57
|
|
|
@@ -239,7 +239,16 @@ psf_stray_light(full_disc_image, p)["s_dc_range"] # scattered fract
|
|
|
239
239
|
throughput_trend(dates, ifact_per_date)["throughput_pct_per_yr"]
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
Stray light is an optional step that `run_pipeline` does not call. `fit_instrumental_profile` fits one profile to both windows at disk centre (a 0.100 Å Gaussian core plus a 1.8 Å broad term with 14.5 % of the light), and `remove_stray_light` takes that stray light from the disk-centre profile and removes the same amount from every pixel. Do not apply it off the limb, and light scattered from a pixel's immediate surroundings is not treated. Details in [docs/CALIBRATION.md](docs/CALIBRATION.md) §8, example in `examples/stray_light.py`:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from chase import fit_instrumental_profile, remove_stray_light
|
|
246
|
+
fit = fit_instrumental_profile([wav_ha, wav_fe], [dc_spec_ha, dc_spec_fe]) # both windows, one profile
|
|
247
|
+
fe = fit["windows"][1] # profile = I/Ic at disk centre
|
|
248
|
+
fe_corr = remove_stray_light(fe_cube_ic, wav_fe, fe["profile"], fit["frac"], fit["broad_fwhm"])
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The atlas factor changes by about 12 % per year, so fit it for each observation (the pipeline does this by default) rather than reusing one from another date. For Hα-only data use `wing_continuum_frac=0.75`: no Hα channel passes the 0.85 default, and `atlas_calibrate` warns about this.
|
|
243
252
|
|
|
244
253
|
## Prefer clicking to typing?
|
|
245
254
|
|
|
@@ -248,7 +257,7 @@ chase-tui
|
|
|
248
257
|
```
|
|
249
258
|
|
|
250
259
|
<p align="center">
|
|
251
|
-
<img src="https://raw.githubusercontent.com/
|
|
260
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/tui_e2e_walkthrough.gif" width="800" alt="End-to-end chase-tui walkthrough">
|
|
252
261
|
</p>
|
|
253
262
|
|
|
254
263
|
Every checkbox is one of the switches above. There is also a one-file config workflow: `chase --config my.toml`, keys mirror the flags ([examples/config.toml](examples/config.toml)).
|
|
@@ -265,13 +274,14 @@ CHASE (the Chinese Hα Solar Explorer) scans the full solar disk in Hα (118 wav
|
|
|
265
274
|
| `chase.calib.spatial` / `.align` | shift-then-crop tracking, optical-flow stabilisation | <img src="assets/diag_optical_flow.png" width="220"> |
|
|
266
275
|
| `chase.calib.wavelength` | per-frame resampling onto a common grid, spectral-drift xcorr | <img src="assets/diag_qs_spectrum.png" width="220"> |
|
|
267
276
|
| `chase.calib.intensity` | quiet-Sun norm, integral scaling, FTS-atlas absolute calibration | <img src="assets/atlas_calibration.png" width="220"> |
|
|
277
|
+
| `chase.calib.straylight` | one instrumental profile for both windows, disk-centre stray-light removal | |
|
|
268
278
|
| `chase.analysis.contrast` | wavelength-vs-time flare contrast | <img src="assets/contrast_profile.png" width="220"> |
|
|
269
279
|
| `chase.analysis.temperature` | Hα width to T (Molnar), Fe I to T (Planck / EB / Voigt) | <img src="assets/double_temp_map_peak.png" width="220"> |
|
|
270
280
|
| `chase.analysis.validation` | residual pointing (arcsec), wavelength scatter (mÅ, km/s), frame-to-frame RMS (%, cgs) | |
|
|
271
281
|
| `chase.analysis.systematics` | stray light from the off-limb profile, throughput trend of the atlas factor | |
|
|
272
282
|
|
|
273
283
|
<p align="center">
|
|
274
|
-
<img src="https://raw.githubusercontent.com/
|
|
284
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/temperature.gif" width="720" alt="Double temperature map animation"><br>
|
|
275
285
|
<em>Double temperature map: chromosphere from Hα width (~10⁴ K), photosphere from Fe I Planck inversion (~5100 K).</em>
|
|
276
286
|
</p>
|
|
277
287
|
|
|
@@ -287,7 +297,7 @@ CHASE (the Chinese Hα Solar Explorer) scans the full solar disk in Hα (118 wav
|
|
|
287
297
|
## Development
|
|
288
298
|
|
|
289
299
|
```bash
|
|
290
|
-
git clone https://github.com/
|
|
300
|
+
git clone https://github.com/waleedsworld/enchase.git
|
|
291
301
|
cd enchase
|
|
292
302
|
pip install -e '.[all]'
|
|
293
303
|
pytest -q # 27 tests, synthetic FITS fixtures, no big data needed
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Calibration and flare analysis for CHASE/HIS solar spectroscopy. Install one package, point it at data, get aligned cubes, animations, contrast profiles, and temperature maps.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/waleedsworld/enchase/actions/workflows/test.yml)
|
|
6
6
|
[](https://pypi.org/project/enchase/)
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<img src="https://raw.githubusercontent.com/
|
|
11
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/flare.gif" width="760" alt="Stabilised 2-panel animation of the 2023-03-29 X1.2 flare"><br>
|
|
12
12
|
<em>The 2023-03-29 X1.2 flare, stabilised by this pipeline. Photosphere left, chromosphere right.</em>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
@@ -196,7 +196,16 @@ psf_stray_light(full_disc_image, p)["s_dc_range"] # scattered fract
|
|
|
196
196
|
throughput_trend(dates, ifact_per_date)["throughput_pct_per_yr"]
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
Stray light is an optional step that `run_pipeline` does not call. `fit_instrumental_profile` fits one profile to both windows at disk centre (a 0.100 Å Gaussian core plus a 1.8 Å broad term with 14.5 % of the light), and `remove_stray_light` takes that stray light from the disk-centre profile and removes the same amount from every pixel. Do not apply it off the limb, and light scattered from a pixel's immediate surroundings is not treated. Details in [docs/CALIBRATION.md](docs/CALIBRATION.md) §8, example in `examples/stray_light.py`:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from chase import fit_instrumental_profile, remove_stray_light
|
|
203
|
+
fit = fit_instrumental_profile([wav_ha, wav_fe], [dc_spec_ha, dc_spec_fe]) # both windows, one profile
|
|
204
|
+
fe = fit["windows"][1] # profile = I/Ic at disk centre
|
|
205
|
+
fe_corr = remove_stray_light(fe_cube_ic, wav_fe, fe["profile"], fit["frac"], fit["broad_fwhm"])
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The atlas factor changes by about 12 % per year, so fit it for each observation (the pipeline does this by default) rather than reusing one from another date. For Hα-only data use `wing_continuum_frac=0.75`: no Hα channel passes the 0.85 default, and `atlas_calibrate` warns about this.
|
|
200
209
|
|
|
201
210
|
## Prefer clicking to typing?
|
|
202
211
|
|
|
@@ -205,7 +214,7 @@ chase-tui
|
|
|
205
214
|
```
|
|
206
215
|
|
|
207
216
|
<p align="center">
|
|
208
|
-
<img src="https://raw.githubusercontent.com/
|
|
217
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/tui_e2e_walkthrough.gif" width="800" alt="End-to-end chase-tui walkthrough">
|
|
209
218
|
</p>
|
|
210
219
|
|
|
211
220
|
Every checkbox is one of the switches above. There is also a one-file config workflow: `chase --config my.toml`, keys mirror the flags ([examples/config.toml](examples/config.toml)).
|
|
@@ -222,13 +231,14 @@ CHASE (the Chinese Hα Solar Explorer) scans the full solar disk in Hα (118 wav
|
|
|
222
231
|
| `chase.calib.spatial` / `.align` | shift-then-crop tracking, optical-flow stabilisation | <img src="assets/diag_optical_flow.png" width="220"> |
|
|
223
232
|
| `chase.calib.wavelength` | per-frame resampling onto a common grid, spectral-drift xcorr | <img src="assets/diag_qs_spectrum.png" width="220"> |
|
|
224
233
|
| `chase.calib.intensity` | quiet-Sun norm, integral scaling, FTS-atlas absolute calibration | <img src="assets/atlas_calibration.png" width="220"> |
|
|
234
|
+
| `chase.calib.straylight` | one instrumental profile for both windows, disk-centre stray-light removal | |
|
|
225
235
|
| `chase.analysis.contrast` | wavelength-vs-time flare contrast | <img src="assets/contrast_profile.png" width="220"> |
|
|
226
236
|
| `chase.analysis.temperature` | Hα width to T (Molnar), Fe I to T (Planck / EB / Voigt) | <img src="assets/double_temp_map_peak.png" width="220"> |
|
|
227
237
|
| `chase.analysis.validation` | residual pointing (arcsec), wavelength scatter (mÅ, km/s), frame-to-frame RMS (%, cgs) | |
|
|
228
238
|
| `chase.analysis.systematics` | stray light from the off-limb profile, throughput trend of the atlas factor | |
|
|
229
239
|
|
|
230
240
|
<p align="center">
|
|
231
|
-
<img src="https://raw.githubusercontent.com/
|
|
241
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/temperature.gif" width="720" alt="Double temperature map animation"><br>
|
|
232
242
|
<em>Double temperature map: chromosphere from Hα width (~10⁴ K), photosphere from Fe I Planck inversion (~5100 K).</em>
|
|
233
243
|
</p>
|
|
234
244
|
|
|
@@ -244,7 +254,7 @@ CHASE (the Chinese Hα Solar Explorer) scans the full solar disk in Hα (118 wav
|
|
|
244
254
|
## Development
|
|
245
255
|
|
|
246
256
|
```bash
|
|
247
|
-
git clone https://github.com/
|
|
257
|
+
git clone https://github.com/waleedsworld/enchase.git
|
|
248
258
|
cd enchase
|
|
249
259
|
pip install -e '.[all]'
|
|
250
260
|
pytest -q # 27 tests, synthetic FITS fixtures, no big data needed
|
|
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
from . import _compat # noqa: F401 (lzma stub + headless matplotlib)
|
|
26
26
|
|
|
27
|
-
__version__ = "2.
|
|
27
|
+
__version__ = "2.3.0"
|
|
28
28
|
|
|
29
29
|
# --- I/O -------------------------------------------------------------------
|
|
30
30
|
from .io import (
|
|
@@ -82,6 +82,11 @@ from .calib import (
|
|
|
82
82
|
rotation_shift,
|
|
83
83
|
track_shifts,
|
|
84
84
|
tie_to_header,
|
|
85
|
+
fit_instrumental_profile,
|
|
86
|
+
instrumental_profile_model,
|
|
87
|
+
load_fts_atlas,
|
|
88
|
+
remove_stray_light,
|
|
89
|
+
stray_light_veil,
|
|
85
90
|
)
|
|
86
91
|
|
|
87
92
|
# --- Analysis --------------------------------------------------------------
|
|
@@ -135,6 +140,8 @@ __all__ = [
|
|
|
135
140
|
"activity_mask", "wing_channels", "atlas_sensitivity", "atlas_factor_series",
|
|
136
141
|
"planck_inversion", "limb_darkening", "clv_factor", "clv_reference",
|
|
137
142
|
"download_clv_table", "load_clv_table", "mu_map",
|
|
143
|
+
"fit_instrumental_profile", "instrumental_profile_model", "load_fts_atlas",
|
|
144
|
+
"remove_stray_light", "stray_light_veil",
|
|
138
145
|
# analysis
|
|
139
146
|
"contrast_profile", "halpha_width_map", "halpha_width_temperature",
|
|
140
147
|
"width_to_temperature", "fe_planck_temperature", "fe_eb_temperature",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Calibration stages: spatial, alignment, wavelength, intensity.
|
|
1
|
+
"""Calibration stages: spatial, alignment, wavelength, intensity, stray light.
|
|
2
2
|
|
|
3
3
|
Every function here is usable on its own — nothing forces a full pipeline.
|
|
4
4
|
"""
|
|
@@ -58,6 +58,13 @@ from .intensity import (
|
|
|
58
58
|
quiet_sun_mask,
|
|
59
59
|
wing_channels,
|
|
60
60
|
)
|
|
61
|
+
from .straylight import (
|
|
62
|
+
fit_instrumental_profile,
|
|
63
|
+
instrumental_profile_model,
|
|
64
|
+
load_fts_atlas,
|
|
65
|
+
remove_stray_light,
|
|
66
|
+
stray_light_veil,
|
|
67
|
+
)
|
|
61
68
|
|
|
62
69
|
__all__ = [
|
|
63
70
|
"derotate",
|
|
@@ -110,4 +117,10 @@ __all__ = [
|
|
|
110
117
|
"integral_scaling",
|
|
111
118
|
"normalize_intensity",
|
|
112
119
|
"planck_inversion",
|
|
120
|
+
# stray light
|
|
121
|
+
"fit_instrumental_profile",
|
|
122
|
+
"instrumental_profile_model",
|
|
123
|
+
"load_fts_atlas",
|
|
124
|
+
"remove_stray_light",
|
|
125
|
+
"stray_light_veil",
|
|
113
126
|
]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Instrumental profile and stray light of CHASE/HIS.
|
|
2
|
+
|
|
3
|
+
The disk-center profile is compared with the FTS atlas seen through one
|
|
4
|
+
instrumental profile for both spectral windows, a narrow Gaussian core plus a
|
|
5
|
+
broad Gaussian that carries a fraction ``frac`` of the light,
|
|
6
|
+
|
|
7
|
+
T(lambda) * DN = (1 - frac) * atlas (x) G(fwhm) + frac * atlas (x) G(broad_fwhm)
|
|
8
|
+
|
|
9
|
+
where (x) is a convolution and T a low-order polynomial for the prefilter
|
|
10
|
+
slope. This is the model of Hou et al. (2020, RAA 20, 045, Eq. 1), used for
|
|
11
|
+
CHASE by Hong et al. (2022, A&A 668, A9), with their constant stray-light
|
|
12
|
+
level replaced by the broad term. With ``broad_fwhm=None`` it is their model
|
|
13
|
+
exactly. A constant level needs different values in the two windows (about
|
|
14
|
+
10 percent in Fe I and 5 percent in H-alpha), which cannot be right for two
|
|
15
|
+
windows recorded in the same exposure on the same detector. The broad term
|
|
16
|
+
fits both with one set of numbers.
|
|
17
|
+
|
|
18
|
+
The stray light is then assumed to be the same over the field of view (Hong
|
|
19
|
+
et al. 2022), so it is taken from the disk-center profile and removed from
|
|
20
|
+
every pixel,
|
|
21
|
+
|
|
22
|
+
I = (I_obs - frac * I_dc (x) G(broad_fwhm)) / (1 - frac).
|
|
23
|
+
|
|
24
|
+
This removes the broad wings of the profile. The narrow core is left in.
|
|
25
|
+
|
|
26
|
+
Limits. The level is set at disk center, so the correction takes away too
|
|
27
|
+
much close to and beyond the limb and must not be used on off-limb features.
|
|
28
|
+
Light scattered from the immediate surroundings of a pixel is not treated.
|
|
29
|
+
|
|
30
|
+
Functions
|
|
31
|
+
---------
|
|
32
|
+
load_fts_atlas FTS disk-center atlas, I/Ic (ISPy or an idlsave file)
|
|
33
|
+
instrumental_profile_model right-hand side of the first equation above
|
|
34
|
+
fit_instrumental_profile fit of the profile at disk center, one or several windows
|
|
35
|
+
stray_light_veil frac * I_dc (x) G(broad_fwhm)
|
|
36
|
+
remove_stray_light the correction
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from typing import Optional, Sequence, Tuple, Union
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
from scipy.ndimage import gaussian_filter1d, uniform_filter1d
|
|
44
|
+
|
|
45
|
+
__all__ = ["load_fts_atlas", "instrumental_profile_model", "fit_instrumental_profile",
|
|
46
|
+
"stray_light_veil", "remove_stray_light",
|
|
47
|
+
"DEFAULT_FWHM", "DEFAULT_BROAD_FWHM", "DEFAULT_FRAC", "CAL_MASK"]
|
|
48
|
+
|
|
49
|
+
DEFAULT_FWHM = 0.100 # A, Gaussian core (before the detector pixel)
|
|
50
|
+
DEFAULT_BROAD_FWHM = 1.8 # A, broad term
|
|
51
|
+
DEFAULT_FRAC = 0.145 # fraction of the light in the broad term
|
|
52
|
+
|
|
53
|
+
# Wavelengths left out of the fit (center, half width, in A). Telluric lines
|
|
54
|
+
# and weak blends that differ between the ground-based atlas and CHASE.
|
|
55
|
+
CAL_MASK = [(6559.56, 0.14), (6560.22, 0.12), (6561.10, 0.13), (6564.04, 0.12),
|
|
56
|
+
(6564.18, 0.14), (6565.51, 0.12), (6565.88, 0.12), (6568.71, 0.10),
|
|
57
|
+
(6568.83, 0.10)]
|
|
58
|
+
|
|
59
|
+
_FWHM_TO_SIGMA = 1.0 / (2.0 * np.sqrt(2.0 * np.log(2.0)))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_fts_atlas(w0: float, w1: float, atlas_file: Optional[str] = None
|
|
63
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
64
|
+
"""FTS disk-center atlas between w0 and w1 (A), as (wavelength, I/Ic).
|
|
65
|
+
|
|
66
|
+
Wavelengths are in the observer frame (no gravitational redshift taken
|
|
67
|
+
out). Without ``atlas_file`` the atlas shipped with ISPy is used
|
|
68
|
+
(https://github.com/ISP-SST/ISPy). ``atlas_file`` can point at an
|
|
69
|
+
``fts_disk_center*.idlsave`` file instead.
|
|
70
|
+
"""
|
|
71
|
+
if atlas_file is None:
|
|
72
|
+
try:
|
|
73
|
+
from ISPy.spec import atlas as isp_atlas
|
|
74
|
+
except ImportError as e:
|
|
75
|
+
raise ImportError(
|
|
76
|
+
"load_fts_atlas needs ISPy (pip install "
|
|
77
|
+
"git+https://github.com/ISP-SST/ISPy) or atlas_file=...") from e
|
|
78
|
+
fts = isp_atlas.atlas()
|
|
79
|
+
wav, spec, cont = fts.wave, fts.spec, fts.cont
|
|
80
|
+
else:
|
|
81
|
+
from scipy.io import readsav
|
|
82
|
+
sav = {k.lower(): v for k, v in readsav(atlas_file).items()}
|
|
83
|
+
wav = sav["ftswav"]
|
|
84
|
+
spec = sav.get("ftsint_si", sav.get("ftsint"))
|
|
85
|
+
cont = sav.get("ftscnt_si", sav.get("ftscnt"))
|
|
86
|
+
keep = (wav >= w0) & (wav <= w1)
|
|
87
|
+
return (np.asarray(wav[keep], float),
|
|
88
|
+
np.asarray(spec[keep], float) / np.asarray(cont[keep], float))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _smeared_atlas(atlas_wav, atlas_int, fwhm, pixel):
|
|
92
|
+
"""Atlas convolved with a Gaussian of this FWHM and one detector pixel."""
|
|
93
|
+
step = float(np.median(np.diff(atlas_wav)))
|
|
94
|
+
out = gaussian_filter1d(atlas_int, fwhm * _FWHM_TO_SIGMA / step,
|
|
95
|
+
mode="reflect", truncate=4.0)
|
|
96
|
+
npix = max(1, int(round(pixel / step)))
|
|
97
|
+
return uniform_filter1d(out, npix, mode="reflect")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def instrumental_profile_model(wav, atlas_wav, atlas_int, fwhm: float = DEFAULT_FWHM,
|
|
101
|
+
frac: float = DEFAULT_FRAC,
|
|
102
|
+
broad_fwhm: Optional[float] = DEFAULT_BROAD_FWHM,
|
|
103
|
+
shift: float = 0.0, pixel: Optional[float] = None) -> np.ndarray:
|
|
104
|
+
"""Atlas seen through the instrumental profile, I/Ic on ``wav``.
|
|
105
|
+
|
|
106
|
+
``(1 - frac) * atlas (x) G(fwhm) + frac * atlas (x) G(broad_fwhm)``,
|
|
107
|
+
evaluated at ``wav + shift``. ``broad_fwhm=None`` gives the constant
|
|
108
|
+
stray-light level of Hou et al. (2020), ``(1 - frac) * ... + frac``.
|
|
109
|
+
``pixel`` is the detector pixel in A (default, the step of ``wav``).
|
|
110
|
+
"""
|
|
111
|
+
wav = np.asarray(wav, float)
|
|
112
|
+
pixel = float(np.median(np.diff(wav))) if pixel is None else float(pixel)
|
|
113
|
+
core = np.interp(wav + shift, atlas_wav,
|
|
114
|
+
_smeared_atlas(atlas_wav, atlas_int, fwhm, pixel))
|
|
115
|
+
if broad_fwhm is None:
|
|
116
|
+
return (1.0 - frac) * core + frac
|
|
117
|
+
broad = np.interp(wav + shift, atlas_wav,
|
|
118
|
+
_smeared_atlas(atlas_wav, atlas_int, broad_fwhm, pixel))
|
|
119
|
+
return (1.0 - frac) * core + frac * broad
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _grid(value):
|
|
123
|
+
return np.atleast_1d(np.asarray([np.nan] if value is None else value, float))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def fit_instrumental_profile(wav, spectrum, atlas: Optional[Tuple[np.ndarray, np.ndarray]] = None,
|
|
127
|
+
fwhm: Union[float, Sequence[float]] = DEFAULT_FWHM,
|
|
128
|
+
frac: Union[float, Sequence[float]] = DEFAULT_FRAC,
|
|
129
|
+
broad_fwhm: Union[None, float, Sequence[float]] = DEFAULT_BROAD_FWHM,
|
|
130
|
+
poly_deg: Optional[int] = None,
|
|
131
|
+
mask: Sequence[Tuple[float, float]] = CAL_MASK,
|
|
132
|
+
shifts: Optional[Sequence[float]] = None,
|
|
133
|
+
atlas_file: Optional[str] = None, verbose: bool = False) -> dict:
|
|
134
|
+
"""Fit the instrumental profile to a disk-center spectrum.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
wav, spectrum : array (nwav,), or lists of such arrays
|
|
139
|
+
Mean disk-center spectrum of one window, in any units (DN is fine).
|
|
140
|
+
Pass lists, e.g. ``[wav_ha, wav_fe], [spec_ha, spec_fe]``, to fit
|
|
141
|
+
several windows with ONE profile. Each window then gets its own
|
|
142
|
+
wavelength offset and polynomial, and the windows have equal weight.
|
|
143
|
+
atlas : (wavelength, I/Ic), optional
|
|
144
|
+
Reference atlas. Default, :func:`load_fts_atlas` over the windows.
|
|
145
|
+
fwhm, frac, broad_fwhm : float or sequence of float
|
|
146
|
+
Core FWHM (A), fraction of the light in the broad term, and FWHM of
|
|
147
|
+
the broad term (A). A single number holds that parameter fixed, a
|
|
148
|
+
sequence is searched as a grid. ``broad_fwhm=None`` is the constant
|
|
149
|
+
level of Hou et al. (2020), where ``frac`` is their alpha.
|
|
150
|
+
poly_deg : int, optional
|
|
151
|
+
Degree of the transmission polynomial. Default 3 for windows wider
|
|
152
|
+
than 4 A (H-alpha) and 2 otherwise (Fe I).
|
|
153
|
+
mask : sequence of (center, half width)
|
|
154
|
+
Wavelengths left out of the fit.
|
|
155
|
+
shifts : sequence of float, optional
|
|
156
|
+
Wavelength offsets to try (A). Default -0.2 to 0.2 in steps of 0.002.
|
|
157
|
+
verbose : bool
|
|
158
|
+
Print one line per core width while a grid is searched.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
dict with the best ``fwhm``, ``frac``, ``broad_fwhm``, the summed
|
|
163
|
+
``chi2`` (mean squared residual, added over windows) and ``windows``, a
|
|
164
|
+
list with one dict per window holding ``shift``, ``poly`` and ``wmean``
|
|
165
|
+
(transmission = polyval(poly, wav - wmean)), ``transmission``,
|
|
166
|
+
``profile`` (= transmission * spectrum, the observed I/Ic), ``model``,
|
|
167
|
+
``rms`` (of profile - model over the fitted points) and ``keep``. For a
|
|
168
|
+
single window these entries are also at the top level.
|
|
169
|
+
"""
|
|
170
|
+
single = np.ndim(wav[0]) == 0
|
|
171
|
+
wavs = [np.asarray(wav, float)] if single else [np.asarray(w, float) for w in wav]
|
|
172
|
+
specs = [np.asarray(spectrum, float)] if single else [np.asarray(s, float) for s in spectrum]
|
|
173
|
+
shifts = np.arange(-0.200, 0.2001, 0.002) if shifts is None else np.asarray(shifts, float)
|
|
174
|
+
fwhms, fracs, broads = _grid(fwhm), _grid(frac), _grid(broad_fwhm)
|
|
175
|
+
flat = broad_fwhm is None
|
|
176
|
+
|
|
177
|
+
if atlas is None:
|
|
178
|
+
margin = 2.0 + (0.0 if flat else 2.5 * float(np.nanmax(broads)))
|
|
179
|
+
atlas = load_fts_atlas(min(w[0] for w in wavs) - margin,
|
|
180
|
+
max(w[-1] for w in wavs) + margin, atlas_file)
|
|
181
|
+
awav, aint = np.asarray(atlas[0], float), np.asarray(atlas[1], float)
|
|
182
|
+
|
|
183
|
+
setup = []
|
|
184
|
+
for w in wavs:
|
|
185
|
+
keep = np.ones(w.size, bool)
|
|
186
|
+
for lam, half in mask:
|
|
187
|
+
keep &= np.abs(w - lam) > half
|
|
188
|
+
deg = poly_deg if poly_deg is not None else (3 if w[-1] - w[0] > 4.0 else 2)
|
|
189
|
+
setup.append((keep, deg, w - w.mean(), float(np.median(np.diff(w)))))
|
|
190
|
+
|
|
191
|
+
best = None
|
|
192
|
+
for i, fw in enumerate(fwhms):
|
|
193
|
+
cores = [_smeared_atlas(awav, aint, fw, s[3]) for s in setup]
|
|
194
|
+
for bw in broads:
|
|
195
|
+
wings = [None if flat else _smeared_atlas(awav, aint, bw, s[3]) for s in setup]
|
|
196
|
+
for fr in fracs:
|
|
197
|
+
total, found = 0.0, []
|
|
198
|
+
for w, sp, (keep, deg, x, _), core, wing in zip(wavs, specs, setup, cores, wings):
|
|
199
|
+
win = None
|
|
200
|
+
for sh in shifts:
|
|
201
|
+
mod = (1.0 - fr) * np.interp(w + sh, awav, core)
|
|
202
|
+
mod = mod + (fr if flat else fr * np.interp(w + sh, awav, wing))
|
|
203
|
+
poly = np.polyfit(x[keep], (mod / sp)[keep], deg)
|
|
204
|
+
ms = float(np.mean((sp * np.polyval(poly, x) - mod)[keep] ** 2))
|
|
205
|
+
if win is None or ms < win[0]:
|
|
206
|
+
win = (ms, sh, poly, mod)
|
|
207
|
+
total += win[0]
|
|
208
|
+
found.append(win)
|
|
209
|
+
if best is None or total < best[0]:
|
|
210
|
+
best = (total, fw, fr, bw, found)
|
|
211
|
+
if verbose and fwhms.size > 1:
|
|
212
|
+
print("fit_instrumental_profile: core %.3f A (%d/%d), best so far %.3f A"
|
|
213
|
+
% (fw, i + 1, fwhms.size, best[1]), flush=True)
|
|
214
|
+
|
|
215
|
+
total, fw, fr, bw, found = best
|
|
216
|
+
out = dict(fwhm=float(fw), frac=float(fr),
|
|
217
|
+
broad_fwhm=None if flat else float(bw), chi2=float(total), windows=[])
|
|
218
|
+
for w, sp, (keep, deg, x, _), (ms, sh, poly, mod) in zip(wavs, specs, setup, found):
|
|
219
|
+
trans = np.polyval(poly, x)
|
|
220
|
+
out["windows"].append(dict(shift=float(sh), poly=poly, wmean=float(w.mean()),
|
|
221
|
+
transmission=trans, profile=sp * trans, model=mod,
|
|
222
|
+
rms=float(np.sqrt(ms)), keep=keep))
|
|
223
|
+
if single:
|
|
224
|
+
out.update(out["windows"][0])
|
|
225
|
+
return out
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def stray_light_veil(wav, dc_profile, frac: float = DEFAULT_FRAC,
|
|
229
|
+
broad_fwhm: Optional[float] = DEFAULT_BROAD_FWHM) -> np.ndarray:
|
|
230
|
+
"""The stray light, ``frac * dc_profile (x) G(broad_fwhm)``.
|
|
231
|
+
|
|
232
|
+
``dc_profile`` is the observed disk-center profile of one window on the
|
|
233
|
+
regular grid ``wav``. The result has its units. The window is padded with
|
|
234
|
+
its edge values for the convolution. ``broad_fwhm=None`` gives the
|
|
235
|
+
constant level ``frac`` (for a profile normalized to the continuum).
|
|
236
|
+
"""
|
|
237
|
+
dc_profile = np.asarray(dc_profile, float)
|
|
238
|
+
if broad_fwhm is None:
|
|
239
|
+
return np.full(dc_profile.shape, float(frac))
|
|
240
|
+
step = float(np.median(np.diff(np.asarray(wav, float))))
|
|
241
|
+
return frac * gaussian_filter1d(dc_profile, broad_fwhm * _FWHM_TO_SIGMA / step,
|
|
242
|
+
mode="nearest", truncate=4.0)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def remove_stray_light(cube, wav, dc_profile, frac: float = DEFAULT_FRAC,
|
|
246
|
+
broad_fwhm: Optional[float] = DEFAULT_BROAD_FWHM) -> np.ndarray:
|
|
247
|
+
"""Remove the stray light from a cube, the same amount in every pixel.
|
|
248
|
+
|
|
249
|
+
``(cube - frac * dc_profile (x) G(broad_fwhm)) / (1 - frac)``
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
cube : array (nwav, ...)
|
|
254
|
+
Observed intensities of one window, wavelength first.
|
|
255
|
+
wav : array (nwav,)
|
|
256
|
+
Its wavelengths (regular grid, A).
|
|
257
|
+
dc_profile : array (nwav,)
|
|
258
|
+
Observed mean profile at disk center, same wavelengths and SAME UNITS
|
|
259
|
+
as ``cube`` (both multiplied by the transmission of
|
|
260
|
+
:func:`fit_instrumental_profile`, or both calibrated with the same factor).
|
|
261
|
+
frac, broad_fwhm : float
|
|
262
|
+
As fitted or adopted in :func:`fit_instrumental_profile`.
|
|
263
|
+
|
|
264
|
+
Notes
|
|
265
|
+
-----
|
|
266
|
+
At disk center this turns the observed profile into the one the fit
|
|
267
|
+
describes with the narrow core alone. Do not use it on off-limb pixels.
|
|
268
|
+
"""
|
|
269
|
+
cube = np.asarray(cube, float)
|
|
270
|
+
veil = stray_light_veil(wav, dc_profile, frac, broad_fwhm)
|
|
271
|
+
return (cube - veil.reshape((-1,) + (1,) * (cube.ndim - 1))) / (1.0 - frac)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: enchase
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: enCHASE: calibration and analysis pipeline for CHASE/HIS solar spectroscopy (TUI + SDK + CLI)
|
|
5
5
|
Author: Waleed Ajmal
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
9
|
-
Project-URL: Changelog, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/waleedsworld/enchase
|
|
8
|
+
Project-URL: Repository, https://github.com/waleedsworld/enchase
|
|
9
|
+
Project-URL: Changelog, https://github.com/waleedsworld/enchase/blob/main/CHANGELOG.md
|
|
10
10
|
Keywords: solar,CHASE,HIS,spectroscopy,flare,calibration,astronomy
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -45,13 +45,13 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
Calibration and flare analysis for CHASE/HIS solar spectroscopy. Install one package, point it at data, get aligned cubes, animations, contrast profiles, and temperature maps.
|
|
47
47
|
|
|
48
|
-
[](https://github.com/waleedsworld/enchase/actions/workflows/test.yml)
|
|
49
49
|
[](https://pypi.org/project/enchase/)
|
|
50
50
|

|
|
51
51
|

|
|
52
52
|
|
|
53
53
|
<p align="center">
|
|
54
|
-
<img src="https://raw.githubusercontent.com/
|
|
54
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/flare.gif" width="760" alt="Stabilised 2-panel animation of the 2023-03-29 X1.2 flare"><br>
|
|
55
55
|
<em>The 2023-03-29 X1.2 flare, stabilised by this pipeline. Photosphere left, chromosphere right.</em>
|
|
56
56
|
</p>
|
|
57
57
|
|
|
@@ -239,7 +239,16 @@ psf_stray_light(full_disc_image, p)["s_dc_range"] # scattered fract
|
|
|
239
239
|
throughput_trend(dates, ifact_per_date)["throughput_pct_per_yr"]
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
Stray light is an optional step that `run_pipeline` does not call. `fit_instrumental_profile` fits one profile to both windows at disk centre (a 0.100 Å Gaussian core plus a 1.8 Å broad term with 14.5 % of the light), and `remove_stray_light` takes that stray light from the disk-centre profile and removes the same amount from every pixel. Do not apply it off the limb, and light scattered from a pixel's immediate surroundings is not treated. Details in [docs/CALIBRATION.md](docs/CALIBRATION.md) §8, example in `examples/stray_light.py`:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from chase import fit_instrumental_profile, remove_stray_light
|
|
246
|
+
fit = fit_instrumental_profile([wav_ha, wav_fe], [dc_spec_ha, dc_spec_fe]) # both windows, one profile
|
|
247
|
+
fe = fit["windows"][1] # profile = I/Ic at disk centre
|
|
248
|
+
fe_corr = remove_stray_light(fe_cube_ic, wav_fe, fe["profile"], fit["frac"], fit["broad_fwhm"])
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The atlas factor changes by about 12 % per year, so fit it for each observation (the pipeline does this by default) rather than reusing one from another date. For Hα-only data use `wing_continuum_frac=0.75`: no Hα channel passes the 0.85 default, and `atlas_calibrate` warns about this.
|
|
243
252
|
|
|
244
253
|
## Prefer clicking to typing?
|
|
245
254
|
|
|
@@ -248,7 +257,7 @@ chase-tui
|
|
|
248
257
|
```
|
|
249
258
|
|
|
250
259
|
<p align="center">
|
|
251
|
-
<img src="https://raw.githubusercontent.com/
|
|
260
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/tui_e2e_walkthrough.gif" width="800" alt="End-to-end chase-tui walkthrough">
|
|
252
261
|
</p>
|
|
253
262
|
|
|
254
263
|
Every checkbox is one of the switches above. There is also a one-file config workflow: `chase --config my.toml`, keys mirror the flags ([examples/config.toml](examples/config.toml)).
|
|
@@ -265,13 +274,14 @@ CHASE (the Chinese Hα Solar Explorer) scans the full solar disk in Hα (118 wav
|
|
|
265
274
|
| `chase.calib.spatial` / `.align` | shift-then-crop tracking, optical-flow stabilisation | <img src="assets/diag_optical_flow.png" width="220"> |
|
|
266
275
|
| `chase.calib.wavelength` | per-frame resampling onto a common grid, spectral-drift xcorr | <img src="assets/diag_qs_spectrum.png" width="220"> |
|
|
267
276
|
| `chase.calib.intensity` | quiet-Sun norm, integral scaling, FTS-atlas absolute calibration | <img src="assets/atlas_calibration.png" width="220"> |
|
|
277
|
+
| `chase.calib.straylight` | one instrumental profile for both windows, disk-centre stray-light removal | |
|
|
268
278
|
| `chase.analysis.contrast` | wavelength-vs-time flare contrast | <img src="assets/contrast_profile.png" width="220"> |
|
|
269
279
|
| `chase.analysis.temperature` | Hα width to T (Molnar), Fe I to T (Planck / EB / Voigt) | <img src="assets/double_temp_map_peak.png" width="220"> |
|
|
270
280
|
| `chase.analysis.validation` | residual pointing (arcsec), wavelength scatter (mÅ, km/s), frame-to-frame RMS (%, cgs) | |
|
|
271
281
|
| `chase.analysis.systematics` | stray light from the off-limb profile, throughput trend of the atlas factor | |
|
|
272
282
|
|
|
273
283
|
<p align="center">
|
|
274
|
-
<img src="https://raw.githubusercontent.com/
|
|
284
|
+
<img src="https://raw.githubusercontent.com/waleedsworld/enchase/main/assets/temperature.gif" width="720" alt="Double temperature map animation"><br>
|
|
275
285
|
<em>Double temperature map: chromosphere from Hα width (~10⁴ K), photosphere from Fe I Planck inversion (~5100 K).</em>
|
|
276
286
|
</p>
|
|
277
287
|
|
|
@@ -287,7 +297,7 @@ CHASE (the Chinese Hα Solar Explorer) scans the full solar disk in Hα (118 wav
|
|
|
287
297
|
## Development
|
|
288
298
|
|
|
289
299
|
```bash
|
|
290
|
-
git clone https://github.com/
|
|
300
|
+
git clone https://github.com/waleedsworld/enchase.git
|
|
291
301
|
cd enchase
|
|
292
302
|
pip install -e '.[all]'
|
|
293
303
|
pytest -q # 27 tests, synthetic FITS fixtures, no big data needed
|
|
@@ -21,6 +21,7 @@ chase/calib/align.py
|
|
|
21
21
|
chase/calib/intensity.py
|
|
22
22
|
chase/calib/pointing.py
|
|
23
23
|
chase/calib/spatial.py
|
|
24
|
+
chase/calib/straylight.py
|
|
24
25
|
chase/calib/wavelength.py
|
|
25
26
|
chase/io/__init__.py
|
|
26
27
|
chase/io/download.py
|
|
@@ -42,5 +43,6 @@ tests/test_io.py
|
|
|
42
43
|
tests/test_limb_clv.py
|
|
43
44
|
tests/test_pipeline.py
|
|
44
45
|
tests/test_pointing.py
|
|
46
|
+
tests/test_straylight.py
|
|
45
47
|
tests/test_systematics.py
|
|
46
48
|
tests/test_validation.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "enchase"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "enCHASE: calibration and analysis pipeline for CHASE/HIS solar spectroscopy (TUI + SDK + CLI)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -43,9 +43,9 @@ chase-tui = "chase.tui:main"
|
|
|
43
43
|
chase-portal = "chase.portal:main"
|
|
44
44
|
|
|
45
45
|
[project.urls]
|
|
46
|
-
Homepage = "https://github.com/
|
|
47
|
-
Repository = "https://github.com/
|
|
48
|
-
Changelog = "https://github.com/
|
|
46
|
+
Homepage = "https://github.com/waleedsworld/enchase"
|
|
47
|
+
Repository = "https://github.com/waleedsworld/enchase"
|
|
48
|
+
Changelog = "https://github.com/waleedsworld/enchase/blob/main/CHANGELOG.md"
|
|
49
49
|
|
|
50
50
|
[tool.setuptools.packages.find]
|
|
51
51
|
include = ["chase*", "enchase*"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Instrumental profile and stray light (chase.calib.straylight).
|
|
2
|
+
|
|
3
|
+
Everything runs on a synthetic atlas, so neither ISPy nor real data is needed.
|
|
4
|
+
The synthetic "observation" is built with the same forward model the fit uses,
|
|
5
|
+
multiplied by a known transmission polynomial, so the fit must give back the
|
|
6
|
+
numbers that went in.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from chase.calib import (
|
|
13
|
+
fit_instrumental_profile,
|
|
14
|
+
instrumental_profile_model,
|
|
15
|
+
remove_stray_light,
|
|
16
|
+
stray_light_veil,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _atlas():
|
|
21
|
+
"""Continuum normalized atlas on a 0.0078 A grid: H-alpha-like and Fe I-like lines."""
|
|
22
|
+
wav = np.arange(6550.0, 6580.0, 0.0078125)
|
|
23
|
+
spec = np.ones_like(wav)
|
|
24
|
+
for core, depth, width in ((6562.8, 0.84, 0.45), (6569.21, 0.65, 0.06),
|
|
25
|
+
(6560.58, 0.20, 0.05), (6564.20, 0.05, 0.03)):
|
|
26
|
+
spec *= 1.0 - depth * np.exp(-0.5 * ((wav - core) / width) ** 2)
|
|
27
|
+
return wav, spec
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _window(w0, w1, n):
|
|
31
|
+
return np.linspace(w0, w1, n)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
WAV_HA = _window(6559.4, 6565.1, 118)
|
|
35
|
+
WAV_FE = _window(6567.5, 6569.7, 46)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _observe(wav, atlas, fwhm, frac, broad, shift, poly):
|
|
39
|
+
"""DN spectrum whose transmission-corrected profile is the model."""
|
|
40
|
+
model = instrumental_profile_model(wav, *atlas, fwhm=fwhm, frac=frac,
|
|
41
|
+
broad_fwhm=broad, shift=shift)
|
|
42
|
+
trans = np.polyval(poly, wav - wav.mean())
|
|
43
|
+
return model / trans
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_constant_level_is_hou_model():
|
|
47
|
+
"""broad_fwhm=None is Hou et al. 2020 Eq. 1: (1 - a) atlas (x) G + a."""
|
|
48
|
+
atlas = _atlas()
|
|
49
|
+
core = instrumental_profile_model(WAV_FE, *atlas, fwhm=0.1, frac=0.0)
|
|
50
|
+
flat = instrumental_profile_model(WAV_FE, *atlas, fwhm=0.1, frac=0.1, broad_fwhm=None)
|
|
51
|
+
assert np.allclose(flat, 0.9 * core + 0.1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_fit_recovers_one_profile_for_both_windows():
|
|
55
|
+
atlas = _atlas()
|
|
56
|
+
truth = dict(fwhm=0.105, frac=0.12, broad=2.4)
|
|
57
|
+
dn_ha = _observe(WAV_HA, atlas, truth["fwhm"], truth["frac"], truth["broad"],
|
|
58
|
+
-0.004, [2e-6, -1e-5, 1e-3])
|
|
59
|
+
dn_fe = _observe(WAV_FE, atlas, truth["fwhm"], truth["frac"], truth["broad"],
|
|
60
|
+
0.006, [5e-5, 2e-3])
|
|
61
|
+
r = fit_instrumental_profile([WAV_HA, WAV_FE], [dn_ha, dn_fe], atlas=atlas, mask=[],
|
|
62
|
+
fwhm=np.arange(0.09, 0.1201, 0.005),
|
|
63
|
+
broad_fwhm=np.arange(2.0, 2.81, 0.2),
|
|
64
|
+
frac=np.arange(0.10, 0.1401, 0.01),
|
|
65
|
+
shifts=np.arange(-0.01, 0.0101, 0.002))
|
|
66
|
+
assert r["fwhm"] == pytest.approx(truth["fwhm"], abs=1e-6)
|
|
67
|
+
assert r["broad_fwhm"] == pytest.approx(truth["broad"], abs=1e-6)
|
|
68
|
+
assert r["frac"] == pytest.approx(truth["frac"], abs=1e-6)
|
|
69
|
+
assert [w["shift"] for w in r["windows"]] == pytest.approx([-0.004, 0.006], abs=1e-9)
|
|
70
|
+
assert max(w["rms"] for w in r["windows"]) < 1e-4
|
|
71
|
+
# the returned transmission puts the window back on the model
|
|
72
|
+
for w in r["windows"]:
|
|
73
|
+
assert np.allclose(w["profile"], w["model"], atol=1e-4)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_single_window_returns_flat_dict():
|
|
77
|
+
atlas = _atlas()
|
|
78
|
+
dn = _observe(WAV_FE, atlas, 0.1, 0.1, None, 0.0, [1e-3])
|
|
79
|
+
r = fit_instrumental_profile(WAV_FE, dn, atlas=atlas, broad_fwhm=None, mask=[],
|
|
80
|
+
fwhm=np.arange(0.08, 0.1201, 0.01),
|
|
81
|
+
frac=np.arange(0.06, 0.1401, 0.02),
|
|
82
|
+
shifts=[0.0])
|
|
83
|
+
assert r["broad_fwhm"] is None
|
|
84
|
+
assert r["fwhm"] == pytest.approx(0.1) and r["frac"] == pytest.approx(0.1)
|
|
85
|
+
assert "rms" in r and len(r["windows"]) == 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_correction_leaves_only_the_core_at_disk_center():
|
|
89
|
+
"""At disk center the corrected profile is the atlas seen through the core alone."""
|
|
90
|
+
atlas = _atlas()
|
|
91
|
+
dc = instrumental_profile_model(WAV_HA, *atlas, fwhm=0.1, frac=0.145, broad_fwhm=1.8)
|
|
92
|
+
core = instrumental_profile_model(WAV_HA, *atlas, fwhm=0.1, frac=0.0)
|
|
93
|
+
corrected = remove_stray_light(dc[:, None, None], WAV_HA, dc)[:, 0, 0]
|
|
94
|
+
# the veil is built from the observed dc profile, not the atlas, so the match
|
|
95
|
+
# holds to second order in frac
|
|
96
|
+
assert np.max(np.abs(corrected - core)) < 0.01
|
|
97
|
+
assert np.max(np.abs(corrected - core)) < 0.2 * np.max(np.abs(dc - core))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_same_veil_in_every_pixel():
|
|
101
|
+
"""Pixels that differ by a constant offset stay apart by offset / (1 - frac)."""
|
|
102
|
+
atlas = _atlas()
|
|
103
|
+
dc = instrumental_profile_model(WAV_FE, *atlas)
|
|
104
|
+
cube = np.stack([dc, dc - 0.3, dc + 0.1], axis=1)
|
|
105
|
+
out = remove_stray_light(cube, WAV_FE, dc, frac=0.145)
|
|
106
|
+
assert np.allclose(out[:, 0] - out[:, 1], 0.3 / 0.855)
|
|
107
|
+
assert np.allclose(out[:, 2] - out[:, 0], 0.1 / 0.855)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_veil_shapes_and_flat_option():
|
|
111
|
+
atlas = _atlas()
|
|
112
|
+
dc = instrumental_profile_model(WAV_HA, *atlas)
|
|
113
|
+
veil = stray_light_veil(WAV_HA, dc, 0.145, 1.8)
|
|
114
|
+
assert veil.shape == dc.shape
|
|
115
|
+
# a smoothed spectrum: shallower than the profile, same mean to a few percent
|
|
116
|
+
assert veil.min() / 0.145 > dc.min()
|
|
117
|
+
assert abs(veil.mean() / 0.145 - dc.mean()) < 0.03
|
|
118
|
+
assert np.allclose(stray_light_veil(WAV_HA, dc, 0.05, None), 0.05)
|
|
119
|
+
cube4 = np.ones((WAV_HA.size, 2, 3, 4))
|
|
120
|
+
assert remove_stray_light(cube4, WAV_HA, dc).shape == cube4.shape
|
|
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
|