{
  "schema_version": "1",
  "spectrokernel_version": "0.8.1",
  "site_url": "https://docs.spectrokernel.io/",
  "server": {
    "name": "spectro-kernel",
    "instructions": "spectro-kernel exposes a catalogue of astronomical spectroscopy algorithms.\n\nTypical workflow:\n  1. create_session()                       -> session_id\n  2. load_spectrum(session_id, path)        load a FITS/ASCII spectrum\n  3. call analysis tools (normalize_polynomial, snr_der, fit_gaussian_line, ...)\n     each one takes session_id and an optional params object\n  4. get_session_state(session_id)          inspect accumulated results\n\nUse list_algorithms / describe_algorithm to discover tools and their parameters, or\nrun_preset to execute a whole pipeline at once. The catalogue is also readable as\nresources: spectro://algorithms, spectro://algorithms/{name}, spectro://recipes,\nspectro://recipes/{name} and spectro://llms.txt (a short map of this server)."
  },
  "count": 142,
  "n_session_tools": 13,
  "n_algorithm_tools": 129,
  "tools": [
    {
      "name": "create_session",
      "description": "Create a new working session and return its id.\n\n        Every other tool needs a session_id. Call this first.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {},
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "end_session",
      "description": "Discard a working session and free its memory.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "load_spectrum",
      "description": "Load a spectrum file (FITS or ASCII, local path or http(s) URL) into a session.\n\n        The spectrum becomes the session's working spectrum, ready for the analysis tools.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "path": {
            "type": "string"
          }
        },
        "required": [
          "session_id",
          "path"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "load_spectrum_from_url",
      "description": "Download a spectrum from *url* and load it as the session's working spectrum.\n\n        Use after ``request_upload_url`` (with the returned ``download_url``) or\n        with any reachable http(s) URL. The file type is inferred from the URL\n        suffix; pass ``format_hint`` (\"fits\" or \"ascii\") to override.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "format_hint": {
            "default": "",
            "type": "string"
          }
        },
        "required": [
          "session_id",
          "url"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "request_upload_url",
      "description": "Return a presigned URL the client can PUT a file to, plus the GET URL.\n\n        Use this when a spectrum is too large to inline in MCP. The client uploads\n        with HTTP PUT, then calls ``load_spectrum_from_url`` with the returned\n        ``download_url``. Requires SPECTRO_MCP_S3_BUCKET (+ optional endpoint)\n        configured on the server.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "default": "application/octet-stream",
            "type": "string"
          }
        },
        "required": [
          "session_id",
          "filename"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "get_session_state",
      "description": "Return a summary of everything currently held in a session's context.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "get_spectrum_preview",
      "description": "Return a downsampled preview of the session's current spectrum (50 samples).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "get_spectrum_data",
      "description": "Return the session spectrum as JSON arrays, at most ``max_points`` samples.\n\n        Heavier than ``get_spectrum_preview`` — use this when the caller needs\n        to plot or re-analyse the spectrum client-side and the 50-point\n        preview is too coarse. Returns ``{wavelength, flux, uncertainty,\n        wavelength_unit, flux_unit, meta, downsampled, npix_total,\n        npix_returned, stride}``. Spectra longer than ``max_points`` (default\n        20000) are stride-downsampled (every k-th sample) and flagged\n        ``downsampled: true``; pass ``max_points=0`` to get every pixel.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "max_points": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "default": 20000
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "list_algorithms",
      "description": "List the available spectroscopy algorithms, optionally filtered by category.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "category": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "type": "object"
      },
      "output_schema": {
        "properties": {
          "result": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "result"
        ],
        "type": "object",
        "x-fastmcp-wrap-result": true
      },
      "kind": "session"
    },
    {
      "name": "describe_algorithm",
      "description": "Return the full details (parameters, inputs, outputs) of one algorithm.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "get_algorithm_source",
      "description": "Return the source code of one algorithm's wrapper class.\n\n        Useful when the agent (or its user) wants to audit what the wrapper\n        does — especially for algorithms backed by an external library\n        (astropy, easyspec, scipy). The response carries the repository-\n        relative path, a GitHub permalink, the declared backend, and the\n        source as a UTF-8 string. The absolute path on disk (``path``) is\n        only filled in when the server runs locally (stdio mode).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "list_presets",
      "description": "List every preset available on this server (bundled + installed collections).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {},
        "type": "object"
      },
      "output_schema": {
        "properties": {
          "result": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array"
          }
        },
        "required": [
          "result"
        ],
        "type": "object",
        "x-fastmcp-wrap-result": true
      },
      "kind": "session"
    },
    {
      "name": "run_preset",
      "description": "Run a complete preset pipeline against a session's context in one call.\n\n        ``variables`` binds the preset's declared variables (its instrument /\n        observer profile); ``list_presets`` says how many a preset declares and\n        ``describe`` them via the preset's page or ``spectro preset show``.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "preset_name": {
            "type": "string"
          },
          "variables": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id",
          "preset_name"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "session"
    },
    {
      "name": "activity_index_caii_hk",
      "description": "Mount Wilson S index from the Ca II H & K cores, plus R'_HK when B−V is given.\n\nS = alpha · 8 · (H + K) / (R + V) where H, K, R, V are fluxes integrated (flux × Å) through the HKP-2 passbands : K 3933.664 Å and H 3968.470 Å triangular with 1.09 Å FWHM (2.18 Å base), V 3891.07–3911.07 Å and R 3991.07–4011.07 Å rectangular. Each sample is weighted by its transmission and its pixel width, so the value is sampling-independent ; band edges fall on the nearest pixel centre. The spectrum must cover 3891–4011 Å (air wavelengths), be at rest wavelength (correct the radial velocity first) and have at least 3 samples inside the 1.09 Å FWHM of each H / K triangle (pixels ≲ 0.36 Å). s_index is an INSTRUMENTAL S : bring it onto the Mount Wilson scale with a linear fit against standard stars, then pass the (a, b) of S_MW = a·S + b as s_index_calibration. s_index_error propagates the spectrum's uncertainty array (None without one). log_rhk is written only when b_v is given ; it applies Noyes et al. 1984 to the calibrated S (log C_cf = 1.13(B−V)³ − 3.91(B−V)² + 2.84(B−V) − 0.47 + blue-end term, R_HK = 1.34e-4·C_cf·S, log R_phot = −4.898 + 1.918(B−V)² − 2.893(B−V)³). The photospheric correction is calibrated on 0.44 < B−V < 0.82 : outside that range log_rhk is still written but flagged in the message. Rejects (no log_rhk) when R_HK ≤ R_phot. Metrics are prefixed for provenance: s_index_alpha and s_index_calibrated echo the inputs. v2.0.0: masked samples (Spectrum1D.mask) are ignored — dropped from the passband integrals (and their transmission from the weight sum) like non-finite ones.\n\nParameters — pass as the 'params' object:\n- alpha (default 2.3): Mount Wilson calibration constant α in S = α·8·(H+K)/(R+V). 2.3 (Lovis et al. 2011) by default ; Duncan et al. 1991 use 2.4.\n- b_v (default None): Johnson B−V colour of the star. When given, log R'_HK (Noyes et al. 1984) is computed ; valid for 0.44 < B−V < 0.82.\n- s_index_calibration (default None): Optional [a, b] such that S_MW = a·S_inst + b, applied before the R'_HK conversion and reported as s_index_calibrated.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Vaughan, Preston & Wilson 1978, PASP 90, 267 — HKP-2 : triangular 1.09 Å FWHM H and K passbands, 20 Å R and V bands, S = α(H+K)/(R+V).; Duncan et al. 1991, ApJS 76, 383 — Mount Wilson survey ; α = 2.4 and the 8× H/K exposure ratio of HKP-2.; Noyes et al. 1984, ApJ 279, 763 — R'_HK = R_HK − R_phot with R_HK = 1.34e-4·C_cf·S and the C_cf / R_phot polynomials in B−V.; Middelkoop 1982, A&A 107, 31 — log C_cf polynomial adopted by Noyes.; Lovis et al. 2011, arXiv:1107.5325 — S from échelle spectra (α = 2.3) and S_MW = 1.111·S_HARPS + 0.0153 calibration.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "activity",
      "category_title": "Stellar activity",
      "summary": "Mount Wilson S index from the Ca II H & K cores, plus R'_HK when B−V is given.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/activity/activity_index_caii_hk/"
    },
    {
      "name": "activity_index_halpha",
      "description": "Hα activity index : mean core flux over the sum of two reference-band fluxes.\n\nI_Hα = F_Hα / (F_1 + F_2) with F the pixel-width-weighted MEAN flux in each rectangular band (6562.008–6563.608 Å core ; 6545.495–6556.245 Å and 6575.935–6584.685 Å references). Band edges fall on the nearest pixel centre. The spectrum must cover 6545.5–6584.7 Å in the star's rest frame with at least 3 samples in the core band. The index is instrument-dependent : use it to follow one star's chromospheric variability, not to compare instruments. halpha_index_error propagates the uncertainty array (absent without one). For a Ca II H & K equivalent see activity_index_caii_hk. v2.0.0: masked samples (Spectrum1D.mask) are ignored — dropped from the passband means (and their pixel width from the weight sum) like non-finite ones.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Gomes da Silva et al. 2011, A&A 534, A30 — Hα index : 1.6 Å core on 6562.808 Å, reference bands 6550.87 ± 5.375 Å and 6580.31 ± 4.375 Å.; Boisse et al. 2009, A&A 495, 959 — Hα index definition for HD 189733.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "activity",
      "category_title": "Stellar activity",
      "summary": "Hα activity index : mean core flux over the sum of two reference-band fluxes.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/activity/activity_index_halpha/"
    },
    {
      "name": "air_to_vacuum",
      "description": "Convert the wavelength axis from air to vacuum wavelengths.\n\nλ_vac = λ_air · n(λ_air) with the VALD3 closed-form inverse of Morton 2000 (coefficients 8.336624212083e-5, 2.408926869968e-2/(130.1065924522 - s²), 1.599740894897e-4/(38.92568793293 - s²), s = 1e4/λ[Å]). Acceptance: Hα 6562.79 → 6564.603 Å. Valid above ~2000 Å.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Piskunov N., VALD3 'Air to vacuum conversion' (Uppsala VALD wiki, https://www.astro.uu.se/valdwiki/Air-to-vacuum%20conversion) — closed-form inverse fit n(λ_air) of the Morton 2000 relation.; Morton 2000, ApJS 130, 403 — air/vacuum dispersion relation n(λ_vac).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Convert the wavelength axis from air to vacuum wavelengths.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/air_to_vacuum/"
    },
    {
      "name": "aperture_photometry",
      "description": "Differential aperture photometry on ``ctx.image`` (photutils).\n\nProvide the target's (x, y) pixel coordinates and one or more comparison stars. The differential magnitude for each comparison is -2.5 * log10(target_flux / comp_flux); positive means the target is fainter than the comparison.\n\nParameters — pass as the 'params' object:\n- target_xy (default None) [required]: Target (x, y) pixel coordinates (length-2 list).\n- comparison_xy (default None) [required]: List of (x, y) coordinates for comparison stars.\n- aperture_radius (default 5.0): Aperture radius (pixels) for source extraction.\n- annulus_in_radius (default 8.0): Inner radius of the sky annulus (pixels).\n- annulus_out_radius (default 12.0): Outer radius of the sky annulus (pixels).\n\nRequires in the session context: image\n\nBackend: photutils.\nReferences: Bradley et al. — Astropy Photutils (https://photutils.readthedocs.io/).; AAVSO Guide to CCD Photometry.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "advanced",
      "category_title": "Advanced",
      "summary": "Differential aperture photometry on ``ctx.image`` (photutils).",
      "docs_url": "https://docs.spectrokernel.io/algorithms/advanced/aperture_photometry/"
    },
    {
      "name": "atmospheric_extinction_correct",
      "description": "Remove atmospheric extinction : F_0 = F · 10^(0.4 · k(λ) · X) with a mean site curve.\n\nF_0(λ) = F(λ) · 10^(0.4 · k(λ) · X). curve='kpno' (default) uses the IRAF KPNO mean extinction table (82 nodes, 3200–10400 Å, mag/airmass) reproduced verbatim ; per the file's own note its last four nodes, 8708–10400 Å, are CTIO values) — a MEAN curve for a 2 km site, not a nightly measurement : expect a few 0.01 mag/airmass of aerosol scatter, and prefer a curve measured at your own site. curve='custom' reads ctx.extras[extinction_table_key] : a Spectrum1D whose flux is k(λ), a dict with 'wavelength_aa' and 'k_mag' lists, or an (N, 2) array. The curve is linearly interpolated onto the spectrum ; samples outside the curve's wavelength range take the nearest end value (counted in metrics.n_outside_curve). Airmass precedence : the airmass parameter, then the FITS AIRMASS keyword, then the geometry (RA/DEC + DATE-OBS at mid-exposure + site latitude/longitude/elevation from parameters or the SITELAT/SITELONG/SITEELEV, LAT-OBS/LONG-OBS/ALT-OBS or OBS-LAT/OBS-LONG/OBS-ELEV keywords) with astropy AltAz (no refraction) and Kasten & Young 1989 Eq. 3 (X(0°) = 0.99971, X(60°) = 1.99429, X(80°) = 5.586, X(90°) = 37.92 — 0.1–0.3 % below sec z at 45–60°) ; the brick fails when none is available. Wavelengths must be in Ångström. The uncertainty is multiplied by the same factor (curve and airmass taken as noise-free). The correction removes the smooth continuum extinction only — the O2 / H2O telluric bands are handled by remove_telluric_division.\n\nParameters — pass as the 'params' object:\n- airmass (default None): Airmass X of the observation. None ⇒ FITS AIRMASS keyword, then computed from the geometry (Kasten & Young 1989).\n- curve (default 'kpno'): 'kpno' (IRAF KPNO mean table, default) or 'custom' (user table in extras).\n- extinction_table_key (default 'extinction_table'): ctx.extras key holding the user extinction table when curve='custom' (Spectrum1D with flux = k mag/airmass, {'wavelength_aa', 'k_mag'} dict, or (N, 2) array).\n- ra_deg (default None): Target right ascension (deg) for the geometric airmass ; None ⇒ FITS RA.\n- dec_deg (default None): Target declination (deg) for the geometric airmass ; None ⇒ FITS DEC.\n- obstime (default None): Start-of-exposure time (ISO-8601) ; None ⇒ FITS DATE-OBS.\n- exposure_seconds (default None): Exposure duration (s) — the airmass is evaluated at mid-exposure ; None ⇒ FITS EXPTIME, then 0.\n- latitude_deg (default None): Observer geodetic latitude (deg) ; None ⇒ SITELAT-style keywords.\n- longitude_deg (default None): Observer geodetic longitude (deg, east positive) ; None ⇒ SITELONG-style keywords.\n- elevation_m (default None): Observer elevation (m) ; None ⇒ SITEELEV-style keywords, then 0.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Hayes & Latham 1975, ApJ 197, 593 — mean atmospheric extinction (Rayleigh + aerosol + ozone) and the 10^(0.4 k X) correction.; Kasten & Young 1989, Applied Optics 28, 4735 — airmass formula X = 1/[cos z + 0.50572 (96.07995 − z)^−1.6364] (Eq. 3).; Tody 1986, Proc. SPIE 627, 733 — IRAF ; the built-in curve is onedstds$kpnoextinct.dat (KPNO mean extinction, 3200–10400 Å).; Hardie 1962, in Astronomical Techniques (ed. Hiltner), Univ. of Chicago Press, ch. 8 — extinction linear in airmass.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Remove atmospheric extinction : F_0 = F · 10^(0.4 · k(λ) · X) with a mean site curve.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/atmospheric_extinction_correct/"
    },
    {
      "name": "balmer_decrement_extinction",
      "description": "Nebular extinction c(Hβ), E(B−V) and A_V from the observed Hα/Hβ Balmer decrement.\n\nR_obs = F(Hα)/F(Hβ) with F = |amp|·|σ|·√(2π) per line (window ±8 Å). c(Hβ) = log10(R_obs / R_int) / (1 − A(Hα)/A(Hβ)) — the reddening function f(λ) = A(λ)/A(Hβ) − 1 (f(Hβ) = 0) is taken from the law ; E(B−V) = 2.5 c(Hβ) / [R_V · A(Hβ)/A_V] and A_V = R_V E(B−V). law='CCM89' (default) is evaluated in-module from Cardelli et al. 1989 Eqs. 3a-3b (for R_V = 3.1 : A(Hα)/A(Hβ) = 0.7025, f(Hα) = −0.2975, A(Hβ)/E(B−V) = 3.609, c(Hβ) = 1.444 E(B−V)) ; 'F99' / 'G23' use dust_extinction (reduction extra). intrinsic_ratio defaults to 2.86 (Osterbrock & Ferland 2006 Table 4.4, Case B, 1e4 K, 1e2 cm⁻³ ; use 2.75 at 2e4 K, 3.04 at 5e3 K, or ~3.1 for AGN narrow-line regions). A ratio below intrinsic_ratio gives a negative extinction : c(Hβ), E(B−V) and A_V are clamped to 0 and the `clamped` flag of extras.balmer_decrement is set. Stellar Balmer absorption under Hβ is NOT corrected — subtract the stellar continuum first for galaxies. The line fluxes must be on the same flux scale (relative flux calibration is enough ; an uncalibrated instrumental response biases the ratio). Uncertainty : none is propagated — the shared Gaussian-line primitive returns fluxes without errors ; from Eq. 3, δc(Hβ) = δR / (R · ln 10 · |f(Hα)|) = 1.46 δR/R for CCM89 at R_V = 3.1 (a 5 % ratio error is δc ≈ 0.07, δE(B−V) ≈ 0.05), the intrinsic ratio and the law being taken as exact. v2.0.0: masked samples (Spectrum1D.mask) are ignored — excluded from both line windows like non-finite ones.\n\nParameters — pass as the 'params' object:\n- lambda_halpha (default 6562.82): Rest wavelength (Å) of Hα (default NIST air 6562.82, as bpt).\n- lambda_hbeta (default 4861.33): Rest wavelength (Å) of Hβ (default NIST air 4861.33, as bpt).\n- window (default 8.0): Half-width (Å) of the Gaussian fit window around each line.\n- intrinsic_ratio (default 2.86): Intrinsic (Hα/Hβ)_int : 2.86 = Case B at 1e4 K, 1e2 cm⁻³ (Osterbrock & Ferland 2006 Table 4.4).\n- rv (default 3.1): R_V = A_V / E(B−V) of the extinction law (3.1 diffuse ISM).\n- law (default 'CCM89'): Extinction law for f(λ) : 'CCM89' (built in), 'F99' or 'G23' (dust_extinction).\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Osterbrock & Ferland 2006, Astrophysics of Gaseous Nebulae and AGN, 2nd ed., University Science Books — Table 4.4 (Case B Hα/Hβ = 2.86 at T_e = 1e4 K, n_e = 1e2 cm⁻³) and §7.2 (c(Hβ), f(λ)).; Cardelli, Clayton & Mathis 1989, ApJ 345, 245 — Eqs. 3a-3b, A(λ)/A_V optical polynomial (built-in law).; Kramida et al., NIST ASD — air rest wavelengths Hα 6562.82 Å, Hβ 4861.33 Å (the values used by bpt_line_ratios).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "nebular",
      "category_title": "Nebular diagnostics",
      "summary": "Nebular extinction c(Hβ), E(B−V) and A_V from the observed Hα/Hβ Balmer decrement.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/nebular/balmer_decrement_extinction/"
    },
    {
      "name": "barycentric_correction",
      "description": "Compute the barycentric (or heliocentric) correction and Julian date, and shift the spectrum.\n\nThe observer location defaults to the geocentre; set latitude/longitude/elevation for the diurnal term (up to ~0.5 km/s). Uses astropy's SkyCoord.radial_velocity_correction and Time.light_travel_time. kind='barycentric' (default) emits metrics.bjd_tdb and a BJD_TDB header card; kind='heliocentric' emits metrics.hjd_utc, an HJD card and the BeSS BSS_VHEL card (BSS_VHEL is heliocentric *by definition* — the kernel refuses to fill it from a barycentric velocity, the two differ by up to ~13 m/s). BSS_VHEL carries the applied velocity when apply_shift is true, else 0 (BeSS convention for an uncorrected product). The Julian date is computed at MID-exposure: start time + exposure_seconds/2, falling back to the FITS EXPTIME when the parameter is null, and to 0 (start = mid) when neither exists.\n\nParameters — pass as the 'params' object:\n- ra_deg (default None): Target right ascension (deg); falls back to the FITS RA keyword.\n- dec_deg (default None): Target declination (deg); falls back to the FITS DEC keyword.\n- obstime (default None): Start-of-exposure time (ISO-8601); falls back to FITS DATE-OBS.\n- latitude_deg (default 0.0): Observer geodetic latitude (deg).\n- longitude_deg (default 0.0): Observer geodetic longitude (deg, east positive).\n- elevation_m (default 0.0): Observer elevation above the ellipsoid (m).\n- apply_shift (default True): If true, Doppler-shift the wavelength axis to the chosen frame.\n- kind (default 'barycentric'): 'barycentric' (default; emits BJD_TDB) or 'heliocentric' (emits HJD and the BeSS BSS_VHEL card).\n- exposure_seconds (default None): Exposure duration (s), used to compute the Julian date at mid-exposure; null falls back to the FITS EXPTIME keyword, then to 0.\n\nRequires in the session context: spectrum\n\nBackend: astropy.\nReferences: astropy.coordinates SkyCoord.radial_velocity_correction; Wright & Eastman 2014, PASP 126, 838 — barycentric correction precision; Eastman, Siverd & Gaudi 2010, PASP 122, 935 — BJD_TDB as the unambiguous time standard (HJD/UTC ambiguities reach the minute).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Compute the barycentric (or heliocentric) correction and Julian date, and shift the spectrum.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/barycentric_correction/"
    },
    {
      "name": "bias_combine",
      "description": "Combine a stack of bias frames into one master bias.\n\nParameters — pass as the 'params' object:\n- method (default 'median'): Combination method: 'median' (robust) or 'mean'.\n\nRequires in the session context: images\n\nBackend: numpy.\nReferences: Howell 2006 — Handbook of CCD Astronomy, ch. 4 (standard CCD reduction).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "master_creation",
      "category_title": "Master frames",
      "summary": "Combine a stack of bias frames into one master bias.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/master_creation/bias_combine/"
    },
    {
      "name": "bias_combine_easyspec",
      "description": "Combine bias frames into a master bias via ``easyspec.cleaning.master``.\n\nRequires the optional `reduction` extra. The matplotlib backend is forced to 'Agg' at import time so easyspec's plotting defaults never open a window in a server context.\n\nParameters — pass as the 'params' object:\n- bias_dir (default None): Directory containing the bias FITS files.\n- bias_paths (default None): List of bias FITS file paths (symlinked into a temp dir).\n- method (default 'median'): Stacking method passed to easyspec: median, mean, or mode.\n- header_hdu_entry (default 0): HDU extension where the FITS header lives.\n\nBackend: easyspec.\nReferences: easyspec — Lobão et al. (https://pypi.org/project/easyspec/).; easyspec.cleaning.cleaning.master.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "master_creation",
      "category_title": "Master frames",
      "summary": "Combine bias frames into a master bias via ``easyspec.cleaning.master``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/master_creation/bias_combine_easyspec/"
    },
    {
      "name": "box_least_squares",
      "description": "Box least squares transit search (Kovács, Zucker & Mazeh 2002) via astropy.\n\nWraps astropy.timeseries.BoxLeastSquares. Durations are tried in turn at every period (pass the list expected for the target ; the reported depth is diluted when the true duration is shorter than the best box). The period grid is astropy's autoperiod grid (uniform in frequency with spacing min(duration)/baseline², bounds period_min / period_max defaulting to 2·max(duration) and baseline/3) unless n_periods is given, in which case n_periods frequencies are spaced uniformly between 1/period_max and 1/period_min. objective='likelihood' maximises the log-likelihood of the box model (astropy default) ; 'snr' maximises depth/depth_err. Point uncertainties are used as inverse-variance weights when the light curve carries them ; otherwise every point gets the same σ = 1.4826 × MAD(flux − median), the robust point-to-point scatter, so that depth_err and bls_snr are on the data's noise scale (astropy alone would assume σ = 1 and report a meaningless SNR ; the uniform weight leaves the period ranking identical to the unweighted fit). Regime : times in days (metric names say so), a detrended/normalised flux, at least ~2 transits inside the baseline ; period_min must exceed the longest duration. bls_snr is astropy's depth_snr = depth/depth_err at the best period — compare with the ≈ 6 detection level of Kovács et al. ; no false-alarm probability is computed.\n\nParameters — pass as the 'params' object:\n- time (default None): List of observation times; omit to use a light curve from the context.\n- flux (default None): List of flux values, paired with 'time'.\n- uncertainty (default None): Optional list of per-point flux uncertainties.\n- light_curve_key (default None): Key in ctx.light_curves to use when time/flux are omitted.\n- period_min (default None): Shortest trial period in days; null = 2 × longest duration.\n- period_max (default None): Longest trial period in days; null = baseline / 3.\n- duration_hours (default [1.0, 2.0, 4.0]): Transit duration(s) to try, in hours (single value or list).\n- n_periods (default None): Number of trial periods, uniform in frequency; null = astropy's autoperiod grid.\n- objective (default 'likelihood'): Quantity maximised over phase/depth/duration: one of ['likelihood', 'snr'].\n- output_key (default 'bls'): Key under which the power spectrum is stored in ctx.periodograms.\n\nBackend: astropy.\nReferences: Kovács, Zucker & Mazeh 2002, A&A 391, 369 — the box-fitting least squares algorithm: periodic alternation between two levels with a short low state of fractional length q ; detection driven by the effective SNR depth/σ (significant above ≈ 6 in their simulations).; astropy.timeseries.BoxLeastSquares — the wrapped implementation (likelihood / snr objectives ; depth, depth_err, depth_snr ; autoperiod grid heuristics).; Rousseeuw & Croux 1993, J. Am. Stat. Assoc. 88, 1273 — 1.4826 × MAD as a consistent robust estimate of σ for Gaussian noise, used as the uniform point uncertainty when the light curve carries none.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "timeseries",
      "category_title": "Time series",
      "summary": "Box least squares transit search (Kovács, Zucker & Mazeh 2002) via astropy.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/timeseries/box_least_squares/"
    },
    {
      "name": "bpt_line_ratios",
      "description": "Measure BPT line ratios + classify a galaxy (HII / Composite / Seyfert / LINER).\n\nBPT classification uses the [N II]/Hα plane only (the most-cited standard). Auxiliary log ratios log_oii_hb (3727/Hβ) and log_sii_ha ((6716+6731)/Hα) are reported for downstream analysis but not used for the class label. Override the default rest wavelengths via params['lines'] (dict). v1.1.0: extras['method'] (shared with the other nebular bricks) is also written as extras['bpt_method'], and line_fluxes / rest_wavelengths / fit_details as bpt_line_fluxes / bpt_rest_wavelengths / bpt_fit_details, so a pipeline keeps unambiguous provenance. v2.0.0: masked samples (Spectrum1D.mask) are ignored — excluded from every line window like non-finite ones.\n\nParameters — pass as the 'params' object:\n- window (default 8.0): Half-width (Å) of the fit window around each line.\n- lines (default {'ha': 6562.82, 'hb': 4861.33, 'oiii_5007': 5006.84, 'nii_6583': 6583.45, 'sii_6716': 6716.44, 'sii_6731': 6730.82, 'oii_3727': 3727.42}): Mapping of line keys to rest wavelengths (Å). Keys: ha, hb, oiii_5007, nii_6583, sii_6716, sii_6731, oii_3727.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Baldwin, Phillips & Terlevich 1981, PASP 93, 5 — original BPT.; Kewley & Dopita 2001, ApJ 556, 121 — maximum-starburst line.; Kauffmann et al. 2003, MNRAS 346, 1055 — HII/AGN empirical split.; Cid Fernandes et al. 2010, MNRAS 403, 1036 — Seyfert/LINER cut.; Kewley et al. 2006, MNRAS 372, 961 — [S II], [O I] variants.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "nebular",
      "category_title": "Nebular diagnostics",
      "summary": "Measure BPT line ratios + classify a galaxy (HII / Composite / Seyfert / LINER).",
      "docs_url": "https://docs.spectrokernel.io/algorithms/nebular/bpt_line_ratios/"
    },
    {
      "name": "ccf_bisector",
      "description": "Bisector of a CCF (or one line) and the Queloz et al. 2001 Bisector Inverse Slope.\n\nReads ctx.extras['ccf'] as written by cross_correlate_rv ({'lags_kms', 'ccf'}) — a Pearson peak whose baseline is the median of the outer baseline_fraction of the lag range on each side — or, when line_center_angstrom is given, one line of ctx.spectrum within ± window_angstrom converted to v = c·(λ − λ0)/λ0 with a straight continuum through the outer continuum_fraction of the window (absorption or emission, whichever excursion is larger). The depth profile runs from 0 (continuum) to 1 (core) ; the bisector is sampled on n_levels equally spaced depths in (0, 1) and stored in extras['ccf_bisector'] = {'depth', 'velocity_kms', 'source', ...} (None where a wing does not reach the level). bis_kms = mean bisector velocity over top_depth_range minus mean over bottom_depth_range (Queloz et al. 2001 : 10–40 % and 55–90 %) ; at least 3 valid levels are required in each zone, and the top_depth_range[0] level must be crossed on both wings inside the region excluded from the continuum / baseline anchors (fails when the wings do not reach the continuum inside the window). bisector_span_kms = v_bis(top_depth_range[0]) − v_bis(bottom_depth_range[1]) at the nearest sampled levels (Gray's velocity span between fixed depths). The measurement needs the profile sampled with at least ~10 points across the line (a cross_correlate_rv CCF at n_grid=4096 on 4000–7000 Å has 41 km/s bins : keep its default n_grid=None, which follows the native sampling) and a S/N high enough that the bisector scatter (≈ noise / slope of the wings) is below the effect sought. v2.0.0: in line mode masked samples (Spectrum1D.mask) are ignored — dropped like non-finite ones before the continuum fit and the bisection.\n\nParameters — pass as the 'params' object:\n- line_center_angstrom (default None): Rest-frame centre (Å) of a single line of ctx.spectrum to bisect. None (default) analyses the CCF in ctx.extras instead.\n- window_angstrom (default 5.0): Half-width (Å) of the line window on each side of the centre.\n- continuum_fraction (default 0.2): Fraction of each window edge used to anchor the straight continuum (line mode only).\n- ccf_key (default 'ccf'): Key of the CCF dict in ctx.extras (cross_correlate_rv writes 'ccf').\n- baseline_fraction (default 0.2): Fraction of the lag range, at each end, whose median defines the CCF baseline (CCF mode only).\n- n_levels (default 99): Number of equally spaced depth levels in (0, 1) at which to bisect.\n- top_depth_range (default None): [lo, hi] depth fractions of the 'top' zone ; default [0.10, 0.40] (Queloz et al. 2001).\n- bottom_depth_range (default None): [lo, hi] depth fractions of the 'bottom' zone ; default [0.55, 0.90] (Queloz et al. 2001).\n\nBackend: numpy.\nReferences: Queloz et al. 2001, A&A 379, 279 — bisector inverse slope BIS = v_top(10–40 % depth) − v_bottom(55–90 % depth) of the CCF.; Toner & Gray 1988, ApJ 334, 1008 — bisector by interpolating both wings at each flux level.; Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 17, line bisectors and velocity span.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "activity",
      "category_title": "Stellar activity",
      "summary": "Bisector of a CCF (or one line) and the Queloz et al. 2001 Bisector Inverse Slope.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/activity/ccf_bisector/"
    },
    {
      "name": "classify_template_chi2",
      "description": "Spectral classification by χ² against a Pickles-style template atlas.\n\nCommon wavelength grid (default 3800–7500 Å at 2 Å step, matching pickles_atlas.canonical_grid()), running-median continuum normalisation (window 80 samples), unweighted χ² over the samples shared by observed + template. Templates with useable overlap < min_overlap are skipped. Confidence flags : ambiguous (χ²[1]/χ²[0] < 1.20) and teff_unstable (top-3 ΔT_eff > 500 K). Supply either `atlas` (an Atlas object, Python API) or `atlas_dir` (path to the Pickles .dat files, loaded with load_atlas_from_dir and cached per directory) ; `atlas` wins when both are given. The grid is always the atlas's own grid_aa : grid_lo / grid_hi / grid_step are accepted for backwards compatibility but currently UNUSED. Normalised templates are cached on Atlas.norm_cache (identical numbers, ~100× faster repeat calls). A descending observed wavelength axis is sorted before resampling.\n\nParameters — pass as the 'params' object:\n- atlas (default None): An Atlas instance (see classification.pickles_atlas.Atlas). Build with load_atlas_from_dir(path) or supply a custom {template_id → flux_on_grid} mapping plus meta. Python API only ; CLI/MCP callers use atlas_dir.\n- atlas_dir (default None): Directory holding the Pickles uk*.dat templates ; loaded with load_atlas_from_dir onto the canonical grid (cached). Ignored when atlas is given.\n- top_n (default 5): Number of best-fit matches to report (≥ 1).\n- min_overlap (default 0.6): Reject the run if observed-vs-grid overlap drops below this fraction.\n- continuum_window (default 80): Running-median window (samples) for continuum normalisation.\n- grid_lo (default 3800.0): Reserved (currently unused): the atlas's own grid_aa is used.\n- grid_hi (default 7500.0): Reserved (currently unused): the atlas's own grid_aa is used.\n- grid_step (default 2.0): Reserved (currently unused): the atlas's own grid_aa is used.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Pickles 1998, PASP 110, 863 — UVKLIB stellar template atlas.; Cappellari 2017, MNRAS 466, 798 — pPXF full-spectrum fitting.; Koleva et al. 2009, A&A 501, 1269 — ULySS.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "classification",
      "category_title": "Spectral classification",
      "summary": "Spectral classification by χ² against a Pickles-style template atlas.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/classification/classify_template_chi2/"
    },
    {
      "name": "clip_cosmic_rays",
      "description": "Detect and replace cosmic-ray hits on ``ctx.image``.\n\nL.A.Cosmic detects cosmic-ray cores by Laplacian edge detection and replaces the affected pixels with a local median. The fallback path uses a 3x3 median filter as the reference: any pixel whose excess over the median exceeds `sigma` standard deviations is replaced by that local median.\n\nParameters — pass as the 'params' object:\n- sigma (default 5.0): Detection threshold in noise units.\n- gain (default 1.0): Detector gain in electrons/ADU (used by L.A.Cosmic).\n- readnoise (default 6.0): Detector read noise in electrons (used by L.A.Cosmic).\n\nRequires in the session context: image\n\nBackend: scipy.\nReferences: van Dokkum 2001, PASP 113, 1420 — L.A.Cosmic; McCully et al. — astroscrappy (https://github.com/astropy/astroscrappy)",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "cosmic_ray",
      "category_title": "Cosmic-ray rejection",
      "summary": "Detect and replace cosmic-ray hits on ``ctx.image``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/cosmic_ray/clip_cosmic_rays/"
    },
    {
      "name": "clip_sigma",
      "description": "Remove outlier samples (cosmic rays, hot pixels) by iterative sigma clipping.\n\nParameters — pass as the 'params' object:\n- sigma (default 5.0): Clipping threshold, in standard deviations of the residual.\n- window (default 5): Median-filter window (samples) used to estimate the local baseline.\n- iterations (default 3): Maximum number of clipping iterations.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Bevington & Robinson 2003, Data Reduction and Error Analysis for the Physical Sciences, 3rd ed., McGraw-Hill — Chauvenet's criterion / iterative rejection of samples beyond k·σ of the fit.; astropy.stats.sigma_clip — iterative σ-clipping about a robust centre (the same iterate-until-no-new-rejection scheme, applied here to the residual from a running median).; scipy.ndimage.median_filter — local baseline.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "transform",
      "category_title": "Transforms",
      "summary": "Remove outlier samples (cosmic rays, hot pixels) by iterative sigma clipping.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/transform/clip_sigma/"
    },
    {
      "name": "combine_spectra_arithmetic",
      "description": "Add, subtract, multiply or divide ``ctx.spectrum`` by a reference.\n\noperation ∈ {add, sub, mul, div}. For 'div', samples where |reference| < min_denominator become NaN (clamp on near-zero responses). Pass-through outside the reference domain matches IRAF sarith and prevents wing zero-out. v2.0.0 — uncertainty propagation (independent errors, Bevington & Robinson §3.2) : add/sub σ = sqrt(σ_s² + σ_r²) ; mul/div σ = |result| · sqrt((σ_s/s)² + (σ_r/r)²) when the reference carries an uncertainty (σ_r resampled as sqrt(interp(σ_r²))), else σ = σ_s · |r| (mul) or σ_s / |r| (div). Outside the reference domain σ passes through with the flux ; NaN samples get a NaN σ ; a science spectrum without uncertainty stays without one. No covariance between science and reference is assumed (a response derived from the same night's standard is treated as independent).\n\nParameters — pass as the 'params' object:\n- operation (default 'div'): One of 'add', 'sub', 'mul', 'div'.\n- min_denominator (default 5e-05): Division samples with |reference| below this become NaN. Ignored for non-div operations.\n- reference_path (default None): Path / URL of a FITS reference spectrum (loaded with read_fits).\n- reference_key (default 'reference_spectrum'): ctx.extras key holding a Spectrum1D reference.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — IRAF sarith heritage.; Astropy Collaboration (Robitaille et al.) 2013, A&A 558, A33 — the astropy units layer whose arithmetic conventions this brick follows ; specutils arithmetic (documentation) is the reference implementation of spectrum ± × ÷ spectrum.; Bevington & Robinson 2003, Data Reduction and Error Analysis for the Physical Sciences, 3rd ed., McGraw-Hill — §3.2, propagation of errors through sums, products and quotients.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "transform",
      "category_title": "Transforms",
      "summary": "Add, subtract, multiply or divide ``ctx.spectrum`` by a reference.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/transform/combine_spectra_arithmetic/"
    },
    {
      "name": "compare_line_fits",
      "description": "Fit one spectral line with every profile in turn and pick the best one.\n\nUseful when you do not know whether a line is pressure-broadened (Lorentzian wings dominate), thermally / instrumentally broadened (Gaussian is enough), or genuinely mixed (Voigt). The wrapper picks no winner itself — it leaves all three fits on the context for you to compare.\n\nParameters — pass as the 'params' object:\n- line_center_angstrom (default None) [required]: Approximate line centre in Å (required).\n- window_angstrom (default 20.0): Half-width of the fit window on each side (Å).\n- profiles (default ['fit_gaussian_line', 'fit_lorentzian_line', 'fit_voigt_line']): Names of fit_*_line algorithms to run.\n- label_prefix (default ''): Prefix for the keys written to ctx.line_fits (defaults to '').\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Catalogue fit_*_line algorithms; this wrapper is composition only.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "line_fitting",
      "category_title": "Line fitting",
      "summary": "Fit one spectral line with every profile in turn and pick the best one.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/line_fitting/compare_line_fits/"
    },
    {
      "name": "compare_normalisations",
      "description": "Run every continuum-normalisation method on ``ctx.spectrum`` and collect them.\n\nThe Spectrum1D ``ctx.spectrum`` you pass in is *not* modified — only ``ctx.extras['normalisations']`` is populated. To then apply one of the results, copy it back into ``ctx.spectrum`` yourself. v2.0.0: masked samples (Spectrum1D.mask) are ignored by the pairwise RMS ; each method receives the mask and ignores those samples itself.\n\nParameters — pass as the 'params' object:\n- methods (default ['normalize_polynomial', 'normalize_percentile', 'normalize_max', 'normalize_edges']): Names of normalisation algorithms to run (defaults to the 4 native ones).\n- per_method_params (default {}): Optional dict {method_name: {param: value}} for non-default params.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Catalogue normalise_* algorithms; this wrapper is composition only.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "continuum",
      "category_title": "Continuum",
      "summary": "Run every continuum-normalisation method on ``ctx.spectrum`` and collect them.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/continuum/compare_normalisations/"
    },
    {
      "name": "compare_smoothings",
      "description": "Run every smoothing kernel on ``ctx.spectrum`` and collect the results.\n\nPairwise residual RMS between any two smoothings is recorded in ``ctx.metrics`` so the caller can see how much the kernel choice actually matters on this particular spectrum.\n\nParameters — pass as the 'params' object:\n- methods (default ['smooth_savgol', 'smooth_gaussian']): Names of smoothing algorithms to run (defaults to the 2 native ones).\n- per_method_params (default {}): Optional dict {method_name: {param: value}} for overrides.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Catalogue smooth_* algorithms; this wrapper is composition only.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "smoothing",
      "category_title": "Smoothing",
      "summary": "Run every smoothing kernel on ``ctx.spectrum`` and collect the results.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/smoothing/compare_smoothings/"
    },
    {
      "name": "compare_snr_methods",
      "description": "Run every SNR estimator on ``ctx.spectrum`` and collect their numbers.\n\nEach method writes to ``ctx.metrics``; this wrapper aggregates them into ``ctx.extras['snr_methods']`` so a downstream caller (or an MCP agent) can compare at a glance. The wrapper itself does not mutate ``ctx.spectrum`` ; each method runs on a slim context holding a copy of the spectrum only.\n\nParameters — pass as the 'params' object:\n- methods (default ['snr_der', 'snr_edge', 'snr_linear_fit']): Names of SNR algorithms to run (defaults to the 3 native ones).\n- per_method_params (default {}): Optional dict {method_name: {param: value}} for overrides.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Catalogue snr_* algorithms; this wrapper is composition only.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "quality",
      "category_title": "Quality / SNR",
      "summary": "Run every SNR estimator on ``ctx.spectrum`` and collect their numbers.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/quality/compare_snr_methods/"
    },
    {
      "name": "correct_slant_affine",
      "description": "Make monochromatic lines parallel to the rows by a horizontal shear.\n\nscipy.ndimage.affine_transform with shear [[1, 0], [-tan θ, 1]] and offset [0, tan θ · pivot_row], order=1 bilinear. Zero slant is a no-op.\n\nParameters — pass as the 'params' object:\n- slant_deg (default 0.0): Slant angle of monochromatic lines (degrees).\n- pivot_row (default 0): Row where the shear has zero displacement (use the trace row).\n\nRequires in the session context: image\n\nBackend: scipy.\nReferences: Howell 2006, Handbook of CCD Astronomy 2nd ed. §5.2 (Cambridge UP).; Tody 1986, Proc. SPIE 627, 733 — IRAF transform heritage.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Make monochromatic lines parallel to the rows by a horizontal shear.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/correct_slant_affine/"
    },
    {
      "name": "correct_smile_polynomial",
      "description": "Undo smile curvature via the Schroeder even-order expansion.\n\nComputes the column-displacement field from the paraxial Seidel expansion, then resamples with scipy.ndimage.map_coordinates (order=1, zero-fill outside). Reference row is the optical axis (usually the science trace) — no displacement there.\n\nParameters — pass as the 'params' object:\n- reference_row (default 0) [required]: Optical-axis row y₀ (no lateral shift there); use the trace row.\n- smile_radius (default 0.0) [required]: Smile radius R in pixels (instrument-specific). 0 disables.\n- polynomial_order (default 6): Highest even power kept: one of 2, 4, 6.\n\nRequires in the session context: image\n\nBackend: scipy.\nReferences: Schroeder 2000, Astronomical Optics 2nd ed. ch.15 §15.3 — off-axis aberration expansion.; Bottema 1980, Appl. Opt. 19, 444 — smile curvature in concave-grating spectrographs.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Undo smile curvature via the Schroeder even-order expansion.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/correct_smile_polynomial/"
    },
    {
      "name": "correct_tilt_affine",
      "description": "Re-align the slit with the detector columns by a vertical shear.\n\nscipy.ndimage.affine_transform with shear matrix [[1, -tan θ], [0, 1]], order=1 bilinear, zero-fill outside. The offset re-centres the shear on the image so the centre column remains in place. Pair with correct_slant_affine and correct_smile_polynomial in a long-slit preset.\n\nParameters — pass as the 'params' object:\n- tilt_deg (default 0.0): Tilt angle of the slit vs. detector columns (degrees).\n\nRequires in the session context: image\n\nBackend: scipy.\nReferences: Howell 2006, Handbook of CCD Astronomy 2nd ed. §5.2 (Cambridge UP).; Tody 1986, Proc. SPIE 627, 733 — IRAF transform heritage.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Re-align the slit with the detector columns by a vertical shear.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/correct_tilt_affine/"
    },
    {
      "name": "cosmic_ray_remove_easyspec",
      "description": "Cosmic-ray (and optional gain) correction via ``CR_and_gain_corrections``.\n\nParameters — pass as the 'params' object:\n- target_path (default None): Science FITS; falls back to ctx.image.\n- gain (default None): Detector gain (e-/ADU); null reads it from the FITS header.\n- gain_header_entry (default 'GAIN'): Header keyword for the gain when 'gain' is null.\n- readnoise (default None): Read noise (electrons); null reads it from the FITS header.\n- readnoise_header_entry (default 'RDNOISE'): Header keyword for the read noise when 'readnoise' is null.\n- sigclip (default 5.0): Sigma threshold for cosmic-ray detection (easyspec default 5).\n\nBackend: easyspec.\nReferences: easyspec.cleaning.cleaning.CR_and_gain_corrections.; van Dokkum 2001, PASP 113, 1420 — L.A.Cosmic.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "cosmic_ray",
      "category_title": "Cosmic-ray rejection",
      "summary": "Cosmic-ray (and optional gain) correction via ``CR_and_gain_corrections``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/cosmic_ray/cosmic_ray_remove_easyspec/"
    },
    {
      "name": "cross_correlate_rv",
      "description": "Measure radial velocity by cross-correlation against a template spectrum.\n\nTemplate comes from `template_path` (FITS), `template_key` (a Spectrum1D under ctx.extras), `ctx.extras['template_spectrum']` or `ctx.spectra[0]` — first match wins. By default the brick subtracts a running-median continuum from both observed and template (continuum_subtract=True) — the Tonry-Davis CCF only behaves correctly on continuum-normalised inputs ; with raw flux the SED slope dominates the correlation and biases the RV toward zero. Disable with continuum_subtract=False when the caller has already normalised the spectrum. The RV error is the Tonry-Davis 1979 formula ; it underestimates the true error for asymmetric CCFs (multi-component blended templates) — for a physics-grounded floor see rv_precision_bouchy (Bouchy, Pepe & Queloz 2001). v3.0.0: n_grid defaults to None — the log-λ grid follows the finest native Δlnλ of the two spectra over the overlap (capped at 2^18 samples). The former default of 4096 samples (41 km/s per sample on 4000-7000 Å) under-sampled finely sampled spectra and the parabolic refinement did not recover the loss : on a 30 000-px synthetic (σ_line 0.15 Å, S/N 100) an injected +10 km/s came back as +0.9 km/s with n_grid=4096 and as +9.97 km/s on the native grid ; pass n_grid=4096 explicitly to reproduce the pre-3.0.0 numbers. σ_v is a lower bound at coarse grids because it ignores the interpolation error. Masked samples (Spectrum1D.mask) of either spectrum are skipped like NaN when resampling (v3.0.0). Descending wavelength axes are sorted before resampling. DEPRECATED: the ctx.spectra[0] template fallback violates the 'second spectrum in ctx.extras' convention and will be removed in a future major release — it is flagged in the message, in extras['template_source'] and via a DeprecationWarning.\n\nParameters — pass as the 'params' object:\n- template_path (default None): Path / URL of a template FITS spectrum to load.\n- template_key (default None): Key in ctx.extras where a Spectrum1D template is stored.\n- n_grid (default None): Number of log-wavelength samples on which to interpolate. null/None (default since v3.0.0) derives it from the finest native Δlnλ of the two spectra inside the overlap (capped at 2^18) ; 4096 reproduces the pre-3.0.0 grid (41 km/s per sample on 4000-7000 Å, under-sampled for most spectra).\n- vmin_kms (default -800.0): Lower bound of the velocity search range (km/s).\n- vmax_kms (default 800.0): Upper bound of the velocity search range (km/s).\n- continuum_subtract (default True): If True (default), subtract a running-median continuum from both observed and template before correlation — the Tonry-Davis 1979 prerequisite. Disable only when the spectra are already continuum-normalised.\n- continuum_window (default 101): Running-median window (samples on the log-λ grid) for the continuum estimator. Must be odd ≥ 3; the brick rounds up if even. Ignored when continuum_subtract is False.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Tonry & Davis 1979, AJ 84, 1511 — fundamental CCF method for RV; r-value, σ_v = 3·w/(8·(1+r)) error formula (§III), and the requirement of continuum-normalised inputs (§II).; Bouchy, Pepe & Queloz 2001, A&A 374, 733 — photon-limited RV precision (see also: rv_precision_bouchy).; scipy.signal.correlate — cross-correlation engine.; scipy.ndimage.median_filter — running-median continuum.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "radial_velocity",
      "category_title": "Radial velocity",
      "summary": "Measure radial velocity by cross-correlation against a template spectrum.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/radial_velocity/cross_correlate_rv/"
    },
    {
      "name": "dark_combine",
      "description": "Combine a stack of dark frames into a master dark.\n\nPixel-wise median (robust default) or σ-clipped mean over the stack. require_uniform_exptime=True (the default) ensures every input has the same EXPTIME — otherwise a non-uniform mix would render the master un-scalable by exposure time. Pass bias_key to subtract a master bias from each frame before combining. v2.0.0 (method='mean' with sigma_clip only): the σ-clip scale is 1.4826·MAD about the per-pixel median; when the MAD is 0 (integer-ADU darks with ~1 ADU read noise) it now falls back to 1.2533 × the mean absolute deviation about the median, and keeps all values when that is 0 too. v1 floored the scale at machine epsilon, which rejected every value ≠ median and turned the clipped mean into a median ([10,10,10,11,12], σ=3: v1 → 10.0, v2 → 10.6; a cosmic ray [10,10,10,11,1000] is still rejected → 10.25). method='median' is unchanged.\n\nParameters — pass as the 'params' object:\n- method (default 'median'): 'median' (robust default) or 'mean' (with optional sigma_clip).\n- bias_key (default None): ctx.extras key for a master bias to subtract from each frame before combining. None = no pre-subtraction.\n- require_uniform_exptime (default True): When True, refuse to combine frames with non-uniform EXPTIME (otherwise a scale_by_exptime downstream would be undefined).\n- sigma_clip (default None): σ threshold for σ-clipped mean (method='mean' only). None or ≤0 disables clipping.\n\nRequires in the session context: images\n\nBackend: numpy.\nReferences: Howell 2006 — Handbook of CCD Astronomy, ch. 4 (CCD reduction).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "master_creation",
      "category_title": "Master frames",
      "summary": "Combine a stack of dark frames into a master dark.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/master_creation/dark_combine/"
    },
    {
      "name": "dark_combine_easyspec",
      "description": "Combine dark frames into a master dark via ``easyspec.cleaning.master``.\n\nParameters — pass as the 'params' object:\n- dark_dir (default None): Directory containing the dark FITS files.\n- dark_paths (default None): List of dark FITS file paths.\n- method (default 'median'): Stacking method: median, mean, or mode.\n- header_hdu_entry (default 0): HDU extension where the FITS header lives.\n\nBackend: easyspec.\nReferences: easyspec — Lobão et al. (https://pypi.org/project/easyspec/).; easyspec.cleaning.cleaning.master.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "master_creation",
      "category_title": "Master frames",
      "summary": "Combine dark frames into a master dark via ``easyspec.cleaning.master``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/master_creation/dark_combine_easyspec/"
    },
    {
      "name": "dark_subtract",
      "description": "Subtract a master dark from ``ctx.image``.\n\nReads the master dark from ``ctx.extras['master_dark']`` (an ImageFrame) or from ``dark_key`` if you stored it under a different key.\n\nParameters — pass as the 'params' object:\n- dark_key (default 'master_dark'): Key in ctx.extras where the master dark is stored.\n- scale_by_exptime (default True): Scale the dark by EXPTIME(science)/EXPTIME(dark) before subtracting.\n\nRequires in the session context: image\n\nBackend: numpy.\nReferences: Howell 2006 — Handbook of CCD Astronomy, ch. 4.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Subtract a master dark from ``ctx.image``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/dark_subtract/"
    },
    {
      "name": "denoise_gaussian_2d",
      "description": "Separable isotropic Gaussian smoothing of ``ctx.image``.\n\nscipy.ndimage.gaussian_filter(sigma), default mode 'reflect'. Isotropic — the same σ is applied to rows and columns. For anisotropic smoothing, pass a tuple via the underlying scipy API directly.\n\nParameters — pass as the 'params' object:\n- sigma (default 0.0): Gaussian σ in pixels (isotropic). 0 = no-op.\n\nRequires in the session context: image\n\nBackend: scipy.\nReferences: Lindeberg 1994, Scale-Space Theory in Computer Vision (Kluwer).; scipy.ndimage.gaussian_filter — separable Gaussian implementation.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Separable isotropic Gaussian smoothing of ``ctx.image``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/denoise_gaussian_2d/"
    },
    {
      "name": "denoise_median_2d",
      "description": "Square median filter over ``ctx.image``, optionally banded by rows.\n\nscipy.ndimage.median_filter(size=kernel_size) over rows [row_lo, row_hi). Outside the band the original pixels are kept.\n\nParameters — pass as the 'params' object:\n- kernel_size (default 3): Square kernel size, odd integer ≥ 3.\n- row_lo (default 0): First row (inclusive); 0 ⇒ top of image.\n- row_hi (default 0): Last row (exclusive); 0 ⇒ bottom of image.\n\nRequires in the session context: image\n\nBackend: scipy.\nReferences: Tukey 1977, Exploratory Data Analysis (Addison-Wesley) — rank-order filtering for impulse noise.; scipy.ndimage.median_filter — multi-dim median implementation.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Square median filter over ``ctx.image``, optionally banded by rows.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/denoise_median_2d/"
    },
    {
      "name": "deredden_interstellar",
      "description": "Deredden a spectrum for interstellar dust : F_0 = F / 10^(−0.4 A(λ)), CCM89 / F99 / G23.\n\nF_0(λ) = F(λ) / extinguish(λ) with extinguish(λ) = 10^(−0.4 · E(B−V) · R_V · A(λ)/A_V) from dust_extinction.parameter_averages.<law>(Rv=rv).extinguish(λ, Ebv=ebv). Validity (fails outside, no extrapolation) : CCM89 and F99 1000–33 333 Å with R_V ∈ [2.0, 6.0] ; G23 912 Å – 32 μm with R_V ∈ [2.3, 5.6]. ebv ≥ 0 (ebv = 0 is the identity). Wavelengths must be in Ångström. Uncertainty : σ_0 = σ / extinguish(λ) — E(B−V) and R_V are taken as exact. Acceptance : 5500 Å, CCM89, R_V = 3.1, E(B−V) = 0.1 ⇒ factor 1.3300 (A_V = 0.31, A(5500)/A_V = 0.99885). Requires the 'reduction' extra (pip install 'spectro-kernel[reduction]').\n\nParameters — pass as the 'params' object:\n- ebv (default None) [required]: Colour excess E(B−V) in magnitudes (≥ 0).\n- rv (default 3.1): Total-to-selective extinction ratio R_V = A_V / E(B−V) (3.1 diffuse ISM).\n- law (default 'CCM89'): Extinction law : 'CCM89' (default), 'F99' or 'G23'.\n\nRequires in the session context: spectrum\n\nBackend: dust_extinction.\nReferences: Cardelli, Clayton & Mathis 1989, ApJ 345, 245 — CCM89 R_V-dependent extinction law (default).; Fitzpatrick 1999, PASP 111, 63 — F99 law.; Gordon et al. 2023, ApJ 950, 86 — G23 law (912 Å – 32 μm).; dust_extinction (astropy affiliated), https://dust-extinction.readthedocs.io — the package that evaluates the laws (wrapped, not re-implemented).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Deredden a spectrum for interstellar dust : F_0 = F / 10^(−0.4 A(λ)), CCM89 / F99 / G23.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/deredden_interstellar/"
    },
    {
      "name": "detect_lines",
      "description": "Detect emission/absorption peaks, optionally matched to a named catalogue.\n\nBlind mode (catalog=None) returns continuum, signed amplitude, SNR (|amp| / noise), FWHM (from scipy.signal.peak_widths at half-maximum), and the MAD-robust noise level used for the threshold. Catalogue mode preserves the v1.1.0 behaviour: each detection is labelled with the nearest catalogue line within tolerance_angstrom. v2.0.0: masked samples (Spectrum1D.mask) are ignored — they are removed, together with non-finite ones, before the continuum, noise and peak search in both modes.\n\nParameters — pass as the 'params' object:\n- catalog (default 'balmer'): Reference catalogue: 'balmer', 'telluric', 'nebular', 'aurorae', or None for blind mode (no identification).\n- kind (default 'both'): Which features to look for: emission, absorption or both.\n- prominence_sigma (default 5.0): Peak prominence threshold, in units of the noise level.\n- tolerance_angstrom (default 5.0): Maximum detection-to-catalogue separation for a match (Å).\n- continuum_window (default 101): Median-filter window (samples, odd ≥ 3) used to estimate the continuum before MAD noise computation in blind mode.\n- min_separation_angstrom (default 2.0): Minimum spacing between detections (Å). Forwarded to find_peaks(distance=…) in blind mode.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: scipy.signal.find_peaks — prominence-thresholded peak detection.; Press, Teukolsky, Vetterling & Flannery 2007, Numerical Recipes, 3rd ed., Cambridge UP — §10.2, parabolic interpolation through three points (vertex refinement of the peak position).; Hampel 1974, J. Am. Stat. Assoc. 69, 383 — Median Absolute Deviation (blind-mode noise level: 1.4826 · MAD).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "line_detection",
      "category_title": "Line detection",
      "summary": "Detect emission/absorption peaks, optionally matched to a named catalogue.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/line_detection/detect_lines/"
    },
    {
      "name": "detect_trace",
      "description": "Detect the spectral trace on ``ctx.image`` and expose it on ``ctx.extras[\"trace\"]``.\n\nSibling of the trace step inside extract_spectrum_boxcar / extract_spectrum_optimal — but standalone, pure-numpy, and exposing the result on ctx.extras so the geometry and sky bricks (subtract_sky_2d, correct_smile_polynomial, correct_slant_affine) can resolve their hand-tuned row parameters from a measured value rather than a config file. Required at the head of an 'automatic' reduction preset; the manual mode that pre-fills row parameters from a config remains valid.\n\nParameters — pass as the 'params' object:\n- method (default 'argmax'): Per-slice centroid method: 'argmax' (integer pixel) or 'centroid' (sub-pixel).\n- poly_order (default 2): Polynomial order of the fitted trace y(x).\n- search_half_width (default 40): Half-window (rows) around the global peak in which each slice searches for its local maximum.\n- n_slices (default 20): Number of column slices sampled across the dispersion axis.\n- min_snr (default 5.0): Minimum trace SNR (peak above background / off-trace std). The algorithm fails rather than silently writing a bad trace.\n- output_key (default 'trace'): ctx.extras key that receives the trace dict.\n\nRequires in the session context: image\n\nBackend: scipy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — IRAF apall / aptrace heritage.; scipy.signal.find_peaks — global peak of the collapsed spatial profile.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "extraction",
      "category_title": "Extraction (2-D to 1-D)",
      "summary": "Detect the spectral trace on ``ctx.image`` and expose it on ``ctx.extras[\"trace\"]``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/extraction/detect_trace/"
    },
    {
      "name": "disentangle_sb2",
      "description": "Separate the spectra of the two components of an SB2 spectroscopic binary.\n\nImplements the iterative alternating subtraction of Bagnuolo & Gies 1991 (not the SVD of Simon & Sturm 1994 nor the Fourier method of Hadrava 1995, cited as background). All observations are resampled onto a common log-wavelength grid so that a Doppler shift is a constant number of samples ; at each iteration the current estimate of one component is removed from every observation in the other component's rest frame, and the new estimate is the average of the residuals. Stores ``primary_spectrum`` and ``secondary_spectrum`` in ``ctx.extras``. CAVEAT — additive continuum degeneracy: only the SUM of the two continua is constrained by the data, so the split of the continuum level between the components is arbitrary (the secondary typically comes out near a continuum of ~0 and the primary near the total) ; the line profiles are correct but a light-ratio prior is needed to renormalise each component to its own continuum. ``metrics['disentangle_residual_rms']`` (v1.1.0) is the RMS of observation − (shifted primary + shifted secondary) after the last iteration, over the finite samples. Descending wavelength axes and non-finite samples are handled before resampling.\n\nParameters — pass as the 'params' object:\n- v1_kms (default None) [required]: List of per-spectrum primary velocities (km/s, length = len(ctx.spectra)).\n- v2_kms (default None) [required]: List of per-spectrum secondary velocities (km/s, same length).\n- n_iter (default 30): Number of iterations of the alternating subtraction.\n- n_grid (default 4096): Number of log-wavelength samples on the common grid.\n\nRequires in the session context: spectra\n\nBackend: numpy.\nReferences: Bagnuolo & Gies 1991, ApJ 376, 266 — iterative alternating-subtraction ('tomographic') separation of composite spectra: the method implemented here.; Simon & Sturm 1994, A&A 281, 286 — wavelength-domain spectral separation by singular-value decomposition (background).; Hadrava 1995, A&AS 114, 393 — Fourier-domain disentangling (background).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "advanced",
      "category_title": "Advanced",
      "summary": "Separate the spectra of the two components of an SB2 spectroscopic binary.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/advanced/disentangle_sb2/"
    },
    {
      "name": "doppler_shift",
      "description": "Doppler-shift the wavelength axis by a radial velocity.\n\nParameters — pass as the 'params' object:\n- velocity_kms (default None) [required]: Radial velocity in km/s (positive = redshift).\n\nRequires in the session context: spectrum\n\nBackend: astropy.\nReferences: Classical (non-relativistic) Doppler relation: lambda' = lambda * (1 + v/c); Speed of light from astropy.constants",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Doppler-shift the wavelength axis by a radial velocity.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/doppler_shift/"
    },
    {
      "name": "doppler_tomogram",
      "description": "Doppler tomogram of a binary from N phase-resolved spectra.\n\nBack-projection of N phase-resolved spectra onto the (V_x, V_y) plane. Pixels are averaged over the phases that contribute (NaN-aware mean) so the phase-coverage footprint does not bleed into the brightness map. JD_mid is read from each spectrum's meta['jd_mid'], falling back to meta['dateobs'] + meta['exptime_s'] via astropy.time.Time. Descending wavelength axes are sorted before interpolation. v2.0.0: the projection now follows Marsh & Horne 1988 Eq. 1, v = γ − Vx·cos(2πφ) + Vy·sin(2πφ) ; v1.x used γ − Vx·sin(2πφ) + Vy·cos(2πφ), so a spot with RV curve γ + K·sin(2πφ) (the secondary) landed at (−K, 0) instead of (0, +K). Maps produced by v1.x are rotated by −90° (Vx, Vy) → (Vy, −Vx) relative to v2 / the literature convention.\n\nParameters — pass as the 'params' object:\n- period_days (default None) [required]: Orbital period of the binary (days).\n- epoch_hjd (default None) [required]: Reference epoch HJD of phase 0 (days).\n- gamma_kms (default 0.0): Systemic velocity (km/s) added to the back-projection.\n- line_center_aa (default 6562.82): Rest wavelength (Å) of the line to tomogram.\n- velocity_window_kms (default 1000.0): Half-extent (km/s) of the V_x/V_y axes.\n- n_velocity (default 121): Number of velocity samples per axis (odd, 11–401).\n\nBackend: numpy.\nReferences: Marsh & Horne 1988, MNRAS 235, 269 — Doppler tomography by back-projection (§2).; Horne 1985, MNRAS 213, 129 — MEM tomographic reconstruction (not implemented; cited for completeness).; Marsh 2001, Lecture Notes in Physics 573 — astrotomography review.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "advanced",
      "category_title": "Advanced",
      "summary": "Doppler tomogram of a binary from N phase-resolved spectra.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/advanced/doppler_tomogram/"
    },
    {
      "name": "embed_band_power",
      "description": "Embed a spectrum as the integrated flux in N adjacent wavelength bands.\n\nEquivalent to converting a spectrum into a 'virtual photometric catalogue' with N synthetic broad bands. Very cheap and well-suited to indexing very heterogeneous datasets (different dispersions, resolutions, telescopes). Non-finite (NaN/inf) samples are dropped before integration — the trapezoidal band integral uses the actual wavelengths, so this is sampling-independent — and a descending wavelength axis is sorted first (band edges are taken from the finite samples). v3.0.0: masked samples (Spectrum1D.mask) are ignored — dropped like non-finite ones before integration.\n\nParameters — pass as the 'params' object:\n- n_bands (default 16): Number of bands; also the output dim. Typically 8–32.\n- spacing (default 'log'): Band edges in wavelength: 'linear' or 'log'.\n- log_flux (default True): Take log10(integrated_flux + epsilon) before L2 norm.\n- epsilon (default 1e-12): Small floor added before log_flux to keep zero bands finite.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Connolly et al. 1995, AJ, 110, 1071 — photometric SED classification.; Bolzonella et al. 2000, A&A, 363, 476 — band-power feature vectors for spectro-photometric classification.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "embedding",
      "category_title": "Embeddings",
      "summary": "Embed a spectrum as the integrated flux in N adjacent wavelength bands.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/embedding/embed_band_power/"
    },
    {
      "name": "embed_continuum_subtracted",
      "description": "Subtract the polynomial continuum, then embed the line residual.\n\nUses the symmetric sigma-clipped polynomial continuum helper from ``algorithms._common``; the residual (flux - continuum) is then passed to one of the standard embedding recipes. Set ``norm_method='none'`` to skip a second normalisation step — the residual already has zero mean. Non-finite samples (NaN/inf) make the brick fail cleanly: the DCT/resampling recipes act on the sample index, so bad pixels cannot be dropped — replace them first (clip_sigma / clip_cosmic_rays interpolate over outliers). v2.0.0: masked samples (Spectrum1D.mask) are refused the same way.\n\nParameters — pass as the 'params' object:\n- dim (default 256): Output vector length (positive integer).\n- strategy (default 'dct'): Recipe applied to the residual: one of ('naive', 'dct', 'multiscale_dct').\n- continuum_order (default 3): Polynomial degree for the continuum fit (2-5 typical).\n- sigma_clip (default 3.0): Sigma threshold for the continuum-fit clip (default 3.0).\n- norm_method (default 'none'): Pre-embedding flux normalisation: one of ('none', 'min_max', 'z_score', 'continuum'). Defaults to 'none' because the residual is already zero-centred.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Sousa et al. 2007, A&A, 469, 783 — continuum-normalised spectra for stellar parameter retrieval.; Worthey et al. 1994, ApJS, 94, 687 — line-strength indices on continuum-flattened spectra.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "embedding",
      "category_title": "Embeddings",
      "summary": "Subtract the polynomial continuum, then embed the line residual.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/embedding/embed_continuum_subtracted/"
    },
    {
      "name": "embed_lick_indices",
      "description": "Embed a spectrum as the canonical Lick/IDS line-strength indices.\n\nEach index is the equivalent width (or magnitude, for molecular indices) of a feature passband measured against a pseudo-continuum interpolated between two sidebands. The fixed-order vector and the human-readable index names (in extras.embedding_provenance.names) make this the most interpretable embedding the kernel exposes. Non-finite (NaN/inf) samples are dropped before the passband integrals (trapezoidal on the actual wavelengths, hence sampling-independent) and a descending wavelength axis is sorted first. v3.0.0: masked samples (Spectrum1D.mask) are ignored — dropped like non-finite ones before the passband integrals.\n\nParameters — pass as the 'params' object:\n- l2_normalise (default False): L2-normalise the output vector. False by default to preserve the physical units (Å / magnitudes). Set True for similarity search.\n- nan_fill (default 0.0): Value to substitute for indices that cannot be measured (passband outside the spectrum's wavelength range).\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Worthey et al. 1994, ApJS, 94, 687 — definition of the Lick/IDS index system.; Worthey & Ottaviani 1997, ApJS, 111, 377 — revised passband definitions and atmospheric corrections.; Trager et al. 1998, ApJS, 116, 1 — extended index list for old stellar populations.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "embedding",
      "category_title": "Embeddings",
      "summary": "Embed a spectrum as the canonical Lick/IDS line-strength indices.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/embedding/embed_lick_indices/"
    },
    {
      "name": "embed_log_lambda",
      "description": "Resample to a uniform log-λ grid, then embed.\n\nThe grid spacing is set so the output length is exactly ``dim``. Wavelengths must be strictly positive (the log of a non-positive value is undefined); the algorithm fails fast with a clear error otherwise. Non-finite (NaN/inf) samples are dropped before the resampling — the log-λ interpolation is sampling-independent, so this is safe — and a descending wavelength axis is sorted first. v2.0.0: masked samples (Spectrum1D.mask) are ignored — dropped like non-finite ones before the resampling (which bridges the gap linearly).\n\nParameters — pass as the 'params' object:\n- dim (default 256): Output vector length AND number of log-λ samples (positive integer).\n- strategy (default 'naive'): Recipe applied to the resampled flux: one of ('naive', 'dct', 'multiscale_dct').\n- norm_method (default 'min_max'): Pre-embedding flux normalisation: one of ('none', 'min_max', 'z_score', 'continuum').\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Tonry & Davis 1979, AJ, 84, 1511 — radial velocities from cross-correlation in log-λ space.; Baldry et al. 1999, ApJ, 521, 167 — Doppler invariance of log-wavelength representations.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "embedding",
      "category_title": "Embeddings",
      "summary": "Resample to a uniform log-λ grid, then embed.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/embedding/embed_log_lambda/"
    },
    {
      "name": "embed_pretrained",
      "description": "Embed a spectrum with a local pre-trained PyTorch model.\n\nOn the first call for a given model_path the file's SHA-256 is computed and recorded in the provenance dict; subsequent calls reuse the loaded model object (cached on the algorithm instance). Non-finite samples (NaN/inf) make the brick fail cleanly — replace bad pixels before inference (clip_sigma / clip_cosmic_rays interpolate over outliers). v2.0.0: masked samples (Spectrum1D.mask) are refused the same way.\n\nParameters — pass as the 'params' object:\n- model_path (default None) [required]: Filesystem path to the PyTorch model file (.pt / .pth) (REQUIRED). The user is responsible for downloading or training the model; the kernel only loads what's already on disk.\n- model_id (default 'user_model'): Human-readable identifier captured in the provenance dict alongside the SHA-256 hash; lets the similarity layer track which model version produced which vector.\n- input_length (default 1024): Number of pixels the model expects. The spectrum is resampled to this length before being fed to the model.\n- device (default 'cpu'): Torch device ('cpu' or 'cuda:0' if a GPU is present).\n\nRequires in the session context: spectrum\n\nBackend: pytorch.\nReferences: Bishop 2006, 'Pattern Recognition and Machine Learning' — embedding via learned representations.; Naul et al. 2018, Nat. Astron. 2, 151 — RNN autoencoders for irregular astronomical time series (analogous architecture).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "embedding",
      "category_title": "Embeddings",
      "summary": "Embed a spectrum with a local pre-trained PyTorch model.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/embedding/embed_pretrained/"
    },
    {
      "name": "embed_remote",
      "description": "Embed a spectrum via a remote HTTPS inference endpoint.\n\nThe endpoint contract is documented in the module docstring. Any service that respects it (including a self-hosted spectro-kernel MCP cloud running embed_pretrained server-side) can be plugged in. The user is responsible for the endpoint URL and (optionally) an API key; the kernel doesn't ship any defaults. v2.0.0: masked (Spectrum1D.mask) and non-finite samples make the brick fail before any request is sent, like the local recipes — replace bad pixels first (clip_sigma / clip_cosmic_rays interpolate over them).\n\nParameters — pass as the 'params' object:\n- endpoint (default None) [required]: Full HTTPS URL of the inference endpoint (REQUIRED).\n- model (default 'default'): Model identifier passed in the request body.\n- api_key (default None): Bearer token for the endpoint. Falls back to the SPECTRO_EMBED_API_KEY environment variable when null (on a shared server only for hosts listed in SPECTRO_EMBED_ENDPOINT_ALLOW).\n- dim (default 256): Requested output dimension (the server may ignore this hint).\n- timeout_s (default 30.0): HTTP request timeout in seconds (default 30).\n\nRequires in the session context: spectrum\n\nBackend: httpx.\nReferences: MCP-style stateless inference endpoints — see e.g. HuggingFace Inference API, OpenAI embedding endpoints, Replicate.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "embedding",
      "category_title": "Embeddings",
      "summary": "Embed a spectrum via a remote HTTPS inference endpoint.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/embedding/embed_remote/"
    },
    {
      "name": "embed_spectrum",
      "description": "Compute a fixed-length, L2-normalised embedding of a spectrum.\n\nThe actual maths lives in ``spectro_kernel.embeddings`` so a downstream service can batch-embed thousands of spectra without paying the per-call audit-trail overhead — but go through this algorithm if you want the ProcessingStep + content-hash record alongside the vector. Older strategy names ('pca', 'autoencoder') still work but emit a DeprecationWarning. Non-finite samples (NaN/inf) make the brick fail cleanly: the recipes act on the sample index, so bad pixels cannot be dropped — replace them first (clip_sigma / clip_cosmic_rays interpolate over outliers). v2.0.0: masked samples (Spectrum1D.mask) are refused the same way, since ignoring them is impossible here.\n\nParameters — pass as the 'params' object:\n- dim (default 256): Output vector length (positive integer).\n- strategy (default 'naive'): Embedding recipe: one of ('naive', 'dct', 'multiscale_dct'). Older names 'pca' and 'autoencoder' are accepted as aliases with a DeprecationWarning.\n- norm_method (default 'min_max'): Pre-embedding flux normalisation: one of ('none', 'min_max', 'z_score', 'continuum'). ``none`` skips normalisation (useful when the spectrum is already normalised by an upstream pipeline step).\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Ahmed, Natarajan & Rao 1974, 'Discrete Cosine Transform', IEEE Trans. Computers, C-23, 90.; Bu et al. 2014, ApJS, 211, 28 — z-score standardisation for spectra.; Sharma et al. 2020, MNRAS, 491, 2280 — min-max rescaling of stellar spectra.; Sanchez-Saez et al. 2021, AJ, 162, 206 — spectral representations for anomaly detection.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "embedding",
      "category_title": "Embeddings",
      "summary": "Compute a fixed-length, L2-normalised embedding of a spectrum.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/embedding/embed_spectrum/"
    },
    {
      "name": "embed_wavelets",
      "description": "Embed a spectrum via a truncated discrete wavelet transform.\n\nReturns the first ``dim`` coefficients of the concatenated multi-level decomposition (approximation + level-L details + level-(L-1) details + … + level-1 details), zero-padded when the decomposition is shorter than dim, then L2-normalised. The ordering is stable: each output dimension always represents the same wavelet coefficient across all spectra, so cosine similarity is meaningful. Non-finite samples (NaN/inf) make the brick fail cleanly — the transform acts on the sample index, so bad pixels cannot be dropped ; replace them first (clip_sigma / clip_cosmic_rays interpolate over outliers). v3.0.0: masked samples (Spectrum1D.mask) are refused the same way.\n\nParameters — pass as the 'params' object:\n- dim (default 256): Output vector length (positive integer).\n- wavelet (default 'db4'): PyWavelets family name. Sensible choices: 'db1' (Haar - sharpest), 'db4' (default, smooth), 'sym8', 'coif5'.\n- level (default 5): Number of wavelet decomposition levels. Higher = coarser scales captured; bounded by log2(npix).\n\nRequires in the session context: spectrum\n\nBackend: pywavelets.\nReferences: Daubechies 1992, 'Ten Lectures on Wavelets', SIAM.; Mallat 2008, 'A Wavelet Tour of Signal Processing'.; Starck & Murtagh 2002, 'Astronomical Image and Data Analysis' — wavelet methods on astronomical signals.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "embedding",
      "category_title": "Embeddings",
      "summary": "Embed a spectrum via a truncated discrete wavelet transform.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/embedding/embed_wavelets/"
    },
    {
      "name": "equivalent_width",
      "description": "Measure a line's equivalent width without assuming a profile shape.\n\nEW = ∫(1 − F/Fc) dλ over [centre − window, centre + window] with Fc a straight line through the outer `continuum_fraction` of the window on each side (trapezoidal integration on the actual wavelengths). metrics['equivalent_width_angstrom'] is kept as the flat key (last call wins) ; v1.1.0 also writes metrics['equivalent_width_angstrom[<label>]'] with label defaulting to the centre rounded to 0.1 Å, like the fit_*_line bricks. When the spectrum carries an uncertainty array, metrics['equivalent_width_error_angstrom'] (and its per-label key) is the Vollmann & Eversberg 2006 Eq. 7 error with Δλ the actual window width, F̄ the mean flux over the window, F̄c the mean fitted continuum and S/N = mean(Fc) / RMS(uncertainty) over the edge bands. A descending wavelength axis is sorted before integration (v1.0.0 returned a sign-flipped, truncated value on such input). v2.0.0: masked samples (Spectrum1D.mask) are ignored — dropped, with non-finite ones, before the continuum fit and the trapezoidal integration (which then bridges the gap linearly) ; at least 6 usable samples must remain.\n\nParameters — pass as the 'params' object:\n- line_center_angstrom (default None) [required]: Line centre in Å (required).\n- window_angstrom (default 20.0): Half-width of the integration window on each side (Å).\n- continuum_fraction (default 0.3): Fraction of each window edge used to anchor the continuum.\n- label (default ''): Suffix for the per-label metric keys equivalent_width_angstrom[<label>]; defaults to the centre rounded to 0.1 Å.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Vollmann & Eversberg 2006, Astron. Nachr. 327, 862 — statistical error of an equivalent width, Eq. 7: σ(EW) = √(1 + F̄c/F̄)·(Δλ − EW)/(S/N).; Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 12, equivalent widths and continuum placement.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "line_fitting",
      "category_title": "Line fitting",
      "summary": "Measure a line's equivalent width without assuming a profile shape.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/line_fitting/equivalent_width/"
    },
    {
      "name": "export_csv",
      "description": "Export ``ctx.spectrum`` to a CSV file.\n\nParameters — pass as the 'params' object:\n- path (default None): Destination file path; null keeps the result in memory only.\n- delimiter (default ','): Column delimiter.\n\nRequires in the session context: spectrum\n\nBackend: numpy.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "export",
      "category_title": "Export",
      "summary": "Export ``ctx.spectrum`` to a CSV file.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/export/export_csv/"
    },
    {
      "name": "export_fits",
      "description": "Export ``ctx.spectrum`` to a FITS file.\n\nParameters — pass as the 'params' object:\n- path (default None): Destination file path; null keeps the result in memory only.\n\nRequires in the session context: spectrum\n\nBackend: astropy.\nReferences: astropy.io.fits; FITS Standard 4.0 — Pence et al. 2010, A&A 524, A42",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "export",
      "category_title": "Export",
      "summary": "Export ``ctx.spectrum`` to a FITS file.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/export/export_fits/"
    },
    {
      "name": "export_fits_bess",
      "description": "Export ``ctx.spectrum`` as a BeSS / ARAS-compliant FITS image.\n\nWrites the linear-grid Spectrum1D as a 1D image (BITPIX=-32) with linear WCS (CRVAL1, CDELT1, CRPIX1=1) and the BeSS observation keywords (OBJNAME, BSS_INST, BSS_SITE, OBSERVER, DATE-OBS, MJD-OBS, JD-OBS, EXPTIME, BSS_VHEL, BSS_ESRC). Pixels marked invalid (by the spectrum's mask) are replaced with the BeSS sentinel -32000 before write. The output bytes are always stored in ctx.exports['fits_bess']; a canonical filename (_<object>_<date>_<time>_<observer>.fits) is generated when path is null.\n\nParameters — pass as the 'params' object:\n- path (default None): Destination file path; null ⇒ canonical filename in a temp dir, bytes only.\n- object_name (default 'UNKNOWN'): Target identifier (BeSS OBJNAME).\n- instrument (default 'UNKNOWN'): Instrument identifier (BeSS BSS_INST, also INSTRUME).\n- site (default 'UNKNOWN'): Observing site identifier (BeSS BSS_SITE).\n- observer (default 'UNKNOWN'): Observer name or initials (BeSS OBSERVER).\n- date_obs_utc (default '') [required]: Start-of-exposure UTC timestamp (ISO-8601, e.g. '2026-06-09T22:13:45'). MJD-OBS and JD-OBS are computed from this.\n- exposure_seconds (default 0.0): Exposure time in seconds (BeSS EXPTIME).\n- vhelio_kms (default 0.0): Heliocentric velocity correction already applied (BeSS BSS_VHEL).\n- spectrum_source (default 'obs'): BeSS BSS_ESRC: 'obs' (raw observation), 'cor' (calibrated), 'pro' (processed).\n- telescope (default None): Optional telescope identifier (TELESCOP); null leaves it out.\n- n_combined (default 1): Number of co-added frames (BeSS BSS_NCMB).\n\nRequires in the session context: spectrum\n\nBackend: astropy.\nReferences: Teyssier 2015, A&A Pro-Am collaboration — BeSS/ARAS submission protocol.; Buil 2012, ARAS Observation Guide — BeSS FITS header convention.; FITS Standard 4.0 — Pence et al. 2010, A&A 524, A42.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "export",
      "category_title": "Export",
      "summary": "Export ``ctx.spectrum`` as a BeSS / ARAS-compliant FITS image.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/export/export_fits_bess/"
    },
    {
      "name": "export_hdf5",
      "description": "Export ``ctx.spectrum`` to an HDF5 file.\n\nParameters — pass as the 'params' object:\n- path (default None): Destination file path; null keeps the result in memory only.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: HDF5 — The HDF Group; h5py — https://www.h5py.org/",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "export",
      "category_title": "Export",
      "summary": "Export ``ctx.spectrum`` to an HDF5 file.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/export/export_hdf5/"
    },
    {
      "name": "export_votable",
      "description": "Export ``ctx.spectrum`` to a VOTable file (IVOA exchange format).\n\nParameters — pass as the 'params' object:\n- path (default None): Destination file path; null keeps the result in memory only.\n\nRequires in the session context: spectrum\n\nBackend: astropy.\nReferences: astropy.io.votable; IVOA VOTable 1.4 — Ochsenbein et al. 2019",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "export",
      "category_title": "Export",
      "summary": "Export ``ctx.spectrum`` to a VOTable file (IVOA exchange format).",
      "docs_url": "https://docs.spectrokernel.io/algorithms/export/export_votable/"
    },
    {
      "name": "extinction_correct_easyspec",
      "description": "Apply atmospheric extinction correction to ``ctx.spectrum`` via easyspec.\n\nSupported observatory keys depend on the installed easyspec version. Common entries include 'lapalma', 'cerropachon', 'paranal', 'kpno', 'lasilla'; pass a 2-column ASCII file via ``custom_observatory_path`` if your site is not bundled.\n\nParameters — pass as the 'params' object:\n- observatory (default 'lapalma'): Observatory key for the bundled extinction curve.\n- data_type (default 'target'): easyspec data_type label (target / standard_star).\n- airmass (default None) [required]: Airmass of the observation (mandatory — extinction scales with it).\n- custom_observatory_path (default None): Optional 2-col file (wavelength, mag/airmass) for a custom site.\n- spline_order (default 1): Spline order used to interpolate the extinction curve onto the spectrum.\n\nRequires in the session context: spectrum\n\nBackend: easyspec.\nReferences: easyspec.extraction.extraction.extinction_correction.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Apply atmospheric extinction correction to ``ctx.spectrum`` via easyspec.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/extinction_correct_easyspec/"
    },
    {
      "name": "extract_region",
      "description": "Crop a spectrum to the wavelength window ``[wavelength_min, wavelength_max]``.\n\nParameters — pass as the 'params' object:\n- wavelength_min (default None) [required]: Lower bound of the window to keep (Å).\n- wavelength_max (default None) [required]: Upper bound of the window to keep (Å).\n\nRequires in the session context: spectrum\n\nBackend: numpy.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "transform",
      "category_title": "Transforms",
      "summary": "Crop a spectrum to the wavelength window ``[wavelength_min, wavelength_max]``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/transform/extract_region/"
    },
    {
      "name": "extract_sky_lateral_bands",
      "description": "Median-combine two off-trace sky bands into a 1D pixel-axis reference.\n\nBand rows are instrument- and exposure-specific; set them from the preset that describes the slit geometry. The bands should be far enough from the trace to avoid wing contamination.\n\nParameters — pass as the 'params' object:\n- band_above_lo (default 0) [required]: First row (inclusive) of the band above the trace.\n- band_above_hi (default 0) [required]: Last row (exclusive) of the band above the trace.\n- band_below_lo (default 0) [required]: First row (inclusive) of the band below the trace.\n- band_below_hi (default 0) [required]: Last row (exclusive) of the band below the trace.\n- combine (default 'mean'): 'mean' or 'median' to combine the two band profiles.\n- extras_key (default 'sky_spectrum'): ctx.extras key under which the sky spectrum is stored.\n\nRequires in the session context: image\n\nBackend: numpy.\nReferences: Hanuschik 2003, A&A 407, 1157 — UVES optical sky emission atlas.; Osterbrock & Martel 1992, PASP 104, 76 — night-sky emission lines.; Stoughton et al. 2002, AJ 123, 485 — SDSS in-situ sky-line wavelength strategy.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "extraction",
      "category_title": "Extraction (2-D to 1-D)",
      "summary": "Median-combine two off-trace sky bands into a 1D pixel-axis reference.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/extraction/extract_sky_lateral_bands/"
    },
    {
      "name": "extract_spectrum_boxcar",
      "description": "Trace (easyspec) + pure-numpy aperture extraction on ``ctx.image``.\n\nTracing uses easyspec.tracing (argmax → polynomial). Extraction is a numpy aperture sum over the symmetric window round(y(x)) ± trace_half_width (2·trace_half_width + 1 rows) around the per-column trace position, fully edge-safe (any partial slice outside the detector contributes zero). tophat ⇒ standard boxcar; gaussian ⇒ profile-weighted (Gaussian fit on the mean column profile). easyspec is imported lazily inside run(). v3.0.0 (audit A9): up to v2 the aperture was [int(y) − hw, int(y) + hw) — int() truncates and the half-open window holds 2·hw rows, so the aperture sat 0.5 to 1.5 px below the trace depending on its fractional part and the captured fraction rippled with it (Gaussian profile σ = 1.5 px, hw = 3: 0.947 at trace row 20.0, 0.928 at 20.3, 0.910 at 20.5, 0.860 at 20.9). The rounded, symmetric window captures 0.983 / 0.980 / 0.976 / 0.982 on the same profile (ripple 0.7 % instead of 8.7 %). The sky region is likewise round(median_trace) ± shift_y_pixels inclusive, so each flanking window is exactly shift_y_pixels − trace_half_width rows high on the median row. Numerical impact on a well-sampled trace: one more row in the sum (+1 × sky per column when shift_y_pixels = 0) and up to +9 % flux where the old window was off-centre. Note: the gaussian mode here is profile-weighted but NOT variance-weighted and does NOT propagate per-pixel uncertainty or reject cosmics during extraction. For the full Horne 1986 estimator (inverse-variance weighting, empirical profile, iterative cosmic rejection, uncertainty propagation) see ``extract_spectrum_optimal`` — choose it for faint, read-noise-limited sources or when downstream code needs Spectrum1D.uncertainty. Sky geometry: with shift_y_pixels > 0 the two flanking windows run from the aperture edge (round(trace) ± trace_half_width) out to round(median_trace) ± shift_y_pixels, so each is shift_y_pixels − trace_half_width rows high on the median row; shift_y_pixels must exceed trace_half_width (refused otherwise). v2.0.0: the per-column sky is the mean of the NON-empty windows — v1 averaged a 0 in for an empty window, so a tilted trace (which empties one window over many columns because the bounds follow the median trace) had its sky halved there (audit case: 162/400 columns at half sky). Columns where both windows are empty get sky = 0 and are counted in the additive metric boxcar_columns_without_sky. The pixel axis is 0-based (numpy.arange(npix)).\n\nParameters — pass as the 'params' object:\n- trace_method (default 'argmax'): easyspec trace method: 'argmax', 'moments' or 'multi'.\n- trace_poly_order (default 2): Polynomial order of the trace fit.\n- trace_y_pixel_range (default 15): Half-window (rows) for the trace search.\n- trace_peak_height (default 100.0): Minimum peak height when locating the trace.\n- trace_peak_distance (default 50): Minimum separation between traces (multi mode).\n- trace_half_width (default 7): Aperture half-width (rows) for both trace and extraction: the aperture is the symmetric window round(y(x)) ± trace_half_width, i.e. 2·trace_half_width + 1 rows.\n- extraction_weights (default 'tophat'): 'tophat' (boxcar sum) or 'gaussian' (Horne-weighted).\n- shift_y_pixels (default 0): Half-height (rows) of the sky region centred on the median trace row (round(median_trace) ± shift_y_pixels inclusive): each flanking sky window spans from the aperture edge (round(trace) ± trace_half_width) out to that region's edge, i.e. shift_y_pixels - trace_half_width rows on the median row. Must exceed trace_half_width; 0 disables background subtraction.\n\nRequires in the session context: image\n\nBackend: easyspec.\nReferences: Horne 1986, PASP 98, 609 — optimal aperture extraction (tophat is the standard unweighted variant).; Tody 1986, Proc. SPIE 627, 733 — IRAF apall / aptrace heritage.; easyspec.extraction.extraction.tracing — argmax / moments trace fit.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "extraction",
      "category_title": "Extraction (2-D to 1-D)",
      "summary": "Trace (easyspec) + pure-numpy aperture extraction on ``ctx.image``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/extraction/extract_spectrum_boxcar/"
    },
    {
      "name": "extract_spectrum_easyspec",
      "description": "Trace and extract a 1D spectrum from a 2D frame via easyspec.\n\nGaussian-weighted aperture extraction with Monte-Carlo error estimation. Pair the resulting pixel-index spectrum with ``wavelength_calibrate_easyspec`` (or the native polynomial calibrator) to get a proper wavelength solution. The pixel axis is 0-based (numpy.arange(npix)), like every other extractor. v2.0.0: when ctx.image is staged to disk, BZERO/BSCALE/BITPIX are no longer copied onto the float64 staging HDU — a uint16-headered frame ([100, 200, …] with BZERO=32768) used to be read back by easyspec as [32868, 32968, …]; the extracted flux is now correct for such inputs. easyspec sessions are serialised behind a process-wide lock (they chdir into a temporary directory).\n\nParameters — pass as the 'params' object:\n- target_path (default None): 2D science FITS to extract; falls back to ctx.image.\n- target_name (default 'target'): Label used internally by easyspec for diagnostics.\n- exposure_seconds (default None): Exposure time (seconds); null reads it from the FITS header.\n- airmass (default None): Airmass; null reads it from the FITS header.\n- exposure_header_entry (default 'AVEXP'): Header keyword for exposure when 'exposure_seconds' is null.\n- airmass_header_entry (default 'AVAIRMAS'): Header keyword for airmass when 'airmass' is null.\n- trace_method (default 'argmax'): Trace-finding method (easyspec: 'argmax' or 'fit').\n- trace_poly_order (default 2): Polynomial order of the trace fit.\n- trace_half_width (default 7): Half-width of the aperture (pixels).\n- trace_y_pixel_range (default 15): Half-window for trace centroiding (pixels).\n- trace_n_slices (default 20): Number of detector columns sampled to fit the trace.\n- trace_peak_height (default 100): Minimum peak height when searching for the trace.\n- trace_peak_distance (default 50): Minimum separation between peaks (pixels).\n- mc_steps (default 25): Monte-Carlo realisations for the per-pixel flux error.\n- extraction_weights (default 'gaussian'): Aperture weighting: 'gaussian' or 'uniform'.\n- shift_y_pixels (default 30): Allowed shift between tracing and extracting (pixels).\n\nBackend: easyspec.\nReferences: easyspec.extraction.extraction.{import_data, tracing, extracting}.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "extraction",
      "category_title": "Extraction (2-D to 1-D)",
      "summary": "Trace and extract a 1D spectrum from a 2D frame via easyspec.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/extraction/extract_spectrum_easyspec/"
    },
    {
      "name": "extract_spectrum_optimal",
      "description": "Optimal (Horne 1986) aperture extraction of ``ctx.image``.\n\nFaithful Horne 1986 recipe (Table I) : sky from ctx.extras / the flanking rows / none ; spatial profile from low-order polynomial fits along the dispersion of the normalised data (D − S)/f per detector row (eq. 14, weights f²/V, σ-clipped, P ≥ 0, unit sum per column) ; model variance V = (RON/gain)² + |f·P + S|/gain (eq. 13) ; optimal sum f = Σ M P (D − S)/V ÷ Σ M P²/V with variance 1/Σ M P²/V (eqs. 8–9) ; cosmic rays rejected one pixel per column per iteration (§II.E) ; the profile → extraction sequence runs twice, the second time normalised by the optimal spectrum and without the rejected pixels (§II.F). v2.0.0 (audit A4/A9) : v1 built the variance from the data (|D|/gain + RON²) and the profile from a 21-column running median of the raw cutouts, whose noise is correlated with the pixel noise — measured bias on a σ = 1.5 px Gaussian trace at gain 1, RON 3 e⁻, 65 536 columns : −23.1 % at 5 e⁻/column, −13.7 % at 20, −4.8 % at 100, −0.8 % at 1000 (v2 : −0.3 % ± 0.6 %, +0.6 %, +0.1 %, 0.0 %, the last three identical to the boxcar control on the same frames) ; v1 added RON² in e⁻² to a variance in ADU², so the reported σ was ×2.05 the empirical scatter at gain 2.5 / RON 6 e⁻, 100 e⁻/column (v2 : ×1.00, 0.995–1.03 over 20–1000 e⁻) ; v1 rejected every pixel above the threshold at once. The aperture is now the symmetric window round(y) ± trace_half_width (2·hw + 1 rows) instead of [int(y) − hw, int(y) + hw) — the old window sat 0.5–1.5 px below the trace, so the captured fraction rippled with the fractional trace position (0.947 at row 20.0, 0.860 at 20.9 for σ = 1.5 px, hw = 3 ; now 0.983, 0.976 at the worst half-pixel offset, 0.982 at 20.9). Profile normalisation : a column is normalised by its own flux when its S/N ≥ 10, else by the running median of the spectrum over profile_smooth_columns — Horne's per-column normaliser attenuates the profile at low S/N (errors-in-variables ; +17 % at 5 e⁻/column, profile peak 0.195 instead of 0.266), which biases the sum ; the running median keeps the estimator unbiased down to a few e⁻ per column. Single-pixel spikes along the dispersion are excluded from the profile *fit* only. The flanking sky fit is unweighted : Horne's 1/V weights (eq. 12) bias a Poisson mean by ≈ −1 count per pixel, i.e. +(2·hw + 1) ADU per column in the flux. Uncertainty : √(1/Σ M P²/V), sky taken as noise-free (Horne §II.A). Edge columns keep the aperture rows that fit the detector. Strongly tilted or curved traces (drift of several rows) need a higher profile_poly_order or a prior geometric rectification (correct_slant_affine / correct_smile_polynomial) — the profile of a detector row is fitted along the dispersion in fixed rows, as in the paper, not in trace-relative coordinates (Marsh 1989). When to choose this vs the lighter alternatives : ``extract_spectrum_sum`` is the simplest pure sum (no trace fit) ; ``extract_spectrum_boxcar`` is the edge-safe boxcar with optional Gaussian profile weighting but no variance model, no uncertainty propagation and no cosmic rejection — pick it for high-SNR sources or extended emission. Pick ``extract_spectrum_optimal`` when SNR matters most (faint, read-noise-limited targets), when residual cosmics may remain after ``clip_cosmic_rays``, or when the downstream pipeline needs per-pixel ``Spectrum1D.uncertainty`` (e.g. radial-velocity error propagation). The pixel axis is 0-based (numpy.arange(npix)).\n\nParameters — pass as the 'params' object:\n- trace_method (default 'argmax'): easyspec trace method ('argmax' / 'moments' / 'multi').\n- trace_poly_order (default 2): Polynomial order of the trace fit.\n- trace_y_pixel_range (default 15): Half-window (rows) for the trace search.\n- trace_peak_height (default 100.0): Minimum peak height when locating the trace.\n- trace_peak_distance (default 50): Minimum separation between traces (multi mode).\n- trace_half_width (default 12): Aperture half-width (rows) for trace + extraction : the aperture is the symmetric window round(y(x)) ± trace_half_width, i.e. 2·trace_half_width + 1 rows.\n- gain (default 1.0): Detector gain in e⁻/ADU (variance model, Horne's Q).\n- readnoise (default 6.0): Detector read noise in e⁻ (variance model) ; converted to ADU² as V0 = (readnoise / gain)².\n- sky_source (default 'auto'): Where the sky S comes from. 'extras' : the per-column 1-D Spectrum1D at ctx.extras[sky_extras_key] (ADU per pixel on the pixel axis, as written by extract_sky_lateral_bands ; fails if absent). 'flanking' : per-column weighted polynomial fit across the slit of the rows flanking the aperture (sky_offset, sky_half_width, sky_poly_order — the geometry of subtract_sky_2d). 'none' : S = 0, for frames already sky-subtracted. 'auto' (default) : 'none' when ctx.image.meta['sky_subtracted'] is set (subtract_sky_2d output), else 'extras' when the key is present, else 'flanking' when sky_half_width > 0, else 'none'. The resolved choice is recorded in ctx.spectrum.meta['sky_source'].\n- sky_extras_key (default 'sky_spectrum'): ctx.extras key holding the 1-D sky spectrum ('extras' / 'auto').\n- sky_offset (default 4): Rows between the aperture edge and the start of each flanking sky window ('flanking' / 'auto').\n- sky_half_width (default 10): Height (rows) of each flanking sky window ('flanking' / 'auto') ; 0 disables the flanking estimate.\n- sky_poly_order (default 1): Order of the per-column polynomial fitted across the slit to the flanking sky rows (0 = weighted mean, 1 = linear gradient).\n- profile_poly_order (default 2): Order of the polynomial fitted along the dispersion to the normalised data of each detector row (Horne step 5). Low (0–2) for a straight trace ; raise it for a drifting trace.\n- profile_smooth_columns (default 401): Odd window (columns) of the running median of the spectrum used to normalise the profile points of columns whose S/N is below 10 (brighter columns use their own flux, Horne eq. 14). Wider ⇒ less attenuation of the profile at low S/N ; a window ≥ the number of columns uses the global median.\n- reject_sigma (default 5.0): σ threshold of the cosmic-ray rejection (one pixel per column per iteration, Horne uses 5) and of the σ-clipping in the sky and profile fits.\n- iterations (default 3): Maximum extract → reject → re-extract cycles per pass (one pixel per column per cycle) and maximum σ-clipping rounds of the profile fits.\n\nRequires in the session context: image\n\nBackend: scipy.\nReferences: Horne 1986, PASP 98, 609 — optimal extraction algorithm for CCD spectroscopy.; Marsh 1989, PASP 101, 1032 — empirical profile for tilted traces.; Tody 1986, SPIE 627, 733 — IRAF apall heritage.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "extraction",
      "category_title": "Extraction (2-D to 1-D)",
      "summary": "Optimal (Horne 1986) aperture extraction of ``ctx.image``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/extraction/extract_spectrum_optimal/"
    },
    {
      "name": "extract_spectrum_sum",
      "description": "Extract a 1D spectrum from ``ctx.image`` by summing across the trace.\n\nThis is the simple unweighted sum: useful for bright targets and as a baseline. The resulting wavelength axis is in **pixel indices** — pass the spectrum through `wavelength_calibrate_polynomial` to convert to Ångström. Pixel convention: the axis is 0-based (numpy.arange(npix), first column = pixel 0), the same convention as extract_spectrum_boxcar / extract_spectrum_optimal / extract_spectrum_easyspec / extract_sky_lateral_bands and as the pixel positions returned by match_lamp_lines / reidentify_arc_features. v2.0.0: v1 produced a 1-based axis (arange + 1), so chaining it into match_lamp_lines → wavelength_calibrate_polynomial shifted the solution by one pixel (~ +0.9 Å at 0.9 Å/px); the axis is now 0-based like every other extractor.\n\nParameters — pass as the 'params' object:\n- dispersion_axis (default 1): Axis along which the spectrum disperses (1 = horizontal rows).\n- half_width (default 5): Half-width of the extraction window in spatial pixels.\n\nRequires in the session context: image\n\nBackend: numpy.\nReferences: Horne 1986, PASP 98, 609 — optimal extraction (variance-weighted variant).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "extraction",
      "category_title": "Extraction (2-D to 1-D)",
      "summary": "Extract a 1D spectrum from ``ctx.image`` by summing across the trace.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/extraction/extract_spectrum_sum/"
    },
    {
      "name": "fit_emission_lines_gaussian",
      "description": "Measure sub-pixel centroids of several emission lines at once.\n\nEach line is fit in its own window after a cheap argmax recentring step. Fit failures degrade gracefully to the recentred argmax (warning recorded in the result message); the algorithm never raises mid-batch.\n\nParameters — pass as the 'params' object:\n- guess_positions (default []) [required]: Approximate pixel positions of the lines to fit.\n- search_width (default 40.0): Full width (pixels) of the fit window around each guess (≥ 4).\n- initial_sigma (default 5.0): Starting Gaussian σ (pixels).\n- source_key (default 'sky_spectrum'): ctx.extras key of the spectrum to fit; falls back to ctx.spectrum when the key is absent.\n\nBackend: astropy.\nReferences: Markwardt 2009, ASP Conf. 411, 251 — Levenberg-Marquardt (MINPACK lmdif) algorithm in astronomy.; Robitaille et al. 2013, A&A 558, A33 — Astropy.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "wavelength_calibration",
      "category_title": "Wavelength calibration",
      "summary": "Measure sub-pixel centroids of several emission lines at once.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/wavelength_calibration/fit_emission_lines_gaussian/"
    },
    {
      "name": "fit_gaussian_line",
      "description": "Fit a single Gaussian (plus a linear continuum) to one spectral line.\n\nEquivalent width follows the convention EW > 0 for absorption and EW < 0 for emission. The Gaussian is the right choice for instrument- or thermally-broadened lines. v2.0.0: masked samples (Spectrum1D.mask) are ignored — excluded from the fit window like non-finite ones ; at least 6 usable samples must remain.\n\nParameters — pass as the 'params' object:\n- line_center_angstrom (default None) [required]: Approximate line centre in Å (required).\n- window_angstrom (default 20.0): Half-width of the fit window on each side of the line (Å).\n- label (default ''): Key under which to store the result; defaults to the rounded centre.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: scipy.optimize.curve_fit — Levenberg-Marquardt / Trust Region Reflective",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "line_fitting",
      "category_title": "Line fitting",
      "summary": "Fit a single Gaussian (plus a linear continuum) to one spectral line.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/line_fitting/fit_gaussian_line/"
    },
    {
      "name": "fit_keplerian_orbit",
      "description": "Fit a single-companion Keplerian RV curve to a velocity time series.\n\nThe eccentricity is bound to [0, 0.95]; the argument of periastron ω to [−2π, 2π] (unwrapped, so the solver can cross 0/2π freely ; reduce omega_rad mod 2π downstream). Initial guesses are derived from the data when not supplied (period from the dominant Lomb-Scargle peak if astropy is available, otherwise the user must provide ``period_initial``) ; an explicit 0.0 guess is honoured. The fit is UNWEIGHTED: ``chi_squared`` is the plain sum of squared residuals (km/s)², not a χ². When ``curve.uncertainty`` is set (finite, > 0) the additive ``reduced_chi_squared`` = Σ((v − model)/σ)² / (N − 6) is reported as a goodness-of-fit diagnostic ; the residuals are still not weighted by σ in the optimisation.\n\nParameters — pass as the 'params' object:\n- light_curve_key (default 'rv'): Key into ctx.light_curves for the RV time series.\n- period_initial (default None): Initial guess for the orbital period (days). Falls back to a Lomb-Scargle peak when null.\n- semi_amplitude_initial (default None): Initial guess for K (km/s). Default: half the peak-to-peak velocity range.\n- eccentricity_initial (default 0.1): Initial guess for e (0..0.95).\n- omega_initial (default 0.0): Initial guess for ω (radians).\n- t_peri_initial (default None): Initial guess for time of periastron (same unit as the time axis).\n- gamma_initial (default None): Initial systemic velocity guess (km/s). Default: median of the data.\n- max_nfev (default 5000): Maximum function evaluations for the least-squares solver.\n\nBackend: scipy.\nReferences: scipy.optimize.least_squares (Trust Region Reflective).; Murray & Correia 2010 — Keplerian elements / RV formalism review.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "radial_velocity",
      "category_title": "Radial velocity",
      "summary": "Fit a single-companion Keplerian RV curve to a velocity time series.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/radial_velocity/fit_keplerian_orbit/"
    },
    {
      "name": "fit_lorentzian_line",
      "description": "Fit a single Lorentzian (plus a linear continuum) to one spectral line.\n\nThe fitted width parameter is the half-width at half-maximum (gamma); the reported FWHM is 2*gamma. v2.0.0: masked samples (Spectrum1D.mask) are ignored — excluded from the fit window like non-finite ones ; at least 6 usable samples must remain.\n\nParameters — pass as the 'params' object:\n- line_center_angstrom (default None) [required]: Approximate line centre in Å (required).\n- window_angstrom (default 20.0): Half-width of the fit window on each side of the line (Å).\n- label (default ''): Key under which to store the result; defaults to the rounded centre.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: scipy.optimize.curve_fit — Levenberg-Marquardt / Trust Region Reflective",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "line_fitting",
      "category_title": "Line fitting",
      "summary": "Fit a single Lorentzian (plus a linear continuum) to one spectral line.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/line_fitting/fit_lorentzian_line/"
    },
    {
      "name": "fit_telluric_scaling",
      "description": "Fit the airmass that best matches a telluric template to ``ctx.spectrum``.\n\nOnly wavelengths inside the telluric bands carry information for this fit. The algorithm restricts the residual to the regions where the template transmission drops below ``band_threshold`` (default 0.9 — i.e. pixels where the template absorbs at least 10%). Outside those regions the spectrum is dominated by continuum / stellar lines and would only bias the fit. The rescaled template is written to ctx.extras['telluric_template_scaled'] (on the observed wavelength grid), which remove_telluric_division picks up by default.\n\nParameters — pass as the 'params' object:\n- template_key (default 'telluric_template'): Where to read the template from ``ctx.extras`` (output of synth_telluric).\n- band_threshold (default 0.9): Pixels of the template where T < threshold count as 'inside a band'.\n- airmass_min (default 0.5): Lower bound of the airmass search interval.\n- airmass_max (default 4.0): Upper bound of the airmass search interval.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Beer-Lambert atmospheric transmission scaling: T(airmass) = T(1)^airmass.; scipy.optimize.minimize_scalar.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Fit the airmass that best matches a telluric template to ``ctx.spectrum``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/fit_telluric_scaling/"
    },
    {
      "name": "fit_voigt_line",
      "description": "Fit a single Voigt profile (plus a linear continuum) to one spectral line.\n\nTwo width parameters are fitted: the Gaussian sigma and the Lorentzian gamma. Both are recorded in the result's meta. v2.0.0: masked samples (Spectrum1D.mask) are ignored — excluded from the fit window like non-finite ones ; at least 6 usable samples must remain.\n\nParameters — pass as the 'params' object:\n- line_center_angstrom (default None) [required]: Approximate line centre in Å (required).\n- window_angstrom (default 20.0): Half-width of the fit window on each side of the line (Å).\n- label (default ''): Key under which to store the result; defaults to the rounded centre.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: scipy.special.voigt_profile; Olivero & Longbothum 1977, JQSRT 17, 233 — Voigt FWHM approximation; scipy.optimize.curve_fit",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "line_fitting",
      "category_title": "Line fitting",
      "summary": "Fit a single Voigt profile (plus a linear continuum) to one spectral line.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/line_fitting/fit_voigt_line/"
    },
    {
      "name": "flat_combine",
      "description": "Combine a stack of flat frames into a master flat (raw, not normalised).\n\nPixel-wise median (default) or σ-clipped mean. Normalisation is intentionally NOT folded in: keep flat_combine producing the raw master, then call flat_normalize on the result. Separation of concerns mirrors the bias_combine / dark_combine pattern. Caveat: flat_normalize divides the master by a single global median (not row-by-row / not along the dispersion), so the flat lamp's spectral shape stays imprinted in the normalised flat and is divided into the science frame — the catalogue has no equivalent of IRAF's `response` (fit of the lamp continuum along the dispersion) yet; for spectrophotometry derive the response with response_from_standard afterwards. v2.0.0 (method='mean' with sigma_clip only): same MAD = 0 fallback as dark_combine v2.0.0 — the σ-clip scale falls back to 1.2533 × the mean absolute deviation about the median instead of rejecting every value ≠ median ([10,10,10,11,12], σ=3: v1 → 10.0, v2 → 10.6; [10,10,10,11,1000] → 10.25, outlier still rejected). method='median' is unchanged.\n\nParameters — pass as the 'params' object:\n- method (default 'median'): 'median' (robust default) or 'mean' (with optional sigma_clip).\n- bias_key (default None): ctx.extras key for a master bias to subtract from each frame. None = no pre-subtraction.\n- dark_key (default None): ctx.extras key for a master dark to subtract from each frame. None = no pre-subtraction.\n- scale_by_exptime (default True): Scale the master dark by EXPTIME_flat / EXPTIME_dark before subtracting. Requires both EXPTIMEs in the headers.\n- sigma_clip (default None): σ threshold for σ-clipped mean (method='mean' only). None or ≤0 disables clipping.\n\nRequires in the session context: images\n\nBackend: numpy.\nReferences: Howell 2006 — Handbook of CCD Astronomy, ch. 4 (CCD reduction).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "master_creation",
      "category_title": "Master frames",
      "summary": "Combine a stack of flat frames into a master flat (raw, not normalised).",
      "docs_url": "https://docs.spectrokernel.io/algorithms/master_creation/flat_combine/"
    },
    {
      "name": "flat_combine_easyspec",
      "description": "Combine flat frames into a master flat via ``easyspec.cleaning.master``.\n\nParameters — pass as the 'params' object:\n- flat_dir (default None): Directory containing the flat FITS files.\n- flat_paths (default None): List of flat FITS file paths.\n- method (default 'median'): Stacking method: median, mean, or mode.\n- header_hdu_entry (default 0): HDU extension where the FITS header lives.\n\nBackend: easyspec.\nReferences: easyspec — Lobão et al. (https://pypi.org/project/easyspec/).; easyspec.cleaning.cleaning.master.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "master_creation",
      "category_title": "Master frames",
      "summary": "Combine flat frames into a master flat via ``easyspec.cleaning.master``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/master_creation/flat_combine_easyspec/"
    },
    {
      "name": "flat_normalize",
      "description": "Divide ``ctx.image`` by a master flat normalised to unity.\n\nNormalisation is by ONE global median of the master flat — not per row and not along the dispersion axis — so the flat lamp's spectral shape remains in the normalised flat and is divided into the science frame. This removes pixel-to-pixel response but also imprints the inverse lamp continuum; the catalogue has no equivalent of IRAF's `response` (dispersion-direction fit of the lamp) yet, so for spectrophotometry derive the instrumental response afterwards with response_from_standard. Low-response pixels (normalised flat < min_response, or non-finite result) are NOT masked: the original science value is substituted, i.e. those pixels stay un-flattened.\n\nParameters — pass as the 'params' object:\n- flat_key (default 'master_flat'): Key in ctx.extras where the master flat is stored.\n- min_response (default 0.05): Below this normalised flat value the science pixel is left unchanged (not divided, not masked).\n\nRequires in the session context: image\n\nBackend: numpy.\nReferences: Howell 2006 — Handbook of CCD Astronomy, ch. 4 (flat fielding).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Divide ``ctx.image`` by a master flat normalised to unity.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/flat_normalize/"
    },
    {
      "name": "flat_normalize_easyspec",
      "description": "Flat-field correct a science frame via ``cleaning.flatten``.\n\nParameters — pass as the 'params' object:\n- target_path (default None): Science FITS to flat-field; falls back to ctx.image.\n- master_flat_path (default None): Master-flat FITS; falls back to ctx.extras['master_flat'].\n- auto_normalise (default True): If true, divide the master flat by its median first.\n\nBackend: easyspec.\nReferences: easyspec.cleaning.cleaning.flatten.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Flat-field correct a science frame via ``cleaning.flatten``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/flat_normalize_easyspec/"
    },
    {
      "name": "flux_calibrate_easyspec",
      "description": "Flux-calibrate ``ctx.spectrum`` via a standard-star observation.\n\nUse ``list_available_standards()`` from easyspec.extraction to find the archive datasets it bundles (calspec, oke1990, irscal, …). The target spectrum on ``ctx.spectrum`` must be wavelength-calibrated *and* extinction-corrected before this step.\n\nParameters — pass as the 'params' object:\n- std_star_wavelength_angstrom (default None) [required]: Wavelength axis (Å) of the standard star's extinction-corrected spectrum.\n- std_star_flux_extinction_corrected (default None) [required]: Flux of the standard star, already corrected for atmospheric extinction.\n- std_star_dataset (default 'calspec'): Archive name bundled with easyspec (e.g. calspec, oke1990).\n- std_star_archive_file (default None) [required]: Reference file inside the chosen dataset (e.g. alpha_lyr_stis_011.dat).\n- exposure_target_seconds (default None) [required]: Exposure time of the target observation (s).\n- exposure_std_star_seconds (default None) [required]: Exposure time of the standard-star observation (s).\n- smooth_window (default 101): Smoothing window for the observed/archive ratio.\n- smooth_window_archive (default 11): Smoothing window for the archive spectrum before division.\n- reddening_ebv (default None): Optional interstellar E(B-V) to apply (CCM/F99 inside easyspec).\n- rv_extinction (default None): R_V to pair with reddening_ebv (default ~3.1 if unset).\n- save_output (default False): If true, let easyspec write its diagnostic FITS to the temp dir.\n\nRequires in the session context: spectrum\n\nBackend: easyspec.\nReferences: easyspec.extraction.extraction.std_star_normalization.; easyspec.extraction.extraction.target_flux_calibration.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "flux_calibration",
      "category_title": "Flux calibration",
      "summary": "Flux-calibrate ``ctx.spectrum`` via a standard-star observation.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/flux_calibration/flux_calibrate_easyspec/"
    },
    {
      "name": "gaia_query",
      "description": "Cone-search the Gaia archive around ICRS coordinates.\n\nRequires network access and the optional 'catalogs' extra. By default queries the latest release available via astroquery.gaia. Use `data_release` to pin a specific release such as 'DR3' or 'DR2'.\n\nParameters — pass as the 'params' object:\n- ra_deg (default None) [required]: Right ascension (ICRS) in degrees.\n- dec_deg (default None) [required]: Declination (ICRS) in degrees.\n- radius_arcsec (default 5.0): Cone-search radius around the position.\n- data_release (default None): Gaia release table to query (e.g. 'gaiadr3.gaia_source').\n\nBackend: astroquery.\nReferences: Gaia Collaboration 2016, A&A 595, A1 — the Gaia mission; Salgado et al. 2017 — Gaia archive TAP+ access; Ginsburg et al. 2019, AJ 157, 98 — astroquery",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "catalog",
      "category_title": "External catalogues",
      "summary": "Cone-search the Gaia archive around ICRS coordinates.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/catalog/gaia_query/"
    },
    {
      "name": "lomb_scargle",
      "description": "Compute a Lomb-Scargle periodogram and report the dominant period.\n\nFrequency bounds default to astropy's automatic grid. The false-alarm probability of the highest peak is reported using the 'baluev' approximation, evaluated over the same frequency bounds as the searched grid (minimum_frequency / maximum_frequency are forwarded to astropy's false_alarm_probability ; with both null the FAP is identical to v1.1.0). The 'normalization' parameter is forwarded verbatim to astropy and selects between 'standard' (default, scaled χ² of the fit), 'model' (χ² of the null model), 'log' (log-likelihood), and 'psd' (unnormalised power spectral density). Use 'psd' when stitching with classical Fourier-domain tools or comparing to noise variance ; 'standard' is the right default for peak-detection and FAP.\n\nParameters — pass as the 'params' object:\n- time (default None): List of observation times; omit to use a light curve from the context.\n- flux (default None): List of flux values, paired with 'time'.\n- uncertainty (default None): Optional list of per-point flux uncertainties.\n- light_curve_key (default None): Key in ctx.light_curves to use when time/flux are omitted.\n- minimum_frequency (default None): Lower bound of the frequency grid (null = automatic).\n- maximum_frequency (default None): Upper bound of the frequency grid (null = automatic).\n- samples_per_peak (default 5): Frequency-grid oversampling factor.\n- normalization (default 'standard'): Astropy normalisation mode for the power spectrum ; one of ['standard', 'model', 'log', 'psd']. Forwarded verbatim to astropy.timeseries.LombScargle.autopower.\n\nBackend: astropy.\nReferences: Lomb 1976, Ap&SS 39, 447.; Scargle 1982, ApJ 263, 835.; VanderPlas 2018, ApJS 236, 16 — understanding the Lomb-Scargle periodogram (normalisation conventions reviewed §7.2).; astropy.timeseries.LombScargle (normalization parameter).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "timeseries",
      "category_title": "Time series",
      "summary": "Compute a Lomb-Scargle periodogram and report the dominant period.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/timeseries/lomb_scargle/"
    },
    {
      "name": "mask_range",
      "description": "Flag every sample inside a wavelength window as masked.\n\nParameters — pass as the 'params' object:\n- wavelength_min (default None) [required]: Lower bound of the range to mask (Å).\n- wavelength_max (default None) [required]: Upper bound of the range to mask (Å).\n\nRequires in the session context: spectrum\n\nBackend: numpy.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "transform",
      "category_title": "Transforms",
      "summary": "Flag every sample inside a wavelength window as masked.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/transform/mask_range/"
    },
    {
      "name": "match_lamp_lines",
      "description": "Auto-identify arc-lamp lines against the bundled NIST atlas.\n\nGlobal brute-force seed (linear λ(x) grid scored by atlas-match count) followed by σ-clipped polynomial refinement. The atlas comes from spectro_kernel.algorithms.wavelength_calibration.lamp_atlas (NIST ASD persistent lines for Ne / Ar / NeAr / ThAr). Output goes to ctx.extras[output_key] in the same dict shape wavelength_calibrate_polynomial expects: {'pixel_positions': [...], 'wavelengths_angstrom': [...]}. For night-to-night re-anchoring of an already-identified instrument set-up (IRAF reidentify), use the companion brick reidentify_arc_features — it conserves a stored empirical feature list instead of re-matching the atlas.\n\nParameters — pass as the 'params' object:\n- lamp (default 'NeAr') [required]: Arc-lamp identifier. One of ('Ar', 'Ne', 'NeAr', 'ThAr').\n- wave_min (default None): Lower bound (Å) of the expected spectral domain. Strongly recommended — used to bound the linear-seed grid.\n- wave_max (default None): Upper bound (Å) of the expected spectral domain.\n- dispersion_hint (default None): Expected dispersion (Å / pixel). Optional but accelerates and constrains the linear seed.\n- detection_sigma (default 5.0): Peak prominence threshold expressed as a multiple of the median-absolute-deviation noise (DER_SNR style).\n- min_prominence (default 0.01): Floor on the prominence as a fraction of peak amplitude (prevents detection_sigma from going to 0 on flat regions).\n- min_distance_px (default 5): Minimum spacing between detected peaks (pixels).\n- poly_order (default 3): Polynomial order of the fitted λ(x).\n- match_tol_px (default 2.0): Maximum residual (pixels) between an observed peak and the nearest atlas line for the match to count.\n- sigma_clip (default 3.0): σ-clip threshold during the polyfit refinement.\n- max_lines (default 30): Cap on the number of detected peaks fed to matching.\n- source_key (default 'lamp_spectrum'): ctx.extras key holding the lamp Spectrum1D (pixel axis).\n- output_key (default 'lamp_identifications'): ctx.extras key receiving the identifications dict {'pixel_positions': [...], 'wavelengths_angstrom': [...]}.\n\nBackend: scipy.\nReferences: Tody 1986, SPIE 627, 733 — IRAF Data Reduction and Analysis System.; Tody 1993, ASP Conf. Ser. 52, 173 — IRAF identify/autoidentify.; Murphy et al. 2007, MNRAS 378, 221 — robust ThAr wavelength solution.; Kramida, Ralchenko, Reader & NIST ASD Team — NIST Atomic Spectra Database (Ne I / Ar I / Th-Ar persistent lines), https://physics.nist.gov/asd.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "wavelength_calibration",
      "category_title": "Wavelength calibration",
      "summary": "Auto-identify arc-lamp lines against the bundled NIST atlas.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/wavelength_calibration/match_lamp_lines/"
    },
    {
      "name": "measure_arc_geometry",
      "description": "Measure smile_radius + slant_deg from an arc-lamp 2-D image.\n\nSums the arc along the slit, picks bright arc lines with scipy.signal.find_peaks, then walks each line row-by-row across the slit computing a sub-pixel column centroid in a small window. Fits a Schroeder parabola dx(y) = (y - y₀)² / (2 R) on the row-averaged offsets to get R; the residual linear slope dx/dy → tilt angle. y₀ comes from ctx.extras['trace'] (set by detect_trace) when present, otherwise defaults to the image centre. v2.0.0: slant_deg is now returned with the sign correct_slant_affine expects (slant_deg = -atan(dx/dy)); feeding it unchanged to correct_slant_affine(slant_deg=…, pivot_row=reference_row) straightens the lines. v1 returned the opposite sign, so the documented chain doubled the slant (synthetic +0.05 px/row: residual +0.10 px/row in v1, 0.000 in v2).\n\nParameters — pass as the 'params' object:\n- lamp_key (default 'lamp_image'): ctx.extras key holding the arc-lamp 2-D ImageFrame.\n- trace_key (default 'trace'): ctx.extras key holding the science trace dict (from detect_trace). The trace's center_row becomes the reference row y₀ ; absent ⇒ image centre.\n- detection_sigma (default 5.0): Peak prominence threshold (× DER_SNR-style noise) when finding arc lines on the summed spectrum.\n- min_prominence (default 0.05): Lower bound on the prominence as a fraction of the peak amplitude.\n- min_lines (default 6): Minimum number of arc lines successfully tracked across the slit. The algorithm fails below this — too few lines makes the smile fit unreliable.\n- max_lines (default 30): Cap on the number of arc lines fed to the fit.\n- search_half_width_px (default 12): Half-window (column pixels) around each line's reference x for the per-row centroid.\n- row_step (default 5): Spacing (rows) between sample points along the slit.\n- row_half_range (default 200): Half-range (rows) above and below the reference row over which each line is tracked.\n- smile_order (default 2): Polynomial order for the smile fit (2 → Schroeder parabola).\n- fit_tilt (default True): When True, also estimate the linear tilt slope.\n- output_key (default 'geometry'): ctx.extras key receiving the geometry dict.\n\nBackend: scipy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — IRAF identify / reidentify / fitcoords 2-D wavelength solutions.; Prochaska et al. 2020, JOSS 5, 2308 — PypeIt wavelength / tilts module.; Schroeder 2000, Astronomical Optics 2nd ed. ch. 15 §15.3 — smile curvature parametrisation R.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "wavelength_calibration",
      "category_title": "Wavelength calibration",
      "summary": "Measure smile_radius + slant_deg from an arc-lamp 2-D image.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/wavelength_calibration/measure_arc_geometry/"
    },
    {
      "name": "measure_radial_velocity",
      "description": "Measure a radial velocity from the Doppler shift of a single line.\n\nA positive velocity means the source is receding (redshift). For an absolute velocity, apply barycentric_correction to the spectrum first. profile='voigt' is slower but the right choice when neither pure Gaussian (Doppler-dominated) nor pure Lorentzian (collisionally-dominated) fits the data. v2.0.0: masked samples (Spectrum1D.mask) are ignored — excluded from the fit window like non-finite ones when a new centroid is fitted.\n\nParameters — pass as the 'params' object:\n- rest_wavelength_angstrom (default None) [required]: Laboratory (rest) wavelength of the line in Å.\n- line_label (default None): Key of an existing fit in ctx.line_fits; null fits a new line.\n- window_angstrom (default 20.0): Half-width of the fit window when fitting a new line (Å).\n- profile (default 'gaussian'): Line-profile model used when fitting a new centroid; one of ['gaussian', 'lorentzian', 'voigt'].\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Classical Doppler relation: v = c · (λ_obs − λ_rest) / λ_rest.; Line centroid via scipy.optimize.curve_fit on the selected profile (Gaussian / Lorentzian / Voigt — see fit_gaussian_line, fit_lorentzian_line, fit_voigt_line for the standalone bricks).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "radial_velocity",
      "category_title": "Radial velocity",
      "summary": "Measure a radial velocity from the Doppler shift of a single line.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/radial_velocity/measure_radial_velocity/"
    },
    {
      "name": "measure_resolving_power",
      "description": "Resolving power R = λ/FWHM from Gaussian fits of the strongest isolated lamp / sky lines.\n\nR_i = λ_i / FWHM_i for each of the n_lines most prominent emission peaks (scipy.signal.find_peaks with height and prominence above prominence_sigma × 1.4826·MAD of the residual after a continuum_window running median) that have no other detection within min_separation_aa (both members of a closer pair are dropped) ; each peak is fitted with a Gaussian + linear pedestal in ± window_aa (lines/_profiles.fit_line, as fit_gaussian_line) and kept when its FWHM lies in (1 pixel, window_aa), its centre within window_aa/2 of the detection and its amplitude above the threshold. resolving_power_median / _std summarise the per-line values, instrumental_fwhm_aa_median is the median fitted FWHM and instrumental_fwhm_kms_median the median of c·FWHM_i/λ_i. Per-line results (wavelength, fwhm, R, amplitude, prominence) are in extras['resolving_power_lines']. Use on an arc lamp or a sky spectrum whose lines are intrinsically unresolved ; blends bias R low (raise min_separation_aa). The Gaussian approximation reads a few per cent low on boxy fibre profiles. On a dense forest raise continuum_window so the running median stays on the pedestal. Fails when no peak is detected, when no detection is isolated, or when every fit is rejected. v2.0.0: masked samples (Spectrum1D.mask) are dropped like non-finite ones before detection and fitting.\n\nParameters — pass as the 'params' object:\n- n_lines (default 10): Maximum number of strongest isolated lines to fit.\n- min_separation_aa (default 5.0): Minimum distance (Å) to any other detection for a line to count as isolated ; both members of a closer pair are dropped.\n- window_aa (default 3.0): Half-width (Å) of the Gaussian fit window around each line.\n- prominence_sigma (default 5.0): Peak prominence threshold in units of the MAD noise.\n- continuum_window (default 101): Running-median window (samples, odd ≥ 3) used to remove the pedestal before peak detection.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 3 and ch. 12 : R = λ/Δλ with Δλ the FWHM of the instrumental profile.; Tody 1986, Proc. SPIE 627, 733 — IRAF splot Gaussian line measurement (the per-line fit).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "quality",
      "category_title": "Quality / SNR",
      "summary": "Resolving power R = λ/FWHM from Gaussian fits of the strongest isolated lamp / sky lines.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/quality/measure_resolving_power/"
    },
    {
      "name": "merge_echelle_orders",
      "description": "Merge every Spectrum1D in ``ctx.spectra`` into a single ``ctx.spectrum``.\n\nUse after ``read_echelle_fits``. The output is a 1D spectrum with a log-uniformly-sampled wavelength axis ready for analysis algorithms (``snr_der``, ``detect_lines``, …) — most of which expect a single Spectrum1D, not a list of orders.\n\nParameters — pass as the 'params' object:\n- n_grid_per_order (default 4000): Number of log-λ samples to allocate per order on the merged grid. Higher = finer output but slower.\n- weighting (default 'uncertainty'): How to combine overlapping pixels: 'uncertainty' (1/σ², requires Spectrum1D.uncertainty), 'mean' (plain average), or 'first' (keep the first order in declared sequence).\n\nRequires in the session context: spectra\n\nBackend: numpy.\nReferences: Tody 1993, ASP Conf. Ser. 52, 173 — IRAF scombine (echelle package): orders interpolated to a common dispersion and combined by (weighted) average in the overlap regions.; Horne 1986, PASP 98, 609 — inverse-variance (1/σ²) weighting of independent estimates of the same flux.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "stacking",
      "category_title": "Stacking",
      "summary": "Merge every Spectrum1D in ``ctx.spectra`` into a single ``ctx.spectrum``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/stacking/merge_echelle_orders/"
    },
    {
      "name": "normalize_edges",
      "description": "Normalise a spectrum using a continuum fitted only on its line-free edges.\n\nPolynomial of degree `order` fitted through the outer `edge_fraction` of samples at each end, evaluated over the full axis and divided out. Non-finite (NaN/inf) edge samples are masked before the fit ; the brick fails when fewer than order + 2 finite edge samples remain. v2.0.0 — uncertainty (rule in place since 1.0.0, now documented ; no numerical change) : σ_out = σ_in / |continuum| like the flux — the edge-fitted continuum is treated as noise-free (its extrapolation error across the window is not propagated) ; samples where it is 0 get a NaN σ. v3.0.0: masked samples (Spectrum1D.mask) are excluded from the edge fit like non-finite ones ; the output keeps their flux, divided like every other sample, and carries the mask through.\n\nParameters — pass as the 'params' object:\n- edge_fraction (default 0.15): Fraction of the spectrum, at each end, used to fit the continuum.\n- order (default 1): Polynomial order of the edge continuum fit (1 = linear).\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — the IRAF data reduction and analysis system (continuum fitting with sample windows, onedspec.continuum).; Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 12, continuum placement for line measurement.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "continuum",
      "category_title": "Continuum",
      "summary": "Normalise a spectrum using a continuum fitted only on its line-free edges.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/continuum/normalize_edges/"
    },
    {
      "name": "normalize_max",
      "description": "Normalise a spectrum by dividing the flux by its maximum value.\n\nout = flux / nanmax(flux). v2.0.0 — uncertainty (rule in place since 1.0.0, now documented ; no numerical change) : σ_out = σ_in / |max_flux| — the peak is treated as a noise-free scale factor (a noisy peak biases the normalisation high ; clip cosmics or smooth first). v3.0.0: masked samples (Spectrum1D.mask) are ignored when locating the maximum ; the output keeps their flux, divided like every other sample, and carries the mask through.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 12, continuum placement (the peak flux as the crudest continuum proxy).; Tody 1986, Proc. SPIE 627, 733 — the IRAF data reduction and analysis system (reference implementation of spectrum normalisation tasks).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "continuum",
      "category_title": "Continuum",
      "summary": "Normalise a spectrum by dividing the flux by its maximum value.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/continuum/normalize_max/"
    },
    {
      "name": "normalize_percentile",
      "description": "Normalise a spectrum by dividing the flux by a high percentile of itself.\n\nout = flux / nanpercentile(flux, percentile). v2.0.0 — uncertainty (rule in place since 1.0.0, now documented ; no numerical change) : σ_out = σ_in / |continuum_level| — the percentile level is treated as a noise-free scale factor. v3.0.0: masked samples (Spectrum1D.mask) are ignored by the percentile ; the output keeps their flux, divided like every other sample, and carries the mask through.\n\nParameters — pass as the 'params' object:\n- percentile (default 95.0): Flux percentile (0-100) taken as the continuum level.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 12, continuum placement through the highest flux points of an absorption-line spectrum.; Tody 1986, Proc. SPIE 627, 733 — the IRAF data reduction and analysis system (reference implementation of spectrum normalisation tasks).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "continuum",
      "category_title": "Continuum",
      "summary": "Normalise a spectrum by dividing the flux by a high percentile of itself.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/continuum/normalize_percentile/"
    },
    {
      "name": "normalize_polynomial",
      "description": "Normalise the continuum to unity with a sigma-clipped polynomial fit.\n\nUse a low order (2-4) for a slowly varying continuum; higher orders risk absorbing real spectral features. For absorption-line-rich sources set sigma_low=1.5, sigma_high=3. v2.0.0 — uncertainty (rule in place since 1.0.0, now documented ; no numerical change) : σ_out = σ_in / |continuum| — the fitted continuum is treated as noise-free (its own fit error, small for a low-order polynomial over many samples, is not propagated) ; samples where the continuum is 0 get a NaN σ like the flux. v3.0.0: masked samples (Spectrum1D.mask) are ignored by the fit and by normalized_rms ; the output keeps their flux, divided by the continuum like every other sample, and carries the mask through.\n\nParameters — pass as the 'params' object:\n- order (default 3): Polynomial degree of the continuum fit.\n- sigma_clip (default 3.0): Symmetric clip threshold (default 3.0). Used as a fallback for sigma_low / sigma_high when those are left at None.\n- sigma_low (default None): Asymmetric clip threshold on the LOW side (rejects absorption lines). None ⇒ falls back to sigma_clip.\n- sigma_high (default None): Asymmetric clip threshold on the HIGH side (rejects emission lines / cosmics). None ⇒ falls back to sigma_clip.\n- iterations (default 3): Number of sigma-clipping iterations.\n- windows (default None): Optional list of (λ_lo, λ_hi) wavelength intervals ; when given, the polynomial is fitted only on samples inside at least one window (IRAF 'sample' parameter).\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — IRAF continuum task (low_reject / high_reject / sample heritage).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "continuum",
      "category_title": "Continuum",
      "summary": "Normalise the continuum to unity with a sigma-clipped polynomial fit.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/continuum/normalize_polynomial/"
    },
    {
      "name": "normalize_spline",
      "description": "Normalise the continuum with an IRAF-style iteratively clipped cubic spline.\n\nLeast-squares cubic spline (IRAF function=spline3) with n_knots equally spaced interior knots (IRAF order = n_knots + 1 pieces), refitted up to niterate times after rejecting samples below −low_reject·σ or above +high_reject·σ of the residual (σ over the kept samples) plus grow neighbours on each side ; a threshold ≤ 0 disables that side, as in IRAF. Stops early when no new sample is rejected. Knots left without data by the rejection are dropped. Writes ctx.spectrum = F / S(λ) (flux_unit 'normalized'), the uncertainty as σ / |S(λ)|, meta['continuum_method'] = 'spline3', extras['continuum_spline'] = {'continuum', 'knots', 'kept'} with the continuum on the input axis, and metrics continuum_rms (RMS of F/S − 1 over the kept samples), n_rejected, n_iterations and continuum_median. Keep n_knots small (5–15 across an optical spectrum) so broad features (Balmer wings, molecular bands) are treated as lines, not continuum ; increase it only to follow a blaze ripple. Compared with normalize_polynomial (one global polynomial, same asymmetric clip), the spline is more flexible — better on wavy responses, more prone to bending into wide lines. Descending wavelength axes are sorted internally ; duplicate wavelengths are fitted once. Fails when the finite samples cannot constrain the requested spline. v2.0.0: masked samples (Spectrum1D.mask) are excluded from the fit and from continuum_rms / n_rejected like non-finite ones ; the output keeps their flux, divided like every other sample, and carries the mask through.\n\nParameters — pass as the 'params' object:\n- n_knots (default 10): Number of equally spaced interior knots (IRAF spline3 order − 1).\n- low_reject (default 2.0): Rejection threshold below the fit, in σ (IRAF low_reject) ; ≤ 0 disables.\n- high_reject (default 3.0): Rejection threshold above the fit, in σ (IRAF high_reject) ; ≤ 0 disables.\n- niterate (default 10): Maximum number of fit-reject iterations (IRAF niterate).\n- grow (default 0): Number of neighbouring pixels rejected on each side of a rejected one (IRAF grow).\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — the IRAF data reduction and analysis system.; Tody 1993, ASP Conf. Ser. 52, 173 — IRAF in the nineties ; onedspec.continuum (function=spline3, order, low_reject, high_reject, niterate, grow).; scipy.interpolate.LSQUnivariateSpline — least-squares spline engine.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "continuum",
      "category_title": "Continuum",
      "summary": "Normalise the continuum with an IRAF-style iteratively clipped cubic spline.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/continuum/normalize_spline/"
    },
    {
      "name": "normalize_to_region",
      "description": "Divide the flux by its NaN-safe mean over ``[wave_lo, wave_hi]``.\n\nout = flux / nanmean(flux[wave_lo:wave_hi]). The window is closed at both ends and clamped to the spectrum's wavelength range. v2.0.0 — uncertainty (rule in place since 1.0.0, now documented ; no numerical change) : σ_out = σ_in / |region_mean| — the region mean is treated as noise-free (its standard error, σ/√N over the window, is not propagated). If the region mean is zero or non-finite the algorithm fails with a clear message rather than emitting NaN. v3.0.0: masked samples (Spectrum1D.mask) are ignored by the region mean and n_samples counts the usable samples ; the output keeps their flux, divided like every other sample, and carries the mask through.\n\nParameters — pass as the 'params' object:\n- wave_lo (default 0.0) [required]: Lower bound of the reference window (same unit as the wavelength axis).\n- wave_hi (default 0.0) [required]: Upper bound of the reference window.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — IRAF continuum heritage.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "continuum",
      "category_title": "Continuum",
      "summary": "Divide the flux by its NaN-safe mean over ``[wave_lo, wave_hi]``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/continuum/normalize_to_region/"
    },
    {
      "name": "oiii_electron_temperature",
      "description": "Electron temperature Te (K) from the [O III] (λ4959+λ5007)/λ4363 ratio.\n\nF = |amp| · |σ| · √(2π) per line (shared Gaussian-line primitive, default window ±8.0 Å). metrics['ratio'] (= 'oiii_ratio' = 'oiii_ratio_doublet') is the doublet ratio [F(4959)+F(5007)]/F(4363); the v1 single-line ratio is kept as metrics['ratio_5007_4363']. Returns NaN te_kelvin when ratio ≤ 7.90 (log argument non-positive) or Te ∉ [3000, 30000] K. The validity regime is exposed in extras['regime'] (and the prefixed alias extras['oiii_regime']) so downstream code can treat it explicitly. v2.0.0: the 7.90 prefactor now multiplies the (4959+5007)/4363 doublet ratio as in Osterbrock & Ferland 2006 Eq. 5.4 — v1.x applied it to F(5007)/F(4363) alone, which biased Te high (R5007 = 100 : 12961 K → 11635 K ; R5007 = 50 : 17830 K → 15413 K). λ4959 is now fitted (new param lambda_4959) and falls back to F(5007)·(1+1/2.98) when unmeasurable ; the path is recorded in extras['oiii_doublet_source']. Prefixed keys oiii_* were added alongside the generic ratio/regime/notes/method keys. v3.0.0: masked samples (Spectrum1D.mask) are ignored — excluded from every line window like non-finite ones.\n\nParameters — pass as the 'params' object:\n- lambda_5007 (default 5006.84): Rest wavelength (Å) of [O III] 5007 (default: NIST air).\n- lambda_4959 (default 4958.911): Rest wavelength (Å) of [O III] 4959 (default: NIST air). Fitted when in range; otherwise F(4959) = F(5007)/2.98 is assumed.\n- lambda_4363 (default 4363.21): Rest wavelength (Å) of [O III] 4363 (default: NIST air).\n- window (default 8.0): Half-width (Å) of the fit window around each line.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Osterbrock & Ferland 2006, Astrophysics of Gaseous Nebulae and AGN, 2nd ed., University Science Books — Eq. 5.4, (4959+5007)/4363 = 7.90·exp(32900/T).; Aller 1984, Physics of Thermal Gaseous Nebulae, Reidel — Eq. 5-3 (5-level atom inversion).; Storey & Zeippen 2000, MNRAS 312, 813 — theoretical [O III] 5007/4959 ratio 2.98 (doublet reconstruction when 4959 is absent).; Kramida et al., NIST ASD — air rest wavelengths λ5006.84, λ4958.911, λ4363.21.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "nebular",
      "category_title": "Nebular diagnostics",
      "summary": "Electron temperature Te (K) from the [O III] (λ4959+λ5007)/λ4363 ratio.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/nebular/oiii_electron_temperature/"
    },
    {
      "name": "outlier_rejection_mad_adaptive",
      "description": "Replace pixels whose deviation from a local median exceeds ``threshold·MAD``.\n\nVectorised with numpy.lib.stride_tricks.sliding_window_view, so the cost is one O(kernel · pixels) sort. Pixels within the band are replaced by the local median if their deviation exceeds threshold × MAD. The half-pixel border on each side is left untouched (window does not fit there). v1.0.1: rows are processed in chunks so the transient window copies stay around 64 MB regardless of frame size (a 4k² frame with k=5 needed ~6 GB before) — identical output.\n\nParameters — pass as the 'params' object:\n- kernel_size (default 3): Square neighbourhood size, odd integer ≥ 3.\n- threshold (default 3.0): Multiplier on the local MAD above which a pixel is replaced.\n- row_lo (default 0): First row (inclusive) where the filter applies; 0 = top.\n- row_hi (default 0): Last row (exclusive); 0 ⇒ bottom of the image.\n\nRequires in the session context: image\n\nBackend: numpy.\nReferences: Hwang & Haddad 1995, IEEE Trans. Image Processing 4(4):499 — adaptive median filter for impulsive noise.; Hoaglin, Mosteller & Tukey 1983, Understanding Robust and Exploratory Data Analysis — Median Absolute Deviation properties.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Replace pixels whose deviation from a local median exceeds ``threshold·MAD``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/outlier_rejection_mad_adaptive/"
    },
    {
      "name": "phase_dispersion_minimization",
      "description": "Stellingwerf (1978) phase dispersion minimization: Θ = s²/σ² over a period grid.\n\nThe light curve is folded on every trial period of a grid uniform in frequency between 1/period_max and 1/period_min (phase zero at the first observation). Phases are split into n_bins equal bins, repeated n_covers times with a phase offset of 1/(n_bins·n_covers) between sequences — Stellingwerf's (N_b, N_c) structure; (10, 3) and (5, 2) are the usual choices. Θ = s²/σ² compares the pooled within-bin variance to the total variance: ≈ 1 when the fold is wrong, ≪ 1 at the true period. Point uncertainties are not used (the 1978 statistic is unweighted). Regime: period_max should stay well below the time baseline (Θ is meaningless beyond one cycle) and period_min above twice the typical cadence; at least 2·n_bins points are required. Integer multiples of the true period also produce low Θ (a fold on 2P is still coherent) — inspect the Θ curve, and prefer the shortest period among near-equal minima. No significance level is attached because Stellingwerf's F-test is known to be incorrect (Schwarzenberg-Czerny 1997). Metric names assume times in days.\n\nParameters — pass as the 'params' object:\n- time (default None): List of observation times; omit to use a light curve from the context.\n- flux (default None): List of flux values, paired with 'time'.\n- uncertainty (default None): Optional list of per-point flux uncertainties.\n- light_curve_key (default None): Key in ctx.light_curves to use when time/flux are omitted.\n- period_min (default None): Shortest trial period (time units of the curve); null = twice the median cadence.\n- period_max (default None): Longest trial period; null = half the time baseline.\n- n_periods (default 2000): Number of trial periods, spaced uniformly in frequency.\n- n_bins (default 10): N_b — number of phase bins per sequence (Stellingwerf 1978).\n- n_covers (default 3): N_c — number of offset bin sequences ('covers'); 1 = plain non-overlapping bins.\n- output_key (default 'pdm'): Key under which the Θ curve is stored in ctx.periodograms.\n\nBackend: numpy.\nReferences: Stellingwerf 1978, ApJ 224, 953 — the PDM statistic Θ = s²/σ² with σ² = Σ(x−x̄)²/(N−1) and s² = Σ_j (n_j−1) s_j² / (Σ_j n_j − M) over M overlapping phase samples (N_b bins × N_c covers); Θ ≈ 1 for a wrong period, minimum at the true one.; Schwarzenberg-Czerny 1997, ApJ 489, 941 — the PDM statistic follows a (incomplete) beta distribution, not the F distribution of the 1978 paper; hence no false-alarm probability is reported here.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "timeseries",
      "category_title": "Time series",
      "summary": "Stellingwerf (1978) phase dispersion minimization: Θ = s²/σ² over a period grid.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/timeseries/phase_dispersion_minimization/"
    },
    {
      "name": "phase_fold",
      "description": "Phase-fold a light curve on a known period.\n\nParameters — pass as the 'params' object:\n- period (default None) [required]: Folding period, in the light curve's time unit (required).\n- epoch (default 0.0): Reference time mapped to phase 0.\n- light_curve_key (default None): Key in ctx.light_curves to fold; null uses the first one.\n\nRequires in the session context: light_curves\n\nBackend: numpy.\nReferences: Stellingwerf 1978, ApJ 224, 953 — phase dispersion minimization ; defines the phase-folding convention φ = ((t − t₀)/P) mod 1 used here.; Lafler & Kinman 1965, ApJS 11, 216 — period search by folding the light curve on trial periods.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "timeseries",
      "category_title": "Time series",
      "summary": "Phase-fold a light curve on a known period.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/timeseries/phase_fold/"
    },
    {
      "name": "plot_3d_surface_plotly",
      "description": "Render every spectrum in ``ctx.spectra`` as one row of a 3D surface.\n\nParameters — pass as the 'params' object:\n- title (default 'stacked spectra — 3D surface'): Figure title.\n- n_grid (default 512): Number of wavelength samples on the common grid.\n- colormap (default 'Viridis'): Plotly colour scale name.\n\nRequires in the session context: spectra\n\nBackend: plotly.\nReferences: Plotly — https://plotly.com/python/3d-surface-plots/",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "visualization",
      "category_title": "Visualisation",
      "summary": "Render every spectrum in ``ctx.spectra`` as one row of a 3D surface.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/visualization/plot_3d_surface_plotly/"
    },
    {
      "name": "plot_animation_plotly",
      "description": "Render an animated Plotly figure that plays through ``ctx.spectra``.\n\nParameters — pass as the 'params' object:\n- title (default 'stacked spectra — animation'): Figure title.\n- frame_duration_ms (default 600): Milliseconds each frame is displayed.\n\nRequires in the session context: spectra\n\nBackend: plotly.\nReferences: Plotly — https://plotly.com/python/animations/",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "visualization",
      "category_title": "Visualisation",
      "summary": "Render an animated Plotly figure that plays through ``ctx.spectra``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/visualization/plot_animation_plotly/"
    },
    {
      "name": "plot_dynamic_spectrum",
      "description": "Render every spectrum in ``ctx.spectra`` as one row of a 2D heatmap.\n\nAll spectra are resampled to a common wavelength grid (linear, taking the intersection of their ranges) before stacking. Use this together with phase_fold or lomb_scargle for variable-star and exoplanet work.\n\nParameters — pass as the 'params' object:\n- title (default 'dynamic spectrum'): Figure title.\n- n_grid (default 1024): Number of wavelength samples on the common grid.\n- colormap (default 'Viridis'): Plotly colour scale name (Viridis, Cividis, Plasma…).\n\nRequires in the session context: spectra\n\nBackend: plotly.\nReferences: Plotly — open-source graphing library, https://plotly.com/python/",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "visualization",
      "category_title": "Visualisation",
      "summary": "Render every spectrum in ``ctx.spectra`` as one row of a 2D heatmap.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/visualization/plot_dynamic_spectrum/"
    },
    {
      "name": "plot_overlay_plotly",
      "description": "Render every spectrum in ``ctx.spectra`` overlaid on one Plotly figure.\n\nParameters — pass as the 'params' object:\n- title (default 'spectra overlay'): Figure title.\n\nRequires in the session context: spectra\n\nBackend: plotly.\nReferences: Plotly — open-source graphing library, https://plotly.com/python/",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "visualization",
      "category_title": "Visualisation",
      "summary": "Render every spectrum in ``ctx.spectra`` overlaid on one Plotly figure.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/visualization/plot_overlay_plotly/"
    },
    {
      "name": "plot_spectrum_plotly",
      "description": "Render ``ctx.spectrum`` as a Plotly line figure.\n\nParameters — pass as the 'params' object:\n- title (default ''): Figure title; empty uses the spectrum's object name.\n- show_uncertainty (default True): Draw the uncertainty band when the spectrum has one.\n\nRequires in the session context: spectrum\n\nBackend: plotly.\nReferences: Plotly — open-source graphing library, https://plotly.com/python/",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "visualization",
      "category_title": "Visualisation",
      "summary": "Render ``ctx.spectrum`` as a Plotly line figure.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/visualization/plot_spectrum_plotly/"
    },
    {
      "name": "read_ascii_spectrum",
      "description": "Read a 1D spectrum from a two- or three-column text file.\n\nParameters — pass as the 'params' object:\n- path (default None) [required]: Local path of the text file to read.\n- delimiter (default None): Column delimiter; null auto-detects comma/semicolon/tab/whitespace.\n- flux_unit (default 'ADU'): Label recorded as the spectrum's flux unit.\n\nBackend: numpy.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "io",
      "category_title": "Input / output",
      "summary": "Read a 1D spectrum from a two- or three-column text file.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/io/read_ascii_spectrum/"
    },
    {
      "name": "read_echelle_fits",
      "description": "Read a multi-order échelle FITS file into ``ctx.spectra``.\n\nEach order is loaded with full sample count; no resampling happens here. Set ``flux_column`` and ``wavelength_column`` to override the column-name auto-detection when needed.\n\nParameters — pass as the 'params' object:\n- path (default None) [required]: FITS file path (local).\n- flux_column (default None): Name of the flux column for the table layout (auto-detected when null).\n- wavelength_column (default None): Name of the wavelength column (auto-detected when null).\n- wavelength_unit (default 'Angstrom'): Unit label written on each Spectrum1D.\n\nBackend: astropy.\nReferences: astropy.io.fits.; Common amateur échelle FITS layouts (eShel, Lhires-III echelle reductions).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "io",
      "category_title": "Input / output",
      "summary": "Read a multi-order échelle FITS file into ``ctx.spectra``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/io/read_echelle_fits/"
    },
    {
      "name": "read_fits",
      "description": "Read a 1D spectrum from a FITS file (local path or http(s) URL).\n\nLinear WCS: λ = CRVAL1 + (p − CRPIX1)·step with 1-based CRPIX1 and step = CD1_1 when present, else CDELT1 (× PC1_1). Logarithmic axes: IRAF DC-FLAG = 1 means CRVAL1/CDELT1 are log10(λ) (axis = 10**(…)); a CTYPE1 ending in '-LOG' (WAVE-LOG, AWAV-LOG) is a WCS Paper III logarithmic axis, λ = CRVAL1·exp((p − CRPIX1)·step / CRVAL1). CUNIT1 (or IRAF WAT1 'units=') in nm / µm / m is converted to Å and the original label kept in meta['wavelength_unit_original']. v3.0.0 (contract change, cf. audit C6/C7): (1) CD1_1 now wins over CDELT1 when both exist (v2 preferred CDELT1; a file with CDELT1=1, CD1_1=2 read step 1, now 2, matching astropy.wcs); (2) a header with no CRVAL1/CDELT1/CD1_1 gives the 0-based pixel index with wavelength_unit='pixel' and meta['wcs']='none' (v2 labelled the same numbers 'Angstrom'); (3) a genuine 2-D image is refused unless row=<index> is passed (v2 silently returned row 0); length-1 axes are still squeezed; IRAF multispec/échelle markers redirect to read_echelle_fits; (4) a data-bearing PRIMARY now wins over table extensions (v2 preferred any table); hdu=<index|EXTNAME> overrides; (5) ERROR/ERR/ERRS/SIGMA/UNCERT/UNCERTAINTY (1σ) and IVAR (1/√ivar) image extensions and err/error/ivar/sigma/uncertainty/flux_error table columns fill Spectrum1D.uncertainty; MASK extensions and mask/and_mask columns fill Spectrum1D.mask; SDSS loglam columns are decoded as 10**loglam; (6) a descending axis is kept as stored and flagged meta['wavelength_descending']=True; (7) an unrecognised CUNIT1 (e.g. 'km/s') is kept verbatim as wavelength_unit instead of being relabelled 'Angstrom'. meta['source'], meta['hdu'], meta['hdu_name'], meta['layout'] and meta['wcs'] document the provenance; the selected HDU's header is kept in Spectrum1D.header. The '-LOG' decoding (v2.0.0) and the IRAF DC-FLAG branch are unchanged.\n\nParameters — pass as the 'params' object:\n- path (default None) [required]: Local path or http(s) URL of the FITS file to read.\n- hdu (default None): Extension to read: null = automatic (data-bearing PRIMARY, else the first table with wavelength/flux columns, else the first image extension); an integer index or an EXTNAME string forces one.\n- row (default None): 0-based row to read when the data is a 2-D image (or a multi-row table of array cells). null squeezes length-1 axes only and fails on a genuine 2-D image instead of guessing.\n\nBackend: astropy.\nReferences: FITS Standard 4.0 — Pence et al. 2010, A&A 524, A42; astropy.io.fits — Astropy Collaboration 2022, ApJ 935, 167; Greisen & Calabretta 2002, A&A 395, 1061 — WCS Paper I (CDi_j takes precedence over CDELTi / PCi_j; CRPIXi is 1-based).; Greisen, Calabretta, Valdes & Allen 2006, A&A 446, 747 — WCS Paper III (spectral coordinates; '-LOG' axes).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "io",
      "category_title": "Input / output",
      "summary": "Read a 1D spectrum from a FITS file (local path or http(s) URL).",
      "docs_url": "https://docs.spectrokernel.io/algorithms/io/read_fits/"
    },
    {
      "name": "read_sdss_spectrum",
      "description": "Read an SDSS-format spectrum (``spec-*.fits``) into ``ctx.spectrum``.\n\nReading ``loglam``-based spectra is the one big gap of the generic FITS reader — this algorithm closes it. Compatible with SDSS DR9 through the current DR (the ``COADD`` HDU layout has been stable since DR9).\n\nParameters — pass as the 'params' object:\n- path (default None) [required]: Local path or http(s) URL of an SDSS spec-*.fits file.\n\nBackend: astropy.\nReferences: York et al. 2000, AJ, 120, 1579 — Sloan Digital Sky Survey overview.; Smee et al. 2013, AJ, 146, 32 — SDSS BOSS spectrograph and data format.; Bolton et al. 2012, AJ, 144, 144 — DR9 spectroscopic data release documenting the spec-*.fits layout.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "io",
      "category_title": "Input / output",
      "summary": "Read an SDSS-format spectrum (``spec-*.fits``) into ``ctx.spectrum``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/io/read_sdss_spectrum/"
    },
    {
      "name": "read_votable_spectrum",
      "description": "Read a 1D spectrum from a VOTable file (the IVOA Virtual Observatory format).\n\nParameters — pass as the 'params' object:\n- path (default None) [required]: Local path of the VOTable file to read.\n\nBackend: astropy.\nReferences: IVOA VOTable 1.4 — Ochsenbein et al. 2019; astropy.io.votable",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "io",
      "category_title": "Input / output",
      "summary": "Read a 1D spectrum from a VOTable file (the IVOA Virtual Observatory format).",
      "docs_url": "https://docs.spectrokernel.io/algorithms/io/read_votable_spectrum/"
    },
    {
      "name": "redshift_lines",
      "description": "Redshift z by per-line Gaussian fits against a list of rest-frame anchors.\n\nPer-line Gaussian+constant fit in a window centred on the seeded λ_obs = λ_rest · (1 + z_guess); both emission and absorption amplitudes are tried, the better residual wins. Reported z is the median over surviving anchors ; z_error is the POPULATION standard deviation (np.std, ddof=0) of the per-line z values — a spread, not the error of the median — and both are rounded to 5 decimals (contract). Refuses to commit with < 2 matched lines (no spread). Non-finite samples are dropped and a descending wavelength axis is sorted before fitting (v1.0.1). v2.0.0: masked samples (Spectrum1D.mask) are ignored — dropped like non-finite ones.\n\nParameters — pass as the 'params' object:\n- z_guess (default 0.0) [required]: Seed redshift (within ±z_search_width of the true value).\n- z_search_width (default 0.05): Reject |z_est − z_guess| > this (z-units). Keeps catastrophic mis-identifications out of the median.\n- fit_window (default 15.0): Half-window (Å) for each per-line Gaussian fit.\n- anchor_lines (default [['[O II] 3727', 3727.42], ['Ca II K', 3933.66], ['Ca II H', 3968.47], ['Hβ', 4861.33], ['[O III] 5007', 5006.84], ['Mg b', 5175.3], ['Na D', 5895.92], ['Hα', 6562.82], ['[N II] 6583', 6583.45]]): List of (name, λ_rest_aa) pairs to fit. Default = 9 strong SDSS-spec1d anchors (override for AGN templates etc.).\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Stoughton et al. 2002, AJ 123, 485 — SDSS spec1d redshift pipeline (anchor-line seed approach).; Bolton et al. 2012, AJ 144, 144 — BOSS spec1d (template-based refinement on top of an anchor-line seed).; Kramida et al., NIST ASD — air rest wavelengths of the anchor lines.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "radial_velocity",
      "category_title": "Radial velocity",
      "summary": "Redshift z by per-line Gaussian fits against a list of rest-frame anchors.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/radial_velocity/redshift_lines/"
    },
    {
      "name": "reidentify_arc_features",
      "description": "Re-anchor stored (pixel, λ) arc features onto a fresh arc exposure.\n\nThe complement of match_lamp_lines: that brick identifies lines against the bundled NIST atlas from scratch (IRAF identify/autoidentify); this one re-anchors a previously stored, instrument-specific feature list (IRAF reidentify). The stored wavelengths are an empirical description of one instrument unit — at low resolution blends are stable barycentres, not atlas lines — and are therefore conserved, never re-matched. Peaks are detected with the same DER_SNR-scaled prominence recipe as match_lamp_lines; each reference takes the nearest peak within search_window_px, centroids are refined to sub-pixel by a Gaussian+constant fit (refine=true), and the run fails explicitly when fewer than min_fraction of the references are recovered. Output: ctx.extras[output_key] = {'pixel_positions': [...], 'wavelengths_angstrom': [...]}.\n\nParameters — pass as the 'params' object:\n- reference_pixels (default None) [required]: Stored feature positions (pixels) from the one-off identification of this instrument set-up (REQUIRED).\n- reference_wavelengths (default None) [required]: Wavelengths (Å) paired with reference_pixels (REQUIRED). Conserved verbatim — this is an empirical feature list, not a physical atlas.\n- search_window_px (default 8.0): Half-width (pixels) of the search window around each reference. Bounds the drift the brick will absorb; a shift beyond it is a failure, not a guess.\n- min_fraction (default 0.7): Minimum fraction of references that must be recovered; below it the run fails explicitly (weak arc or abnormal shift → back to assisted identification).\n- refine (default True): Refine each matched peak to sub-pixel with a Gaussian+constant centroid fit.\n- detection_sigma (default 5.0): Peak prominence threshold as a multiple of the DER_SNR-style noise (same recipe as match_lamp_lines).\n- min_prominence (default 0.01): Floor on the prominence as a fraction of the flux amplitude.\n- min_distance_px (default 5): Minimum spacing between detected peaks (pixels).\n- source_key (default 'lamp_spectrum'): ctx.extras key holding the arc Spectrum1D (pixel axis).\n- output_key (default 'lamp_identifications'): ctx.extras key receiving the identifications dict {'pixel_positions': [...], 'wavelengths_angstrom': [...]}.\n\nBackend: scipy.\nReferences: Tody 1986, SPIE 627, 733 — IRAF Data Reduction and Analysis System.; Tody 1993, ASP Conf. Ser. 52, 173 — IRAF identify/autoidentify.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "wavelength_calibration",
      "category_title": "Wavelength calibration",
      "summary": "Re-anchor stored (pixel, λ) arc features onto a fresh arc exposure.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/wavelength_calibration/reidentify_arc_features/"
    },
    {
      "name": "remove_telluric_division",
      "description": "Remove telluric absorption by dividing the science spectrum by a reference.\n\nProvide the telluric template either via `template_path` (a FITS file) or by storing a Spectrum1D in `ctx.extras['telluric_spectrum']`. Since v1.1.0, when template_key is left at its default 'telluric_spectrum' and that key is absent, the brick falls back to ctx.extras['telluric_template_scaled'] (written by fit_telluric_scaling) and then ctx.extras['telluric_template'] (written by synth_telluric), so the chain synth_telluric → fit_telluric_scaling → remove_telluric_division works with all defaults. The key actually used is recorded in meta['telluric_template_key']. The template's wavelength axis may be in any order (sorted internally).\n\nParameters — pass as the 'params' object:\n- template_path (default None): Path / URL to the telluric standard FITS spectrum.\n- template_key (default 'telluric_spectrum'): Key into ctx.extras holding a Spectrum1D template. With the default 'telluric_spectrum' absent, 'telluric_template_scaled' then 'telluric_template' are tried.\n- min_transmission (default 0.05): Below this normalised value the spectrum is masked, not divided.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Vacca, Cushing & Rayner 2003, PASP 115, 389 — telluric correction methodology",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Remove telluric absorption by dividing the science spectrum by a reference.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/remove_telluric_division/"
    },
    {
      "name": "resample_flux_conserving",
      "description": "Resample onto a new wavelength grid while preserving integrated flux.\n\nSpecify the new grid either with a fixed step (Å) or with a fixed number of points. Samples outside the input wavelength range come back as NaN. The flux_unit label is only used to build the specutils object: 'ADU' / 'adu' map to astropy's adu, 'counts' / 'count' to count, any label astropy does not recognise ('transmission', 'response', …) is treated as dimensionless, and the output keeps the input label verbatim (v1.0.1 — v1.0.0 failed for every unrecognised label, including the kernel default 'ADU').\n\nParameters — pass as the 'params' object:\n- wavelength_min (default None): Lower bound of the new grid (Å); null keeps the input minimum.\n- wavelength_max (default None): Upper bound of the new grid (Å); null keeps the input maximum.\n- step (default None): Constant sampling step (Å). Mutually exclusive with n_points.\n- n_points (default None): Number of samples in the new grid. Mutually exclusive with step.\n\nRequires in the session context: spectrum\n\nBackend: specutils.\nReferences: specutils.manipulation.FluxConservingResampler; Carnall 2017, arXiv:1705.05165 — SpectRes (the conservation idea)",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "resampling",
      "category_title": "Resampling",
      "summary": "Resample onto a new wavelength grid while preserving integrated flux.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/resampling/resample_flux_conserving/"
    },
    {
      "name": "resample_linear",
      "description": "Resample a spectrum onto a uniform wavelength grid by linear interpolation.\n\nLinear interpolation is fast and adequate when the target sampling is similar to the source. For large changes in resolution prefer a flux-conserving method. v2.0.0 — uncertainty propagation : σ_out = sqrt(interp(σ²)), the linear interpolation of the VARIANCE onto the new grid (v1.x interpolated σ itself, which is not a propagation rule). For an output sample at fraction t between two inputs the exact variance of the interpolant is (1−t)² σ_a² + t² σ_b² ≤ (1−t) σ_a² + t σ_b², so the per-sample value is conservative ; the covariance between neighbouring output samples (they share input samples, and oversampling duplicates information) is NOT tracked, which under-states the errors of anything fitted to the resampled spectrum. Without an input uncertainty the brick is unchanged.\n\nParameters — pass as the 'params' object:\n- wavelength_min (default None): Lower bound of the new grid (Å); null keeps the input minimum.\n- wavelength_max (default None): Upper bound of the new grid (Å); null keeps the input maximum.\n- step (default None): Constant sampling step (Å). Mutually exclusive with n_points.\n- n_points (default None): Number of samples in the new grid. Mutually exclusive with step.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: numpy.interp — piecewise-linear interpolation; For large resolution changes, prefer flux-conserving resampling (specutils.manipulation.FluxConservingResampler); Bevington & Robinson 2003, Data Reduction and Error Analysis for the Physical Sciences, 3rd ed., McGraw-Hill — §3.2, variance of a linear combination.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "resampling",
      "category_title": "Resampling",
      "summary": "Resample a spectrum onto a uniform wavelength grid by linear interpolation.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/resampling/resample_linear/"
    },
    {
      "name": "response_from_standard",
      "description": "Derive the instrumental response curve from a standard-star observation.\n\nComputes observed / catalog (after resampling the catalogue to the observed grid), masks Balmer + telluric windows, σ-clips, fits a low-frequency spline (default) or polynomial. spline_knots does NOT place a fixed number of knots: it sets the scipy UnivariateSpline smoothing factor s = var(ratio)·N / spline_knots (N = samples kept), which scipy converts into a knot count — larger spline_knots ⇒ smaller s ⇒ more knots ⇒ tighter fit (risks carving real features in), smaller ⇒ smoother. Sensible default : 20 for the full visible. No extinction / airmass step is performed here (correct upstream), and no exposure-time normalisation: the response absorbs the standard's exposure time unless the observed flux is already per second. Catalogue spectrum lives in ctx.extras['catalog_spectrum'], consistent with the second-spectrum idiom used by remove_telluric_division and combine_spectra_arithmetic.\n\nParameters — pass as the 'params' object:\n- fit (default 'spline'): 'spline' (default low-frequency UnivariateSpline) or 'polynomial'.\n- spline_knots (default 20): Smoothness control for fit='spline': sets the UnivariateSpline smoothing factor s = var(ratio)·N / spline_knots (roughly the number of knots scipy ends up placing). Larger ⇒ tighter fit (risks eating real features) ; smaller ⇒ smoother.\n- poly_order (default 5): Polynomial order when fit='polynomial'.\n- exclude_regions (default None): List of (wave_lo, wave_hi) windows (Å) to exclude from the fit. None ⇒ a default set covering Balmer + visible telluric bands.\n- sigma_clip (default 3.0): σ threshold for residual clipping during the fit (≥ 1).\n- max_iter (default 3): Maximum σ-clip iterations.\n- catalog_key (default 'catalog_spectrum'): ctx.extras key holding the catalogue Spectrum1D.\n- output_key (default 'reference_spectrum'): ctx.extras key receiving the fitted response curve.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Oke 1990, AJ 99, 1621 — flux calibration with secondary standards.; Bessell 1999, PASP 111, 1426 — UBVRI flux standards review.; Hamuy et al. 1992 PASP 104, 533 + 1994 PASP 106, 566 — Southern spectrophotometric standards.; Bohlin et al. 2014, PASP 126, 711 — CALSPEC HST standard stars.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "flux_calibration",
      "category_title": "Flux calibration",
      "summary": "Derive the instrumental response curve from a standard-star observation.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/flux_calibration/response_from_standard/"
    },
    {
      "name": "rotation_curve",
      "description": "Projected long-slit rotation curve v_los(r) from Hα per slit offset.\n\nPer-spectrum Hα Gaussian fit ⇒ v_los = c·(μ−λ_obs_rest)/λ_obs_rest with λ_obs_rest = λ_rest·(1+z_galaxy). Systemic velocity = median(v_los_i). Slit offsets are read from each spectrum's ``meta['slit_offset_arcsec']`` (preferred) or from params['slit_offsets']={index: arcsec}. Points sorted by offset.\n\nParameters — pass as the 'params' object:\n- z_galaxy (default 0.0): Galaxy redshift used to compute λ_obs_rest = λ_rest·(1+z).\n- lambda_rest (default 6562.82): Rest wavelength (Å) of the line fit per spectrum (default Hα).\n- fit_window (default 15.0): Half-window (Å) of the Hα Gaussian fit.\n- slit_offsets (default {}): Optional fallback mapping {spectrum_index: arcsec_offset} for spectra whose meta does not carry 'slit_offset_arcsec'.\n\nBackend: scipy.\nReferences: Rubin et al. 1980, ApJ 238, 471 — long-slit optical rotation curves of Sa-Sc spirals.; Sofue & Rubin 2001, ARA&A 39, 137 — disc rotation curves review.; Kramida et al., NIST ASD — Hα air rest wavelength 6562.82 Å.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "kinematics",
      "category_title": "Kinematics",
      "summary": "Projected long-slit rotation curve v_los(r) from Hα per slit offset.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/kinematics/rotation_curve/"
    },
    {
      "name": "rv_precision_bouchy",
      "description": "Compute the fundamental photon-noise limit on RV precision (Bouchy 2001).\n\nUses sigma_v = c / sqrt(sum_i ( (dF/d(lambda))_i * lambda_i / sigma_F_i )^2). When the spectrum's ``uncertainty`` array is not set, sigma_F is approximated from sqrt(|F|) (photon-noise assumption) scaled by the spectrum's median SNR. For an empirical companion measurement, run ``cross_correlate_rv`` and compare its ``radial_velocity_error_kms`` (Tonry & Davis r-value error) with this photon-noise floor — they bracket the realistic uncertainty budget. v2.0.0: masked samples (Spectrum1D.mask) are ignored — read as NaN, they drop out of the information sum together with the two neighbouring gradient terms.\n\nParameters — pass as the 'params' object:\n- snr_floor (default 50.0): Assumed SNR when no uncertainty array is set (photon-noise floor).\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Bouchy, Pepe & Queloz 2001, A&A 374, 733 — fundamental noise limits on RV.; Brault 1987, ARA&A 25, 575 — original derivation in the line-fitting context.; See also cross_correlate_rv for the empirical CCF-based RV error (Tonry & Davis 1979).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "radial_velocity",
      "category_title": "Radial velocity",
      "summary": "Compute the fundamental photon-noise limit on RV precision (Bouchy 2001).",
      "docs_url": "https://docs.spectrokernel.io/algorithms/radial_velocity/rv_precision_bouchy/"
    },
    {
      "name": "sii_electron_density",
      "description": "Electron density n_e (cm⁻³) from the [S II] λ6716/λ6731 ratio.\n\nF = |amp| · |σ| · √(2π) per line (shared Gaussian-line primitive). Default window ±5.0 Å so the 14.4 Å doublet stays resolved. The polynomial domain [0.45, 1.43] is enforced by clamp; the regime label records 'valid' / 'low-density' / 'high-density' / 'invalid'. v1.1.0: the generic keys metrics['ratio'] and extras['regime'|'notes'|'method'] are shared with oiii_electron_temperature and get overwritten in a pipeline ; the prefixed aliases metrics['sii_ratio'] and extras['sii_regime'|'sii_notes'|'sii_method'] carry the same values unambiguously. v2.0.0: masked samples (Spectrum1D.mask) are ignored — excluded from both line windows like non-finite ones.\n\nParameters — pass as the 'params' object:\n- lambda_6716 (default 6716.44): Rest wavelength (Å) of [S II] 6716 (default: NIST air).\n- lambda_6731 (default 6730.82): Rest wavelength (Å) of [S II] 6731 (default: NIST air).\n- window (default 5.0): Half-width (Å) of the fit window around each line.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Proxauf, Öttl & Kimeswenger 2014, A&A 561, A10 — improved n_e from nebular line ratios (Eq. 6, Table 2).; Osterbrock & Ferland 2006, Astrophysics of Gaseous Nebulae and AGN, 2nd ed., University Science Books — §5.4.; Kramida et al., NIST ASD — air rest wavelengths λ6716.44, λ6730.82.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "nebular",
      "category_title": "Nebular diagnostics",
      "summary": "Electron density n_e (cm⁻³) from the [S II] λ6716/λ6731 ratio.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/nebular/sii_electron_density/"
    },
    {
      "name": "simbad_query",
      "description": "Resolve an object name against SIMBAD and store the record in the context.\n\nRequires network access and the optional 'catalogs' extra. Results are not cached by the algorithm itself; wrap it in a caching layer for batch use.\n\nParameters — pass as the 'params' object:\n- object_name (default None) [required]: Object identifier to resolve (e.g. 'Vega').\n\nBackend: astroquery.\nReferences: Wenger et al. 2000, A&AS 143, 9 — the SIMBAD astronomical database; Ginsburg et al. 2019, AJ 157, 98 — astroquery",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "catalog",
      "category_title": "External catalogues",
      "summary": "Resolve an object name against SIMBAD and store the record in the context.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/catalog/simbad_query/"
    },
    {
      "name": "smooth_gaussian",
      "description": "Smooth a spectrum by convolution with a Gaussian kernel.\n\nNon-finite samples (NaN/inf) are bridged by linear interpolation over the finite neighbours before the convolution, so a single bad pixel no longer spreads NaN across the kernel footprint ; the original non-finite positions are restored to NaN in the output (finite inputs are processed bit-identically to v1.0.0). v2.0.0 — uncertainty propagation : when the input carries an uncertainty, σ_out,i = sqrt(Σ_k w_k² σ_{i+k}²) with the very kernel the filter applies (normalised Gaussian truncated at 4σ) ; for white noise of σ and sigma = 2 px this gives σ_out ≈ 0.376 σ (≈ σ / sqrt(2 σ_px √π)) in the interior. Within int(4σ + 0.5) samples of either end the filter's 'nearest' padding replicates the end sample, so the kernel taps falling off the array add before squaring — the propagation reproduces the filter's exact linear operator there too, and the edge σ_out is larger (0.641 σ at the very first sample for sigma = 2). Bridged (non-finite) samples contribute no variance and get a NaN uncertainty. The correlation the kernel introduces between neighbouring output samples is NOT tracked — χ² fits on a smoothed spectrum under-estimate their parameter errors. Without an input uncertainty the brick is unchanged from v1.0.1.\n\nParameters — pass as the 'params' object:\n- sigma (default 2.0): Standard deviation of the Gaussian kernel, in samples.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: scipy.ndimage.gaussian_filter1d; Bevington & Robinson 2003, Data Reduction and Error Analysis for the Physical Sciences, 3rd ed., McGraw-Hill — §3.2, error propagation through a linear combination (σ_out² = Σ w_k² σ_k²).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "smoothing",
      "category_title": "Smoothing",
      "summary": "Smooth a spectrum by convolution with a Gaussian kernel.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/smoothing/smooth_gaussian/"
    },
    {
      "name": "smooth_savgol",
      "description": "Smooth a spectrum with a Savitzky-Golay filter.\n\nNon-finite samples (NaN/inf) are bridged by linear interpolation over the finite neighbours before filtering, so a bad pixel neither raises nor smears ; the original non-finite positions are restored to NaN in the output (finite inputs are processed bit-identically to v1.0.0). v2.0.0 — uncertainty propagation : when the input carries an uncertainty, σ_out,i = sqrt(Σ_k c_k² σ_{i+k}²) with the Savitzky-Golay coefficients of scipy.signal.savgol_coeffs (interior samples) and, for the window//2 samples at each end, the coefficients of the edge polynomial the filter's default 'interp' mode evaluates there. For white noise σ, window 11 and polyorder 3 the interior gives σ_out = 0.4555 σ (= sqrt(Σ c_k²)). Bridged (non-finite) samples contribute no variance and get a NaN uncertainty. The correlation between neighbouring output samples is NOT tracked — χ² fits on a smoothed spectrum under-estimate their parameter errors. Without an input uncertainty the brick is unchanged from v1.0.1.\n\nParameters — pass as the 'params' object:\n- window (default 11): Sliding-window length in samples (odd; coerced up to the next odd).\n- polyorder (default 3): Polynomial order fitted within each window (< window).\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Savitzky & Golay 1964, Analytical Chemistry 36, 1627; scipy.signal.savgol_filter / savgol_coeffs; Bevington & Robinson 2003, Data Reduction and Error Analysis for the Physical Sciences, 3rd ed., McGraw-Hill — §3.2, error propagation through a linear combination (σ_out² = Σ c_k² σ_k²).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "smoothing",
      "category_title": "Smoothing",
      "summary": "Smooth a spectrum with a Savitzky-Golay filter.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/smoothing/smooth_savgol/"
    },
    {
      "name": "snr_der",
      "description": "Derivative-based SNR estimator (DER_SNR, Stoehr et al. 2008).\n\nSNR = median(flux) / (1.482602 / sqrt(6) * median(|2*F[i] - F[i-2] - F[i+2]|)). Works on any reasonably sampled spectrum and is insensitive to broadband features. v2.0.0: masked samples (Spectrum1D.mask) are ignored — a masked or non-finite sample drops out of the signal median together with the three derivative terms it enters.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Stoehr et al. 2008, 'DER_SNR: A Simple & General Spectroscopic Signal-to-Noise Measurement Algorithm', ASP Conf. Ser. 394, 505",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "quality",
      "category_title": "Quality / SNR",
      "summary": "Derivative-based SNR estimator (DER_SNR, Stoehr et al. 2008).",
      "docs_url": "https://docs.spectrokernel.io/algorithms/quality/snr_der/"
    },
    {
      "name": "snr_edge",
      "description": "Estimate SNR from the flat, line-free regions at the spectrum's edges.\n\nSNR = median(flux) / std(detrended edge flux), where each edge window is detrended with a linear fit. Non-finite samples (NaN/inf) are masked before the fit and excluded from the noise estimate ; the brick fails when fewer than 3 finite samples remain in either edge window. Returns inf when the detrended edges have zero scatter. v2.0.0: masked samples (Spectrum1D.mask) are ignored like non-finite ones.\n\nParameters — pass as the 'params' object:\n- region_fraction (default 0.1): Fraction of the spectrum, at each end, used as a noise window.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Stoehr et al. 2008, 'DER_SNR: A Simple & General Spectroscopic Signal-to-Noise Measurement Algorithm', ASP Conf. Ser. 394, 505 — the comparison baseline (snr_der) for continuum-window SNR estimators.; Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 12, continuum placement and noise estimation in line-free windows.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "quality",
      "category_title": "Quality / SNR",
      "summary": "Estimate SNR from the flat, line-free regions at the spectrum's edges.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/quality/snr_edge/"
    },
    {
      "name": "snr_linear_fit",
      "description": "Estimate SNR from the scatter around a linear fit of a continuum region.\n\nSNR = median(flux) / std(flux − linear fit) over the window. Non-finite samples (NaN/inf) are masked before the fit and excluded from the statistics ; the brick fails when fewer than 3 finite samples remain. Returns inf when the residuals have zero scatter. v2.0.0: masked samples (Spectrum1D.mask) are ignored like non-finite ones.\n\nParameters — pass as the 'params' object:\n- wavelength_min (default None): Lower bound of the continuum window (Å); null uses the start.\n- wavelength_max (default None): Upper bound of the continuum window (Å); null uses the end.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Stoehr et al. 2008, 'DER_SNR: A Simple & General Spectroscopic Signal-to-Noise Measurement Algorithm', ASP Conf. Ser. 394, 505 — the comparison baseline (snr_der) for continuum-window SNR estimators.; Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 12, continuum placement and noise estimation in line-free windows.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "quality",
      "category_title": "Quality / SNR",
      "summary": "Estimate SNR from the scatter around a linear fit of a continuum region.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/quality/snr_linear_fit/"
    },
    {
      "name": "stack_spectra",
      "description": "Combine every spectrum in ``ctx.spectra`` into one stacked spectrum.\n\nMedian stacking is the most robust against cosmic rays and outliers; mean maximises SNR for clean data; sum is useful for co-adding sub-exposures. Each spectrum contributes only inside its own wavelength range: samples of the reference grid outside a spectrum's coverage are NaN for that spectrum and ignored by the NaN-aware combiners (a sample covered by no spectrum at all is NaN in the output; count in metrics.n_nan_samples). v2.0.0: v1 let numpy.interp clamp to the edge value, so a spectrum was extrapolated as a constant over the whole grid — with s1 = 1.0 on 4000-5000 Å and s2 = 3.0 on 4500-5500 Å the v1 mean at 4000 Å was 2.0; it is now 1.0 (2.0 inside the overlap). v2.1.0 adds method='weighted' (mean, median and sum are untouched): the Bevington & Robinson weighted mean Σ w S / Σ w, sample by sample, with the stack uncertainty (Σ w)^(−1/2) written to ctx.spectrum.uncertainty. Weights, in order of precedence: the 'weights' parameter (one positive number per spectrum, interpreted as inverse variances 1/σ_i² — the output uncertainty is only meaningful on that scale); else, when every spectrum carries an uncertainty array, per-sample inverse variances w_ij = 1/σ_ij² (interpolated onto the grid like the flux; non-finite or zero σ excludes that sample); else per-spectrum w_i = DER_SNR_i² (Stoehr et al. 2008) — for continuum-normalised spectra this is 1/σ_i² in continuum units, otherwise the output uncertainty is relative to the median flux. The weighting actually used is recorded in ctx.spectrum.meta ('weighting', 'weights').\n\nParameters — pass as the 'params' object:\n- method (default 'median'): Combination method: one of ['mean', 'median', 'sum', 'weighted'].\n- reference_index (default 0): Index in ctx.spectra whose wavelength grid is the target.\n- weights (default None): method='weighted' only: list of one positive weight per spectrum (inverse variances); null = from the uncertainty arrays, else DER_SNR².\n\nRequires in the session context: spectra\n\nBackend: numpy.\nReferences: Tody 1993, ASP Conf. Ser. 52, 173 — IRAF scombine: spectra interpolated to a common dispersion, then summed / averaged / medianed sample by sample (weighted combination via its 'weight' parameter).; Bevington & Robinson 2003, Data Reduction and Error Analysis for the Physical Sciences, 3rd ed., ch. 4 — weighted mean μ = Σ w_i x_i / Σ w_i with w_i = 1/σ_i² and its uncertainty σ_μ = (Σ w_i)^(−1/2).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "stacking",
      "category_title": "Stacking",
      "summary": "Combine every spectrum in ``ctx.spectra`` into one stacked spectrum.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/stacking/stack_spectra/"
    },
    {
      "name": "standard_star_reference",
      "description": "Load a spectrophotometric standard (CALSPEC or Pickles template) as the catalogue spectrum.\n\nsource='calspec' : fetches <CALSPEC_BASE_URL><filename> (STScI archive.stsci.edu, current_calspec) through open_safe_url (SSRF guard, no redirects, timeout_s, max_bytes cap) and caches it in cache_dir (default <tmp>/spectro-kernel-calspec ; refresh=true re-downloads). The filename comes from the filename parameter, or from a built-in name map that only knows Vega (alpha_lyr_stis_011.fits) — any other star needs filename. Columns WAVELENGTH (vacuum Å) and FLUX (erg/s/cm²/Å) are read ; STATERROR and SYSERROR, when present, are combined in quadrature into the uncertainty. to_air=true (default) converts the vacuum axis to air with the Morton 2000 relation above 2000 Å (below, the axis is left as is), so the reference matches a ground-based, air-calibrated observation ; set to_air=false when the observation is on a vacuum scale. source='pickles' : reads <atlas_dir>/<template_id>.dat with pickles_atlas.load_atlas_from_dir (canonical 3800–7500 Å grid, 2 Å step) — a NORMALISED template (Pickles 1998 §3 : 'normalized to unity at 5556 Å' ; §5 : 'arbitrarily normalized to unity around 5556 Å, so the magnitude measurements are relative' ; flux_unit 'normalized', meta absolute_calibration=false) : the derived response has the right shape but an arbitrary scale (relative flux calibration only). The brick does NOT re-normalise the file ; it reports the value it finds at 5556 Å (flux_at_5556_aa in artifacts and meta — 1.0 for a genuine Pickles file, NaN when 5556 Å is outside the template). The CatalogResult is filed in ctx.catalog_lookups under name when given, else under the identifier (CALSPEC TARGETID or the Pickles template_id). Never fetches at import ; no network for source='pickles' or a cache hit.\n\nParameters — pass as the 'params' object:\n- name (default None): Star name (e.g. 'Vega') ; resolved through the built-in CALSPEC map.\n- source (default 'calspec'): 'calspec' (absolute, network) or 'pickles' (normalised template, local).\n- filename (default None): CALSPEC file name (e.g. 'alpha_lyr_stis_011.fits') ; required unless the name is in the built-in map.\n- atlas_dir (default None): Directory of Pickles uk*.dat files (source='pickles').\n- template_id (default None): Pickles template stem, e.g. 'uka0v' (source='pickles').\n- to_air (default True): Convert the CALSPEC vacuum wavelengths to air (Morton 2000) above 2000 Å.\n- cache_dir (default None): Directory caching downloaded CALSPEC files ; None ⇒ temp directory.\n- refresh (default False): Re-download even when the file is already in the cache.\n- timeout_s (default 60.0): Socket timeout (s) of the download.\n- max_bytes (default 67108864): Byte cap on the download.\n- output_key (default 'catalog_spectrum'): ctx.extras key receiving the reference Spectrum1D.\n\nBackend: astropy.\nReferences: Bohlin, Gordon & Tremblay 2014, PASP 126, 711 — CALSPEC absolute flux calibration (HST STIS / NICMOS / WFC3 standards).; Bohlin, Hubeny & Rauch 2020, AJ 160, 21 — CALSPEC update.; Pickles 1998, PASP 110, 863 — stellar spectral flux library (normalised templates).; Morton 2000, ApJS 130, 403 — vacuum → air conversion applied to the CALSPEC axis when to_air is true.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "catalog",
      "category_title": "External catalogues",
      "summary": "Load a spectrophotometric standard (CALSPEC or Pickles template) as the catalogue spectrum.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/catalog/standard_star_reference/"
    },
    {
      "name": "subtract_bias",
      "description": "Subtract the master bias (or a constant bias level) from ``ctx.image``.\n\nNative counterpart of ``subtract_bias_easyspec``: the same arithmetic without the easyspec staging (no temporary FITS, no clipping unless ``clip_negative`` is set), so integer-ADU frames and BZERO-scaled files are handled in float64 end to end. Precedence: an explicit ``bias_level`` wins over the master frame stored in ``ctx.extras[bias_key]``; with neither available the brick fails. Negative pixels are kept by default because clipping biases the sky statistics of faint frames; ``clip_negative=True`` reproduces the easyspec ``pad_with_zeros`` behaviour. Companion of ``dark_subtract`` (which scales by exposure time) and ``flat_normalize``.\n\nParameters — pass as the 'params' object:\n- bias_key (default 'master_bias'): Key in ctx.extras where the master bias (an ImageFrame) is stored.\n- bias_level (default None): Constant bias level in ADU to subtract instead of a master frame (e.g. the overscan mean); takes precedence over the master frame when set.\n- clip_negative (default False): Set the pixels that become negative after subtraction to zero.\n\nRequires in the session context: image\n\nBackend: numpy.\nReferences: Howell 2006 — Handbook of CCD Astronomy, 2nd ed., ch. 4 (bias frames, zero level).; Massey 1997 — A User's Guide to CCD Reductions with IRAF (NOAO), zero-level correction.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Subtract the master bias (or a constant bias level) from ``ctx.image``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/subtract_bias/"
    },
    {
      "name": "subtract_bias_easyspec",
      "description": "Subtract a master bias from a single science frame via ``cleaning.debias``.\n\nWrapper of easyspec's ``debias`` (temporary FITS staging, negative pixels clipped to zero by default). ``subtract_bias`` is the native numpy counterpart: same arithmetic on ``ctx.image`` in float64, no staging, no clipping unless asked.\n\nParameters — pass as the 'params' object:\n- target_path (default None): Science FITS to debias; falls back to ctx.image.\n- master_bias_path (default None): Master-bias FITS; falls back to ctx.extras['master_bias'].\n- pad_with_zeros (default True): Clip negative pixels to zero after subtraction (easyspec default).\n\nBackend: easyspec.\nReferences: easyspec.cleaning.cleaning.debias.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Subtract a master bias from a single science frame via ``cleaning.debias``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/subtract_bias_easyspec/"
    },
    {
      "name": "subtract_continuum",
      "description": "Subtract a sigma-clipped polynomial continuum, leaving the line residual.\n\nout = flux − continuum with the same symmetric sigma-clipped polynomial as normalize_polynomial. v2.0.0: masked samples (Spectrum1D.mask) are ignored by the fit and by residual_rms ; the output keeps their flux minus the continuum, like every other sample, and carries the mask through.\n\nParameters — pass as the 'params' object:\n- order (default 3): Polynomial degree of the continuum fit.\n- sigma_clip (default 3.0): Reject samples beyond this many standard deviations per iteration.\n- iterations (default 3): Number of sigma-clipping iterations.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — the IRAF data reduction and analysis system ; onedspec.continuum fits an iteratively sigma-clipped function and offers the 'difference' output type implemented here.; Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 12, continuum placement for line measurement.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "continuum",
      "category_title": "Continuum",
      "summary": "Subtract a sigma-clipped polynomial continuum, leaving the line residual.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/continuum/subtract_continuum/"
    },
    {
      "name": "subtract_dark_easyspec",
      "description": "Subtract a master dark from a science frame via ``cleaning.sub_dark``.\n\nParameters — pass as the 'params' object:\n- target_path (default None): Debiased science FITS; falls back to ctx.image.\n- master_dark_path (default None): Master-dark FITS; falls back to ctx.extras['master_dark'].\n- pad_with_zeros (default True): Clip negative pixels to zero after subtraction.\n\nBackend: easyspec.\nReferences: easyspec.cleaning.cleaning.sub_dark.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Subtract a master dark from a science frame via ``cleaning.sub_dark``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/subtract_dark_easyspec/"
    },
    {
      "name": "subtract_sky_2d",
      "description": "Subtract a per-column sky model fitted from off-trace rows of ``ctx.image``.\n\nSky regions are taken on either side of the trace and far enough away to be clean of stellar flux. The fit defaults to a degree-1 polynomial (linear gradient across the slit), which handles tilted sky and twilight gradients without over-fitting. v1.0.1: all columns are fitted in one vectorised least-squares call (numpy.polyfit on the 2-D sky block) — identical numbers, ~60× faster on a 200×2048 frame.\n\nParameters — pass as the 'params' object:\n- trace_row (default None) [required]: Approximate y-pixel of the spectral trace.\n- trace_half_width (default 8): Half-width (rows) of the protected aperture around the trace.\n- sky_offset (default 4): Number of rows between trace edge and sky window start.\n- sky_half_width (default 10): Half-width (rows) of each sky window.\n- poly_order (default 1): Polynomial degree for the per-column cross-slit sky fit (0 = constant).\n\nRequires in the session context: image\n\nBackend: numpy.\nReferences: Horne 1986, PASP 98, 609 — §2 (standard extraction): sky estimated at each wavelength by a low-order polynomial fit across the spatial direction to the pixels outside the object aperture.; Tody 1986, Proc. SPIE 627, 733 — IRAF apall background fitting (per-column polynomial in the background windows).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "preprocessing",
      "category_title": "Preprocessing (2-D image)",
      "summary": "Subtract a per-column sky model fitted from off-trace rows of ``ctx.image``.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/preprocessing/subtract_sky_2d/"
    },
    {
      "name": "synth_telluric",
      "description": "Generate a synthetic telluric transmission spectrum.\n\nEach band is modelled as an inverted Gaussian (1 - depth * G) with hand-curated centre / depth / FWHM — a quick-look approximation, not a line-by-line radiative-transfer model (for that use molecfit, Smette et al. 2015, A&A 576, A77, or an observed telluric standard). Depths and widths are unit-airmass values; the ``airmass`` parameter scales them via T = T0**airmass, the Bouguer / Beer-Lambert relation. The output spectrum is stored in ``ctx.extras['telluric_template']`` and, when ``store_as_spectrum=True`` (default False), also on ``ctx.spectrum``. remove_telluric_division falls back to this key (after 'telluric_template_scaled') when its default key is absent.\n\nParameters — pass as the 'params' object:\n- wavelength_min (default 5800.0): Lower bound of the synthetic axis (Å).\n- wavelength_max (default 9500.0): Upper bound of the synthetic axis (Å).\n- n_points (default 4000): Number of samples in the synthetic axis.\n- airmass (default 1.0): Atmospheric path length (1.0 = zenith).\n- store_as_spectrum (default False): If true, also write the template to ctx.spectrum.\n\nBackend: numpy.\nReferences: Hinkle, Wallace & Livingston 2003, BAAS 35, 1260 — Kitt Peak atmospheric transmission atlas 0.5-5.5 µm (positions of the O2 γ/B/A bands and the H2O bands used here).; Hardie 1962, in Astronomical Techniques (Stars and Stellar Systems II, ed. Hiltner), Univ. Chicago Press, p. 178 — Bouguer / Beer-Lambert extinction law: transmission scales as T = T(1)**airmass.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Generate a synthetic telluric transmission spectrum.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/synth_telluric/"
    },
    {
      "name": "temporal_variance_spectrum",
      "description": "Temporal variance spectrum (Fullerton, Gies & Bolton 1996) of N ≥ 3 line profiles.\n\nImplements the noise-weighted TVS of Fullerton et al. (1996, §3). The per-epoch continuum noise σ_ic is the median of Spectrum1D.uncertainty inside the continuum window when present, else the DER_SNR estimate on the epoch's own (native, not resampled) samples in that window (the whole overlap when continuum_lo/continuum_hi are null — fine as long as lines cover less than half of the pixels; DER_SNR is a median statistic). The weights w_i = (σ_0/σ_ic)² are normalised to Σ w_i = N through σ_0 = [(1/N) Σ σ_ic⁻²]^(−1/2), so the mean profile S̄_j = (1/N) Σ w_i S_ij is the inverse-variance weighted mean. Under the null hypothesis (N−1)(TVS)_j/σ_0² follows χ² with N−1 degrees of freedom, hence the threshold (TVS)^1/2 > σ_0 [χ²_(N−1)(p)/(N−1)]^1/2 with p = 1 − confidence; at the default 99 % about 1 % of non-variable pixels exceed it by chance. (TVS)^1/2 is reported in continuum units (multiply by 100 for the paper's percent-of-continuum presentation). Caveats: the pixel-dependent Poisson term α_ij = S_ij^1/2 of the paper's fully general form is not applied (uniform noise across the profile, i.e. photon noise inside deep lines is treated as equal to the continuum noise, which is conservative for absorption lines); resampling onto a common grid correlates adjacent pixels, so for a strict test feed epochs already on one wavelength grid (they are then used as-is); wavelength-shifted epochs (RV, barycentric) must be aligned beforehand or the shift itself shows up as variability. tvs_variable_lo_aa / tvs_variable_hi_aa are a reporting convenience (the bounds of the longest run of at least min_run_pixels contiguous significant pixels), not part of the paper's formalism; they are only written when such a run exists. v2.0.0: masked samples (Spectrum1D.mask) are ignored, per epoch — a common-grid pixel that any epoch lacks (masked or non-finite, including the resampled pixels that would draw on one) gets no TVS (null in extras.tvs, never significant) instead of failing the run ; the per-epoch noise skips them too ; extras.tvs.n_excluded_pixels counts them and at least 5 usable pixels are required.\n\nParameters — pass as the 'params' object:\n- continuum_lo (default None): Lower edge (wavelength units) of the line-free window used for the per-epoch noise σ_ic; null = whole overlap.\n- continuum_hi (default None): Upper edge of the continuum window; null = whole overlap.\n- confidence (default 0.99): Confidence level of the χ²_(N−1) variability threshold (paper: 0.99).\n- min_run_pixels (default 3): Minimum number of contiguous significant pixels for a run to be reported as tvs_variable_lo_aa / tvs_variable_hi_aa.\n\nRequires in the session context: spectra\n\nBackend: scipy.\nReferences: Fullerton, Gies & Bolton 1996, ApJS 103, 475 — §3, the temporal variance spectrum: (TVS)_j = 1/(N−1) Σ_i w_i (S_ij − S̄_j)² with w_i = (σ_0/σ_ic)², σ_0 = [(1/N) Σ_i σ_ic⁻²]^(−1/2) so that Σ w_i = N, S̄_j = (1/N) Σ_i w_i S_ij, and the significance test (N−1)(TVS)_j/σ_0² ~ χ²_(N−1) under the null hypothesis.; Stoehr et al. 2008, ASP Conf. Ser. 394, 505 — DER_SNR, the recipe used for σ_ic when a spectrum carries no uncertainty array.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "timeseries",
      "category_title": "Time series",
      "summary": "Temporal variance spectrum (Fullerton, Gies & Bolton 1996) of N ≥ 3 line profiles.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/timeseries/temporal_variance_spectrum/"
    },
    {
      "name": "vacuum_to_air",
      "description": "Convert the wavelength axis from vacuum to air wavelengths.\n\nn(λ_vac) = 1 + 8.34254e-5 + 2.406147e-2/(130 - s²) + 1.5998e-4/(38.9 - s²), s = 1e4/λ[Å] (Morton 2000). Valid above ~2000 Å.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Morton 2000, ApJS 130, 403 — air/vacuum dispersion relation n(λ_vac).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "correction",
      "category_title": "Corrections",
      "summary": "Convert the wavelength axis from vacuum to air wavelengths.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/correction/vacuum_to_air/"
    },
    {
      "name": "validate_bess_header",
      "description": "Check a FITS header against the BeSS keyword contract.\n\nThe mirror of export_fits_bess: both bricks consume the same keyword table (algorithms.exports._bess_contract), so a file produced by the exporter always validates cleanly. Checks: NAXIS == 1 and NAXIS1 > 0; spectral WCS (CRVAL1, CDELT1, CRPIX1, CTYPE1, CUNIT1) present and non-empty; BeSS identification (BSS_INST, BSS_SITE, BSS_ESRC) and observation keywords (OBJNAME, DATE-OBS, EXPTIME, OBSERVER) present and non-empty; BSS_VHEL present (value 0 licit). strict=false reports without failing — refusing a product is application policy, not kernel policy.\n\nParameters — pass as the 'params' object:\n- strict (default False): If true, the step fails when the header is non-conforming; if false (default), issues are reported and the step succeeds.\n- header_key (default None): Optional ctx.extras key holding the header dict to check; null reads ctx.spectrum.header.\n- require_geo (default False): Also require observing-site coordinates (BSS_LAT/GEO_LAT, BSS_LONG/GEO_LONG, BSS_ELEV/GEO_ELEV) — optional in BeSS but mandatory for a STAROS deposit.\n\nBackend: numpy.\nReferences: Teyssier 2015, A&A Pro-Am collaboration — BeSS/ARAS submission protocol.; Buil 2012, ARAS Observation Guide — BeSS FITS header convention.; FITS Standard 4.0 — Pence et al. 2010, A&A 524, A42.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "quality",
      "category_title": "Quality / SNR",
      "summary": "Check a FITS header against the BeSS keyword contract.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/quality/validate_bess_header/"
    },
    {
      "name": "vizier_query",
      "description": "Look up an object in VizieR — the CDS table service.\n\nRequires network access and the optional 'catalogs' extra. By default the row count per catalogue is capped at 5; set `row_limit` to change it.\n\nParameters — pass as the 'params' object:\n- object_name (default None) [required]: Object identifier (e.g. 'HD 209458').\n- catalog (default None): Optional VizieR catalogue ID to restrict to (e.g. 'I/350/gaiaedr3').\n- row_limit (default 5): Maximum number of rows to fetch per catalogue.\n- radius_arcsec (default 5.0): Cone-search radius around the resolved position.\n\nBackend: astroquery.\nReferences: Ochsenbein, Bauer & Marcout 2000, A&AS 143, 23 — VizieR; Ginsburg et al. 2019, AJ 157, 98 — astroquery",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "catalog",
      "category_title": "External catalogues",
      "summary": "Look up an object in VizieR — the CDS table service.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/catalog/vizier_query/"
    },
    {
      "name": "vr_ratio",
      "description": "Violet/Red intensity ratio of a double-peaked emission line.\n\nContinuum from window-edge medians (mean of the two edge medians, edge = max(3, 0.1 · n)). Detection on the normalised flux with scipy.signal.find_peaks(prominence=min_prominence), restricted to ±0.7 · window around the centre. Returns single_peaked=True with vr_ratio=None when only one side has a peak. v2.0.0: masked samples (Spectrum1D.mask) are ignored — dropped like non-finite ones before the continuum estimate and the peak search.\n\nParameters — pass as the 'params' object:\n- line_center_aa (default 6562.82): Rest wavelength (Å) of the line centre.\n- window_half_width_aa (default 15.0): Half-width (Å) of the analysis window around the centre.\n- min_prominence (default 0.05): Peak-detection prominence on the normalised flux (find_peaks).\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Okazaki 1991, PASJ 43, 75 — long-term V/R variations of Be stars from global one-armed oscillations of the disc.; Hummel & Vrancken 2000, A&A 359, 1075 — Be-star V/R variability.; scipy.signal.find_peaks — prominence-thresholded peak detection.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "line_fitting",
      "category_title": "Line fitting",
      "summary": "Violet/Red intensity ratio of a double-peaked emission line.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/line_fitting/vr_ratio/"
    },
    {
      "name": "vsini_fourier",
      "description": "v sin i from the first zero of the Fourier transform of one line profile.\n\nCuts ± window_angstrom around line_center_angstrom, fits a straight continuum through the outer continuum_fraction of the window on each side, forms D = 1 − F/Fc on v = c·(λ − λ0)/λ0, resamples to the median native velocity step, zero-pads by zero_pad_factor and takes the FFT. The first local minimum of |d(σ)| below 30 % of |d(0)| at which the centroid-shifted real part changes sign is the first zero σ1 when the side lobe that follows it (max of |d| over (σ1, 1.6 σ1]) reaches 3 % of |d(0)| (the transform of the rotation profile has a first side lobe of 0.132 |d(0)| at ε = 0, 0.104 at ε = 0.6, 0.086 at ε = 1, halved by Gaussian broadening at the validity limit), stands 3× above the Fourier noise floor (median |d(σ)| over the upper half of the frequency axis) and 1.5× above the minimum ; a rejected minimum already at the noise level ends the search. v sin i = q1(epsilon)/σ1 with the Dravins et al. 1990 polynomial (q1 = 0.610 at ε = 0, 0.660 at ε = 0.6). Fails when the window holds an emission feature rather than an absorption line, when the line depth is below 3× the continuum-anchor scatter, or when no validated zero exists below the Nyquist frequency 1/(2Δv) — the line is unresolved (v sin i ≲ 2·q1·Δv), Gaussian-dominated, or the window clips the profile. Needs an isolated absorption line ; blends and strong macroturbulence (slow rotators, v sin i ≲ 10 km/s) bias the zero. fourier_sidelobe_ratio well below the theoretical 0.09–0.13, or under ~4× fourier_noise_floor, means the zero sits in the noise (a Gaussian-dominated line can then yield a spurious v sin i below 2× its FWHM). Match window_angstrom to the line (±2–3 × λ0·v sin i/c plus continuum) : the noise floor grows as √window. extras['vsini_fourier'] holds |d(σ)|/|d(0)| up to 3 σ1 for plotting. v2.0.0: masked samples (Spectrum1D.mask) are ignored — dropped like non-finite ones before the continuum fit and the resampling onto the uniform velocity grid (which bridges the gap linearly).\n\nParameters — pass as the 'params' object:\n- line_center_angstrom (default None) [required]: Rest-frame centre (Å) of an isolated absorption line (required).\n- window_angstrom (default 5.0): Half-width (Å) of the analysis window ; must contain the whole rotation profile (λ0 · v sin i / c on each side) plus continuum.\n- epsilon (default 0.6): Linear limb-darkening coefficient ε in [0, 1] for q₁(ε).\n- continuum_fraction (default 0.2): Fraction of each window edge used to anchor the continuum.\n- zero_pad_factor (default 16): Zero-padding factor of the FFT (σ sampling refinement), ≥ 2.\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Carroll 1933, MNRAS 93, 478 — zeros of the Fourier transform of the rotation profile as a v sin i diagnostic.; Gray 2005, The Observation and Analysis of Stellar Photospheres, 3rd ed., Cambridge UP — ch. 18, rotation profile G(Δλ) and its Fourier analysis (first zero at σ₁ v sin i = 0.660 for ε = 0.6).; Dravins, Lindegren & Torkelsson 1990, A&A 237, 137 — q₁(ε) = 0.610 + 0.062 ε + 0.027 ε² + 0.012 ε³ + 0.004 ε⁴.; Díaz, González, Levato & Grosso 2011, A&A 531, A143 — Fourier v sin i recipe and error analysis.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "kinematics",
      "category_title": "Kinematics",
      "summary": "v sin i from the first zero of the Fourier transform of one line profile.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/kinematics/vsini_fourier/"
    },
    {
      "name": "wavelength_calibrate_easyspec",
      "description": "Fit a wavelength polynomial via ``extraction.wavelength_calibration`` and apply it.\n\nLamp peaks must be pre-identified — typically you run an arc-lamp through ``extract_spectrum_easyspec`` first, pick the line peaks (e.g. with ``detect_lines``) and match them to a reference list (NIST, NeAr…). The matched ``(pixel, wavelength)`` pairs feed this algorithm.\n\nParameters — pass as the 'params' object:\n- lamp_peak_positions (default None) [required]: List of identified peak pixel positions in the arc-lamp spectrum.\n- corresponding_wavelengths (default None) [required]: Reference wavelengths (Å) for those peaks, same length.\n- poly_order (default 2): Polynomial order of the wavelength fit.\n- data_type (default 'target'): easyspec data_type label (target / standard_star).\n\nRequires in the session context: spectrum\n\nBackend: easyspec.\nReferences: easyspec.extraction.extraction.wavelength_calibration.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "wavelength_calibration",
      "category_title": "Wavelength calibration",
      "summary": "Fit a wavelength polynomial via ``extraction.wavelength_calibration`` and apply it.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/wavelength_calibration/wavelength_calibrate_easyspec/"
    },
    {
      "name": "wavelength_calibrate_polynomial",
      "description": "Fit a polynomial to (pixel → wavelength) pairs and apply it to the spectrum.\n\nThe polynomial is fitted with numpy.polynomial.Polynomial.fit, which handles the domain mapping so high orders stay well-conditioned. pixel_positions are 0-based indices on the current ctx.spectrum.wavelength axis (see the class docstring).\n\nParameters — pass as the 'params' object:\n- pixel_positions (default None) [required]: List of 0-based pixel indices identified in the arc spectrum (same frame as ctx.spectrum.wavelength; first sample = 0).\n- wavelengths_angstrom (default None) [required]: Rest wavelengths corresponding to those pixels (Å).\n- order (default 3): Polynomial degree.\n\nRequires in the session context: spectrum\n\nBackend: numpy.\nReferences: Tody 1986, Proc. SPIE 627, 733 — IRAF identify: polynomial dispersion solution fitted through identified arc lines.; Tody 1993, ASP Conf. Ser. 52, 173 — IRAF in the Nineties (identify / dispcor dispersion functions).",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "wavelength_calibration",
      "category_title": "Wavelength calibration",
      "summary": "Fit a polynomial to (pixel → wavelength) pairs and apply it to the spectrum.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/wavelength_calibration/wavelength_calibrate_polynomial/"
    },
    {
      "name": "wavelength_calibration_in_situ",
      "description": "Refine the wavelength zero-point from simultaneously-acquired sky lines.\n\nThe polynomial coefficients describe the initial wavelength solution (highest order first, as accepted by numpy.poly1d). The Δλ shift is the *zero-point* refinement — slope and higher-order coefficients are kept. RMS of residuals after subtracting the mean Δλ measures how well a pure zero-point shift explains the calibration error; large RMS means the initial polynomial itself needs re-fitting (use wavelength_calibrate_polynomial).\n\nParameters — pass as the 'params' object:\n- polynomial_coef (default []) [required]: Initial λ(x) coefficients, highest order first (numpy.poly1d).\n- reference_wavelengths (default []) [required]: Catalogue λ (Å) of the reference lines, same length as guesses.\n- guess_positions (default []) [required]: Pixel positions of those lines in the sky spectrum.\n- search_width (default 40.0): Fit-window width (px) forwarded to the line fitter.\n- sky_key (default 'sky_spectrum'): ctx.extras key holding the sky reference Spectrum1D.\n- oversampling (default 2.0): Uniform-grid oversampling factor vs. the native pixel step (≥ 1).\n\nRequires in the session context: spectrum\n\nBackend: astropy.\nReferences: Stoughton et al. 2002, AJ 123, 485 — SDSS in-situ wavelength refinement from night-sky lines.; Hanuschik 2003, A&A 407, 1157 — UVES optical sky atlas.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "wavelength_calibration",
      "category_title": "Wavelength calibration",
      "summary": "Refine the wavelength zero-point from simultaneously-acquired sky lines.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/wavelength_calibration/wavelength_calibration_in_situ/"
    },
    {
      "name": "wavelength_calibration_solar",
      "description": "Calibrate a solar spectrum from built-in Fraunhofer line wavelengths.\n\nBuilt-in catalogue of 15 strong Fraunhofer lines (Ca II K/H, Hβ, Mg b triplet, Na D, Hα, O2 telluric bands). Use the solar or daylight twilight spectrum directly as input; no arc lamp needed. Set min_lines_for_fit ≥ poly_order + 1 — typically 5+. approx_wavelength_min_angstrom helps the matcher choose the right Fraunhofer line per peak. The pixel axis is 0-based. v2.0.0: when several detected peaks fall within match_tolerance_angstrom of the same catalogue line, the peak whose approximate wavelength is closest to that line is kept — v1 compared the catalogue wavelength with itself and therefore always kept the first peak encountered (a spurious dip a few Å blueward of a real line displaced the identification and the fit).\n\nParameters — pass as the 'params' object:\n- approx_wavelength_min_angstrom (default 3800.0) [required]: Approximate wavelength (Å) of pixel 0 — used as the starting point of the peak-to-Fraunhofer matcher.\n- approx_dispersion_angstrom_per_pixel (default 1.0) [required]: Dispersion estimate (Å / pixel) for the grating + camera combination. Read it off the instrument documentation.\n- poly_order (default 3): Polynomial order of the fitted λ(x) (typically 2 or 3).\n- min_lines_for_fit (default 5): Refuse to fit fewer than this many matched lines (must be ≥ poly_order + 1).\n- match_tolerance_angstrom (default 8.0): Maximum residual (Å) between a peak's approximate wavelength and the nearest Fraunhofer line for the match to count.\n- peak_prominence_quantile (default 0.5): scipy.signal.find_peaks prominence threshold expressed as a quantile of the inverted-spectrum amplitude (0..1, higher = stricter).\n- peak_min_distance_pixels (default 5): Minimum spacing between detected peaks (pixels).\n\nRequires in the session context: spectrum\n\nBackend: scipy.\nReferences: Delbouille, Roland & Neven 1973, Atlas du spectre solaire — high-resolution Fraunhofer atlas.; Kurucz 2005, Mem. Soc. Astron. It. Suppl. 8, 14 — synthetic solar atlas (BASS2000).; Tody 1986, Proc. SPIE 627, 733 — IRAF identify heritage.",
      "input_schema": {
        "additionalProperties": false,
        "properties": {
          "session_id": {
            "type": "string"
          },
          "params": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "session_id"
        ],
        "type": "object"
      },
      "output_schema": {
        "additionalProperties": true,
        "type": "object"
      },
      "kind": "algorithm",
      "category": "wavelength_calibration",
      "category_title": "Wavelength calibration",
      "summary": "Calibrate a solar spectrum from built-in Fraunhofer line wavelengths.",
      "docs_url": "https://docs.spectrokernel.io/algorithms/wavelength_calibration/wavelength_calibration_solar/"
    }
  ],
  "resources": [
    {
      "uri": "spectro://algorithms",
      "name": "algorithms",
      "description": "The whole algorithm catalogue as JSON (same shape as docs/ai/algorithms.json): every algorithm with its parameters, defaults, inputs, outputs, references and call shapes.",
      "mime_type": "application/json"
    },
    {
      "uri": "spectro://recipes",
      "name": "recipes",
      "description": "Every recipe (preset) discoverable on this server as JSON (same shape as docs/ai/presets.json): variables, steps, status, references.",
      "mime_type": "application/json"
    },
    {
      "uri": "spectro://llms.txt",
      "name": "llms.txt",
      "description": "Plain-text map of this server for language models: what it is, the tool families, the resources, links to the documentation.",
      "mime_type": "text/plain"
    }
  ],
  "resource_templates": [
    {
      "uri_template": "spectro://algorithms/{name}",
      "name": "algorithm",
      "description": "One algorithm's catalogue entry, by name (see spectro://algorithms).",
      "mime_type": "application/json"
    },
    {
      "uri_template": "spectro://recipes/{name}",
      "name": "recipe",
      "description": "One recipe's catalogue entry, by name (see spectro://recipes).",
      "mime_type": "application/json"
    }
  ]
}
