Skip to content

Spectral Operations

spectrakit.ops.spectral_subtract

spectral_subtract(
    spectrum: ndarray,
    background: ndarray,
    factor: float = 1.0,
) -> np.ndarray

Subtract background from spectrum.

Computes spectrum - factor * background. Useful for background subtraction, solvent subtraction, or difference spectroscopy.

Parameters:

Name Type Description Default
spectrum ndarray

Spectrum or batch, shape (W,) or (N, W).

required
background ndarray

Spectrum to subtract, shape (W,). If spectrum is 2-D, background is subtracted from every row.

required
factor float

Scaling factor for background before subtraction.

1.0

Returns:

Type Description
ndarray

Difference spectrum, same shape as spectrum.

Raises:

Type Description
SpectrumShapeError

If shapes are incompatible (different number of wavelength points).

EmptySpectrumError

If spectrum has zero elements.

Source code in src/spectrakit/ops/subtract.py
def spectral_subtract(
    spectrum: np.ndarray,
    background: np.ndarray,
    factor: float = 1.0,
) -> np.ndarray:
    """Subtract *background* from *spectrum*.

    Computes ``spectrum - factor * background``. Useful for background
    subtraction, solvent subtraction, or difference spectroscopy.

    Args:
        spectrum: Spectrum or batch, shape ``(W,)`` or ``(N, W)``.
        background: Spectrum to subtract, shape ``(W,)``. If *spectrum*
            is 2-D, *background* is subtracted from every row.
        factor: Scaling factor for *background* before subtraction.

    Returns:
        Difference spectrum, same shape as *spectrum*.

    Raises:
        SpectrumShapeError: If shapes are incompatible (different number
            of wavelength points).
        EmptySpectrumError: If *spectrum* has zero elements.
    """
    spectrum = ensure_float64(spectrum)
    background = ensure_float64(background)
    validate_1d_or_2d(spectrum, name="spectrum")
    warn_if_not_finite(spectrum, name="spectrum")
    warn_if_not_finite(background, name="background")

    # Validate compatible shapes
    spec_w = spectrum.shape[-1]
    bg_w = background.shape[-1] if background.ndim >= 1 else 0
    if spec_w != bg_w:
        raise SpectrumShapeError(f"spectrum has {spec_w} points but background has {bg_w} points")

    if spectrum.ndim == 1:
        return spectrum - factor * background

    return spectrum - factor * background[np.newaxis, :]

spectrakit.ops.spectral_average

spectral_average(intensities: ndarray) -> np.ndarray

Compute the mean spectrum from a batch.

Parameters:

Name Type Description Default
intensities ndarray

Spectral batch, shape (N, W).

required

Returns:

Type Description
ndarray

Mean spectrum, shape (W,).

Raises:

Type Description
SpectrumShapeError

If input is not 2-D.

Source code in src/spectrakit/ops/average.py
def spectral_average(intensities: np.ndarray) -> np.ndarray:
    """Compute the mean spectrum from a batch.

    Args:
        intensities: Spectral batch, shape ``(N, W)``.

    Returns:
        Mean spectrum, shape ``(W,)``.

    Raises:
        SpectrumShapeError: If input is not 2-D.
    """
    intensities = ensure_float64(intensities)
    validate_1d_or_2d(intensities)
    warn_if_not_finite(intensities)

    if intensities.ndim == 1:
        return intensities.copy()

    return np.mean(intensities, axis=0)  # type: ignore[no-any-return]

spectrakit.ops.spectral_interpolate

spectral_interpolate(
    intensities: ndarray,
    wavenumbers: ndarray,
    new_wavenumbers: ndarray,
    kind: str = "linear",
) -> np.ndarray

Interpolate spectra onto a new wavenumber axis.

Useful for aligning spectra measured on different instruments or resampling to a uniform grid.

Parameters:

Name Type Description Default
intensities ndarray

Spectral intensities, shape (W,) or (N, W).

required
wavenumbers ndarray

Original wavenumber axis, shape (W,).

required
new_wavenumbers ndarray

Target wavenumber axis, shape (M,).

required
kind str

Interpolation method ("linear", "cubic", etc.). Passed to scipy.interpolate.interp1d.

'linear'

Returns:

Type Description
ndarray

Interpolated intensities, shape (M,) or (N, M).

Raises:

Type Description
SpectrumShapeError

If intensities is not 1-D or 2-D.

EmptySpectrumError

If intensities has zero elements.

