Skip to content

Low-level access

Direct access to the two files inside a .d folder: PandasTdf reads the analysis.tdf SQLite metadata into pandas DataFrames, and TimsData decodes frames from analysis.tdf_bin in pure Python (no Bruker native library). Prefer the high-level DDA/DIA/PRM readers unless you need raw frame or scan data.

tdfpy.PandasTdf dataclass

PandasTdf(db_path: str | Path)

A class for working with TDF (Bruker Data File) using pandas DataFrames.

calibration_info property

calibration_info: pd.DataFrame

The 'CalibrationInfo' table as a pandas DataFrame. :return: table as a pandas DataFrame

dia_frame_msms_info property

dia_frame_msms_info: pd.DataFrame

The 'DiaFrameMsMsInfo' table as a pandas DataFrame. :return: table as a pandas DataFrame

dia_frame_msms_window_groups property

dia_frame_msms_window_groups: pd.DataFrame

The 'DiaFrameMsMsWindowGroups' table as a pandas DataFrame. :return: table as a pandas DataFrame

dia_frame_msms_windows property

dia_frame_msms_windows: pd.DataFrame

The 'DiaFrameMsMsWindows' table as a pandas DataFrame. :return: table as a pandas DataFrame

error_log property

error_log: pd.DataFrame

The 'ErrorLog' table as a pandas DataFrame. :return: table as a pandas DataFrame

frame_msms_info property

frame_msms_info: pd.DataFrame

The 'FrameMsMsInfo' table as a pandas DataFrame. :return: table as a pandas DataFrame

frame_properties property

frame_properties: pd.DataFrame

The 'FrameProperties' table as a pandas DataFrame. :return: table as a pandas DataFrame

frames property

frames: pd.DataFrame

The 'Frames' table as a pandas DataFrame. :return: table as a pandas DataFrame

global_metadata property

global_metadata: pd.DataFrame

The 'GlobalMetadata' table as a pandas DataFrame. :return: table as a pandas DataFrame

group_properties property

group_properties: pd.DataFrame

The 'GroupProperties' table as a pandas DataFrame. :return: table as a pandas DataFrame

mz_calibration property

mz_calibration: pd.DataFrame

The 'MzCalibration' table as a pandas DataFrame. :return: table as a pandas DataFrame

pasef_frame_msms_info property

pasef_frame_msms_info: pd.DataFrame

The 'PasefFrameMsMsInfo' table as a pandas DataFrame. :return: table as a pandas DataFrame

precursors property

precursors: pd.DataFrame

The 'Precursors' table as a pandas DataFrame. :return: table as a pandas DataFrame

properties property

properties: pd.DataFrame

The 'Properties' table as a pandas DataFrame. :return: table as a pandas DataFrame

property_definitions property

property_definitions: pd.DataFrame

The 'PropertyDefinitions' table as a pandas DataFrame. :return: table as a pandas DataFrame

property_groups property

property_groups: pd.DataFrame

The 'PropertyGroups' table as a pandas DataFrame. :return: table as a pandas DataFrame

segments property

segments: pd.DataFrame

The 'Segments' table as a pandas DataFrame. :return: table as a pandas DataFrame

tims_calibration property

tims_calibration: pd.DataFrame

The 'TimsCalibration' table as a pandas DataFrame. :return: table as a pandas DataFrame

prm_frame_measurement_mode property

prm_frame_measurement_mode: pd.DataFrame

The 'PrmFrameMeasurementMode' table as a pandas DataFrame. :return: table as a pandas DataFrame

prm_frame_msms_info property

prm_frame_msms_info: pd.DataFrame

The 'PrmFrameMsMsInfo' table as a pandas DataFrame. :return: table as a pandas DataFrame

prm_targets property

prm_targets: pd.DataFrame

The 'PrmTargets' table as a pandas DataFrame. :return: table as a pandas DataFrame

is_dda property

is_dda: bool

Checks if the database contains DDA (Data-Dependent Acquisition) data.

Returns:

Name Type Description
bool bool

