Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ helper performs three steps:

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

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

print(f"Consensus contains {len(consensus)} features")
Expand Down Expand Up @@ -326,7 +328,11 @@ annotated = map_identifications_to_features(feature_map, filtered)

# 3) Align multiple maps and link them into a consensus representation
aligned = align_feature_maps([annotated, second_run])
consensus = link_features(aligned)
consensus = link_features(
aligned,
grouping_method="qt", # or "kd" / "labeled" / "unlabeled"
params={"distance_RT:max_difference": 100.0}
)

# 4) Export a tidy quantitation table (per-sample intensities)
quant_df = export_quant_table(consensus)
Expand Down Expand Up @@ -355,7 +361,10 @@ picker.pickExperiment(exp, centroided, True)
```python
from openms_python import Py_MSExperiment

centroided = exp.pick_peaks(method="HiRes", params={"signal_to_noise": 3.0})
# Choose from multiple peak picking algorithms
centroided = exp.pick_peaks(method="hires", params={"signal_to_noise": 3.0})
# Available methods: "hires" (default), "cwt", "iterative"

# or modify in-place
exp.pick_peaks(inplace=True)
```
Expand Down
39 changes: 37 additions & 2 deletions openms_python/py_consensusmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ def align_and_link(
*,
alignment_method: str = "pose_clustering",
alignment_params: Optional[Dict[str, Union[int, float, str]]] = None,
grouping_method: str = "qt",
grouping_params: Optional[Dict[str, Union[int, float, str]]] = None,
) -> 'Py_ConsensusMap':
"""Align multiple feature maps and return their linked consensus map.

Expand All @@ -180,14 +182,21 @@ def align_and_link(
alignment_params:
Optional dictionary of parameters applied to the selected
alignment algorithm.
grouping_method:
Name of the OpenMS feature grouping algorithm to use. Supported values
are ``"qt"`` (QT clustering, default), ``"kd"`` (KD-tree based),
``"labeled"`` (for labeled data), and ``"unlabeled"`` (for unlabeled data).
grouping_params:
Optional dictionary of parameters applied to the selected
grouping algorithm.
"""

if not feature_maps:
return cls()

native_maps = [cls._copy_feature_map(feature_map) for feature_map in feature_maps]
cls._align_feature_maps(native_maps, alignment_method, alignment_params)
consensus_map = cls._link_feature_maps(native_maps)
consensus_map = cls._link_feature_maps(native_maps, grouping_method, grouping_params)
return cls(consensus_map)

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

@staticmethod
def _create_grouping_algorithm(method: str):
"""Create the appropriate feature grouping algorithm."""
normalized = method.lower()
if normalized in {"qt", "qtcluster"}:
return oms.FeatureGroupingAlgorithmQT()
if normalized in {"kd", "tree"}:
return oms.FeatureGroupingAlgorithmKD()
if normalized == "labeled":
return oms.FeatureGroupingAlgorithmLabeled()
if normalized == "unlabeled":
return oms.FeatureGroupingAlgorithmUnlabeled()
raise ValueError(
"Unsupported grouping_method. Use 'qt', 'kd', 'labeled', or 'unlabeled'."
)

@staticmethod
def _link_feature_maps(
feature_maps: Sequence[oms.FeatureMap],
grouping_method: str = "qt",
grouping_params: Optional[Dict[str, Union[int, float, str]]] = None,
) -> oms.ConsensusMap:
grouping = oms.FeatureGroupingAlgorithmQT()
grouping = Py_ConsensusMap._create_grouping_algorithm(grouping_method)

# Apply parameters if provided
if grouping_params:
params = grouping.getDefaults()
for key, value in grouping_params.items():
params.setValue(str(key), value)
grouping.setParameters(params)

consensus_map = oms.ConsensusMap()
grouping.group(feature_maps, consensus_map)

Expand Down
1 change: 1 addition & 0 deletions openms_python/py_msexperiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
PEAK_PICKER_REGISTRY: Dict[str, Any] = {
"hires": oms.PeakPickerHiRes,
"cwt": getattr(oms, "PeakPickerCWT", oms.PeakPickerHiRes),
"iterative": oms.PeakPickerIterative,
}

