Repository: Quantomatic/pyzx

Issue:
a round-trip through `graph_to_json` and `json_to_graph` renumbers the vertices
The [`graph_to_json`](https://github.com/Quantomatic/pyzx/blob/c657667529918acf85d584e43ac73592ad3f2505/pyzx/graph/jsonparser.py#L180) and [`json_to_graph`](https://github.com/Quantomatic/pyzx/blob/c657667529918acf85d584e43ac73592ad3f2505/pyzx/graph/jsonparser.py#L73) functions work with a JSON format which is "compatible with" Quantomatic. Where is this Quantomatic format documented?

In particular, the vertices are given names based on whether they are boundary or interior vertices on conversion of a graph to JSON ("b1", "v1", and so on), and when these are read back, they are assigned a vertex numbering which no longer corresponds to the original graph. I'm wondering if it would be possible to perserve the original numbering while maintaining compatibility with Quantomatic, for example by just using the labels "bN" and "vN" where N is the original vertex numbering of the input graph. But I don't know if such a change would break compatibility with Quantomatic.

The vertex numbering should probably be perserved before implementing functions to load and save the graphs (issue #187), as there should be a round-trip test that saving and loading a graph results in exactly the original graph.

The vertex renumbering seems to be the cause of issue https://github.com/Quantomatic/zxlive/issues/207. After PR https://github.com/Quantomatic/zxlive/pull/204, ZXLive is calling `graph_to_json` and `json_to_graph` to store the graphs which make up the proof steps, and the vertex renumbering is causing the diffs to be applied incorrectly. (Prior to that PR, the graphs were saved in TikZ format, but that format doesn't support parametrized phases.)

Hints from issue comments:
@jvdwetering: Compatibility with Quantomatic is not really necessary, as Quantomatic has been dead for a couple of years now. I was actually thinking whether it wouldn't make sense to make a new format for saving in json from the ground up that is more reasonable for pyzx and ZXLive. @akissinger: Quantomatic file formats are documented here:

https://github.com/Quantomatic/quantomatic/blob/stable/docs/json_formats.txt

Note that a vertex or boundary name can be any string. There is no requirement that they be of the form "bN" and "vN", this is just how it is done in the examples. So, just using the ints as quoted strings should work fine.

When we adopted the Quantomatic format for pyzx, we decided that it was fine to break quantomatic compatibility (hence also using a different file extension). So, I don't think there is a problem making some changes. E.g. adding input/output lists could be useful. I guess currently, it tries to guess the ordering based on the name? We could also ditch the "node-vertex" vs "wire-vertex" distinction, since the only "wire-vertices" we use in pyzx are boundaries, which we already identify their type.
@dlyongemallo: > I guess currently, it tries to guess the ordering based on the name?

The way it's implemented, it puts all the boundary vertices first on save, and assigns the vertex numbers sequentially on load, so unless the original graph had all the boundary vertices up front, the loaded graph's vertices will get numbered differently.

If it's fine to just completely break compatibility, then it would be a good idea to redesign the file format around what is necessary and convenient for PyZX and ZXLive.@jvdwetering: Ordering of inputs/outputs was based on position, but that's changed now and it actually stores which number of input or output it was separately. As this changed broke backwards compatibility anyway, maybe now is a good time to have a different json format altogether.@jvdwetering: The simplified format is now implemented, so I'm closing this issue.

Solution patch:
diff --git a/pyzx/__init__.py b/pyzx/__init__.py
index f320dcfe..53e142bb 100644
--- a/pyzx/__init__.py
+++ b/pyzx/__init__.py
@@ -25,7 +25,7 @@
 from .simplify import *
 from .optimize import *
 from .extract import *
-from .io import *
+from .graph.jsonparser import *
 from .tensor import *
 from .local_search.simulated_annealing import anneal
 from .local_search.genetic import GeneticOptimizer
diff --git a/pyzx/graph/base.py b/pyzx/graph/base.py
index 9f40eaef..c59e9a04 100644
--- a/pyzx/graph/base.py
+++ b/pyzx/graph/base.py
@@ -729,13 +729,12 @@ def to_matrix(self,preserve_scalar:bool=True) -> np.ndarray:
         return tensor_to_matrix(tensorfy(self, preserve_scalar), self.num_inputs(), self.num_outputs())
 
     def to_dict(self, include_scalar:bool=True) -> Dict[str, Any]:
-        """Returns a json representation of the graph that follows the Quantomatic .qgraph format.
-        Convert back into a graph using :meth:`from_json`."""
+        """Returns a dictionary representation of the graph, which can then be converted into json."""
         from .jsonparser import graph_to_dict
         return graph_to_dict(self, include_scalar)
 
     def to_json(self, include_scalar:bool=True) -> str:
-        """Returns a json representation of the graph that follows the Quantomatic .qgraph format.
+        """Returns a json representation of the graph.
         Convert back into a graph using :meth:`from_json`."""
         from .jsonparser import graph_to_json
         return graph_to_json(self, include_scalar)
@@ -755,7 +754,7 @@ def from_json(cls, js:Union[str,Dict[str,Any]]) -> BaseGraph[VT,ET]:
         """Converts the given .qgraph json string into a Graph.
         Works with the output of :meth:`to_json`."""
         from .jsonparser import json_to_graph
-        return json_to_graph(js,cls.backend)
+        return json_to_graph(js)
 
     @classmethod
     def from_tikz(cls, tikz: str, warn_overlap:bool= True, fuse_overlap:bool = True, ignore_nonzx:bool = False) -> BaseGraph[VT,ET]:
diff --git a/pyzx/graph/jsonparser.py b/pyzx/graph/jsonparser.py
index 79c0fe95..8286fb69 100644
--- a/pyzx/graph/jsonparser.py
+++ b/pyzx/graph/jsonparser.py
@@ -14,10 +14,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+__all__ = ['string_to_phase','to_graphml']
+
 import json
 import re
 from fractions import Fraction
-from typing import List, Dict, Any, Optional, Callable, Union, TYPE_CHECKING
+from typing import List, Dict, Tuple, Any, Optional, Callable, Union, TYPE_CHECKING
+
+from pyzx.graph.multigraph import Multigraph
 
 from ..utils import FractionLike, EdgeType, VertexType, phase_to_s
 from .graph import Graph
@@ -27,18 +31,7 @@
 from ..symbolic import parse, Poly, new_var
 if TYPE_CHECKING:
     from .diff import GraphDiff
-
-
-#def _phase_to_quanto_value(p: FractionLike) -> str:
-#    if not p: return ""
-#    try:
-#        p = Fraction(p)
-#        if p.numerator == -1: v = "-"
-#        elif p.numerator == 1: v = ""
-#        else: v = str(p.numerator)
-#        d = "/"+str(p.denominator) if p.denominator!=1 else ""
-#        return r"{}\pi{}".format(v,d)
-#    except TypeError:
+    from .multigraph import Multigraph
 
 
 def string_to_phase(string: str, g: Union[BaseGraph,'GraphDiff']) -> Union[Fraction, Poly]:
@@ -71,8 +64,10 @@ def _new_var(name: str) -> Poly:
         except Exception as e:
             raise ValueError(e)
 
-def json_to_graph(js: Union[str,Dict[str,Any]], backend:Optional[str]=None) -> BaseGraph:
-    """Converts the json representation of a .qgraph Quantomatic graph into
+def json_to_graph_old(js: Union[str,Dict[str,Any]], backend:Optional[str]=None) -> BaseGraph:
+    """This method is deprecated. Use `json_to_graph` or `dict_to_graph` instead.
+
+    Converts the json representation of a .qgraph Quantomatic graph into
     a pyzx graph. If JSON is given as a string, parse it first."""
     if isinstance(js, str):
         j = json.loads(js)
@@ -185,6 +180,56 @@ def graph_to_dict(g: BaseGraph[VT,ET], include_scalar: bool=True) -> Dict[str, A
     """Converts a PyZX graph into Python dict for JSON output.
     If include_scalar is set to True (the default), then this includes the value
     of g.scalar with the json, which will also be loaded by the ``from_json`` method."""
+    d = {
+        "version": 2,
+        "backend": g.backend,
+        "variable_types": g.variable_types, # Potential source of error: this dictionary is mutable
+    }
+    if hasattr(g,'name'):
+        d['name'] = g.name
+    if include_scalar:
+        d["scalar"] = g.scalar.to_dict()
+    d['inputs'] = g.inputs()
+    d['outputs'] = g.outputs()
+    if g.backend == 'multigraph':
+        d['auto_simplify'] = g.get_auto_simplify()
+    
+    verts = []
+    for v in g.vertices():
+        d_v = {
+            'id': v,
+            't': g.type(v),
+            'pos': (round(g.row(v),3),round(g.qubit(v),3)),
+        }
+        if g.phase(v):
+            d_v['phase'] = phase_to_s(g.phase(v))
+        vdata_keys = g.vdata_keys(v)
+        if vdata_keys:
+            d_v['data'] = {k: g.vdata(v,k) for k in vdata_keys}
+        if g.is_ground(v):
+            d_v['is_ground'] = True
+        verts.append(d_v)
+
+    edges: List[Tuple[VT,VT,EdgeType]] = []
+    if g.backend == 'multigraph':
+        for e in g.edges():
+            edges.append(e)  # type: ignore  # We know what we are doing, for multigraphs this has the right type.
+    else:
+        for e in g.edges():
+            src,tgt = g.edge_st(e)
+            et = g.edge_type(e)
+            edges.append((src,tgt,et))
+    
+    d['vertices'] = verts
+    d['edges']  = edges
+    return d
+
+
+def graph_to_dict_old(g: BaseGraph[VT,ET], include_scalar: bool=True) -> Dict[str, Any]:
+    """This method is deprecated, and replaced by `graph_to_dict`.
+    Converts a PyZX graph into Python dict for JSON output that is compatible with the Quantomatic format.
+    If include_scalar is set to True (the default), then this includes the value
+    of g.scalar with the json, which will also be loaded by the ``from_json`` method."""
     node_vs: Dict[str, Dict[str, Any]] = {}
     wire_vs: Dict[str, Dict[str, Any]] = {}
     edges: Dict[str, Dict[str, str]] = {}
@@ -288,6 +333,57 @@ def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str:
     of g.scalar with the json, which will also be loaded by the ``from_json`` method."""
     return json.dumps(graph_to_dict(g, include_scalar))
 
+def dict_to_graph(d: Dict[str,Any], backend: Optional[str]=None) -> BaseGraph:
+    """Converts a Python dict representation a graph produced by `graph_to_dict` into
+    a pyzx Graph.
+    If backend is given, it will be used as the backend for the graph, 
+    otherwise the backend will be read from the dict description."""
+    if not 'version' in d:
+        # "Version is not specified in dictionary, will try to parse it as an older format")
+        return json_to_graph_old(d, backend)
+    else:
+        if d['version'] != 2:
+            raise ValueError("Unsupported version "+str(d['version']))
+    if backend == None:
+        backend = d.get('backend', None)
+        if backend is None: raise ValueError("No backend specified in dictionary")
+    
+    g = Graph(backend)
+    g.variable_types = d.get('variable_types',{})
+    if g.backend == 'multigraph':
+        if TYPE_CHECKING:
+            assert isinstance(g, Multigraph)
+        b = True if d.get('auto_simplify', True) in ('true', True) else False
+        g.set_auto_simplify(b)
+    for v_d in d['vertices']:
+        pos = v_d['pos']
+        v = v_d['id']
+        g.add_vertex_indexed(v)
+        g.set_type(v,v_d['t'])
+        g.set_row(v,pos[0])
+        g.set_qubit(v,pos[1])
+        if 'phase' in v_d:
+            g.set_phase(v,string_to_phase(v_d['phase'],g))
+        if 'is_ground' in v_d and v_d['is_ground'] == True:
+            g.set_ground(v)
+        if 'data' in v_d:
+            for k,val in v_d['data'].items():
+                g.set_vdata(v,k,val)
+
+    for (s,t,et) in d['edges']:
+        g.add_edge((s,t),et)
+
+    return g
+
+def json_to_graph(js: Union[str,Dict[str,Any]], backend:Optional[str]=None) -> BaseGraph:
+    """Converts the json representation of a pyzx graph (as a string or dict) into
+    a `Graph`. If JSON is given as a string, parse it first."""
+    if isinstance(js, str):
+        d = json.loads(js)
+    else:
+        d = js
+    return dict_to_graph(d, backend)
+
 def to_graphml(g: BaseGraph[VT,ET]) -> str:
     gml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <graphml xmlns="http://graphml.graphdrawing.org/xmlns">
diff --git a/pyzx/graph/multigraph.py b/pyzx/graph/multigraph.py
index 951d14d6..ef21b683 100644
--- a/pyzx/graph/multigraph.py
+++ b/pyzx/graph/multigraph.py
@@ -177,7 +177,7 @@ def add_edge(self, edge_pair, edgetype=EdgeType.SIMPLE):
         elif edgetype == EdgeType.HADAMARD: e.add(h=1)
         else: e.add(w_io=1)
 
-        if self._auto_simplify:
+        if self._auto_simplify: # This currently can keep a parallel regular and Hadamard edge
             t1 = self.ty[s]
             t2 = self.ty[t]
             if (vertex_is_zx_like(t1) and vertex_is_zx_like(t2)):
@@ -189,16 +189,25 @@ def add_edge(self, edge_pair, edgetype=EdgeType.SIMPLE):
                         else:
                             self.add_to_phase(s, 1)
                     self.scalar.add_power(-e.h)
+                    self.nedges = self.nedges - e.h
                     e.h = 0
                 else: # apply spider and hopf to merge/cancel parallel edges
                     if t1 == t2:
-                        e.s = 1 if e.s > 0 else 0
-                        self.scalar.add_power(-2 * (e.h - (e.h % 2)))
-                        e.h = e.h % 2
+                        if e.s > 0:
+                            self.nedges = self.nedges - e.s + 1
+                            e.s = 1
+                        if e.h > 0:
+                            self.nedges = self.nedges - (e.h - e.h % 2)
+                            self.scalar.add_power(-2 * (e.h - (e.h % 2)))
+                            e.h = e.h % 2
                     else:
-                        e.h = 1 if e.h > 0 else 0
-                        self.scalar.add_power(-2 * (e.s - (e.s % 2)))
-                        e.s = e.s % 2
+                        if e.h > 0:
+                            self.nedges = self.nedges - e.h + 1
+                            e.h = 1
+                        if e.s > 0:
+                            self.nedges = self.nedges - (e.s - e.s % 2)
+                            self.scalar.add_power(-2 * (e.s - (e.s % 2)))
+                            e.s = e.s % 2
             if e.is_empty():
                 del self.graph[s][t]
                 del self.graph[t][s]


Test patch:
diff --git a/tests/test_graph.py b/tests/test_graph.py
index 56a2a937..e796408b 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -220,278 +220,3 @@ def test_compose_unitary(self):
         g2.compose(g)
         self.assertTrue(compare_tensors(g2,identity(2), False))
 
-
-test_graph = {'node_vertices': {
-                   'v0': {'annotation': {'coord': [1.0, -1.0]},
-                          'data': {'type': 'X', 'value': '\\pi'}},
-                   'v1': {'annotation': {'coord': [2.0, -1.0]},
-                          'data': {'type': 'Z'}},
-                   'v10': {'annotation': {'coord': [6.0, -2.0]},
-                           'data': {'type': 'X'}},
-                   'v11': {'annotation': {'coord': [7.0, -2.0]},
-                           'data': {'type': 'Z'}},
-                   'v12': {'annotation': {'coord': [2.0, -2.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v13': {'annotation': {'coord': [3.0, -2.0]},
-                           'data': {'type': 'Z'}},
-                   'v14': {'annotation': {'coord': [4.0, -2.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v15': {'annotation': {'coord': [11.0, -1.0]},
-                           'data': {'type': 'Z'}},
-                   'v16': {'annotation': {'coord': [2.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi/2'}},
-                   'v17': {'annotation': {'coord': [3.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi/2'}},
-                   'v18': {'annotation': {'coord': [4.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi/2'}},
-                   'v19': {'annotation': {'coord': [6.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi/2'}},
-                   'v2': {'annotation': {'coord': [3.0, -1.0]},
-                          'data': {'type': 'Z'}},
-                   'v20': {'annotation': {'coord': [7.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi/2'}},
-                   'v21': {'annotation': {'coord': [8.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi/2'}},
-                   'v22': {'annotation': {'coord': [9.0, -4.15]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi'}},
-                   'v23': {'annotation': {'coord': [10.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi'}},
-                   'v24': {'annotation': {'coord': [11.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi'}},
-                   'v25': {'annotation': {'coord': [13.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi'}},
-                   'v26': {'annotation': {'coord': [16.0, -3.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi'}},
-                   'v27': {'annotation': {'coord': [3.0, -4.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi'}},
-                   'v28': {'annotation': {'coord': [6.0, -4.0]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi'}},
-                   'v29': {'annotation': {'coord': [6.0, -5.0]},
-                           'data': {'type': 'Z'}},
-                   'v3': {'annotation': {'coord': [4.0, -1.0]},
-                          'data': {'type': 'Z'}},
-                   'v30': {'annotation': {'coord': [7.0, -5.0]},
-                           'data': {'type': 'X'}},
-                   'v31': {'annotation': {'coord': [8.0, -5.0]},
-                           'data': {'type': 'Z'}},
-                   'v32': {'annotation': {'coord': [9.75, -4.975]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v33': {'annotation': {'coord': [2.0, -5.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v34': {'annotation': {'coord': [3.0, -5.0]},
-                           'data': {'type': 'Z'}},
-                   'v35': {'annotation': {'coord': [4.0, -5.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v36': {'annotation': {'coord': [1.0, -6.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v37': {'annotation': {'coord': [6.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v38': {'annotation': {'coord': [7.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v39': {'annotation': {'coord': [8.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v4': {'annotation': {'coord': [6.0, -1.0]},
-                          'data': {'type': 'Z'}},
-                   'v40': {'annotation': {'coord': [1.0, -6.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v41': {'annotation': {'coord': [2.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v42': {'annotation': {'coord': [3.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v43': {'annotation': {'coord': [4.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v44': {'annotation': {'coord': [9.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v45': {'annotation': {'coord': [10.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v46': {'annotation': {'coord': [11.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v47': {'annotation': {'coord': [12.0, -1.0]},
-                           'data': {'type': 'Z'}},
-                   'v48': {'annotation': {'coord': [12.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v49': {'annotation': {'coord': [13.0, -2.0]},
-                           'data': {'type': 'Z'}},
-                   'v5': {'annotation': {'coord': [7.0, -1.0]},
-                          'data': {'type': 'Z'}},
-                   'v50': {'annotation': {'coord': [13.0, -5.0]},
-                           'data': {'type': 'Z'}},
-                   'v51': {'annotation': {'coord': [14.0, -2.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v52': {'annotation': {'coord': [15.0, -2.0]},
-                           'data': {'type': 'Z'}},
-                   'v53': {'annotation': {'coord': [16.0, -2.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v54': {'annotation': {'coord': [17.0, -2.0]},
-                           'data': {'type': 'Z'}},
-                   'v55': {'annotation': {'coord': [14.0, -5.0]},
-                           'data': {'type': 'Z'}},
-                   'v56': {'annotation': {'coord': [15.0, -5.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v57': {'annotation': {'coord': [16.0, -5.0]},
-                           'data': {'type': 'Z'}},
-                   'v58': {'annotation': {'coord': [17.0, -5.0]},
-                           'data': {'type': 'X', 'value': '\\pi'}},
-                   'v59': {'annotation': {'coord': [1.0, -2.0]},
-                           'data': {'type': 'Z'}},
-                   'v6': {'annotation': {'coord': [8.0, -1.0]},
-                          'data': {'type': 'Z'}},
-                   'v60': {'annotation': {'coord': [1.0, -5.0]},
-                           'data': {'type': 'Z'}},
-                   'v61': {'annotation': {'coord': [16.0, -1.0]},
-                           'data': {'type': 'Z'}},
-                   'v62': {'annotation': {'coord': [17.0, -6.0]},
-                           'data': {'type': 'Z'}},
-                   'v63': {'annotation': {'coord': [9.75374984741211,
-                                                    -4.166666793823242]},
-                           'data': {'is_edge': 'false',
-                                    'type': 'hadamard',
-                                    'value': '\\pi'}},
-                   'v7': {'annotation': {'coord': [9.0, -1.0]},
-                          'data': {'type': 'Z'}},
-                   'v8': {'annotation': {'coord': [10.0, -1.0]},
-                          'data': {'type': 'Z'}},
-                   'v9': {'annotation': {'coord': [5.0, -2.0]},
-                          'data': {'type': 'Z'}}},
- 'undir_edges': {'e0': {'src': 'v0', 'tgt': 'v59'},
-                 'e1': {'src': 'v0', 'tgt': 'v1'},
-                 'e10': {'src': 'v5', 'tgt': 'v6'},
-                 'e11': {'src': 'v5', 'tgt': 'v20'},
-                 'e12': {'src': 'v6', 'tgt': 'v7'},
-                 'e13': {'src': 'v6', 'tgt': 'v21'},
-                 'e14': {'src': 'v7', 'tgt': 'v8'},
-                 'e15': {'src': 'v7', 'tgt': 'v22'},
-                 'e16': {'src': 'v8', 'tgt': 'v15'},
-                 'e17': {'src': 'v8', 'tgt': 'v23'},
-                 'e18': {'src': 'v9', 'tgt': 'v14'},
-                 'e19': {'src': 'v9', 'tgt': 'v10'},
-                 'e2': {'src': 'v1', 'tgt': 'v2'},
-                 'e20': {'src': 'v9', 'tgt': 'v16'},
-                 'e21': {'src': 'v10', 'tgt': 'v11'},
-                 'e22': {'src': 'v10', 'tgt': 'v27'},
-                 'e23': {'src': 'v11', 'tgt': 'v49'},
-                 'e24': {'src': 'v11', 'tgt': 'v18'},
-                 'e25': {'src': 'v12', 'tgt': 'v59'},
-                 'e26': {'src': 'v12', 'tgt': 'v13'},
-                 'e27': {'src': 'v13', 'tgt': 'v14'},
-                 'e28': {'src': 'v13', 'tgt': 'v23'},
-                 'e29': {'src': 'v15', 'tgt': 'v47'},
-                 'e3': {'src': 'v1', 'tgt': 'v16'},
-                 'e30': {'src': 'v15', 'tgt': 'v25'},
-                 'e31': {'src': 'v16', 'tgt': 'v41'},
-                 'e32': {'src': 'v17', 'tgt': 'v42'},
-                 'e33': {'src': 'v17', 'tgt': 'v27'},
-                 'e34': {'src': 'v18', 'tgt': 'v43'},
-                 'e35': {'src': 'v19', 'tgt': 'v37'},
-                 'e36': {'src': 'v19', 'tgt': 'v29'},
-                 'e37': {'src': 'v20', 'tgt': 'v28'},
-                 'e38': {'src': 'v20', 'tgt': 'v38'},
-                 'e39': {'src': 'v21', 'tgt': 'v31'},
-                 'e4': {'src': 'v2', 'tgt': 'v3'},
-                 'e40': {'src': 'v21', 'tgt': 'v39'},
-                 'e41': {'src': 'v22', 'tgt': 'v44'},
-                 'e42': {'src': 'v22', 'tgt': 'v63'},
-                 'e43': {'src': 'v24', 'tgt': 'v34'},
-                 'e44': {'src': 'v24', 'tgt': 'v45'},
-                 'e45': {'src': 'v25', 'tgt': 'v55'},
-                 'e46': {'src': 'v25', 'tgt': 'v61'},
-                 'e47': {'src': 'v25', 'tgt': 'v52'},
-                 'e48': {'src': 'v26', 'tgt': 'v57'},
-                 'e49': {'src': 'v26', 'tgt': 'v62'},
-                 'e5': {'src': 'v2', 'tgt': 'v17'},
-                 'e50': {'src': 'v26', 'tgt': 'v54'},
-                 'e51': {'src': 'v26', 'tgt': 'v46'},
-                 'e52': {'src': 'v28', 'tgt': 'v30'},
-                 'e53': {'src': 'v29', 'tgt': 'v35'},
-                 'e54': {'src': 'v29', 'tgt': 'v30'},
-                 'e55': {'src': 'v30', 'tgt': 'v31'},
-                 'e56': {'src': 'v31', 'tgt': 'v32'},
-                 'e57': {'src': 'v32', 'tgt': 'v50'},
-                 'e58': {'src': 'v32', 'tgt': 'v63'},
-                 'e59': {'src': 'v33', 'tgt': 'v60'},
-                 'e6': {'src': 'v3', 'tgt': 'v4'},
-                 'e60': {'src': 'v33', 'tgt': 'v34'},
-                 'e61': {'src': 'v34', 'tgt': 'v35'},
-                 'e62': {'src': 'v37', 'tgt': 'v43'},
-                 'e63': {'src': 'v37', 'tgt': 'v38'},
-                 'e64': {'src': 'v38', 'tgt': 'v39'},
-                 'e65': {'src': 'v39', 'tgt': 'v44'},
-                 'e66': {'src': 'v40', 'tgt': 'v60'},
-                 'e67': {'src': 'v40', 'tgt': 'v41'},
-                 'e68': {'src': 'v41', 'tgt': 'v42'},
-                 'e69': {'src': 'v42', 'tgt': 'v43'},
-                 'e7': {'src': 'v3', 'tgt': 'v18'},
-                 'e70': {'src': 'v44', 'tgt': 'v45'},
-                 'e71': {'src': 'v45', 'tgt': 'v46'},
-                 'e72': {'src': 'v46', 'tgt': 'v48'},
-                 'e73': {'src': 'v47', 'tgt': 'v49'},
-                 'e74': {'src': 'v48', 'tgt': 'v50'},
-                 'e75': {'src': 'v49', 'tgt': 'v51'},
-                 'e76': {'src': 'v50', 'tgt': 'v55'},
-                 'e77': {'src': 'v51', 'tgt': 'v52'},
-                 'e78': {'src': 'v52', 'tgt': 'v53'},
-                 'e79': {'src': 'v53', 'tgt': 'v54'},
-                 'e8': {'src': 'v4', 'tgt': 'v5'},
-                 'e80': {'src': 'v54', 'tgt': 'b1'},
-                 'e81': {'src': 'v55', 'tgt': 'v56'},
-                 'e82': {'src': 'v56', 'tgt': 'v57'},
-                 'e83': {'src': 'v57', 'tgt': 'v58'},
-                 'e84': {'src': 'v58', 'tgt': 'b3'},
-                 'e85': {'src': 'v59', 'tgt': 'b0'},
-                 'e86': {'src': 'v60', 'tgt': 'b2'},
-                 'e9': {'src': 'v4', 'tgt': 'v19'}},
- 'wire_vertices': {'b0': {'annotation': {'boundary': True,
-                                         'coord': [0.0, -2.0],
-                                         'input': 0}},
-                   'b1': {'annotation': {'boundary': True,
-                                         'coord': [18.0, -2.0],
-                                         'output': 0}},
-                   'b2': {'annotation': {'boundary': True,
-                                         'coord': [0.0, -5.0],
-                                         'input': 1}},
-                   'b3': {'annotation': {'boundary': True,
-                                         'coord': [18.0, -5.0],
-                                         'output': 1}}}}
-
-
-class TestGraphIO(unittest.TestCase):
-
-    def test_load_json(self):
-        js = json.dumps(test_graph)
-        g = Graph.from_json(js)
-        js2 = g.to_json()
-
-    def test_load_tikz(self):
-        js = json.dumps(test_graph)
-        g = Graph.from_json(js)
-        tikz = g.to_tikz()
-        g2 = Graph.from_tikz(tikz, warn_overlap=False)
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/test_jsonparser.py b/tests/test_jsonparser.py
new file mode 100644
index 00000000..0ea961e6
--- /dev/null
+++ b/tests/test_jsonparser.py
@@ -0,0 +1,327 @@
+# PyZX - Python library for quantum circuit rewriting 
+#        and optimization using the ZX-calculus
+# Copyright (C) 2018 - Aleks Kissinger and John van de Wetering
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#    http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import unittest
+from fractions import Fraction
+import json
+import sys
+if __name__ == '__main__':
+    sys.path.append('..')
+    sys.path.append('.')
+
+from pyzx.graph import Graph
+from pyzx.utils import EdgeType, VertexType
+
+
+# A graph in the old format (a Quantomatic .qgraph file)
+test_graph_old_format = {'node_vertices': {
+                   'v0': {'annotation': {'coord': [1.0, -1.0]},
+                          'data': {'type': 'X', 'value': '\\pi'}},
+                   'v1': {'annotation': {'coord': [2.0, -1.0]},
+                          'data': {'type': 'Z'}},
+                   'v10': {'annotation': {'coord': [6.0, -2.0]},
+                           'data': {'type': 'X'}},
+                   'v11': {'annotation': {'coord': [7.0, -2.0]},
+                           'data': {'type': 'Z'}},
+                   'v12': {'annotation': {'coord': [2.0, -2.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v13': {'annotation': {'coord': [3.0, -2.0]},
+                           'data': {'type': 'Z'}},
+                   'v14': {'annotation': {'coord': [4.0, -2.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v15': {'annotation': {'coord': [11.0, -1.0]},
+                           'data': {'type': 'Z'}},
+                   'v16': {'annotation': {'coord': [2.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi/2'}},
+                   'v17': {'annotation': {'coord': [3.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi/2'}},
+                   'v18': {'annotation': {'coord': [4.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi/2'}},
+                   'v19': {'annotation': {'coord': [6.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi/2'}},
+                   'v2': {'annotation': {'coord': [3.0, -1.0]},
+                          'data': {'type': 'Z'}},
+                   'v20': {'annotation': {'coord': [7.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi/2'}},
+                   'v21': {'annotation': {'coord': [8.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi/2'}},
+                   'v22': {'annotation': {'coord': [9.0, -4.15]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi'}},
+                   'v23': {'annotation': {'coord': [10.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi'}},
+                   'v24': {'annotation': {'coord': [11.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi'}},
+                   'v25': {'annotation': {'coord': [13.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi'}},
+                   'v26': {'annotation': {'coord': [16.0, -3.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi'}},
+                   'v27': {'annotation': {'coord': [3.0, -4.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi'}},
+                   'v28': {'annotation': {'coord': [6.0, -4.0]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi'}},
+                   'v29': {'annotation': {'coord': [6.0, -5.0]},
+                           'data': {'type': 'Z'}},
+                   'v3': {'annotation': {'coord': [4.0, -1.0]},
+                          'data': {'type': 'Z'}},
+                   'v30': {'annotation': {'coord': [7.0, -5.0]},
+                           'data': {'type': 'X'}},
+                   'v31': {'annotation': {'coord': [8.0, -5.0]},
+                           'data': {'type': 'Z'}},
+                   'v32': {'annotation': {'coord': [9.75, -4.975]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v33': {'annotation': {'coord': [2.0, -5.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v34': {'annotation': {'coord': [3.0, -5.0]},
+                           'data': {'type': 'Z'}},
+                   'v35': {'annotation': {'coord': [4.0, -5.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v36': {'annotation': {'coord': [1.0, -6.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v37': {'annotation': {'coord': [6.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v38': {'annotation': {'coord': [7.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v39': {'annotation': {'coord': [8.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v4': {'annotation': {'coord': [6.0, -1.0]},
+                          'data': {'type': 'Z'}},
+                   'v40': {'annotation': {'coord': [1.0, -6.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v41': {'annotation': {'coord': [2.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v42': {'annotation': {'coord': [3.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v43': {'annotation': {'coord': [4.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v44': {'annotation': {'coord': [9.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v45': {'annotation': {'coord': [10.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v46': {'annotation': {'coord': [11.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v47': {'annotation': {'coord': [12.0, -1.0]},
+                           'data': {'type': 'Z'}},
+                   'v48': {'annotation': {'coord': [12.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v49': {'annotation': {'coord': [13.0, -2.0]},
+                           'data': {'type': 'Z'}},
+                   'v5': {'annotation': {'coord': [7.0, -1.0]},
+                          'data': {'type': 'Z'}},
+                   'v50': {'annotation': {'coord': [13.0, -5.0]},
+                           'data': {'type': 'Z'}},
+                   'v51': {'annotation': {'coord': [14.0, -2.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v52': {'annotation': {'coord': [15.0, -2.0]},
+                           'data': {'type': 'Z'}},
+                   'v53': {'annotation': {'coord': [16.0, -2.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v54': {'annotation': {'coord': [17.0, -2.0]},
+                           'data': {'type': 'Z'}},
+                   'v55': {'annotation': {'coord': [14.0, -5.0]},
+                           'data': {'type': 'Z'}},
+                   'v56': {'annotation': {'coord': [15.0, -5.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v57': {'annotation': {'coord': [16.0, -5.0]},
+                           'data': {'type': 'Z'}},
+                   'v58': {'annotation': {'coord': [17.0, -5.0]},
+                           'data': {'type': 'X', 'value': '\\pi'}},
+                   'v59': {'annotation': {'coord': [1.0, -2.0]},
+                           'data': {'type': 'Z'}},
+                   'v6': {'annotation': {'coord': [8.0, -1.0]},
+                          'data': {'type': 'Z'}},
+                   'v60': {'annotation': {'coord': [1.0, -5.0]},
+                           'data': {'type': 'Z'}},
+                   'v61': {'annotation': {'coord': [16.0, -1.0]},
+                           'data': {'type': 'Z'}},
+                   'v62': {'annotation': {'coord': [17.0, -6.0]},
+                           'data': {'type': 'Z'}},
+                   'v63': {'annotation': {'coord': [9.75374984741211,
+                                                    -4.166666793823242]},
+                           'data': {'is_edge': 'false',
+                                    'type': 'hadamard',
+                                    'value': '\\pi'}},
+                   'v7': {'annotation': {'coord': [9.0, -1.0]},
+                          'data': {'type': 'Z'}},
+                   'v8': {'annotation': {'coord': [10.0, -1.0]},
+                          'data': {'type': 'Z'}},
+                   'v9': {'annotation': {'coord': [5.0, -2.0]},
+                          'data': {'type': 'Z'}}},
+ 'undir_edges': {'e0': {'src': 'v0', 'tgt': 'v59'},
+                 'e1': {'src': 'v0', 'tgt': 'v1'},
+                 'e10': {'src': 'v5', 'tgt': 'v6'},
+                 'e11': {'src': 'v5', 'tgt': 'v20'},
+                 'e12': {'src': 'v6', 'tgt': 'v7'},
+                 'e13': {'src': 'v6', 'tgt': 'v21'},
+                 'e14': {'src': 'v7', 'tgt': 'v8'},
+                 'e15': {'src': 'v7', 'tgt': 'v22'},
+                 'e16': {'src': 'v8', 'tgt': 'v15'},
+                 'e17': {'src': 'v8', 'tgt': 'v23'},
+                 'e18': {'src': 'v9', 'tgt': 'v14'},
+                 'e19': {'src': 'v9', 'tgt': 'v10'},
+                 'e2': {'src': 'v1', 'tgt': 'v2'},
+                 'e20': {'src': 'v9', 'tgt': 'v16'},
+                 'e21': {'src': 'v10', 'tgt': 'v11'},
+                 'e22': {'src': 'v10', 'tgt': 'v27'},
+                 'e23': {'src': 'v11', 'tgt': 'v49'},
+                 'e24': {'src': 'v11', 'tgt': 'v18'},
+                 'e25': {'src': 'v12', 'tgt': 'v59'},
+                 'e26': {'src': 'v12', 'tgt': 'v13'},
+                 'e27': {'src': 'v13', 'tgt': 'v14'},
+                 'e28': {'src': 'v13', 'tgt': 'v23'},
+                 'e29': {'src': 'v15', 'tgt': 'v47'},
+                 'e3': {'src': 'v1', 'tgt': 'v16'},
+                 'e30': {'src': 'v15', 'tgt': 'v25'},
+                 'e31': {'src': 'v16', 'tgt': 'v41'},
+                 'e32': {'src': 'v17', 'tgt': 'v42'},
+                 'e33': {'src': 'v17', 'tgt': 'v27'},
+                 'e34': {'src': 'v18', 'tgt': 'v43'},
+                 'e35': {'src': 'v19', 'tgt': 'v37'},
+                 'e36': {'src': 'v19', 'tgt': 'v29'},
+                 'e37': {'src': 'v20', 'tgt': 'v28'},
+                 'e38': {'src': 'v20', 'tgt': 'v38'},
+                 'e39': {'src': 'v21', 'tgt': 'v31'},
+                 'e4': {'src': 'v2', 'tgt': 'v3'},
+                 'e40': {'src': 'v21', 'tgt': 'v39'},
+                 'e41': {'src': 'v22', 'tgt': 'v44'},
+                 'e42': {'src': 'v22', 'tgt': 'v63'},
+                 'e43': {'src': 'v24', 'tgt': 'v34'},
+                 'e44': {'src': 'v24', 'tgt': 'v45'},
+                 'e45': {'src': 'v25', 'tgt': 'v55'},
+                 'e46': {'src': 'v25', 'tgt': 'v61'},
+                 'e47': {'src': 'v25', 'tgt': 'v52'},
+                 'e48': {'src': 'v26', 'tgt': 'v57'},
+                 'e49': {'src': 'v26', 'tgt': 'v62'},
+                 'e5': {'src': 'v2', 'tgt': 'v17'},
+                 'e50': {'src': 'v26', 'tgt': 'v54'},
+                 'e51': {'src': 'v26', 'tgt': 'v46'},
+                 'e52': {'src': 'v28', 'tgt': 'v30'},
+                 'e53': {'src': 'v29', 'tgt': 'v35'},
+                 'e54': {'src': 'v29', 'tgt': 'v30'},
+                 'e55': {'src': 'v30', 'tgt': 'v31'},
+                 'e56': {'src': 'v31', 'tgt': 'v32'},
+                 'e57': {'src': 'v32', 'tgt': 'v50'},
+                 'e58': {'src': 'v32', 'tgt': 'v63'},
+                 'e59': {'src': 'v33', 'tgt': 'v60'},
+                 'e6': {'src': 'v3', 'tgt': 'v4'},
+                 'e60': {'src': 'v33', 'tgt': 'v34'},
+                 'e61': {'src': 'v34', 'tgt': 'v35'},
+                 'e62': {'src': 'v37', 'tgt': 'v43'},
+                 'e63': {'src': 'v37', 'tgt': 'v38'},
+                 'e64': {'src': 'v38', 'tgt': 'v39'},
+                 'e65': {'src': 'v39', 'tgt': 'v44'},
+                 'e66': {'src': 'v40', 'tgt': 'v60'},
+                 'e67': {'src': 'v40', 'tgt': 'v41'},
+                 'e68': {'src': 'v41', 'tgt': 'v42'},
+                 'e69': {'src': 'v42', 'tgt': 'v43'},
+                 'e7': {'src': 'v3', 'tgt': 'v18'},
+                 'e70': {'src': 'v44', 'tgt': 'v45'},
+                 'e71': {'src': 'v45', 'tgt': 'v46'},
+                 'e72': {'src': 'v46', 'tgt': 'v48'},
+                 'e73': {'src': 'v47', 'tgt': 'v49'},
+                 'e74': {'src': 'v48', 'tgt': 'v50'},
+                 'e75': {'src': 'v49', 'tgt': 'v51'},
+                 'e76': {'src': 'v50', 'tgt': 'v55'},
+                 'e77': {'src': 'v51', 'tgt': 'v52'},
+                 'e78': {'src': 'v52', 'tgt': 'v53'},
+                 'e79': {'src': 'v53', 'tgt': 'v54'},
+                 'e8': {'src': 'v4', 'tgt': 'v5'},
+                 'e80': {'src': 'v54', 'tgt': 'b1'},
+                 'e81': {'src': 'v55', 'tgt': 'v56'},
+                 'e82': {'src': 'v56', 'tgt': 'v57'},
+                 'e83': {'src': 'v57', 'tgt': 'v58'},
+                 'e84': {'src': 'v58', 'tgt': 'b3'},
+                 'e85': {'src': 'v59', 'tgt': 'b0'},
+                 'e86': {'src': 'v60', 'tgt': 'b2'},
+                 'e9': {'src': 'v4', 'tgt': 'v19'}},
+ 'wire_vertices': {'b0': {'annotation': {'boundary': True,
+                                         'coord': [0.0, -2.0],
+                                         'input': 0}},
+                   'b1': {'annotation': {'boundary': True,
+                                         'coord': [18.0, -2.0],
+                                         'output': 0}},
+                   'b2': {'annotation': {'boundary': True,
+                                         'coord': [0.0, -5.0],
+                                         'input': 1}},
+                   'b3': {'annotation': {'boundary': True,
+                                         'coord': [18.0, -5.0],
+                                         'output': 1}}}}
+
+
+class TestGraphIO(unittest.TestCase):
+
+    def test_load_multigraph_preserve_parallel_edges(self):
+        g = Graph('multigraph')
+        g.set_auto_simplify(False)
+        v = g.add_vertex(VertexType.Z, 0, 0)
+        w = g.add_vertex(VertexType.X, 0, 1)
+        x = g.add_vertex(VertexType.X, 0, 2)
+        g.add_edge((v,w))
+        g.add_edge((v,w))
+        g.add_edge((w,x))
+        g.add_edge((w,x),EdgeType.HADAMARD)
+
+        d = g.to_dict()
+        g2 = Graph.from_json(d)
+        self.assertTrue(g2.backend,'multigraph')
+        self.assertFalse(g2.get_auto_simplify())
+        self.assertEqual(g.num_edges(), g2.num_edges())
+
+        js = json.dumps(d)
+        g3 = Graph.from_json(js)
+        self.assertTrue(g3.backend,'multigraph')
+        self.assertFalse(g3.get_auto_simplify())
+        self.assertEqual(g.num_edges(), g3.num_edges())
+
+    def test_load_json_old_format(self):
+        js = json.dumps(test_graph_old_format)
+        g = Graph.from_json(js)
+        js2 = g.to_json()
+
+    def test_load_tikz(self):
+        js = json.dumps(test_graph_old_format)
+        g = Graph.from_json(js)
+        tikz = g.to_tikz()
+        g2 = Graph.from_tikz(tikz, warn_overlap=False)
+
+if __name__ == '__main__':
+    unittest.main()