True if DDA data is present, False otherwise.

is_prm property

is_prm: bool

Checks if the database contains PRM (Parallel Reaction Monitoring) data.

Returns:

Name Type Description
bool bool

True if PRM data is present, False otherwise.

is_dia property

is_dia: bool

Checks if the database contains DIA (Data-Independent Acquisition) data.

Returns:

Name Type Description
bool bool

True if DIA data is present, False otherwise.

is_maldi property

is_maldi: bool

Checks if the database contains MALDI (Matrix-Assisted Laser Desorption/Ionization) data. Not supported in tdfpy, but this method can be used to check for MALDI data if it is added in the future.

Returns:

Name Type Description
bool bool

True if MALDI data is present, False otherwise.

get_table_names

get_table_names() -> list[str]

Retrieves the names of all tables in the SQLite database.

Returns:

Type Description
list[str]

list[str]: A list of table names in the database.

Raises:

Type Description
TdfpyError

If SQLite cannot read the database.

Source code in src/tdfpy/tdf.py
def get_table_names(self) -> list[str]:
    """
    Retrieves the names of all tables in the SQLite database.

    Returns:
        list[str]: A list of table names in the database.

    Raises:
        TdfpyError: If SQLite cannot read the database.
    """
    # closing(), not the connection's own context manager, which only ends
    # the transaction and would leave the handle open until GC.
    try:
        with closing(sqlite3.connect(Path(self.db_path).resolve().as_uri() + "?mode=ro", uri=True)) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
            table_names = [table[0] for table in cursor.fetchall()]
    except sqlite3.Error as exc:
        raise TdfpyError(f"Failed to list tables of TDF database {self.db_path}: {exc}") from exc
    return table_names

tdfpy.TimsData

TimsData(
    analysis_directory: str | os.PathLike[str],
    *,
    use_recalibrated_state: bool = False,
    pressure_compensation_strategy: PressureCompensationStrategy = PressureCompensationStrategy.NoPressureCompensation
)

Random-access reader for a Bruker .d folder.

Metadata is loaded eagerly on open; spectral data is read from analysis.tdf_bin on demand.

Reading frames from several threads through one open reader is safe: frame bytes are fetched with :func:os.pread, which takes its offset as an argument and so shares no file position between threads, and decompression goes through stateless one-shot entry points. Where os.pread is unavailable (Windows) the seek + read pair is serialised by a lock instead. close() is not safe to race against an in-flight read, and the sqlite3 connection on :attr:conn keeps sqlite3's own thread rules.

Raises:

Type Description
FileNotFoundError

If analysis.tdf or analysis.tdf_bin is missing.

UnsupportedTdfError

For unsupported options, compression types or a folder whose metadata is incomplete.

TdfpyError

If SQLite cannot read analysis.tdf.

TdfpyKeyError

From per-frame methods, for a frame ID not in the file.

ReaderClosedError

From spectral reads after :meth:close.

