Repository: matchms/matchms
Pull request #676 (2024-06-04 16:07:17)

Add repair parent mass from smiles
Adds a filter to repair the parent mass from smiles
The parent mass is only repaired if it is not already close to the smiles monoisotopic mass.

Also moves all adduct/parent mass repairing filters after requires valid annotaion. 


Patch:
diff --git a/matchms/filtering/__init__.py b/matchms/filtering/__init__.py
index 515df4fd..76d14b3a 100644
--- a/matchms/filtering/__init__.py
+++ b/matchms/filtering/__init__.py
@@ -114,6 +114,8 @@ from matchms.filtering.metadata_processing.repair_inchi_inchikey_smiles import \
     repair_inchi_inchikey_smiles
 from matchms.filtering.metadata_processing.repair_not_matching_annotation import \
     repair_not_matching_annotation
+from matchms.filtering.metadata_processing.repair_parent_mass_from_smiles import \
+    repair_parent_mass_from_smiles
 from matchms.filtering.metadata_processing.repair_parent_mass_is_molar_mass import \
     repair_parent_mass_is_molar_mass
 from matchms.filtering.metadata_processing.repair_parent_mass_match_smiles_wrapper import \
@@ -200,6 +202,7 @@ __all__ = [
     "require_correct_ionmode",
     "remove_noise_below_frequent_intensities",
     "repair_inchi_inchikey_smiles",
+    "repair_parent_mass_from_smiles",
     "repair_parent_mass_is_molar_mass",
     "repair_parent_mass_match_smiles_wrapper",
     "repair_smiles_of_salts",
diff --git a/matchms/filtering/default_pipelines.py b/matchms/filtering/default_pipelines.py
index 6356673f..58c0cc81 100644
--- a/matchms/filtering/default_pipelines.py
+++ b/matchms/filtering/default_pipelines.py
@@ -35,8 +35,8 @@ REQUIRE_COMPLETE_METADATA = [msfilters.require_precursor_mz,
 REPAIR_ANNOTATION = [
     (msfilters.repair_smiles_of_salts, {'mass_tolerance': 0.1}),
     (msfilters.repair_parent_mass_is_molar_mass, {'mass_tolerance': 0.1}),
+    (msfilters.repair_parent_mass_from_smiles, {"mass_tolerance": 0.1}),
     (msfilters.repair_adduct_based_on_parent_mass, {'mass_tolerance': 0.1}),
-    (msfilters.repair_adduct_and_parent_mass_based_on_smiles, {'mass_tolerance': 0.1}),
     msfilters.repair_not_matching_annotation,
     (msfilters.derive_annotation_from_compound_name, {"mass_tolerance": 0.1}),
 ]
@@ -60,6 +60,7 @@ OTHER_FILTERS = [matchms.filtering.metadata_processing.require_precursor_mz.requ
                  msfilters.add_fingerprint,
                  msfilters.add_losses,
                  msfilters.repair_parent_mass_match_smiles_wrapper,
+                 (msfilters.repair_adduct_and_parent_mass_based_on_smiles, {'mass_tolerance': 0.1}),
                  ]
 
 BASIC_FILTERS = HARMONIZE_METADATA_FIELD_NAMES + DERIVE_METADATA_IN_WRONG_FIELD + HARMONIZE_METADATA_ENTRIES
diff --git a/matchms/filtering/filter_order.py b/matchms/filtering/filter_order.py
index 77f92330..62dcd6a9 100644
--- a/matchms/filtering/filter_order.py
+++ b/matchms/filtering/filter_order.py
@@ -30,11 +30,12 @@ ALL_FILTERS = [msfilters.require_correct_ms_level,
                msfilters.derive_inchikey_from_inchi,
                msfilters.repair_smiles_of_salts,
                msfilters.repair_parent_mass_is_molar_mass,
-               msfilters.repair_adduct_based_on_parent_mass,
-               msfilters.repair_adduct_and_parent_mass_based_on_smiles,
                msfilters.repair_parent_mass_match_smiles_wrapper,
                msfilters.repair_not_matching_annotation,
                msfilters.require_valid_annotation,
+               msfilters.repair_parent_mass_from_smiles,
+               msfilters.repair_adduct_based_on_parent_mass,
+               msfilters.repair_adduct_and_parent_mass_based_on_smiles,
                msfilters.require_correct_ionmode,
                msfilters.require_compound_name,
                msfilters.require_formula,
diff --git a/matchms/filtering/metadata_processing/repair_parent_mass_from_smiles.py b/matchms/filtering/metadata_processing/repair_parent_mass_from_smiles.py
new file mode 100644
index 00000000..ad4d673d
--- /dev/null
+++ b/matchms/filtering/metadata_processing/repair_parent_mass_from_smiles.py
@@ -0,0 +1,27 @@
+import logging
+from matchms import Spectrum
+from matchms.filtering.filter_utils.get_neutral_mass_from_smiles import \
+    get_monoisotopic_neutral_mass
+
+
+logger = logging.getLogger("matchms")
+
+
+def repair_parent_mass_from_smiles(spectrum_in: Spectrum,
+                                        mass_tolerance: float = 0.1):
+    """Sets the parent mass to match the smiles mass, if not already close to smiles mass"""
+    if spectrum_in is None:
+        return None
+    changed_spectrum = spectrum_in.clone()
+    smiles_mass = get_monoisotopic_neutral_mass(changed_spectrum.get("smiles"))
+    if smiles_mass is None:
+        return spectrum_in
+    parent_mass = spectrum_in.get("parent_mass")
+
+    if parent_mass is None:
+        changed_spectrum.set("parent_mass", smiles_mass)
+        return changed_spectrum
+    if abs(parent_mass - smiles_mass) > mass_tolerance:
+        changed_spectrum.set("parent_mass", smiles_mass)
+        return changed_spectrum
+    return spectrum_in


Test patch:
diff --git a/tests/filtering/test_all_filter_order.py b/tests/filtering/test_all_filter_order.py
index 264b2dfe..080d8227 100644
--- a/tests/filtering/test_all_filter_order.py
+++ b/tests/filtering/test_all_filter_order.py
@@ -54,7 +54,12 @@ DERIVE_ANNOTATION_FILTERS = [msfilters.derive_smiles_from_inchi,
       msfilters.clean_adduct, msfilters.require_correct_ionmode, msfilters.derive_ionmode,
       msfilters.derive_adduct_from_name],
      [msfilters.require_matching_adduct_and_ionmode]],
-    [[msfilters.remove_profiled_spectra,], [msfilters.remove_peaks_around_precursor_mz]]
+    [[msfilters.remove_profiled_spectra,], [msfilters.remove_peaks_around_precursor_mz]],
+    [[msfilters.repair_parent_mass_from_smiles], [msfilters.repair_adduct_based_on_parent_mass,
+                                                  msfilters.require_parent_mass_match_smiles,
+                                                  msfilters.require_matching_adduct_precursor_mz_parent_mass,
+                                                  ]],
+    [[msfilters.require_valid_annotation], [msfilters.repair_parent_mass_from_smiles]]
 ])
 def test_all_filter_order(early_filters: List[Callable], later_filters: List[Callable]):
     """Tests if early_filter is run before later_filter"""
