Tutorial: add an algorithm¶
The promise of spectro-kernel is that extending the catalogue is one Python file plus one test - no core to modify, no front-end to touch. This tutorial proves it by adding a real algorithm: a band integrator that sums the flux inside a wavelength window.
Step 1 - write the algorithm file¶
Create src/spectro_kernel/algorithms/quality/band_flux.py:
"""Algorithm: integrate the flux inside a wavelength band."""
from __future__ import annotations
from typing import Any
import numpy as np
from ...base import AlgorithmOutput, BaseAlgorithm
from ...registry import register_algorithm
from ...types import AlgorithmCategory, WorkContext
# numpy renamed trapz -> trapezoid in 2.0; support both.
try:
from numpy import trapezoid as _trapezoid
except ImportError: # numpy < 2.0
from numpy import trapz as _trapezoid
@register_algorithm("band_flux", category=AlgorithmCategory.QUALITY, version="1.0.0")
class BandFlux(BaseAlgorithm):
"""Integrate the flux over a wavelength band.
Returns the area under the spectrum between two wavelengths - a simple proxy for
the energy emitted in that band.
"""
backend = "numpy"
references = ["numpy.trapezoid - trapezoidal integration"]
default_params = {"wavelength_min": None, "wavelength_max": None}
required_params = ["wavelength_min", "wavelength_max"]
param_descriptions = {
"wavelength_min": "Lower bound of the band (Å).",
"wavelength_max": "Upper bound of the band (Å).",
}
input_requirements = ["spectrum"]
output_produces = ["metrics.band_flux"]
def run(self, ctx: WorkContext, params: dict[str, Any]) -> AlgorithmOutput:
band = ctx.spectrum.select_range(
float(params["wavelength_min"]), float(params["wavelength_max"])
)
value = float(_trapezoid(band.flux, band.wavelength))
ctx.metrics["band_flux"] = value
return AlgorithmOutput.ok(
metrics={"band_flux": value},
message=f"Integrated flux over the band = {value:.4g}.",
)
That is the whole algorithm. A few things to notice:
@register_algorithmis all the wiring there is. Automatic discovery imports the file; the decorator runs; the algorithm joins the catalogue.run(self, ctx, params)is the only method. Read fromctx, write toctx, return anAlgorithmOutput.paramsarrives already merged withdefault_paramsand validated.backendandreferencesmake the provenance explicit - see Algorithms → Provenance.input_requirements = ["spectrum"]means the runner fails cleanly if no spectrum is loaded - you do not have to check it yourself.
Step 2 - write a test¶
Create tests/unit/test_band_flux.py:
import numpy as np
from spectro_kernel import WorkContext, run_algorithm
from spectro_kernel.types import Spectrum1D
def test_band_flux_of_a_flat_spectrum():
# A flat spectrum at flux = 2 over a 100 Å band integrates to ~200.
wave = np.linspace(5000, 5200, 400)
ctx = WorkContext(spectrum=Spectrum1D(wave, np.full(wave.size, 2.0)))
run_algorithm("band_flux", ctx,
{"wavelength_min": 5050, "wavelength_max": 5150})
assert abs(ctx.metrics["band_flux"] - 200.0) < 1.0
Prefer a synthetic input with a known answer - that is what turns the test into a real check of correctness.
Step 3 - it is already everywhere¶
You did not edit a single other file. Verify:
spectro describe band_flux # the CLI sees it
spectro run band_flux -i obs.fits -p wavelength_min=6500 -p wavelength_max=6620
pytest tests/unit/test_band_flux.py # the test passes
It is also now an MCP tool, and list_algorithms() returns it, and it can be used as a
pipeline step. One file in, available through every door.
Conventions to follow¶
- Naming:
snake_case, verb-first. Add a suffix when several implementations of one operation coexist (band_flux,band_flux_weighted). - Determinism: no
eval/exec, no hidden global state; seed any RNG fromparams. - Versioning: bump
versionon any behaviour change so old results stay reproducible. - Provenance: always set
backend; cite the literature inreferenceswhen you wrap a library or implement a published method. - Optional dependencies: if the algorithm needs a package outside the core, import
it lazily and register the algorithm conditionally (see
algorithms/catalogs/simbad.pyfor the pattern), so the catalogue still loads without the extra.
See CONTRIBUTING.md for the full checklist.