Source code in src/spectrakit/ops/interpolate.py
def spectral_interpolate(
    intensities: np.ndarray,
    wavenumbers: np.ndarray,
    new_wavenumbers: np.ndarray,
    kind: str = "linear",
) -> np.ndarray:
    """Interpolate spectra onto a new wavenumber axis.

    Useful for aligning spectra measured on different instruments or
    resampling to a uniform grid.

    Args:
        intensities: Spectral intensities, shape ``(W,)`` or ``(N, W)``.
        wavenumbers: Original wavenumber axis, shape ``(W,)``.
        new_wavenumbers: Target wavenumber axis, shape ``(M,)``.
        kind: Interpolation method (``"linear"``, ``"cubic"``, etc.).
            Passed to ``scipy.interpolate.interp1d``.

    Returns:
        Interpolated intensities, shape ``(M,)`` or ``(N, M)``.

    Raises:
        SpectrumShapeError: If *intensities* is not 1-D or 2-D.
        EmptySpectrumError: If *intensities* has zero elements.
    """
    intensities = ensure_float64(intensities)
    wavenumbers = ensure_float64(wavenumbers)
    new_wavenumbers = ensure_float64(new_wavenumbers)
    validate_1d_or_2d(intensities)
    warn_if_not_finite(intensities)

    expected_w = intensities.shape[-1]
    if wavenumbers.shape[0] != expected_w:
        raise ValueError(
            f"wavenumbers length {wavenumbers.shape[0]} does not match "
            f"intensities spectral width {expected_w}"
        )

    if new_wavenumbers.min() < wavenumbers.min() or new_wavenumbers.max() > wavenumbers.max():
        warnings.warn(
            "new_wavenumbers extends beyond the original range "
            f"[{wavenumbers.min():.2f}, {wavenumbers.max():.2f}]. "
            "Extrapolated values may be unreliable.",
            stacklevel=2,
        )

    return apply_along_spectra(
        _interpolate_1d,
        intensities,
        wavenumbers=wavenumbers,
        new_wavenumbers=new_wavenumbers,
        kind=kind,
    )

spectrakit.ops.spectral_correlate

spectral_correlate(
    query: ndarray,
    reference: ndarray,
    *,
    mode: str = "full",
    normalize: bool = True,
) -> np.ndarray

Compute cross-correlation between spectral signals.

Wraps :func:scipy.signal.correlate with optional L2 normalization so the peak value equals the cosine similarity.

Parameters:

Name Type Description Default
query ndarray

Query spectrum, shape (W,) or (N, W).

required
reference ndarray

Reference spectrum, shape (W,).

required
mode str

Correlation mode passed to scipy.signal.correlate: "full" (default), "same", or "valid".

'full'
normalize bool

If True (default), normalize by the product of L2 norms.

True

Returns:

Type Description
ndarray

Cross-correlation array. Shape depends on mode:

ndarray
  • "full": (2W - 1,) or (N, 2W - 1)
ndarray
  • "same": (W,) or (N, W)
ndarray
  • "valid": (1,) or (N, 1) (when query and reference have the same width)

Raises:

Type Description
SpectrumShapeError

If query is not 1-D or 2-D, or reference is not 1-D.

SpectrumShapeError

If spectral widths do not match.

EmptySpectrumError

If inputs have zero elements.

ValueError

If mode is not one of "full", "same", "valid".

Examples:

>>> import numpy as np
>>> from spectrakit import spectral_correlate
>>> a = np.array([0.0, 1.0, 0.0])
>>> spectral_correlate(a, a, mode="same").shape
(3,)
Source code in src/spectrakit/ops/correlate.py
def spectral_correlate(
    query: np.ndarray,
    reference: np.ndarray,
    *,
    mode: str = "full",
    normalize: bool = True,
) -> np.ndarray:
    """Compute cross-correlation between spectral signals.

    Wraps :func:`scipy.signal.correlate` with optional L2 normalization
    so the peak value equals the cosine similarity.

    Args:
        query: Query spectrum, shape ``(W,)`` or ``(N, W)``.
        reference: Reference spectrum, shape ``(W,)``.
        mode: Correlation mode passed to ``scipy.signal.correlate``:
            ``"full"`` (default), ``"same"``, or ``"valid"``.
        normalize: If ``True`` (default), normalize by the product of
            L2 norms.

    Returns:
        Cross-correlation array.  Shape depends on *mode*:

        - ``"full"``: ``(2W - 1,)`` or ``(N, 2W - 1)``
        - ``"same"``: ``(W,)`` or ``(N, W)``
        - ``"valid"``: ``(1,)`` or ``(N, 1)`` (when query and reference
          have the same width)

    Raises:
        SpectrumShapeError: If *query* is not 1-D or 2-D, or *reference*
            is not 1-D.
        SpectrumShapeError: If spectral widths do not match.
        EmptySpectrumError: If inputs have zero elements.
        ValueError: If *mode* is not one of ``"full"``, ``"same"``,
            ``"valid"``.

    Examples:
        >>> import numpy as np
        >>> from spectrakit import spectral_correlate
        >>> a = np.array([0.0, 1.0, 0.0])
        >>> spectral_correlate(a, a, mode="same").shape
        (3,)
    """
    query = ensure_float64(query)
    reference = ensure_float64(reference)
    validate_1d_or_2d(query, name="query")
    validate_1d_or_2d(reference, name="reference")
    warn_if_not_finite(query, name="query")
    warn_if_not_finite(reference, name="reference")

    if reference.ndim != 1:
        raise ValueError("reference must be 1-D")
    validate_matching_width(query, reference)

    if mode not in _VALID_MODES:
        raise ValueError(f"mode must be one of {sorted(_VALID_MODES)}, got {mode!r}")

    return apply_along_spectra(
        _correlate_1d,
        query,
        reference=reference,
        mode=mode,
        normalize=normalize,
    )

