Skip to content

Full reduction walkthrough - raw FITS → flux-calibrated 1D

This walkthrough chains the twelve EasySpec shelf wrappers to take a fresh night's worth of observations from raw CCD frames to a 1D, wavelength-calibrated, extinction-corrected, flux-calibrated spectrum - without leaving spectro-kernel.

It mirrors what a manual EasySpec session looks like in a notebook, but every step is an algorithm registered in the catalogue (visible via spectro list, callable from MCP, recorded in the audit trail).

Inputs you need on disk

  • Bias frames - a directory of bias FITS, one per exposure.
  • Dark frames - at the same temperature/integration as your science.
  • Flat frames - typically twilight or dome flats.
  • Science frame - your target.
  • Arc-lamp frame - for wavelength calibration.
  • Standard-star frame - extracted, ideally the same night, same airmass range.
  • Standard-star archive reference - one of the bundled EasySpec catalogues (calspec, oke1990, irscal, …).

1. Build the masters

from spectro_kernel import WorkContext, run_algorithm

ctx = WorkContext()
run_algorithm("bias_combine_easyspec", ctx, {"bias_dir": "/data/biases"})
run_algorithm("dark_combine_easyspec", ctx, {"dark_dir": "/data/darks"})
run_algorithm("flat_combine_easyspec", ctx, {"flat_dir": "/data/flats"})
# → ctx.extras now has master_bias, master_dark, master_flat (all ImageFrames).

2. Apply the corrections to the science frame

run_algorithm("subtract_bias_easyspec",     ctx, {"target_path": "/data/science.fits"})
run_algorithm("subtract_dark_easyspec",     ctx)               # uses ctx.image + ctx.extras['master_dark']
run_algorithm("flat_normalize_easyspec",    ctx, {"auto_normalise": True})
run_algorithm("cosmic_ray_remove_easyspec", ctx, {"sigclip": 5.0})
# → ctx.image is now the fully cleaned 2D detector frame.

3. Extract a 1D spectrum

run_algorithm(
    "extract_spectrum_easyspec",
    ctx,
    {
        "target_name": "alpha_lyr_001",
        "exposure_seconds": 60.0,
        "airmass": 1.4,
        "mc_steps": 25,
        "extraction_weights": "gaussian",
    },
)
# → ctx.spectrum is the 1D extraction on a *pixel-index* wavelength axis.

4. Calibrate the wavelength axis

You need a set of identified arc-lamp peaks (pixel position ↔ rest wavelength). You typically build this list once per setup using detect_lines on the lamp extraction and matching against a published Ne/Ar/Hg list.

run_algorithm(
    "wavelength_calibrate_easyspec",
    ctx,
    {
        "lamp_peak_positions":      [134.2, 421.7, 615.5, 902.1, 1289.4],
        "corresponding_wavelengths": [4358.3, 5460.7, 5790.7, 6402.2, 7245.1],
        "poly_order": 2,
    },
)
# → ctx.spectrum.wavelength is now in Ångström.

5. Atmospheric extinction correction

run_algorithm(
    "extinction_correct_easyspec",
    ctx,
    {"observatory": "lapalma", "airmass": 1.4},
)
# → ctx.spectrum is extinction-corrected; flux is still in ADU / electrons.

6. Flux calibration via a standard star

Run the same chain (extractwavelength_calibrateextinction_correct) on the standard-star observation first, keeping its 1D spectrum aside. Then:

run_algorithm(
    "flux_calibrate_easyspec",
    ctx,
    {
        "std_star_wavelength_angstrom":      std_wave_array,
        "std_star_flux_extinction_corrected": std_flux_array,
        "std_star_dataset":  "calspec",
        "std_star_archive_file": "alpha_lyr_stis_011.dat",
        "exposure_target_seconds":   60.0,
        "exposure_std_star_seconds": 30.0,
    },
)
# → ctx.spectrum.flux is now in erg/s/cm²/Å.

The same as a preset

The whole chain (steps 1-3 - the parameter-free, file-driven portion) is templated in the bundled preset full_reduction_easyspec. It is a template: the __REPLACE__ placeholders are the runtime paths you fill in before building the pipeline. A typical usage:

from spectro_kernel import PipelineBuilder, WorkContext
from spectro_kernel.presets import load_preset

config = load_preset("full_reduction_easyspec")
for step in config["steps"]:
    p = step["params"]
    for key, value in p.items():
        if isinstance(value, str) and value.startswith("__REPLACE_WITH_BIAS_DIR__"):
            p[key] = "/data/biases"
        elif isinstance(value, str) and value.startswith("__REPLACE_WITH_DARK_DIR__"):
            p[key] = "/data/darks"
        # … etc.

pipeline = PipelineBuilder().from_config(config).build()
result = pipeline.execute(WorkContext())

Steps 4-6 (wavelength + extinction + flux) stay separate calls because their parameters change per observation.

What you have at the end

spec = result.context.spectrum  # or simply ctx.spectrum from the manual chain
spec.wavelength            # Ångström
spec.flux                  # erg/s/cm²/Å
spec.meta                  # full provenance (every step, every backend, every reference)
result.context.history     # the audit trail - every algorithm + version + params + hashes

…all reproducible from the recorded steps. This is the reduction face of the shelf: ten lines of code, three dozen lines of YAML, and you have a publishable 1D spectrum.

Going further

  • Pair with compare_normalisations once flux-calibrated to inspect continuum choices side-by-side.
  • Drop the resulting Spectrum1D into the Be-star variability notebook for multi-epoch analysis.
  • Switch any EasySpec step for its native counterpart (e.g. bias_combine, extract_spectrum_sum) to compare implementations on the same input - that is exactly the shelf philosophy.