Skip to main content

Guide: Spectrum Preprocessing

A raw spectrum from the LAMOST telescope is not directly usable for machine learning. Each spectrum encodes both the real physics of the star and instrumental artifacts: absolute-brightness variations between stars, photon noise, cosmic pixels, and differences in observing conditions. Preprocessing is the step that removes these biases while preserving the relevant physical information.

Main module

All preprocessing logic is encapsulated in SpectraPreprocessor. Orchestration is handled by spectra_manager.py, which applies the pipeline in parallel across the ~250,000 downloaded spectra.


Pipeline overview​

The preprocessing pipeline proceeds through five sequential steps, from reading the raw FITS file to producing the 183-descriptor vector ready for training. Click each step to view the details.

Preprocessing pipeline — click a step
📄
FITS reading
fits.gz → memory
λ
λ reconstruction
wavelength grid
σ
Normalisation
flux / median → continuum ≈ 1
✓
Quality control
SNR · RUWE · masks
p
Feature extraction
183 physical features
Step 1 — FITS reading
›Opening with astropy.io.fits
›Flux + invvar extraction from extension 0
›Header reading for metadata
›Robust handling of missing values
hdul = fits.open(fits_path) flux = hdul[0].data[0] invvar = hdul[0].data[1]

Step 1 — Wavelength-grid reconstruction​

The λ grid is not stored explicitly in LAMOST files: it must be reconstructed from the FITS header metadata. SpectraPreprocessor implements a robust fallback chain to handle the different conventions used across observing campaigns.

Fallback chain — robust λ-grid reconstruction
1
✓
COEFF0 + COEFF1λ = 10^(COEFF0 + i × COEFF1)
Primary — ~99% of LAMOST DR5 spectra
2
✓
loglam columnλ = 10^loglam[i]
Fallback — some DR3/DR4 campaigns
3
✓
WCS standardλ = CRVAL1 + i × CDELT1
Fallback 2 — special formats
4
✗
Loading failureSpectrum ignored, logged in errors.csv
Error handling — very rare
Reconstructed grid — 3,921 channels · 3,690–9,100 Å
Blue arm
Red arm
Ca K
Ca H
Hβ
Mg b
Na D
Hα
CaIR
3700
4000
4500
5000
5500
6000
6500
7000
7500
8000
8500
9000
N = 3,921 channels · Δλ/pixel ≈ 1.36 Å · Hover over lines
src/pipeline/preprocessor.py — Method 1: COEFF0 + COEFF1
# Primary method — COEFF0 + COEFF1 (log-linear)
coeff0 = header['COEFF0']      # λ₀ of the first pixel (log10)
coeff1 = header['COEFF1']      # logarithmic step / pixel
n_pix  = flux.shape[0]         # 3921 channels
loglam  = coeff0 + np.arange(n_pix) * coeff1
wavelength = 10 ** loglam      # → Angstroms [3690 ... 9100]

Result: a vector of 3,921 wavelengths covering 3,690–9,100 Å, with a logarithmic step of Δlog(λ) ≈ 0.0001 (about ~1.36 Å/pixel at 5,500 Å).


Step 2 — Flux normalization​

Stars have very different absolute brightnesses and distances — an A-type star at 500 pc may have an apparent flux 1,000 times higher than a nearby M dwarf. Without normalization, these magnitude differences dominate the features and hide spectral-type differences.

Before normalisation
Raw flux — incomparable scales
flux4000550070008500
Type A — hot (> 7,500 K)
Type K — cool (< 5,200 K)
→
After normalisation
Flux / median — continuum ≈ 1 · comparable lines
Hαf/f̃4000550070008500
Type A — hot (> 7,500 K)
Type K — cool (< 5,200 K)
✓ Why the median?
The median is robust to emission lines (positive outliers) and cosmic rays that inflate the mean. 50% of continuum pixels are above, 50% below — a stable reference point.
✗ Why not the mean?
LAMOST spectra contain nebular emission lines, cosmic rays and instrumental artefacts that pull the mean upward, producing a biased normalisation.
Guard clause: if the median is ≤ 0 (corrupted or fully masked spectrum), the flux is returned unchanged and the spectrum is flagged in the error log for manual inspection.if median > 0: flux /= median

Why median normalization is sufficient​

Median normalization is deliberately simple: it answers the question “is this continuum above or below its average level?” without trying to model the continuum shape. This simplicity is a strength:

  1. Robustness — insensitive to emission lines and artifacts
  2. Reproducibility — one parameter, no arbitrary choices
  3. Fairness between descriptors — the StandardScaler applied afterward to the feature matrix completes the statistical normalization (mean 0, variance 1)
