User Guide

Fitting a single spectrum

fit_single_spectrum() searches for the brightest emission-line peak within a velocity window, normalises by that peak, then runs a bounded Levenberg-Marquardt Gaussian fit on the surrounding pixel window.

import numpy as np
from gaussfit_rs import fit_single_spectrum, FitResult

velocity_grid = np.linspace(-300, 300, 60, dtype=np.float32)  # km/s
spectrum = np.exp(-0.5 * (velocity_grid / 30.0) ** 2).astype(np.float32)
noise = np.full_like(spectrum, 0.05)

result_arr, (i_left, i_right) = fit_single_spectrum(
    spectrum=spectrum,
    dopp_slit=velocity_grid,
    spec_noise=noise,
    guide_velocity=0.0,
    velocity_range=200.0,
    npix=10,
    npix_slack=2,
    dv=12.0,
    width_min=5.0,
    width_max=100.0,
    width_guess=30.0,
    amplitude_rel_min=0.1,
    amplitude_rel_max=2.0,
)

r = FitResult.from_array(result_arr)
if r.converged:
    print(f"velocity = {r.velocity:.1f} ± {r.velocity_err:.1f} km/s")

Batch fitting (IFU data)

fit_spectra_batch() is the recommended entry point for IFU data. It accepts a 2-D (N, M) array and processes all N rows concurrently using Rayon, releasing the Python GIL for the full computation. spec_noise must have exactly the same shape as spectra. When n_pixels is supplied, the first n_pixels columns of both arrays are used.

from gaussfit_rs import fit_spectra_batch

fits, indices = fit_spectra_batch(
    spectra=spectra_2d,       # (N, M) float32, C-contiguous
    dopp_slit=velocity_grid,  # (M,) float32
    spec_noise=noise_2d,      # (N, M) float32, C-contiguous
    guide_velocity=0.0,
    velocity_range=200.0,
    npix=10,
    npix_slack=2,
    dv=12.0,
    width_min=5.0,
    width_max=100.0,
    width_guess=30.0,
    amplitude_rel_min=0.1,
    amplitude_rel_max=2.0,
)

# fits[i, 7] == FLAG_SUCCESS means row i converged
converged_mask = fits[:, 7] == 0.0
velocities = fits[converged_mask, 1]   # km/s

Output format

Both functions return an 8-element float array per spectrum:

Index

Field

Description

0

amplitude

Fitted peak amplitude (same units as input spectrum)

1

velocity

Line-centre velocity [km/s]

2

sigma

Gaussian width 1-σ [km/s]

3

amplitude_err

1-σ uncertainty on amplitude

4

velocity_err

1-σ uncertainty on velocity [km/s]

5

sigma_err

1-σ uncertainty on sigma [km/s]

6

reduced_chi2

Reduced χ² of best fit

7

flag

Status code (0 = success, 1 = no peak, 2 = no convergence)

Fields 0-6 are NaN when flag != FLAG_SUCCESS.

Use FitResult to unpack by name:

from gaussfit_rs import FitResult

r = FitResult.from_array(result_arr)
r.velocity      # float [km/s]
r.sigma         # float [km/s]
r.converged     # bool

Input validation and tolerances

All fitting functions require finite, positive tolerances and max_iter. Spectrum fitting also requires positive npix and dv, non-negative npix_slack and velocity_range, increasing amplitude and width bounds, and finite numeric inputs. Low-level Gaussian fitting requires finite x and y arrays, finite positive error values, finite initial parameters, and finite increasing bounds.

The f32 functions default to:

fit_single_spectrum(
    ...,
    xtol=1e-6,     # parameter step tolerance
    ftol=1e-6,     # cost-function change tolerance
    gtol=1e-6,     # gradient norm tolerance
    max_iter=2000,
)

The f64 low-level function uses tighter defaults:

fit_gaussian_f64(
    ...,
    xtol=1e-10,
    ftol=1e-10,
    gtol=1e-10,
    max_iter=2000,
)

Tighten these for high-SNR data; relax (e.g. gtol=1e-4) for bulk runs where exact convergence is less critical.

Low-level Gaussian fitting

fit_gaussian_f32() and fit_gaussian_f64() fit a Gaussian to arbitrary (x, y ± error) data without the spectral peak-finding step. Use the f64 variant when the data span a wide dynamic range or when f32 rounding is significant.

from gaussfit_rs import fit_gaussian_f64
import numpy as np

result = fit_gaussian_f64(
    x=x, y=y, error=err,
    initial=np.array([1.0, 0.0, 1.0]),
    lower_bounds=np.array([0.1, -5.0, 0.1]),
    upper_bounds=np.array([2.0,  5.0, 5.0]),
)