Source code in src/tdfpy/timsdata.py
def __init__(
    self,
    analysis_directory: str | os.PathLike[str],
    *,
    use_recalibrated_state: bool = False,
    pressure_compensation_strategy: PressureCompensationStrategy = PressureCompensationStrategy.NoPressureCompensation,
) -> None:
    analysis_directory = str(analysis_directory)

    if use_recalibrated_state:
        raise UnsupportedTdfError("use_recalibrated_state=True is not supported; tdfpy reads the calibration recorded in analysis.tdf.")
    if pressure_compensation_strategy is not PressureCompensationStrategy.NoPressureCompensation:
        raise UnsupportedTdfError(f"{pressure_compensation_strategy.name} is not supported; only NoPressureCompensation is implemented.")

    if not os.path.isdir(analysis_directory):
        raise FileNotFoundError(f"Analysis directory not found: {analysis_directory!r}")
    tdf_path = os.path.join(analysis_directory, "analysis.tdf")
    bin_path = os.path.join(analysis_directory, "analysis.tdf_bin")
    for path in (tdf_path, bin_path):
        if not os.path.exists(path):
            raise FileNotFoundError(f"{os.path.basename(path)} not found in {analysis_directory!r}")

    self.analysis_directory = analysis_directory
    self.conn: sqlite3.Connection | None = None
    self.handle: Any = None
    try:
        self.conn = sqlite3.connect(Path(tdf_path).resolve().as_uri() + "?mode=ro", uri=True)
        self.conn.row_factory = sqlite3.Row
        self._load_metadata()
    except sqlite3.Error as exc:
        self.close()
        raise TdfpyError(f"Cannot read {tdf_path!r}: {exc}") from exc
    except KeyError as exc:
        self.close()
        raise UnsupportedTdfError(f"{tdf_path!r} lacks required metadata {exc}; the file may be corrupt or from an unsupported schema.") from exc
    except BaseException:
        self.close()
        raise

    #: Serialises seek + read on the shared handle for the no-``pread``
    #: fallback. Unused, but still created, when ``os.pread`` exists.
    self._read_lock = threading.Lock()
    #: Open binary file object, or ``None`` once :meth:`close` has run.
    #: Callers use this only to test whether the reader is still open.
    try:
        self.handle = open(bin_path, "rb")
    except BaseException:
        self.close()
        raise
    self._fd: int = self.handle.fileno()

frame_ids property

frame_ids: tuple[int, ...]

Frame IDs in acquisition ID order. Requires an open reader.

frame_metadata

frame_metadata(frame_id: int) -> FrameMetadata

Read eagerly loaded metadata without accessing SQLite.

Source code in src/tdfpy/timsdata.py
def frame_metadata(self, frame_id: int) -> FrameMetadata:
    """Read eagerly loaded metadata without accessing SQLite."""
    self._require_open()
    self._frame(frame_id)
    return self._frame_metadata[frame_id]

metadata_table

metadata_table(name: str) -> tuple[sqlite3.Row, ...]

Read an immutable snapshot of a gate metadata table.

Supported names are PropertyDefinitions, GroupProperties, and DiaFrameMsMsWindows. An absent table produces an empty tuple.

Raises:

Type Description
TdfpyKeyError

If name is not one of the supported tables.

Source code in src/tdfpy/timsdata.py
def metadata_table(self, name: str) -> tuple[sqlite3.Row, ...]:
    """Read an immutable snapshot of a gate metadata table.

    Supported names are PropertyDefinitions, GroupProperties, and
    DiaFrameMsMsWindows. An absent table produces an empty tuple.

    Raises:
        TdfpyKeyError: If ``name`` is not one of the supported tables.
    """
    self._require_open()
    try:
        return self._metadata_tables[name]
    except KeyError:
        raise TdfpyKeyError(f"Unsupported metadata table {name!r}; choose from {sorted(self._metadata_tables)}.") from None

mz_calibration_key

mz_calibration_key(frame_id: int) -> tuple[float, ...]

Identify the effective m/z conversion, including temperature drift.

Source code in src/tdfpy/timsdata.py
def mz_calibration_key(self, frame_id: int) -> tuple[float, ...]:
    """Identify the effective m/z conversion, including temperature drift."""
    self._require_open()
    cal, t1, t2 = self._mz_cal(frame_id)
    return (
        cal.digitizer_timebase,
        cal.digitizer_delay,
        cal.c0,
        cal._c1_at(t1, t2),
        cal.c2,
    )

calibration_key

calibration_key(
    frame_id: int,
) -> tuple[tuple[float, ...], TimsCalibration]

Identify the effective m/z and mobility conversions for caching.

Source code in src/tdfpy/timsdata.py
def calibration_key(self, frame_id: int) -> tuple[tuple[float, ...], TimsCalibration]:
    """Identify the effective m/z and mobility conversions for caching."""
    return (self.mz_calibration_key(frame_id), self._tims_cal(frame_id))

close

close() -> None

Close analysis.tdf_bin and the SQLite connection. Safe to call twice.