# Excerpt from SpectraPreprocessor.normalize_spectrum()
def normalize_spectrum(self, flux: np.ndarray) -> np.ndarray:
"""Normalize the flux by the median — robust to outliers."""
positive_flux = flux[flux > 0]
if len(positive_flux) == 0:
return flux # fully masked spectrum — return as is
median_flux = np.median(positive_flux)
if median_flux > 0:
return flux / median_flux
return flux

Step 3 — Continuum fitting for slopes​

To extract local continuum slopes (Family 6 — ~65 descriptors), an additional sigma-clipping step is applied locally within each spectral window:

from astropy.stats import sigma_clip

def fit_local_slope(wavelength, flux, wl_min, wl_max, sigma=2.5):
"""
Sigma-clipped linear fit of the local continuum.
σ = 2.5 removes absorption lines without biasing the slope.
"""
mask = (wavelength >= wl_min) & (wavelength <= wl_max)
wl_local = wavelength[mask]
fl_local = flux[mask]

# Sigma-clipping — removes lines and artifacts
clipped = sigma_clip(fl_local, sigma=sigma, maxiters=3)

# Linear regression on the clean continuum
coeffs = np.polyfit(wl_local[~clipped.mask], fl_local[~clipped.mask], deg=1)
return coeffs[0] # slope in flux/Å units

Five slopes are extracted: UV (3800–4200 Å), green visible (4500–5500 Å), red visible (5500–6500 Å), near IR (6500–7500 Å), and global slope (3800–7500 Å).

Local curvatures (Savitzky-Golay second derivative) follow the same principle:

from scipy.signal import savgol_filter

def compute_curvature(flux, wavelength, center_wl, window=50, polyorder=3):
"""Continuum curvature (second derivative) in Ångströms⁻²."""
idx = np.argmin(np.abs(wavelength - center_wl))
window_pts = min(window, idx, len(flux) - idx - 1)
smooth = savgol_filter(flux, window_length=2*window_pts+1, polyorder=polyorder, deriv=2)
return smooth[idx]

Step 4 — Quality filters​

Two sequential filters ensure that only scientifically usable spectra enter the analysis:

Filter 1 — Signal-to-noise ratio​

# SNR in the LAMOST red band (~6000–7000 Å)
mask_snr = catalog['snr_r'] > 10

Justification: Below SNR = 10, the prominence of weak lines (Fe, Mg b) becomes comparable to the noise — the descriptors would be dominated by statistical artifacts. A threshold of 10 is the minimum for equivalent-width measurements to be reliable.

Filter 2 — RUWE astrometric quality (Gaia DR3)​

# RUWE (Re-normalised Unit Weight Error) — Gaia astrometric-solution quality
mask_ruwe = catalog['ruwe'] < 1.4

Justification: RUWE > 1.4 indicates a faulty proper-motion solution — typically an unresolved binary star or a measurement artifact. Binary stars have composite spectra that blur the classification lines.

Filter impact​

StepSpectraNotes
Downloaded~250,000Raw FITS files
After SNR_r > 10~180,000Usable spectra
After RUWE < 1.4~150,000Reliable single sources
After Gaia 1″ cross-match43,019Final set with reference parameters

Step 5 — Extraction of the 183 descriptors​

Once the spectrum has been normalized and validated, FeatureEngineer.extract_features() computes the 183-physical-descriptor vector:

from src.pipeline.feature_engineering import FeatureEngineer

fe = FeatureEngineer()

# For each clean spectrum
vec = fe.extract_features(
matched_lines, # lines detected by peak_detector.py
wavelength, # reconstructed λ grid (3921 points)
flux_norm, # median-normalized flux
invvar, # inverse variance (pixel masking)
)
# vec: np.ndarray with shape (183,)

Measured processing throughput: ~340,000 spectra/hour (Ryzen 9 5950X, 32 threads, joblib parallelization).


Reference figures​

◎
SNR distribution by band
Quality control
SNR distribution by band
SNR_r > 10 — quality-filtering threshold retained for the 43,019 spectra
◎
Color indices — f_blue / f_red
Continuum
Color indices — f_blue / f_red
Distribution of continuum indices — T_eff proxy through slope
◎
FWHM(Hα) vs Gaia DR3 T_eff
Validation
FWHM(Hα) vs Gaia DR3 T_eff
Validation of the width measurement — expected correlation with temperature

Future improvement tracks​

The current preprocessing is designed to be fast and robust on the ~250,000 available spectra. The project Roadmap includes:

  • Polynomial continuum fitting — iterative method for stars with strong UV continuum
  • Systematic denoising — Savitzky-Golay applied to the full spectrum before extracting weak lines
  • Interstellar-extinction correction — A_G × R_V for strongly reddened stars
  • Binary-spectrum processing — deconvolution of composite line profiles for the C19/C1/C12 clusters identified by HDBSCAN