Skip to content

Commit b8d5db0

Browse files
committed
Merge branch 'feature/symbolic'
2 parents 09864df + 45da234 commit b8d5db0

File tree

13 files changed

+525
-55
lines changed

13 files changed

+525
-55
lines changed

pyzx/graph/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __init__(self) -> None:
9090
# merge_vdata(v0,v1) is an optional, custom function for merging
9191
# vdata of v1 into v0 during spider fusion etc.
9292
self.merge_vdata: Optional[Callable[[VT,VT], None]] = None
93+
self.variable_types: Dict[str,bool] = dict() # mapping of variable names to their type (bool or continuous)
9394

9495
def __str__(self) -> str:
9596
return "Graph({} vertices, {} edges)".format(

pyzx/graph/diff.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
from typing import Any, Callable, Generic, Optional, List, Dict, Tuple
1919
import copy
2020

21-
from ..utils import VertexType, EdgeType, FractionLike, FloatInt
21+
from ..utils import VertexType, EdgeType, FractionLike, FloatInt, phase_to_s
2222
from .base import BaseGraph, VT, ET
2323
from .graph_s import GraphS
24-
from .jsonparser import ComplexDecoder, ComplexEncoder, _phase_to_quanto_value, _quanto_value_to_phase
24+
from .jsonparser import ComplexDecoder, ComplexEncoder, string_to_phase
2525

2626
class GraphDiff(Generic[VT, ET]):
2727
removed_verts: List[VT]
@@ -33,6 +33,7 @@ class GraphDiff(Generic[VT, ET]):
3333
changed_phases: Dict[VT, FractionLike]
3434
changed_pos: Dict[VT, Tuple[FloatInt,FloatInt]]
3535
changed_vdata: Dict[VT, Any]
36+
variable_types: Dict[str,bool]
3637

3738
def __init__(self, g1: BaseGraph[VT,ET], g2: BaseGraph[VT,ET]) -> None:
3839
self.calculate_diff(g1,g2)
@@ -43,6 +44,8 @@ def calculate_diff(self, g1: BaseGraph[VT,ET], g2: BaseGraph[VT,ET]) -> None:
4344
self.changed_phases = {}
4445
self.changed_pos = {}
4546
self.changed_vdata = {}
47+
self.variable_types = g1.variable_types.copy()
48+
self.variable_types.update(g2.variable_types)
4649

4750
old_verts = g1.vertex_set()
4851
new_verts = g2.vertex_set()
@@ -132,7 +135,7 @@ def to_json(self) -> str:
132135
changed_edge_types_str_dict = {}
133136
for key, value in self.changed_edge_types.items():
134137
changed_edge_types_str_dict[f"{key[0]},{key[1]}"] = value # type: ignore
135-
changed_phases_str = {k: _phase_to_quanto_value(v) for k, v in self.changed_phases.items()}
138+
changed_phases_str = {k: phase_to_s(v) for k, v in self.changed_phases.items()}
136139
return json.dumps({
137140
"removed_verts": self.removed_verts,
138141
"new_verts": self.new_verts,
@@ -143,6 +146,7 @@ def to_json(self) -> str:
143146
"changed_phases": changed_phases_str,
144147
"changed_pos": self.changed_pos,
145148
"changed_vdata": self.changed_vdata,
149+
"variable_types": self.variable_types
146150
}, cls=ComplexEncoder)
147151

148152
@staticmethod
@@ -155,7 +159,7 @@ def from_json(json_str: str) -> "GraphDiff":
155159
gd.new_edges = list(map(tuple, d["new_edges"])) # type: ignore
156160
gd.changed_vertex_types = map_dict_keys(d["changed_vertex_types"], int)
157161
gd.changed_edge_types = map_dict_keys(d["changed_edge_types"], lambda x: tuple(map(int, x.split(","))))
158-
gd.changed_phases = {int(k): _quanto_value_to_phase(v) for k, v in d["changed_phases"].items()}
162+
gd.changed_phases = {int(k): string_to_phase(v,gd) for k, v in d["changed_phases"].items()}
159163
gd.changed_pos = map_dict_keys(d["changed_pos"], int)
160164
gd.changed_vdata = map_dict_keys(d["changed_vdata"], int)
161165
return gd

pyzx/graph/jsonparser.py

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,60 @@
1515
# limitations under the License.
1616

1717
import json
18+
import re
1819
from fractions import Fraction
19-
from typing import List, Dict, Any, Optional
20+
from typing import List, Dict, Any, Optional, Callable, Union, TYPE_CHECKING
2021

21-
from ..utils import FractionLike, EdgeType, VertexType
22+
from ..utils import FractionLike, EdgeType, VertexType, phase_to_s
2223
from .graph import Graph
2324
from .scalar import Scalar
2425
from .base import BaseGraph, VT, ET
2526
from ..simplify import id_simp
26-
27-
def _quanto_value_to_phase(s: str) -> Fraction:
28-
if not s: return Fraction(0)
29-
if r'\pi' in s:
27+
from ..symbolic import parse, Poly, new_var
28+
if TYPE_CHECKING:
29+
from .diff import GraphDiff
30+
31+
32+
#def _phase_to_quanto_value(p: FractionLike) -> str:
33+
# if not p: return ""
34+
# try:
35+
# p = Fraction(p)
36+
# if p.numerator == -1: v = "-"
37+
# elif p.numerator == 1: v = ""
38+
# else: v = str(p.numerator)
39+
# d = "/"+str(p.denominator) if p.denominator!=1 else ""
40+
# return r"{}\pi{}".format(v,d)
41+
# except TypeError:
42+
43+
44+
def string_to_phase(string: str, g: Union[BaseGraph,'GraphDiff']) -> Union[Fraction, Poly]:
45+
if not string:
46+
return Fraction(0)
47+
try:
48+
s = string.lower().replace(' ', '')
49+
s = re.sub(r'\\?(pi|\u04c0)', '', s)
50+
if s == '': return Fraction(1)
51+
if s == '-': return Fraction(-1)
52+
if '.' in s or 'e' in s:
53+
return Fraction(float(s))
54+
elif '/' in s:
55+
a, b = s.split("/", 2)
56+
if not a:
57+
return Fraction(1, int(b))
58+
if a == '-':
59+
a = '-1'
60+
return Fraction(int(a), int(b))
61+
else:
62+
return Fraction(int(s))
63+
except ValueError:
64+
def _new_var(name: str) -> Poly:
65+
if name not in g.variable_types:
66+
g.variable_types[name] = False
67+
return new_var(name, g.variable_types)
3068
try:
31-
r = s.replace(r'\pi','').strip()
32-
if r.startswith('-'): r = "-1"+r[1:]
33-
if r.startswith('/'): r = "1"+r
34-
return Fraction(str(r)) if r else Fraction(1)
35-
except ValueError:
36-
raise ValueError("Invalid phase '{}'".format(s))
37-
return Fraction(s)
38-
39-
def _phase_to_quanto_value(p: FractionLike) -> str:
40-
if not p: return ""
41-
p = Fraction(p)
42-
if p.numerator == -1: v = "-"
43-
elif p.numerator == 1: v = ""
44-
else: v = str(p.numerator)
45-
d = "/"+str(p.denominator) if p.denominator!=1 else ""
46-
return r"{}\pi{}".format(v,d)
47-
69+
return parse(string, _new_var)
70+
except Exception as e:
71+
raise ValueError(e)
4872

4973
def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph:
5074
"""Converts the json representation of a .qgraph Quantomatic graph into
@@ -76,7 +100,7 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph:
76100
elif d['type'] == 'Z_box': g.set_type(v,VertexType.Z_BOX)
77101
else: raise TypeError("unsupported type '{}'".format(d['type']))
78102
if 'value' in d:
79-
g.set_phase(v,_quanto_value_to_phase(d['value']))
103+
g.set_phase(v,string_to_phase(d['value'],g))
80104
else:
81105
g.set_phase(v,Fraction(0,1))
82106
if d.get('ground', False):
@@ -89,7 +113,7 @@ def json_to_graph(js: str, backend:Optional[str]=None) -> BaseGraph:
89113
continue
90114
elif key == 'label':
91115
if type(value) != complex:
92-
value = _quanto_value_to_phase(value)
116+
value = string_to_phase(value,g)
93117
g.set_vdata(v, key, value)
94118

95119
inputs = {}
@@ -207,10 +231,10 @@ def graph_to_json(g: BaseGraph[VT,ET], include_scalar: bool=True) -> str:
207231
node_vs[name]["data"]["type"] = "Z_box"
208232
zbox_label = g.vdata(v, 'label', 1)
209233
if type(zbox_label) == Fraction:
210-
zbox_label = _phase_to_quanto_value(zbox_label)
234+
zbox_label = phase_to_s(zbox_label)
211235
node_vs[name]["annotation"]["label"] = zbox_label
212236
else: raise Exception("Unkown vertex type "+ str(t))
213-
phase = _phase_to_quanto_value(g.phase(v))
237+
phase = phase_to_s(g.phase(v))
214238
if phase: node_vs[name]["data"]["value"] = phase
215239
if g.is_ground(v):
216240
node_vs[name]["data"]["ground"] = True

pyzx/graph/scalar.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import json
2525

2626
from ..utils import FloatInt, FractionLike
27+
from ..symbolic import Poly
2728

2829
__all__ = ['Scalar']
2930

@@ -53,7 +54,7 @@ class Scalar(object):
5354
"""Represents a global scalar for a Graph instance."""
5455
def __init__(self) -> None:
5556
self.power2: int = 0 # Stores power of square root of two
56-
self.phase: Fraction = Fraction(0) # Stores complex phase of the number
57+
self.phase: FractionLike = Fraction(0) # Stores complex phase of the number
5758
self.phasenodes: List[FractionLike] = [] # Stores list of legless spiders, by their phases.
5859
self.floatfactor: complex = 1.0
5960
self.is_unknown: bool = False # Whether this represents an unknown scalar value
@@ -114,7 +115,10 @@ def to_latex(self) -> str:
114115
if self.power2 != 0:
115116
s += r"\sqrt{{2}}^{{{:d}}}".format(self.power2)
116117
if self.phase not in (0,1):
117-
s += r"\exp(i~\frac{{{:d}\pi}}{{{:d}}})".format(self.phase.numerator,self.phase.denominator)
118+
if isinstance(self.phase, Poly):
119+
s += fr"\exp(i~{str(self.phase)})".format(str(self.phase))
120+
else:
121+
s += r"\exp(i~\frac{{{:d}\pi}}{{{:d}}})".format(self.phase.numerator,self.phase.denominator)
118122
s += "$"
119123
if s == "$$": return ""
120124
return s
@@ -127,6 +131,8 @@ def to_unicode(self) -> str:
127131
f = self.floatfactor
128132
for node in self.phasenodes:
129133
f *= 1+cexp(node)
134+
if isinstance(self.phase, Poly):
135+
raise NotImplementedError("Unicode representation of Poly not implemented")
130136
phase = Fraction(self.phase)
131137
if self.phase >= 1:
132138
f *= -1
@@ -218,6 +224,9 @@ def add_spider_pair(self, p1: FractionLike,p2: FractionLike) -> None:
218224
self.add_power(1)
219225
self.add_phase(p2)
220226
return
227+
if isinstance(p1, Poly) or isinstance(p2, Poly):
228+
self.set_unknown()
229+
return
221230
if p2.denominator == 2:
222231
p1, p2 = p2, p1
223232
if p1 == Fraction(1,2):

pyzx/io.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .graph import Graph, EdgeType, VertexType
2323
from .graph.base import BaseGraph, VT, ET
2424
from .simplify import id_simp
25+
from .symbolic import Poly
2526

2627
__all__ = ['json_to_graph', 'graph_to_json', 'to_graphml']
2728

@@ -39,6 +40,8 @@ def _quanto_value_to_phase(s: str) -> Fraction:
3940

4041
def _phase_to_quanto_value(p: FractionLike) -> str:
4142
if not p: return ""
43+
if isinstance(p, Poly):
44+
raise ValueError("Symbolic phases not supported")
4245
p = Fraction(p)
4346
if p.numerator == -1: v = "-"
4447
elif p.numerator == 1: v = ""

pyzx/mbqc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from . import Graph
22
from .graph.base import BaseGraph
3+
from .symbolic import Poly
34
from .utils import VertexType, EdgeType, FractionLike
45

56
from fractions import Fraction
@@ -35,6 +36,8 @@ def measure(g:BaseGraph, pos:Tuple[int,int], t:VertexType.Type=VertexType.Z, pha
3536
q = 2*pos[0]-0.8
3637
r = 2*pos[1]+0.8
3738
found = False
39+
if isinstance(phase, Poly):
40+
raise ValueError("Symbolic phases not supported")
3841
if not isinstance(phase, Fraction): phase = Fraction(phase)
3942
outputs = list(g.outputs())
4043
for v in g.vertices():

pyzx/quimb.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from .utils import EdgeType, VertexType
3232
from .graph.base import BaseGraph
3333
from .simplify import to_gh
34+
from .symbolic import Poly
3435

3536

3637
def to_quimb_tensor(g: BaseGraph) -> "qtn.TensorNetwork": # type:ignore
@@ -56,7 +57,10 @@ def to_quimb_tensor(g: BaseGraph) -> "qtn.TensorNetwork": # type:ignore
5657
# Here we have phase tensors corresponding to Z-spiders with only one output and no input.
5758
for v in g.vertices():
5859
if g.type(v) == VertexType.Z and g.phase(v) != 0:
59-
tensors.append(qtn.Tensor(data = [1, np.exp(1j * np.pi * g.phase(v))],
60+
phase = g.phase(v)
61+
if isinstance(phase, Poly):
62+
raise NotImplementedError("Quimb does not support symbolic phases")
63+
tensors.append(qtn.Tensor(data = [1, np.exp(1j * np.pi * phase)],
6064
inds = (f'{v}',),
6165
tags = ("V",)))
6266

@@ -74,8 +78,13 @@ def to_quimb_tensor(g: BaseGraph) -> "qtn.TensorNetwork": # type:ignore
7478
# In particular, it doesn't check g.scalar.phasenodes
7579
# TODO: This will give the wrong tensor when g.scalar.is_zero == True.
7680
# Grab the float factor and exponent from the scalar
77-
scalar_float = np.exp(1j * np.pi * g.scalar.phase) * g.scalar.floatfactor
81+
phase = g.scalar.phase
82+
if isinstance(phase, Poly):
83+
raise NotImplementedError("Quimb does not support symbolic phases")
84+
scalar_float = np.exp(1j * np.pi * phase) * g.scalar.floatfactor
7885
for node in g.scalar.phasenodes: # Each node is a Fraction
86+
if isinstance(node, Poly):
87+
raise NotImplementedError("Quimb does not support symbolic phases")
7988
scalar_float *= 1 + np.exp(1j * np.pi * node)
8089
scalar_exp = math.log10(math.sqrt(2)) * g.scalar.power2
8190

pyzx/rules.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757

5858
from .utils import VertexType, EdgeType, get_w_partner, get_z_box_label, set_z_box_label, toggle_edge, vertex_is_w, vertex_is_zx, FloatInt, FractionLike, get_w_io, vertex_is_z_like
5959
from .graph.base import BaseGraph, VT, ET
60+
from .symbolic import Poly
6061

6162
RewriteOutputType = Tuple[Dict[ET,List[int]], List[VT], List[ET], bool]
6263
MatchObject = TypeVar('MatchObject')
@@ -273,9 +274,10 @@ def match_z_to_z_box_parallel(
273274
if matchf is not None: candidates = set([v for v in g.vertices() if matchf(v)])
274275
else: candidates = g.vertex_set()
275276
types = g.types()
277+
phases = g.phases()
276278
m = []
277279
for v in candidates:
278-
if types[v] == VertexType.Z:
280+
if types[v] == VertexType.Z and not isinstance(phases[v],Poly):
279281
if num == 0: break
280282
m.append(v)
281283
num -= 1
@@ -285,7 +287,9 @@ def z_to_z_box(g: BaseGraph[VT,ET], matches: List[VT]) -> RewriteOutputType[ET,V
285287
"""Converts a Z vertex to a Z-box."""
286288
for v in matches:
287289
g.set_type(v, VertexType.Z_BOX)
288-
label = np.round(np.e**(1j * np.pi * g.phase(v)), 8)
290+
phase = g.phase(v)
291+
assert not isinstance(phase, Poly)
292+
label = np.round(np.e**(1j * np.pi * phase), 8)
289293
set_z_box_label(g, v, label)
290294
g.set_phase(v, 0)
291295
return ({}, [], [], True)
@@ -729,6 +733,7 @@ def lcomp(g: BaseGraph[VT,ET], matches: List[MatchLcompType[VT]]) -> RewriteOutp
729733
for m in matches:
730734
a = g.phase(m[0])
731735
rem.append(m[0])
736+
assert isinstance(a,Fraction) # For mypy
732737
if a.numerator == 1: g.scalar.add_phase(Fraction(1,4))
733738
else: g.scalar.add_phase(Fraction(7,4))
734739
n = len(m[1])
@@ -824,10 +829,7 @@ def match_phase_gadgets(g: BaseGraph[VT,ET],vertexf:Optional[Callable[[VT],bool]
824829
# First we find all the phase-gadgets, and the list of vertices they act on
825830
for v in candidates:
826831
non_clifford = phases[v] != 0 and getattr(phases[v], 'denominator', 1) > 2
827-
try: # symbol check
828-
float(phases[v])
829-
except:
830-
non_clifford = True
832+
if isinstance(phases[v], Poly): non_clifford = True
831833
if non_clifford and len(list(g.neighbors(v)))==1:
832834
n = list(g.neighbors(v))[0]
833835
if phases[n] not in (0,1): continue # Not a real phase gadget (happens for scalar diagrams)

pyzx/simulate.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
sq2 = math.sqrt(2)
2525
omega = (1+1j)/sq2
2626
from fractions import Fraction
27-
import itertools
2827
from typing import List, Optional, Dict, Tuple, Any
2928

3029
import numpy as np
@@ -34,6 +33,7 @@
3433
from .circuit import Circuit
3534
from .graph import Graph
3635
from .graph.base import BaseGraph,VT,ET
36+
from .symbolic import Poly
3737

3838
MAGIC_GLOBAL = -(7+5*sq2)/(2+2j)
3939
MAGIC_B60 = -16 + 12*sq2
@@ -241,13 +241,22 @@ def calculate_path_sum(g: BaseGraph[VT,ET]) -> complex:
241241
if t == v: continue
242242
if t not in variable_dict:
243243
variable_dict[t] = len(variables)
244-
variables.append(int(float(phases[t]*4)))
244+
phase_t = phases[t]
245+
if isinstance(phase_t, Poly):
246+
raise NotImplementedError("Symbolic phases not supported")
247+
variables.append(int(float(phase_t*4)))
245248
targets.add(variable_dict[t])
246249
prefactor += len(targets)-1
247-
xors[frozenset(targets)] = int(float(phases[v]*4))
250+
phase_v = phases[v]
251+
if isinstance(phase_v, Poly):
252+
raise NotImplementedError("Symbolic phases not supported")
253+
xors[frozenset(targets)] = int(float(phase_v*4))
248254
continue
249255
variable_dict[v] = len(variables)
250-
variables.append(int(float(phases[v]*4)))
256+
phase_v = phases[v]
257+
if isinstance(phase_v, Poly):
258+
raise NotImplementedError("Symbolic phases not supported")
259+
variables.append(int(float(phase_v*4)))
251260
verts = sorted(list(variable_dict.keys()), key=lambda x: variable_dict[x])
252261
n = len(verts)
253262
for i in range(n):

0 commit comments

Comments
 (0)