_FeatureMapLike = Union[Py_FeatureMap, oms.FeatureMap]
Expand Down
38 changes: 35 additions & 3 deletions openms_python/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,43 @@ def align_feature_maps(
def link_features(
feature_maps: Sequence[_FeatureMapLike],
*,
grouping_method: str = "qt",
params: Optional[Dict[str, Union[int, float, str]]] = None,
) -> Py_ConsensusMap:
"""Group features across runs into a consensus map."""

grouping = oms.FeatureGroupingAlgorithmQT()
"""Group features across runs into a consensus map.

Parameters
----------
feature_maps:
Sequence of feature maps to link.
grouping_method:
Name of the OpenMS feature grouping algorithm to use. Supported values
are ``"qt"`` (QT clustering, default), ``"kd"`` (KD-tree based),
``"labeled"`` (for labeled data), and ``"unlabeled"`` (for unlabeled data).
params:
Optional dictionary of parameters applied to the selected grouping algorithm.

Returns
-------
Py_ConsensusMap
The linked consensus map.
"""

# Create grouping algorithm
normalized = grouping_method.lower()
if normalized in {"qt", "qtcluster"}:
grouping = oms.FeatureGroupingAlgorithmQT()
elif normalized in {"kd", "tree"}:
grouping = oms.FeatureGroupingAlgorithmKD()
elif normalized == "labeled":
grouping = oms.FeatureGroupingAlgorithmLabeled()
elif normalized == "unlabeled":
grouping = oms.FeatureGroupingAlgorithmUnlabeled()
else:
raise ValueError(
"Unsupported grouping_method. Use 'qt', 'kd', 'labeled', or 'unlabeled'."
)

param_obj = grouping.getDefaults()
if params:
for key, value in params.items():
Expand Down
42 changes: 42 additions & 0 deletions tests/test_consensus_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,45 @@ def test_align_and_link_invalid_method_raises():
fmap = Py_FeatureMap(_simple_feature_map(10.0))
with pytest.raises(ValueError):
Py_ConsensusMap.align_and_link([fmap], alignment_method="unknown")


def test_align_and_link_with_kd_grouping():
"""Test that KD-tree grouping method is supported."""
fmap_a = Py_FeatureMap(_simple_feature_map(10.0))
fmap_b = Py_FeatureMap(_simple_feature_map(10.0))

consensus = Py_ConsensusMap.align_and_link(
[fmap_a, fmap_b],
alignment_method="identity",
grouping_method="kd",
)

assert isinstance(consensus, Py_ConsensusMap)
assert len(consensus) == 1


def test_align_and_link_invalid_grouping_raises():
"""Test that invalid grouping method raises error."""
fmap = Py_FeatureMap(_simple_feature_map(10.0))
with pytest.raises(ValueError, match="Unsupported grouping_method"):
Py_ConsensusMap.align_and_link(
[fmap],
alignment_method="identity",
grouping_method="invalid"
)


def test_align_and_link_with_grouping_params():
"""Test that grouping parameters can be passed."""
fmap_a = Py_FeatureMap(_simple_feature_map(10.0))
fmap_b = Py_FeatureMap(_simple_feature_map(10.0))

consensus = Py_ConsensusMap.align_and_link(
[fmap_a, fmap_b],
alignment_method="identity",
grouping_method="qt",
grouping_params={"distance_RT:max_difference": 100.0},
)

assert isinstance(consensus, Py_ConsensusMap)
assert len(consensus) == 1
12 changes: 12 additions & 0 deletions tests/test_py_msexperiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,15 @@ def pick(self, source, dest):
assert len(processed) == len(exp)
assert all(np.allclose(spec.mz, 42.0) for spec in exp)


def test_peak_picking_iterative_method():
"""Test that iterative peak picking method is available."""
exp = build_experiment()
# Just verify that iterative method can be selected
try:
picked = exp.pick_peaks(method="iterative", ms_levels=1)
assert isinstance(picked, Py_MSExperiment)
except Exception as e:
# It may fail on minimal data, but should not fail on unknown method
assert "Unknown peak picking method" not in str(e)

47 changes: 47 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,50 @@ def test_link_features_and_export_quant_table():
assert df.shape[0] >= 1
# Expect one column per input map
assert {col for col in df.columns if col.startswith("map_")}


def test_link_features_with_kd_grouping():
"""Test that KD-tree grouping method works in link_features."""
fmap_a = oms.FeatureMap()
feat_a = oms.Feature()
feat_a.setRT(10.0)
feat_a.setMZ(500.0)
feat_a.setIntensity(100.0)
fmap_a.push_back(feat_a)

fmap_b = oms.FeatureMap()
feat_b = oms.Feature()
feat_b.setRT(10.0)
feat_b.setMZ(500.0)
feat_b.setIntensity(110.0)
fmap_b.push_back(feat_b)

consensus = link_features(
[Py_FeatureMap(fmap_a), Py_FeatureMap(fmap_b)],
grouping_method="kd"
)
assert len(consensus) == 1


def test_link_features_with_unlabeled_grouping():
"""Test that unlabeled grouping method works in link_features."""
fmap_a = oms.FeatureMap()
feat_a = oms.Feature()
feat_a.setRT(10.0)
feat_a.setMZ(500.0)
feat_a.setIntensity(100.0)
fmap_a.push_back(feat_a)

fmap_b = oms.FeatureMap()
feat_b = oms.Feature()
feat_b.setRT(10.0)
feat_b.setMZ(500.0)
feat_b.setIntensity(110.0)
fmap_b.push_back(feat_b)

consensus = link_features(
[Py_FeatureMap(fmap_a), Py_FeatureMap(fmap_b)],
grouping_method="unlabeled"
)
assert len(consensus) == 1