Source code in src/tdfpy/timsdata.py
def close(self) -> None:
    """Close ``analysis.tdf_bin`` and the SQLite connection. Safe to call twice."""
    if getattr(self, "handle", None) is not None:
        self.handle.close()
        self.handle = None
    conn = getattr(self, "conn", None)
    if conn is not None:
        conn.close()
        self.conn = None

index_to_mz

index_to_mz(
    frame_id: int, indices: npt.ArrayLike
) -> npt.NDArray[np.float64]

Convert TOF sample indices to m/z for frame_id.

Source code in src/tdfpy/timsdata.py
def index_to_mz(self, frame_id: int, indices: npt.ArrayLike) -> npt.NDArray[np.float64]:
    """Convert TOF sample indices to m/z for ``frame_id``."""
    cal, t1, t2 = self._mz_cal(frame_id)
    return cal.index_to_mz(indices, t1, t2)

mz_to_index

mz_to_index(
    frame_id: int, mzs: npt.ArrayLike
) -> npt.NDArray[np.float64]

Convert m/z to (fractional) TOF sample indices for frame_id.

Source code in src/tdfpy/timsdata.py
def mz_to_index(self, frame_id: int, mzs: npt.ArrayLike) -> npt.NDArray[np.float64]:
    """Convert m/z to (fractional) TOF sample indices for ``frame_id``."""
    cal, t1, t2 = self._mz_cal(frame_id)
    return cal.mz_to_index(mzs, t1, t2)

scan_num_to_ook0

scan_num_to_ook0(
    frame_id: int, scan_nums: npt.ArrayLike
) -> npt.NDArray[np.float64]

Convert scan numbers to inverse reduced mobility 1/K0 (V·s/cm²).

Source code in src/tdfpy/timsdata.py
def scan_num_to_ook0(self, frame_id: int, scan_nums: npt.ArrayLike) -> npt.NDArray[np.float64]:
    """Convert scan numbers to inverse reduced mobility 1/K0 (V·s/cm²)."""
    return self._tims_cal(frame_id).scan_to_ook0(scan_nums)

ook0_to_scan_num

ook0_to_scan_num(
    frame_id: int, ook0: npt.ArrayLike
) -> npt.NDArray[np.float64]

Convert 1/K0 (V·s/cm²) to (fractional) scan numbers.

Source code in src/tdfpy/timsdata.py
def ook0_to_scan_num(self, frame_id: int, ook0: npt.ArrayLike) -> npt.NDArray[np.float64]:
    """Convert 1/K0 (V·s/cm²) to (fractional) scan numbers."""
    return self._tims_cal(frame_id).ook0_to_scan(ook0)

scan_num_to_voltage

scan_num_to_voltage(
    frame_id: int, scan_nums: npt.ArrayLike
) -> npt.NDArray[np.float64]

Convert scan numbers to TIMS ramp voltage.

Source code in src/tdfpy/timsdata.py
def scan_num_to_voltage(self, frame_id: int, scan_nums: npt.ArrayLike) -> npt.NDArray[np.float64]:
    """Convert scan numbers to TIMS ramp voltage."""
    return self._tims_cal(frame_id).scan_to_voltage(scan_nums)

voltage_to_scan_num

voltage_to_scan_num(
    frame_id: int, voltages: npt.ArrayLike
) -> npt.NDArray[np.float64]

Convert TIMS ramp voltage to (fractional) scan numbers.

Source code in src/tdfpy/timsdata.py
def voltage_to_scan_num(self, frame_id: int, voltages: npt.ArrayLike) -> npt.NDArray[np.float64]:
    """Convert TIMS ramp voltage to (fractional) scan numbers."""
    return self._tims_cal(frame_id).voltage_to_scan(voltages)

read_frame_arrays

read_frame_arrays(
    frame_id: int,
    scan_begin: int = 0,
    scan_end: int | None = None,
) -> tuple[
    npt.NDArray[np.int64],
    npt.NDArray[np.uint32],
    npt.NDArray[np.uint32],
]

Read scans [scan_begin, scan_end) as three flat, parallel arrays.

