Skip to content

Tutorial: analyse a spectrum

A complete, guided analysis - from a FITS file to fitted lines and an exported result. It assumes you have installed spectro-kernel.

We will do the same analysis three times - as a library, as a pipeline, and from the CLI - so you can see that the three doors really do lead to the same room.

The goal

Take a stellar spectrum, and:

  1. normalise its continuum,
  2. measure its signal-to-noise ratio,
  3. fit the H-alpha line,
  4. export the normalised spectrum.

Step 0 - a spectrum to work with

If you do not have a FITS file handy, make a synthetic one:

import numpy as np
from spectro_kernel.io import write_fits
from spectro_kernel.types import Spectrum1D

wave = np.linspace(6300, 6800, 2000)
flux = 1000 + 80 * np.exp(-0.5 * ((wave - 6562.8) / 3.0) ** 2)   # H-alpha in emission
flux += np.random.default_rng(42).normal(0, 8, wave.size)         # noise
write_fits(Spectrum1D(wave, flux, meta={"object": "demo star"}), "demo.fits")

Approach 1 - step by step, as a library

from spectro_kernel import WorkContext, run_algorithm
from spectro_kernel.io import read_fits

ctx = WorkContext(spectrum=read_fits("demo.fits"))

run_algorithm("normalize_polynomial", ctx, {"order": 3})
run_algorithm("snr_der", ctx)
run_algorithm("fit_gaussian_line", ctx,
              {"line_center_angstrom": 6562.8, "window_angstrom": 30, "label": "H-alpha"})
run_algorithm("export_fits", ctx, {"path": "demo_normalised.fits"})

fit = ctx.line_fits["H-alpha"]
print(f"SNR            : {ctx.metrics['snr_der']:.0f}")
print(f"H-alpha centre : {fit.line_center_angstrom:.2f} Å")
print(f"H-alpha FWHM   : {fit.fwhm_angstrom:.2f} Å")
print(f"emission?      : {fit.is_emission}")

Notice that nothing is passed between the calls explicitly - each algorithm reads from and writes to the shared WorkContext.

Inspect what happened

for step in ctx.history:
    print(step)         # [ok] normalize_polynomial v1.0.0 (2.1ms) …

print(ctx.summary())    # everything the context now holds

That history is the audit trail - see Pipelines → Reproducibility.

Approach 2 - as a pipeline

The same four steps, declared once and run as a unit:

from spectro_kernel import PipelineBuilder, WorkContext
from spectro_kernel.io import read_fits

pipeline = (
    PipelineBuilder()
    .named("h-alpha analysis")
    .add("normalize_polynomial", order=3)
    .add("snr_der")
    .add("fit_gaussian_line", line_center_angstrom=6562.8, window_angstrom=30, label="H-alpha")
    .add("export_fits", path="demo_normalised.fits")
    .build()
)

result = pipeline.execute(WorkContext(spectrum=read_fits("demo.fits")))
print("success:", result.success)
print(result.context.metrics)

Approach 3 - from the command line

No Python at all:

spectro run normalize_polynomial -i demo.fits -p order=3 -o demo_normalised.fits
spectro run snr_der -i demo.fits
spectro run fit_gaussian_line -i demo.fits -p line_center_angstrom=6562.8 -p window_angstrom=30

Or, capturing the snr_check preset's full output as JSON:

spectro pipeline snr_check --input demo.fits --json

What you have learned

  • A WorkContext carries the analysis; algorithms read and write it.
  • run_algorithm runs one step; a Pipeline runs many.
  • The library, the CLI (and the MCP server) all drive the same catalogue.
  • Every run leaves a reproducible audit trail.

Next: make the catalogue your own → Tutorial: add an algorithm.