diff --git a/demos/Time Benchmark.ipynb b/demos/Time Benchmark.ipynb
index 6f278dc3..c6464f38 100644
--- a/demos/Time Benchmark.ipynb
+++ b/demos/Time Benchmark.ipynb
@@ -1269,7 +1269,7 @@
"plt.scatter(x, y,color='grey') # Plot all the datapoints\n",
"c = np.polyfit(np.log(x),np.log(y),1, w=np.sqrt(y)) # Generate power law fit\n",
"a = np.linspace(0,max(x),100); b = (math.e**c[1])*a**c[0]\n",
- "plt.plot(a,b,'-',color='red',label=\"$y = a\\cdot x^{{{:.2f}}}$\".format(c[0])) # Plot the fit\n",
+ "plt.plot(a,b,'-',color='red',label=r\"$y = a\\cdot x^{{{:.2f}}}$\".format(c[0])) # Plot the fit\n",
"plt.ylim(ymin=0); plt.xlim(xmin=0,xmax=max(x)+50); \n",
"plt.xlabel(\"Gate-count\"); plt.ylabel(\"Time [seconds]\"); plt.legend(loc='upper left');"
]
diff --git a/pyzx/generate.py b/pyzx/generate.py
index d06acfd7..32dbd652 100644
--- a/pyzx/generate.py
+++ b/pyzx/generate.py
@@ -130,7 +130,7 @@ def CNOT_HAD_PHASE_circuit(
clifford:bool=False
) -> Circuit:
"""Construct a Circuit consisting of CNOT, HAD and phase gates.
- The default phase gate is the T gate, but if ``clifford=True``, then
+ The default phase gate is the T gate, but if ``clifford=True``\\ , then
this is replaced by the S gate.
Args:
diff --git a/pyzx/gflow.py b/pyzx/gflow.py
index 46fdd733..c57916e7 100644
--- a/pyzx/gflow.py
+++ b/pyzx/gflow.py
@@ -33,8 +33,8 @@
gFlowaux (V,Gamma,In,Out,k) =
begin
C := {}
- for all u in V \ Out do
- Solve in F2 : Gamma[V \ Out, Out \ In] * I[X] = I[{u}]
+ for all u in V \\ Out do
+ Solve in F2 : Gamma[V \\ Out, Out \\ In] * I[X] = I[{u}]
if there is a solution X0 then
C := C union {u}
g(u) := X0
diff --git a/pyzx/graph/base.py b/pyzx/graph/base.py
index 7420213f..f966c303 100644
--- a/pyzx/graph/base.py
+++ b/pyzx/graph/base.py
@@ -480,6 +480,12 @@ def to_matrix(self,preserve_scalar:bool=True) -> np.ndarray:
"""Returns a representation of the graph as a matrix using :func:`~pyzx.tensor.tensorfy`"""
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`."""
+ 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.
Convert back into a graph using :meth:`from_json`."""
@@ -497,7 +503,7 @@ def to_tikz(self,draw_scalar:bool=False) -> str:
return to_tikz(self,draw_scalar)
@classmethod
- def from_json(cls, js) -> 'BaseGraph':
+ def from_json(cls, js:Union[str,Dict[str,Any]]) -> 'BaseGraph':
"""Converts the given .qgraph json string into a Graph.
Works with the output of :meth:`to_json`."""
from .jsonparser import json_to_graph
@@ -990,3 +996,27 @@ def is_well_formed(self) -> bool:
if self.vertex_degree(v) < 2:
return False
return True
+
+ def get_auto_simplify(self) -> bool:
+ """Returns whether this graph auto-simplifies parallel edges
+
+ For multigraphs, this parameter might change, but simple graphs should always return True."""
+ return True
+
+ def set_auto_simplify(self, s: bool) -> None:
+ """Set whether this graph auto-simplifies parallel edges
+
+ Simple graphs should always auto-simplify, so this method is a no-op."""
+ pass
+
+ def is_phase_gadget(self, v: VT) -> bool:
+ """Returns True if the vertex is the 'hub' of a phase gadget"""
+ if self.phase(v) != 0 or self.vertex_degree(v) < 2:
+ return False
+ for w in self.neighbors(v):
+ if self.vertex_degree(w) == 1:
+ return True
+ return False
+
+
+
diff --git a/pyzx/graph/diff.py b/pyzx/graph/diff.py
index 66f2e640..f68713f8 100644
--- a/pyzx/graph/diff.py
+++ b/pyzx/graph/diff.py
@@ -22,147 +22,150 @@
from ..utils import VertexType, EdgeType, FractionLike, FloatInt, phase_to_s
from .base import BaseGraph, VT, ET
from .graph_s import GraphS
-from .jsonparser import ComplexDecoder, ComplexEncoder, string_to_phase
+from .jsonparser import string_to_phase
class GraphDiff(Generic[VT, ET]):
- removed_verts: List[VT]
- new_verts: List[VT]
- removed_edges: List[ET]
- new_edges: List[Tuple[Tuple[VT,VT],EdgeType]]
- changed_vertex_types: Dict[VT,VertexType]
- changed_edge_types: Dict[ET, EdgeType]
- changed_phases: Dict[VT, FractionLike]
- changed_pos: Dict[VT, Tuple[FloatInt,FloatInt]]
- changed_vdata: Dict[VT, Any]
- variable_types: Dict[str,bool]
-
- def __init__(self, g1: BaseGraph[VT,ET], g2: BaseGraph[VT,ET]) -> None:
- self.calculate_diff(g1,g2)
-
- def calculate_diff(self, g1: BaseGraph[VT,ET], g2: BaseGraph[VT,ET]) -> None:
- self.changed_vertex_types = {}
- self.changed_edge_types = {}
- self.changed_phases = {}
- self.changed_pos = {}
- self.changed_vdata = {}
- self.variable_types = g1.variable_types.copy()
- self.variable_types.update(g2.variable_types)
-
- old_verts = g1.vertex_set()
- new_verts = g2.vertex_set()
- self.removed_verts = list(old_verts - new_verts)
- self.new_verts = list(new_verts - old_verts)
- old_edges = g1.edge_set()
- new_edges = g2.edge_set()
- self.new_edges = []
- self.removed_edges = []
-
- for e in Counter(new_edges - old_edges).elements():
- self.new_edges.append((g2.edge_st(e), g2.edge_type(e)))
-
- for e in Counter(old_edges - new_edges).elements():
- s,t = g1.edge_st(e)
- self.removed_edges.append(e)
-
- for v in new_verts:
- if v in old_verts:
- if g1.type(v) != g2.type(v):
- self.changed_vertex_types[v] = g2.type(v)
- if g1.phase(v) != g2.phase(v):
- self.changed_phases[v] = g2.phase(v)
- if g1._vdata.get(v, None) != g2._vdata.get(v, None):
- self.changed_vdata[v] = g2._vdata.get(v, None)
- pos1 = g1.qubit(v), g1.row(v)
- pos2 = g2.qubit(v), g2.row(v)
- if pos1 != pos2:
- self.changed_pos[v] = pos2
- else: # It is a new vertex
- if g2.type(v) != VertexType.Z: # We are taking the Z type to be the default
- self.changed_vertex_types[v] = g2.type(v)
- if g2.phase(v) != 0:
- self.changed_phases[v] = g2.phase(v)
- pos2 = g2.qubit(v), g2.row(v)
- self.changed_pos[v] = pos2
-
- for e in new_edges:
- if e in old_edges:
- if g1.edge_type(e) != g2.edge_type(e):
- self.changed_edge_types[e] = g2.edge_type(e)
- else:
- if g2.edge_type(e) != EdgeType.HADAMARD: # We take Hadamard edges to be the default
- self.changed_edge_types[e] = g2.edge_type(e)
-
- def apply_diff(self,g: BaseGraph[VT,ET]) -> BaseGraph[VT,ET]:
- g = copy.deepcopy(g)
- g.remove_edges(self.removed_edges)
- g.remove_vertices(self.removed_verts)
- for v in self.new_verts:
- g.add_vertex_indexed(v)
- g.set_position(v,*self.changed_pos[v])
- if v in self.changed_vertex_types:
- g.set_type(v,self.changed_vertex_types[v])
- else:
- g.set_type(v,VertexType.Z)
- if v in self.changed_phases:
- g.set_phase(v,self.changed_phases[v])
- if v in self.changed_vdata:
- g._vdata[v] = self.changed_vdata[v]
- for st, ty in self.new_edges:
- g.add_edge(st,ty)
-
- for v in self.changed_pos:
- if v in self.new_verts: continue
- g.set_position(v,*self.changed_pos[v])
-
- for v in self.changed_vertex_types:
- if v in self.new_verts: continue
- g.set_type(v,self.changed_vertex_types[v])
-
- for v in self.changed_phases:
- if v in self.new_verts: continue
- g.set_phase(v,self.changed_phases[v])
-
- for v in self.changed_vdata:
- if v in self.new_verts: continue
- g._vdata[v] = self.changed_vdata[v]
-
- for e in self.changed_edge_types:
- g.set_edge_type(e,self.changed_edge_types[e])
-
- return g
-
- def to_json(self) -> str:
- changed_edge_types_str_dict = {}
- for key, value in self.changed_edge_types.items():
- changed_edge_types_str_dict[f"{key[0]},{key[1]}"] = value # type: ignore
- changed_phases_str = {k: phase_to_s(v) for k, v in self.changed_phases.items()}
- return json.dumps({
- "removed_verts": self.removed_verts,
- "new_verts": self.new_verts,
- "removed_edges": self.removed_edges,
- "new_edges": self.new_edges,
- "changed_vertex_types": self.changed_vertex_types,
- "changed_edge_types": changed_edge_types_str_dict,
- "changed_phases": changed_phases_str,
- "changed_pos": self.changed_pos,
- "changed_vdata": self.changed_vdata,
- "variable_types": self.variable_types
- }, cls=ComplexEncoder)
-
- @staticmethod
- def from_json(json_str: str) -> "GraphDiff":
- d = json.loads(json_str, cls=ComplexDecoder)
- gd = GraphDiff(GraphS(),GraphS())
- gd.removed_verts = d["removed_verts"]
- gd.new_verts = d["new_verts"]
- gd.removed_edges = list(map(tuple, d["removed_edges"])) # type: ignore
- gd.new_edges = list(map(tuple, d["new_edges"])) # type: ignore
- gd.changed_vertex_types = map_dict_keys(d["changed_vertex_types"], int)
- gd.changed_edge_types = map_dict_keys(d["changed_edge_types"], lambda x: tuple(map(int, x.split(","))))
- gd.changed_phases = {int(k): string_to_phase(v,gd) for k, v in d["changed_phases"].items()}
- gd.changed_pos = map_dict_keys(d["changed_pos"], int)
- gd.changed_vdata = map_dict_keys(d["changed_vdata"], int)
- return gd
+ removed_verts: List[VT]
+ new_verts: List[VT]
+ removed_edges: List[ET]
+ new_edges: List[Tuple[Tuple[VT,VT],EdgeType]]
+ changed_vertex_types: Dict[VT,VertexType]
+ changed_edge_types: Dict[ET, EdgeType]
+ changed_phases: Dict[VT, FractionLike]
+ changed_pos: Dict[VT, Tuple[FloatInt,FloatInt]]
+ changed_vdata: Dict[VT, Any]
+ variable_types: Dict[str,bool]
+
+ def __init__(self, g1: BaseGraph[VT,ET], g2: BaseGraph[VT,ET]) -> None:
+ self.calculate_diff(g1,g2)
+
+ def calculate_diff(self, g1: BaseGraph[VT,ET], g2: BaseGraph[VT,ET]) -> None:
+ self.changed_vertex_types = {}
+ self.changed_edge_types = {}
+ self.changed_phases = {}
+ self.changed_pos = {}
+ self.changed_vdata = {}
+ self.variable_types = g1.variable_types.copy()
+ self.variable_types.update(g2.variable_types)
+
+ old_verts = g1.vertex_set()
+ new_verts = g2.vertex_set()
+ self.removed_verts = list(old_verts - new_verts)
+ self.new_verts = list(new_verts - old_verts)
+ old_edges = g1.edge_set()
+ new_edges = g2.edge_set()
+ self.new_edges = []
+ self.removed_edges = []
+
+ for e in Counter(new_edges - old_edges).elements():
+ self.new_edges.append((g2.edge_st(e), g2.edge_type(e)))
+
+ for e in Counter(old_edges - new_edges).elements():
+ s,t = g1.edge_st(e)
+ self.removed_edges.append(e)
+
+ for v in new_verts:
+ if v in old_verts:
+ if g1.type(v) != g2.type(v):
+ self.changed_vertex_types[v] = g2.type(v)
+ if g1.phase(v) != g2.phase(v):
+ self.changed_phases[v] = g2.phase(v)
+ if g1._vdata.get(v, None) != g2._vdata.get(v, None):
+ self.changed_vdata[v] = g2._vdata.get(v, None)
+ pos1 = g1.qubit(v), g1.row(v)
+ pos2 = g2.qubit(v), g2.row(v)
+ if pos1 != pos2:
+ self.changed_pos[v] = pos2
+ else: # It is a new vertex
+ if g2.type(v) != VertexType.Z: # We are taking the Z type to be the default
+ self.changed_vertex_types[v] = g2.type(v)
+ if g2.phase(v) != 0:
+ self.changed_phases[v] = g2.phase(v)
+ pos2 = g2.qubit(v), g2.row(v)
+ self.changed_pos[v] = pos2
+
+ for e in new_edges:
+ if e in old_edges:
+ if g1.edge_type(e) != g2.edge_type(e):
+ self.changed_edge_types[e] = g2.edge_type(e)
+ else:
+ if g2.edge_type(e) != EdgeType.HADAMARD: # We take Hadamard edges to be the default
+ self.changed_edge_types[e] = g2.edge_type(e)
+
+ def apply_diff(self,g: BaseGraph[VT,ET]) -> BaseGraph[VT,ET]:
+ g = copy.deepcopy(g)
+ g.remove_edges(self.removed_edges)
+ g.remove_vertices(self.removed_verts)
+ for v in self.new_verts:
+ g.add_vertex_indexed(v)
+ g.set_position(v,*self.changed_pos[v])
+ if v in self.changed_vertex_types:
+ g.set_type(v,self.changed_vertex_types[v])
+ else:
+ g.set_type(v,VertexType.Z)
+ if v in self.changed_phases:
+ g.set_phase(v,self.changed_phases[v])
+ if v in self.changed_vdata:
+ g._vdata[v] = self.changed_vdata[v]
+ for st, ty in self.new_edges:
+ g.add_edge(st,ty)
+
+ for v in self.changed_pos:
+ if v in self.new_verts: continue
+ g.set_position(v,*self.changed_pos[v])
+
+ for v in self.changed_vertex_types:
+ if v in self.new_verts: continue
+ g.set_type(v,self.changed_vertex_types[v])
+
+ for v in self.changed_phases:
+ if v in self.new_verts: continue
+ g.set_phase(v,self.changed_phases[v])
+
+ for v in self.changed_vdata:
+ if v in self.new_verts: continue
+ g._vdata[v] = self.changed_vdata[v]
+
+ for e in self.changed_edge_types:
+ g.set_edge_type(e,self.changed_edge_types[e])
+
+ return g
+
+ def to_dict(self) -> Dict[str, Any]:
+ changed_edge_types_str_dict = {}
+ for key, value in self.changed_edge_types.items():
+ changed_edge_types_str_dict[f"{key[0]},{key[1]}"] = value # type: ignore
+ changed_phases_str = {k: phase_to_s(v) for k, v in self.changed_phases.items()}
+ return {
+ "removed_verts": self.removed_verts,
+ "new_verts": self.new_verts,
+ "removed_edges": self.removed_edges,
+ "new_edges": self.new_edges,
+ "changed_vertex_types": self.changed_vertex_types,
+ "changed_edge_types": changed_edge_types_str_dict,
+ "changed_phases": changed_phases_str,
+ "changed_pos": self.changed_pos,
+ "changed_vdata": self.changed_vdata,
+ "variable_types": self.variable_types
+ }
+
+ def to_json(self) -> str:
+ return json.dumps(self.to_dict())
+
+ @staticmethod
+ def from_json(json_str: str) -> "GraphDiff":
+ d = json.loads(json_str)
+ gd = GraphDiff(GraphS(),GraphS())
+ gd.removed_verts = d["removed_verts"]
+ gd.new_verts = d["new_verts"]
+ gd.removed_edges = list(map(tuple, d["removed_edges"])) # type: ignore
+ gd.new_edges = list(map(tuple, d["new_edges"])) # type: ignore
+ gd.changed_vertex_types = map_dict_keys(d["changed_vertex_types"], int)
+ gd.changed_edge_types = map_dict_keys(d["changed_edge_types"], lambda x: tuple(map(int, x.split(","))))
+ gd.changed_phases = {int(k): string_to_phase(v,gd) for k, v in d["changed_phases"].items()}
+ gd.changed_pos = map_dict_keys(d["changed_pos"], int)
+ gd.changed_vdata = map_dict_keys(d["changed_vdata"], int)
+ return gd
def map_dict_keys(d: Dict[str, Any], f: Callable[[str], Any]) -> Dict[Any, Any]:
- return {f(k): v for k, v in d.items()}
+ return {f(k): v for k, v in d.items()}
diff --git a/pyzx/graph/jsonparser.py b/pyzx/graph/jsonparser.py
index 6a03f816..79c0fe95 100644
--- a/pyzx/graph/jsonparser.py
+++ b/pyzx/graph/jsonparser.py
@@ -71,10 +71,13 @@ def _new_var(name: str) -> Poly:
except Exception as e:
raise ValueError(e)
-def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph:
+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
- a pyzx graph."""
- j = json.loads(js, cls=ComplexDecoder)
+ a pyzx graph. If JSON is given as a string, parse it first."""
+ if isinstance(js, str):
+ j = json.loads(js)
+ else:
+ j = js
g = Graph(backend)
g.variable_types = j.get('variable_types',{})
@@ -178,8 +181,8 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph:
return g
-def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str:
- """Converts a PyZX graph into JSON output compatible with Quantomatic.
+def graph_to_dict(g: BaseGraph[VT,ET], include_scalar: bool=True) -> Dict[str, Any]:
+ """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."""
node_vs: Dict[str, Dict[str, Any]] = {}
@@ -275,9 +278,15 @@ def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str:
"variable_types": g.variable_types,
}
if include_scalar:
- d["scalar"] = g.scalar.to_json()
+ d["scalar"] = g.scalar.to_dict()
- return json.dumps(d, cls=ComplexEncoder)
+ return d
+
+def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str:
+ """Converts a PyZX graph into JSON output compatible with Quantomatic.
+ 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."""
+ return json.dumps(graph_to_dict(g, include_scalar))
def to_graphml(g: BaseGraph[VT,ET]) -> str:
gml = """
@@ -326,22 +335,22 @@ def to_graphml(g: BaseGraph[VT,ET]) -> str:
return gml
-class ComplexEncoder(json.JSONEncoder):
- def default(self, obj):
- if isinstance(obj, complex):
- return str(obj)
- return super().default(obj)
-
-class ComplexDecoder(json.JSONDecoder):
- def __init__(self, *args, **kwargs):
- json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
-
- def object_hook(self, dct):
- for k, v in dct.items():
- if isinstance(v, str):
- try:
- dct[k] = complex(v)
- except ValueError:
- pass
- return dct
+# class ComplexEncoder(json.JSONEncoder):
+# def default(self, obj):
+# if isinstance(obj, complex):
+# return str(obj)
+# return super().default(obj)
+
+# class ComplexDecoder(json.JSONDecoder):
+# def __init__(self, *args, **kwargs):
+# json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
+
+# def object_hook(self, dct):
+# for k, v in dct.items():
+# if isinstance(v, str):
+# try:
+# dct[k] = complex(v)
+# except ValueError:
+# pass
+# return dct
diff --git a/pyzx/graph/multigraph.py b/pyzx/graph/multigraph.py
index 37e14817..b8e17179 100644
--- a/pyzx/graph/multigraph.py
+++ b/pyzx/graph/multigraph.py
@@ -69,7 +69,7 @@ def __init__(self) -> None:
self._auto_simplify: bool = True
self._vindex: int = 0
self.nedges: int = 0
- self.ty: Dict[int,VertexType] = dict()
+ self.ty: Dict[int,VertexType] = dict()
self._phase: Dict[int, FractionLike] = dict()
self._qindex: Dict[int, FloatInt] = dict()
self._maxq: FloatInt = -1
@@ -108,6 +108,9 @@ def set_auto_simplify(self, s: bool):
"""Automatically remove parallel edges as edges are added"""
self._auto_simplify = s
+ def get_auto_simplify(self):
+ return self._auto_simplify
+
def multigraph(self):
return False
diff --git a/pyzx/graph/scalar.py b/pyzx/graph/scalar.py
index 7d9e2a9c..ca51837f 100644
--- a/pyzx/graph/scalar.py
+++ b/pyzx/graph/scalar.py
@@ -20,7 +20,7 @@
import cmath
import copy
from fractions import Fraction
-from typing import List
+from typing import Dict, List, Any, Union
import json
from ..utils import FloatInt, FractionLike
@@ -178,26 +178,39 @@ def to_unicode(self) -> str:
s += "{:d}/{:d}π)".format(phase.numerator,phase.denominator)
return s
- def to_json(self) -> str:
+ def to_dict(self) -> Dict[str, Any]:
d = {"power2": self.power2, "phase": str(self.phase)}
if abs(self.floatfactor - 1) > 0.00001:
- d["floatfactor"] = self.floatfactor
+ d["floatfactor"] = str(self.floatfactor)
if self.phasenodes:
d["phasenodes"] = [str(p) for p in self.phasenodes]
if self.is_zero:
d["is_zero"] = self.is_zero
if self.is_unknown:
d["is_unknown"] = self.is_unknown,
- return json.dumps(d)
+ return d
+
+ def to_json(self) -> str:
+ return json.dumps(self.to_dict())
@classmethod
- def from_json(cls, s: str) -> 'Scalar':
- d = json.loads(s)
- d["phase"] = Fraction(d["phase"])
- if "phasenodes" in d:
- d["phasenodes"] = [Fraction(p) for p in d["phasenodes"]]
+ def from_json(cls, s: Union[str,Dict[str,Any]]) -> 'Scalar':
+ if isinstance(s, str):
+ d = json.loads(s)
+ else:
+ d = s
+ # print('scalar from json', repr(d))
scalar = Scalar()
- scalar.__dict__.update(d)
+ scalar.phase = Fraction(d["phase"]) # TODO support parameters
+ scalar.power2 = int(d["power2"])
+ if "floatfactor" in d:
+ scalar.floatfactor = complex(d["floatfactor"])
+ if "phasenodes" in d:
+ scalar.phasenodes = [Fraction(p) for p in d["phasenodes"]]
+ if "is_zero" in d:
+ scalar.is_zero = bool(d["is_zero"])
+ if "is_unknown" in d:
+ scalar.is_unknown = bool(d["is_unknown"])
return scalar
def set_unknown(self) -> None:
diff --git a/pyzx/hrules.py b/pyzx/hrules.py
index f362d2fc..26ed83fa 100644
--- a/pyzx/hrules.py
+++ b/pyzx/hrules.py
@@ -22,6 +22,54 @@
from .graph.base import BaseGraph, ET, VT, upair
from . import rules
+def is_hadamard(g: BaseGraph[VT,ET], v: VT) -> bool:
+ """Returns whether the vertex v in graph g is a Hadamard gate."""
+ if g.type(v) != VertexType.H_BOX: return False
+ if g.phase(v) != 1: return False
+ if g.vertex_degree(v) != 2: return False
+ return True
+
+def replace_hadamard(g: BaseGraph[VT,ET], v: VT) -> bool:
+ """Replaces a Hadamard gate with a Hadamard edge."""
+ if not is_hadamard(g, v): return False
+ n1,n2 = g.neighbors(v)
+ et1 = g.edge_type(g.edge(v,n1))
+ et2 = g.edge_type(g.edge(v,n2))
+ if et1 == et2: # both connecting edges are HADAMARD or SIMPLE
+ g.add_edge((n1,n2), EdgeType.HADAMARD)
+ else:
+ g.add_edge((n1,n2), EdgeType.SIMPLE)
+ g.remove_vertex(v)
+ g.scalar.add_power(1) # Correct for the sqrt(2) difference in H-boxes and H-edges
+ return True
+
+def had_edge_to_hbox(g: BaseGraph[VT,ET], e: ET) -> bool:
+ """Converts a Hadamard edge to a Hadamard gate.
+ Note that while this works with multigraphs, it will put the new H-box in the middle of the vertices,
+ so that the diagram might look wrong.
+ """
+ et = g.edge_type(e)
+ if et != EdgeType.HADAMARD: return False
+ s,t = g.edge_st(e)
+ rs = g.row(s)
+ rt = g.row(t)
+ qs = g.qubit(s)
+ qt = g.qubit(t)
+ g.remove_edge(e)
+ h = g.add_vertex(VertexType.H_BOX)
+ g.scalar.add_power(-1) # Correct for sqrt(2) scalar difference in H-edge and H-box.
+ g.add_edge((s, h),EdgeType.SIMPLE)
+ g.add_edge((h, t),EdgeType.SIMPLE)
+
+ if qs == qt:
+ g.set_qubit(h, qs)
+ else:
+ q = (qs + qt) / 2
+ if round(q) == q: q += 0.5
+ g.set_qubit(h, q)
+ g.set_row(h, (rs + rt) / 2)
+ return True
+
def match_hadamards(g: BaseGraph[VT,ET],
vertexf: Optional[Callable[[VT],bool]] = None
diff --git a/pyzx/optimize.py b/pyzx/optimize.py
index 8eed0cf4..39c5bd45 100644
--- a/pyzx/optimize.py
+++ b/pyzx/optimize.py
@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""This module implements several optimization methods on ``Circuit`` s.
+"""This module implements several optimization methods on ``Circuit``\\ s.
The function :func:`basic_optimization` runs a set of back-and-forth gate commutation and cancellation routines.
:func:`phase_block_optimize` does phase polynomial optimization using the TODD algorithm,
and :func:`full_optimize` combines these two methods."""
diff --git a/pyzx/simplify.py b/pyzx/simplify.py
index e25b45aa..582580ae 100644
--- a/pyzx/simplify.py
+++ b/pyzx/simplify.py
@@ -27,12 +27,15 @@
'full_reduce', 'teleport_reduce', 'reduce_scalar', 'supplementarity_simp',
'to_clifford_normal_form_graph', 'to_graph_like', 'is_graph_like']
+from ast import Mult
+from functools import reduce
from optparse import Option
from typing import List, Callable, Optional, Union, Generic, Tuple, Dict, Iterator, cast
from .utils import EdgeType, VertexType, toggle_edge, vertex_is_zx, toggle_vertex
from .rules import *
from .graph.base import BaseGraph, VT, ET
+from .graph.multigraph import Multigraph
from .circuit import Circuit
class Stats(object):
@@ -58,6 +61,7 @@ def simp(
name: str,
match: Callable[..., List[MatchObject]],
rewrite: Callable[[BaseGraph[VT,ET],List[MatchObject]],RewriteOutputType[VT,ET]],
+ auto_simplify_parallel_edges: bool = False,
matchf:Optional[Union[Callable[[ET],bool], Callable[[VT],bool]]]=None,
quiet:bool=False,
stats:Optional[Stats]=None) -> int:
@@ -73,6 +77,7 @@ def simp(
str name: The name to display if ``quiet`` is set to False.
match: One of the ``match_*`` functions of rules_.
rewrite: One of the rewrite functions of rules_.
+ auto_simplify_parallel_edges: whether to automatically combine parallel edges between vertices if the graph is a Multigraph
matchf: An optional filtering function on candidate vertices or edges, which
is passed as the second argument to the match function.
quiet: Suppress output on numbers of matches found during simplification.
@@ -80,6 +85,9 @@ def simp(
Returns:
Number of iterations of ``rewrite`` that had to be applied before no more matches were found."""
+ if auto_simplify_parallel_edges:
+ auto_simp_value = g.get_auto_simplify()
+ g.set_auto_simplify(True)
i = 0
new_matches = True
while new_matches:
@@ -103,19 +111,25 @@ def simp(
new_matches = True
if stats is not None: stats.count_rewrites(name, len(m))
if not quiet and i>0: print(' {!s} iterations'.format(i))
+ if auto_simplify_parallel_edges:
+ g.set_auto_simplify(auto_simp_value)
return i
def pivot_simp(g: BaseGraph[VT,ET], matchf:Optional[Callable[[ET],bool]]=None, quiet:bool=False, stats:Optional[Stats]=None) -> int:
- return simp(g, 'pivot_simp', match_pivot_parallel, pivot, matchf=matchf, quiet=quiet, stats=stats)
+ return simp(g, 'pivot_simp', match_pivot_parallel, pivot,
+ auto_simplify_parallel_edges=True, matchf=matchf, quiet=quiet, stats=stats)
def pivot_gadget_simp(g: BaseGraph[VT,ET], matchf:Optional[Callable[[ET],bool]]=None, quiet:bool=False, stats:Optional[Stats]=None) -> int:
- return simp(g, 'pivot_gadget_simp', match_pivot_gadget, pivot, matchf=matchf, quiet=quiet, stats=stats)
+ return simp(g, 'pivot_gadget_simp', match_pivot_gadget, pivot,
+ auto_simplify_parallel_edges=True, matchf=matchf, quiet=quiet, stats=stats)
def pivot_boundary_simp(g: BaseGraph[VT,ET], matchf:Optional[Callable[[ET],bool]]=None, quiet:bool=False, stats:Optional[Stats]=None) -> int:
- return simp(g, 'pivot_boundary_simp', match_pivot_boundary, pivot, matchf=matchf, quiet=quiet, stats=stats)
+ return simp(g, 'pivot_boundary_simp', match_pivot_boundary, pivot,
+ auto_simplify_parallel_edges=True, matchf=matchf, quiet=quiet, stats=stats)
def lcomp_simp(g: BaseGraph[VT,ET], matchf:Optional[Callable[[VT],bool]]=None, quiet:bool=False, stats:Optional[Stats]=None) -> int:
- return simp(g, 'lcomp_simp', match_lcomp_parallel, lcomp, matchf=matchf, quiet=quiet, stats=stats)
+ return simp(g, 'lcomp_simp', match_lcomp_parallel, lcomp,
+ auto_simplify_parallel_edges=True, matchf=matchf, quiet=quiet, stats=stats)
def bialg_simp(g: BaseGraph[VT,ET], quiet:bool=False, stats: Optional[Stats]=None) -> int:
return simp(g, 'bialg_simp', match_bialg_parallel, bialg, quiet=quiet, stats=stats)
@@ -127,10 +141,12 @@ def id_simp(g: BaseGraph[VT,ET], matchf:Optional[Callable[[VT],bool]]=None, quie
return simp(g, 'id_simp', match_ids_parallel, remove_ids, matchf=matchf, quiet=quiet, stats=stats)
def gadget_simp(g: BaseGraph[VT,ET], matchf: Optional[Callable[[VT],bool]]=None, quiet:bool=False, stats:Optional[Stats]=None) -> int:
- return simp(g, 'gadget_simp', match_phase_gadgets, merge_phase_gadgets, matchf=matchf, quiet=quiet, stats=stats)
+ return simp(g, 'gadget_simp', match_phase_gadgets, merge_phase_gadgets,
+ auto_simplify_parallel_edges=True, matchf=matchf, quiet=quiet, stats=stats)
def supplementarity_simp(g: BaseGraph[VT,ET], quiet:bool=False, stats:Optional[Stats]=None) -> int:
- return simp(g, 'supplementarity_simp', match_supplementarity, apply_supplementarity, quiet=quiet, stats=stats)
+ return simp(g, 'supplementarity_simp', match_supplementarity, apply_supplementarity,
+ auto_simplify_parallel_edges=True, quiet=quiet, stats=stats)
def copy_simp(g: BaseGraph[VT,ET], quiet:bool=False, stats:Optional[Stats]=None) -> int:
"""Copies 1-ary spiders with 0/pi phase through neighbors.
@@ -144,6 +160,19 @@ def phase_free_simp(g: BaseGraph[VT,ET], quiet:bool=False, stats:Optional[Stats]
i2 = bialg_simp(g, quiet=quiet, stats=stats)
return i1+i2
+def basic_simp(g: BaseGraph[VT,ET], matchf: Optional[Callable[[Union[VT, ET]],bool]]=None, quiet:bool=False, stats:Optional[Stats]=None) -> int:
+ """Keeps doing the simplifications ``id_simp`` and ``spider_simp`` until none of them can be applied anymore. If
+ starting from a circuit, the result should still have causal flow."""
+ spider_simp(g, matchf=matchf, quiet=quiet, stats=stats)
+ to_gh(g)
+ i = 0
+ while True:
+ i1 = id_simp(g, matchf=matchf, quiet=quiet, stats=stats)
+ i2 = spider_simp(g, matchf=matchf, quiet=quiet, stats=stats)
+ if i1+i2==0: break
+ i += 1
+ return i
+
def interior_clifford_simp(g: BaseGraph[VT,ET], matchf: Optional[Callable[[Union[VT, ET]],bool]]=None, quiet:bool=False, stats:Optional[Stats]=None) -> int:
"""Keeps doing the simplifications ``id_simp``, ``spider_simp``,
``pivot_simp`` and ``lcomp_simp`` until none of them can be applied anymore."""
@@ -283,23 +312,55 @@ def to_gh(g: BaseGraph[VT,ET],quiet:bool=True) -> None:
et = g.edge_type(e)
g.set_edge_type(e, toggle_edge(et))
-def to_rg(g: BaseGraph[VT,ET], select:Optional[Callable[[VT],bool]]=None) -> None:
- """Turn green nodes into red nodes by color-changing vertices which satisfy the predicate ``select``.
- By default, the predicate is set to greedily reducing the number of Hadamard-edges.
+def to_rg(g: BaseGraph[VT,ET], select:Optional[Callable[[VT],bool]]=None, change_gadgets: bool=True) -> None:
+ """Try to eliminate H-edges by turning green nodes red
+
+ By default, this does a breadth-first search starting at an arbitrary node, flipping the
+ color of alternating layers. For a ZX-diagram that is graph-like and 2-colorable, this will
+ eliminate all of the interior H-edges.
+
+ Alternatively, the function `select` can be provided instructing the method where to flip colors.
+
:param g: A ZX-graph.
- :param select: A function taking in vertices and returning ``True`` or ``False``."""
- if select is None:
- select = lambda v: (
- len([e for e in g.incident_edges(v) if g.edge_type(e) == EdgeType.SIMPLE]) <
- len([e for e in g.incident_edges(v) if g.edge_type(e) == EdgeType.HADAMARD])
- )
+ :param select: A function taking in vertices and returning ``True`` or ``False``.
+ :param change_gadgets: A flag saying always change gadgets to X."""
ty = g.types()
- for v in g.vertices():
- if select(v) and vertex_is_zx(ty[v]):
- g.set_type(v, toggle_vertex(ty[v]))
- for e in g.incident_edges(v):
- g.set_edge_type(e, toggle_edge(g.edge_type(e)))
+ if select is None:
+ remaining = set()
+ for w in g.vertices():
+ if change_gadgets and g.is_phase_gadget(w):
+ if vertex_is_zx(ty[w]):
+ g.set_type(w, toggle_vertex(ty[w]))
+ for e in g.incident_edges(w):
+ g.set_edge_type(e, toggle_edge(g.edge_type(e)))
+ else:
+ remaining.add(w)
+ while len(remaining) > 0:
+ v = next(iter(remaining))
+ # if v is a boundary, set `flip` such that its adjacent edge will not be an H-edge afterwards
+ if ty[v] == VertexType.BOUNDARY and g.incident_edges(v)[0] == EdgeType.SIMPLE:
+ flip = True
+ else:
+ flip = False
+ nhd = set([v])
+ while len(nhd) > 0:
+ for w in nhd:
+ if flip and vertex_is_zx(ty[w]):
+ g.set_type(w, toggle_vertex(ty[w]))
+ for e in g.incident_edges(w):
+ g.set_edge_type(e, toggle_edge(g.edge_type(e)))
+ flip = not flip
+ remaining -= nhd
+ nhd = set.union(*(set(g.neighbors(w)) for w in nhd)).intersection(remaining)
+
+ else:
+ for v in g.vertices():
+ if g.is_phase_gadget(v) and select(v) and vertex_is_zx(ty[v]):
+ g.set_type(v, toggle_vertex(ty[v]))
+ for e in g.incident_edges(v):
+ g.set_edge_type(e, toggle_edge(g.edge_type(e)))
+
def tcount(g: Union[BaseGraph[VT,ET], Circuit]) -> int:
"""Returns the amount of nodes in g that have a non-Clifford phase."""
diff --git a/pyzx/todd.py b/pyzx/todd.py
index e665b8df..798cf3e3 100644
--- a/pyzx/todd.py
+++ b/pyzx/todd.py
@@ -207,7 +207,7 @@ def phase_gates_to_poly(gates: List[Gate], qubits: int) -> Tuple[ParityPolynomia
def xi(m: Mat2, z: List[Z2]) -> Mat2:
- r"""Constructs the \chi matrix from the TOpt paper."""
+ """Constructs the \\chi matrix from the TOpt paper."""
arr = np.asarray(m.data)
rows = m.rows()
data = []
diff --git a/scratchpads/JohnDoodle.ipynb b/scratchpads/JohnDoodle.ipynb
index d21fda45..4123f11b 100644
--- a/scratchpads/JohnDoodle.ipynb
+++ b/scratchpads/JohnDoodle.ipynb
@@ -6167,7 +6167,7 @@
"\n",
"c = np.polyfit(np.log(x),np.log(y),1, w=np.sqrt(y)) # Generate power law fit\n",
"a = np.linspace(0,max(x),100); b = (math.e**c[1])*a**c[0]\n",
- "ax1.plot(a,b,'-',color='red',label=\"$y = a\\cdot x^{{{:.2f}}}$\".format(c[0])) # Plot the fit\n",
+ "ax1.plot(a,b,'-',color='red',label=r\"$y = a\\cdot x^{{{:.2f}}}$\".format(c[0])) # Plot the fit\n",
"\n",
"ax1.set_ylabel(\"time [s]\")\n",
"ax1.set_xlabel(\"qubits\")\n",
diff --git a/scratchpads/ak.ipynb b/scratchpads/ak.ipynb
index 5831c475..e6c73f05 100644
--- a/scratchpads/ak.ipynb
+++ b/scratchpads/ak.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
- "execution_count": 1,
+ "execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
@@ -26,13 +26,13 @@
},
{
"cell_type": "code",
- "execution_count": 11,
+ "execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
- "
\n",
+ "\n",
""
],
"text/plain": [
@@ -350,3964 +375,35 @@
"metadata": {},
"output_type": "display_data"
},
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "[T(0)]\n",
- "OPENQASM 2.0;\n",
- "include \"qelib1.inc\";\n",
- "qreg q[1];\n",
- "t q[0];\n",
- "\n"
- ]
- }
- ],
- "source": [
- "c = zx.qasm(\"\"\"\n",
- "qreg q[1];\n",
- "t q[0];\n",
- "t q[0];\n",
- "t q[0];\n",
- "t q[0];\n",
- "t q[0];\n",
- "\"\"\")\n",
- "c = zx.basic_optimization(c)\n",
- "zx.draw(c)\n",
- "print(c.gates)\n",
- "print(c.to_qasm())"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
{
"data": {
"text/plain": [
- "Circuit(1 qubits, 0 bits, 1 gates)"
+ "Graph(62 vertices, 69 edges)"
]
},
- "execution_count": 5,
+ "execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
- "zx.basic_optimization(c)"
+ "random.seed(1339)\n",
+ "g = zx.generate.cliffordT(5, 30, p_t=0.1)\n",
+ "zx.draw(g)\n",
+ "g"
]
},
{
"cell_type": "code",
- "execution_count": 12,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "\n",
- ""
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "\n",
- ""
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "g = zx.Graph()\n",
- "g.add_vertex(qubit=0,row=0)\n",
- "g.add_vertex(qubit=1,row=0)\n",
- "g.add_vertex(ty=Z, qubit=0,row=1)\n",
- "g.add_vertex(ty=Z, qubit=1,row=1)\n",
- "g.add_vertex(ty=X, qubit=0.5,row=1.5)\n",
- "g.add_vertex(qubit=0,row=2)\n",
- "g.add_vertex(qubit=1,row=2)\n",
- "g.add_edges([(0,2), (2,5), (1,3), (3,6), (2,4), (3,4)])\n",
- "zx.draw(g,labels=True)\n",
- "zx.clifford_simp(g)\n",
- "zx.draw(g,labels=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/svg+xml": [
- "\n",
- "\n",
- "\n"
- ],
- "text/plain": [
- "