Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplified json file format for graphs #281

Merged
merged 7 commits into from
Jan 22, 2025
2 changes: 1 addition & 1 deletion pyzx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions pyzx/graph/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]:
Expand Down
126 changes: 111 additions & 15 deletions pyzx/graph/jsonparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]] = {}
Expand Down Expand Up @@ -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">
Expand Down
23 changes: 16 additions & 7 deletions pyzx/graph/multigraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand All @@ -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]
Expand Down
Loading
Loading