Repository: matchms/matchms

Issue:
Add `.cyjs` export to networking module
**Is your feature request related to a problem? Please describe.**
Currently only `.graphml` output is supported, which is not supported by the cytoscape Galaxy plugin.

**Describe the solution you'd like**
Add the option to export to `.cyjs` using the networkx package to the Networking module.

**Any good starting points?**
[Do you know of any alternative solutions or do you have some helping code/links at hand?
](https://networkx.org/documentation/stable/reference/readwrite/json_graph.html)

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d41be843..d43aec48 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 - `Spectrum` objects now also have `.mz` and `.intensities` properties [#339](https://github.com/matchms/matchms/pull/339)
+- `SimilarityNetwork`: similarity-network graphs can now be exported to [cyjs](http://manual.cytoscape.org/en/stable/index.html),
+[gexf](http://gexf.net/schema.html), [gml](https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1),
+and node-link JSON formats [#349](https://github.com/matchms/matchms/pull/349)
 
 ### Changed
 - metadata filtering: made prefilter check for SMILES and InChI more lenient, eventually resulting in longer runtimes but more accurate checks [#337](https://github.com/matchms/matchms/pull/337)
diff --git a/matchms/networking/SimilarityNetwork.py b/matchms/networking/SimilarityNetwork.py
index 4be05392..72ead9da 100644
--- a/matchms/networking/SimilarityNetwork.py
+++ b/matchms/networking/SimilarityNetwork.py
@@ -1,3 +1,4 @@
+import json
 from typing import Optional
 import networkx as nx
 import numpy
@@ -6,7 +7,7 @@ from .networking_functions import get_top_hits
 
 
 class SimilarityNetwork:
-    """Create a spectal network from spectrum similarities.
+    """Create a spectral network from spectrum similarities.
 
     For example
 
@@ -54,7 +55,7 @@ class SimilarityNetwork:
         Parameters
         ----------
         identifier_key
-            Metadata key for unique intentifier for each spectrum in scores.
+            Metadata key for unique identifier for each spectrum in scores.
             Will also be used for the naming the network nodes. Default is 'spectrum_id'.
         top_n
             Consider edge between spectrumA and spectrumB if score falls into
@@ -68,7 +69,7 @@ class SimilarityNetwork:
             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).
+            scores results 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.
@@ -152,6 +153,35 @@ class SimilarityNetwork:
             msnet.remove_nodes_from(list(nx.isolates(msnet)))
         self.graph = msnet
 
+    def export_to_file(self, filename: str, graph_format: str = "graphml"):
+        """
+        Save the network to a file with chosen format.
+
+        Parameters
+        ----------
+        filename
+            Path to file to write to.
+        graph_format
+            Format, in which to store the network graph. Supported formats are: "cyjs", "gexf", "gml", "graphml", "json".
+            Default is "graphml".
+        """
+        if not self.graph:
+            raise ValueError("No network found. Make sure to first run .create_network() step")
+
+        writer = self._generate_writer(graph_format)
+        writer(filename)
+
+    def _generate_writer(self, graph_format: str):
+        writer = {"cyjs": self._export_to_cyjs,
+                  "gexf": self._export_to_gexf,
+                  "gml": self._export_to_gml,
+                  "graphml": self.export_to_graphml,
+                  "json": self._export_to_node_link_json}
+
+        assert graph_format in writer, "Format not supported.\n" \
+                                       "Please use one of supported formats: 'cyjs', 'gexf', 'gml', 'graphml', 'json'"
+        return writer[graph_format]
+
     def export_to_graphml(self, filename: str):
         """Save the network as .graphml file.
 
@@ -161,6 +191,37 @@ class SimilarityNetwork:
             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)
+
+    def _export_to_cyjs(self, filename: str):
+        """Save the network in cyjs format."""
+        graph = nx.cytoscape_data(self.graph)
+        return self._write_to_json(graph, filename)
+
+    def _export_to_node_link_json(self, filename: str):
+        """Save the network in node-link format."""
+        graph = nx.node_link_data(self.graph)
+        return self._write_to_json(graph, filename)
+
+    @staticmethod
+    def _write_to_json(graph: dict, filename: str):
+        """Save the network as JSON file.
+
+        Parameters
+        ----------
+        graph
+            JSON-dictionary type graph to save.
+        filename
+            Specify filename for exporting the graph.
+
+        """
+        with open(filename, "w", encoding="utf-8") as file:
+            json.dump(graph, file)
+
+    def _export_to_gexf(self, filename: str):
+        """Save the network as .gexf file."""
+        nx.write_gexf(self.graph, filename)
+
+    def _export_to_gml(self, filename: str):
+        """Save the network as .gml file."""
+        nx.write_gml(self.graph, filename)


Test patch:
diff --git a/tests/test_SimilarityNetwork.py b/tests/test_SimilarityNetwork.py
index 9e5787a6..bf86a88e 100644
--- a/tests/test_SimilarityNetwork.py
+++ b/tests/test_SimilarityNetwork.py
@@ -7,11 +7,17 @@ from matchms.networking import SimilarityNetwork
 from matchms.similarity import FingerprintSimilarity, ModifiedCosine
 
 
-@pytest.fixture
-def filename():
+@pytest.fixture(params=["cyjs", "gexf", "gml", "graphml", "json"])
+def graph_format(request):
+    yield request.param
+
+
+@pytest.fixture()
+def filename(graph_format):
+    filename = f"test.{graph_format}"
     with tempfile.TemporaryDirectory() as temp_dir:
-        filename = os.path.join(temp_dir, "test.graphml")
-        yield filename
+        filepath = os.path.join(temp_dir, filename)
+        yield filepath
 
 
 def create_dummy_spectrums():
@@ -127,15 +133,15 @@ def test_create_network_symmetric_modified_cosine():
     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"""
+def test_create_network_export_to_file(filename, graph_format):
+    """Test creating a graph file 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)
+    msnet.export_to_file(filename, graph_format)
 
-    assert os.path.isfile(filename), "graphml file not found"
+    assert os.path.isfile(filename), "network file not found"
 
 
 def test_create_network_symmetric_higher_cutoff():