Skip to content

Spectrum batches and file checks

These helpers support reading and centroiding timsTOF data. Extraction methods return NumPy arrays.

Window batches

To process DIA or PRM windows with bounded reuse, pass windows in their existing order to iter_window_spectra. Adjacent windows of the same frame share one decode. Each result is a (window, peaks) pair. The numerical array matches window.centroid() with the same settings. No diagnostic accounting runs as part of batch extraction.

from itertools import islice
from tdfpy import DIA, MergePeaksCentroider, iter_window_spectra

with DIA(D_PATH) as reader:
    for window, peaks in iter_window_spectra(
        islice(reader.windows, 4),
        centroid=MergePeaksCentroider(max_peaks=10),
    ):
        assert peaks.shape[1] == 3
        assert window.frame_id > 0

For PRM, pass reader.transitions. Unsorted inputs retain caller order and may decode a frame again. Consume the iterator inside the reader's context. It retains the current frame and uses no global spectrum cache or worker pool. Returned numerical arrays remain usable after the reader closes.

Precursor batches

A PASEF frame holds windows of many precursors, so calling merged_peaks() on each precursor decodes the same frame many times. iter_precursor_spectra decodes each frame once and yields (precursor, peaks) pairs, where peaks equals precursor.merged_peaks(): an (N, 2) [m/z, intensity] array.

from tdfpy import DDA, iter_precursor_spectra

with DDA(D_PATH) as reader:
    for precursor, peaks in iter_precursor_spectra(reader.precursors):
        assert peaks.shape[1] == 2
        assert precursor.precursor_mz > 0

Pass precursors in reader order (or any subset of it). The last 64 decoded frames are kept, so a scattered order is still correct but may decode a frame again. Consume the iterator inside the reader's context.

File checks

For acquisition checks, run tdfpy validate sample.d or python -m tdfpy validate sample.d. Add --full to decode every binary frame. The command writes JSON and exits with status 0 on success or 1 for a failed check. The Python function returns a structured report:

from tdfpy import validate_acquisition

report = validate_acquisition(D_PATH)
assert report.valid
assert report.frames_checked > 0

Metadata mode checks supported metadata and calibration references. It does not validate compressed payloads. Full mode additionally runs the decoder's integrity checks for every frame, collecting frame-specific failures. Neither mode repairs data or proves numerical equivalence to vendor software.

Threads and CCS

The built-in extraction and gate paths can share an open reader across worker threads. Metadata needed by those paths is snapshotted when the reader opens. Direct access to td.conn retains SQLite's thread rules. User-written filters must be thread-safe themselves. Wait for workers before closing the reader.

All mobility_type="ccs" raw-spectrum conversions assume charge +1. Raw peaks do not identify charge states. Precursor.ccs uses a known precursor charge when present, falling back to +1 when it is absent. Precursor scan coordinates retain the original fractional metadata value.

For AI agents, an optional MCP server exposes acquisition queries, spectrum extraction, conversions, and file checks without changing the core Python installation.

API

tdfpy.iter_window_spectra

iter_window_spectra(
    windows: Iterable[DiaWindow | PrmTransition],
    *,
    exclude: ChargeStateRegion | None = None,
    smooth: Smooth | None = None,
    noise: NoiseSpec = None,
    centroid: Centroider | None = None,
    mobility_type: Literal[
        "ook0", "ccs", "voltage"
    ] = "ook0"
) -> Iterator[tuple[DiaWindow | PrmTransition, np.ndarray]]

Yield (window, peaks) pairs, decoding adjacent windows' frame once.

Pass reader.windows or reader.transitions in their existing order. Peaks have shape (N, 3), matching window.centroid with the same options. Only the current frame is retained. Unsorted input preserves caller order and may decode a frame again when it reappears. Keep readers open while consuming the iterator. Results contain no diagnostic or provenance wrappers.

Source code in src/tdfpy/processing.py
def iter_window_spectra(
    windows: Iterable[DiaWindow | PrmTransition],
    *,
    exclude: ChargeStateRegion | None = None,
    smooth: Smooth | None = None,
    noise: NoiseSpec = None,
    centroid: Centroider | None = None,
    mobility_type: Literal["ook0", "ccs", "voltage"] = "ook0",
) -> Iterator[tuple[DiaWindow | PrmTransition, np.ndarray]]:
    """Yield (window, peaks) pairs, decoding adjacent windows' frame once.

    Pass reader.windows or reader.transitions in their existing order. Peaks
    have shape (N, 3), matching window.centroid with the same options. Only the
    current frame is retained. Unsorted input preserves caller order and may
    decode a frame again when it reappears. Keep readers open while consuming
    the iterator. Results contain no diagnostic or provenance wrappers.
    """
    cfg = centroid if centroid is not None else MergePeaksCentroider()
    for (td, frame_id), group in groupby(windows, key=lambda w: (w.timsdata, w.frame_id)):
        spectrum = read_spectrum(td, frame_id)
        for window in group:
            td._require_open()
            prepared = _prepare_spectrum(
                spectrum,
                td,
                frame_id,
                scan_range=(window.scan_num_begin, window.scan_num_end),
                exclude=exclude,
                smoothing=smooth,
                noise=noise,
                mobility_type=mobility_type,
            )
            peaks = cfg(prepared, td, frame_id, mobility_type=mobility_type) if not prepared.empty else np.empty((0, 3), dtype=np.float64)
            yield window, peaks

tdfpy.iter_precursor_spectra

iter_precursor_spectra(
    precursors: Iterable[Precursor],
) -> Iterator[tuple[Precursor, np.ndarray]]

