SpecProc 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- specproc/__init__.py +9 -0
- specproc/calib_data/linelists/FeAr.dat +375 -0
- specproc/calib_data/linelists/README +79 -0
- specproc/calib_data/linelists/airvac.cl +61 -0
- specproc/calib_data/linelists/argon.dat +161 -0
- specproc/calib_data/linelists/ctiohear.dat +106 -0
- specproc/calib_data/linelists/ctiohenear.dat +146 -0
- specproc/calib_data/linelists/cuar.dat +459 -0
- specproc/calib_data/linelists/henear.dat +93 -0
- specproc/calib_data/linelists/henearhres.dat +300 -0
- specproc/calib_data/linelists/idhenear.dat +127 -0
- specproc/calib_data/linelists/krypton.dat +109 -0
- specproc/calib_data/linelists/lowskylines.dat +577 -0
- specproc/calib_data/linelists/ohlines.dat +268 -0
- specproc/calib_data/linelists/skylines.dat +1385 -0
- specproc/calib_data/linelists/thar-noao.dat +4169 -0
- specproc/calib_data/linelists/thar.dat +3057 -0
- specproc/calib_data/linelists/thorium.dat +3057 -0
- specproc/calib_data/linelists/vacidhenear.dat +127 -0
- specproc/calib_data/linelists/vacthar.dat +3057 -0
- specproc/calib_data/linelists/vacthorium.dat +3057 -0
- specproc/calib_data/linelists/xenon.dat +89 -0
- specproc/calib_data/telescopes/lijiang24hrs/lijiang24_hrs_thar_lines.csv +550 -0
- specproc/calib_data/telescopes/xinglong216bfosc/BFOSC/345/256/232/346/240/207/347/201/257/350/260/261/350/256/244/350/257/201/346/211/213/345/206/214v171118.pdf +0 -0
- specproc/calib_data/telescopes/xinglong216bfosc//346/212/245/345/221/212/345/233/276.docx +0 -0
- specproc/calib_data/telescopes/xinglong216hrs/216HRS /347/201/257/350/260/261/350/257/201/350/256/244/346/226/207/344/273/266.pdf +0 -0
- specproc/calib_data/telescopes/xinglong216hrs/wlcalib_20211123011_A.png +0 -0
- specproc/calib_data/telescopes/xinglong216hrs/xinglong216_thar_lines.csv +517 -0
- specproc/config/__init__.py +1 -0
- specproc/config/config_manager.py +431 -0
- specproc/core/__init__.py +1 -0
- specproc/core/basic_reduction.py +615 -0
- specproc/core/data_structures.py +525 -0
- specproc/core/de_blazing.py +210 -0
- specproc/core/extraction.py +375 -0
- specproc/core/flat_correction.py +1255 -0
- specproc/core/line_identification.py +302 -0
- specproc/core/order_stitching.py +187 -0
- specproc/core/order_tracing.py +1587 -0
- specproc/core/processing_pipeline.py +1183 -0
- specproc/core/scattered_light.py +654 -0
- specproc/core/slit_pipeline.py +762 -0
- specproc/core/wave_calibration.py +961 -0
- specproc/gui/__init__.py +1 -0
- specproc/gui/line_identifier.py +731 -0
- specproc/gui/main_window.py +2315 -0
- specproc/gui/settings_dialog.py +946 -0
- specproc/main.py +388 -0
- specproc/plotting/__init__.py +1 -0
- specproc/plotting/spectra_plotter.py +483 -0
- specproc/utils/__init__.py +1 -0
- specproc/utils/airvac.py +123 -0
- specproc/utils/fits_io.py +269 -0
- specproc/utils/image_processing.py +426 -0
- specproc/utils/overscan.py +600 -0
- specproc-1.0.0.dist-info/METADATA +668 -0
- specproc-1.0.0.dist-info/RECORD +61 -0
- specproc-1.0.0.dist-info/WHEEL +5 -0
- specproc-1.0.0.dist-info/entry_points.txt +5 -0
- specproc-1.0.0.dist-info/licenses/LICENSE +674 -0
- specproc-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core data structures for spectral data representation.
|
|
3
|
+
|
|
4
|
+
Defines classes for storing and managing spectral data throughout the reduction pipeline.
|
|
5
|
+
"""
|
|
6
|
+
# 定义光谱、孔径、标定及流程状态数据结构。
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Dict, Optional, Tuple, Any
|
|
11
|
+
import json
|
|
12
|
+
from numpy.polynomial.chebyshev import chebval
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# 保存单个光谱孔径的位置与边界。
|
|
16
|
+
@dataclass
|
|
17
|
+
class ApertureLocation:
|
|
18
|
+
"""Single aperture (order) location and properties.
|
|
19
|
+
|
|
20
|
+
Supports two storage modes:
|
|
21
|
+
- **Polynomial**: ``center_coef`` / ``lower_coef`` / ``upper_coef`` are
|
|
22
|
+
polynomial coefficient arrays evaluated via ``np.polyval``.
|
|
23
|
+
- **Dense array**: ``center_arr`` / ``lower_arr`` / ``upper_arr`` are
|
|
24
|
+
per-column value arrays (one value per detector column) evaluated via
|
|
25
|
+
``np.interp``. This mode is used when boundaries come from a labeled
|
|
26
|
+
mask image (``Orders_mask.fits``) rather than from a parametric fit.
|
|
27
|
+
|
|
28
|
+
When Chebyshev width coefficients (``w_up_cheb_coef`` / ``w_low_cheb_coef``)
|
|
29
|
+
are present, ``get_lower()`` / ``get_upper()`` reconstruct boundaries as::
|
|
30
|
+
|
|
31
|
+
Y_upper(x) = center(x) + chebval(x, w_up_cheb_coef)
|
|
32
|
+
Y_lower(x) = center(x) - chebval(x, w_low_cheb_coef)
|
|
33
|
+
|
|
34
|
+
Otherwise dense arrays or polynomial coefficients are used as fallback.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
aperture: int # Aperture number
|
|
38
|
+
order: int # Physical diffraction order
|
|
39
|
+
|
|
40
|
+
# Polynomial coefficients (descending order: c0*x^n + … + cn)
|
|
41
|
+
center_coef: np.ndarray = None
|
|
42
|
+
lower_coef: np.ndarray = None
|
|
43
|
+
upper_coef: np.ndarray = None
|
|
44
|
+
|
|
45
|
+
# Dense per-column arrays (index = column number)
|
|
46
|
+
center_arr: np.ndarray = None
|
|
47
|
+
lower_arr: np.ndarray = None
|
|
48
|
+
upper_arr: np.ndarray = None
|
|
49
|
+
|
|
50
|
+
# Chebyshev width coefficients (from hybrid smoothing).
|
|
51
|
+
# W_up(x) = chebval(x, w_up_cheb_coef) → upper = center + W_up
|
|
52
|
+
# W_low(x) = chebval(x, w_low_cheb_coef) → lower = center − W_low
|
|
53
|
+
w_up_cheb_coef: np.ndarray = None
|
|
54
|
+
w_low_cheb_coef: np.ndarray = None
|
|
55
|
+
|
|
56
|
+
width: float = 30.0 # Aperture width (pixels)
|
|
57
|
+
is_chebyshev: bool = False # Whether coefs use Chebyshev basis
|
|
58
|
+
domain: Tuple[float, float] = (0, 4096) # Domain for Chebyshev evaluation
|
|
59
|
+
|
|
60
|
+
# Advanced Missing Order Recovery Flags
|
|
61
|
+
is_interpolated: bool = False # True if the entire order was mathematically interpolated
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# 按稠密数组或切比雪夫系数计算位置。
|
|
65
|
+
def _eval(self, coef, arr, x):
|
|
66
|
+
"""Return values at column positions *x*.
|
|
67
|
+
|
|
68
|
+
Uses dense array when available (``np.interp``), otherwise uses
|
|
69
|
+
Chebyshev polynomial evaluation (``chebval``). Standard power-series
|
|
70
|
+
polynomials (np.polyval) are no longer supported for trace storage.
|
|
71
|
+
"""
|
|
72
|
+
x = np.asarray(x, dtype=float)
|
|
73
|
+
if arr is not None:
|
|
74
|
+
# Dense array mode (evaluated via interpolation)
|
|
75
|
+
idx = np.arange(len(arr), dtype=float)
|
|
76
|
+
return np.interp(x, idx, arr)
|
|
77
|
+
if coef is not None:
|
|
78
|
+
# Chebyshev basis mode.
|
|
79
|
+
# Coefficients produced by Chebyshev.fit(..., domain=(d0, d1)) live in
|
|
80
|
+
# the canonical [-1, 1] window. chebval() always evaluates in the
|
|
81
|
+
# canonical window, so we must map raw column coordinates first.
|
|
82
|
+
if self.is_chebyshev and self.domain is not None:
|
|
83
|
+
d0, d1 = float(self.domain[0]), float(self.domain[1])
|
|
84
|
+
if d1 > d0:
|
|
85
|
+
x_eval = 2.0 * (x - d0) / (d1 - d0) - 1.0
|
|
86
|
+
else:
|
|
87
|
+
x_eval = x
|
|
88
|
+
return chebval(x_eval, coef)
|
|
89
|
+
return chebval(x, coef)
|
|
90
|
+
return np.full_like(x, np.nan)
|
|
91
|
+
|
|
92
|
+
# 计算指定列坐标处的孔径中心。
|
|
93
|
+
def get_position(self, y: np.ndarray) -> np.ndarray:
|
|
94
|
+
"""Get aperture center position at column coordinates *y*."""
|
|
95
|
+
return self._eval(self.center_coef, self.center_arr, y)
|
|
96
|
+
|
|
97
|
+
# 计算指定列坐标处的孔径下边界。
|
|
98
|
+
def get_lower(self, y: np.ndarray) -> np.ndarray:
|
|
99
|
+
"""Get lower boundary at column coordinates *y*."""
|
|
100
|
+
if self.w_low_cheb_coef is not None:
|
|
101
|
+
from numpy.polynomial.chebyshev import chebval
|
|
102
|
+
x = np.asarray(y, dtype=float)
|
|
103
|
+
center = self.get_position(x)
|
|
104
|
+
w_low = np.maximum(chebval(x, self.w_low_cheb_coef), 0.0)
|
|
105
|
+
return center - w_low
|
|
106
|
+
return self._eval(self.lower_coef, self.lower_arr, y)
|
|
107
|
+
|
|
108
|
+
# 计算指定列坐标处的孔径上边界。
|
|
109
|
+
def get_upper(self, y: np.ndarray) -> np.ndarray:
|
|
110
|
+
"""Get upper boundary at column coordinates *y*."""
|
|
111
|
+
if self.w_up_cheb_coef is not None:
|
|
112
|
+
from numpy.polynomial.chebyshev import chebval
|
|
113
|
+
x = np.asarray(y, dtype=float)
|
|
114
|
+
center = self.get_position(x)
|
|
115
|
+
w_up = np.maximum(chebval(x, self.w_up_cheb_coef), 0.0)
|
|
116
|
+
return center + w_up
|
|
117
|
+
return self._eval(self.upper_coef, self.upper_arr, y)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# 管理探测器上的全部光谱孔径。
|
|
121
|
+
@dataclass
|
|
122
|
+
class ApertureSet:
|
|
123
|
+
"""Collection of apertures (orders) for entire detector."""
|
|
124
|
+
|
|
125
|
+
apertures: Dict[int, ApertureLocation] = field(default_factory=dict)
|
|
126
|
+
norders: int = 0
|
|
127
|
+
direct_axis: int = 0 # 0 for Y-direction, 1 for X-direction
|
|
128
|
+
|
|
129
|
+
# 将孔径加入集合并更新数量。
|
|
130
|
+
def add_aperture(self, aperture: ApertureLocation):
|
|
131
|
+
"""Add aperture to set."""
|
|
132
|
+
self.apertures[aperture.aperture] = aperture
|
|
133
|
+
self.norders = len(self.apertures)
|
|
134
|
+
|
|
135
|
+
# 按编号查找孔径。
|
|
136
|
+
def get_aperture(self, aperture_id: int) -> Optional[ApertureLocation]:
|
|
137
|
+
"""Retrieve aperture by ID."""
|
|
138
|
+
return self.apertures.get(aperture_id)
|
|
139
|
+
|
|
140
|
+
# 返回排序后的孔径编号。
|
|
141
|
+
def get_orders(self) -> list:
|
|
142
|
+
"""Return list of order numbers."""
|
|
143
|
+
return sorted(self.apertures.keys())
|
|
144
|
+
|
|
145
|
+
def __len__(self) -> int:
|
|
146
|
+
"""Return number of apertures (orders) in the set."""
|
|
147
|
+
return len(self.apertures)
|
|
148
|
+
|
|
149
|
+
# 按偏移量更新孔径与阶次编号。
|
|
150
|
+
def shift_orders(self, delta_m: int):
|
|
151
|
+
"""Shift all aperture IDs and order numbers by a constant offset."""
|
|
152
|
+
if delta_m == 0:
|
|
153
|
+
return
|
|
154
|
+
new_apertures = {}
|
|
155
|
+
for old_id, ap in self.apertures.items():
|
|
156
|
+
new_id = old_id + delta_m
|
|
157
|
+
ap.aperture = new_id
|
|
158
|
+
ap.order = new_id
|
|
159
|
+
new_apertures[new_id] = ap
|
|
160
|
+
self.apertures = new_apertures
|
|
161
|
+
|
|
162
|
+
# 从标记图像重建孔径边界集合。
|
|
163
|
+
@staticmethod
|
|
164
|
+
def from_labeled_mask(order_labels: np.ndarray) -> 'ApertureSet':
|
|
165
|
+
"""Build an ApertureSet from a labeled order mask image.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
order_labels : np.ndarray[int16], shape (rows, cols)
|
|
170
|
+
Pixel value = order ID, 0 = background.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
ApertureSet with dense-array ApertureLocation objects.
|
|
175
|
+
"""
|
|
176
|
+
rows, cols = order_labels.shape
|
|
177
|
+
unique_ids = sorted(int(v) for v in np.unique(order_labels) if v != 0)
|
|
178
|
+
aps = ApertureSet()
|
|
179
|
+
|
|
180
|
+
for oid in unique_ids:
|
|
181
|
+
order_mask = (order_labels == oid)
|
|
182
|
+
lo_arr = np.full(cols, np.nan, dtype=np.float64)
|
|
183
|
+
hi_arr = np.full(cols, np.nan, dtype=np.float64)
|
|
184
|
+
|
|
185
|
+
for xi in range(cols):
|
|
186
|
+
col_rows = np.where(order_mask[:, xi])[0]
|
|
187
|
+
if col_rows.size > 0:
|
|
188
|
+
lo_arr[xi] = float(col_rows[0])
|
|
189
|
+
hi_arr[xi] = float(col_rows[-1])
|
|
190
|
+
|
|
191
|
+
center_arr = 0.5 * (lo_arr + hi_arr)
|
|
192
|
+
valid = np.isfinite(lo_arr)
|
|
193
|
+
w = float(np.nanmedian(hi_arr[valid] - lo_arr[valid])) if np.any(valid) else 10.0
|
|
194
|
+
|
|
195
|
+
ap = ApertureLocation(
|
|
196
|
+
aperture=oid, order=oid,
|
|
197
|
+
center_arr=center_arr,
|
|
198
|
+
lower_arr=lo_arr,
|
|
199
|
+
upper_arr=hi_arr,
|
|
200
|
+
width=w,
|
|
201
|
+
)
|
|
202
|
+
aps.add_aperture(ap)
|
|
203
|
+
|
|
204
|
+
return aps
|
|
205
|
+
|
|
206
|
+
# 载入轨迹中心及切比雪夫宽度系数。
|
|
207
|
+
def load_trace_coefs(self, coefs_path: str):
|
|
208
|
+
"""Load trace centers and Chebyshev width coefficients from a companion JSON file.
|
|
209
|
+
|
|
210
|
+
For each order present in both the file and this ApertureSet, sets
|
|
211
|
+
``w_up_cheb_coef`` and ``w_low_cheb_coef`` on the ApertureLocation,
|
|
212
|
+
and replaces ``center_arr`` with the B-spline-smoothed centre array
|
|
213
|
+
stored in the file (sub-pixel precision).
|
|
214
|
+
"""
|
|
215
|
+
import json as _json
|
|
216
|
+
from pathlib import Path as _Path
|
|
217
|
+
|
|
218
|
+
p = _Path(coefs_path)
|
|
219
|
+
if not p.exists():
|
|
220
|
+
return
|
|
221
|
+
with open(p, 'r') as f:
|
|
222
|
+
data = _json.load(f)
|
|
223
|
+
for oid_str, entry in data.get('orders', {}).items():
|
|
224
|
+
oid = int(oid_str)
|
|
225
|
+
ap = self.apertures.get(oid)
|
|
226
|
+
if ap is None:
|
|
227
|
+
continue
|
|
228
|
+
if 'center_arr' in entry:
|
|
229
|
+
ap.center_arr = np.array(entry['center_arr'], dtype=np.float64)
|
|
230
|
+
if 'w_up_cheb' in entry:
|
|
231
|
+
ap.w_up_cheb_coef = np.array(entry['w_up_cheb'], dtype=np.float64)
|
|
232
|
+
if 'w_low_cheb' in entry:
|
|
233
|
+
ap.w_low_cheb_coef = np.array(entry['w_low_cheb'], dtype=np.float64)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# 保存单个光谱阶次的一维数据。
|
|
237
|
+
@dataclass
|
|
238
|
+
class Spectrum:
|
|
239
|
+
"""1D spectrum data for a single order."""
|
|
240
|
+
|
|
241
|
+
aperture: int
|
|
242
|
+
order: int
|
|
243
|
+
|
|
244
|
+
# Core spectrum data
|
|
245
|
+
wavelength: np.ndarray # Wavelength array [Angstrom]
|
|
246
|
+
flux: np.ndarray # Flux array [counts or normalized]
|
|
247
|
+
error: Optional[np.ndarray] = None # Error/uncertainty
|
|
248
|
+
background: Optional[np.ndarray] = None # Background/stray light
|
|
249
|
+
mask: Optional[np.ndarray] = None # Pixel mask (0=good, 1=bad)
|
|
250
|
+
|
|
251
|
+
# Metadata
|
|
252
|
+
exptime: float = 1.0
|
|
253
|
+
bjd: float = 0.0 # Barycentric Julian Date
|
|
254
|
+
ra: float = 0.0 # Right Ascension
|
|
255
|
+
dec: float = 0.0 # Declination
|
|
256
|
+
|
|
257
|
+
# 返回光谱包含的像素数量。
|
|
258
|
+
def npixel(self) -> int:
|
|
259
|
+
"""Number of pixels in spectrum."""
|
|
260
|
+
return len(self.flux)
|
|
261
|
+
|
|
262
|
+
# 返回标记为有效的像素掩码。
|
|
263
|
+
def get_good_pixels(self) -> np.ndarray:
|
|
264
|
+
"""Get mask of good pixels."""
|
|
265
|
+
if self.mask is None:
|
|
266
|
+
return np.ones(len(self.flux), dtype=bool)
|
|
267
|
+
return self.mask == 0
|
|
268
|
+
|
|
269
|
+
# 将光谱数据转换为可存储字典。
|
|
270
|
+
def to_dict(self) -> dict:
|
|
271
|
+
"""Convert to dictionary for storage."""
|
|
272
|
+
return {
|
|
273
|
+
'aperture': self.aperture,
|
|
274
|
+
'order': self.order,
|
|
275
|
+
'wavelength': self.wavelength.tolist(),
|
|
276
|
+
'flux': self.flux.tolist(),
|
|
277
|
+
'error': self.error.tolist() if self.error is not None else None,
|
|
278
|
+
'background': self.background.tolist() if self.background is not None else None,
|
|
279
|
+
'mask': self.mask.tolist() if self.mask is not None else None,
|
|
280
|
+
'exptime': self.exptime,
|
|
281
|
+
'bjd': self.bjd,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# 深拷贝光谱及其数组数据。
|
|
285
|
+
def copy(self):
|
|
286
|
+
"""Return a deep copy of this Spectrum instance.
|
|
287
|
+
|
|
288
|
+
Uses numpy's copy for array fields to avoid shared references.
|
|
289
|
+
"""
|
|
290
|
+
return type(self)(
|
|
291
|
+
aperture=self.aperture,
|
|
292
|
+
order=self.order,
|
|
293
|
+
wavelength=np.copy(self.wavelength) if self.wavelength is not None else None,
|
|
294
|
+
flux=np.copy(self.flux) if self.flux is not None else None,
|
|
295
|
+
error=np.copy(self.error) if self.error is not None else None,
|
|
296
|
+
background=np.copy(self.background) if self.background is not None else None,
|
|
297
|
+
mask=np.copy(self.mask) if self.mask is not None else None,
|
|
298
|
+
exptime=self.exptime,
|
|
299
|
+
bjd=self.bjd,
|
|
300
|
+
ra=self.ra,
|
|
301
|
+
dec=self.dec,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# 管理全部阶次的一维光谱集合。
|
|
306
|
+
@dataclass
|
|
307
|
+
class SpectraSet:
|
|
308
|
+
"""Collection of 1D spectra for all orders."""
|
|
309
|
+
|
|
310
|
+
spectra: Dict[int, Spectrum] = field(default_factory=dict)
|
|
311
|
+
norders: int = 0
|
|
312
|
+
|
|
313
|
+
# 将光谱加入集合并更新数量。
|
|
314
|
+
def add_spectrum(self, spec: Spectrum):
|
|
315
|
+
"""Add spectrum to set."""
|
|
316
|
+
self.spectra[spec.aperture] = spec
|
|
317
|
+
self.norders = len(self.spectra)
|
|
318
|
+
|
|
319
|
+
# 按孔径编号查找光谱。
|
|
320
|
+
def get_spectrum(self, aperture_id: int) -> Optional[Spectrum]:
|
|
321
|
+
"""Retrieve spectrum by aperture ID."""
|
|
322
|
+
return self.spectra.get(aperture_id)
|
|
323
|
+
|
|
324
|
+
# 返回排序后的孔径编号。
|
|
325
|
+
def get_orders(self) -> list:
|
|
326
|
+
"""Return list of aperture numbers."""
|
|
327
|
+
return sorted(self.spectra.keys())
|
|
328
|
+
|
|
329
|
+
# 按偏移量更新孔径与阶次编号。
|
|
330
|
+
def shift_orders(self, delta_m: int):
|
|
331
|
+
"""Shift all aperture IDs and order numbers by a constant offset."""
|
|
332
|
+
if delta_m == 0:
|
|
333
|
+
return
|
|
334
|
+
new_spectra = {}
|
|
335
|
+
for old_id, spec in self.spectra.items():
|
|
336
|
+
new_id = old_id + delta_m
|
|
337
|
+
spec.aperture = new_id
|
|
338
|
+
spec.order = new_id
|
|
339
|
+
new_spectra[new_id] = spec
|
|
340
|
+
self.spectra = new_spectra
|
|
341
|
+
|
|
342
|
+
# 保存平场图像、校正图及相关数据。
|
|
343
|
+
@dataclass
|
|
344
|
+
class FlatField:
|
|
345
|
+
"""Flat field calibration data."""
|
|
346
|
+
|
|
347
|
+
# Raw combined flat field image
|
|
348
|
+
flat_data: np.ndarray
|
|
349
|
+
|
|
350
|
+
# Sensitivity map (relative response per pixel)
|
|
351
|
+
flat_sens: np.ndarray
|
|
352
|
+
|
|
353
|
+
# Normalized flat (for order tracing)
|
|
354
|
+
flat_norm: np.ndarray
|
|
355
|
+
|
|
356
|
+
# Bad/saturated pixel mask
|
|
357
|
+
flat_mask: np.ndarray
|
|
358
|
+
|
|
359
|
+
# Scattered light model subtracted from master flat
|
|
360
|
+
scattered_light: Optional[np.ndarray] = None
|
|
361
|
+
|
|
362
|
+
# Smoothed 2D model preserving blaze/order envelope
|
|
363
|
+
smoothed_model: Optional[np.ndarray] = None
|
|
364
|
+
|
|
365
|
+
# Pixel-to-pixel flat (high-frequency component near 1.0)
|
|
366
|
+
pixel_flat: Optional[np.ndarray] = None
|
|
367
|
+
|
|
368
|
+
# Illumination correction map (large-scale spatial profile)
|
|
369
|
+
illumination_flat: Optional[np.ndarray] = None
|
|
370
|
+
|
|
371
|
+
# Final 2D flat correction map used on science images
|
|
372
|
+
flat_corr_2d: Optional[np.ndarray] = None
|
|
373
|
+
|
|
374
|
+
# Per-order 1D flat spectra (optional)
|
|
375
|
+
flat_spec: Optional[SpectraSet] = None
|
|
376
|
+
|
|
377
|
+
# Aperture traces from flat field
|
|
378
|
+
aperture_set: Optional[ApertureSet] = None
|
|
379
|
+
|
|
380
|
+
# Cross-order spatial profiles per order (optional)
|
|
381
|
+
cross_profiles: Dict[int, np.ndarray] = field(default_factory=dict)
|
|
382
|
+
|
|
383
|
+
# Blaze profiles per order (for de-blazing)
|
|
384
|
+
blaze_profiles: Dict[int, np.ndarray] = field(default_factory=dict)
|
|
385
|
+
|
|
386
|
+
# Metadata
|
|
387
|
+
exptime: float = 1.0
|
|
388
|
+
|
|
389
|
+
# 按偏移量更新孔径与阶次编号。
|
|
390
|
+
def shift_orders(self, delta_m: int):
|
|
391
|
+
"""Shift all order-keyed dictionaries by a constant offset."""
|
|
392
|
+
if delta_m == 0:
|
|
393
|
+
return
|
|
394
|
+
if self.cross_profiles:
|
|
395
|
+
self.cross_profiles = {k + delta_m: v for k, v in self.cross_profiles.items()}
|
|
396
|
+
if self.blaze_profiles:
|
|
397
|
+
self.blaze_profiles = {k + delta_m: v for k, v in self.blaze_profiles.items()}
|
|
398
|
+
if self.flat_spec:
|
|
399
|
+
self.flat_spec.shift_orders(delta_m)
|
|
400
|
+
# aperture_set 统一在外部进行 shift,这里不重复处理
|
|
401
|
+
|
|
402
|
+
# 保存二维波长定标模型及质量信息。
|
|
403
|
+
@dataclass
|
|
404
|
+
class WaveCalib:
|
|
405
|
+
"""Wavelength calibration solution."""
|
|
406
|
+
|
|
407
|
+
# 2D polynomial coefficients for wavelength
|
|
408
|
+
# wavelength(x, y) = sum(p_ij * x^i * y^j)
|
|
409
|
+
poly_coef: np.ndarray # Polynomial coefficients, shape (xorder+1, yorder+1)
|
|
410
|
+
|
|
411
|
+
xorder: int = 3 # Wavelength poly order in X
|
|
412
|
+
yorder: int = 3 # Wavelength poly order in Y
|
|
413
|
+
poly_type: str = 'chebyshev' # Polynomial type: 'chebyshev', 'legendre', or 'polynomial'
|
|
414
|
+
domain_x: Tuple[float, float] = (0.0, 4096.0) # Normalization domain for X (pixels)
|
|
415
|
+
domain_y: Tuple[float, float] = (10.0, 100.0) # Normalization domain for Y (orders)
|
|
416
|
+
|
|
417
|
+
# Identified calibration lines
|
|
418
|
+
line_catalog: Optional[np.ndarray] = None # Reference wavelengths
|
|
419
|
+
line_pixels: Optional[np.ndarray] = None # Detected pixel positions
|
|
420
|
+
line_orders: Optional[np.ndarray] = None # Order numbers of lines
|
|
421
|
+
|
|
422
|
+
# Quality metrics
|
|
423
|
+
rms: float = 0.0 # RMS residual [Angstrom]
|
|
424
|
+
nlines: int = 0 # Number of identified lines
|
|
425
|
+
|
|
426
|
+
# Metadata
|
|
427
|
+
calib_type: str = "ThAr" # Calibration lamp type
|
|
428
|
+
delta_m: int = 0 # Order offset applied to match references
|
|
429
|
+
|
|
430
|
+
# 将二维像素与阶次坐标映射为波长。
|
|
431
|
+
def apply_to_pixel(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
432
|
+
"""Apply wavelength calibration to pixel coordinates.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
x: X pixel coordinates
|
|
436
|
+
y: Y pixel coordinates (order number)
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Wavelength array
|
|
440
|
+
"""
|
|
441
|
+
xmin, xmax = self.domain_x
|
|
442
|
+
ymin, ymax = self.domain_y
|
|
443
|
+
|
|
444
|
+
# 将坐标归一化到 [-1, 1]
|
|
445
|
+
x_norm = 2.0 * (x - xmin) / max(xmax - xmin, 1e-6) - 1.0
|
|
446
|
+
y_norm = 2.0 * (y - ymin) / max(ymax - ymin, 1e-6) - 1.0
|
|
447
|
+
|
|
448
|
+
# 使用对应的 2D 多项式求 m * lambda
|
|
449
|
+
if self.poly_type == 'chebyshev':
|
|
450
|
+
from numpy.polynomial.chebyshev import chebval2d
|
|
451
|
+
m_lambda = chebval2d(x_norm, y_norm, self.poly_coef)
|
|
452
|
+
elif self.poly_type == 'legendre':
|
|
453
|
+
from numpy.polynomial.legendre import legval2d
|
|
454
|
+
m_lambda = legval2d(x_norm, y_norm, self.poly_coef)
|
|
455
|
+
else:
|
|
456
|
+
from numpy.polynomial.polynomial import polyval2d
|
|
457
|
+
m_lambda = polyval2d(x_norm, y_norm, self.poly_coef)
|
|
458
|
+
|
|
459
|
+
# 最终波长 lambda = m_lambda / m
|
|
460
|
+
return m_lambda / y
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# 保存单个处理阶段的配置参数。
|
|
464
|
+
@dataclass
|
|
465
|
+
class ProcessingConfig:
|
|
466
|
+
"""Configuration for a single processing stage."""
|
|
467
|
+
|
|
468
|
+
name: str
|
|
469
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
470
|
+
enabled: bool = True
|
|
471
|
+
|
|
472
|
+
# 读取阶段参数并提供默认值。
|
|
473
|
+
def get_param(self, key: str, default: Any = None) -> Any:
|
|
474
|
+
"""Get parameter value."""
|
|
475
|
+
return self.params.get(key, default)
|
|
476
|
+
|
|
477
|
+
# 设置阶段配置参数。
|
|
478
|
+
def set_param(self, key: str, value: Any):
|
|
479
|
+
"""Set parameter value."""
|
|
480
|
+
self.params[key] = value
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# 记录光谱归约各阶段的执行状态。
|
|
484
|
+
@dataclass
|
|
485
|
+
class ProcessingState:
|
|
486
|
+
"""State tracking for reduction pipeline."""
|
|
487
|
+
|
|
488
|
+
# Input data
|
|
489
|
+
raw_images: list = field(default_factory=list) # List of raw FITS filenames
|
|
490
|
+
|
|
491
|
+
# Calibration data
|
|
492
|
+
bias_frame: Optional[np.ndarray] = None
|
|
493
|
+
flat_field: Optional[FlatField] = None
|
|
494
|
+
apertures: Optional[ApertureSet] = None
|
|
495
|
+
wavelength_calib: Optional[WaveCalib] = None
|
|
496
|
+
|
|
497
|
+
# Pipeline stage flags
|
|
498
|
+
bias_done: bool = False
|
|
499
|
+
flat_done: bool = False
|
|
500
|
+
trace_done: bool = False
|
|
501
|
+
wavelength_done: bool = False
|
|
502
|
+
background_done: bool = False
|
|
503
|
+
extraction_done: bool = False
|
|
504
|
+
de_blazing_done: bool = False
|
|
505
|
+
|
|
506
|
+
# Output data
|
|
507
|
+
extracted_spectra: Optional[SpectraSet] = None
|
|
508
|
+
de_blazed_spectra: Optional[SpectraSet] = None
|
|
509
|
+
|
|
510
|
+
# Progress tracking
|
|
511
|
+
current_stage: str = ""
|
|
512
|
+
progress: float = 0.0 # 0.0 to 1.0
|
|
513
|
+
|
|
514
|
+
# 清除处理结果并重置阶段标记。
|
|
515
|
+
def reset(self):
|
|
516
|
+
"""Reset pipeline state."""
|
|
517
|
+
self.bias_done = False
|
|
518
|
+
self.flat_done = False
|
|
519
|
+
self.trace_done = False
|
|
520
|
+
self.wavelength_done = False
|
|
521
|
+
self.background_done = False
|
|
522
|
+
self.extraction_done = False
|
|
523
|
+
self.de_blazing_done = False
|
|
524
|
+
self.current_stage = ""
|
|
525
|
+
self.progress = 0.0
|