Repository: matchms/matchms
Pull request #170 (2021-01-26 21:47:48)

Expand adducts handling
- consistently change adduct style from `M+H+` to `[M+H]+` (also made consistent by now having *one* function to clean up adduct strings: `clean_adduct()` in utils)
- expand list of known adducts (see references here: https://fiehnlab.ucdavis.edu/staff/kind/metabolomics/ms-adduct-calculator/)
- import dictionary from json in function `load_known_adduct_conversions()` (e.g. `[M+H+H2O]+` is the same as `[M+H2O+H]+`)
- import table (pd.DataFrame) from csv file containing known adduct properties for later parent mass estimation: `load_adducts_table()`, see also #171 
- add test for adduct cleaning, and tests for functions in `load_adducts.py`
- expand `add_parent_mass` filter to take known adduct masses and charges into account!

jspaaks: https://github.com/matchms/matchms/pull/160/commits/1c7e0ec2f2d16a4379a4b30d9c45227c2fb38b21

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2dcfb9d6..fbc3dbed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Added
+
+- helper functions to clean adduct strings, `clean_adduct()` [#170](https://github.com/matchms/matchms/pull/170)
+
+### Changed
+
+- more thorough adduct cleaning effecting `derive_adduct_from_name()` and `derive_ionmode()` [#171](https://github.com/matchms/matchms/issues/171)
+- significant expansion of `add_parent_mass()` filter to take known adduct properties into account [#170](https://github.com/matchms/matchms/pull/170)
+
 ## Fixed
 
 - too unspecific formula detection (and removal) from given compound names in `derive_formula_from_name` [#172](https://github.com/matchms/matchms/issues/172)
diff --git a/conda/environment-dev.yml b/conda/environment-dev.yml
index 7e19acd3..4f1fa6f0 100644
--- a/conda/environment-dev.yml
+++ b/conda/environment-dev.yml
@@ -11,7 +11,6 @@ dependencies:
   - pyteomics >=4.2
   - requests
   - python >=3.7,<3.9
-  - pyyaml
   - rdkit >=2020.03.1
   - scipy >=1.4.0
   - pip:
diff --git a/conda/environment.yml b/conda/environment.yml
index 602e42a8..133e957e 100644
--- a/conda/environment.yml
+++ b/conda/environment.yml
@@ -11,6 +11,5 @@ dependencies:
   - pyteomics >=4.2
   - requests
   - python >=3.7,<3.9
-  - pyyaml
   - rdkit >=2020.03.1
   - scipy >=1.4.0
diff --git a/conda/meta.yaml b/conda/meta.yaml
index b6e0e310..10130517 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -42,7 +42,6 @@ requirements:
     - pyteomics >=4.2
     - requests
     - python >=3.7,<3.9
-    - pyyaml
     - rdkit >=2020.03.1
     - scipy >=1.4.0
 
diff --git a/matchms/data/known_adduct_conversions.csv b/matchms/data/known_adduct_conversions.csv
new file mode 100644
index 00000000..6259fe1c
--- /dev/null
+++ b/matchms/data/known_adduct_conversions.csv
@@ -0,0 +1,34 @@
+input_adduct,corrected_adduct
+[M+H-H2O],[M-H2O+H]+
+[M-H2O+H],[M-H2O+H]+
+[M+H-H2O]+,[M-H2O+H]+
+[M-2H2O+H],[M-2H2O+H]+
+[M+H-2H2O],[M-2H2O+H]+
+[M+H-2H2O]+,[M-2H2O+H]+
+[M+H+Na],[M+H+Na]2+
+[M+H-NH3],[M+H-NH3]+
+[M-H+2Na]+,[M+2Na-H]+
+[M+ACN+H],[M+ACN+H]+
+[M+H],[M+H]+
+[M+2H],[M+2H]2+
+[M+3H],[M+3H]3+
+[M+Na],[M+Na]+
+[M+K],[M+K]+
+[2M+H],[2M+H]+
+[2M+Na],[2M+Na]+
+[2M+K],[2M+K]+
+[M+NH4],[M+NH4]+
+[M-H],[M-H]-
+[M-H2O-H],[M-H2O-H]-
+[M-H-H2O]-,[M-H2O-H]-
+[M-H-H2O],[M-H2O-H]-
+[M+H-CH3NH2],[M+H-CH3NH2]+
+[M+Cl],[M+Cl]-
+[M+Br],[M+Br]-
+[3M-H],[3M-H]-
+[2M-H],[2M-H]-
+[M+CH3COO],[M+CH3COO]-
+[M+FA-H],[M+FA-H]-
+[M+OAc],[M+OAc]-
+[M+Oac],[M+OAc]-
+[M+Oac]-,[M+OAc]-
diff --git a/matchms/data/known_adducts.yaml b/matchms/data/known_adducts.yaml
deleted file mode 100644
index 272cd035..00000000
--- a/matchms/data/known_adducts.yaml
+++ /dev/null
@@ -1,64 +0,0 @@
----
-data_type: "Known adducts"
-description: > 
-    Adducts allow to infer the ionization mode for mass spectrometry, as well
-    at the correct charge value. There are many possible adducts, and adduct
-    notation is often not sufficiently standardized.
-    This document will list known adducts which can then be used to better
-    interpret mass spectra metadata.
-last_updated_by: JJJ van der Hooft
-last_updated: 06-05-2020
-
-# Known adducts for positive ionization mode.
-adducts_positive:
-- M-2H2O+H
-- M+H
-- M+H-CH3NH2
-- M+Na
-- M-H2O+H+
-- M+H+Na
-- M+H+
-- M+K
-- M+H
-- M+K+
-- 2M+Na
-- M+
-- M+3H
-- M+2H++
-- M+NH4
-- M+ACN+H
-- M+H-NH3
-- M-H2O+H
-- M+
-- M+Na+
-- M+
-- M+2H
-- M+H-H2O
-- M-2H2O+H+
-- M+
-- M+NH4+
-- Cat
-- M+
-- M+Na
-- 2M+H
-- M+H-2H2O
-- M+2H
-- M+2H
-
-# Known adducts for negative ionization mode.
-adducts_negative:
-- M+CH3COO-/M-CH3-
-- M-H
-- M+CH3COO-
-- M-
-- 2M-H
-- M-H-/M-Ser-
-- M-
-- M-H-
-- M-2H-
-- M-H2O-H
-- M+FA-H
-- M+Cl
-- (M+CH3COOH)-H-
-- M-H-H2O
-- M-H-CO2-2HF-
\ No newline at end of file
diff --git a/matchms/data/known_adducts_table.csv b/matchms/data/known_adducts_table.csv
new file mode 100644
index 00000000..f0416b63
--- /dev/null
+++ b/matchms/data/known_adducts_table.csv
@@ -0,0 +1,60 @@
+﻿adduct,ionmode,charge,mass_multiplier,correction_mass
+[M+3H]3+,positive,3,0.33,1.007276
+[M+2H+Na]3+,positive,3,0.33,8.33459
+[M+H+2Na]3+,positive,3,0.33,15.662453
+[M+3Na]3+,positive,3,0.33,22.989218
+[M+2H]2+,positive,2,0.5,1.007276
+[M+H+NH4]2+,positive,2,0.5,9.52055
+[M+H+Na]2+,positive,2,0.5,11.998247
+[M+H+K]2+,positive,2,0.5,19.985217
+[M+ACN+2H]2+,positive,2,0.5,21.52055
+[M+2Na]2+,positive,2,0.5,22.989218
+[M+2ACN+2H]2+,positive,2,0.5,42.033823
+[M+3ACN+2H]2+,positive,2,0.5,62.547097
+[M+H]+,positive,1,1,1.007276
+[M+H-NH3]+,positive,1,1,-16.019273
+[M-H2O+H]+,positive,1,1,-17.003289
+[M-2H2O+H]+,positive,1,1,-35.013854
+[M+NH4]+,positive,1,1,18.033823
+[M+Na]+,positive,1,1,22.989218
+[M+CH3OH+H]+,positive,1,1,33.033489
+[M+K]+,positive,1,1,38.963158
+[M+ACN+H]+,positive,1,1,42.033823
+[M+2Na-H]+,positive,1,1,44.97116
+[M+IsoProp+H]+,positive,1,1,61.06534
+[M+ACN+Na]+,positive,1,1,64.015765
+[M+2K-H]+,positive,1,1,76.91904
+[M+DMSO+H]+,positive,1,1,79.02122
+[M+2ACN+H]+,positive,1,1,83.06037
+[M+IsoProp+Na+H]+,positive,1,1,84.05511
+[2M+H]+,positive,1,2,1.007276
+[2M+NH4]+,positive,1,2,18.033823
+[2M+Na]+,positive,1,2,22.989218
+[2M+K]+,positive,1,2,38.963158
+[2M+ACN+H]+,positive,1,2,42.033823
+[2M+ACN+Na]+,positive,1,2,64.015765
+[M-3H]3-,negative,-3,0.333333333,-1.007276
+[M-2H]2-,negative,-2,0.5,-1.007276
+[M-H2O-H]-,negative,-1,1,-19.01839
+[M-H]-,negative,-1,1,-1.007276
+[M+Na-2H]-,negative,-1,1,20.974666
+[M+Cl]-,negative,-1,1,34.969402
+[M+K-2H]-,negative,-1,1,36.948606
+[M+FA-H]-,negative,-1,1,44.998201
+[M+Hac-H]-,negative,-1,1,59.013851
+[M+Br]-,negative,-1,1,78.918885
+[M+TFA-H]-,negative,-1,1,112.985586
+[2M-H]-,negative,-1,2,-1.007276
+[2M+FA-H]-,negative,-1,2,44.998201
+[2M+Hac-H]-,negative,-1,2,59.013851
+[3M-H]-,negative,-1,3,-1.007276
+[M]-,negative,-1,n/a,n/a
+[M-H-CO2-2HF]-,negative,-1,n/a,n/a
+[M-H-/M-Ser]-,negative,-1,n/a,n/a
+[M-2H]-,negative,-1,n/a,n/a
+[M+CH3COO]-,negative,-1,n/a,n/a
+[M+CH3COO-/M-CH3]-,negative,-1,n/a,n/a
+[M+OAc]-,negative,-1,n/a,n/a
+[M]+,positive,1,n/a,n/a
+[M+H-CH3NH2]+,positive,1,n/a,n/a
+[Cat],positive,1,n/a,n/a
diff --git a/matchms/filtering/add_parent_mass.py b/matchms/filtering/add_parent_mass.py
index f7a77c9a..d7b48dfa 100644
--- a/matchms/filtering/add_parent_mass.py
+++ b/matchms/filtering/add_parent_mass.py
@@ -1,31 +1,59 @@
 from ..constants import PROTON_MASS
+from ..importing import load_adducts_dict
 from ..typing import SpectrumType
 
 
-def add_parent_mass(spectrum_in: SpectrumType) -> SpectrumType:
-    """Add parentmass to metadata (if not present yet).
+def add_parent_mass(spectrum_in: SpectrumType, estimate_from_adduct: bool = True) -> SpectrumType:
+    """Add estimated parent mass to metadata (if not present yet).
 
-    Method to calculate the parent mass from given precursor m/z
-    and charge. Will take precursor m/z from either "precursor_mz"
+    Method to calculate the parent mass from given precursor m/z together
+    with charge and/or adduct. Will take precursor m/z from either "precursor_mz"
     or "pepmass" field.
+    For estimate_from_adduct=True this function will estimate the parent mass based on
+    the mass and charge of known adducts. The table of known adduct properties can be
+    found under :download:`matchms/data/known_adducts_table.csv </../matchms/data/known_adducts_table.csv>`.
+
+    Parameters
+    ----------
+    spectrum_in
+        Input spectrum.
+    estimate_from_adduct
+        When set to True, use adduct to estimate actual molecular mass ("parent mass").
+        Default is True. Switches back to charge-based estimate if adduct does not match
+        a known adduct.
     """
     if spectrum_in is None:
         return None
 
     spectrum = spectrum_in.clone()
+    adducts_dict = load_adducts_dict()
 
     if spectrum.get("parent_mass", None) is None:
+        parent_mass = None
+        charge = spectrum.get("charge")
+        adduct = spectrum.get("adduct")
+        # Get precursor m/z
         try:
-            charge = spectrum.get("charge")
-            protons_mass = PROTON_MASS * charge
             precursor_mz = spectrum.get("precursor_mz", None)
             if precursor_mz is None:
                 precursor_mz = spectrum.get("pepmass")[0]
+        except KeyError:
+            print("Not sufficient spectrum metadata to derive parent mass.")
+
+        spectrum = spectrum_in.clone()
+        if estimate_from_adduct and adduct in adducts_dict:
+            multiplier = adducts_dict[adduct]["mass_multiplier"]
+            correction_mass = adducts_dict[adduct]["correction_mass"]
+            parent_mass = precursor_mz * multiplier - correction_mass
+
+        if parent_mass is None and charge is not None:
+            # Otherwise assume adduct of shape [M+xH] or [M-xH]
+            protons_mass = PROTON_MASS * charge
             precursor_mass = precursor_mz * abs(charge)
             parent_mass = precursor_mass - protons_mass
-            if parent_mass:
-                spectrum.set("parent_mass", parent_mass)
-        except KeyError:
+        else:
             print("Not sufficient spectrum metadata to derive parent mass.")
 
+        if parent_mass is not None:
+            spectrum.set("parent_mass", parent_mass)
     return spectrum
diff --git a/matchms/filtering/clean_compound_name.py b/matchms/filtering/clean_compound_name.py
index 7f611e83..09971c60 100644
--- a/matchms/filtering/clean_compound_name.py
+++ b/matchms/filtering/clean_compound_name.py
@@ -42,6 +42,14 @@ def clean_compound_name(spectrum_in: SpectrumType) -> SpectrumType:
             name = name.replace(part, "")
         return name.strip("; ")
 
+    def remove_misplaced_mass(name):
+        """Remove occasionally occurring parent mass addition to name."""
+        regex_mass = r"^[0-9]{2,4}\.[0-9]$"
+        end_part = name.split(" ")[-1]
+        if re.search(regex_mass, end_part) is not None:
+            return name.replace(end_part, "").strip()
+        return name
+
     if spectrum_in is None:
         return None
 
@@ -57,6 +65,7 @@ def clean_compound_name(spectrum_in: SpectrumType) -> SpectrumType:
     # Clean compound name
     name_cleaned = remove_parts_by_regular_expression(name)
     name_cleaned = remove_known_non_compound_parts(name_cleaned)
+    name_cleaned = remove_misplaced_mass(name_cleaned)
     if name_cleaned != name:
         spectrum.set("compound_name", name_cleaned)
         print("Added cleaned compound name:", name_cleaned)
diff --git a/matchms/filtering/correct_charge.py b/matchms/filtering/correct_charge.py
index 782b0acd..0553efd3 100644
--- a/matchms/filtering/correct_charge.py
+++ b/matchms/filtering/correct_charge.py
@@ -7,6 +7,11 @@ def correct_charge(spectrum_in: SpectrumType) -> SpectrumType:
 
     For some spectrums, the charge value is either undefined or inconsistent with its
     ionmode, which is corrected by this filter.
+
+    Parameters
+    ----------
+    spectrum_in
+        Input spectrum.
     """
 
     if spectrum_in is None:
diff --git a/matchms/filtering/derive_adduct_from_name.py b/matchms/filtering/derive_adduct_from_name.py
index 90e8f334..fe12deb0 100644
--- a/matchms/filtering/derive_adduct_from_name.py
+++ b/matchms/filtering/derive_adduct_from_name.py
@@ -1,4 +1,5 @@
 from ..typing import SpectrumType
+from ..utils import clean_adduct
 from ..utils import looks_like_adduct
 
 
@@ -42,7 +43,8 @@ def derive_adduct_from_name(spectrum_in: SpectrumType,
 
     # Add found adduct to metadata (if not present yet)
     if adduct_from_name and not looks_like_adduct(spectrum.get("adduct")):
-        spectrum.set("adduct", adduct_from_name.strip().replace("*", ""))
-        print("Added adduct {} to metadata.".format(adduct_from_name))
+        adduct_cleaned = clean_adduct(adduct_from_name)
+        spectrum.set("adduct", adduct_cleaned)
+        print(f"Added adduct {adduct_cleaned} to metadata.")
 
     return spectrum
diff --git a/matchms/filtering/derive_ionmode.py b/matchms/filtering/derive_ionmode.py
index a07ab30b..bb3464c3 100644
--- a/matchms/filtering/derive_ionmode.py
+++ b/matchms/filtering/derive_ionmode.py
@@ -1,8 +1,9 @@
-from ..importing import load_adducts
+from ..importing import load_adducts_dict
 from ..typing import SpectrumType
+from ..utils import clean_adduct
 
 
-def derive_ionmode(spectrum_in: SpectrumType, adducts_filename: str = None) -> SpectrumType:
+def derive_ionmode(spectrum_in: SpectrumType) -> SpectrumType:
     """Derive missing ionmode based on adduct.
 
     Some input formates (e.g. MGF files) do not always provide a correct ionmode.
@@ -13,13 +14,9 @@ def derive_ionmode(spectrum_in: SpectrumType, adducts_filename: str = None) -> S
     ----------
     spectrum:
         Input spectrum.
-    adducts_filename:
-        Load known adducts from file, if filename is given. Default is None.
-        Method makes sure that file loading is cached.
 
     Returns:
     --------
-
     Returns Spectrum object with `ionmode` attribute set.
     """
 
@@ -29,32 +26,26 @@ def derive_ionmode(spectrum_in: SpectrumType, adducts_filename: str = None) -> S
     spectrum = spectrum_in.clone()
 
     # Load lists of known adducts
-    known_adducts = load_adducts(filename=adducts_filename)
+    known_adducts = load_adducts_dict()
 
     adduct = spectrum.get("adduct", None)
     # Harmonize adduct string
     if adduct:
-        adduct = adduct.replace("\n", "") \
-                       .replace(" ", "") \
-                       .replace("[", "") \
-                       .replace("]", "") \
-                       .replace("*", "")
+        adduct = clean_adduct(adduct)
 
     ionmode = spectrum.get("ionmode")
     if ionmode:
         assert ionmode == ionmode.lower(), ("Ionmode field not harmonized.",
                                             "Apply 'make_ionmode_lowercase' filter first.")
+    if ionmode in ["positive", "negative"]:
+        return spectrum
 
     # Try completing missing or incorrect ionmodes
-    if ionmode not in ["positive", "negative"]:
-        if adduct in known_adducts["adducts_positive"]:
-            ionmode = "positive"
-            print("Added ionmode '" + ionmode + "' based on adduct: ", adduct)
-        elif adduct in known_adducts["adducts_negative"]:
-            ionmode = "negative"
-            print("Added ionmode '" + ionmode + "' based on adduct: ", adduct)
-        else:
-            ionmode = "n/a"
+    if adduct in known_adducts:
+        ionmode = known_adducts[adduct]["ionmode"]
+    else:
+        ionmode = "n/a"
+
     spectrum.set("ionmode", ionmode)
 
     return spectrum
diff --git a/matchms/importing/__init__.py b/matchms/importing/__init__.py
index 9dcd276e..98bf21bf 100644
--- a/matchms/importing/__init__.py
+++ b/matchms/importing/__init__.py
@@ -10,8 +10,12 @@ Another option is to load spectra based on a unique identifier (USI)
 
 For more extensive import options we recommend building custom importers using `pyteomics
 <https://github.com/levitsky/pyteomics>`_ or `pymzml <https://github.com/pymzml/pymzML>`_.
+
+To process spectrum metadata, matchms can also make use of known adduct information
+which is imported via :mod:`~matchms.importing.load_adducts`.
 """
-from .load_adducts import load_adducts
+from .load_adducts import load_adducts_dict
+from .load_adducts import load_known_adduct_conversions
 from .load_from_json import load_from_json
 from .load_from_mgf import load_from_mgf
 from .load_from_msp import load_from_msp
@@ -27,5 +31,6 @@ __all__ = [
     "load_from_mzml",
     "load_from_mzxml",
     "load_from_usi",
-    "load_adducts"
+    "load_adducts_dict",
+    "load_known_adduct_conversions",
 ]
diff --git a/matchms/importing/load_adducts.py b/matchms/importing/load_adducts.py
index 86835b5f..61a400e0 100644
--- a/matchms/importing/load_adducts.py
+++ b/matchms/importing/load_adducts.py
@@ -1,29 +1,95 @@
+import csv
 import os
 from functools import lru_cache
 from typing import Dict
-import yaml
+import numpy
 
 
 @lru_cache(maxsize=4)
-def load_adducts(filename: str = None) -> Dict:
-    """Load dictionary of known adducts. Makes sure that file loading is cached.
+def load_adducts_dict() -> Dict[str, dict]:
+    """Load dictionary of known adducts containing the adduct mass and charge.
+    Makes sure that file loading is cached.
 
-    Parameters
-    ----------
-    filename:
-        Yaml file containing adducts.
+    Adduct information is based on information from
+    https://fiehnlab.ucdavis.edu/staff/kind/metabolomics/ms-adduct-calculator/
+    and was extended by F.Huber and JJJ.v.d.Hooft.
+
+    The full table can be found at
+    https://github.com/matchms/matchms/blob/expand_adducts/matchms/data/known_adducts_table.csv
+
+    TODO: change to relative path link or update link
+
+    """
+    known_adducts_file = os.path.join(os.path.dirname(__file__), "..", "data", "known_adducts_table.csv")
+    assert os.path.isfile(known_adducts_file), "Could not find known_adducts_table.csv."
+
+    with open(known_adducts_file, newline='', encoding='utf-8-sig') as csvfile:
+        reader = csv.DictReader(csvfile)
+        adducts_dict = dict()
+        for row in reader:
+            assert "adduct" in row
+            adducts_dict[row["adduct"]] = {x[0]: x[1] for x in row.items() if x[0] != "adduct"}
+
+    return _convert_and_fill_dict(adducts_dict)
+
+
+@lru_cache(maxsize=4)
+def load_known_adduct_conversions() -> Dict[str, dict]:
+    """Load dictionary of known adduct conversions. Makes sure that file loading is cached.
     """
-    if filename is None:
-        known_adducts_file = os.path.join(os.path.dirname(__file__), "..", "data", "known_adducts.yaml")
-    else:
-        known_adducts_file = filename
-
-    if os.path.isfile(known_adducts_file):
-        with open(known_adducts_file, 'r') as f:
-            known_adducts = yaml.safe_load(f)
-    else:
-        print("Could not find yaml file with known adducts.")
-        known_adducts = {'adducts_positive': [],
-                         'adducts_negative': []}
-
-    return known_adducts
+    adduct_conversions_file = os.path.join(os.path.dirname(__file__), "..", "data", "known_adduct_conversions.csv")
+    assert os.path.isfile(adduct_conversions_file), "Could not find known_adduct_conversions.csv."
+
+    with open(adduct_conversions_file, newline='', encoding='utf-8-sig') as csvfile:
+        reader = csv.DictReader(csvfile)
+        known_adduct_conversions = dict()
+        for row in reader:
+            known_adduct_conversions[row['input_adduct']] = row['corrected_adduct']
+
+    return known_adduct_conversions
+
+
+def _convert_and_fill_dict(adduct_dict: Dict[str, dict]) -> Dict[str, dict]:
+    """Convert string entries to int/float and fill missing entries ('n/a')
+    with best basic guesses."""
+    def is_float(value):
+        try:
+            float(value)
+            return True
+        except ValueError:
+            return False
+
+    def is_int(value):
+        try:
+            int(value)
+            return True
+        except ValueError:
+            return False
+
+    filled_dict = dict()
+    for adduct, values in adduct_dict.items():
+        ionmode = values["ionmode"]
+        charge = values["charge"]
+        mass_multiplier = values["mass_multiplier"]
+        correction_mass = values["correction_mass"]
+
+        if is_int(charge):
+            charge = int(charge)
+        else:
+            charge = 1 * (ionmode == "positive") - 1 * (ionmode == "negative")
+        values["charge"] = charge
+
+        if is_float(mass_multiplier):
+            mass_multiplier = float(mass_multiplier)
+        else:
+            mass_multiplier = 1.0
+        values["mass_multiplier"] = mass_multiplier
+
+        if is_float(correction_mass):
+            correction_mass = float(correction_mass)
+        else:
+            correction_mass = 1.007276 * numpy.sign(charge)
+        values["correction_mass"] = correction_mass
+
+        filled_dict[adduct] = values
+    return filled_dict
diff --git a/matchms/importing/parsing_utils.py b/matchms/importing/parsing_utils.py
index f58da5b7..c7050a35 100644
--- a/matchms/importing/parsing_utils.py
+++ b/matchms/importing/parsing_utils.py
@@ -32,8 +32,8 @@ def find_by_key(data: Union[list, dict], target: str) -> Any:
 def parse_mzml_mzxml_metadata(spectrum_dict: dict) -> dict:
     """Parse relevant mzml (or mzxml) metadata entries.
 
-    Parameters:
-    -----------
+    Parameters
+    ----------
     spectrum_dict:
         Spectrum dictionary containing metadata fields. Metadata parsing may easily
         break when field key names vary. The following metadata information is considered
diff --git a/matchms/utils.py b/matchms/utils.py
index d1fe939f..e871e5c7 100644
--- a/matchms/utils.py
+++ b/matchms/utils.py
@@ -1,6 +1,8 @@
 import re
 from typing import Optional
 import numpy
+from .importing import load_adducts_dict
+from .importing import load_known_adduct_conversions
 
 
 try:  # rdkit is not included in pip package
@@ -243,10 +245,58 @@ def looks_like_adduct(adduct):
     """Return True if input string has expected format of an adduct."""
     if not isinstance(adduct, str):
         return False
-    adduct = adduct.strip().replace("*", "")
+    # Clean adduct
+    adduct = clean_adduct(adduct)
+    # Load lists of default known adducts
+    known_adducts = load_adducts_dict()
+    if adduct in known_adducts:
+        return True
+
+    # Expect format like: "[2M-H]" or "[2M+Na]+"
+    regexp1 = r"^\[(([0-4]M)|(M[0-9])|(M))((Br)|(Br81)|(Cl)|(Cl37)|(S)){0,}[+-0-9][A-Z0-9\+\-\(\)|(Na)|(Ca)|(Mg)|(Cl)|(Li)|(Br)|(Ser)]{1,}[\]0-4+-]{1,4}"
+    return re.search(regexp1, adduct) is not None
+
+
+def clean_adduct(adduct: str) -> str:
+    """Clean adduct and make it consistent in style.
+    Will transform adduct strings of type 'M+H+' to '[M+H]+'.
 
-    # Format 1, e.g. "[2M-H]" or "[2M+Na]+"
-    regexp1 = r"^\[(([0-9]M)|(M[0-9])|(M)|(MBr)|(MCl))[+-0-9][A-Z0-9\+\-\(\)|(Na)|(Ca)|(Mg)|(Cl)|(Li)|(Br)|(Ser)]{1,}[\]0-9+-]{1,4}"
-    # Format 2, e.g. "M+Na+K" or "M+H-H20"
-    regexp2 = r"^(([0-9]M)|(M[0-9])|(M)|(MBr)|(MCl))[+-0-9][A-Z0-9\+\-\(\)|(Na)|(Ca)|(Mg)|(Cl)|(Li)|(Br)|(Ser)]{1,}"
-    return re.search(regexp1, adduct) is not None or re.search(regexp2, adduct) is not None
+    Parameters
+    ----------
+    adduct
+        Input adduct string to be cleaned/edited.
+    """
+    def get_adduct_charge(adduct):
+        regex_charges = r"[1-3]{0,1}[+,-]{1,2}$"
+        match = re.search(regex_charges, adduct)
+        if match:
+            return match.group(0)
+        return match
+
+    def adduct_conversion(adduct):
+        """Convert adduct if conversion rule is known"""
+        adduct_conversions = load_known_adduct_conversions()
+        if adduct in adduct_conversions:
+            return adduct_conversions[adduct]
+        return adduct
+
+    adduct = adduct.strip().replace("\n", "").replace("*", "")
+    adduct = adduct.replace("++", "2+").replace("--", "2-")
+    if adduct.startswith("["):
+        return adduct_conversion(adduct)
+
+    if adduct.endswith("]"):
+        return adduct_conversion("[" + adduct)
+
+    adduct_core = "[" + adduct
+    # Remove parts that can confuse the charge extraction
+    for mol_part in ["CH2", "CH3", "NH3", "NH4", "O2"]:
+        if mol_part in adduct:
+            adduct = adduct.split(mol_part)[-1]
+    adduct_charge = get_adduct_charge(adduct)
+
+    if adduct_charge is None:
+        return adduct_conversion(adduct_core + "]")
+
+    adduct_cleaned = adduct_core[:-len(adduct_charge)] + "]" + adduct_charge
+    return adduct_conversion(adduct_cleaned)
diff --git a/readthedocs/index.rst b/readthedocs/index.rst
index 15e64129..f2fae2c0 100644
--- a/readthedocs/index.rst
+++ b/readthedocs/index.rst
@@ -89,7 +89,7 @@ Should output
 .. testoutput::
 
     Removed adduct M-H from compound name.
-    Added adduct M-H to metadata.
+    Added adduct [M-H]- to metadata.
     ...
     Reference scan id: 1320
     Query scan id: 1372
diff --git a/setup.py b/setup.py
index 8bec4af3..99b7edce 100644
--- a/setup.py
+++ b/setup.py
@@ -52,7 +52,6 @@ setup(
         "numba >=0.47",
         "numpy",
         "pyteomics >=4.2",
-        "pyyaml",
         "requests",
         "scipy",
     ],
@@ -61,10 +60,10 @@ setup(
                             "prospector[with_pyroma]",
                             "pytest",
                             "pytest-cov",
-                            "sphinx>=3.0.0,!=3.2.0,<4.0.0",
+                            "sphinx>=3.0.0,!=3.2.0,!=3.5.0,<4.0.0",
                             "sphinx_rtd_theme",
                             "sphinxcontrib-apidoc",
                             "yapf",],
                     "chemistry": ["rdkit >=2020.03.1"]},
-    package_data={"matchms": ["data/*.yaml"]},
+    package_data={"matchms": ["data/*.csv"]},
 )


Test patch:
diff --git a/tests/test_add_parent_mass.py b/tests/test_add_parent_mass.py
index 4e1f73cb..46d2b1cf 100644
--- a/tests/test_add_parent_mass.py
+++ b/tests/test_add_parent_mass.py
@@ -1,4 +1,5 @@
 import numpy
+import pytest
 from matchms import Spectrum
 from matchms.filtering import add_parent_mass
 
@@ -33,6 +34,25 @@ def test_add_parent_mass_no_pepmass():
     assert numpy.abs(spectrum.get("parent_mass") - 445.0) < .01, "Expected parent mass of about 445.0."
 
 
+@pytest.mark.parametrize("adduct, expected", [("[M+2Na-H]+", 399.02884),
+                                              ("[M+H+NH4]2+", 212.47945),
+                                              ("[2M+FA-H]-", 843.001799)])
+def test_add_parent_mass_using_adduct(adduct, expected):
+    """Test if parent mass is correctly derived from adduct information."""
+    mz = numpy.array([], dtype='float')
+    intensities = numpy.array([], dtype='float')
+    metadata = {"precursor_mz": 444.0,
+                "adduct": adduct,
+                "charge": +1}
+    spectrum_in = Spectrum(mz=mz,
+                           intensities=intensities,
+                           metadata=metadata)
+
+    spectrum = add_parent_mass(spectrum_in)
+
+    assert numpy.allclose(spectrum.get("parent_mass"), expected, atol=1e-4), f"Expected parent mass of about {expected}."
+
+
 def test_empty_spectrum():
     spectrum_in = None
     spectrum = add_parent_mass(spectrum_in)
diff --git a/tests/test_clean_compound_name.py b/tests/test_clean_compound_name.py
index 428a5db9..0215a201 100644
--- a/tests/test_clean_compound_name.py
+++ b/tests/test_clean_compound_name.py
@@ -23,7 +23,9 @@ def test_clean_compound_name_removing_known_non_name_parts():
         ["ReSpect:PS013405 option1|option2|option3",
          "option3"],
         ["ReSpect:PS013405 option1name",
-         "option1name"]
+         "option1name"],
+        ["4,4-Dimethylcholest-8(9),24-dien-3.beta.-ol  231.2",
+         "4,4-Dimethylcholest-8(9),24-dien-3.beta.-ol"],
     ]
     for name_strings in test_name_strings:
         spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
diff --git a/tests/test_load_adducts.py b/tests/test_load_adducts.py
new file mode 100644
index 00000000..104c01c1
--- /dev/null
+++ b/tests/test_load_adducts.py
@@ -0,0 +1,40 @@
+import numpy
+from matchms.importing import load_adducts_dict
+from matchms.importing import load_known_adduct_conversions
+from matchms.importing.load_adducts import _convert_and_fill_dict
+
+
+def test_load_adducts_dict():
+    """Test if correct dict is imported."""
+    known_adducts = load_adducts_dict()
+    assert isinstance(known_adducts, dict), "Expected dictionary"
+    assert "[M+2H+Na]3+" in known_adducts, "Expected adduct to be in dictionary"
+    assert "[M+CH3COO]-" in known_adducts, "Expected adduct to be in dictionary"
+    assert known_adducts["[M+2H+Na]3+"]["charge"] == 3, "Expected different entry"
+    assert numpy.all([(key[0] == "[") for key in known_adducts]), \
+        "Expected all keys to start with '['."
+    assert known_adducts["[M]+"]["charge"] == 1, "Expected different added entry"
+    assert numpy.allclose(known_adducts["[M]-"]["correction_mass"], -1.007276, atol=1e-5), \
+        "Expected different added entry"
+
+
+def test_convert_and_fill_dict():
+    """Test if conversion is done right."""
+    test_dict = dict()
+    test_dict["[M]+"] = {'ionmode': 'negative',
+                         'charge': "n/a",
+                         'mass_multiplier': "n/a",
+                         'correction_mass': "n/a"}
+    converted_dict = _convert_and_fill_dict(test_dict)
+    assert isinstance(converted_dict["[M]+"], dict), "Expected dictionary"
+    assert converted_dict["[M]+"]["charge"] == -1, "Expected charge of -1"
+    assert converted_dict["[M]+"]["mass_multiplier"] == 1, "Expected mass_multiplier of 1"
+    assert numpy.allclose(converted_dict["[M]+"]["correction_mass"], -1.007276, atol=1e-5), \
+        "Expected correction_mass to be changed to -1.007276"
+
+
+def test_load_known_adduct_conversions():
+    """Test if correct data is imported."""
+    adduct_conversions = load_known_adduct_conversions()
+    assert isinstance(adduct_conversions, dict), "Expected result to be dict"
+    assert adduct_conversions["[M-H-H2O]"] == "[M-H2O-H]-", "Expected different conversion rule."
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4ef47eb3..63000f9e 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -7,11 +7,13 @@ from unittest import mock
 import numpy
 import pytest
 import matchms.utils
+from matchms.utils import clean_adduct
 from matchms.utils import derive_fingerprint_from_inchi
 from matchms.utils import derive_fingerprint_from_smiles
 from matchms.utils import is_valid_inchi
 from matchms.utils import is_valid_inchikey
 from matchms.utils import is_valid_smiles
+from matchms.utils import looks_like_adduct
 from matchms.utils import mol_converter
 
 
@@ -199,3 +201,28 @@ def test_missing_rdkit_module_error():
         with pytest.raises(ImportError) as msg:
             _ = matchms.utils.mol_to_fingerprint(mol_input, "test", 0)
         assert expected_msg in str(msg.value), "Expected different ImportError."
+
+
+def test_looks_like_adduct():
+    """Test if adducts are correctly identified"""
+    for adduct in ["M+", "M*+", "M+Cl", "[M+H]", "[2M+Na]+", "M+H+K", "Cat",
+                   "MS+Na", "MS+H", "M3Cl37+Na", "[M+H+H2O]"]:
+        assert looks_like_adduct(adduct), "Expected this to be identified as adduct"
+    for adduct in ["N+", "B*+", "++", "--", "[--]", "H+M+K"]:
+        assert not looks_like_adduct(adduct), "Expected this not to be identified as adduct"
+
+
+@pytest.mark.parametrize("input_adduct, expected_adduct",
+                         [("M+", "[M]+"),
+                          ("M+CH3COO-", "[M+CH3COO]-"),
+                          ("M+CH3COO", "[M+CH3COO]-"),
+                          ("M-CH3-", "[M-CH3]-"),
+                          ("M+2H++", "[M+2H]2+"),
+                          ("[2M+Na]", "[2M+Na]+"),
+                          ("2M+Na", "[2M+Na]+"),
+                          ("M+NH3+", "[M+NH3]+"),
+                          ("M-H2O+2H2+", "[M-H2O+2H]2+")])
+def test_clean_adduct_examples(input_adduct, expected_adduct):
+    """Test if typical examples are correctly edited."""
+    assert clean_adduct(input_adduct) == expected_adduct, \
+        "Expected different cleaned adduct"