Yield (precursor, peaks) pairs, decoding each MS/MS frame once.

peaks equals :meth:Precursor.merged_peaks: the (N, 2) [m/z, intensity] mobility-collapsed spectrum over all of the precursor's PASEF windows. A PASEF frame carries windows of many precursors (often ten or more), so looping over merged_peaks() decodes each frame that many times; this decodes it once and serves every window from it.

Pass reader.precursors (or any subset) in its existing order. The most recent 64 decoded frames are kept, so any roughly frame-ordered input decodes each frame once; a very scattered order still gives correct results but may decode a frame again. Keep the reader open while consuming the iterator.

Raises:

Type Description
ReaderClosedError

If the reader was closed.

TdfpyError

If an item is not a :class:Precursor, or a window's scan range does not fit its frame (corrupt file).

Source code in src/tdfpy/processing.py
def iter_precursor_spectra(precursors: Iterable[Precursor]) -> Iterator[tuple[Precursor, np.ndarray]]:
    """Yield ``(precursor, peaks)`` pairs, decoding each MS/MS frame once.

    ``peaks`` equals :meth:`Precursor.merged_peaks`: the ``(N, 2)`` ``[m/z,
    intensity]`` mobility-collapsed spectrum over all of the precursor's PASEF
    windows. A PASEF frame carries windows of many precursors (often ten or
    more), so looping over ``merged_peaks()`` decodes each frame that many
    times; this decodes it once and serves every window from it.

    Pass ``reader.precursors`` (or any subset) in its existing order. The most
    recent 64 decoded frames are kept, so any roughly frame-ordered input decodes
    each frame once; a very scattered order still gives correct results but may
    decode a frame again. Keep the reader open while consuming the iterator.

    Raises:
        ReaderClosedError: If the reader was closed.
        TdfpyError: If an item is not a :class:`Precursor`, or a window's scan
            range does not fit its frame (corrupt file).
    """
    cache: OrderedDict[tuple[TimsData, int], tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None] = OrderedDict()

    for precursor in precursors:
        if not isinstance(precursor, Precursor):
            raise TdfpyError(
                f"iter_precursor_spectra takes DDA precursors, got {type(precursor).__name__}; use iter_window_spectra for DIA windows or PRM transitions."
            )
        td = precursor.timsdata

        def read(frame_id: int, begin: int, end: int, td=td) -> tuple[np.ndarray, np.ndarray]:
            td._require_open()
            key = (td, frame_id)
            if key in cache:
                cache.move_to_end(key)
                decoded = cache[key]
            else:
                full = td._decode(frame_id)
                decoded = None if full is None else full[1:]
                cache[key] = decoded
                if len(cache) > _PRECURSOR_FRAME_CACHE:
                    cache.popitem(last=False)
            if decoded is None or begin == end:
                return np.empty(0, dtype=np.uint32), np.empty(0, dtype=np.uint32)
            starts, counts, tof, intensity = decoded
            lo = int(starts[begin])
            hi = int(starts[end - 1] + counts[end - 1])
            return tof[lo:hi], intensity[lo:hi]

        ranges = [(info.frame_id, *info._window_scans()) for info in precursor.pasef_frame_msms_infos]
        yield precursor, _collapsed_spectrum(td, ranges, read)

tdfpy.validate_acquisition

validate_acquisition(
    analysis_dir: str | Path, *, full: bool = False
) -> ValidationReport

Check supported metadata, optionally decoding every frame.

Uses the same strict format and calibration guards as extraction. Collects per-frame failures and continues through the remaining frames. This is structural validation, not a comparison with independent vendor spectra.

Source code in src/tdfpy/validation.py
def validate_acquisition(analysis_dir: str | Path, *, full: bool = False) -> ValidationReport:
    """Check supported metadata, optionally decoding every frame.

    Uses the same strict format and calibration guards as extraction. Collects
    per-frame failures and continues through the remaining frames. This is
    structural validation, not a comparison with independent vendor spectra.
    """
    issues = []
    checked = 0
    failures = (
        OSError,
        ValueError,
        KeyError,
        TypeError,
        sqlite3.Error,
        UnsupportedTdfError,
        UnsupportedCalibrationError,
    )
    try:
        with TimsData(analysis_dir) as td:
            for fid in td.frame_ids:
                checked += 1
                try:
                    frame = td.frame_metadata(fid)
                    if frame.num_scans < 0 or frame.num_peaks < 0 or not np.isfinite(frame.rt):
                        raise TdfpyError("Invalid frame counts or retention time.")
                    td.calibration_key(fid)
                    # Exercise calibration references and finite conversion values.
                    mz = td.index_to_mz(fid, [0])
                    mobility = td.scan_num_to_ook0(fid, [0])
                    if not np.all(np.isfinite(mz)) or not np.all(np.isfinite(mobility)):
                        raise TdfpyError("Calibration produces non-finite coordinates.")
                    if full:
                        td.read_frame_arrays(fid)
                except failures as exc:
                    issues.append(ValidationIssue(fid, str(exc)))
    except failures as exc:
        issues.append(ValidationIssue(None, str(exc)))
    return ValidationReport(str(analysis_dir), "full" if full else "metadata", checked, tuple(issues))

tdfpy.ValidationReport dataclass

ValidationReport(
    analysis_directory: str,
    mode: Literal["metadata", "full"],
    frames_checked: int,
    issues: tuple[ValidationIssue, ...],
)

Validation outcome. Metadata mode does not verify compressed payloads.

tdfpy.ValidationIssue dataclass

ValidationIssue(frame_id: int | None, message: str)

An acquisition-level or frame-specific validation failure.