Skip to content

Commit 72ef880

Browse files
Add algorithm selection to feature grouping and peak picking methods (#19)
* Initial plan * Add algorithm selection options for feature grouping Co-authored-by: timosachsenberg <5803621+timosachsenberg@users.noreply.github.com> * Add iterative peak picking algorithm option Co-authored-by: timosachsenberg <5803621+timosachsenberg@users.noreply.github.com> * Update README with new algorithm selection options Co-authored-by: timosachsenberg <5803621+timosachsenberg@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: timosachsenberg <5803621+timosachsenberg@users.noreply.github.com>
1 parent 6548c14 commit 72ef880

File tree

7 files changed

+186
-8
lines changed

7 files changed

+186
-8
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ helper performs three steps:
123123

124124
1. Copies the incoming feature maps to avoid mutating your data
125125
2. Aligns the feature maps with your choice of OpenMS alignment algorithm
126-
3. Links the aligned runs using `FeatureGroupingAlgorithmQT`
126+
3. Links the aligned runs using your choice of feature grouping algorithm
127127

128128
```python
129129
from openms_python import Py_FeatureMap, Py_ConsensusMap
@@ -137,6 +137,8 @@ consensus = Py_ConsensusMap.align_and_link(
137137
feature_maps,
138138
alignment_method="pose_clustering", # or "identification" / "identity"
139139
alignment_params={"max_rt_shift": 15.0},
140+
grouping_method="qt", # or "kd" / "labeled" / "unlabeled" (default: "qt")
141+
grouping_params={"distance_RT:max_difference": 100.0},
140142
)
141143

142144
print(f"Consensus contains {len(consensus)} features")
@@ -468,7 +470,11 @@ annotated = map_identifications_to_features(feature_map, filtered)
468470

469471
# 3) Align multiple maps and link them into a consensus representation
470472
aligned = align_feature_maps([annotated, second_run])
471-
consensus = link_features(aligned)
473+
consensus = link_features(
474+
aligned,
475+
grouping_method="qt", # or "kd" / "labeled" / "unlabeled"
476+
params={"distance_RT:max_difference": 100.0}
477+
)
472478

473479
# 4) Export a tidy quantitation table (per-sample intensities)
474480
quant_df = export_quant_table(consensus)
@@ -497,7 +503,10 @@ picker.pickExperiment(exp, centroided, True)
497503
```python
498504
from openms_python import Py_MSExperiment
499505

500-
centroided = exp.pick_peaks(method="HiRes", params={"signal_to_noise": 3.0})
506+
# Choose from multiple peak picking algorithms
507+
centroided = exp.pick_peaks(method="hires", params={"signal_to_noise": 3.0})
508+
# Available methods: "hires" (default), "cwt", "iterative"
509+
501510
# or modify in-place
502511
exp.pick_peaks(inplace=True)
503512
```

openms_python/py_consensusmap.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ def align_and_link(
164164
*,
165165
alignment_method: str = "pose_clustering",
166166
alignment_params: Optional[Dict[str, Union[int, float, str]]] = None,
167+
grouping_method: str = "qt",
168+
grouping_params: Optional[Dict[str, Union[int, float, str]]] = None,
167169
) -> 'Py_ConsensusMap':
168170
"""Align multiple feature maps and return their linked consensus map.
169171
@@ -180,14 +182,21 @@ def align_and_link(
180182
alignment_params:
181183
Optional dictionary of parameters applied to the selected
182184
alignment algorithm.
185+
grouping_method:
186+
Name of the OpenMS feature grouping algorithm to use. Supported values
187+
are ``"qt"`` (QT clustering, default), ``"kd"`` (KD-tree based),
188+
``"labeled"`` (for labeled data), and ``"unlabeled"`` (for unlabeled data).
189+
grouping_params:
190+
Optional dictionary of parameters applied to the selected
191+
grouping algorithm.
183192
"""
184193

185194
if not feature_maps:
186195
return cls()
187196

188197
native_maps = [cls._copy_feature_map(feature_map) for feature_map in feature_maps]
189198
cls._align_feature_maps(native_maps, alignment_method, alignment_params)
190-
consensus_map = cls._link_feature_maps(native_maps)
199+
consensus_map = cls._link_feature_maps(native_maps, grouping_method, grouping_params)
191200
return cls(consensus_map)
192201

193202
# ==================== pandas integration ====================
@@ -388,11 +397,37 @@ def _create_alignment_algorithm(method: str):
388397
"Unsupported alignment_method. Use 'pose_clustering', 'identification', or 'identity'."
389398
)
390399

400+
@staticmethod
401+
def _create_grouping_algorithm(method: str):
402+
"""Create the appropriate feature grouping algorithm."""
403+
normalized = method.lower()
404+
if normalized in {"qt", "qtcluster"}:
405+
return oms.FeatureGroupingAlgorithmQT()
406+
if normalized in {"kd", "tree"}:
407+
return oms.FeatureGroupingAlgorithmKD()
408+
if normalized == "labeled":
409+
return oms.FeatureGroupingAlgorithmLabeled()
410+
if normalized == "unlabeled":
411+
return oms.FeatureGroupingAlgorithmUnlabeled()
412+
raise ValueError(
413+
"Unsupported grouping_method. Use 'qt', 'kd', 'labeled', or 'unlabeled'."
414+
)
415+
391416
@staticmethod
392417
def _link_feature_maps(
393418
feature_maps: Sequence[oms.FeatureMap],
419+
grouping_method: str = "qt",
420+
grouping_params: Optional[Dict[str, Union[int, float, str]]] = None,
394421
) -> oms.ConsensusMap:
395-
grouping = oms.FeatureGroupingAlgorithmQT()
422+
grouping = Py_ConsensusMap._create_grouping_algorithm(grouping_method)
423+
424+
# Apply parameters if provided
425+
if grouping_params:
426+
params = grouping.getDefaults()
427+
for key, value in grouping_params.items():
428+
params.setValue(str(key), value)
429+
grouping.setParameters(params)
430+
396431
consensus_map = oms.ConsensusMap()
397432
grouping.group(feature_maps, consensus_map)
398433

openms_python/py_msexperiment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
PEAK_PICKER_REGISTRY: Dict[str, Any] = {
1717
"hires": oms.PeakPickerHiRes,
1818
"cwt": getattr(oms, "PeakPickerCWT", oms.PeakPickerHiRes),
19+
"iterative": oms.PeakPickerIterative,
1920
}
2021

2122
_FeatureMapLike = Union[Py_FeatureMap, oms.FeatureMap]

openms_python/workflows.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,43 @@ def align_feature_maps(
193193
def link_features(
194194
feature_maps: Sequence[_FeatureMapLike],
195195
*,
196+
grouping_method: str = "qt",
196197
params: Optional[Dict[str, Union[int, float, str]]] = None,
197198
) -> Py_ConsensusMap:
198-
"""Group features across runs into a consensus map."""
199-
200-
grouping = oms.FeatureGroupingAlgorithmQT()
199+
"""Group features across runs into a consensus map.
200+
201+
Parameters
202+
----------
203+
feature_maps:
204+
Sequence of feature maps to link.
205+
grouping_method:
206+
Name of the OpenMS feature grouping algorithm to use. Supported values
207+
are ``"qt"`` (QT clustering, default), ``"kd"`` (KD-tree based),
208+
``"labeled"`` (for labeled data), and ``"unlabeled"`` (for unlabeled data).
209+
params:
210+
Optional dictionary of parameters applied to the selected grouping algorithm.
211+
212+
Returns
213+
-------
214+
Py_ConsensusMap
215+
The linked consensus map.
216+
"""
217+
218+
# Create grouping algorithm
219+
normalized = grouping_method.lower()
220+
if normalized in {"qt", "qtcluster"}:
221+
grouping = oms.FeatureGroupingAlgorithmQT()
222+
elif normalized in {"kd", "tree"}:
223+
grouping = oms.FeatureGroupingAlgorithmKD()
224+
elif normalized == "labeled":
225+
grouping = oms.FeatureGroupingAlgorithmLabeled()
226+
elif normalized == "unlabeled":
227+
grouping = oms.FeatureGroupingAlgorithmUnlabeled()
228+
else:
229+
raise ValueError(
230+
"Unsupported grouping_method. Use 'qt', 'kd', 'labeled', or 'unlabeled'."
231+
)
232+
201233
param_obj = grouping.getDefaults()
202234
if params:
203235
for key, value in params.items():

tests/test_consensus_map.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,45 @@ def test_align_and_link_invalid_method_raises():
3333
fmap = Py_FeatureMap(_simple_feature_map(10.0))
3434
with pytest.raises(ValueError):
3535
Py_ConsensusMap.align_and_link([fmap], alignment_method="unknown")
36+
37+
38+
def test_align_and_link_with_kd_grouping():
39+
"""Test that KD-tree grouping method is supported."""
40+
fmap_a = Py_FeatureMap(_simple_feature_map(10.0))
41+
fmap_b = Py_FeatureMap(_simple_feature_map(10.0))
42+
43+
consensus = Py_ConsensusMap.align_and_link(
44+
[fmap_a, fmap_b],
45+
alignment_method="identity",
46+
grouping_method="kd",
47+
)
48+
49+
assert isinstance(consensus, Py_ConsensusMap)
50+
assert len(consensus) == 1
51+
52+
53+
def test_align_and_link_invalid_grouping_raises():
54+
"""Test that invalid grouping method raises error."""
55+
fmap = Py_FeatureMap(_simple_feature_map(10.0))
56+
with pytest.raises(ValueError, match="Unsupported grouping_method"):
57+
Py_ConsensusMap.align_and_link(
58+
[fmap],
59+
alignment_method="identity",
60+
grouping_method="invalid"
61+
)
62+
63+
64+
def test_align_and_link_with_grouping_params():
65+
"""Test that grouping parameters can be passed."""
66+
fmap_a = Py_FeatureMap(_simple_feature_map(10.0))
67+
fmap_b = Py_FeatureMap(_simple_feature_map(10.0))
68+
69+
consensus = Py_ConsensusMap.align_and_link(
70+
[fmap_a, fmap_b],
71+
alignment_method="identity",
72+
grouping_method="qt",
73+
grouping_params={"distance_RT:max_difference": 100.0},
74+
)
75+
76+
assert isinstance(consensus, Py_ConsensusMap)
77+
assert len(consensus) == 1

tests/test_py_msexperiment.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,15 @@ def pick(self, source, dest):
323323
assert len(processed) == len(exp)
324324
assert all(np.allclose(spec.mz, 42.0) for spec in exp)
325325

326+
327+
def test_peak_picking_iterative_method():
328+
"""Test that iterative peak picking method is available."""
329+
exp = build_experiment()
330+
# Just verify that iterative method can be selected
331+
try:
332+
picked = exp.pick_peaks(method="iterative", ms_levels=1)
333+
assert isinstance(picked, Py_MSExperiment)
334+
except Exception as e:
335+
# It may fail on minimal data, but should not fail on unknown method
336+
assert "Unknown peak picking method" not in str(e)
337+

tests/test_workflows.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,50 @@ def test_link_features_and_export_quant_table():
156156
assert df.shape[0] >= 1
157157
# Expect one column per input map
158158
assert {col for col in df.columns if col.startswith("map_")}
159+
160+
161+
def test_link_features_with_kd_grouping():
162+
"""Test that KD-tree grouping method works in link_features."""
163+
fmap_a = oms.FeatureMap()
164+
feat_a = oms.Feature()
165+
feat_a.setRT(10.0)
166+
feat_a.setMZ(500.0)
167+
feat_a.setIntensity(100.0)
168+
fmap_a.push_back(feat_a)
169+
170+
fmap_b = oms.FeatureMap()
171+
feat_b = oms.Feature()
172+
feat_b.setRT(10.0)
173+
feat_b.setMZ(500.0)
174+
feat_b.setIntensity(110.0)
175+
fmap_b.push_back(feat_b)
176+
177+
consensus = link_features(
178+
[Py_FeatureMap(fmap_a), Py_FeatureMap(fmap_b)],
179+
grouping_method="kd"
180+
)
181+
assert len(consensus) == 1
182+
183+
184+
def test_link_features_with_unlabeled_grouping():
185+
"""Test that unlabeled grouping method works in link_features."""
186+
fmap_a = oms.FeatureMap()
187+
feat_a = oms.Feature()
188+
feat_a.setRT(10.0)
189+
feat_a.setMZ(500.0)
190+
feat_a.setIntensity(100.0)
191+
fmap_a.push_back(feat_a)
192+
193+
fmap_b = oms.FeatureMap()
194+
feat_b = oms.Feature()
195+
feat_b.setRT(10.0)
196+
feat_b.setMZ(500.0)
197+
feat_b.setIntensity(110.0)
198+
fmap_b.push_back(feat_b)
199+
200+
consensus = link_features(
201+
[Py_FeatureMap(fmap_a), Py_FeatureMap(fmap_b)],
202+
grouping_method="unlabeled"
203+
)
204+
assert len(consensus) == 1
205+

0 commit comments

Comments
 (0)