Repository: matchms/matchms
Pull request #90 (2020-07-02 12:29:12)

Creating spectrum from metabolomics USI
Added a new method to allow a `Spectrum` object to be created from the json returned in a post request from the  metabolomics USI (https://metabolomics-usi.ucsd.edu).
The USI provides a single access point for a large number of metabolomics MS2 data resources including both experimental and library spectra.

fdiblen: - [ ] Get a clear description from Florian and add it to this issue
- [ ] Decide if we need to 
- [ ] implement min_peaks filter
- [ ] add tests
- [ ] add docstrings
jspaaks: Hi @fdiblen I think Florian discussed this in the video. I can take a stab at this I think with the current information. Basically you want to have a minimum number of peaks for any given spectrum, but then there can be an additional requirement that raises this minimum depending on the parent mass, since you expect heavy parents to yield a greater number of peaks.

This filter will also be the first one that can yield a return argument ``None``, so we should adapt the existing filters as well, such that the first thing they check is if the input spectrum is ``None``, and if so, ``return None`` themselves immediately.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79391e9d..4c7e9ab4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Added similarity score based on comparing parent masses [#79](https://github.com/matchms/matchms/pull/79)
+- Added a method for instantiating a spectrum from the metabolomics USI 
 
 ## [0.4.0] - 2020-06-11
 
diff --git a/conda/environment-dev.yml b/conda/environment-dev.yml
index eb4bdb45..686ad171 100644
--- a/conda/environment-dev.yml
+++ b/conda/environment-dev.yml
@@ -10,6 +10,7 @@ dependencies:
   - numpy
   - pip
   - pyteomics >=4.2
+  - requests
   - python >=3.7,<3.8
   - pyyaml
   - rdkit >=2020.03.1
diff --git a/conda/environment.yml b/conda/environment.yml
index 5af9b494..69310935 100644
--- a/conda/environment.yml
+++ b/conda/environment.yml
@@ -9,6 +9,7 @@ dependencies:
   - numba >=0.47
   - numpy
   - pyteomics >=4.2
+  - requests
   - python >=3.7,<3.8
   - pyyaml
   - rdkit >=2020.03.1
diff --git a/conda/meta.yaml b/conda/meta.yaml
index 1226c1ec..bfad6561 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -40,6 +40,7 @@ requirements:
     - numpy
     - pip
     - pyteomics >=4.2
+    - requests
     - python >=3.7,<3.8
     - pyyaml
     - rdkit >=2020.03.1
diff --git a/matchms/importing/load_from_usi.py b/matchms/importing/load_from_usi.py
new file mode 100644
index 00000000..f044e345
--- /dev/null
+++ b/matchms/importing/load_from_usi.py
@@ -0,0 +1,48 @@
+import json
+import numpy as np
+import requests
+from matchms import Spectrum
+
+
+def load_from_usi(usi: str, server='https://metabolomics-usi.ucsd.edu'):
+    """Load spectrum from metabolomics USI.
+
+USI returns JSON data with keys 'peaks', 'n_peaks' and 'precuror_mz'
+
+    Args:
+    ----
+    usi: str
+        Provide the usi.
+
+    server: string
+        USI server
+    """
+
+    # Create the url
+    url = server + '/json/?usi=' + usi
+    metadata = {'usi': usi, 'server': server}
+    response = requests.get(url)
+
+    if response.statuscode == 404:
+        return None
+    # Extract data and create Spectrum object
+    try:
+        spectral_data = response.json()
+        if spectral_data is None or 'peaks' not in spectral_data:
+            return None
+        peaks = spectral_data['peaks']
+        if len(peaks) == 0:
+            return None
+        mz_list, intensity_list = zip(*peaks)
+        mz_array = np.array(mz_list)
+        intensity_array = np.array(intensity_list)
+
+        metadata['precursor_mz'] = spectral_data.get('precursor_mz', None)
+
+        s = Spectrum(mz_array, intensity_array, metadata)
+
+        return s
+
+    except json.decoder.JSONDecodeError:
+        # failed to unpack json
+        return None


Test patch:
diff --git a/tests/test_collect_peak_pairs.py b/tests/test_collect_peak_pairs.py
index cccb8a8f..d259cbe0 100644
--- a/tests/test_collect_peak_pairs.py
+++ b/tests/test_collect_peak_pairs.py
@@ -1,37 +1,28 @@
-"""Test function to collect matching peaks. Run tests both on numba compiled and
-pure Python version."""
 import numpy
-import pytest
 from matchms.similarity.collect_peak_pairs import collect_peak_pairs
 
 
-@pytest.mark.parametrize("shift, expected_pairs",
-                         [(0.0, [(2, 2, 1.0), (3, 3, 1.0)]),
-                          (-5.0, [(0, 0, 0.01), (1, 1, 0.01)])])
-def test_cosine_hungarian_compiled(shift, expected_pairs):
-    """Test finding expected peak matches for given tolerance."""
+def test_cosine_hungarian_tolerance_01():
+    """Test finding expected peak matches within tolerance=0.2."""
     spec1 = numpy.array([[100, 200, 300, 500],
                          [0.1, 0.1, 1.0, 1.0]], dtype="float").T
 
     spec2 = numpy.array([[105, 205.1, 300, 500.1],
                          [0.1, 0.1, 1.0, 1.0]], dtype="float").T
 
-    matching_pairs = collect_peak_pairs(spec1, spec2, tolerance=0.2, shift=shift)
+    matching_pairs = collect_peak_pairs(spec1, spec2, tolerance=0.2)
     assert len(matching_pairs) == 2, "Expected different number of matching peaks"
-    assert matching_pairs == [pytest.approx(x, 1e-9) for x in expected_pairs], "Expected different pairs."
+    assert matching_pairs == [(2, 2, 1.0), (3, 3, 1.0)], "Expected different matchin pairs."
 
 
-@pytest.mark.parametrize("shift, expected_pairs",
-                         [(0.0, [(2, 2, 1.0), (3, 3, 1.0)]),
-                          (-5.0, [(0, 0, 0.01), (1, 1, 0.01)])])
-def test_cosine_hungarian(shift, expected_pairs):
-    """Test finding expected peak matches for tolerance=0.2 and given shift."""
+def test_cosine_hungarian_tolerance_01_shift_min5():
+    """Test finding expected peak matches when given a mass_shift of -5.0."""
     spec1 = numpy.array([[100, 200, 300, 500],
-                         [0.1, 0.1, 1.0, 1.0]], dtype="float").T
+                         [1.0, 1.0, 0.1, 0.1]], dtype="float").T
 
     spec2 = numpy.array([[105, 205.1, 300, 500.1],
-                         [0.1, 0.1, 1.0, 1.0]], dtype="float").T
+                         [1.0, 1.0, 0.1, 0.1]], dtype="float").T
 
