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 (extract → wavelength_calibrate → extinction_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_normalisationsonce flux-calibrated to inspect continuum choices side-by-side. - Drop the resulting
Spectrum1Dinto 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.