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.
Files changed (50) hide show
  1. {enchase-2.2.0/enchase.egg-info → enchase-2.3.0}/PKG-INFO +20 -10
  2. {enchase-2.2.0 → enchase-2.3.0}/README.md +16 -6
  3. {enchase-2.2.0 → enchase-2.3.0}/chase/__init__.py +8 -1
  4. {enchase-2.2.0 → enchase-2.3.0}/chase/calib/__init__.py +14 -1
  5. enchase-2.3.0/chase/calib/straylight.py +271 -0
  6. {enchase-2.2.0 → enchase-2.3.0/enchase.egg-info}/PKG-INFO +20 -10
  7. {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/SOURCES.txt +2 -0
  8. {enchase-2.2.0 → enchase-2.3.0}/pyproject.toml +4 -4
  9. enchase-2.3.0/tests/test_straylight.py +120 -0
  10. {enchase-2.2.0 → enchase-2.3.0}/LICENSE +0 -0
  11. {enchase-2.2.0 → enchase-2.3.0}/NOTICE +0 -0
  12. {enchase-2.2.0 → enchase-2.3.0}/chase/_compat.py +0 -0
  13. {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/__init__.py +0 -0
  14. {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/contrast.py +0 -0
  15. {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/systematics.py +0 -0
  16. {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/temperature.py +0 -0
  17. {enchase-2.2.0 → enchase-2.3.0}/chase/analysis/validation.py +0 -0
  18. {enchase-2.2.0 → enchase-2.3.0}/chase/calib/align.py +0 -0
  19. {enchase-2.2.0 → enchase-2.3.0}/chase/calib/intensity.py +0 -0
  20. {enchase-2.2.0 → enchase-2.3.0}/chase/calib/pointing.py +0 -0
  21. {enchase-2.2.0 → enchase-2.3.0}/chase/calib/spatial.py +0 -0
  22. {enchase-2.2.0 → enchase-2.3.0}/chase/calib/wavelength.py +0 -0
  23. {enchase-2.2.0 → enchase-2.3.0}/chase/cli.py +0 -0
  24. {enchase-2.2.0 → enchase-2.3.0}/chase/config.py +0 -0
  25. {enchase-2.2.0 → enchase-2.3.0}/chase/core.py +0 -0
  26. {enchase-2.2.0 → enchase-2.3.0}/chase/io/__init__.py +0 -0
  27. {enchase-2.2.0 → enchase-2.3.0}/chase/io/download.py +0 -0
  28. {enchase-2.2.0 → enchase-2.3.0}/chase/io/fits_io.py +0 -0
  29. {enchase-2.2.0 → enchase-2.3.0}/chase/io/loader.py +0 -0
  30. {enchase-2.2.0 → enchase-2.3.0}/chase/pipeline.py +0 -0
  31. {enchase-2.2.0 → enchase-2.3.0}/chase/portal.py +0 -0
  32. {enchase-2.2.0 → enchase-2.3.0}/chase/shifts_cache.py +0 -0
  33. {enchase-2.2.0 → enchase-2.3.0}/chase/tui.py +0 -0
  34. {enchase-2.2.0 → enchase-2.3.0}/chase/viz/__init__.py +0 -0
  35. {enchase-2.2.0 → enchase-2.3.0}/chase/viz/animate.py +0 -0
  36. {enchase-2.2.0 → enchase-2.3.0}/chase/viz/diagnostics.py +0 -0
  37. {enchase-2.2.0 → enchase-2.3.0}/enchase/__init__.py +0 -0
  38. {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/dependency_links.txt +0 -0
  39. {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/entry_points.txt +0 -0
  40. {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/requires.txt +0 -0
  41. {enchase-2.2.0 → enchase-2.3.0}/enchase.egg-info/top_level.txt +0 -0
  42. {enchase-2.2.0 → enchase-2.3.0}/setup.cfg +0 -0
  43. {enchase-2.2.0 → enchase-2.3.0}/tests/test_analysis.py +0 -0
  44. {enchase-2.2.0 → enchase-2.3.0}/tests/test_calib.py +0 -0
  45. {enchase-2.2.0 → enchase-2.3.0}/tests/test_io.py +0 -0
  46. {enchase-2.2.0 → enchase-2.3.0}/tests/test_limb_clv.py +0 -0
  47. {enchase-2.2.0 → enchase-2.3.0}/tests/test_pipeline.py +0 -0
  48. {enchase-2.2.0 → enchase-2.3.0}/tests/test_pointing.py +0 -0
  49. {enchase-2.2.0 → enchase-2.3.0}/tests/test_systematics.py +0 -0
  50. {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.2.0
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/Majboor/enchase
8
- Project-URL: Repository, https://github.com/Majboor/enchase
9
- Project-URL: Changelog, https://github.com/Majboor/enchase/blob/main/CHANGELOG.md
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
- [![tests](https://github.com/Majboor/enchase/actions/workflows/test.yml/badge.svg)](https://github.com/Majboor/enchase/actions/workflows/test.yml)
48
+ [![tests](https://github.com/waleedsworld/enchase/actions/workflows/test.yml/badge.svg)](https://github.com/waleedsworld/enchase/actions/workflows/test.yml)
49
49
  [![PyPI](https://img.shields.io/pypi/v/enchase)](https://pypi.org/project/enchase/)
50
50
  ![python](https://img.shields.io/badge/python-3.9%2B-blue)
51
51
  ![license](https://img.shields.io/badge/license-MIT-green)
52
52
 
53
53
  <p align="center">
54
- <img src="https://raw.githubusercontent.com/Majboor/enchase/main/assets/flare.gif" width="760" alt="Stabilised 2-panel animation of the 2023-03-29 X1.2 flare"><br>
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
- The pipeline does not correct stray light. For CHASE it is about 6–10 % of the disc-centre intensity, which makes small dark features (umbrae, filaments) look brighter than they are. 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.
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/Majboor/enchase/main/assets/tui_e2e_walkthrough.gif" width="800" alt="End-to-end chase-tui walkthrough">
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/Majboor/enchase/main/assets/temperature.gif" width="720" alt="Double temperature map animation"><br>
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/Majboor/enchase.git
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
- [![tests](https://github.com/Majboor/enchase/actions/workflows/test.yml/badge.svg)](https://github.com/Majboor/enchase/actions/workflows/test.yml)
5
+ [![tests](https://github.com/waleedsworld/enchase/actions/workflows/test.yml/badge.svg)](https://github.com/waleedsworld/enchase/actions/workflows/test.yml)
6
6
  [![PyPI](https://img.shields.io/pypi/v/enchase)](https://pypi.org/project/enchase/)
7
7
  ![python](https://img.shields.io/badge/python-3.9%2B-blue)
8
8
  ![license](https://img.shields.io/badge/license-MIT-green)
9
9
 
10
10
  <p align="center">
11
- <img src="https://raw.githubusercontent.com/Majboor/enchase/main/assets/flare.gif" width="760" alt="Stabilised 2-panel animation of the 2023-03-29 X1.2 flare"><br>
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
- The pipeline does not correct stray light. For CHASE it is about 6–10 % of the disc-centre intensity, which makes small dark features (umbrae, filaments) look brighter than they are. 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.
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/Majboor/enchase/main/assets/tui_e2e_walkthrough.gif" width="800" alt="End-to-end chase-tui walkthrough">
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/Majboor/enchase/main/assets/temperature.gif" width="720" alt="Double temperature map animation"><br>
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/Majboor/enchase.git
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.2.0"
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.2.0
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/Majboor/enchase
8
- Project-URL: Repository, https://github.com/Majboor/enchase
9
- Project-URL: Changelog, https://github.com/Majboor/enchase/blob/main/CHANGELOG.md
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
- [![tests](https://github.com/Majboor/enchase/actions/workflows/test.yml/badge.svg)](https://github.com/Majboor/enchase/actions/workflows/test.yml)
48
+ [![tests](https://github.com/waleedsworld/enchase/actions/workflows/test.yml/badge.svg)](https://github.com/waleedsworld/enchase/actions/workflows/test.yml)
49
49
  [![PyPI](https://img.shields.io/pypi/v/enchase)](https://pypi.org/project/enchase/)
50
50
  ![python](https://img.shields.io/badge/python-3.9%2B-blue)
51
51
  ![license](https://img.shields.io/badge/license-MIT-green)
52
52
 
53
53
  <p align="center">
54
- <img src="https://raw.githubusercontent.com/Majboor/enchase/main/assets/flare.gif" width="760" alt="Stabilised 2-panel animation of the 2023-03-29 X1.2 flare"><br>
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
- The pipeline does not correct stray light. For CHASE it is about 6–10 % of the disc-centre intensity, which makes small dark features (umbrae, filaments) look brighter than they are. 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.
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/Majboor/enchase/main/assets/tui_e2e_walkthrough.gif" width="800" alt="End-to-end chase-tui walkthrough">
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/Majboor/enchase/main/assets/temperature.gif" width="720" alt="Double temperature map animation"><br>
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/Majboor/enchase.git
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.2.0"
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/Majboor/enchase"
47
- Repository = "https://github.com/Majboor/enchase"
48
- Changelog = "https://github.com/Majboor/enchase/blob/main/CHANGELOG.md"
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