Returns (scan_indices, tof_indices, intensities), one entry per peak. This is the cheap path: peaks for a contiguous scan range are already contiguous in the decoded frame, so it slices rather than splitting the frame into per-scan arrays the way :meth:read_scans must.

Prefer this whenever you were going to concatenate read_scans output back together.

Raises:

Type Description
TdfpyError

If a bound is not an integer, or the range is outside 0 <= scan_begin <= scan_end <= num_scans. scan_end=None means num_scans; an empty range returns empty arrays.

TdfpyKeyError

If frame_id is not in the Frames table.

Source code in src/tdfpy/timsdata.py
def read_frame_arrays(
    self, frame_id: int, scan_begin: int = 0, scan_end: int | None = None
) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.uint32], npt.NDArray[np.uint32]]:
    """Read scans ``[scan_begin, scan_end)`` as three flat, parallel arrays.

    Returns ``(scan_indices, tof_indices, intensities)``, one entry per peak.
    This is the cheap path: peaks for a contiguous scan range are already
    contiguous in the decoded frame, so it slices rather than splitting the
    frame into per-scan arrays the way :meth:`read_scans` must.

    Prefer this whenever you were going to concatenate ``read_scans`` output
    back together.

    Raises:
        TdfpyError: If a bound is not an integer, or the range is outside
            ``0 <= scan_begin <= scan_end <= num_scans``. ``scan_end=None``
            means ``num_scans``; an empty range returns empty arrays.
        TdfpyKeyError: If ``frame_id`` is not in the Frames table.
    """
    begin, end = self._scan_range(frame_id, scan_begin, scan_end)
    # Decode even for an empty range so a corrupt frame is still reported.
    decoded = self._decode(frame_id, begin, end)
    if decoded is None or begin == end:
        return np.zeros(0, dtype=np.int64), _EMPTY_U32, _EMPTY_U32
    _, _, counts, tof, intensity = decoded
    scan_indices = np.repeat(np.arange(begin, end, dtype=np.int64), counts[begin:end])
    return scan_indices, tof, intensity

read_scans

read_scans(
    frame_id: int, scan_begin: int, scan_end: int
) -> list[
    tuple[npt.NDArray[np.uint32], npt.NDArray[np.uint32]]
]

Read scans [scan_begin, scan_end) of a frame.

Returns one (tof_indices, intensities) pair per scan. Intensities are normalised to a 100 ms accumulation window, matching Bruker.

See :meth:read_frame_arrays for a flat-array alternative that avoids materialising one array pair per scan.

Raises:

Type Description
TdfpyError

If a bound is not an integer, or the range is outside 0 <= scan_begin <= scan_end <= num_scans. An empty range (scan_begin == scan_end) returns [].

TdfpyKeyError

If frame_id is not in the Frames table.

Source code in src/tdfpy/timsdata.py
def read_scans(self, frame_id: int, scan_begin: int, scan_end: int) -> list[tuple[npt.NDArray[np.uint32], npt.NDArray[np.uint32]]]:
    """Read scans ``[scan_begin, scan_end)`` of a frame.

    Returns one ``(tof_indices, intensities)`` pair per scan. Intensities are
    normalised to a 100 ms accumulation window, matching Bruker.

    See :meth:`read_frame_arrays` for a flat-array alternative that avoids
    materialising one array pair per scan.

    Raises:
        TdfpyError: If a bound is not an integer, or the range is outside
            ``0 <= scan_begin <= scan_end <= num_scans``. An empty range
            (``scan_begin == scan_end``) returns ``[]``.
        TdfpyKeyError: If ``frame_id`` is not in the Frames table.
    """
    begin, end = self._scan_range(frame_id, scan_begin, scan_end)
    # Decode even for an empty range so a corrupt frame is still reported.
    decoded = self._decode(frame_id, begin, end)
    if decoded is None:
        return [(_EMPTY_U32, _EMPTY_U32) for _ in range(begin, end)]
    _, starts, counts, tof, intensity = decoded

    result = []
    for i in range(begin, end):
        start = int(starts[i])
        stop = start + int(counts[i])
        result.append((tof[start:stop], intensity[start:stop]))
    return result

