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.
Files changed (61) hide show
  1. specproc/__init__.py +9 -0
  2. specproc/calib_data/linelists/FeAr.dat +375 -0
  3. specproc/calib_data/linelists/README +79 -0
  4. specproc/calib_data/linelists/airvac.cl +61 -0
  5. specproc/calib_data/linelists/argon.dat +161 -0
  6. specproc/calib_data/linelists/ctiohear.dat +106 -0
  7. specproc/calib_data/linelists/ctiohenear.dat +146 -0
  8. specproc/calib_data/linelists/cuar.dat +459 -0
  9. specproc/calib_data/linelists/henear.dat +93 -0
  10. specproc/calib_data/linelists/henearhres.dat +300 -0
  11. specproc/calib_data/linelists/idhenear.dat +127 -0
  12. specproc/calib_data/linelists/krypton.dat +109 -0
  13. specproc/calib_data/linelists/lowskylines.dat +577 -0
  14. specproc/calib_data/linelists/ohlines.dat +268 -0
  15. specproc/calib_data/linelists/skylines.dat +1385 -0
  16. specproc/calib_data/linelists/thar-noao.dat +4169 -0
  17. specproc/calib_data/linelists/thar.dat +3057 -0
  18. specproc/calib_data/linelists/thorium.dat +3057 -0
  19. specproc/calib_data/linelists/vacidhenear.dat +127 -0
  20. specproc/calib_data/linelists/vacthar.dat +3057 -0
  21. specproc/calib_data/linelists/vacthorium.dat +3057 -0
  22. specproc/calib_data/linelists/xenon.dat +89 -0
  23. specproc/calib_data/telescopes/lijiang24hrs/lijiang24_hrs_thar_lines.csv +550 -0
  24. 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
  25. specproc/calib_data/telescopes/xinglong216bfosc//346/212/245/345/221/212/345/233/276.docx +0 -0
  26. 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
  27. specproc/calib_data/telescopes/xinglong216hrs/wlcalib_20211123011_A.png +0 -0
  28. specproc/calib_data/telescopes/xinglong216hrs/xinglong216_thar_lines.csv +517 -0
  29. specproc/config/__init__.py +1 -0
  30. specproc/config/config_manager.py +431 -0
  31. specproc/core/__init__.py +1 -0
  32. specproc/core/basic_reduction.py +615 -0
  33. specproc/core/data_structures.py +525 -0
  34. specproc/core/de_blazing.py +210 -0
  35. specproc/core/extraction.py +375 -0
  36. specproc/core/flat_correction.py +1255 -0
  37. specproc/core/line_identification.py +302 -0
  38. specproc/core/order_stitching.py +187 -0
  39. specproc/core/order_tracing.py +1587 -0
  40. specproc/core/processing_pipeline.py +1183 -0
  41. specproc/core/scattered_light.py +654 -0
  42. specproc/core/slit_pipeline.py +762 -0
  43. specproc/core/wave_calibration.py +961 -0
  44. specproc/gui/__init__.py +1 -0
  45. specproc/gui/line_identifier.py +731 -0
  46. specproc/gui/main_window.py +2315 -0
  47. specproc/gui/settings_dialog.py +946 -0
  48. specproc/main.py +388 -0
  49. specproc/plotting/__init__.py +1 -0
  50. specproc/plotting/spectra_plotter.py +483 -0
  51. specproc/utils/__init__.py +1 -0
  52. specproc/utils/airvac.py +123 -0
  53. specproc/utils/fits_io.py +269 -0
  54. specproc/utils/image_processing.py +426 -0
  55. specproc/utils/overscan.py +600 -0
  56. specproc-1.0.0.dist-info/METADATA +668 -0
  57. specproc-1.0.0.dist-info/RECORD +61 -0
  58. specproc-1.0.0.dist-info/WHEEL +5 -0
  59. specproc-1.0.0.dist-info/entry_points.txt +5 -0
  60. specproc-1.0.0.dist-info/licenses/LICENSE +674 -0
  61. 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