Repository: matchms/matchms
Pull request #198 (2021-03-12 08:27:43)

Add initial networking functions
Addressing #196 

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=matchms_matchms&pullRequest=198&metric=coverage&view=list) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo.png' alt='No Duplication information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=matchms_matchms&pullRequest=198&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=matchms_matchms&pullRequest=198&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=matchms_matchms&pullRequest=198&metric=coverage&view=list) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo.png' alt='No Duplication information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=matchms_matchms&pullRequest=198&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c641b06..8086a52e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+- new `matchms.networking` module which allows to build and export graphs from `scores` objects [#198](https://github.com/matchms/matchms/pull/198)
 - Expand list of known negative ionmode adducts and conversion rules [#213](https://github.com/matchms/matchms/pull/213)
 - `.to_numpy` method for Spikes class which allows to run `spectrum.peaks.to_numpy` [#214](https://github.com/matchms/matchms/issues/214)
 - `save_as_msp()` function to export spectrums to .msp file [#215](https://github.com/matchms/matchms/pull/215)
diff --git a/README.rst b/README.rst
index 4739ad22..6108d696 100644
--- a/README.rst
+++ b/README.rst
@@ -202,7 +202,7 @@ Glossary of terms
        in retrieving information associated with a certain molecule from a
        database.
    * - InChIKey / InChI key / :code:`INCHIKEY`
-     - An indentifier for molecules. For example, the InChI key for carbon
+     - An identifier for molecules. For example, the InChI key for carbon
        dioxide is :code:`InChIKey=CURLTUGMZLYLDI-UHFFFAOYSA-N` (yes, it
        includes the substring :code:`InChIKey=`).
    * - MGF File / Mascot Generic Format
diff --git a/conda/environment-dev.yml b/conda/environment-dev.yml
index 69db070c..9da1e629 100644
--- a/conda/environment-dev.yml
+++ b/conda/environment-dev.yml
@@ -5,6 +5,7 @@ channels:
   - defaults
 dependencies:
   - matplotlib
+  - networkx
   - numba >=0.47
   - numpy
   - pip
diff --git a/conda/environment.yml b/conda/environment.yml
index 133e957e..ab9bbc34 100644
--- a/conda/environment.yml
+++ b/conda/environment.yml
@@ -6,6 +6,7 @@ channels:
 dependencies:
   - deprecated
   - matplotlib
+  - networkx
   - numba >=0.47
   - numpy
   - pyteomics >=4.2
diff --git a/matchms/networking/SimilarityNetwork.py b/matchms/networking/SimilarityNetwork.py
new file mode 100644
index 00000000..22eef01d
--- /dev/null
+++ b/matchms/networking/SimilarityNetwork.py
@@ -0,0 +1,166 @@
+from typing import Optional
+import networkx as nx
+import numpy
+from matchms import Scores
+from .networking_functions import get_top_hits
+
+
+class SimilarityNetwork:
+    """Create a spectal network from spectrum similarities.
+
+    For example
+
+    .. testcode::
+
+        import numpy as np
+        from matchms import Spectrum, calculate_scores
+        from matchms.similarity import ModifiedCosine
+        from matchms.networking import SimilarityNetwork
+
+        spectrum_1 = Spectrum(mz=np.array([100, 150, 200.]),
+                              intensities=np.array([0.7, 0.2, 0.1]),
+                              metadata={"precursor_mz": 100.0,
+                                        "testID": "one"})
+        spectrum_2 = Spectrum(mz=np.array([104.9, 140, 190.]),
+                              intensities=np.array([0.4, 0.2, 0.1]),
+                              metadata={"precursor_mz": 105.0,
+                                        "testID": "two"})
+
+        # Use factory to construct a similarity function
+        modified_cosine = ModifiedCosine(tolerance=0.2)
+        spectrums = [spectrum_1, spectrum_2]
+        scores = calculate_scores(spectrums, spectrums, modified_cosine)
+        ms_network = SimilarityNetwork(identifier_key="testID")
+        ms_network.create_network(scores)
+
+        nodes = list(ms_network.graph.nodes())
+        nodes.sort()
+        print(nodes)
+
+    Should output
+
+    .. testoutput::
+
+        ['one', 'two']
+
+    """
+    def __init__(self, identifier_key: str = "spectrumid",
+                 top_n: int = 20,
+                 max_links: int = 10,
+                 score_cutoff: float = 0.7,
+                 link_method: str = 'single',
+                 keep_unconnected_nodes: bool = True):
+        """
+        Parameters
+        ----------
+        identifier_key
+            Metadata key for unique intentifier for each spectrum in scores.
+            Will also be used for the naming the network nodes. Default is 'spectrumid'.
+        top_n
+            Consider edge between spectrumA and spectrumB if score falls into
+            top_n for spectrumA or spectrumB (link_method="single"), or into
+            top_n for spectrumA and spectrumB (link_method="mutual"). From those
+            potential links, only max_links will be kept, so top_n must be >= max_links.
+        max_links
+            Maximum number of links to add per node. Default = 10.
+            Due to incoming links, total number of links per node can be higher.
+            The links are populated by looping over the query spectrums.
+            Important side note: The max_links restriction is strict which means that
+            if scores around max_links are equal still only max_links will be added
+            which can results in some random variations (sorting spectra with equal
+            scores restuls in a random order of such elements).
+        score_cutoff
+            Threshold for given similarities. Edges/Links will only be made for
+            similarities > score_cutoff. Default = 0.7.
+        link_method
+            Chose between 'single' and 'mutual'. 'single will add all links based
+            on individual nodes. 'mutual' will only add links if that link appears
+            in the given top-n list for both nodes.
+        keep_unconnected_nodes
+            If set to True (default) all spectra will be included as nodes even
+            if they have no connections/edges of other spectra. If set to False
+            all nodes without connections will be removed.
+        """
+        # pylint: disable=too-many-arguments
+        self.identifier_key = identifier_key
+        self.top_n = top_n
+        self.max_links = max_links
+        self.score_cutoff = score_cutoff
+        self.link_method = link_method
+        self.keep_unconnected_nodes = keep_unconnected_nodes
+        self.graph: Optional[nx.Graph] = None
+        """NetworkX graph. Set after calling create_network()"""
+
+    @staticmethod
+    def _select_edge_score(similars_scores: dict, scores_type: numpy.dtype):
+        """Chose one value if score contains multiple values (e.g. "score" and "matches")"""
+        if len(scores_type) > 1 and "score" in scores_type.names:
+            return {key: value["score"] for key, value in similars_scores.items()}
+        if len(scores_type) > 1:  # Assume that first entry is desired score
+            return {key: value[0] for key, value in similars_scores.items()}
+        return similars_scores
+
+    def create_network(self, scores: Scores):
+        """
+        Function to create network from given top-n similarity values. Expects that
+        similarities given in scores are from an all-vs-all comparison including all
+        possible pairs.
+
+        Parameters
+        ----------
+        scores
+            Matchms Scores object containing all spectrums and pair similarities for
+            generating a network.
+        """
+        assert self.top_n >= self.max_links, "top_n must be >= max_links"
+        assert numpy.all(scores.queries == scores.references), \
+            "Expected symmetric scores object with queries==references"
+        unique_ids = list({s.get(self.identifier_key) for s in scores.queries})
+
+        # Initialize network graph, add nodes
+        msnet = nx.Graph()
+        msnet.add_nodes_from(unique_ids)
+
+        # Collect location and score of highest scoring candidates for queries and references
+        similars_idx, similars_scores = get_top_hits(scores, identifier_key=self.identifier_key,
+                                                     top_n=self.top_n,
+                                                     search_by="queries",
+                                                     ignore_diagonal=True)
+        similars_scores = self._select_edge_score(similars_scores, scores.scores.dtype)
+
+        # Add edges based on global threshold (cutoff) for weights
+        for i, spec in enumerate(scores.queries):
+            query_id = spec.get(self.identifier_key)
+
+            ref_candidates = numpy.array([scores.references[x].get(self.identifier_key)
+                                          for x in similars_idx[query_id]])
+            idx = numpy.where((similars_scores[query_id] >= self.score_cutoff) &
+                              (ref_candidates != query_id))[0][:self.max_links]
+            if self.link_method == "single":
+                new_edges = [(query_id, str(ref_candidates[x]),
+                              float(similars_scores[query_id][x])) for x in idx]
+            elif self.link_method == "mutual":
+                new_edges = [(query_id, str(ref_candidates[x]),
+                              float(similars_scores[query_id][x]))
+                             for x in idx if i in similars_idx[ref_candidates[x]][:]]
+            else:
+                raise ValueError("Link method not kown")
+
+            msnet.add_weighted_edges_from(new_edges)
+
+        if not self.keep_unconnected_nodes:
+            msnet.remove_nodes_from(list(nx.isolates(msnet)))
+        self.graph = msnet
+
+    def export_to_graphml(self, filename: str):
+        """Save the network as .graphml file.
+
+        Parameters
+        ----------
+        filename
+            Specify filename for exporting the graph.
+
+        """
+        if not self.graph:
+            raise ValueError("No network found. Make sure to first run .create_network() step")
+        nx.write_graphml_lxml(self.graph, filename)
diff --git a/matchms/networking/__init__.py b/matchms/networking/__init__.py
new file mode 100644
index 00000000..cab0803e
--- /dev/null
+++ b/matchms/networking/__init__.py
@@ -0,0 +1,11 @@
+"""
+Functions for creating and analysing spectral networks
+######################################################
+
+"""
+from .SimilarityNetwork import SimilarityNetwork
+
+
+__all__ = [
+    "SimilarityNetwork",
+]
diff --git a/matchms/networking/networking_functions.py b/matchms/networking/networking_functions.py
new file mode 100644
index 00000000..c45e254a
--- /dev/null
+++ b/matchms/networking/networking_functions.py
@@ -0,0 +1,56 @@
+""" Helper functions to build and handle spectral networks
+"""
+from typing import Tuple
+from matchms import Scores
+
+
+def get_top_hits(scores: Scores, identifier_key: str = "spectrumid",
+                 top_n: int = 25, search_by: str = "queries",
+                 ignore_diagonal: bool = False) -> Tuple[dict, dict]:
+    """Get top_n highest scores (and indices) for every entry.
+
+    Parameters
+    ----------
+    scores
+        Matchms Scores object containing all similarities.
+    identifier_key
+        Metadata key for unique intentifier for each spectrum in scores.
+        Will also be used for the naming the network nodes. Default is 'spectrumid'.
+    top_n
+        Return the indexes and scores for the top_n highest scores. Scores between
+        a spectrum with itself (diagonal of scores.scores) will not be taken into
+        account.
+    search_by
+        Chose between 'queries' or 'references' which decides if the top_n matches
+        for every spectrum in scores.queries or in scores.references will be
+        collected and returned
+    ignore_diagonal
+        Set to True if scores.scores is symmetric (i.e. if references and queries
+        were the same) and if scores between spectra with themselves should be
+        excluded.
+    """
+    assert search_by in ["queries", "references"], \
+        "search_by must be 'queries' or 'references"
+
+    similars_idx = dict()
+    similars_scores = dict()
+
+    if search_by == "queries":
+        for i, spec in enumerate(scores.queries):
+            spec_id = spec.get(identifier_key)
+            idx = scores.similarity_function.sort(scores.scores[:, i])
+            if ignore_diagonal:
+                similars_idx[spec_id] = idx[idx != i][:top_n]
+            else:
+                similars_idx[spec_id] = idx[:top_n]
+            similars_scores[spec_id] = scores.scores[similars_idx[spec_id], i]
+    elif search_by == "references":
+        for i, spec in enumerate(scores.references):
+            spec_id = spec.get(identifier_key)
+            idx = scores.similarity_function.sort(scores.scores[i, :])
+            if ignore_diagonal:
+                similars_idx[spec_id] = idx[idx != i][:top_n]
+            else:
+                similars_idx[spec_id] = idx[:top_n]
+            similars_scores[spec_id] = scores.scores[i, similars_idx[spec_id]]
+    return similars_idx, similars_scores
diff --git a/setup.py b/setup.py
index 9d2911f7..96338a98 100644
--- a/setup.py
+++ b/setup.py
@@ -49,6 +49,7 @@ setup(
         "deprecated",
         "lxml",
         "matplotlib",
+        "networkx",
         "numba >=0.47",
         "numpy",
         "pyteomics >=4.2",


Test patch:
diff --git a/tests/test_SimilarityNetwork.py b/tests/test_SimilarityNetwork.py
new file mode 100644
index 00000000..32430795
--- /dev/null
+++ b/tests/test_SimilarityNetwork.py
@@ -0,0 +1,194 @@
+import os
+import tempfile
+import numpy as np
+import pytest
+from matchms import Spectrum
+from matchms import calculate_scores
+from matchms.networking import SimilarityNetwork
+from matchms.similarity import FingerprintSimilarity
+from matchms.similarity import ModifiedCosine
+
+
+@pytest.yield_fixture
+def filename():
+    with tempfile.TemporaryDirectory() as temp_dir:
+        filename = os.path.join(temp_dir, "test.graphml")
+        yield filename
+
+
+def create_dummy_spectrums():
+    """Create dummy spectrums"""
+    fingerprints1 = [[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 1, 0], [1, 0, 1]]
+    fingerprints2 = [[1, 0, 1], [0, 1, 1], [1, 1, 1]]
+    spectrums = []
+    for i, fp in enumerate(fingerprints1):
+        spectrums.append(Spectrum(mz=np.array([100, 200.]),
+                                  intensities=np.array([0.7, 0.1 * i]),
+                                  metadata={"spectrumid": 'ref_spec_'+str(i),
+                                            "fingerprint": np.array(fp),
+                                            "smiles": 'C1=CC=C2C(=C1)NC(=N2)C3=CC=CO3',
+                                            "precursor_mz": 100+50*i}))
+    for i, fp in enumerate(fingerprints2):
+        spectrums.append(Spectrum(mz=np.array([100, 200.]),
+                                  intensities=np.array([0.5, 0.1 * i]),
+                                  metadata={"spectrumid": 'query_spec_'+str(i),
+                                            "fingerprint": np.array(fp),
+                                            "smiles": 'CC1=C(C=C(C=C1)NC(=O)N(C)C)Cl',
+                                            "precursor_mz": 110+50*i}))
+    return spectrums
+
+
+def create_dummy_scores():
+    """Creat asymmetric scores object (references != queries)"""
+    spectrums = create_dummy_spectrums()
+    references = spectrums[:5]
+    queries = spectrums[5:]
+
+    # Create Scores object by calculating dice scores
+    similarity_measure = FingerprintSimilarity("dice")
+    scores = calculate_scores(references, queries, similarity_measure)
+    return scores
+
+
+def create_dummy_scores_symmetric():
+    spectrums = create_dummy_spectrums()
+
+    # Create Scores object by calculating dice scores
+    similarity_measure = FingerprintSimilarity("dice")
+    scores = calculate_scores(spectrums, spectrums, similarity_measure)
+    return scores
+
+
+def create_dummy_scores_symmetric_modified_cosine():
+    spectrums = create_dummy_spectrums()
+
+    # Create Scores object by calculating dice scores
+    similarity_measure = ModifiedCosine()
+    scores = calculate_scores(spectrums, spectrums, similarity_measure)
+    return scores
+
+
+def test_create_network_symmetric_wrong_input():
+    """Test if function is used with non-symmetric scores object"""
+    scores = create_dummy_scores()
+    msnet = SimilarityNetwork()
+    with pytest.raises(AssertionError) as msg:
+        msnet.create_network(scores)
+
+    expected_msg = "Expected symmetric scores object with queries==references"
+    assert expected_msg in str(msg), "Expected different exception"
+
+
+def test_create_network_symmetric():
+    """Test creating a graph from a symmetric Scores object"""
+    cutoff = 0.7
+    scores = create_dummy_scores_symmetric()
+    msnet = SimilarityNetwork(score_cutoff=cutoff)
+    msnet.create_network(scores)
+
+    nodes_list = list(msnet.graph.nodes())
+    edges_list = list(msnet.graph.edges())
+    edges_list.sort()
+    nodes_without_edges = ['ref_spec_0',
+                           'ref_spec_1',
+                           'ref_spec_2']
+    assert len(nodes_list) == 8, "Expected different number of nodes"
+    assert len(edges_list) == 5, "Expected different number of edges"
+    assert np.all([(x[0] not in nodes_without_edges) for x in edges_list]), \
+        "Expected this node to have no edges"
+    assert np.all([(x[1] not in nodes_without_edges) for x in edges_list]), \
+        "Expected this node to have no edges"
+
+
+def test_create_network_symmetric_remove_unconnected_nodes():
+    """Test if unconnected nodes are removed"""
+    cutoff = 0.7
+    scores = create_dummy_scores_symmetric()
+    msnet = SimilarityNetwork(score_cutoff=cutoff, keep_unconnected_nodes=False)
+    msnet.create_network(scores)
+
+    nodes_list = list(msnet.graph.nodes())
+    edges_list = list(msnet.graph.edges())
+    edges_list.sort()
+    nodes_with_edges = ['query_spec_0', 'ref_spec_4',
+                        'query_spec_2', 'ref_spec_3', 'query_spec_1']
+    assert len(nodes_list) == 5, "Expected different number of nodes"
+    assert np.all([(x in nodes_with_edges) for x in nodes_list]), \
+        "Expected this node to have edges"
+
+
+def test_create_network_symmetric_modified_cosine():
+    """Test creating a graph from a symmetric Scores object using ModifiedCosine"""
+    cutoff = 0.7
+    scores = create_dummy_scores_symmetric_modified_cosine()
+    msnet = SimilarityNetwork(score_cutoff=cutoff)
+    msnet.create_network(scores)
+
+    edges_list = list(msnet.graph.edges())
+    edges_list.sort()
+    assert len(edges_list) == 28, "Expected different number of edges"
+
+
+def test_create_network_export_to_graphml(filename):
+    """Test creating a graph from a symmetric Scores object using ModifiedCosine"""
+    cutoff = 0.7
+    scores = create_dummy_scores_symmetric_modified_cosine()
+    msnet = SimilarityNetwork(score_cutoff=cutoff)
+    msnet.create_network(scores)
+    msnet.export_to_graphml(filename)
+
+    assert os.path.isfile(filename), "graphml file not found"
+
+
+def test_create_network_symmetric_higher_cutoff():
+    cutoff = 0.9
+    scores = create_dummy_scores_symmetric()
+    msnet = SimilarityNetwork(score_cutoff=cutoff)
+    msnet.create_network(scores)
+
+    edges_list = list(msnet.graph.edges())
+    edges_list.sort()
+    assert len(edges_list) == 1, "Expected only one link"
+    assert edges_list[0][0] in ['query_spec_0', 'ref_spec_4'], \
+        "Expected different node to have a link"
+    assert edges_list[0][1] in ['query_spec_0', 'ref_spec_4'], \
+        "Expected different node to have a link"
+
+
+def test_create_network_symmetric_mutual_method():
+    """Test creating a graph from a Scores object"""
+    cutoff = 0.7
+    scores = create_dummy_scores_symmetric()
+    # change some scores
+    scores.scores[7, 6] = scores.scores[6, 7] = 0.85
+    scores.scores[7, 5] = scores.scores[5, 7] = 0.75
+    scores.scores[7, 3] = scores.scores[3, 7] = 0.7
+
+    msnet = SimilarityNetwork(score_cutoff=cutoff, top_n=3,
+                              max_links=3, link_method="mutual")
+    msnet.create_network(scores)
+    nodes_with_edges = ['query_spec_0', 'query_spec_1', 'query_spec_2', 'ref_spec_4']
+    edges_list = list(msnet.graph.edges())
+    edges_list.sort()
+    assert len(edges_list) == 4, "Expected four links"
+    assert np.all([(x[0] in nodes_with_edges) for x in edges_list]), "Expected different edges in graph"
+    assert np.all([(x[1] in nodes_with_edges) for x in edges_list]), "Expected different edges in graph"
+
+
+def test_create_network_symmetric_max_links_1():
+    """Test creating a graph from a Scores object using max_links=1"""
+    cutoff = 0.7
+    scores = create_dummy_scores_symmetric()
+    msnet = SimilarityNetwork(score_cutoff=cutoff, max_links=1, link_method="single")
+    msnet.create_network(scores)
+
+    edges_list = list(msnet.graph.edges())
+    edges_list.sort()
+    nodes_without_edges = ['ref_spec_0',
+                           'ref_spec_1',
+                           'ref_spec_2']
+    assert len(edges_list) == 3, "Expected different number of edges"
+    assert np.all([(x[0] not in nodes_without_edges) for x in edges_list]), \
+        "Expected this node to have no edges"
+    assert np.all([(x[1] not in nodes_without_edges) for x in edges_list]), \
+        "Expected this node to have no edges"
diff --git a/tests/test_networking_functions.py b/tests/test_networking_functions.py
new file mode 100644
index 00000000..e2d7638a
--- /dev/null
+++ b/tests/test_networking_functions.py
@@ -0,0 +1,74 @@
+import numpy as np
+from matchms import calculate_scores
+from matchms.networking.networking_functions import get_top_hits
+from matchms.similarity import FingerprintSimilarity
+from .test_SimilarityNetwork import create_dummy_spectrums
+
+
+def create_dummy_scores():
+    spectrums = create_dummy_spectrums()
+    references = spectrums[:5]
+    queries = spectrums[5:]
+
+    # Create Scores object by calculating dice scores
+    similarity_measure = FingerprintSimilarity("dice")
+    scores = calculate_scores(references, queries, similarity_measure)
+    return scores
+
+
+def test_get_top_hits_by_references():
+    scores = create_dummy_scores()
+    idx_ref, scores_ref = get_top_hits(scores, top_n=10, search_by="references")
+
+    expected_scores_ref = {'ref_spec_0': np.array([0.66666667, 0.5, 0.]),
+                           'ref_spec_1': np.array([0.66666667, 0.5, 0.]),
+                           'ref_spec_2': np.array([0.66666667, 0.66666667, 0.5]),
+                           'ref_spec_3': np.array([0.8, 0.5, 0.5]),
+                           'ref_spec_4': np.array([1., 0.8, 0.5])}
+    expected_idx_ref = {'ref_spec_0': np.array([0, 2, 1], dtype=np.int64),
+                        'ref_spec_1': np.array([1, 2, 0], dtype=np.int64),
+                        'ref_spec_2': np.array([1, 0, 2], dtype=np.int64),
+                        'ref_spec_3': np.array([2, 1, 0], dtype=np.int64),
+                        'ref_spec_4': np.array([0, 2, 1], dtype=np.int64)}
+    for key in scores_ref:
+        assert np.allclose(scores_ref[key], expected_scores_ref[key], atol=1e-5), \
+            "Expected different selected scores"
+    for key in idx_ref:
+        assert np.allclose(idx_ref[key], expected_idx_ref[key], atol=1e-5), \
+            "Expected different selected indices"
+
+    # Test lower top_n
+    idx_ref, scores_ref = get_top_hits(scores, top_n=2, search_by="references")
+    for key in scores_ref:
+        assert np.allclose(scores_ref[key], expected_scores_ref[key][:2], atol=1e-5), \
+            "Expected different selected scores"
+    for key in idx_ref:
+        assert np.allclose(idx_ref[key], expected_idx_ref[key][:2], atol=1e-5), \
+            "Expected different selected indices"
+
+
+def test_get_top_hits_by_queries():
+    scores = create_dummy_scores()
+    idx_query, scores_query = get_top_hits(scores, top_n=10, search_by="queries")
+
+    expected_scores_query = {'query_spec_0': np.array([1., 0.66666667, 0.66666667, 0.5, 0.]),
+                             'query_spec_1': np.array([0.66666667, 0.66666667, 0.5, 0.5, 0.]),
+                             'query_spec_2': np.array([0.8, 0.8, 0.5, 0.5, 0.5])}
+    expected_idx_query = {'query_spec_0': np.array([4, 2, 0, 3, 1], dtype=np.int64),
+                          'query_spec_1': np.array([2, 1, 4, 3, 0], dtype=np.int64),
+                          'query_spec_2': np.array([4, 3, 2, 1, 0], dtype=np.int64)}
+    for key in scores_query:
+        assert np.allclose(scores_query[key], expected_scores_query[key], atol=1e-5), \
+            "Expected different selected scores"
+    for key in idx_query:
+        assert np.allclose(idx_query[key], expected_idx_query[key], atol=1e-5), \
+            "Expected different selected indices"
+
+    # Test lower top_n
+    idx_query, scores_query = get_top_hits(scores, top_n=2, search_by="queries")
+    for key in scores_query:
+        assert np.allclose(scores_query[key], expected_scores_query[key][:2], atol=1e-5), \
+            "Expected different selected scores"
+    for key in idx_query:
+        assert np.allclose(idx_query[key], expected_idx_query[key][:2], atol=1e-5), \
+            "Expected different selected indices"