tdfpy.timsdata_connect

timsdata_connect(
    analysis_dir: str | os.PathLike[str],
) -> Generator[TimsData, None, None]

Open a :class:TimsData and close it on exit.

Source code in src/tdfpy/timsdata.py
@contextmanager
def timsdata_connect(analysis_dir: str | os.PathLike[str]) -> Generator[TimsData, None, None]:
    """Open a :class:`TimsData` and close it on exit."""
    td: TimsData | None = None
    try:
        td = TimsData(str(analysis_dir))
        yield td
    finally:
        if td:
            td.close()

tdfpy.FrameMetadata dataclass

FrameMetadata(
    frame_id: int,
    rt: float,
    msms_type: int,
    polarity: str,
    num_scans: int,
    num_peaks: int,
    property_group_id: int | None,
    mz_calibration_id: int,
    tims_calibration_id: int,
    t1: float,
    t2: float,
)

Immutable per-frame metadata, loaded from the Frames table when the reader opens.

Returned by :meth:TimsData.frame_metadata. Reading it needs no SQLite access.

frame_id instance-attribute

frame_id: int

Frame ID (Frames.Id, 1-based).

rt instance-attribute

rt: float

Retention time in seconds (Frames.Time).

msms_type instance-attribute

msms_type: int

Frames.MsMsType: 0 MS1, 8 DDA MS2, 9 DIA MS2, 10 PRM MS2 (see :class:~tdfpy.MsMsType).

polarity instance-attribute

polarity: str

Frames.Polarity as stored ("+" or "-").

num_scans instance-attribute

num_scans: int

Number of TIMS scans in the frame.

num_peaks instance-attribute

num_peaks: int

Number of peaks stored for the frame.

property_group_id instance-attribute

property_group_id: int | None

Frames.PropertyGroup, or None when NULL.

mz_calibration_id instance-attribute

mz_calibration_id: int

Frames.MzCalibration: row ID in the MzCalibration table.

tims_calibration_id instance-attribute

tims_calibration_id: int

Frames.TimsCalibration: row ID in the TimsCalibration table.

t1 instance-attribute

t1: float

Frames.T1 temperature, used by the m/z calibration drift term.

t2 instance-attribute

t2: float

Frames.T2 temperature, used by the m/z calibration drift term.

Errors

Every tdfpy error subclasses TdfpyError (a ValueError). Unsupported or unvalidated formats raise instead of returning approximate values.

tdfpy.TdfpyError

Bases: ValueError

Base class of every tdfpy error.

Raised directly for invalid arguments (a negative tolerance, an unknown option name) and for SQLite errors while reading analysis.tdf.

tdfpy.TdfpyKeyError

Bases: TdfpyError, KeyError

An ID or key that is not in the acquisition.

Raised by lookup indexing (dda.precursors[99999]), by the MetaData and Calibration key accessors, and by :class:~tdfpy.TimsData for an unknown frame ID.

tdfpy.ReaderClosedError

Bases: TdfpyError, RuntimeError

Spectral data was requested after the reader was closed.

Frame elements keep a reference to the reader's :class:~tdfpy.TimsData; once the with block exits, their spectral accessors raise this instead of returning stale data.

tdfpy.AcquisitionTypeError

Bases: TdfpyError

A reader was opened on a .d folder of another acquisition type.

For example DDA("run.d") on a diaPASEF run. Check the type first with :func:~tdfpy.get_acquisition_type.

tdfpy.UnsupportedTdfError

Bases: TdfpyError, NotImplementedError

A .d folder this reader has not been validated against, or a corrupt one.

Raised for legacy compression types, unsupported reader options and any analysis.tdf_bin whose layout checks fail.

tdfpy.UnsupportedCalibrationError

Bases: TdfpyError, NotImplementedError

A calibration model type that has not been validated.

Bruker ships several model types; only those seen on real data are implemented. An unknown model raises rather than returning plausible but wrong numbers.