Skip to content

Peak Analysis

spectrakit.peaks.peaks_find

peaks_find(
    intensities: ndarray,
    wavenumbers: ndarray | None = None,
    height: float | None = None,
    distance: int = DEFAULT_DISTANCE,
    prominence: float | None = None,
) -> PeakResult

Find peaks in a 1-D spectrum.

Wraps scipy.signal.find_peaks with spectroscopy-friendly defaults and returns a structured result.

Parameters:

Name Type Description Default
intensities ndarray

Spectral intensities, shape (W,).

required
wavenumbers ndarray | None

Wavenumber axis, shape (W,). Used to report peak positions in wavenumber units.

None
height float | None

Minimum peak height. If None, uses the 10th percentile of the spectrum as a threshold.

None
distance int

Minimum number of points between peaks.

DEFAULT_DISTANCE
prominence float | None

Minimum peak prominence. If None, no prominence filter is applied.

None

Returns:

Type Description
PeakResult

PeakResult with indices, heights, and optional wavenumbers.

Raises:

Type Description
SpectrumShapeError

If input is not 1-D.

Source code in src/spectrakit/peaks/find.py
def peaks_find(
    intensities: np.ndarray,
    wavenumbers: np.ndarray | None = None,
    height: float | None = None,
    distance: int = DEFAULT_DISTANCE,
    prominence: float | None = None,
) -> PeakResult:
    """Find peaks in a 1-D spectrum.

    Wraps ``scipy.signal.find_peaks`` with spectroscopy-friendly
    defaults and returns a structured result.

    Args:
        intensities: Spectral intensities, shape ``(W,)``.
        wavenumbers: Wavenumber axis, shape ``(W,)``. Used to report
            peak positions in wavenumber units.
        height: Minimum peak height. If ``None``, uses the 10th
            percentile of the spectrum as a threshold.
        distance: Minimum number of points between peaks.
        prominence: Minimum peak prominence. If ``None``, no
            prominence filter is applied.

    Returns:
        ``PeakResult`` with indices, heights, and optional wavenumbers.

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

    if intensities.ndim != 1:
        raise SpectrumShapeError("peaks_find requires a 1-D spectrum. For batches, call per-row.")

    if height is None:
        height = float(np.percentile(intensities, DEFAULT_HEIGHT_PERCENTILE))

    kwargs: dict[str, float | int] = {"height": height, "distance": distance}
    if prominence is not None:
        kwargs["prominence"] = prominence

    indices, properties = scipy_find_peaks(intensities, **kwargs)

    peak_wavenumbers = None
    if wavenumbers is not None:
        wavenumbers = ensure_float64(wavenumbers)
        peak_wavenumbers = wavenumbers[indices]

    return PeakResult(
        indices=indices,
        heights=intensities[indices],
        wavenumbers=peak_wavenumbers,
        properties=properties,
    )

spectrakit.peaks.PeakResult dataclass

Container for peak detection results.

Attributes:

Name Type Description
indices ndarray

Array of peak indices, shape (P,).

heights ndarray

Peak heights at the detected positions, shape (P,).

wavenumbers ndarray | None

Peak wavenumber positions if wavenumbers were provided, shape (P,). None otherwise.

Source code in src/spectrakit/peaks/find.py
@dataclass
class PeakResult:
    """Container for peak detection results.

    Attributes:
        indices: Array of peak indices, shape ``(P,)``.
        heights: Peak heights at the detected positions, shape ``(P,)``.
        wavenumbers: Peak wavenumber positions if wavenumbers were
            provided, shape ``(P,)``. ``None`` otherwise.
    """

    indices: np.ndarray
    heights: np.ndarray
    wavenumbers: np.ndarray | None = None
    properties: dict[str, np.ndarray] = field(default_factory=dict)

spectrakit.peaks.peaks_integrate

peaks_integrate(
    intensities: ndarray,
    wavenumbers: ndarray | None = None,
    ranges: list[tuple[float, float]] | None = None,
) -> np.ndarray | float

Integrate peak areas over specified wavenumber ranges.

If ranges is provided, computes the trapezoidal integral for each range. Otherwise, integrates the entire spectrum.

Parameters:

Name Type Description Default
intensities ndarray

Spectral intensities, shape (W,).

required
wavenumbers ndarray | None

Wavenumber axis, shape (W,). Required when ranges is specified.

None
ranges list[tuple[float, float]] | None

List of (start, end) wavenumber ranges to integrate. Each range defines a spectral region. If None, integrates the full spectrum.

None

Returns:

Type Description
ndarray | float

If ranges is None, a scalar (total area). If ranges

ndarray | float

is provided, an array of shape (len(ranges),) with the area

ndarray | float

for each range.

Raises:

Type Description
ValueError

If ranges is specified but wavenumbers is None.

Source code in src/spectrakit/peaks/integrate.py
def peaks_integrate(
    intensities: np.ndarray,
    wavenumbers: np.ndarray | None = None,
    ranges: list[tuple[float, float]] | None = None,
) -> np.ndarray | float:
    """Integrate peak areas over specified wavenumber ranges.

    If ``ranges`` is provided, computes the trapezoidal integral for
    each range. Otherwise, integrates the entire spectrum.

    Args:
        intensities: Spectral intensities, shape ``(W,)``.
        wavenumbers: Wavenumber axis, shape ``(W,)``. Required when
            ``ranges`` is specified.
        ranges: List of ``(start, end)`` wavenumber ranges to integrate.
            Each range defines a spectral region. If ``None``, integrates
            the full spectrum.

    Returns:
        If ``ranges`` is ``None``, a scalar (total area). If ``ranges``
        is provided, an array of shape ``(len(ranges),)`` with the area
        for each range.

    Raises:
        ValueError: If *ranges* is specified but *wavenumbers* is ``None``.
    """
    intensities = ensure_float64(intensities)
    validate_1d_or_2d(intensities)
    if intensities.ndim != 1:
        raise SpectrumShapeError(
            f"peaks_integrate requires 1-D input, got shape {intensities.shape}. "
            "Apply row-by-row for 2-D batches."
        )
    warn_if_not_finite(intensities)

    if ranges is None:
        return float(np.trapezoid(intensities, x=wavenumbers))

    if wavenumbers is None:
        raise ValueError("wavenumbers are required when ranges is specified")

    wavenumbers = ensure_float64(wavenumbers)
    areas = []

    for start, end in ranges:
        low, high = min(start, end), max(start, end)
        mask = (wavenumbers >= low) & (wavenumbers <= high)

        if not np.any(mask):
            areas.append(0.0)
            continue

        region_wn = wavenumbers[mask]
        region_y = intensities[mask]
        areas.append(float(np.trapezoid(region_y, x=region_wn)))

    return np.array(areas, dtype=np.float64)