Skip to content

Aurora line monitor

Aurorae are noisy on every time scale. detect_lines with the bundled aurorae catalogue + plot_dynamic_spectrum turn a string of short exposures into a single picture of the night. Useful for aurorex-style near-real-time pipelines.

This walk-through synthesises five aurora-like spectra, runs the catalogue chain on them, then asks for the time-series visualisation.

1. Synthesise five minutes of green-line variability

The [O I] 5577 Å line is the bright auroral one. Throw in N₂⁺ 4278 and 3914 as secondary features, scale the green-line amplitude over time, sprinkle noise.

import numpy as np
from spectro_kernel.types import Spectrum1D, WorkContext

def _gauss(wave, centre, amp, fwhm):
    sigma = fwhm / 2.3548
    return amp * np.exp(-0.5 * ((wave - centre) / sigma) ** 2)

wave = np.linspace(3800, 6000, 4000)
green_strength = [0.6, 1.0, 1.4, 1.1, 0.5]  # five short exposures
spectra = []
for i, scale in enumerate(green_strength):
    flux = (
        50.0
        + _gauss(wave, 5577.34, amp=400.0 * scale, fwhm=1.2)  # [O I] green
        + _gauss(wave, 4278.10, amp=180.0,         fwhm=2.0)  # N2+ 1NG
        + _gauss(wave, 3914.40, amp=140.0,         fwhm=1.8)  # N2+ 1NG
        + np.random.default_rng(10 + i).normal(0.0, 4.0, wave.size)
    )
    spectra.append(
        Spectrum1D(wave, flux, meta={"date_obs": f"2026-01-12T22:0{i}:00"})
    )

2. Identify auroral lines on every frame

from spectro_kernel import run_algorithm

per_epoch = []
for i, sp in enumerate(spectra):
    ctx_i = WorkContext(spectrum=sp)
    run_algorithm("detect_lines", ctx_i, {"catalog": "aurorae", "prominence_sigma": 5.0})
    matched = [d["matched_line"] for d in ctx_i.extras["detected_lines"] if d["matched_line"]]
    per_epoch.append(matched)
    print(f"frame {i + 1}:", matched)

Every frame should pick up [O I] green plus the two N₂⁺ bands (and possibly a spurious match - that's why this is a monitor, not an absolute identifier).

3. Variability metric on the green line

For each epoch, fit a Gaussian to 5577 Å and grab the amplitude; that's the proxy you'd plot on the dashboard's left rail.

amplitudes = []
for sp in spectra:
    ctx_i = WorkContext(spectrum=sp)
    run_algorithm(
        "fit_gaussian_line",
        ctx_i,
        {"line_center_angstrom": 5577.34, "window_angstrom": 8.0, "label": "OI"},
    )
    amplitudes.append(ctx_i.line_fits["OI"].amplitude)

print("5577 Å amplitudes:", [round(a, 1) for a in amplitudes])

4. The dashboard view

Drop every epoch into a single WorkContext.spectra and let the time_series_overview preset render the three complementary views (dynamic heatmap + 3D surface + animation) in one call.

from spectro_kernel import PipelineBuilder

stack_ctx = WorkContext(spectra=spectra)
result = PipelineBuilder().from_preset("time_series_overview").build().execute(stack_ctx)

# ctx.figures now holds 3 ready-to-render Plotly JSON dicts:
for name in stack_ctx.figures:
    print(name, "→", type(stack_ctx.figures[name]).__name__)

For a real aurora pipeline the steps above run continuously: each new exposure appends to ctx.spectra, you re-run stack_spectra + plot_dynamic_spectrum on a 10-second cadence, and the dashboard refreshes its single PNG. That is exactly what a thin wrapper around spectro-kernel buys you over hand-coding it.

Going further

  • Pair with compare_normalisations once you have a brighter exposure: the N₂⁺ continuum is steep and the "best" normalisation changes between bands.
  • Drop the green-line amplitudes list into a light curve and run lomb_scargle to look for sub-hour periodicity (geomagnetic substorms).
  • Switch the catalogue to "nebular" and you have a faint-target dashboard that uses the same algorithms.