-    matching_pairs = collect_peak_pairs.py_func(spec1, spec2, tolerance=0.2, shift=shift)
+    matching_pairs = collect_peak_pairs(spec1, spec2, tolerance=0.2, shift=-5.0)
     assert len(matching_pairs) == 2, "Expected different number of matching peaks"
-    assert matching_pairs == [pytest.approx(x, 1e-9) for x in expected_pairs], "Expected different pairs."
+    assert matching_pairs == [(0, 0, 1.0), (1, 1, 1.0)], "Expected different matchin pairs."
diff --git a/tests/test_usi.py b/tests/test_usi.py
new file mode 100644
index 00000000..c99c49ca
--- /dev/null
+++ b/tests/test_usi.py
@@ -0,0 +1,34 @@
+from unittest.mock import Mock
+from unittest.mock import patch
+import numpy as np
+from matchms import Spectrum
+from matchms.importing.load_from_usi import load_from_usi
+
+
+@patch('matchms.importing.load_from_usi.requests.get')
+def normal_test(mock_get):
+    mock_get.return_value = Mock(ok=True)
+    mock_get.return_value.json.return_value = {'peaks': [[1., 2.], [3., 4.]]}
+    spec = load_from_usi('something')
+    expected_metadata = {'usi': 'something', 'url': 'https://metabolomics-usi.ucsd.edu/json/?usi=something', 'precursor_mz': None}
+    expected = Spectrum(np.array([1., 3.]), np.array([2., 4.]), expected_metadata)
+    assert spec == expected
+
+
+@patch('matchms.importing.load_from_usi.requests.get')
+def test_404(mock_get):
+    mock_get.return_value = Mock(ok=True)
+    mock_get.return_value.status_code = 404
+    mock_get.return_value.json.return_value = None
+    spec = load_from_usi('something')
+    expected = None
+    assert spec == expected
+
+
+@patch('matchms.importing.load_from_usi.requests.get')
+def test_no_peaks(mock_get):
+    mock_get.return_value = Mock(ok=True)
+    mock_get.return_value.json.return_value = {'peaks': []}
+    spec = load_from_usi('something')
+    expected = None
+    assert spec == expected