Repository: matchms/matchms
Pull request #156 (2020-11-24 15:00:51)

Add precursor-mz match score
- fixed minor bug in ``add_parent_mass()`` : now finds precursor_mz from either pepmass or precursor_mz field.
- added ``PrecursorMzMatch`` which is analog to ``ParentMassMatch`` (renamed here from ``ParentmassMatch``).
- include option to match based on relative ``ppm`` tolerance

jspaaks: BTW be aware that the changes in #150 will affect basically everything we had up to this point, so probably good to take a look at that before any other work.
florian-huber: @jspaaks Do you want to merge the different PRs? 
#155 and then #156 will after merging probably imply some changes according to #150.
jspaaks: I think I'm right in saying that
- #155 can now be merged into master
- this should make #156 mergable (I didn't check btw)
- #158 can be merged

If you agree, then feel free to go ahead and merge them (also delete any of your branches after they have been merged)

And then I will merge #150 sometime next week, but I wanted to give the others opportunity to comment, even though @fdiblen kindly approved the PR already.

florian-huber: Thanks for reviewing Juriaan!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b131584..3b00b723 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Added
+
+- PrecursorMzMatch for deriving precursor m/z matches within a given tolerance [#156](https://github.com/matchms/matchms/pull/156)
+
 ### Changed
 
 - Raise error for improper use of reduce_to_number_of_peaks filter [#151](https://github.com/matchms/matchms/pull/151)
+- Renamed ParentmassMatch to ParentMassMatch [#156](https://github.com/matchms/matchms/pull/156)
 
 ### Fixed
 
diff --git a/matchms/filtering/add_parent_mass.py b/matchms/filtering/add_parent_mass.py
index ccea3e75..f7a77c9a 100644
--- a/matchms/filtering/add_parent_mass.py
+++ b/matchms/filtering/add_parent_mass.py
@@ -5,8 +5,9 @@ from ..typing import SpectrumType
 def add_parent_mass(spectrum_in: SpectrumType) -> SpectrumType:
     """Add parentmass to metadata (if not present yet).
 
-    Method to calculate the parent mass from given precursor mass
-    and charge.
+    Method to calculate the parent mass from given precursor m/z
+    and charge. Will take precursor m/z from either "precursor_mz"
+    or "pepmass" field.
     """
     if spectrum_in is None:
         return None
@@ -17,7 +18,9 @@ def add_parent_mass(spectrum_in: SpectrumType) -> SpectrumType:
         try:
             charge = spectrum.get("charge")
             protons_mass = PROTON_MASS * charge
-            precursor_mz = spectrum.get("pepmass")[0]
+            precursor_mz = spectrum.get("precursor_mz", None)
+            if precursor_mz is None:
+                precursor_mz = spectrum.get("pepmass")[0]
             precursor_mass = precursor_mz * abs(charge)
             parent_mass = precursor_mass - protons_mass
             if parent_mass:
diff --git a/matchms/similarity/ParentmassMatch.py b/matchms/similarity/ParentMassMatch.py
similarity index 96%
rename from matchms/similarity/ParentmassMatch.py
rename to matchms/similarity/ParentMassMatch.py
index f3599431..16e71c3e 100644
--- a/matchms/similarity/ParentmassMatch.py
+++ b/matchms/similarity/ParentMassMatch.py
@@ -5,7 +5,7 @@ from matchms.typing import SpectrumType
 from .BaseSimilarity import BaseSimilarity
 
 
-class ParentmassMatch(BaseSimilarity):
+class ParentMassMatch(BaseSimilarity):
     """Return True if spectrums match in parent mass (within tolerance), and False otherwise.
 
     Example to calculate scores between 2 spectrums and iterate over the scores
@@ -15,7 +15,7 @@ class ParentmassMatch(BaseSimilarity):
         import numpy as np
         from matchms import calculate_scores
         from matchms import Spectrum
-        from matchms.similarity import ParentmassMatch
+        from matchms.similarity import ParentMassMatch
 
         spectrum_1 = Spectrum(mz=np.array([]),
                               intensities=np.array([]),
@@ -32,7 +32,7 @@ class ParentmassMatch(BaseSimilarity):
         references = [spectrum_1, spectrum_2]
         queries = [spectrum_3, spectrum_4]
 
-        similarity_score = ParentmassMatch(tolerance=5.0)
+        similarity_score = ParentMassMatch(tolerance=5.0)
         scores = calculate_scores(references, queries, similarity_score)
 
         for (reference, query, score) in scores:
diff --git a/matchms/similarity/PrecursorMzMatch.py b/matchms/similarity/PrecursorMzMatch.py
new file mode 100644
index 00000000..f852327c
--- /dev/null
+++ b/matchms/similarity/PrecursorMzMatch.py
@@ -0,0 +1,164 @@
+from typing import List
+import numba
+import numpy
+from matchms.typing import SpectrumType
+from .BaseSimilarity import BaseSimilarity
+
+
+class PrecursorMzMatch(BaseSimilarity):
+    """Return True if spectrums match in precursor m/z (within tolerance), and False otherwise.
+    The match within tolerance can be calculated based on an absolute m/z difference
+    (tolerance_type="Dalton") or based on a relative difference in ppm (tolerance_type="ppm").
+
+    Example to calculate scores between 2 pairs of spectrums and iterate over the scores
+
+    .. testcode::
+
+        import numpy as np
+        from matchms import calculate_scores
+        from matchms import Spectrum
+        from matchms.similarity import PrecursorMzMatch
+
+        spectrum_1 = Spectrum(mz=np.array([]),
+                              intensities=np.array([]),
+                              metadata={"id": "1", "precursor_mz": 100})
+        spectrum_2 = Spectrum(mz=np.array([]),
+                              intensities=np.array([]),
+                              metadata={"id": "2", "precursor_mz": 110})
+        spectrum_3 = Spectrum(mz=np.array([]),
+                              intensities=np.array([]),
+                              metadata={"id": "3", "precursor_mz": 103})
+        spectrum_4 = Spectrum(mz=np.array([]),
+                              intensities=np.array([]),
+                              metadata={"id": "4", "precursor_mz": 111})
+        references = [spectrum_1, spectrum_2]
+        queries = [spectrum_3, spectrum_4]
+
+        similarity_score = PrecursorMzMatch(tolerance=5.0, tolerance_type="Dalton")
+        scores = calculate_scores(references, queries, similarity_score)
+
+        for (reference, query, score) in scores:
+            print(f"Precursor m/z match between {reference.get('id')} and {query.get('id')}" +
+                  f" is {score:.2f}")
+
+    Should output
+
+    .. testoutput::
+
+        Precursor m/z match between 1 and 3 is 1.00
+        Precursor m/z match between 1 and 4 is 0.00
+        Precursor m/z match between 2 and 3 is 0.00
+        Precursor m/z match between 2 and 4 is 1.00
+
+    """
+    # Set key characteristics as class attributes
+    is_commutative = True
+
+    def __init__(self, tolerance: float = 0.1, tolerance_type: str = "Dalton"):
+        """
+        Parameters
+        ----------
+        tolerance
+            Specify tolerance below which two m/z are counted as match.
+        tolerance_type
+            Chose between fixed tolerance in Dalton (="Dalton") or a relative difference
+            in ppm (="ppm").
+        """
+        self.tolerance = tolerance
+        assert tolerance_type in ["Dalton", "ppm"], "Expected type from ['Dalton', 'ppm']"
+        self.type = tolerance_type
+
+    def pair(self, reference: SpectrumType, query: SpectrumType) -> float:
+        """Compare precursor m/z between reference and query spectrum.
+
+        Parameters
+        ----------
+        reference
+            Single reference spectrum.
+        query
+            Single query spectrum.
+        """
+        precursormz_ref = reference.get("precursor_mz")
+        precursormz_query = query.get("precursor_mz")
+        assert precursormz_ref is not None and precursormz_query is not None, "Missing precursor m/z."
+
+        if self.type == "Dalton":
+            return abs(precursormz_ref - precursormz_query) <= self.tolerance
+
+        mean_mz = (precursormz_ref + precursormz_query) / 2
+        return abs(precursormz_ref - precursormz_query)/mean_mz <= self.tolerance
+
+    def matrix(self, references: List[SpectrumType], queries: List[SpectrumType],
+               is_symmetric: bool = False) -> numpy.ndarray:
+        """Compare parent masses between all references and queries.
+
+        Parameters
+        ----------
+        references
+            List/array of reference spectrums.
+        queries
+            List/array of Single query spectrums.
+        is_symmetric
+            Set to True when *references* and *queries* are identical (as for instance for an all-vs-all
+            comparison). By using the fact that score[i,j] = score[j,i] the calculation will be about
+            2x faster.
+        """
+        def collect_precursormz(spectrums):
+            """Collect precursors."""
+            precursors = []
+            for spectrum in spectrums:
+                precursormz = spectrum.get("precursor_mz")
+                assert precursormz is not None, "Missing precursor m/z."
+                precursors.append(precursormz)
+            return numpy.asarray(precursors)
+
+        precursors_ref = collect_precursormz(references)
+        precursors_query = collect_precursormz(queries)
+        if is_symmetric and self.type == "Dalton":
+            return precursormz_scores_symmetric(precursors_ref, precursors_query, self.tolerance).astype(bool)
+        if is_symmetric and self.type == "ppm":
+            return precursormz_scores_symmetric_ppm(precursors_ref, precursors_query, self.tolerance).astype(bool)
+        if self.type == "Dalton":
+            return precursormz_scores(precursors_ref, precursors_query, self.tolerance).astype(bool)
+        return precursormz_scores_ppm(precursors_ref, precursors_query, self.tolerance).astype(bool)
+
+
+@numba.njit
+def precursormz_scores(precursors_ref, precursors_query, tolerance):
+    scores = numpy.zeros((len(precursors_ref), len(precursors_query)))
+    for i, precursormz_ref in enumerate(precursors_ref):
+        for j, precursormz_query in enumerate(precursors_query):
+            scores[i, j] = (abs(precursormz_ref - precursormz_query) <= tolerance)
+    return scores
+
+
+@numba.njit
+def precursormz_scores_symmetric(precursors_ref, precursors_query, tolerance):
+    scores = numpy.zeros((len(precursors_ref), len(precursors_query)))
+    for i, precursormz_ref in enumerate(precursors_ref):
+        for j in range(i, len(precursors_query)):
+            scores[i, j] = (abs(precursormz_ref - precursors_query[j]) <= tolerance)
+            scores[j, i] = scores[i, j]
+    return scores
+
+
+@numba.njit
+def precursormz_scores_ppm(precursors_ref, precursors_query, tolerance_ppm):
+    scores = numpy.zeros((len(precursors_ref), len(precursors_query)))
+    for i, precursormz_ref in enumerate(precursors_ref):
+        for j, precursormz_query in enumerate(precursors_query):
+            mean_mz = (precursormz_ref + precursormz_query)/2
+            scores[i, j] = (abs(precursormz_ref - precursormz_query)/mean_mz * 1e6 <= tolerance_ppm)
+    return scores
+
+
+@numba.njit
+def precursormz_scores_symmetric_ppm(precursors_ref, precursors_query, tolerance_ppm):
+    scores = numpy.zeros((len(precursors_ref), len(precursors_query)))
+    for i, precursormz_ref in enumerate(precursors_ref):
+        for j in range(i, len(precursors_query)):
+            mean_mz = (precursormz_ref + precursors_query[j])/2
+            diff_ppm = abs(precursormz_ref - precursors_query[j])/mean_mz * 1e6
+            scores[i, j] = (diff_ppm <= tolerance_ppm)
+            scores[j, i] = scores[i, j]
+    return scores
diff --git a/matchms/similarity/__init__.py b/matchms/similarity/__init__.py
index 638db92d..c2db3804 100644
--- a/matchms/similarity/__init__.py
+++ b/matchms/similarity/__init__.py
@@ -7,8 +7,8 @@ spectra. This includes
 
 * scores based on comparing peak positions and intensities
   (:class:`~matchms.similarity.CosineGreedy` or :class:`~matchms.similarity.ModifiedCosine`)
-* simple scores that only assess parent mass matches
-  (:class:`~matchms.similarity.ParentmassMatch`)
+* simple scores that only assess precursor m/z or parent mass matches
+  (:class:`~matchms.similarity.PrecursorMzMatch` or: :class:`~matchms.similarity.ParentMassMatch`)
 * scores assessing molecular similarity if structures (SMILES, InchiKey) are given as metadata
   (:class:`~matchms.similarity.FingerprintSimilarity`)
 
@@ -20,7 +20,8 @@ from .CosineHungarian import CosineHungarian
 from .FingerprintSimilarity import FingerprintSimilarity
 from .IntersectMz import IntersectMz
 from .ModifiedCosine import ModifiedCosine
-from .ParentmassMatch import ParentmassMatch
+from .ParentMassMatch import ParentMassMatch
+from .PrecursorMzMatch import PrecursorMzMatch
 
 
 __all__ = [
@@ -29,5 +30,6 @@ __all__ = [
     "FingerprintSimilarity",
     "IntersectMz",
     "ModifiedCosine",
-    "ParentmassMatch",
+    "ParentMassMatch",
+    "PrecursorMzMatch",
 ]


Test patch:
diff --git a/tests/test_ParentmassMatch.py b/tests/test_ParentmassMatch.py
index 657b420b..1bded5cb 100644
--- a/tests/test_ParentmassMatch.py
+++ b/tests/test_ParentmassMatch.py
@@ -1,9 +1,9 @@
 import numpy
 import pytest
 from matchms import Spectrum
-from matchms.similarity import ParentmassMatch
-from matchms.similarity.ParentmassMatch import parentmass_scores
-from matchms.similarity.ParentmassMatch import parentmass_scores_symmetric
+from matchms.similarity import ParentMassMatch
+from matchms.similarity.ParentMassMatch import parentmass_scores
+from matchms.similarity.ParentMassMatch import parentmass_scores_symmetric
 
 
 def test_parentmass_match():
@@ -16,7 +16,7 @@ def test_parentmass_match():
                           intensities=numpy.array([], dtype="float"),
                           metadata={"parent_mass": 101.0})
 
-    similarity_score = ParentmassMatch()
+    similarity_score = ParentMassMatch()
     score = similarity_score.pair(spectrum_1, spectrum_2)
     assert not score, "Expected different score."
 
@@ -31,7 +31,7 @@ def test_parentmass_match_tolerance2():
                           intensities=numpy.array([], dtype="float"),
                           metadata={"parent_mass": 101.0})
 
-    similarity_score = ParentmassMatch(tolerance=2.0)
+    similarity_score = ParentMassMatch(tolerance=2.0)
     score = similarity_score.pair(spectrum_1, spectrum_2)
     assert score, "Expected different score."
 
@@ -46,7 +46,7 @@ def test_parentmass_match_missing_parentmass():
                           intensities=numpy.array([], dtype="float"),
                           metadata={})
 
-    similarity_score = ParentmassMatch(tolerance=2.0)
+    similarity_score = ParentMassMatch(tolerance=2.0)
 
     with pytest.raises(AssertionError) as msg:
         _ = similarity_score.pair(spectrum_1, spectrum_2)
@@ -73,7 +73,7 @@ def test_parentmass_match_array():
                           intensities=numpy.array([], dtype="float"),
                           metadata={"parent_mass": 98.0})
 
-    similarity_score = ParentmassMatch()
+    similarity_score = ParentMassMatch()
     scores = similarity_score.matrix([spectrum_1, spectrum_2],
                                      [spectrum_a, spectrum_b])
     assert numpy.all(scores == numpy.array([[False, False],
@@ -98,7 +98,7 @@ def test_parentmass_match_tolerance2_array():
                           intensities=numpy.array([], dtype="float"),
                           metadata={"parent_mass": 98.0})
 
-    similarity_score = ParentmassMatch(tolerance=2.0)
+    similarity_score = ParentMassMatch(tolerance=2.0)
     scores = similarity_score.matrix([spectrum_1, spectrum_2],
                                      [spectrum_a, spectrum_b])
     assert numpy.all(scores == numpy.array([[True, True],
@@ -124,7 +124,7 @@ def test_parentmass_match_array_symmetric():
                           metadata={"parent_mass": 98.0})
 
     spectrums = [spectrum_1, spectrum_2, spectrum_3, spectrum_4]
-    similarity_score = ParentmassMatch()
+    similarity_score = ParentMassMatch()
     scores = similarity_score.matrix(spectrums, spectrums, is_symmetric=True)
     scores2 = similarity_score.matrix(spectrums, spectrums, is_symmetric=False)
 
diff --git a/tests/test_PrecursormzMatch.py b/tests/test_PrecursormzMatch.py
new file mode 100644
index 00000000..c09400f2
--- /dev/null
+++ b/tests/test_PrecursormzMatch.py
@@ -0,0 +1,264 @@
+import numpy
+import pytest
+from matchms import Spectrum
+from matchms.similarity import PrecursorMzMatch
+from matchms.similarity.PrecursorMzMatch import precursormz_scores
+from matchms.similarity.PrecursorMzMatch import precursormz_scores_ppm
+from matchms.similarity.PrecursorMzMatch import precursormz_scores_symmetric
+from matchms.similarity.PrecursorMzMatch import \
+    precursormz_scores_symmetric_ppm
+
+
+def test_precursormz_match():
+    """Test with default tolerance."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 101.0})
+
+    similarity_score = PrecursorMzMatch()
+    score = similarity_score.pair(spectrum_1, spectrum_2)
+    assert not score, "Expected different score."
+
+
+def test_precursormz_match_tolerance2():
+    """Test with tolerance > difference."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 101.0})
+
+    similarity_score = PrecursorMzMatch(tolerance=2.0)
+    score = similarity_score.pair(spectrum_1, spectrum_2)
+    assert score, "Expected different score."
+
+
+def test_precursormz_match_tolerance_ppm():
+    """Test with tolerance > difference in ppm."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 600.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 600.001})
+
+    similarity_score = PrecursorMzMatch(tolerance=2.0, tolerance_type="ppm")
+    score = similarity_score.pair(spectrum_1, spectrum_2)
+    assert score, "Expected different score."
+
+
+def test_precursormz_match_missing_precursormz():
+    """Test with missing precursormz."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={})
+
+    similarity_score = PrecursorMzMatch(tolerance=2.0)
+
+    with pytest.raises(AssertionError) as msg:
+        _ = similarity_score.pair(spectrum_1, spectrum_2)
+
+    expected_message_part = "Missing precursor m/z."
+    assert expected_message_part in str(msg.value), "Expected particular error message."
+
+
+def test_precursormz_match_array():
+    """Test with array and default tolerance."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 101.0})
+
+    spectrum_a = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 99.0})
+
+    spectrum_b = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 98.0})
+
+    similarity_score = PrecursorMzMatch()
+    scores = similarity_score.matrix([spectrum_1, spectrum_2],
+                                     [spectrum_a, spectrum_b])
+    assert numpy.all(scores == numpy.array([[False, False],
+                                            [False, False]])), "Expected different scores."
+
+
+def test_precursormz_match_tolerance2_array():
+    """Test with array and tolerance=2."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 101.0})
+
+    spectrum_a = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 99.0})
+
+    spectrum_b = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 98.0})
+
+    similarity_score = PrecursorMzMatch(tolerance=2.0)
+    scores = similarity_score.matrix([spectrum_1, spectrum_2],
+                                     [spectrum_a, spectrum_b])
+    assert numpy.all(scores == numpy.array([[True, True],
+                                            [True, False]])), "Expected different scores."
+
+
+def test_precursormz_match_tolerance2_array_ppm():
+    """Test with array and tolerance=2 and type=ppm."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 101.0})
+
+    spectrum_a = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 99.99})
+
+    spectrum_b = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 98.0})
+
+    similarity_score = PrecursorMzMatch(tolerance=101.0, tolerance_type="ppm")
+    scores = similarity_score.matrix([spectrum_1, spectrum_2],
+                                     [spectrum_a, spectrum_b])
+    assert numpy.all(scores == numpy.array([[True, False],
+                                            [False, False]])), "Expected different scores."
+
+
+def test_precursormz_match_array_symmetric():
+    """Test with array and is_symmetric=True."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 101.0})
+
+    spectrum_3 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 99.95})
+
+    spectrum_4 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 98.0})
+
+    spectrums = [spectrum_1, spectrum_2, spectrum_3, spectrum_4]
+    similarity_score = PrecursorMzMatch()
+    scores = similarity_score.matrix(spectrums, spectrums, is_symmetric=True)
+    scores2 = similarity_score.matrix(spectrums, spectrums, is_symmetric=False)
+
+    assert numpy.all(scores == scores2), "Expected identical scores"
+    assert numpy.all(scores == numpy.array(
+        [[True, False, True, False],
+         [False, True, False, False],
+         [True, False, True, False],
+         [False, False, False, True]])), "Expected different scores"
+
+
+def test_precursormz_match_array_symmetric_pmm():
+    """Test with array and is_symmetric=True for type="ppm"."""
+    spectrum_1 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.0})
+
+    spectrum_2 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 100.01})
+
+    spectrum_3 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 99.99999})
+
+    spectrum_4 = Spectrum(mz=numpy.array([], dtype="float"),
+                          intensities=numpy.array([], dtype="float"),
+                          metadata={"precursor_mz": 99.9})
+
+    spectrums = [spectrum_1, spectrum_2, spectrum_3, spectrum_4]
+    similarity_score = PrecursorMzMatch(tolerance=5.0, tolerance_type="ppm")
+    scores = similarity_score.matrix(spectrums, spectrums, is_symmetric=True)
+    scores2 = similarity_score.matrix(spectrums, spectrums, is_symmetric=False)
+
+    assert numpy.all(scores == scores2), "Expected identical scores"
+    assert numpy.all(scores == numpy.array(
+        [[True, False, True, False],
+         [False, True, False, False],
+         [True, False, True, False],
+         [False, False, False, True]])), "Expected different scores"
+
+
+@pytest.mark.parametrize("numba_compiled", [True, False])
+def test_precursormz_scores(numba_compiled):
+    """Test the underlying score function (pure Python and numba compiled)."""
+    precursors_ref = numpy.asarray([101, 200, 300])
+    precursors_query = numpy.asarray([100, 301])
+    if numba_compiled:
+        scores = precursormz_scores(precursors_ref, precursors_query, tolerance=2.0)
+    else:
+        scores = precursormz_scores.py_func(precursors_ref, precursors_query, tolerance=2.0)
+    assert numpy.all(scores == numpy.array([[1., 0.],
+                                            [0., 0.],
+                                            [0., 1.]])), "Expected different scores."
+
+
+@pytest.mark.parametrize("numba_compiled", [True, False])
+def test_precursormz_scores_symmetric(numba_compiled):
+    """Test the underlying score function (non-compiled)."""
+    precursors = numpy.asarray([101, 100, 200])
+    if numba_compiled:
+        scores = precursormz_scores_symmetric(precursors, precursors, tolerance=2.0)
+    else:
+        scores = precursormz_scores_symmetric.py_func(precursors, precursors, tolerance=2.0)
+    assert numpy.all(scores == numpy.array([[1., 1., 0.],
+                                            [1., 1., 0.],
+                                            [0., 0., 1.]])), "Expected different scores."
+
+
+@pytest.mark.parametrize("numba_compiled", [True, False])
+def test_precursormz_scores_ppm(numba_compiled):
+    """Test the underlying score function (pure Python and numba compiled)."""
+    precursors_ref = numpy.asarray([100.00001, 200, 300])
+    precursors_query = numpy.asarray([100, 300.00001])
+    if numba_compiled:
+        scores = precursormz_scores_ppm(precursors_ref, precursors_query, tolerance_ppm=2.0)
+    else:
+        scores = precursormz_scores_ppm.py_func(precursors_ref, precursors_query, tolerance_ppm=2.0)
+    assert numpy.all(scores == numpy.array([[1., 0.],
+                                            [0., 0.],
+                                            [0., 1.]])), "Expected different scores."
+
+
+@pytest.mark.parametrize("numba_compiled", [True, False])
+def test_precursormz_scores_symmetric_ppm(numba_compiled):
+    """Test the underlying score function (non-compiled)."""
+    precursors = numpy.asarray([100.00001, 100, 200])
+    if numba_compiled:
+        scores = precursormz_scores_symmetric_ppm(precursors, precursors, tolerance_ppm=2.0)
+    else:
+        scores = precursormz_scores_symmetric_ppm.py_func(precursors, precursors, tolerance_ppm=2.0)
+    assert numpy.all(scores == numpy.array([[1., 1., 0.],
+                                            [1., 1., 0.],
+                                            [0., 0., 1.]])), "Expected different scores."
diff --git a/tests/test_add_parent_mass.py b/tests/test_add_parent_mass.py
index 7929b38c..e777fb9d 100644
--- a/tests/test_add_parent_mass.py
+++ b/tests/test_add_parent_mass.py
@@ -16,3 +16,18 @@ def test_add_parent_mass():
     spectrum = add_parent_mass(spectrum_in)
 
     assert numpy.abs(spectrum.get("parent_mass") - 445.0) < .01, "Expected parent mass of about 445.0."
+
+
+def test_add_parent_mass_no_pepmass():
+    """Test if parent mass is correctly derived if "pepmass" is not present."""
+    mz = numpy.array([], dtype='float')
+    intensities = numpy.array([], dtype='float')
+    metadata = {"precursor_mz": 444.0,
+                "charge": -1}
+    spectrum_in = Spectrum(mz=mz,
+                           intensities=intensities,
+                           metadata=metadata)
+
+    spectrum = add_parent_mass(spectrum_in)
+
+    assert numpy.abs(spectrum.get("parent_mass") - 445.0) < .01, "Expected parent mass of about 445.0."