Skip to content

Ion mobility and MS/MS

Some instruments measure more than m/z. A Bruker timsTOF or a Waters Synapt can also separate ions by their ion mobility. A timsTOF can break selected ions into fragments (MS/MS). Thyra keeps both, next to the usual spectra.

Which data has them

Your data What Thyra keeps
Bruker timsTOF .d folder, with TIMS switched on ion mobility
Bruker timsTOF PASEF or iprm-PASEF imaging run ion mobility and MS/MS
imzML in continuous mode with a mobility array, as TIMSImaging writes ion mobility
imzML in processed mode with a mobility array, as TIMSCONVERT writes ion mobility
Waters Synapt .raw folder, with ion mobility on ion mobility (drift time)

You do not need to switch anything on. Thyra finds the mobility and the MS/MS data by itself.

What you get

Part What it holds When
The spectra, msi_dataset_z0 every pixel's spectrum, added up over mobility always
Mass-mobility heatmap, inside the spectra table the whole image in one picture: m/z against mobility when the data has mobility
Mobility table, msi_dataset_z0_mobility one column for each pair of m/z and mobility continuous imzML: always. timsTOF, processed imzML and Waters: add --mobility-grid
MS/MS table, msi_dataset_z0_msms the fragments, grouped by the ion they came from when every pixel fragments the same list of ions

The spectra table looks the same as for any other data. Tools that know nothing about mobility can still read it.

Look at the heatmap first

The heatmap shows whether mobility separates anything in your sample. Two bright spots at the same m/z but at different mobility are two different ions. The mobility table can show each of them as its own image.

import matplotlib.pyplot as plt
import numpy as np
import spatialdata as sd

sdata = sd.read_zarr("my_slide.zarr")
heat = sdata.tables["msi_dataset_z0"].uns["mobility_heatmap"]
plt.pcolormesh(heat["mobility_edges"], heat["mz_edges"], np.log1p(heat["counts"]))
plt.xlabel("1/K0 (V s/cm2)")
plt.ylabel("m/z")
plt.show()

Get the mobility table from timsTOF or processed imzML

For timsTOF data and processed imzML, ask for the mobility table when you convert:

thyra my_slide.d my_slide.zarr --mobility-grid

The result is larger, and the conversion takes longer. Continuous imzML files with a mobility array get the table without asking.

Advanced: how this table is built

Every pixel measures mobility at slightly different values. So Thyra puts them on shared mobility channels, the same channels as the heatmap: 256 on a timsTOF, one per drift bin on a Waters run. A box drawn on the heatmap then picks the same channels in the table. --mobility-bins, --mobility-min and --mobility-max change the channels. See Output format.

Two ions at one m/z

This makes two images from one m/z value: one for each mobility range.

mob = sdata.tables["msi_dataset_z0_mobility"]
mz = mob.var["mz"].to_numpy()
k0 = mob.var["mobility"].to_numpy()
near = np.abs(mz - 600.25) < 0.01
low = np.asarray(mob.X[:, near & (k0 < 1.3)].sum(axis=1)).ravel()
high = np.asarray(mob.X[:, near & (k0 >= 1.3)].sum(axis=1)).ravel()

low and high hold one value per pixel. Turn them into images with the pixel positions, as in Look at the result.

Collision cross section

The mobility table also gives each column's collision cross section (CCS), in square angstroms:

print(mob.var[["mz", "mobility", "ccs"]].head())

Thyra assumes every ion has charge 1, which is almost always true for MALDI. mob.uns["ccs"] lists this and the other assumptions. See Output format for how to redo it for another charge.

One ion's fragments

In an MS/MS run, the instrument picks a list of ions, called precursors. It breaks each one into fragments at every pixel. The MS/MS table keeps each precursor's fragments together, so every fragment has its own image.

msms = sdata.tables["msi_dataset_z0_msms"]
print(msms.var.groupby("precursor_index")["precursor_mz"].first())
block = (msms.var["precursor_index"] == 0).to_numpy()
image = np.asarray(msms.X[:, block].sum(axis=1)).ravel()

The first line lists the precursors. The last line adds up all fragments of precursor 0, one value per pixel.

Advanced: two precursors at the same m/z

A method can pick the same m/z twice, at two mobility values. That is how isomers are targeted. Thyra keeps them apart, so always select a precursor by precursor_index, not by its m/z. precursor_mobility tells the two apart. See Output format.

Advanced: reading these tables in your own tool

Recognise the tables by their columns, not by their names. A table with a mobility column is a mobility table. A table with a precursor_mz column is an MS/MS table. In both, var["mz"] repeats, so code that expects every m/z once must not be pointed at them. For tool developers lists the rules.

Not covered yet

  • Older Waters Synapt G1 runs with ion mobility on are refused. The Waters library cannot read them safely.
  • CCS is computed from 1/K0 only, never from a drift time.
  • diaPASEF and data-dependent PASEF runs give the spectra table only. Their list of precursors changes from pixel to pixel.
  • MALDI iprm-PASEF runs from the released Bruker software have been tested on a synthetic file only. If you have one, please get in touch.

Go deeper

Output format describes every part of these tables in full detail.