Skip to content

Commit 9576680

Browse files
Add ion mobility and mobilogram support
- Added float_data_arrays property to Py_MSSpectrum for accessing/setting float data arrays - Added ion_mobility property for convenient access to ion mobility data - Added drift_time property for spectrum-level drift time - Updated to_dataframe() to include float data arrays as columns - Updated from_dataframe() to create float data arrays from DataFrame columns - Created Py_Mobilogram wrapper class for MSChromatogram (mobilograms) - Added comprehensive tests for all new functionality - All tests passing (93 tests, 86% coverage) Co-authored-by: timosachsenberg <[email protected]>
1 parent ba71cf5 commit 9576680

File tree

5 files changed

+839
-85
lines changed

5 files changed

+839
-85
lines changed

openms_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from .py_msexperiment import Py_MSExperiment
2525
from .py_msspectrum import Py_MSSpectrum
26+
from .py_mobilogram import Py_Mobilogram
2627
from .py_feature import Py_Feature
2728
from .py_featuremap import Py_FeatureMap
2829
from .py_consensusmap import Py_ConsensusMap
@@ -99,6 +100,7 @@ def get_example(name: str, *, load: bool = False, target_dir: Union[str, Path, N
99100
__all__ = [
100101
"Py_MSExperiment",
101102
"Py_MSSpectrum",
103+
"Py_Mobilogram",
102104
"Py_Feature",
103105
"Py_FeatureMap",
104106
"Py_ConsensusMap",

openms_python/py_mobilogram.py

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
"""
2+
Pythonic wrapper for pyOpenMS MSChromatogram for mobilogram representation.
3+
4+
A mobilogram is a chromatogram in the ion mobility dimension, representing
5+
intensity vs. drift time for a specific m/z value.
6+
"""
7+
8+
from typing import Tuple, Optional
9+
import numpy as np
10+
import pandas as pd
11+
import pyopenms as oms
12+
13+
from ._meta_mapping import MetaInfoMappingMixin
14+
15+
16+
class Py_Mobilogram(MetaInfoMappingMixin):
17+
"""
18+
A Pythonic wrapper around pyOpenMS MSChromatogram for mobilograms.
19+
20+
A mobilogram represents the ion mobility dimension for a specific m/z,
21+
showing intensity vs. drift time (or other ion mobility values).
22+
23+
Example:
24+
>>> mob = Py_Mobilogram(native_chromatogram)
25+
>>> print(f"m/z: {mob.mz:.4f}")
26+
>>> print(f"Number of points: {len(mob)}")
27+
>>> drift_times, intensities = mob.peaks
28+
>>> df = mob.to_dataframe()
29+
"""
30+
31+
def __init__(self, native_chromatogram: oms.MSChromatogram):
32+
"""
33+
Initialize Mobilogram wrapper.
34+
35+
Args:
36+
native_chromatogram: pyOpenMS MSChromatogram object
37+
"""
38+
self._chromatogram = native_chromatogram
39+
40+
# ==================== Meta-info support ====================
41+
42+
def _meta_object(self) -> oms.MetaInfoInterface:
43+
return self._chromatogram
44+
45+
# ==================== Pythonic Properties ====================
46+
47+
@property
48+
def name(self) -> str:
49+
"""Get the name of this mobilogram."""
50+
return self._chromatogram.getName()
51+
52+
@name.setter
53+
def name(self, value: str):
54+
"""Set the name of this mobilogram."""
55+
self._chromatogram.setName(value)
56+
57+
@property
58+
def mz(self) -> Optional[float]:
59+
"""
60+
Get the m/z value this mobilogram represents.
61+
62+
Returns from metadata if available, None otherwise.
63+
"""
64+
if self._chromatogram.metaValueExists("mz"):
65+
return float(self._chromatogram.getMetaValue("mz"))
66+
return None
67+
68+
@mz.setter
69+
def mz(self, value: float):
70+
"""Set the m/z value this mobilogram represents."""
71+
self._chromatogram.setMetaValue("mz", value)
72+
73+
@property
74+
def drift_time(self) -> np.ndarray:
75+
"""
76+
Get drift time values as a NumPy array.
77+
78+
Returns:
79+
NumPy array of drift time values
80+
"""
81+
rt, _ = self.peaks
82+
return rt
83+
84+
@property
85+
def intensity(self) -> np.ndarray:
86+
"""
87+
Get intensity values as a NumPy array.
88+
89+
Returns:
90+
NumPy array of intensity values
91+
"""
92+
_, intensity = self.peaks
93+
return intensity
94+
95+
@property
96+
def peaks(self) -> Tuple[np.ndarray, np.ndarray]:
97+
"""
98+
Get mobilogram data as NumPy arrays.
99+
100+
Returns:
101+
Tuple of (drift_time_array, intensity_array)
102+
"""
103+
rt, intensity = self._chromatogram.get_peaks()
104+
return np.array(rt), np.array(intensity)
105+
106+
@peaks.setter
107+
def peaks(self, values: Tuple[np.ndarray, np.ndarray]):
108+
"""
109+
Set mobilogram data from NumPy arrays.
110+
111+
Args:
112+
values: Tuple of (drift_time_array, intensity_array)
113+
"""
114+
drift_time, intensity = values
115+
# Clear existing peaks
116+
self._chromatogram.clear(False)
117+
# Add new peaks
118+
for dt, i in zip(drift_time, intensity):
119+
peak = oms.ChromatogramPeak()
120+
peak.setRT(float(dt))
121+
peak.setIntensity(float(i))
122+
self._chromatogram.push_back(peak)
123+
124+
@property
125+
def total_ion_current(self) -> float:
126+
"""Get total ion current (sum of all intensities)."""
127+
return float(np.sum(self.intensity))
128+
129+
@property
130+
def base_peak_drift_time(self) -> Optional[float]:
131+
"""Get drift time of the base peak (most intense point)."""
132+
if len(self) == 0:
133+
return None
134+
drift_time, intensities = self.peaks
135+
return float(drift_time[np.argmax(intensities)])
136+
137+
@property
138+
def base_peak_intensity(self) -> Optional[float]:
139+
"""Get intensity of the base peak."""
140+
if len(self) == 0:
141+
return None
142+
return float(np.max(self.intensity))
143+
144+
# ==================== Magic Methods ====================
145+
146+
def __len__(self) -> int:
147+
"""Return number of points in the mobilogram."""
148+
return self._chromatogram.size()
149+
150+
def __repr__(self) -> str:
151+
"""Return string representation."""
152+
mz_str = f"m/z={self.mz:.4f}" if self.mz is not None else "m/z=unset"
153+
return f"Mobilogram({mz_str}, points={len(self)}, " f"TIC={self.total_ion_current:.2e})"
154+
155+
def __str__(self) -> str:
156+
"""Return human-readable string."""
157+
return self.__repr__()
158+
159+
# ==================== Conversion Methods ====================
160+
161+
def to_numpy(self) -> np.ndarray:
162+
"""
163+
Convert mobilogram to NumPy arrays.
164+
165+
Returns:
166+
Tuple of (drift_time_array, intensity_array)
167+
"""
168+
return np.array(self.peaks)
169+
170+
def to_dataframe(self) -> pd.DataFrame:
171+
"""
172+
Convert mobilogram to pandas DataFrame.
173+
174+
Returns:
175+
DataFrame with columns: drift_time, intensity
176+
177+
Example:
178+
>>> df = mob.to_dataframe()
179+
>>> df.head()
180+
drift_time intensity
181+
0 1.5 100.0
182+
1 2.0 150.0
183+
...
184+
"""
185+
drift_time, intensity = self.peaks
186+
data = {"drift_time": drift_time, "intensity": intensity}
187+
188+
# Add m/z as a column if available
189+
if self.mz is not None:
190+
data["mz"] = self.mz
191+
192+
return pd.DataFrame(data)
193+
194+
@classmethod
195+
def from_dataframe(cls, df: pd.DataFrame, **metadata) -> "Py_Mobilogram":
196+
"""
197+
Create mobilogram from pandas DataFrame.
198+
199+
Args:
200+
df: DataFrame with 'drift_time' and 'intensity' columns
201+
**metadata: Optional metadata (name, mz, etc.)
202+
203+
Returns:
204+
Mobilogram object
205+
206+
Example:
207+
>>> df = pd.DataFrame({
208+
... 'drift_time': [1.5, 2.0, 2.5],
209+
... 'intensity': [100, 150, 120]
210+
... })
211+
>>> mob = Py_Mobilogram.from_dataframe(df, mz=500.0, name="mobilogram_500")
212+
"""
213+
chrom = oms.MSChromatogram()
214+
215+
# Add peaks
216+
for dt, intensity in zip(df["drift_time"].values, df["intensity"].values):
217+
peak = oms.ChromatogramPeak()
218+
peak.setRT(float(dt))
219+
peak.setIntensity(float(intensity))
220+
chrom.push_back(peak)
221+
222+
# Set metadata
223+
if "name" in metadata:
224+
chrom.setName(metadata["name"])
225+
226+
# Get m/z from metadata or DataFrame
227+
mz_value = metadata.get("mz")
228+
if mz_value is None and "mz" in df.columns:
229+
# Extract m/z from DataFrame (take first value if it's a column)
230+
mz_value = float(df["mz"].iloc[0])
231+
232+
if mz_value is not None:
233+
chrom.setMetaValue("mz", float(mz_value))
234+
235+
# Mark as mobilogram type
236+
chrom.setMetaValue("chromatogram_type", "mobilogram")
237+
238+
return cls(chrom)
239+
240+
@classmethod
241+
def from_arrays(
242+
cls,
243+
drift_time: np.ndarray,
244+
intensity: np.ndarray,
245+
mz: Optional[float] = None,
246+
name: Optional[str] = None,
247+
) -> "Py_Mobilogram":
248+
"""
249+
Create mobilogram from NumPy arrays.
250+
251+
Args:
252+
drift_time: Array of drift time values
253+
intensity: Array of intensity values
254+
mz: Optional m/z value this mobilogram represents
255+
name: Optional name for the mobilogram
256+
257+
Returns:
258+
Mobilogram object
259+
260+
Example:
261+
>>> mob = Py_Mobilogram.from_arrays(
262+
... np.array([1.5, 2.0, 2.5]),
263+
... np.array([100, 150, 120]),
264+
... mz=500.0
265+
... )
266+
"""
267+
chrom = oms.MSChromatogram()
268+
269+
# Add peaks
270+
for dt, i in zip(drift_time, intensity):
271+
peak = oms.ChromatogramPeak()
272+
peak.setRT(float(dt))
273+
peak.setIntensity(float(i))
274+
chrom.push_back(peak)
275+
276+
# Set metadata
277+
if name:
278+
chrom.setName(name)
279+
if mz is not None:
280+
chrom.setMetaValue("mz", float(mz))
281+
282+
# Mark as mobilogram type
283+
chrom.setMetaValue("chromatogram_type", "mobilogram")
284+
285+
return cls(chrom)
286+
287+
# ==================== Access to Native Object ====================
288+
289+
@property
290+
def native(self) -> oms.MSChromatogram:
291+
"""
292+
Get the underlying pyOpenMS MSChromatogram object.
293+
294+
Use this when you need to access pyOpenMS-specific methods
295+
not wrapped by this class.
296+
"""
297+
return self._chromatogram

0 commit comments

Comments
 (0)