spectrakit.ops.spectral_align

spectral_align(
    intensities: ndarray,
    reference: ndarray,
    *,
    max_shift: int | None = None,
    fill_value: float | None = None,
) -> tuple[np.ndarray, int | np.ndarray]

Align spectra to a reference using cross-correlation.

Finds the integer shift that maximizes cross-correlation between each spectrum and the reference, then applies the shift.

Parameters:

Name Type Description Default
intensities ndarray

Spectrum or batch to align, shape (W,) or (N, W).

required
reference ndarray

Reference spectrum, shape (W,).

required
max_shift int | None

Maximum allowed shift in points. None means no limit (shifts up to W - 1).

None
fill_value float | None

Value for positions exposed by the shift. If None, edge values are repeated (nearest-neighbor fill).

None

Returns:

Type Description
ndarray

Tuple of (aligned, shifts) where:

int | ndarray
  • aligned has the same shape as intensities.
tuple[ndarray, int | ndarray]
  • shifts is the integer offset applied (positive = shifted right, negative = shifted left). Scalar int for 1-D, np.ndarray of shape (N,) for 2-D.

Raises:

Type Description
SpectrumShapeError

If intensities is not 1-D or 2-D, or reference is not 1-D.

SpectrumShapeError

If spectral widths do not match.

EmptySpectrumError

If inputs have zero elements.

ValueError

If max_shift < 0.

Examples:

>>> import numpy as np
>>> from spectrakit import spectral_align
>>> ref = np.zeros(50); ref[25] = 1.0  # peak at 25
>>> shifted = np.zeros(50); shifted[30] = 1.0  # peak at 30
>>> aligned, shift = spectral_align(shifted, ref)
>>> shift
-5
Source code in src/spectrakit/ops/align.py
def spectral_align(
    intensities: np.ndarray,
    reference: np.ndarray,
    *,
    max_shift: int | None = None,
    fill_value: float | None = None,
) -> tuple[np.ndarray, int | np.ndarray]:
    """Align spectra to a reference using cross-correlation.

    Finds the integer shift that maximizes cross-correlation between
    each spectrum and the reference, then applies the shift.

    Args:
        intensities: Spectrum or batch to align, shape ``(W,)`` or
            ``(N, W)``.
        reference: Reference spectrum, shape ``(W,)``.
        max_shift: Maximum allowed shift in points.  ``None`` means no
            limit (shifts up to ``W - 1``).
        fill_value: Value for positions exposed by the shift.
            If ``None``, edge values are repeated (nearest-neighbor fill).

    Returns:
        Tuple of ``(aligned, shifts)`` where:

        - *aligned* has the same shape as *intensities*.
        - *shifts* is the integer offset applied (positive = shifted
          right, negative = shifted left).  Scalar ``int`` for 1-D,
          ``np.ndarray`` of shape ``(N,)`` for 2-D.

    Raises:
        SpectrumShapeError: If *intensities* is not 1-D or 2-D, or
            *reference* is not 1-D.
        SpectrumShapeError: If spectral widths do not match.
        EmptySpectrumError: If inputs have zero elements.
        ValueError: If *max_shift* < 0.

    Examples:
        >>> import numpy as np
        >>> from spectrakit import spectral_align
        >>> ref = np.zeros(50); ref[25] = 1.0  # peak at 25
        >>> shifted = np.zeros(50); shifted[30] = 1.0  # peak at 30
        >>> aligned, shift = spectral_align(shifted, ref)
        >>> shift
        -5
    """
    intensities = ensure_float64(intensities)
    reference = ensure_float64(reference)
    validate_1d_or_2d(intensities, name="intensities")
    validate_1d_or_2d(reference, name="reference")
    warn_if_not_finite(intensities, name="intensities")
    warn_if_not_finite(reference, name="reference")

    if reference.ndim != 1:
        raise ValueError("reference must be 1-D")
    validate_matching_width(intensities, reference)

    if max_shift is not None and max_shift < 0:
        raise ValueError(f"max_shift must be >= 0, got {max_shift}")

    if intensities.ndim == 1:
        aligned, shift = _align_1d(intensities, reference, max_shift, fill_value)
        return aligned, int(shift)

    # 2-D: process each row, collect shifts separately
    n_spectra = intensities.shape[0]
    aligned = np.empty_like(intensities)
    shifts = np.empty(n_spectra, dtype=np.intp)

    for i in range(n_spectra):
        aligned[i], shifts[i] = _align_1d(intensities[i], reference, max_shift, fill_value)

    return aligned, shifts