Centroiding¶
timsTOF raw data is profile-like: the binary file stores one intensity value per scan per m/z index, spread across hundreds of mobility bins. Centroiding collapses that cloud of raw measurements into a compact list of peaks — each with a single m/z, intensity, and ion mobility value.
tdfpy provides these convenience functions:
get_centroided_spectrum: high-level. Reads a full frame from disk, applies optional region exclusion, smoothing and noise filtering, and returns centroided peaks in one call.get_raw_peaks: the same pipeline without the centroider. Returns every raw peak.get_mobility_collapsed_spectrum: sums intensities per TOF index across one or more scan ranges (collapsing ion mobility) and merges them by m/z. This is how DDA precursor MS2 spectra (Precursor.merged_peaks()) are built.merge_peaks: low-level. Centroids pre-assembled NumPy arrays of m/z, intensity, and ion mobility values. Use this when you already have the raw arrays or need fine-grained control.
In practice, most workflows should call .centroid() directly on a Frame, DiaWindow, or
PrmTransition object — that method delegates to get_centroided_spectrum internally.
Numba JIT backend¶
When Numba is installed (it is included in the default
tdfpy dependencies), the core clustering loop runs as a JIT-compiled native function
(_merge_peaks_numba_kernel). This is typically 5–20× faster than the pure-Python
fallback for large frames. The backend is selected automatically:
from tdfpy import merge_peaks
# Numba used if available (default)
peaks = merge_peaks(mz, intensity, im)
# Force the Python fallback (useful for debugging or environments without Numba)
peaks = merge_peaks(mz, intensity, im, use_numba=False)
The first call after import triggers Numba's JIT compilation — expect a few seconds of overhead. Subsequent calls use the cached compiled kernel.
get_centroided_spectrum¶
Reads frame frame_id from the open TimsData connection, converts m/z indices to
m/z values, assembles the raw peak arrays, optionally filters noise, and runs centroiding.
from tdfpy import timsdata_connect, get_centroided_spectrum, MergePeaksCentroider
with timsdata_connect("experiment.d") as td:
# Default: 1/K0 ion mobility, MergePeaksCentroider with 8 ppm m/z tolerance
peaks = get_centroided_spectrum(td, frame_id=1)
print(peaks.shape) # (N, 3) — columns: [m/z, intensity, 1/K0]
# Tighter tolerances (set on the centroider), CCS instead of 1/K0
peaks = get_centroided_spectrum(
td,
frame_id=1,
mobility_type="ccs",
centroid=MergePeaksCentroider(mz_tolerance=5.0, im_tolerance=0.03),
)
# Noise filtering before centroiding (string shorthand)
peaks = get_centroided_spectrum(td, frame_id=1, noise="mad")
# Hard intensity threshold
peaks = get_centroided_spectrum(td, frame_id=1, noise=500.0)
# Composed pipeline + region exclusion + tuned filter
from tdfpy import ChargeStateRegion, MadThreshold, VerticalNoiseFilter
peaks = get_centroided_spectrum(
td, frame_id=1,
exclude=ChargeStateRegion(),
noise=[VerticalNoiseFilter(min_streak_scans=5), MadThreshold(k=3)],
)
# Watershed centroider (integer-index space, no float-m/z binning)
from tdfpy import WatershedCentroider
peaks = get_centroided_spectrum(
td, frame_id=1,
centroid=WatershedCentroider(attach_scan_half_width=10, attach_mz_idx_half_width=3),
)
The noise= parameter accepts the string shorthand ("mad", "percentile",
"histogram", "baseline", "iterative_median"), a numeric absolute
threshold, or any NoiseFilter instance / list — see
Noise filters for the full hierarchy. The exclude= parameter
accepts a ChargeStateRegion. The centroid= parameter
swaps the centroiding algorithm — see
Pipeline → Centroiders.
tdfpy.get_centroided_spectrum
¶
get_centroided_spectrum(
td: TimsData,
frame_id: int,
*,
scan_range: tuple[int, int] | None = None,
exclude: ChargeStateRegion | None = None,
smooth: Smooth | None = None,
noise: NoiseSpec = None,
mobility_type: Literal[
"ook0", "ccs", "voltage"
] = "ook0",
centroid: Centroider | None = None
) -> np.ndarray
Extract a centroided spectrum for a single frame.
Thin orchestrator over the :mod:tdfpy.pipeline ops. Threads a
:class:~tdfpy.pipeline.RawSpectrum through optional scan-range
restriction, region exclusion, intensity smoothing, and noise filtering,
then hands it to the centroider — which decides whether to operate in
integer index space (e.g. :class:~tdfpy.pipeline.WatershedCentroider) or
after float conversion (e.g. :class:~tdfpy.pipeline.MergePeaksCentroider).
Default centroider is :class:~tdfpy.pipeline.MergePeaksCentroider. Pass
smooth=Smooth(...) for a position-preserving box-sum/mean smoothing
pre-step; the :class:~tdfpy.pipeline.WatershedCentroider additionally
has its own seed-stabilising smoother via its smooth_*_half_width fields.
Returns an (N, 3) array of [mz, intensity, ion_mobility] centroids.
Source code in src/tdfpy/centroiding.py
get_raw_peaks¶
Runs the same read, exclusion, smoothing and noise steps as get_centroided_spectrum but
returns the raw peaks without centroiding.
tdfpy.get_raw_peaks
¶
get_raw_peaks(
td: TimsData,
frame_id: int,
*,
scan_range: tuple[int, int] | None = None,
exclude: ChargeStateRegion | None = None,
smooth: Smooth | None = None,
noise: NoiseSpec = None,
mobility_type: Literal[
"ook0", "ccs", "voltage"
] = "ook0"
) -> np.ndarray
Return raw peaks for a frame as a (N, 3) [mz, intensity, ion_mobility] array.
Thin orchestrator over the :mod:tdfpy.pipeline ops:
- :func:
pipeline.read_spectrum— read raw integer-index peaks. - :func:
pipeline.subset_scans— restrict to scan range, if given. - :func:
pipeline.exclude_region— dropexcluderegion, if given. - :func:
pipeline.smooth— box-smooth intensities, ifsmoothgiven. - :func:
pipeline.apply_noise— apply the coerced noise filter pipeline. - :func:
pipeline.convert— convert to(mz, intensity, ion_mobility).
For full control over ordering or to plug in custom steps, call the
pipeline ops directly — each takes and returns a
:class:~tdfpy.pipeline.RawSpectrum.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
td
|
TimsData
|
TimsData instance connected to the analysis directory. |
required |
frame_id
|
int
|
Frame ID to read. |
required |
scan_range
|
tuple[int, int] | None
|
Optional half-open |
None
|
exclude
|
ChargeStateRegion | None
|
Optional region of (m/z, 1/K0) space to drop wholesale — for
timsTOF MS1 use :class: |
None
|
smooth
|
Smooth | None
|
Optional :class: |
None
|
noise
|
NoiseSpec
|
One or more noise filters. Accepts an instance, a list/tuple
of instances, the string shorthand ( |
None
|
mobility_type
|
Literal['ook0', 'ccs', 'voltage']
|
Ion mobility representation — |
'ook0'
|
Source code in src/tdfpy/centroiding.py
get_mobility_collapsed_spectrum¶
tdfpy.get_mobility_collapsed_spectrum
¶
get_mobility_collapsed_spectrum(
td: TimsData,
scan_ranges: Sequence[tuple[int, int, int]],
*,
mz_tolerance: float = COLLAPSED_MZ_TOLERANCE_PPM,
mz_tolerance_unit: ToleranceUnit = "ppm",
use_numba: bool = True
) -> np.ndarray
Centroid a set of scan ranges with the mobility dimension summed away.
This is the shape of spectrum Bruker's tims_read_pasef_msms and
tims_extract_centroided_spectrum_for_frame return: intensities are
summed over the mobility axis, leaving a plain m/z spectrum. A single ion
smears across roughly 8-10 adjacent TOF bins, so the collapsed profile is
then centroided by greedy intensity-ordered merging.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
td
|
TimsData
|
Open :class: |
required |
scan_ranges
|
Sequence[tuple[int, int, int]]
|
|
required |
mz_tolerance
|
float
|
Merge tolerance for the greedy centroider. |
COLLAPSED_MZ_TOLERANCE_PPM
|
mz_tolerance_unit
|
ToleranceUnit
|
|
'ppm'
|
use_numba
|
bool
|
Use the JIT-compiled merge kernel when available. |
True
|
Returns:
| Type | Description |
|---|---|
np.ndarray
|
An |
np.ndarray
|
func: |
np.ndarray
|
i.e. ordered by the brightest raw point that anchored each centroid, |
np.ndarray
|
not by the centroid's own summed intensity. Sort explicitly if you |
np.ndarray
|
need descending centroid intensity. |
Note
Results are close to Bruker's peak picker but not identical to it -- Bruker's algorithm is proprietary and appears to smooth before picking. On the bundled fixtures the strong peaks agree to ~0.5 ppm and the total ion current to within 0.1%, with ~4% more peaks reported.
Source code in src/tdfpy/centroiding.py
merge_peaks¶
Centroids pre-assembled arrays. The algorithm is a greedy intensity-ordered scan: starting from the highest-intensity raw peak, every neighbouring peak within the m/z and ion mobility tolerances is merged into a single centroid via intensity-weighted averaging. Merged peaks are marked as used and skipped in subsequent iterations.
| Parameter | Default | Notes |
|---|---|---|
mz_tolerance |
8.0 |
Width of the m/z matching window |
mz_tolerance_unit |
"ppm" |
"ppm" or "da" |
im_tolerance |
0.1 |
Width of the ion mobility window |
im_tolerance_unit |
"relative" |
"relative" (fraction of 1/K0) or "absolute" |
min_peaks |
3 |
Raw peaks required to form a centroid; set to 0 or 1 to keep all |
max_peaks |
None |
Cap on output peaks by raw seed intensity, not final summed intensity |
peak_noise_filter |
False |
Suppress weak satellite points around each centroid so they cannot seed their own centroids |
peak_noise_window |
0.1 |
Half-width in Da of the satellite window |
peak_noise_end_fraction |
0.1 |
Ramp end, as a fraction of the anchor intensity, at the window edge |
use_numba |
True |
Set to False to force the Python fallback |
import numpy as np
from tdfpy import merge_peaks
mz = np.array([500.001, 500.002, 700.005, 700.006, 700.007])
inten = np.array([8000.0, 4000.0, 6000.0, 5000.0, 3000.0])
im = np.array([0.85, 0.85, 0.92, 0.92, 0.92])
peaks = merge_peaks(mz, inten, im, mz_tolerance=10.0, min_peaks=2)
print(peaks)
# shape (2, 3): two centroided peaks, columns [m/z, intensity, 1/K0]
Noise filtering vs min_peaks¶
The noise= parameter (available on get_centroided_spectrum,
.centroid(), and get_raw_peaks) chains noise filters before the
centroider runs — intensity thresholds, the
vertical-IM streak filter, or any
combination. Intensity-based estimators have a fundamental limitation:
they can't distinguish low-abundance real signal from electronic noise.
Methods like MadThreshold are anchored to the median of the
intensity distribution — if your sample has sparse signal, the
threshold can rise above legitimate low-abundance peaks.
A more reliable strategy is to increase min_peaks instead:
from tdfpy import merge_peaks
# Prefer: raise min_peaks to filter noise without discarding low-abundance signal
peaks = merge_peaks(mz, intensity, im, min_peaks=5)
# Noise arises from single scans; real peaks appear across multiple scans.
# min_peaks=5 means a centroid must be supported by at least 5 raw measurements.
Because electronic noise typically manifests as a singleton in a single
scan, requiring several supporting raw peaks is a structural filter —
it targets the origin of noise rather than its intensity. The
VerticalNoiseFilter extends this idea
to the IM axis, requiring peaks to appear as vertical streaks across
consecutive mobility scans.
Use intensity-based noise= filters only if you have a calibrated
threshold or a method validated for your acquisition; always verify
against noise=None first.
tdfpy.merge_peaks
¶
merge_peaks(
mz_array: np.ndarray,
intensity_array: np.ndarray,
ion_mobility_array: np.ndarray,
mz_tolerance: float = 8.0,
mz_tolerance_unit: ToleranceUnit = "ppm",
im_tolerance: float = 0.1,
im_tolerance_unit: Literal[
"relative", "absolute"
] = "relative",
min_peaks: int = 3,
max_peaks: int | None = None,
peak_noise_filter: bool = False,
peak_noise_window: float = 0.1,
peak_noise_end_fraction: float = 0.1,
use_numba: bool = True,
) -> np.ndarray
Centroid profile-like peaks using m/z and ion mobility tolerances.
This function implements a greedy clustering algorithm that centroids raw peaks (similar to profile mode data) within specified m/z and ion mobility windows. Peaks are processed in descending order of intensity, and nearby peaks are combined using intensity-weighted averaging to produce centroided peaks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mz_array
|
np.ndarray
|
Array of m/z values from raw/profile-like data |
required |
intensity_array
|
np.ndarray
|
Array of intensity values |
required |
ion_mobility_array
|
np.ndarray
|
Array of ion mobility values (1/K0 or CCS) |
required |
mz_tolerance
|
float
|
Tolerance for m/z matching during centroiding |
8.0
|
mz_tolerance_unit
|
ToleranceUnit
|
Type of m/z tolerance - "ppm" or "da" (daltons) |
'ppm'
|
im_tolerance
|
float
|
Tolerance for ion mobility matching during centroiding |
0.1
|
im_tolerance_unit
|
Literal['relative', 'absolute']
|
Type of ion mobility tolerance - "relative" or "absolute" |
'relative'
|
min_peaks
|
int
|
Minimum number of nearby raw peaks required to form a centroid. Set to 0 or 1 to keep all peaks (no filtering). |
3
|
max_peaks
|
int | None
|
Maximum centroids in descending raw seed intensity order. This does not rank final summed intensities. Nonpositive means unlimited. |
None
|
peak_noise_filter
|
bool
|
If True, after each centroid is formed suppress raw
points within ± |
False
|
peak_noise_window
|
float
|
Half-width in Da on each side of the anchor m/z over
which the peak-noise ramp is applied. Defaults to |
0.1
|
peak_noise_end_fraction
|
float
|
Fraction of the anchor's raw intensity used as
the suppression threshold at |
0.1
|
Returns:
| Type | Description |
|---|---|
np.ndarray
|
np.ndarray: Array of shape (N, 3) containing centroided peaks. Columns are: [mz, intensity, ion_mobility] |
Example
Source code in src/tdfpy/centroiding.py
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 | |
Raw-spectrum CCS conversion assumes charge +1. Use a known precursor charge with the explicit CCS conversion function for charge-specific values.