Repository: matchms/matchms
Pull request #277 (2021-12-09 10:26:01)

Add handling MSP files with peak comments (issue #249)
* modified `load_from_msp` to read peak comments
* modified `save_as_msp` to write peak comments to a file and increased the tab size to avoid misaligned comments in the output file
* added peak-comments handling to `Spectrum` class: the comments can now be stored as an `mz: comment` dictionary in `peak_comments` property of `Spectrum()` objects

**Notes:**
0d7d513 selects **mz** & **intensity** rather than **rt** as the commit message suggests


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7bf88df6..c04d8ae7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,10 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - better, more flexible string handling of `ModifiedCosine` [#275](https://github.com/matchms/matchms/pull/275)
 - matchms logger, replacing all former `print` statments to better control logging output [#271](https://github.com/matchms/matchms/pull/271)
 - `add_logging_to_file()`, `set_matchms_logger_level()`, `reset_matchms_logger()` functions to adapt logging output to user needs [#271](https://github.com/matchms/matchms/pull/271)
+- `Spectrum()` objects can now store peak comments as an `mz: comment` dictionary in `peak_comments` property [#277](https://github.com/matchms/matchms/pull/277)
 
 ## Changed
 
-- `save_to_msp()` can now also write to files with other than ".msp" extensions such as ".dat" [#276](https://github.com/matchms/matchms/pull/276)
+- `save_as_msp()` can now also write to files with other than ".msp" extensions such as ".dat" [#276](https://github.com/matchms/matchms/pull/276)
+- `save_as_msp()` now writes peak comments (if present) to the output file [#277](https://github.com/matchms/matchms/pull/277)
+- `load_from_msp()` now also reads peak comments [#277](https://github.com/matchms/matchms/pull/277)
 
 ## Changed
 
diff --git a/matchms/Spectrum.py b/matchms/Spectrum.py
index 1361d8dc..8acb04da 100644
--- a/matchms/Spectrum.py
+++ b/matchms/Spectrum.py
@@ -19,11 +19,13 @@ class Spectrum:
 
         spectrum = Spectrum(mz=np.array([100, 150, 200.]),
                               intensities=np.array([0.7, 0.2, 0.1]),
-                              metadata={'id': 'spectrum1'})
+                              metadata={'id': 'spectrum1'},
+                              peak_comments={200.: "the peak at 200 m/z"})
 
         print(spectrum.peaks.mz[0])
         print(spectrum.peaks.intensities[0])
         print(spectrum.get('id'))
+        print(spectrum.peak_comments.get(200))
 
     Should output
 
@@ -32,6 +34,7 @@ class Spectrum:
         100.0
         0.7
         spectrum1
+        'the peak at 200 m/z'
 
     Attributes
     ----------
@@ -48,10 +51,18 @@ class Spectrum:
             spectrum.losess = Spikes(mz=np.array([50.]), intensities=np.array([0.1]))
     metadata: dict
         Dict of metadata with for example the scan number of precursor m/z.
+    peak_comments: dict
+        Dict of comments for each m/z peak.
 
     """
 
-    def __init__(self, mz: numpy.array, intensities: numpy.array, metadata: Optional[dict] = None):
+    _peak_comments_mz_tolerance = 1e-05
+
+    def __init__(self,
+                 mz: numpy.array,
+                 intensities: numpy.array,
+                 metadata: Optional[dict] = None,
+                 peak_comments: Optional[dict] = None):
         """
 
         Parameters
@@ -62,6 +73,8 @@ class Spectrum:
             Array of intensities for the peaks
         metadata
             Dictionary with for example the scan number of precursor m/z.
+        peak_comments
+            Dictionary with comments assigned to their m/z peaks.
         """
         self.peaks = Spikes(mz=mz, intensities=intensities)
         self.losses = None
@@ -69,6 +82,10 @@ class Spectrum:
             self.metadata = {}
         else:
             self.metadata = metadata
+        if peak_comments:
+            self.peak_comments = peak_comments
+        else:
+            self.peak_comments = {}
 
     def __eq__(self, other):
         return \
@@ -223,4 +240,38 @@ class Spectrum:
 
     @peaks.setter
     def peaks(self, value: Spikes):
+        if hasattr(self, "_peak_comments"):
+            self._reiterate_peak_comments(value)
         self._peaks = value
+
+    @property
+    def peak_comments(self):
+        return self._peak_comments
+
+    @peak_comments.setter
+    def peak_comments(self, value):
+        self._peak_comments = value
+
+    @classmethod
+    def update_peak_comments_mz_tolerance(cls, mz_tolerance: float):
+        cls._peak_comments_mz_tolerance = mz_tolerance
+
+    def _reiterate_peak_comments(self, peaks: Spikes):
+        """Update the peak comments to reflect the new peaks."""
+        mz_tolerance = self._peak_comments_mz_tolerance
+
+        def _append_new_comment():
+            if new_key_comment is not None:
+                comment = "; ".join([new_key_comment, self.peak_comments[key]])
+            else:
+                comment = self.peak_comments[key]
+            return comment
+
+        for key in list(self.peak_comments.keys()):
+            if key not in peaks.mz:
+                if numpy.isclose(key, peaks.mz, rtol=mz_tolerance).any():
+                    new_key = peaks.mz[numpy.isclose(key, peaks.mz, rtol=mz_tolerance).argmax()]
+                    new_key_comment = self.peak_comments.get(new_key, None)
+                    new_key_comment = _append_new_comment()
+                    self.peak_comments[new_key] = new_key_comment
+                self.peak_comments.pop(key)
diff --git a/matchms/exporting/save_as_msp.py b/matchms/exporting/save_as_msp.py
index d7b73529..047a7c5d 100644
--- a/matchms/exporting/save_as_msp.py
+++ b/matchms/exporting/save_as_msp.py
@@ -1,84 +1,95 @@
-import logging
-import os
-from typing import IO
-from typing import List
-from ..Spectrum import Spectrum
-from ..Spikes import Spikes
-
-
-logger = logging.getLogger("matchms")
-
-
-_extentions_not_allowed = ["mzml", "mzxml", "json", "mgf"]
-
-
-def save_as_msp(spectra: List[Spectrum], filename: str):
-    """Save spectrum(s) as msp file.
-
-    :py:attr:`~matchms.Spectrum.losses` of spectrum will not be saved.
-
-    Example:
-
-    .. code-block:: python
-
-        import numpy
-        from matchms import Spectrum
-        from matchms.exporting import save_as_msp
-
-        # Create dummy spectrum
-        spectrum = Spectrum(mz=numpy.array([100, 200, 300], dtype="float"),
-                            intensities=numpy.array([10, 10, 500], dtype="float"),
-                            metadata={"charge": -1,
-                                      "inchi": '"InChI=1S/C6H12"',
-                                      "precursor_mz": 222.2})
-
-        # Write spectrum to test file
-        save_as_msp(spectrum, "test.msp")
-
-    Parameters
-    ----------
-    spectra:
-        Expected input are match.Spectrum.Spectrum() objects.
-    filename:
-        Provide filename to save spectrum(s).
-    """
-    file_extension = filename.split(".")[-1]
-    assert file_extension.lower() not in _extentions_not_allowed, \
-        f"File extension '.{file_extension}' not allowed."
-    if not filename.endswith(".msp"):
-        logger.warning("Spectra will be stored as msp file with extension .%s",
-                       filename.split(".")[-1])
-    spectra = _ensure_list(spectra)
-
-    with open(filename, "w", encoding="utf-8") as outfile:
-        for spectrum in spectra:
-            _write_spectrum(spectrum, outfile)
-
-
-def _write_spectrum(spectrum: Spectrum, outfile: IO):
-    _write_metadata(spectrum.metadata, outfile)
-    _write_peaks(spectrum.peaks, outfile)
-    outfile.write(os.linesep)
-
-
-def _write_peaks(peaks: Spikes, outfile: IO):
-    outfile.write(f"NUM PEAKS: {len(peaks)}\n")
-    for mz, intensity in zip(peaks.mz, peaks.intensities):
-        outfile.write(f"{mz}\t{intensity}\n")
-
-
-def _write_metadata(metadata: dict, outfile: IO):
-    for key, value in metadata.items():
-        if not _is_num_peaks(key):
-            outfile.write(f"{key.upper()}: {value}\n")
-
-
-def _is_num_peaks(key: str) -> bool:
-    return key.lower().startswith("num peaks")
-
-
-def _ensure_list(spectra) -> List[Spectrum]:
-    if not isinstance(spectra, list):
-        # Assume that input was single Spectrum
-        spectra = [spectra]
-    return spectra
+import logging
+import os
+from typing import IO
+from typing import Dict
+from typing import List
+from typing import Union
+from ..Spectrum import Spectrum
+from ..Spikes import Spikes
+
+
+logger = logging.getLogger("matchms")
+
+
+_extentions_not_allowed = ["mzml", "mzxml", "json", "mgf"]
+
+
+def save_as_msp(spectra: List[Spectrum], filename: str):
+    """Save spectrum(s) as msp file.
+
+    :py:attr:`~matchms.Spectrum.losses` of spectrum will not be saved.
+
+    Example:
+
+    .. code-block:: python
+
+        import numpy
+        from matchms import Spectrum
+        from matchms.exporting import save_as_msp
+
+        # Create dummy spectrum
+        spectrum = Spectrum(mz=numpy.array([100, 200, 300], dtype="float"),
+                            intensities=numpy.array([10, 10, 500], dtype="float"),
+                            metadata={"charge": -1,
+                                      "inchi": '"InChI=1S/C6H12"',
+                                      "precursor_mz": 222.2})
+
+        # Write spectrum to test file
+        save_as_msp(spectrum, "test.msp")
+
+    Parameters
+    ----------
+    spectra:
+        Expected input are match.Spectrum.Spectrum() objects.
+    filename:
+        Provide filename to save spectrum(s).
+    """
+    file_extension = filename.split(".")[-1]
+    assert file_extension.lower() not in _extentions_not_allowed, \
+        f"File extension '.{file_extension}' not allowed."
+    if not filename.endswith(".msp"):
+        logger.warning("Spectra will be stored as msp file with extension .%s",
+                       filename.split(".")[-1])
+    spectra = _ensure_list(spectra)
+
+    with open(filename, "w", encoding="utf-8") as outfile:
+        for spectrum in spectra:
+            _write_spectrum(spectrum, outfile)
+
+
+def _write_spectrum(spectrum: Spectrum, outfile: IO):
+    _write_metadata(spectrum.metadata, outfile)
+    _write_peaks(spectrum.peaks, spectrum.peak_comments, outfile)
+    outfile.write(os.linesep)
+
+
+def _write_peaks(peaks: Spikes, peak_comments: Spectrum.peak_comments, outfile: IO):
+    outfile.write(f"NUM PEAKS: {len(peaks)}\n")
+    for mz, intensity in zip(peaks.mz, peaks.intensities):
+        peak_comment = _format_peak_comment(mz, peak_comments)
+        outfile.write(f"{mz}\t{intensity}{peak_comment}\n".expandtabs(12))
+
+
+def _write_metadata(metadata: dict, outfile: IO):
+    for key, value in metadata.items():
+        if not _is_num_peaks(key):
+            outfile.write(f"{key.upper()}: {value}\n")
+
+
+def _format_peak_comment(mz: Union[int, float], peak_comments: Dict):
+    """Format peak comment for given mz to return the quoted comment or empty string if no peak comment is present."""
+    peak_comment = peak_comments.get(mz, None)
+    if peak_comment is None:
+        return ""
+    return f"\t\"{peak_comment}\""
+
+
+def _is_num_peaks(key: str) -> bool:
+    return key.lower().startswith("num peaks")
+
+
+def _ensure_list(spectra) -> List[Spectrum]:
+    if not isinstance(spectra, list):
+        # Assume that input was single Spectrum
+        spectra = [spectra]
+    return spectra
diff --git a/matchms/importing/load_from_msp.py b/matchms/importing/load_from_msp.py
index 07d7ff02..7229346e 100644
--- a/matchms/importing/load_from_msp.py
+++ b/matchms/importing/load_from_msp.py
@@ -1,3 +1,4 @@
+import re
 from typing import Generator
 from typing import Iterator
 from typing import Tuple
@@ -12,6 +13,7 @@ def parse_msp_file(filename: str) -> Generator[dict, None, None]:
     params = {}
     masses = []
     intensities = []
+    peak_comments = {}
 
     # Peaks counter. Used to track and count the number of peaks
     peakscount = 0
@@ -31,6 +33,9 @@ def parse_msp_file(filename: str) -> Generator[dict, None, None]:
 
                 for peak in peak_pairs:
                     mz, intensity = get_peak_values(peak)
+                    comment = get_peak_comment(peak)
+                    if comment is not None:
+                        peak_comments.update({mz: comment})
 
                     peakscount += 1
                     masses.append(mz)
@@ -42,12 +47,14 @@ def parse_msp_file(filename: str) -> Generator[dict, None, None]:
                     yield {
                         'params': (params),
                         'm/z array': numpy.array(masses),
-                        'intensity array': numpy.array(intensities)
+                        'intensity array': numpy.array(intensities),
+                        'peak comments': peak_comments
                     }
 
                     params = {}
                     masses = []
                     intensities = []
+                    peak_comments = {}
 
 
 def get_peak_values(peak: str) -> Tuple[float, float]:
@@ -65,6 +72,15 @@ def get_peak_tuples(rline: str) -> Iterator[str]:
     return peak_pairs
 
 
+def get_peak_comment(rline: str) -> str:
+    """ Get the peak comment from the line containing the peak information. """
+    try:
+        comment = re.findall(r'[\"\'](.*)[\"\']', rline)[0]
+    except IndexError:
+        comment = None
+    return comment
+
+
 def parse_metadata(rline: str, params: dict):
     """ Reads metadata contained in line into params dict. """
     splitted_line = rline.split(":", 1)
@@ -116,6 +132,7 @@ def load_from_msp(filename: str) -> Generator[Spectrum, None, None]:
         metadata = spectrum.get("params", None)
         mz = spectrum["m/z array"]
         intensities = spectrum["intensity array"]
+        peak_comments = spectrum["peak comments"]
 
         # Sort by mz (if not sorted already)
         if not numpy.all(mz[:-1] <= mz[1:]):
@@ -123,4 +140,4 @@ def load_from_msp(filename: str) -> Generator[Spectrum, None, None]:
             mz = mz[idx_sorted]
             intensities = intensities[idx_sorted]
 
-        yield Spectrum(mz=mz, intensities=intensities, metadata=metadata)
+        yield Spectrum(mz=mz, intensities=intensities, metadata=metadata, peak_comments=peak_comments)


Test patch:
diff --git a/tests/test_load_from_msp.py b/tests/test_load_from_msp.py
index 15ccfb54..bf878764 100644
--- a/tests/test_load_from_msp.py
+++ b/tests/test_load_from_msp.py
@@ -83,10 +83,18 @@ def test_load_from_msp_tabs():
         39987
     ]])
 
+    expected_peak_comments = {113.03854: "Theoretical m/z 113.039125, Mass diff 0 (0 ppm), Formula C9H5",
+                              125.03855: "Theoretical m/z 125.039125, Mass diff 0 (0 ppm), Formula C10H5",
+                              249.07072: "Theoretical m/z 249.070425, Mass diff -0.001 (0 ppm), Formula C20H9",
+                              252.09323: "Theoretical m/z 252.093354, Mass diff 0 (0.49 ppm), SMILES "
+                                         "C1=CC=2C=CC=C3C4=CC=CC5=CC=CC(C(=C1)C23)=C54, Annotation [C20H12]+, "
+                                         "Rule of HR False"}
+
     for idx, spectrum in enumerate(spectra):
         assert_matching_inchikey(spectrum, expected_inchikey[idx])
         numpy.testing.assert_array_almost_equal(spectrum.peaks.mz, expected_mz[idx])
         numpy.testing.assert_array_almost_equal(spectrum.peaks.intensities, expected_intensities[idx])
+        assert spectrum.peak_comments == expected_peak_comments
 
 
 def test_load_from_msp_multiline():
diff --git a/tests/test_save_as_msp.py b/tests/test_save_as_msp.py
index 36a1b96b..86f30ebf 100644
--- a/tests/test_save_as_msp.py
+++ b/tests/test_save_as_msp.py
@@ -1,137 +1,147 @@
-import os
-import tempfile
-from typing import List
-import numpy
-import pytest
-from matchms import Spectrum
-from matchms.exporting import save_as_msp
-from matchms.importing import load_from_msp
-
-
-@pytest.fixture
-def none_spectrum():
-    return None
-
-
-@pytest.fixture
-def spectrum():
-    return Spectrum(mz=numpy.array([100, 200, 290, 490, 510], dtype="float"),
-                    intensities=numpy.array([0.1, 0.2, 1.0, 0.3, 0.4], dtype="float"))
-
-
-@pytest.fixture(params=["rcx_gc-ei_ms_20201028_perylene.msp", "MoNA-export-GC-MS-first10.msp", "Hydrogen_chloride.msp"])
-def data(request):
-    module_root = os.path.join(os.path.dirname(__file__), "..")
-    spectrums_file = os.path.join(module_root, "tests", request.param)
-    spectra = load_from_msp(spectrums_file)
-    return list(spectra)
-
-
-@pytest.fixture
-def filename():
-    with tempfile.TemporaryDirectory() as temp_dir:
-        filename = os.path.join(temp_dir, "test.msp")
-        yield filename
-
-
-def test_spectrum_none_exception(none_spectrum, filename):
-    """ Test for exception being thrown if the spectrum to be saved. """
-    with pytest.raises(AttributeError) as exception:
-        save_as_msp(none_spectrum, filename)
-
-    message = exception.value.args[0]
-    assert message == "'NoneType' object has no attribute 'metadata'"
-
-
-def test_not_allowed_filename_extension():
-    """ Test for exception if output file ends with not allowed extension."""
-    with tempfile.TemporaryDirectory() as temp_dir:
-        filename = os.path.join(temp_dir, "test.mzml")
-
-        with pytest.raises(AssertionError) as exception:
-            save_as_msp(None, filename)
-
-        message = exception.value.args[0]
-        assert message == "File extension '.mzml' not allowed."
-
-
-def test_non_msp_filename_extension(spectrum, caplog):
-    """ Test for log message if output file doesn't end with .msp. """
-    with tempfile.TemporaryDirectory() as temp_dir:
-        filename = os.path.join(temp_dir, "test.dat")
-
-        save_as_msp(spectrum, filename)
-
-        expected_log = "Spectra will be stored as msp file with extension .dat"
-        assert expected_log in caplog.text
-
-
-def test_file_exists_single_spectrum(spectrum, filename):
-    """ Test checking if the file is created. """
-    save_as_msp(spectrum, filename)
-
-    assert os.path.isfile(filename)
-
-
-def test_stores_all_spectra(filename, data):
-    """ Test checking if all spectra contained in the original file are stored
-    and loaded back in properly. """
-    spectra = save_and_reload_spectra(filename, data)
-
-    assert len(spectra) == len(data)
-
-
-def test_have_metadata(filename, data):
-    """ Test checking of all metadate is stored correctly. """
-    spectra = save_and_reload_spectra(filename, data)
-
-    assert len(spectra) == len(data)
-
-    for actual, expected in zip(spectra, data):
-        assert actual.metadata == expected.metadata
-
-
-def test_have_peaks(filename, data):
-    """ Test checking if all peaks are stored correctly. """
-    spectra = save_and_reload_spectra(filename, data)
-
-    assert len(spectra) == len(data)
-
-    for actual, expected in zip(spectra, data):
-        assert actual.peaks == expected.peaks
-
-
-def save_and_reload_spectra(filename, spectra: List[Spectrum]):
-    """ Utility function to save spectra to msp and load them again.
-
-    Params:
-    -------
-    spectra: Spectra objects to store
-
-    Returns:
-    --------
-    reloaded_spectra: Spectra loaded from saved msp file.
-    """
-
-    save_as_msp(spectra, filename)
-    reloaded_spectra = list(load_from_msp(filename))
-    return reloaded_spectra
-
-
-def test_num_peaks_last_metadata_field(filename, data):
-    """ Test to check whether the last line before the peaks is NUM PEAKS: ... """
-    save_as_msp(data, filename)
-
-    with open(filename, mode='r', encoding="utf-8") as file:
-        content = file.readlines()
-        for idx, line in enumerate(content):
-            if line.startswith('NUM PEAKS: '):
-                num_peaks = int(line.split()[2])
-                peaks = content[idx + 1: idx + num_peaks + 1]
-                for peak in peaks:
-                    mz, intensity = peak.split()
-                    mz = float(mz)
-                    intensity = float(intensity)
-
-                    assert isinstance(mz, float)
-                    assert isinstance(intensity, float)
+import os
+import tempfile
+from typing import List
+import numpy
+import pytest
+from matchms import Spectrum
+from matchms.exporting import save_as_msp
+from matchms.importing import load_from_msp
+
+
+@pytest.fixture
+def none_spectrum():
+    return None
+
+
+@pytest.fixture
+def spectrum():
+    return Spectrum(mz=numpy.array([100, 200, 290, 490, 510], dtype="float"),
+                    intensities=numpy.array([0.1, 0.2, 1.0, 0.3, 0.4], dtype="float"))
+
+
+@pytest.fixture(params=["rcx_gc-ei_ms_20201028_perylene.msp", "MoNA-export-GC-MS-first10.msp", "Hydrogen_chloride.msp"])
+def data(request):
+    module_root = os.path.join(os.path.dirname(__file__), "..")
+    spectrums_file = os.path.join(module_root, "tests", request.param)
+    spectra = load_from_msp(spectrums_file)
+    return list(spectra)
+
+
+@pytest.fixture
+def filename():
+    with tempfile.TemporaryDirectory() as temp_dir:
+        filename = os.path.join(temp_dir, "test.msp")
+        yield filename
+
+
+def test_spectrum_none_exception(none_spectrum, filename):
+    """ Test for exception being thrown if the spectrum to be saved. """
+    with pytest.raises(AttributeError) as exception:
+        save_as_msp(none_spectrum, filename)
+
+    message = exception.value.args[0]
+    assert message == "'NoneType' object has no attribute 'metadata'"
+
+
+def test_not_allowed_filename_extension():
+    """ Test for exception if output file ends with not allowed extension."""
+    with tempfile.TemporaryDirectory() as temp_dir:
+        filename = os.path.join(temp_dir, "test.mzml")
+
+        with pytest.raises(AssertionError) as exception:
+            save_as_msp(None, filename)
+
+        message = exception.value.args[0]
+        assert message == "File extension '.mzml' not allowed."
+
+
+def test_non_msp_filename_extension(spectrum, caplog):
+    """ Test for log message if output file doesn't end with .msp. """
+    with tempfile.TemporaryDirectory() as temp_dir:
+        filename = os.path.join(temp_dir, "test.dat")
+
+        save_as_msp(spectrum, filename)
+
+        expected_log = "Spectra will be stored as msp file with extension .dat"
+        assert expected_log in caplog.text
+
+
+def test_file_exists_single_spectrum(spectrum, filename):
+    """ Test checking if the file is created. """
+    save_as_msp(spectrum, filename)
+
+    assert os.path.isfile(filename)
+
+
+def test_stores_all_spectra(filename, data):
+    """ Test checking if all spectra contained in the original file are stored
+    and loaded back in properly. """
+    spectra = save_and_reload_spectra(filename, data)
+
+    assert len(spectra) == len(data)
+
+
+def test_have_metadata(filename, data):
+    """ Test checking of all metadate is stored correctly. """
+    spectra = save_and_reload_spectra(filename, data)
+
+    assert len(spectra) == len(data)
+
+    for actual, expected in zip(spectra, data):
+        assert actual.metadata == expected.metadata
+
+
+def test_have_peaks(filename, data):
+    """ Test checking if all peaks are stored correctly. """
+    spectra = save_and_reload_spectra(filename, data)
+
+    assert len(spectra) == len(data)
+
+    for actual, expected in zip(spectra, data):
+        assert actual.peaks == expected.peaks
+
+
+def test_have_peak_comments(filename, data):
+    """ Test checking if all peak comments are stored correctly. """
+    spectra = save_and_reload_spectra(filename, data)
+
+    assert len(spectra) == len(data)
+
+    for actual, expected in zip(spectra, data):
+        assert actual.peak_comments == expected.peak_comments
+
+
+def save_and_reload_spectra(filename, spectra: List[Spectrum]):
+    """ Utility function to save spectra to msp and load them again.
+
+    Params:
+    -------
+    spectra: Spectra objects to store
+
+    Returns:
+    --------
+    reloaded_spectra: Spectra loaded from saved msp file.
+    """
+
+    save_as_msp(spectra, filename)
+    reloaded_spectra = list(load_from_msp(filename))
+    return reloaded_spectra
+
+
+def test_num_peaks_last_metadata_field(filename, data):
+    """ Test to check whether the last line before the peaks is NUM PEAKS: ... """
+    save_as_msp(data, filename)
+
+    with open(filename, mode='r', encoding="utf-8") as file:
+        content = file.readlines()
+        for idx, line in enumerate(content):
+            if line.startswith('NUM PEAKS: '):
+                num_peaks = int(line.split()[2])
+                peaks = content[idx + 1: idx + num_peaks + 1]
+                for peak in peaks:
+                    mz, intensity = peak.split()[:2]
+                    mz = float(mz)
+                    intensity = float(intensity)
+
+                    assert isinstance(mz, float)
+                    assert isinstance(intensity, float)
diff --git a/tests/test_spectrum.py b/tests/test_spectrum.py
index 681d0512..48a66336 100644
--- a/tests/test_spectrum.py
+++ b/tests/test_spectrum.py
@@ -1,6 +1,6 @@
 import numpy
 from matplotlib import pyplot as plt
-from matchms import Spectrum
+from matchms import Spectrum, Spikes
 
 
 def _assert_plots_ok(fig, n_plots):
@@ -191,3 +191,17 @@ def test_spectrum_plot_with_histogram_unspecified():
     spectrum = _create_test_spectrum()
     fig = spectrum.plot()
     _assert_plots_ok(fig, n_plots=1)
+
+
+def test_reiterating_peak_comments():
+    mz = numpy.array([100.0003, 100.0004, 100.0005, 110., 200., 300., 400.0176], dtype='float')
+    intensities = numpy.array([1, 2, 3, 4, 5, 6, 7], dtype='float')
+    peak_comments = ["m/z 100.0003", None, "m/z 100.0005", "m/z 110.", "m/z 200.", "m/z 300.", "m/z 400.0176"]
+    peak_comments = {mz[i]: peak_comments[i] for i in range(len(mz))}
+    spectrum = Spectrum(mz=mz, intensities=intensities, peak_comments=peak_comments)
+
+    spectrum.peaks = Spikes(mz=numpy.array([100.0004, 110., 400.018], dtype='float'),
+                            intensities=numpy.array([5, 4, 7], dtype='float'))
+
+    assert spectrum.peak_comments == {100.0004: "m/z 100.0003; m/z 100.0005", 110.: "m/z 110.", 400.018: "m/z 400.0176"}
+