diff --git a/tests/filtering/test_repair_parent_mass_from_smiles/test_repair_parent_mass_from_smiles.py b/tests/filtering/test_repair_parent_mass_from_smiles/test_repair_parent_mass_from_smiles.py
new file mode 100644
index 00000000..2cde6c2c
--- /dev/null
+++ b/tests/filtering/test_repair_parent_mass_from_smiles/test_repair_parent_mass_from_smiles.py
@@ -0,0 +1,16 @@
+import math
+import pytest
+from matchms.filtering import repair_parent_mass_from_smiles
+from tests.builder_Spectrum import SpectrumBuilder
+
+
+@pytest.mark.parametrize("smiles, parent_mass, expected_parent_mass",
+                         [("CN1CCCC1C2=CN=CC=C2", 162.23, 162.115698455),
+                          ("CN1CCCC1C2=CN=CC=C2", 162.1, 162.1),
+                          ])
+def test_repair_parent_mass_from_smiles(smiles, parent_mass, expected_parent_mass):
+    pytest.importorskip("rdkit")
+    spectrum_in = SpectrumBuilder().with_metadata({"smiles": smiles,
+                                                   "parent_mass": parent_mass}).build()
+    spectrum_out = repair_parent_mass_from_smiles(spectrum_in, mass_tolerance=0.1)
+    assert math.isclose(spectrum_out.get("parent_mass"), expected_parent_mass)