From 69471b79a391a8911caa70151ad166c284ee6da6 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Fri, 11 Aug 2023 01:52:04 +0100 Subject: [PATCH 01/20] add vertex and edge types for W node --- pyzx/graph/graph_s.py | 14 +++++++------- pyzx/utils.py | 5 ++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pyzx/graph/graph_s.py b/pyzx/graph/graph_s.py index f70b706c..3c7477cb 100644 --- a/pyzx/graph/graph_s.py +++ b/pyzx/graph/graph_s.py @@ -1,4 +1,4 @@ -# PyZX - Python library for quantum circuit rewriting +# PyZX - Python library for quantum circuit rewriting # and optimization using the ZX-calculus # Copyright (C) 2018 - Aleks Kissinger and John van de Wetering @@ -25,7 +25,7 @@ class GraphS(BaseGraph[int,Tuple[int,int]]): """Purely Pythonic implementation of :class:`~graph.base.BaseGraph`.""" backend = 'simple' - #The documentation of what these methods do + #The documentation of what these methods do #can be found in base.BaseGraph def __init__(self) -> None: BaseGraph.__init__(self) @@ -43,7 +43,7 @@ def __init__(self) -> None: self._vdata: Dict[int,Any] = dict() self._inputs: Tuple[int, ...] = tuple() self._outputs: Tuple[int, ...] = tuple() - + def clone(self) -> 'GraphS': cpy = GraphS() for v, d in self.graph.items(): @@ -68,11 +68,11 @@ def clone(self) -> 'GraphS': return cpy def vindex(self): return self._vindex - def depth(self): + def depth(self): if self._rindex: self._maxr = max(self._rindex.values()) else: self._maxr = -1 return self._maxr - def qubit_count(self): + def qubit_count(self): if self._qindex: self._maxq = max(self._qindex.values()) else: self._maxq = -1 return self._maxq + 1 @@ -181,8 +181,8 @@ def edges(self): if v1 > v0: yield (v0,v1) def edges_in_range(self, start, end, safe=False): - """like self.edges, but only returns edges that belong to vertices - that are only directly connected to other vertices with + """like self.edges, but only returns edges that belong to vertices + that are only directly connected to other vertices with index between start and end. If safe=True then it also checks that every neighbour is only connected to vertices with the right index""" if not safe: diff --git a/pyzx/utils.py b/pyzx/utils.py index 36fac789..e64b863b 100644 --- a/pyzx/utils.py +++ b/pyzx/utils.py @@ -32,6 +32,8 @@ class VertexType: Z: Final = 1 X: Final = 2 H_BOX: Final = 3 + W_INPUT: Final = 4 + W_OUTPUT: Final = 5 def vertex_is_zx(ty: VertexType.Type) -> bool: """Check if a vertex type corresponds to a green or red spider.""" @@ -48,6 +50,7 @@ class EdgeType: Type = Literal[1,2] SIMPLE: Final = 1 HADAMARD: Final = 2 + W_IO: Final = 3 def toggle_edge(ty: EdgeType.Type) -> EdgeType.Type: """Swap the regular and Hadamard edge types.""" @@ -143,7 +146,7 @@ def get_mode(): settings.mode = "shell" return settings.mode - + def restricted_float(x): From 118c4017178ad59be43af26bc8d6966445791799 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Fri, 11 Aug 2023 01:53:45 +0100 Subject: [PATCH 02/20] made jsonparser compatible with W node --- pyzx/graph/jsonparser.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/pyzx/graph/jsonparser.py b/pyzx/graph/jsonparser.py index 3b47154a..07d58db9 100644 --- a/pyzx/graph/jsonparser.py +++ b/pyzx/graph/jsonparser.py @@ -1,4 +1,4 @@ -# PyZX - Python library for quantum circuit rewriting +# PyZX - Python library for quantum circuit rewriting # and optimization using the ZX-calculus # Copyright (C) 2018 - Aleks Kissinger and John van de Wetering @@ -55,7 +55,7 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph: names: Dict[str, Any] = {} # TODO: Any = VT hadamards: Dict[str, List[Any]] = {} for name,attr in j.get('node_vertices',{}).items(): - if ('data' in attr and 'type' in attr['data'] and attr['data']['type'] == "hadamard" + if ('data' in attr and 'type' in attr['data'] and attr['data']['type'] == "hadamard" and 'is_edge' in attr['data'] and attr['data']['is_edge'] == 'true'): hadamards[name] = [] continue @@ -71,6 +71,8 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph: if not 'type' in d or d['type'] == 'Z': g.set_type(v,VertexType.Z) elif d['type'] == 'X': g.set_type(v,VertexType.X) elif d['type'] == 'hadamard': g.set_type(v,VertexType.H_BOX) + elif d['type'] == 'W_input': g.set_type(v,VertexType.W_INPUT) + elif d['type'] == 'W_output': g.set_type(v,VertexType.W_OUTPUT) else: raise TypeError("unsupported type '{}'".format(d['type'])) if 'value' in d: g.set_phase(v,_quanto_value_to_phase(d['value'])) @@ -85,7 +87,7 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph: if key == 'coord': continue g.set_vdata(v, key, value) - + #g.set_vdata(v, 'x', c[0]) #g.set_vdata(v, 'y', c[1]) @@ -116,7 +118,7 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph: edges: Dict[Any, List[int]] = {} # TODO: Any = ET for edge in j.get('undir_edges',{}).values(): n1, n2 = edge['src'], edge['tgt'] - if n1 in hadamards and n2 in hadamards: #Both + if n1 in hadamards and n2 in hadamards: #Both v = g.add_vertex(VertexType.Z) name = "v"+str(len(names)) g.set_vdata(v, 'name',name) @@ -124,12 +126,15 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph: hadamards[n1].append(v) hadamards[n2].append(v) continue - if n1 in hadamards: + if n1 in hadamards: hadamards[n1].append(names[n2]) continue if n2 in hadamards: hadamards[n2].append(names[n1]) continue + if 'type' in edge and edge['type'] == 'w_io': + g.add_edge(g.edge(names[n1],names[n2]), EdgeType.W_IO) + continue amount = edges.get(g.edge(names[n1],names[n2]),[0,0]) amount[0] += 1 @@ -150,7 +155,7 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph: 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 + 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]] = {} @@ -167,13 +172,13 @@ def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str: if not name: if t == VertexType.BOUNDARY: name = freenamesb.pop(0) else: name = freenamesv.pop(0) - else: + else: try: freenamesb.remove(name) if t==VertexType.BOUNDARY else freenamesv.remove(name) except: pass #print("couldn't remove name '{}'".format(name)) - + names[v] = name if t == VertexType.BOUNDARY: wire_vs[name] = {"annotation":{"boundary":True,"coord":coord, @@ -191,6 +196,10 @@ def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str: elif t==VertexType.H_BOX: node_vs[name]["data"]["type"] = "hadamard" node_vs[name]["data"]["is_edge"] = "false" + elif t==VertexType.W_INPUT: + node_vs[name]["data"]["type"] = "W_input" + elif t==VertexType.W_OUTPUT: + node_vs[name]["data"]["type"] = "W_output" else: raise Exception("Unkown vertex type "+ str(t)) phase = _phase_to_quanto_value(g.phase(v)) if phase: node_vs[name]["data"]["value"] = phase @@ -209,7 +218,7 @@ def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str: if t == EdgeType.SIMPLE: edges["e"+ str(i)] = {"src": names[src],"tgt": names[tgt]} i += 1 - elif t==EdgeType.HADAMARD: + elif t == EdgeType.HADAMARD: x1,y1 = g.row(src), -g.qubit(src) x2,y2 = g.row(tgt), -g.qubit(tgt) hadname = freenamesv.pop(0) @@ -219,12 +228,15 @@ def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str: i += 1 edges["e"+str(i)] = {"src": names[tgt],"tgt": hadname} i += 1 + elif t == EdgeType.W_IO: + edges["e"+str(i)] = {"src": names[src],"tgt": names[tgt], "type": "w_io"} + i += 1 else: raise TypeError("Edge of type 0") d: Dict[str,Any] = { - "wire_vertices": wire_vs, - "node_vertices": node_vs, + "wire_vertices": wire_vs, + "node_vertices": node_vs, "undir_edges": edges } if include_scalar: From 5d7f8a95695d819dc7c49a45852f47eadfc0646a Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Sat, 12 Aug 2023 17:46:24 +0100 Subject: [PATCH 03/20] removed trailing whitespace --- pyzx/graph/diff.py | 2 +- pyzx/rules.py | 108 ++++++++++++++++++++++----------------------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/pyzx/graph/diff.py b/pyzx/graph/diff.py index 113b7d00..f9c46ff7 100644 --- a/pyzx/graph/diff.py +++ b/pyzx/graph/diff.py @@ -114,4 +114,4 @@ def apply_diff(self,g: BaseGraph[VT,ET]) -> BaseGraph[VT,ET]: if e in self.new_edges: continue g.set_edge_type(e,self.changed_edge_types[e]) - return g \ No newline at end of file + return g diff --git a/pyzx/rules.py b/pyzx/rules.py index 01bafd38..d2f944f2 100644 --- a/pyzx/rules.py +++ b/pyzx/rules.py @@ -21,14 +21,14 @@ The matcher finds as many non-overlapping places where the rewrite rule can be applied. The rewriter takes in a list of matches, and performs the necessary changes on the graph to implement the rewrite. -Each match function takes as input a Graph instance, +Each match function takes as input a Graph instance, and an optional "filter function" that tells the matcher to only consider the vertices or edges that the filter function accepts. It outputs a list of "match" objects. What these objects look like differs per rewrite rule. The rewrite function takes as input a Graph instance and a list of match objects -of the appropriate type. It outputs a 4-tuple +of the appropriate type. It outputs a 4-tuple (edges to add, vertices to remove, edges to remove, isolated vertices check). The first of these should be fed to :meth:`~pyzx.graph.base.BaseGraph.add_edge_table`, while the second and third should be fed to @@ -41,7 +41,7 @@ Warning: There is no guarantee that the matcher does not affect the graph, and currently some matchers - do in fact change the graph. Similarly, the rewrite function also changes the graph other + do in fact change the graph. Similarly, the rewrite function also changes the graph other than through the output it generates (for instance by adding vertices or changes phases). """ @@ -60,9 +60,9 @@ MatchObject = TypeVar('MatchObject') def apply_rule( - g: BaseGraph[VT,ET], + g: BaseGraph[VT,ET], rewrite: Callable[[BaseGraph[VT,ET], List[MatchObject]],RewriteOutputType[ET,VT]], - m: List[MatchObject], + m: List[MatchObject], check_isolated_vertices:bool=True ) -> None: etab, rem_verts, rem_edges, check_isolated_vertices = rewrite(g, m) @@ -81,12 +81,12 @@ def match_bialg(g: BaseGraph[VT,ET]) -> List[MatchBialgType[VT]]: #TODO: make it be hadamard edge aware def match_bialg_parallel( - g: BaseGraph[VT,ET], - matchf:Optional[Callable[[ET],bool]]=None, + g: BaseGraph[VT,ET], + matchf:Optional[Callable[[ET],bool]]=None, num: int=-1 ) -> List[MatchBialgType[VT]]: """Finds noninteracting matchings of the bialgebra rule. - + :param g: An instance of a ZX-graph. :param matchf: An optional filtering function for candidate edge, should return True if a edge should considered as a match. Passing None will @@ -99,7 +99,7 @@ def match_bialg_parallel( else: candidates = g.edge_set() phases = g.phases() types = g.types() - + i = 0 m = [] while (num == -1 or i < num) and len(candidates) > 0: @@ -138,7 +138,7 @@ def bialg(g: BaseGraph[VT,ET], matches: List[MatchBialgType[VT]]) -> RewriteOutp for e in es: if e in etab: etab[e][0] += 1 else: etab[e] = [1,0] - + return (etab, rem_verts, [], True) MatchSpiderType = Tuple[VT,VT] @@ -148,12 +148,12 @@ def match_spider(g: BaseGraph[VT,ET]) -> List[MatchSpiderType[VT]]: return match_spider_parallel(g, num=1) def match_spider_parallel( - g: BaseGraph[VT,ET], - matchf:Optional[Callable[[ET],bool]]=None, + g: BaseGraph[VT,ET], + matchf:Optional[Callable[[ET],bool]]=None, num:int=-1 ) -> List[MatchSpiderType[VT]]: """Finds non-interacting matchings of the spider fusion rule. - + :param g: An instance of a ZX-graph. :param matchf: An optional filtering function for candidate edge, should return True if the edge should be considered for matchings. Passing None will @@ -165,7 +165,7 @@ def match_spider_parallel( if matchf is not None: candidates = set([e for e in g.edges() if matchf(e)]) else: candidates = g.edge_set() types = g.types() - + i = 0 m = [] while (num == -1 or i < num) and len(candidates) > 0: @@ -259,13 +259,13 @@ def match_pivot(g: BaseGraph[VT,ET]) -> List[MatchPivotType[VT]]: def match_pivot_parallel( - g: BaseGraph[VT,ET], - matchf:Optional[Callable[[ET],bool]]=None, - num:int=-1, + g: BaseGraph[VT,ET], + matchf:Optional[Callable[[ET],bool]]=None, + num:int=-1, check_edge_types:bool=True ) -> List[MatchPivotType[VT]]: """Finds non-interacting matchings of the pivot rule. - + :param g: An instance of a ZX-graph. :param num: Maximal amount of matchings to find. If -1 (the default) tries to find as many as possible. @@ -280,7 +280,7 @@ def match_pivot_parallel( else: candidates = g.edge_set() types = g.types() phases = g.phases() - + i = 0 m = [] while (num == -1 or i < num) and len(candidates) > 0: @@ -334,8 +334,8 @@ def match_pivot_parallel( return m def match_pivot_gadget( - g: BaseGraph[VT,ET], - matchf:Optional[Callable[[ET],bool]]=None, + g: BaseGraph[VT,ET], + matchf:Optional[Callable[[ET],bool]]=None, num:int=-1) -> List[MatchPivotType[VT]]: """Like :func:`match_pivot_parallel`, but except for pairings of Pauli vertices, it looks for a pair of an interior Pauli vertex and an @@ -345,7 +345,7 @@ def match_pivot_gadget( types = g.types() phases = g.phases() rs = g.rows() - + edge_list = [] i = 0 m: List[MatchPivotType[VT]] = [] @@ -357,7 +357,7 @@ def match_pivot_gadget( v0a = phases[v0] v1a = phases[v1] - + if v0a not in (0,1): if v1a in (0,1): v0, v1 = v1, v0 @@ -368,7 +368,7 @@ def match_pivot_gadget( if g.is_ground(v0): continue - + v0n = list(g.neighbors(v0)) v1n = list(g.neighbors(v1)) if len(v1n) == 1: continue # It is a phase gadget @@ -386,17 +386,17 @@ def match_pivot_gadget( discard_edges.extend(ne) if bad_match: break if bad_match: continue - + if any(types[w]!=VertexType.Z for w in v0n): continue if any(types[w]!=VertexType.Z for w in v1n): continue # Both v0 and v1 are interior - + v = g.add_vertex(VertexType.Z,-2,rs[v0],v1a) g.set_phase(v1, 0) g.set_qubit(v0,-1) g.update_phase_index(v1,v) edge_list.append(g.edge(v,v1)) - + m.append((v0,v1,[],[v])) i += 1 for c in discard_edges: candidates.discard(c) @@ -405,8 +405,8 @@ def match_pivot_gadget( def match_pivot_boundary( - g: BaseGraph[VT,ET], - matchf:Optional[Callable[[VT],bool]]=None, + g: BaseGraph[VT,ET], + matchf:Optional[Callable[[VT],bool]]=None, num:int=-1) -> List[MatchPivotType[VT]]: """Like :func:`match_pivot_parallel`, but except for pairings of Pauli vertices, it looks for a pair of an interior Pauli vertex and a @@ -416,7 +416,7 @@ def match_pivot_boundary( types = g.types() phases = g.phases() rs = g.rows() - + edge_list = [] consumed_vertices : Set[VT] = set() i = 0 @@ -516,7 +516,7 @@ def pivot(g: BaseGraph[VT,ET], matches: List[MatchPivotType[VT]]) -> RewriteOutp g.add_to_phase(v, 1) if g.phase(m[0]) and g.phase(m[1]): g.scalar.add_phase(Fraction(1)) - if not m[2] and not m[3]: + if not m[2] and not m[3]: g.scalar.add_power(-(k0+k1+2*k2-1)) elif not m[2]: g.scalar.add_power(-(k1+k2)) @@ -561,13 +561,13 @@ def match_lcomp(g: BaseGraph[VT,ET]) -> List[MatchLcompType[VT]]: return match_lcomp_parallel(g, num=1, check_edge_types=True) def match_lcomp_parallel( - g: BaseGraph[VT,ET], - vertexf:Optional[Callable[[VT],bool]]=None, - num:int=-1, + g: BaseGraph[VT,ET], + vertexf:Optional[Callable[[VT],bool]]=None, + num:int=-1, check_edge_types:bool=True ) -> List[MatchLcompType[VT]]: """Finds noninteracting matchings of the local complementation rule. - + :param g: An instance of a ZX-graph. :param num: Maximal amount of matchings to find. If -1 (the default) tries to find as many as possible. @@ -582,14 +582,14 @@ def match_lcomp_parallel( else: candidates = g.vertex_set() types = g.types() phases = g.phases() - + i = 0 m = [] while (num == -1 or i < num) and len(candidates) > 0: v = candidates.pop() vt = types[v] va = g.phase(v) - + if vt != VertexType.Z: continue if not (va == Fraction(1,2) or va == Fraction(3,2)): continue @@ -599,7 +599,7 @@ def match_lcomp_parallel( if check_edge_types and not ( all(g.edge_type(e) == EdgeType.HADAMARD for e in g.incident_edges(v)) ): continue - + vn = list(g.neighbors(v)) if not all(types[n] == VertexType.Z for n in vn): continue @@ -639,12 +639,12 @@ def match_ids(g: BaseGraph[VT,ET]) -> List[MatchIdType[VT]]: return match_ids_parallel(g, num=1) def match_ids_parallel( - g: BaseGraph[VT,ET], - vertexf:Optional[Callable[[VT],bool]]=None, + g: BaseGraph[VT,ET], + vertexf:Optional[Callable[[VT],bool]]=None, num:int=-1 ) -> List[MatchIdType[VT]]: """Finds non-interacting identity vertices. - + :param g: An instance of a ZX-graph. :param num: Maximal amount of matchings to find. If -1 (the default) tries to find as many as possible. @@ -692,12 +692,12 @@ def remove_ids(g: BaseGraph[VT,ET], matches: List[MatchIdType[VT]]) -> RewriteOu if et == EdgeType.SIMPLE: etab[e][0] += 1 else: etab[e][1] += 1 return (etab, rem, [], False) - + MatchGadgetType = Tuple[VT,VT,FractionLike,List[VT],List[VT]] def match_phase_gadgets(g: BaseGraph[VT,ET]) -> List[MatchGadgetType[VT]]: """Determines which phase gadgets act on the same vertices, so that they can be fused together. - + :param g: An instance of a ZX-graph. :rtype: List of 5-tuples ``(axel,leaf, total combined phase, other axels with same targets, other leafs)``. """ @@ -721,7 +721,7 @@ def match_phase_gadgets(g: BaseGraph[VT,ET]) -> List[MatchGadgetType[VT]]: m: List[MatchGadgetType[VT]] = [] for par, gad in parities.items(): - if len(gad) == 1: + if len(gad) == 1: n = gad[0] v = gadgets[n] if phases[n] != 0: # If the phase of the axel vertex is pi, we change the phase of the gadget @@ -758,7 +758,7 @@ def merge_phase_gadgets(g: BaseGraph[VT,ET], matches: List[MatchGadgetType[VT]]) def match_supplementarity(g: BaseGraph[VT,ET]) -> List[MatchSupplementarityType[VT]]: """Finds pairs of non-Clifford spiders that are connected to exactly the same set of vertices. - + :param g: An instance of a ZX-graph. :rtype: List of 4-tuples ``(vertex1, vertex2, type of supplementarity, neighbors)``. """ @@ -775,7 +775,7 @@ def match_supplementarity(g: BaseGraph[VT,ET]) -> List[MatchSupplementarityType[ neigh = set(g.neighbors(v)) if not neigh.isdisjoint(taken): continue par = frozenset(neigh) - if par in parities: + if par in parities: for w in parities[par]: if (phases[v]-phases[w]) % 2 == 1 or (phases[v]+phases[w]) % 2 == 1: m.append((v,w,1,par)) @@ -799,7 +799,7 @@ def match_supplementarity(g: BaseGraph[VT,ET]) -> List[MatchSupplementarityType[ return m def apply_supplementarity( - g: BaseGraph[VT,ET], + g: BaseGraph[VT,ET], matches: List[MatchSupplementarityType[VT]] ) -> RewriteOutputType[ET,VT]: """Given the output of :func:``match_supplementarity``, removes non-Clifford spiders that act on the same set of targets trough supplementarity.""" @@ -812,7 +812,7 @@ def apply_supplementarity( g.scalar.add_power(-2*len(neigh)) if t == 1: # v and w are not connected g.scalar.add_node(2*alpha+1) - #if (alpha-beta)%2 == 1: # Standard supplementarity + #if (alpha-beta)%2 == 1: # Standard supplementarity if (alpha+beta)%2 == 1: # Need negation on beta g.scalar.add_phase(-alpha + 1) for n in neigh: @@ -820,7 +820,7 @@ def apply_supplementarity( elif t == 2: # they are connected g.scalar.add_power(-1) g.scalar.add_node(2*alpha) - #if (alpha-beta)%2 == 1: # Standard supplementarity + #if (alpha-beta)%2 == 1: # Standard supplementarity if (alpha+beta)%2 == 0: # Need negation g.scalar.add_phase(-alpha) for n in neigh: @@ -831,7 +831,7 @@ def apply_supplementarity( MatchCopyType = Tuple[VT,VT,FractionLike,FractionLike,List[VT]] def match_copy( - g: BaseGraph[VT,ET], + g: BaseGraph[VT,ET], vertexf:Optional[Callable[[VT],bool]]=None ) -> List[MatchCopyType[VT]]: """Finds spiders with a 0 or pi phase that have a single neighbor, @@ -865,7 +865,7 @@ def apply_copy(g: BaseGraph[VT,ET], matches: List[MatchCopyType[VT]]) -> Rewrite rem.append(w) g.scalar.add_power(-len(neigh)+1) if a: g.scalar.add_phase(alpha) - for n in neigh: + for n in neigh: if types[n] == VertexType.BOUNDARY: r = g.row(n) - 1 if n in outputs else g.row(n)+1 u = g.add_vertex(VertexType.Z, g.qubit(n), r, a) @@ -880,7 +880,7 @@ def apply_copy(g: BaseGraph[VT,ET], matches: List[MatchCopyType[VT]]) -> Rewrite def match_gadgets_phasepoly(g: BaseGraph[VT,ET]) -> List[MatchPhasePolyType[VT]]: """Finds groups of phase-gadgets that act on the same set of 4 vertices in order to apply a rewrite based on - rule R_13 of the paper *A Finite Presentation of CNOT-Dihedral Operators*.""" + rule R_13 of the paper *A Finite Presentation of CNOT-Dihedral Operators*.""" targets: Dict[VT,Set[FrozenSet[VT]]] = {} gadgets: Dict[FrozenSet[VT], Tuple[VT,VT]] = {} inputs = g.inputs() @@ -929,8 +929,8 @@ def match_gadgets_phasepoly(g: BaseGraph[VT,ET]) -> List[MatchPhasePolyType[VT]] return m def apply_gadget_phasepoly(g: BaseGraph[VT,ET], matches: List[MatchPhasePolyType[VT]]) -> None: - """Uses the output of :func:`match_gadgets_phasepoly` to apply a rewrite based - on rule R_13 of the paper *A Finite Presentation of CNOT-Dihedral Operators*.""" + """Uses the output of :func:`match_gadgets_phasepoly` to apply a rewrite based + on rule R_13 of the paper *A Finite Presentation of CNOT-Dihedral Operators*.""" rs = g.rows() phases = g.phases() for group, gadgets in matches: From b9d4632a423c7e11bba81cb6f9889affc9bea231 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Sun, 13 Aug 2023 00:52:59 +0100 Subject: [PATCH 04/20] implemented match_w_fusion and w_fusion rule --- pyzx/rules.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++----- pyzx/utils.py | 17 +++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/pyzx/rules.py b/pyzx/rules.py index d2f944f2..eeba356b 100644 --- a/pyzx/rules.py +++ b/pyzx/rules.py @@ -53,7 +53,7 @@ from fractions import Fraction import itertools -from .utils import VertexType, EdgeType, toggle_edge, vertex_is_zx, FloatInt, FractionLike +from .utils import VertexType, EdgeType, get_w_partner, toggle_edge, vertex_is_w, vertex_is_zx, FloatInt, FractionLike, get_w_io from .graph.base import BaseGraph, VT, ET RewriteOutputType = Tuple[Dict[ET,List[int]], List[VT], List[ET], bool] @@ -175,12 +175,12 @@ def match_spider_parallel( v0t = types[v0] v1t = types[v1] if (v0t == v1t and vertex_is_zx(v0t)): - i += 1 - for v in g.neighbors(v0): - for c in g.incident_edges(v): candidates.discard(c) - for v in g.neighbors(v1): - for c in g.incident_edges(v): candidates.discard(c) - m.append((v0,v1)) + i += 1 + for v in g.neighbors(v0): + for c in g.incident_edges(v): candidates.discard(c) + for v in g.neighbors(v1): + for c in g.incident_edges(v): candidates.discard(c) + m.append((v0,v1)) return m @@ -251,6 +251,73 @@ def unspider(g: BaseGraph[VT,ET], m: List[Any], qubit:FloatInt=-1, row:FloatInt= g.set_phase(u, 0) return v +MatchWType = Tuple[VT,VT] + +def match_w_fusion(g: BaseGraph[VT,ET]) -> List[MatchWType[VT]]: + """Does the same as :func:`match_spider_parallel` but with ``num=1``.""" + return match_spider_parallel(g, num=1) + +def match_w_fusion_parallel( + g: BaseGraph[VT,ET], + matchf:Optional[Callable[[ET],bool]]=None, + num:int=-1 + ) -> List[MatchWType[VT]]: + """Finds non-interacting matchings of the W fusion rule. + + :param g: An instance of a ZX-graph. + :param matchf: An optional filtering function for candidate edge, should + return True if the edge should be considered for matchings. Passing None will + consider all edges. + :param num: Maximal amount of matchings to find. If -1 (the default) + tries to find as many as possible. + :rtype: List of 2-tuples ``(v1, v2)`` + """ + if matchf is not None: candidates = set([e for e in g.edges() if matchf(e)]) + else: candidates = g.edge_set() + types = g.types() + + i = 0 + m = [] + while (num == -1 or i < num) and len(candidates) > 0: + e = candidates.pop() + if g.edge_type(e) != EdgeType.SIMPLE: continue + v0, v1 = g.edge_st(e) + v0t = types[v0] + v1t = types[v1] + if vertex_is_w(v0t) and vertex_is_w(v1t): + i += 1 + candidates_to_remove = [] + candidates_to_remove.extend(list(g.neighbors(v0))) + candidates_to_remove.extend(list(g.neighbors(v1))) + candidates_to_remove.extend(list(g.neighbors(get_w_partner(g, v0)))) + candidates_to_remove.extend(list(g.neighbors(get_w_partner(g, v1)))) + for v in candidates_to_remove: + for c in g.incident_edges(v): candidates.discard(c) + m.append((v0,v1)) + return m + +def w_fusion(g: BaseGraph[VT,ET], matches: List[MatchSpiderType[VT]]) -> RewriteOutputType[ET,VT]: + '''Performs W fusion given a list of matchings from ``match_w_fusion(_parallel)`` + ''' + rem_verts = [] + etab: Dict[ET,List[int]] = dict() + + for v0, v1 in matches: + v0_in, v0_out = get_w_io(g, v0) + v1_in, v1_out = get_w_io(g, v1) + # always delete the second vertex in the match + rem_verts.extend([v1_in, v1_out]) + + # edges from the second vertex are transferred to the first + for w in g.neighbors(v1_out): + if w == v1_in: + continue + e = g.edge(v0_out, w) + if e not in etab: etab[e] = [0,0] + etab[e][g.edge_type(g.edge(v1_out, w)) - 1] += 1 + return (etab, rem_verts, [], True) + + MatchPivotType = Tuple[VT,VT,List[VT],List[VT]] def match_pivot(g: BaseGraph[VT,ET]) -> List[MatchPivotType[VT]]: diff --git a/pyzx/utils.py b/pyzx/utils.py index e64b863b..cbf3eda4 100644 --- a/pyzx/utils.py +++ b/pyzx/utils.py @@ -45,6 +45,23 @@ def toggle_vertex(ty: VertexType.Type) -> VertexType.Type: return ty return VertexType.Z if ty == VertexType.X else VertexType.X +def vertex_is_w(ty: VertexType.Type) -> bool: + return ty == VertexType.W_INPUT or ty == VertexType.W_OUTPUT + +def get_w_partner(g, v): + assert vertex_is_w(g.type(v)) + for u in g.neighbors(v): + if g.edge_type((u, v)) == EdgeType.W_IO: + return u + assert False + +def get_w_io(g, v): + v2 = get_w_partner(g, v) + if g.type(v) == VertexType.W_INPUT: + return v, v2 + return v2, v + + class EdgeType: """Type of an edge in the graph.""" Type = Literal[1,2] From 1eec7ec7372a9de7268f137bb3873d7320e12b6b Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Sun, 13 Aug 2023 00:53:23 +0100 Subject: [PATCH 05/20] added fuse_w in editor_actions --- pyzx/editor_actions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyzx/editor_actions.py b/pyzx/editor_actions.py index ffec4892..8e6f3a9e 100644 --- a/pyzx/editor_actions.py +++ b/pyzx/editor_actions.py @@ -330,6 +330,11 @@ def bialgebra(g: BaseGraph[VT,ET], "matcher": hrules.match_par_hbox, "rule": hrules.par_hbox, "type": MATCHES_VERTICES}, + "fuse_w": {"text": "fuse W nodes", + "tooltip": "Merges two connected W nodes together", + "matcher": rules.match_w_fusion_parallel, + "rule": rules.w_fusion, + "type": MATCHES_EDGES}, "copy": {"text": "copy 0/pi spider", "tooltip": "Copies a single-legged spider with a 0/pi phase through its neighbor", "matcher": hrules.match_copy, From a93ce27444cbda9643547ac6107f8fa53106c403 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Sun, 13 Aug 2023 00:53:55 +0100 Subject: [PATCH 06/20] handling parallel edges for W node --- pyzx/graph/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pyzx/graph/base.py b/pyzx/graph/base.py index a282eb04..2ae7e442 100644 --- a/pyzx/graph/base.py +++ b/pyzx/graph/base.py @@ -767,6 +767,21 @@ def add_edge_table(self, etab:Mapping[ET,List[int]]) -> None: else: new_type = None else: raise ValueError("Unhandled parallel edges between nodes of type (%s,%s)" % (t1,t2)) + elif t1 == VertexType.W_OUTPUT or t2 == VertexType.W_OUTPUT: + # Since we don't yet support parallel edges, we simply add identity Z spiders to hack a parallel edge + r1,r2 = self.row(v1), self.row(v2) + q1,q2 = self.qubit(v1), self.qubit(v2) + id_1 = self.add_vertex(VertexType.Z, (q1 + q2) / 2 - 0.2, (r1 + r2) / 2 - 0.2) + id_2 = self.add_vertex(VertexType.Z, (q1 + q2) / 2 + 0.2, (r1 + r2) / 2 + 0.2) + add[EdgeType.SIMPLE].extend([self.edge(v1, id_1), self.edge(v1, id_2)]) + if n1 > 1: + add[EdgeType.SIMPLE].extend([self.edge(id_1, v2), self.edge(id_2, v2)]) + elif n2 > 2: + add[EdgeType.HADAMARD].extend([self.edge(id_1, v2), self.edge(id_2, v2)]) + else: + add[EdgeType.SIMPLE].append(self.edge(id_1, v2)) + add[EdgeType.HADAMARD].append(self.edge(id_2, v2)) + new_type = None else: raise ValueError("Unhandled parallel edges between nodes of type (%s,%s)" % (t1,t2)) From 546d08b0589a9e1d7fce8ffb08941310d79177f9 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Sun, 13 Aug 2023 02:00:40 +0100 Subject: [PATCH 07/20] allow self-loop for W node --- pyzx/graph/base.py | 6 ++++++ pyzx/rules.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/pyzx/graph/base.py b/pyzx/graph/base.py index 2ae7e442..8a90ed75 100644 --- a/pyzx/graph/base.py +++ b/pyzx/graph/base.py @@ -703,6 +703,12 @@ def add_edge_table(self, etab:Mapping[ET,List[int]]) -> None: if n1 == 1: new_type = EdgeType.SIMPLE elif n2 == 1: new_type = EdgeType.HADAMARD else: new_type = None + # self loops are allowed for W nodes. this is a hack to add self-loops using id Z spiders + if v1 == v2 and t1 == VertexType.W_OUTPUT and new_type: + id_1 = self.add_vertex(VertexType.Z, self.qubit(v1) + 1, self.row(v1) - 0.5) + id_2 = self.add_vertex(VertexType.Z, self.qubit(v1) + 1, self.row(v1) + 0.5) + add[EdgeType.SIMPLE].extend([self.edge(v1, id_1), self.edge(v1, id_2)]) + add[new_type].append(self.edge(id_1, id_2)) # Hence, all the other cases have some kind of parallel edge elif t1 == VertexType.BOUNDARY or t2 == VertexType.BOUNDARY: raise ValueError("Parallel edges to a boundary edge are not supported") diff --git a/pyzx/rules.py b/pyzx/rules.py index eeba356b..c1fb27c4 100644 --- a/pyzx/rules.py +++ b/pyzx/rules.py @@ -312,6 +312,8 @@ def w_fusion(g: BaseGraph[VT,ET], matches: List[MatchSpiderType[VT]]) -> Rewrite for w in g.neighbors(v1_out): if w == v1_in: continue + if w == v1_out: + w = v0_out e = g.edge(v0_out, w) if e not in etab: etab[e] = [0,0] etab[e][g.edge_type(g.edge(v1_out, w)) - 1] += 1 From 52501f57760d2220d422874327c623d45ff177dd Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Sun, 13 Aug 2023 18:44:02 +0100 Subject: [PATCH 08/20] W support for to and from tikz --- pyzx/tikz.py | 48 +++++++++++++++++++++++++++++++----------------- pyzx/utils.py | 5 ++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/pyzx/tikz.py b/pyzx/tikz.py index 32673698..7aeca85e 100644 --- a/pyzx/tikz.py +++ b/pyzx/tikz.py @@ -67,6 +67,10 @@ def _to_tikz(g: BaseGraph[VT,ET], draw_scalar:bool = False, style = settings.tikz_classes['boundary'] elif ty == VertexType.H_BOX: style = settings.tikz_classes['H'] + elif ty == VertexType.W_INPUT: + style = settings.tikz_classes['W input'] + elif ty == VertexType.W_OUTPUT: + style = settings.tikz_classes['W'] else: if p != 0: if ty==VertexType.Z: style = settings.tikz_classes['Z phase'] @@ -91,7 +95,7 @@ def _to_tikz(g: BaseGraph[VT,ET], draw_scalar:bool = False, v,w = g.edge_st(e) ty = g.edge_type(e) s = " \\draw " - if ty == EdgeType.HADAMARD: + if ty == EdgeType.HADAMARD: if g.type(v) != VertexType.BOUNDARY and g.type(w) != VertexType.BOUNDARY: style = settings.tikz_classes['H-edge'] if style: s += "[style={:s}] ".format(style) @@ -101,12 +105,15 @@ def _to_tikz(g: BaseGraph[VT,ET], draw_scalar:bool = False, t = " \\node [style={:s}] ({:d}) at ({:.2f}, {:.2f}) {{}};".format(settings.tikz_classes['H'],maxindex+1, x,y) verts.append(t) maxindex += 1 + elif ty == EdgeType.W_IO: + style = settings.tikz_classes['W-io-edge'] + if style: s += "[style={:s}] ".format(style) else: style = settings.tikz_classes['edge'] if style: s += "[style={:s}] ".format(style) s += "({:d}) to ({:d});".format(v+idoffset,w+idoffset) edges.append(s) - + return (verts, edges) def to_tikz(g: BaseGraph[VT,ET], draw_scalar:bool=False) -> str: @@ -164,27 +171,30 @@ def tikzit(g: Union[BaseGraph[VT,ET],Circuit,str], draw_scalar:bool=False) -> No synonyms_boundary = ['none', 'empty', 'boundary'] -synonyms_z = ['z dot', 'z spider', 'z', 'z phase dot', +synonyms_z = ['z dot', 'z spider', 'z', 'z phase dot', 'white spider', 'white phase spider', 'white dot', 'white phase dot', 'green dot', 'green node', 'green phase node'] -synonyms_x = ['x dot', 'x spider', 'x', 'x phase dot', +synonyms_x = ['x dot', 'x spider', 'x', 'x phase dot', 'grey spider', 'grey phase spider', 'grey dot', 'grey phase dot', 'gray spider', 'gray phase spider', 'gray dot', 'gray phase dot', 'red dot', 'red node', 'red phase node'] synonyms_hadamard = ['hadamard', 'h', 'small hadamard'] +synonyms_w_input = ['w input'] +synonyms_w_output = ['w output', 'w', 'w triangle'] synonyms_edge = ['empty', 'simple', 'none'] synonyms_hedge = ['hadamard edge'] +synonyms_wedge = ['w edge', 'w io edge'] tikz_error_message = "Not a valid tikz picture. Please use Tikzit to generate correct output." def tikz_to_graph( - s: str, - warn_overlap:bool= True, - fuse_overlap:bool = True, + s: str, + warn_overlap:bool= True, + fuse_overlap:bool = True, ignore_nonzx:bool = False, backend:Optional[str]=None) -> BaseGraph: - """Converts a tikz diagram into a pyzx Graph. - The tikz diagram is assumed to be one generated by Tikzit, + """Converts a tikz diagram into a pyzx Graph. + The tikz diagram is assumed to be one generated by Tikzit, and hence should have a nodelayer and a edgelayer.. Args: @@ -211,9 +221,9 @@ def tikz_to_graph( position_dict: Dict[str,List[int]] = {} for c,l in enumerate(lines[2:]): if l == r'\end{pgfonlayer}': break - # l should look like + # l should look like # \node [style=stylename] (integer_id) at (x_float, y_float) {$phase$}; - if not l.startswith(r'\node'): + if not l.startswith(r'\node'): raise ValueError(r"Node definition does not start with '\node': %s" % l) l = l[6:] i = l.find('[') @@ -233,6 +243,8 @@ def tikz_to_graph( elif style.lower() in synonyms_z: ty = VertexType.Z elif style.lower() in synonyms_x: ty = VertexType.X elif style.lower() in synonyms_hadamard: ty = VertexType.H_BOX + elif style.lower() in synonyms_w_input: ty = VertexType.W_INPUT + elif style.lower() in synonyms_w_output: ty = VertexType.W_OUTPUT else: if ignore_nonzx: ty = VertexType.BOUNDARY @@ -316,11 +328,11 @@ def tikz_to_graph( raise ValueError(tikz_error_message) for c,l in enumerate(lines[c+4:]): if l == r'\end{pgfonlayer}': break - if not l.startswith(r'\draw'): + if not l.startswith(r'\draw'): raise ValueError(r"Edge definition does not start with '\draw': %s" % l) l = l[6:] i = l.find('style') - if i == -1: + if i == -1: style = "empty" j = l.find(']') else: @@ -328,7 +340,7 @@ def tikz_to_graph( if j1 == -1: raise ValueError(r"Faulty edge definition %s" % l) j2 = l.find(',',i) - if j2 != -1 and j2 < j1: + if j2 != -1 and j2 < j1: style = l[i+5:j2].replace("=","").strip() else: style = l[i+5:j1].replace("=","").strip() @@ -339,16 +351,18 @@ def tikz_to_graph( e = g.edge(index_dict[int(src)],index_dict[int(tgt)]) - if style.lower() in synonyms_edge: + if style.lower() in synonyms_edge: if e in etab: etab[e][0] += 1 else: etab[e] = [1,0] - elif style.lower() in synonyms_hedge: + elif style.lower() in synonyms_hedge: if e in etab: etab[e][1] += 1 else: etab[e] = [0,1] + elif style.lower() in synonyms_wedge: + g.add_edge(e, EdgeType.W_IO) else: if ignore_nonzx: if e in etab: @@ -357,6 +371,6 @@ def tikz_to_graph( etab[e] = [1,0] else: raise ValueError("Unknown edge style '%s' in edge definition %s" % (style, l)) - + g.add_edge_table(etab) return g diff --git a/pyzx/utils.py b/pyzx/utils.py index 186a9776..93c7119d 100644 --- a/pyzx/utils.py +++ b/pyzx/utils.py @@ -107,8 +107,11 @@ def phase_is_pauli(phase: FractionLike): 'Z phase': 'Z phase dot', 'X phase': 'X phase dot', 'H': 'hadamard', + 'W': 'W triangle', + 'W input': 'W input', 'edge': '', - 'H-edge': 'hadamard edge' + 'H-edge': 'hadamard edge', + 'W-io-edge': 'W io edge' } class Settings(object): # namespace class From 263c654631430ee87a690a216ceab4649be47efb Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Mon, 14 Aug 2023 00:06:08 +0100 Subject: [PATCH 09/20] handled a missing case of self-loop --- pyzx/graph/base.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/pyzx/graph/base.py b/pyzx/graph/base.py index 8a90ed75..556e9f77 100644 --- a/pyzx/graph/base.py +++ b/pyzx/graph/base.py @@ -24,7 +24,7 @@ import numpy as np -from ..utils import EdgeType, VertexType, toggle_edge, vertex_is_zx, toggle_vertex +from ..utils import EdgeType, VertexType, toggle_edge, vertex_is_zx, toggle_vertex, vertex_is_w, get_w_partner from ..utils import FloatInt, FractionLike from ..tensor import tensorfy, tensor_to_matrix @@ -70,7 +70,7 @@ def pack_indices(lst: List[FloatInt]) -> Mapping[FloatInt,int]: class BaseGraph(Generic[VT, ET], metaclass=DocstringMeta): """Base class for letting graph backends interact with PyZX. For a backend to work with PyZX, there should be a class that implements - all the methods of this class. For implementations of this class see + all the methods of this class. For implementations of this class see :class:`~pyzx.graph.graph_s.GraphS` or :class:`~pyzx.graph.graph_ig.GraphIG`.""" backend: ClassVar[str] = 'None' @@ -114,7 +114,7 @@ def stats(self) -> str: return s def copy(self, adjoint:bool=False, backend:Optional[str]=None) -> 'BaseGraph': - """Create a copy of the graph. If ``adjoint`` is set, + """Create a copy of the graph. If ``adjoint`` is set, the adjoint of the graph will be returned (inputs and outputs flipped, phases reversed). When ``backend`` is set, a copy of the graph with the given backend is produced. By default the copy will have the same backend. @@ -150,7 +150,7 @@ def copy(self, adjoint:bool=False, backend:Optional[str]=None) -> 'BaseGraph': for v in self.vertices(): i = g.add_vertex(ty[v],phase=mult*ph[v]) if v in qs: g.set_qubit(i,qs[v]) - if v in rs: + if v in rs: if adjoint: g.set_row(i, maxr-rs[v]) else: g.set_row(i, rs[v]) vtab[v] = i @@ -167,7 +167,7 @@ def copy(self, adjoint:bool=False, backend:Optional[str]=None) -> 'BaseGraph': else: g.set_inputs(new_outputs) g.set_outputs(new_inputs) - + etab = {e:g.edge(vtab[self.edge_s(e)],vtab[self.edge_t(e)]) for e in self.edges()} g.add_edges(etab.values()) for e,f in etab.items(): @@ -204,7 +204,7 @@ def map_qubits(self, qubit_map:Mapping[int,Tuple[float,float]]) -> None: def replace_subgraph(self, left_row: FloatInt, right_row: FloatInt, replace: 'BaseGraph') -> None: """Deletes the subgraph of all nodes with rank strictly between ``left_row`` and ``right_row`` and replaces it with the graph ``replace``. - The amount of nodes on the left row should match the amount of inputs of + The amount of nodes on the left row should match the amount of inputs of the replacement graph and the same for the right row and the outputs. The graphs are glued together based on the qubit index of the vertices.""" qleft = [v for v in self.vertices() if self.row(v)==left_row] @@ -219,7 +219,7 @@ def replace_subgraph(self, left_row: FloatInt, right_row: FloatInt, replace: 'Ba raise TypeError("Input qubit indices do not match") if set(self.qubit(v) for v in qright)!= set(replace.qubit(v) for v in r_outputs): raise TypeError("Output qubit indices do not match") - + self.remove_vertices([v for v in self.vertices() if (left_row < self.row(v) and self.row(v) < right_row)]) self.remove_edges([self.edge(s,t) for s in qleft for t in qright if self.connected(s,t)]) rdepth = replace.depth() -1 @@ -350,7 +350,7 @@ def __matmul__(self, other: 'BaseGraph') -> 'BaseGraph': def merge(self, other: 'BaseGraph') -> Tuple[List[VT],List[ET]]: """Merges this graph with the other graph in-place. - Returns (list-of-vertices, list-of-edges) corresponding to + Returns (list-of-vertices, list-of-edges) corresponding to the id's of the vertices and edges of the other graph.""" ty = other.types() rs = other.rows() @@ -401,7 +401,7 @@ def apply_state(self, state: str) -> None: new_inputs = [] for i,s in enumerate(state): v = inputs[i] - if s == '/': + if s == '/': new_inputs.append(v) continue if s in ('0', '1'): @@ -470,15 +470,15 @@ def to_tikz(self,draw_scalar:bool=False) -> str: @classmethod def from_json(cls, js) -> 'BaseGraph': - """Converts the given .qgraph json string into a Graph. + """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) @classmethod def from_tikz(cls, tikz: str, warn_overlap:bool= True, fuse_overlap:bool = True, ignore_nonzx:bool = False) -> 'BaseGraph': - """Converts a tikz diagram into a pyzx Graph. - The tikz diagram is assumed to be one generated by Tikzit, + """Converts a tikz diagram into a pyzx Graph. + The tikz diagram is assumed to be one generated by Tikzit, and hence should have a nodelayer and a edgelayer.. Args: @@ -494,7 +494,7 @@ def from_tikz(cls, tikz: str, warn_overlap:bool= True, fuse_overlap:bool = True, from ..tikz import tikz_to_graph return tikz_to_graph(tikz,warn_overlap, fuse_overlap, ignore_nonzx, cls.backend) - + def is_id(self) -> bool: """Returns whether the graph is just a set of identity wires, @@ -565,7 +565,7 @@ def normalize(self) -> None: if self.num_inputs() == 0: self.auto_detect_io() max_r = self.depth() - 1 - if max_r <= 2: + if max_r <= 2: for o in self.outputs(): self.set_row(o,4) max_r = self.depth() -1 @@ -704,11 +704,14 @@ def add_edge_table(self, etab:Mapping[ET,List[int]]) -> None: elif n2 == 1: new_type = EdgeType.HADAMARD else: new_type = None # self loops are allowed for W nodes. this is a hack to add self-loops using id Z spiders - if v1 == v2 and t1 == VertexType.W_OUTPUT and new_type: + if new_type and vertex_is_w(t1) and vertex_is_w(t2) and \ + (v1 == v2 or v1 == get_w_partner(self, v2)): id_1 = self.add_vertex(VertexType.Z, self.qubit(v1) + 1, self.row(v1) - 0.5) - id_2 = self.add_vertex(VertexType.Z, self.qubit(v1) + 1, self.row(v1) + 0.5) - add[EdgeType.SIMPLE].extend([self.edge(v1, id_1), self.edge(v1, id_2)]) + id_2 = self.add_vertex(VertexType.Z, self.qubit(v2) + 1, self.row(v2) + 0.5) + add[EdgeType.SIMPLE].extend([self.edge(v1, id_1), self.edge(v2, id_2)]) add[new_type].append(self.edge(id_1, id_2)) + new_type = None + conn_type = None # Hence, all the other cases have some kind of parallel edge elif t1 == VertexType.BOUNDARY or t2 == VertexType.BOUNDARY: raise ValueError("Parallel edges to a boundary edge are not supported") From d5f2312194631780934127d646778345719ded61 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Mon, 14 Aug 2023 00:44:13 +0100 Subject: [PATCH 10/20] making mypy happy --- pyzx/graph/base.py | 37 ++++++++++++++++++------------------- pyzx/utils.py | 4 ++-- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/pyzx/graph/base.py b/pyzx/graph/base.py index 556e9f77..f97b7866 100644 --- a/pyzx/graph/base.py +++ b/pyzx/graph/base.py @@ -116,7 +116,7 @@ def stats(self) -> str: def copy(self, adjoint:bool=False, backend:Optional[str]=None) -> 'BaseGraph': """Create a copy of the graph. If ``adjoint`` is set, the adjoint of the graph will be returned (inputs and outputs flipped, phases reversed). - When ``backend`` is set, a copy of the graph with the given backend is produced. + When ``backend`` is set, a copy of the graph with the given backend is produced. By default the copy will have the same backend. Args: @@ -535,9 +535,9 @@ def qubit_count(self) -> int: def auto_detect_io(self): """Adds every vertex that is of boundary-type to the list of inputs or outputs. Whether it is an input or output is determined by looking whether its neighbor - is further to the right or further to the left of the input. + is further to the right or further to the left of the input. Inputs and outputs are sorted by vertical position. - Raises an exception if boundary vertex does not have a unique neighbor + Raises an exception if boundary vertex does not have a unique neighbor or if this neighbor is on the same horizontal position. """ ty = self.types() @@ -641,10 +641,10 @@ def add_vertices(self, amount: int) -> List[VT]: new vertices added to the graph, namely: range(g.vindex() - amount, g.vindex())""" raise NotImplementedError("Not implemented on backend " + type(self).backend) - def add_vertex(self, - ty:VertexType.Type=VertexType.BOUNDARY, - qubit:FloatInt=-1, - row:FloatInt=-1, + def add_vertex(self, + ty:VertexType.Type=VertexType.BOUNDARY, + qubit:FloatInt=-1, + row:FloatInt=-1, phase:Optional[FractionLike]=None, ground:bool=False ) -> VT: @@ -658,7 +658,7 @@ def add_vertex(self, else: phase = 0 self.set_qubit(v, qubit) self.set_row(v, row) - if phase: + if phase: self.set_phase(v, phase) if ground: self.set_ground(v, True) @@ -671,7 +671,7 @@ def add_vertex(self, def add_vertex_indexed(self,v:VT) -> None: """Adds a vertex that is guaranteed to have the chosen index (i.e. 'name'). If the index isn't available, raises a ValueError. - This method is used in the editor and ZXLive to support undo, + This method is used in the editor and ZXLive to support undo, which requires vertices to preserve their index.""" raise NotImplementedError("Not implemented on backend " + type(self).backend) @@ -686,7 +686,7 @@ def add_edge(self, edge: ET, edgetype:EdgeType.Type=EdgeType.SIMPLE) -> None: def add_edge_table(self, etab:Mapping[ET,List[int]]) -> None: """Takes a dictionary mapping (source,target) --> (#edges, #h-edges) specifying that #edges regular edges must be added between source and target and $h-edges Hadamard edges. - The method selectively adds or removes edges to produce that ZX diagram which would + The method selectively adds or removes edges to produce that ZX diagram which would result from adding (#edges, #h-edges), and then removing all parallel edges using Hopf/spider laws.""" add: Dict[EdgeType.Type,List[ET]] = {EdgeType.SIMPLE: [], EdgeType.HADAMARD: []} # list of edges and h-edges to add new_type: Optional[EdgeType.Type] @@ -710,8 +710,7 @@ def add_edge_table(self, etab:Mapping[ET,List[int]]) -> None: id_2 = self.add_vertex(VertexType.Z, self.qubit(v2) + 1, self.row(v2) + 0.5) add[EdgeType.SIMPLE].extend([self.edge(v1, id_1), self.edge(v2, id_2)]) add[new_type].append(self.edge(id_1, id_2)) - new_type = None - conn_type = None + continue # Hence, all the other cases have some kind of parallel edge elif t1 == VertexType.BOUNDARY or t2 == VertexType.BOUNDARY: raise ValueError("Parallel edges to a boundary edge are not supported") @@ -825,9 +824,9 @@ def update_phase_index(self, old:VT, new:VT) -> None: self.phase_index[new] = i def fuse_phases(self, p1: VT, p2: VT) -> None: - if p1 not in self.phase_index or p2 not in self.phase_index: + if p1 not in self.phase_index or p2 not in self.phase_index: return - if self.phase_master is not None: + if self.phase_master is not None: self.phase_master.fuse_phases(self.phase_index[p1],self.phase_index[p2]) self.phase_index[p2] = self.phase_index[p1] @@ -837,7 +836,7 @@ def phase_negate(self, v: VT) -> None: mult = self.phase_mult[index] if mult == 1: self.phase_mult[index] = -1 else: self.phase_mult[index] = 1 - #self.phase_mult[index] = -1*mult + #self.phase_mult[index] = -1*mult def vertex_from_phase_index(self, i: int) -> VT: return list(self.phase_index.keys())[list(self.phase_index.values()).index(i)] @@ -915,12 +914,12 @@ def edges(self) -> Sequence[ET]: raise NotImplementedError("Not implemented on backend " + type(self).backend) def vertex_set(self) -> Set[VT]: - """Returns the vertices of the graph as a Python set. + """Returns the vertices of the graph as a Python set. Should be overloaded if the backend supplies a cheaper version than this.""" return set(self.vertices()) def edge_set(self) -> Set[ET]: - """Returns the edges of the graph as a Python set. + """Returns the edges of the graph as a Python set. Should be overloaded if the backend supplies a cheaper version than this.""" return set(self.edges()) @@ -989,7 +988,7 @@ def add_to_phase(self, vertex: VT, phase: FractionLike) -> None: self.set_phase(vertex,self.phase(vertex)+phase) def qubit(self, vertex: VT) -> FloatInt: - """Returns the qubit index associated to the vertex. + """Returns the qubit index associated to the vertex. If no index has been set, returns -1.""" raise NotImplementedError("Not implemented on backend" + type(self).backend) def qubits(self) -> Mapping[VT,FloatInt]: @@ -1000,7 +999,7 @@ def set_qubit(self, vertex: VT, q: FloatInt) -> None: raise NotImplementedError("Not implemented on backend" + type(self).backend) def row(self, vertex: VT) -> FloatInt: - """Returns the row that the vertex is positioned at. + """Returns the row that the vertex is positioned at. If no row has been set, returns -1.""" raise NotImplementedError("Not implemented on backend" + type(self).backend) def rows(self) -> Mapping[VT, FloatInt]: diff --git a/pyzx/utils.py b/pyzx/utils.py index 93c7119d..38e3273f 100644 --- a/pyzx/utils.py +++ b/pyzx/utils.py @@ -27,7 +27,7 @@ class VertexType: """Type of a vertex in the graph.""" - Type = Literal[0,1,2,3] + Type = Literal[0, 1, 2, 3, 4, 5] BOUNDARY: Final = 0 Z: Final = 1 X: Final = 2 @@ -64,7 +64,7 @@ def get_w_io(g, v): class EdgeType: """Type of an edge in the graph.""" - Type = Literal[1,2] + Type = Literal[1, 2, 3] SIMPLE: Final = 1 HADAMARD: Final = 2 W_IO: Final = 3 From 75ce985d330c764069c2ed79a3f5c7e214e99e71 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 15 Aug 2023 13:13:39 +0100 Subject: [PATCH 11/20] matplotlib drawing for w node --- pyzx/drawing.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyzx/drawing.py b/pyzx/drawing.py index 251801a5..144c5f49 100644 --- a/pyzx/drawing.py +++ b/pyzx/drawing.py @@ -184,7 +184,7 @@ def draw_matplotlib( vs_on_row: Dict[FloatInt, int] = {} # count the vertices on each row for v in g.vertices(): vs_on_row[g.row(v)] = vs_on_row.get(g.row(v), 0) + 1 - + #Dict[VT,Tuple[FloatInt,FloatInt]] layout = {v:(g.row(v),-g.qubit(v)) for v in g.vertices()} @@ -195,18 +195,22 @@ def draw_matplotlib( else: vertices = g.vertices() edges = g.edges() - + for e in edges: sp = layout[g.edge_s(e)] tp = layout[g.edge_t(e)] et = g.edge_type(e) n_row = vs_on_row.get(g.row(g.edge_s(e)), 0) - dx = tp[0] - sp[0] dy = tp[1] - sp[1] bend_wire = (dx == 0) and h_edge_draw == 'blue' and n_row > 2 - ecol = '#0099ff' if h_edge_draw == 'blue' and et == 2 else 'black' + if et == 2 and h_edge_draw == 'blue': + ecol = '#0099ff' + elif et == 3: + ecol = 'gray' + else: + ecol = 'black' if bend_wire: bend = 0.25 @@ -231,7 +235,7 @@ def draw_matplotlib( ax.add_patch(patches.Rectangle(centre,w,h,angle=angle/math.pi*180,facecolor='yellow',edgecolor='black')) #plt.plot([sp[0],tp[0]],[sp[1],tp[1]], 'k', zorder=0, linewidth=0.8) - + for v in vertices: p = layout[v] t = g.type(v) @@ -245,12 +249,16 @@ def draw_matplotlib( elif t == VertexType.H_BOX: ax.add_patch(patches.Rectangle((p[0]-0.1, p[1]-0.1), 0.2, 0.2, facecolor='yellow', edgecolor='black')) a_offset = 0.25 + elif t == VertexType.W_INPUT: + ax.add_patch(patches.Circle(p, 0.05, facecolor='black', edgecolor='black', zorder=1)) + elif t == VertexType.W_OUTPUT: + ax.add_patch(patches.Polygon([(p[0]-0.2, p[1]-0.2), (p[0]+0.2, p[1]-0.2), (p[0], p[1]+0.15)], facecolor='black', edgecolor='black')) else: ax.add_patch(patches.Circle(p, 0.1, facecolor='black', edgecolor='black', zorder=1)) if labels: plt.text(p[0]+0.25, p[1]+0.25, str(v), ha='center', color='gray', fontsize=5) if a: plt.text(p[0], p[1]-a_offset, phase_to_s(a, t), ha='center', color='blue', fontsize=8) - + if show_scalar: x = min((g.row(v) for v in g.vertices()), default = 0) y = -sum((g.qubit(v) for v in g.vertices()))/(g.num_vertices()+1) From 52d9f4d5a0e6311954fa68e374de3d2db171b9c9 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 15 Aug 2023 13:38:09 +0100 Subject: [PATCH 12/20] W support for generate.spider() --- pyzx/generate.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/pyzx/generate.py b/pyzx/generate.py index b2d6dd20..b0840944 100644 --- a/pyzx/generate.py +++ b/pyzx/generate.py @@ -39,7 +39,7 @@ from pyzx.routing.parity_maps import CNOT_tracker, Parity from pyzx.routing.phase_poly import PhasePoly, mat22partition -from .utils import EdgeType, VertexType, FloatInt, FractionLike +from .utils import EdgeType, VertexType, FloatInt, FractionLike, vertex_is_w from .graph import Graph from .graph.base import BaseGraph from .circuit import Circuit @@ -70,7 +70,7 @@ def identity(qubits: int, depth: FloatInt=1,backend:Optional[str]=None) -> BaseG return g def spider( - typ:Union[Literal["Z"],Literal["X"],Literal["H"],VertexType.Type], + typ:Union[Literal["Z"], Literal["X"], Literal["H"], Literal["W"], VertexType.Type], inputs: int, outputs: int, phase:FractionLike=0 @@ -80,10 +80,20 @@ def spider( if typ == "Z": typ = VertexType.Z elif typ == "X": typ = VertexType.X elif typ == "H": typ = VertexType.H_BOX + elif typ == "W": typ = VertexType.W_OUTPUT else: - if not isinstance(typ,int): + if not isinstance(typ, int): raise TypeError("Wrong type for spider type: " + str(typ)) g = Graph() + if vertex_is_w(typ): + if inputs != 1: + raise ValueError("Wrong number of inputs for W node: " + str(inputs)) + v_in = g.add_vertex(VertexType.W_INPUT, (outputs-1)/2, 0.8) + v_out = g.add_vertex(VertexType.W_OUTPUT, (outputs-1)/2, 1) + g.add_edge(g.edge(v_in, v_out), EdgeType.W_IO) + else: + v_in = g.add_vertex(typ, (inputs-1)/2, 1, phase) + v_out = v_in inp = [] outp = [] for i in range(inputs): @@ -92,11 +102,10 @@ def spider( for i in range(outputs): v = g.add_vertex(VertexType.BOUNDARY,i,2) outp.append(v) - v = g.add_vertex(typ,(inputs-1)/2,1,phase) for w in inp: - g.add_edge(g.edge(v,w)) + g.add_edge(g.edge(v_in, w)) for w in outp: - g.add_edge(g.edge(v,w)) + g.add_edge(g.edge(v_out, w)) g.set_inputs(tuple(inp)) g.set_outputs(tuple(outp)) @@ -105,13 +114,13 @@ def spider( def CNOT_HAD_PHASE_circuit( - qubits: int, - depth: int, - p_had: float = 0.2, - p_t: float = 0.2, + qubits: int, + depth: int, + p_had: float = 0.2, + p_t: float = 0.2, clifford:bool=False ) -> Circuit: - """Construct a Circuit consisting of CNOT, HAD and phase gates. + """Construct a Circuit consisting of CNOT, HAD and phase gates. The default phase gate is the T gate, but if ``clifford=True``\ , then this is replaced by the S gate. From 72996651a0fcc2fef0dbcd99160ed664ac0001d5 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Tue, 15 Aug 2023 14:55:12 +0100 Subject: [PATCH 13/20] drawing W node with d3 backend --- pyzx/js/zx_viewer.inline.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pyzx/js/zx_viewer.inline.js b/pyzx/js/zx_viewer.inline.js index 8149e218..9d842d4c 100644 --- a/pyzx/js/zx_viewer.inline.js +++ b/pyzx/js/zx_viewer.inline.js @@ -20,11 +20,14 @@ function nodeColor(t) { else if (t == 1) return "#ccffcc"; else if (t == 2) return "#ff8888"; else if (t == 3) return "yellow"; + else if (t == 4) return "black"; + else if (t == 5) return "black"; } function edgeColor(t) { if (t == 1) return "black"; else if (t == 2) return "#08f"; + else if (t == 3) return "gray"; } function nodeStyle(selected) { @@ -125,10 +128,11 @@ function showGraph(tag, graph, width, height, scale, node_size, auto_hbox, show_ .attr("transform", "translate(0,"+groundOffset+")") .attr("class", "selectable"); - node.filter(function(d) { return d.t != 3; }) + node.filter(function(d) { return d.t != 3 && d.t != 5; }) .append("circle") .attr("r", function(d) { if (d.t == 0) return 0.5 * node_size; + else if (d.t == 4) return 0.25 * node_size; else return node_size; }) .attr("fill", function(d) { return nodeColor(d.t); }) @@ -144,6 +148,14 @@ function showGraph(tag, graph, width, height, scale, node_size, auto_hbox, show_ .attr("stroke", "black") .attr("class", "selectable"); + // draw a triangle for d.t == 5 + node.filter(function(d) { return d.t == 5; }) + .append("path") + .attr("d", "M 0 0 L "+node_size+" "+node_size+" L -"+node_size+" "+node_size+" Z") + .attr("fill", function(d) { return nodeColor(d.t); }) + .attr("stroke", "black") + .attr("class", "selectable"); + node.filter(function(d) { return d.phase != ''; }) .append("text") .attr("y", 0.7 * node_size + 14) From 62eaedb132d7208309636e6de2d9274e0b1ccf55 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Wed, 16 Aug 2023 00:27:45 +0100 Subject: [PATCH 14/20] w to tensor --- pyzx/tensor.py | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/pyzx/tensor.py b/pyzx/tensor.py index 5f414c66..37ec60c3 100644 --- a/pyzx/tensor.py +++ b/pyzx/tensor.py @@ -14,16 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""This module provides methods for converting ZX-graphs into numpy tensors -and using these tensors to test semantic equality of ZX-graphs. -This module is not meant as an efficient quantum simulator. -Due to the way the tensor is calculated it can only handle -circuits of small size before running out of memory on a regular machine. -Currently, it can reliably transform 9 qubit circuits into tensors. -If the ZX-diagram is not circuit-like, but instead has nodes with high degree, +"""This module provides methods for converting ZX-graphs into numpy tensors +and using these tensors to test semantic equality of ZX-graphs. +This module is not meant as an efficient quantum simulator. +Due to the way the tensor is calculated it can only handle +circuits of small size before running out of memory on a regular machine. +Currently, it can reliably transform 9 qubit circuits into tensors. +If the ZX-diagram is not circuit-like, but instead has nodes with high degree, it will run out of memory even sooner.""" -__all__ = ['tensorfy', 'compare_tensors', 'compose_tensors', +__all__ = ['tensorfy', 'compare_tensors', 'compose_tensors', 'adjoint', 'is_unitary','tensor_to_matrix', 'find_scalar_correction'] @@ -58,7 +58,7 @@ def X_to_tensor(arity: int, phase: float) -> np.ndarray: m[()] = 1 + np.exp(1j*phase) return m for i in range(2**arity): - if bin(i).count("1")%2 == 0: + if bin(i).count("1")%2 == 0: m[i] += np.exp(1j*phase) else: m[i] -= np.exp(1j*phase) @@ -69,6 +69,16 @@ def H_to_tensor(arity: int, phase: float) -> np.ndarray: if phase != 0: m[-1] = np.exp(1j*phase) return m.reshape([2]*arity) +def W_to_tensor(arity: int) -> np.ndarray: + m = np.zeros([2]*arity,dtype=complex) + if arity == 0: + return m + for i in range(arity): + index = [0,]*arity + index[i] = 1 + m[tuple(index)] = 1 + return m + def pop_and_shift(verts, indices): res = [] for v in verts: @@ -121,8 +131,7 @@ def tensorfy(g: 'BaseGraph[VT,ET]', preserve_scalar:bool=True) -> np.ndarray: if v in inputs: if types[v] != 0: raise ValueError("Wrong type for input:", v, types[v]) continue # inputs already taken care of - if v in outputs: - #print("output") + if v in outputs: if d != 1: raise ValueError("Weird output") if types[v] != 0: raise ValueError("Wrong type for output:",v, types[v]) d += 1 @@ -135,18 +144,21 @@ def tensorfy(g: 'BaseGraph[VT,ET]', preserve_scalar:bool=True) -> np.ndarray: t = X_to_tensor(d,phase) elif types[v] == 3: t = H_to_tensor(d,phase) + elif types[v] == 4 or types[v] == 5: + if phase != 0: raise ValueError("Phase on W node") + t = W_to_tensor(d) else: raise ValueError("Vertex %s has non-ZXH type but is not an input or output" % str(v)) nn = list(filter(lambda n: rows[n] np.ndarray: for c in range(2**inputs): a = o.copy() a.extend([int(i) for i in bin(c)[2:].zfill(inputs)]) - #print(a) - #print(t[tuple(a)]) row.append(t[tuple(a)]) rows.append(row) return np.array(rows) @@ -211,7 +221,7 @@ def compare_tensors(t1: TensorConvertible,t2: TensorConvertible, preserve_scalar def find_scalar_correction(t1: TensorConvertible, t2:TensorConvertible) -> complex: """Returns the complex number ``z`` such that ``t1 = z*t2``. - + Warning: This function assumes that ``compare_tensors(t1,t2,preserve_scalar=False)`` is True, i.e. that ``t1`` and ``t2`` indeed are equal up to global scalar. @@ -269,7 +279,7 @@ def adjoint(t: np.ndarray) -> np.ndarray: compare_tensors(adjoint(t),tadj) # This is True """ - + q = len(t.shape)//2 transp = [] for i in range(q): From 1938fce14f120237a2f6785d826ca732a103d389 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 17 Aug 2023 20:02:58 +0100 Subject: [PATCH 15/20] minor bug fix --- pyzx/rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyzx/rules.py b/pyzx/rules.py index c1fb27c4..8925ec5c 100644 --- a/pyzx/rules.py +++ b/pyzx/rules.py @@ -254,8 +254,8 @@ def unspider(g: BaseGraph[VT,ET], m: List[Any], qubit:FloatInt=-1, row:FloatInt= MatchWType = Tuple[VT,VT] def match_w_fusion(g: BaseGraph[VT,ET]) -> List[MatchWType[VT]]: - """Does the same as :func:`match_spider_parallel` but with ``num=1``.""" - return match_spider_parallel(g, num=1) + """Does the same as :func:`match_w_fusion_parallel` but with ``num=1``.""" + return match_w_fusion_parallel(g, num=1) def match_w_fusion_parallel( g: BaseGraph[VT,ET], From 3a6859d472a9fbea1043060e7cb6132f68580d16 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 17 Aug 2023 20:03:20 +0100 Subject: [PATCH 16/20] ZXW demo notebook --- demos/ZXW_demo.ipynb | 1095 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1095 insertions(+) create mode 100644 demos/ZXW_demo.ipynb diff --git a/demos/ZXW_demo.ipynb b/demos/ZXW_demo.ipynb new file mode 100644 index 00000000..48f2b57e --- /dev/null +++ b/demos/ZXW_demo.ipynb @@ -0,0 +1,1095 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ZXW demo\n", + "\n", + "This notebook demonstrates the W node and its interactions (only W fusion implemented so far).\n", + "\n", + "To learn more about the ZXW calculus, check out some of these papers:\n", + "* [Completeness for arbitrary finite dimensions of ZXW-calculus, a unifying calculus](https://arxiv.org/abs/2302.12135)\n", + "* [How to Sum and Exponentiate Hamiltonians in ZXW calculus](https://arxiv.org/abs/2212.04462)\n", + "* [Differentiating and Integrating ZX Diagrams with Applications to Quantum Machine Learning](https://arxiv.org/abs/2201.13250)\n", + "* [Light-Matter interaction in the ZXW calculus](https://arxiv.org/abs/2306.02114)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pyzx as zx" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# create the W node using the zx.generate.spider method\n", + "zx.draw(zx.generate.spider(\"W\", 1, 5))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "w_diagram = zx.generate.spider(\"W\", 1, 3) + \\\n", + " (zx.generate.identity(2) @ zx.generate.spider(\"W\", 1, 4)) + \\\n", + " (zx.generate.identity(5) @ zx.generate.spider(\"W\", 1, 2))\n", + "zx.draw(w_diagram)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "W fusion: 1. 1. 2 iterations\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Apply the simplification based on W fusion\n", + "zx.simplify.simp(w_diagram, 'W fusion', zx.rules.match_w_fusion, zx.rules.w_fusion)\n", + "zx.draw(w_diagram)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 9c11e9033f5eb094752928d187c79ddc160446aa Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 17 Aug 2023 20:11:50 +0100 Subject: [PATCH 17/20] drawing orientation of the W node is left to right --- pyzx/drawing.py | 2 +- pyzx/js/zx_viewer.inline.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyzx/drawing.py b/pyzx/drawing.py index 144c5f49..d040c578 100644 --- a/pyzx/drawing.py +++ b/pyzx/drawing.py @@ -252,7 +252,7 @@ def draw_matplotlib( elif t == VertexType.W_INPUT: ax.add_patch(patches.Circle(p, 0.05, facecolor='black', edgecolor='black', zorder=1)) elif t == VertexType.W_OUTPUT: - ax.add_patch(patches.Polygon([(p[0]-0.2, p[1]-0.2), (p[0]+0.2, p[1]-0.2), (p[0], p[1]+0.15)], facecolor='black', edgecolor='black')) + ax.add_patch(patches.Polygon([(p[0]-0.15, p[1]), (p[0]+0.15, p[1]+0.2), (p[0]+0.15, p[1]-0.2)], facecolor='black', edgecolor='black')) else: ax.add_patch(patches.Circle(p, 0.1, facecolor='black', edgecolor='black', zorder=1)) diff --git a/pyzx/js/zx_viewer.inline.js b/pyzx/js/zx_viewer.inline.js index 9d842d4c..594cac2f 100644 --- a/pyzx/js/zx_viewer.inline.js +++ b/pyzx/js/zx_viewer.inline.js @@ -154,7 +154,8 @@ function showGraph(tag, graph, width, height, scale, node_size, auto_hbox, show_ .attr("d", "M 0 0 L "+node_size+" "+node_size+" L -"+node_size+" "+node_size+" Z") .attr("fill", function(d) { return nodeColor(d.t); }) .attr("stroke", "black") - .attr("class", "selectable"); + .attr("class", "selectable") + .attr("transform", "translate(" + (-node_size/2) + ", 0) rotate(-90)"); node.filter(function(d) { return d.phase != ''; }) .append("text") From 8760f9cf287421fdfb91dd4a3d9fd7d618684de2 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Thu, 17 Aug 2023 22:42:20 +0100 Subject: [PATCH 18/20] added an assert in get_w_partner --- pyzx/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyzx/utils.py b/pyzx/utils.py index 38e3273f..36eb4037 100644 --- a/pyzx/utils.py +++ b/pyzx/utils.py @@ -52,6 +52,7 @@ def get_w_partner(g, v): assert vertex_is_w(g.type(v)) for u in g.neighbors(v): if g.edge_type((u, v)) == EdgeType.W_IO: + assert vertex_is_w(g.type(u)) return u assert False From cf8a2a5196d357cf2517dd5d5191557fee663aa2 Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Fri, 18 Aug 2023 00:59:04 +0100 Subject: [PATCH 19/20] added fuse_w in basicrules.py --- pyzx/basicrules.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/pyzx/basicrules.py b/pyzx/basicrules.py index c5d710dd..3abbeea3 100644 --- a/pyzx/basicrules.py +++ b/pyzx/basicrules.py @@ -47,7 +47,8 @@ from typing import Tuple, List from .graph.base import BaseGraph, VT, ET -from .utils import VertexType, EdgeType, is_pauli +from .rules import apply_rule, w_fusion +from .utils import VertexType, EdgeType, get_w_io, is_pauli, vertex_is_w def color_change_diagram(g: BaseGraph[VT,ET]): """Color-change an entire diagram by applying Hadamards to the inputs and ouputs.""" @@ -95,8 +96,8 @@ def check_strong_comp(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: return True def strong_comp(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: - if not check_strong_comp(g, v1, v2): return False - + if not check_strong_comp(g, v1, v2): return False + nhd: Tuple[List[VT],List[VT]] = ([],[]) v = (v1,v2) @@ -121,7 +122,7 @@ def strong_comp(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: g.remove_vertex(v1) g.remove_vertex(v2) - + return True def check_copy_X(g: BaseGraph[VT,ET], v: VT) -> bool: @@ -136,10 +137,10 @@ def check_copy_X(g: BaseGraph[VT,ET], v: VT) -> bool: return True def copy_X(g: BaseGraph[VT,ET], v: VT) -> bool: - if not check_copy_X(g, v): return False + if not check_copy_X(g, v): return False nv = next(iter(g.neighbors(v))) strong_comp(g, v, nv) - + return True def check_pi_commute_Z(g: BaseGraph[VT, ET], v: VT) -> bool: @@ -163,7 +164,7 @@ def pi_commute_Z(g: BaseGraph[VT, ET], v: VT) -> bool: g.add_edge(g.edge(v, c)) g.add_edge(g.edge(c, w), edgetype=et) return True - + def check_pi_commute_X(g: BaseGraph[VT,ET], v: VT) -> bool: color_change_diagram(g) b = check_pi_commute_Z(g, v) @@ -189,24 +190,40 @@ def copy_Z(g: BaseGraph, v: VT) -> bool: return b def check_fuse(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: + if check_fuse_w(g, v1, v2): + return True if not (g.connected(v1,v2) and ((g.type(v1) == VertexType.Z and g.type(v2) == VertexType.Z) or (g.type(v1) == VertexType.X and g.type(v2) == VertexType.X)) and g.edge_type(g.edge(v1,v2)) == EdgeType.SIMPLE): return False - else: - return True + return True def fuse(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: if not check_fuse(g, v1, v2): return False + if vertex_is_w(g.type(v1)): + return fuse_w(g, v1, v2) g.add_to_phase(v1, g.phase(v2)) for v3 in g.neighbors(v2): if v3 != v1: g.add_edge_smart(g.edge(v1,v3), edgetype=g.edge_type(g.edge(v2,v3))) - g.remove_vertex(v2) return True +def check_fuse_w(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: + if vertex_is_w(g.type(v1)) and vertex_is_w(g.type(v2)): + v1_in, v1_out = get_w_io(g, v1) + v2_in, v2_out = get_w_io(g, v2) + if g.edge_type(g.edge(v1_in, v2_out)) == EdgeType.SIMPLE or \ + g.edge_type(g.edge(v2_in, v1_out)) == EdgeType.SIMPLE: + return True + return False + +def fuse_w(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: + if not check_fuse_w(g, v1, v2): return False + apply_rule(g, w_fusion, [(v1, v2)]) + return True + def check_remove_id(g: BaseGraph[VT,ET], v: VT) -> bool: if not (g.vertex_degree(v) == 2 and g.phase(v) == 0): return False @@ -216,13 +233,13 @@ def check_remove_id(g: BaseGraph[VT,ET], v: VT) -> bool: def remove_id(g: BaseGraph[VT,ET], v: VT) -> bool: if not check_remove_id(g, v): return False - + v1, v2 = tuple(g.neighbors(v)) g.add_edge_smart(g.edge(v1,v2), edgetype=EdgeType.SIMPLE if g.edge_type(g.edge(v,v1)) == g.edge_type(g.edge(v,v2)) else EdgeType.HADAMARD) g.remove_vertex(v) - + return True From f3fde527492d9deeaeca41f1bb97ea24f020ce7d Mon Sep 17 00:00:00 2001 From: Razin Shaikh Date: Fri, 18 Aug 2023 01:18:03 +0100 Subject: [PATCH 20/20] forgot to check cases in the previous commit --- pyzx/basicrules.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyzx/basicrules.py b/pyzx/basicrules.py index 3abbeea3..34f225a0 100644 --- a/pyzx/basicrules.py +++ b/pyzx/basicrules.py @@ -221,7 +221,14 @@ def check_fuse_w(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: def fuse_w(g: BaseGraph[VT,ET], v1: VT, v2: VT) -> bool: if not check_fuse_w(g, v1, v2): return False - apply_rule(g, w_fusion, [(v1, v2)]) + v1_in, v1_out = get_w_io(g, v1) + v2_in, v2_out = get_w_io(g, v2) + if g.edge_type(g.edge(v1_out, v2_in)) == EdgeType.SIMPLE: + apply_rule(g, w_fusion, [(v1, v2)]) + else: + g.set_position(v2_in, g.qubit(v1_in), g.row(v1_in)) + g.set_position(v2_out, g.qubit(v1_out), g.row(v1_out)) + apply_rule(g, w_fusion, [(v2, v1)]) return True def check_remove_id(g: BaseGraph[VT,ET], v: VT) -> bool: