Anders and Briegel in Python
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

346 lines
12KB

  1. """
  2. This module implements Anders and Briegel's method for fast simulation of Clifford circuits.
  3. """
  4. import itertools as it
  5. import json, random
  6. import qi, clifford, util
  7. class GraphState(object):
  8. """
  9. This is the main class used to model stabilizer states.
  10. Internally it uses the same dictionary-of-dictionaries data structure as ``networkx``.
  11. """
  12. def __init__(self, nodes=[], deterministic=False):
  13. """ Construct a GraphState.
  14. :param nodes: An iterable of nodes used to construct the graph.
  15. :param deterministic: If ``True``, the behaviour of the graph is deterministic up to but not including the choice of measurement outcome. This is slightly less efficient, but useful for testing. If ``False``, the specific graph representation will sometimes be random -- of course, all possible representations still map to the same state vector.
  16. """
  17. self.adj, self.node = {}, {}
  18. self.add_nodes(nodes)
  19. self.deterministic = deterministic
  20. def add_node(self, node, **kwargs):
  21. """ Add a node
  22. :param node: The name of the node, e.g. ``9``, ``start``
  23. :type node: Any hashable type
  24. """
  25. assert not node in self.node, "Node {} already exists".format(v)
  26. self.adj[node] = {}
  27. self.node[node] = {"vop": clifford.by_name["hadamard"]}
  28. self.node[node].update(kwargs)
  29. def add_nodes(self, nodes):
  30. """ Add a buncha nodes """
  31. for n in nodes:
  32. self.add_node(n)
  33. def act_circuit(self, circuit):
  34. """ Run many gates in one call
  35. :param circuit: An iterable containing tuples of the form ``(node, operation)``.
  36. If ``operation`` is a name for a local operation (e.g. ``6``, ``hadamard``) then that operation is performed on ``node``.
  37. If ``operation`` is ``"cz"`` then a CZ is performed on the two nodes in ``node.
  38. For example::
  39. >>> g.act_circuit([(0, "hadamard"), (1, "hadamard"), ((0, 1), "cz")])
  40. """
  41. for node, operation in circuit:
  42. if operation == "cz":
  43. self.act_cz(*node)
  44. else:
  45. self.act_local_rotation(node, operation)
  46. def _add_edge(self, v1, v2, data={}):
  47. """ Add an edge between two vertices """
  48. self.adj[v1][v2] = data
  49. self.adj[v2][v1] = data
  50. def _del_edge(self, v1, v2):
  51. """ Delete an edge between two vertices """
  52. del self.adj[v1][v2]
  53. del self.adj[v2][v1]
  54. def has_edge(self, v1, v2):
  55. """ Test existence of an edge between two vertices """
  56. return v2 in self.adj[v1]
  57. def _toggle_edge(self, v1, v2):
  58. """ Toggle an edge between two vertices """
  59. if self.has_edge(v1, v2):
  60. self._del_edge(v1, v2)
  61. else:
  62. self._add_edge(v1, v2)
  63. def edgelist(self):
  64. """ Describe a graph as an edgelist # TODO: inefficient """
  65. edges = set(tuple(sorted((i, n)))
  66. for i, v in self.adj.items()
  67. for n in v)
  68. return tuple(edges)
  69. def remove_vop(self, a, avoid):
  70. """ Reduces VOP[a] to the identity """
  71. others = set(self.adj[a]) - {avoid}
  72. if self.deterministic:
  73. swap_qubit = min(others) if others else avoid
  74. else:
  75. swap_qubit = others.pop() if others else avoid
  76. for v in reversed(clifford.decompositions[self.node[a]["vop"]]):
  77. if v == "x":
  78. self.local_complementation(a, "U ->")
  79. else:
  80. self.local_complementation(swap_qubit, "V ->")
  81. def local_complementation(self, v, prefix=""):
  82. """ As defined in LISTING 1 of Anders & Briegel """
  83. for i, j in it.combinations(self.adj[v], 2):
  84. self._toggle_edge(i, j)
  85. self.node[v]["vop"] = clifford.times_table[
  86. self.node[v]["vop"], clifford.by_name["msqx_h"]]
  87. for i in self.adj[v]:
  88. self.node[i]["vop"] = clifford.times_table[
  89. self.node[i]["vop"], clifford.by_name["sqz_h"]]
  90. def act_local_rotation(self, node, operation):
  91. """ Act a local rotation on a qubit
  92. :param node: The index of the node to act on
  93. :param operation: The Clifford-group operation to perform.
  94. """
  95. rotation = clifford.by_name[str(operation)]
  96. self.node[node]["vop"] = clifford.times_table[
  97. rotation, self.node[node]["vop"]]
  98. def _update_vop(self, v, op):
  99. """ Update a VOP - only used internally"""
  100. rotation = clifford.by_name[str(op)]
  101. self.node[v]["vop"] = clifford.times_table[
  102. self.node[v]["vop"], rotation]
  103. def act_hadamard(self, qubit):
  104. """ Shorthand for ``self.act_local_rotation(qubit, "hadamard")`` """
  105. self.act_local_rotation(qubit, 10)
  106. def _lonely(self, a, b):
  107. """ Is this qubit _lonely ? """
  108. return len(self.adj[a]) > (b in self.adj[a])
  109. def act_cz(self, a, b):
  110. """ Act a controlled-phase gate on two qubits
  111. :param a: The first qubit
  112. :param b: The second qubit
  113. """
  114. if self._lonely(a, b):
  115. self.remove_vop(a, b)
  116. if self._lonely(b, a):
  117. self.remove_vop(b, a)
  118. if self._lonely(a, b) and not clifford.is_diagonal(self.node[a]["vop"]):
  119. self.remove_vop(a, b)
  120. edge = self.has_edge(a, b)
  121. va = self.node[a]["vop"]
  122. vb = self.node[b]["vop"]
  123. new_edge, self.node[a]["vop"], self.node[b]["vop"] = \
  124. clifford.cz_table[int(edge), va, vb]
  125. if new_edge != edge:
  126. self._toggle_edge(a, b)
  127. def measure(self, node, basis, force=None):
  128. """ Measure in an arbitrary basis """
  129. basis = clifford.by_name[basis]
  130. ha = clifford.conjugation_table[self.node[node]["vop"]]
  131. basis, phase = clifford.conjugate(basis, ha)
  132. # Flip a coin
  133. result = force if force != None else random.choice([0, 1])
  134. # Flip the result if we have negative phase
  135. if phase == -1:
  136. result = not result
  137. if basis == clifford.by_name["px"]:
  138. result = self._measure_x(node, result)
  139. elif basis == clifford.by_name["py"]:
  140. result = self._measure_y(node, result)
  141. elif basis == clifford.by_name["pz"]:
  142. result = self._measure_z(node, result)
  143. else:
  144. raise ValueError("You can only measure in {X,Y,Z}")
  145. # Flip the result if we have negative phase
  146. if phase == -1:
  147. result = not result
  148. return result
  149. def _toggle_edges(self, a, b):
  150. """ Toggle edges between vertex sets a and b """
  151. # TODO: i'm pretty sure this is just a single-line it.combinations or
  152. # equiv
  153. done = set()
  154. for i, j in it.product(a, b):
  155. if i != j and not (i, j) in done:
  156. done.add((i, j))
  157. done.add((j, i))
  158. self._toggle_edge(i, j)
  159. def _measure_x(self, node, result):
  160. """ Measure the graph in the X-basis """
  161. if len(self.adj[node]) == 0:
  162. return 0
  163. # Pick a vertex
  164. if self.deterministic:
  165. friend = sorted(self.adj[node].keys())[0]
  166. else:
  167. friend = next(self.adj[node].iterkeys())
  168. # Update the VOPs. TODO: pretty ugly
  169. if result:
  170. # Do a z on all ngb(vb) \ ngb(v) \ {v}, and some other stuff
  171. self._update_vop(friend, "msqy")
  172. self._update_vop(node, "pz")
  173. for n in set(self.adj[friend]) - set(self.adj[node]) - {node}:
  174. self._update_vop(n, "pz")
  175. else:
  176. # Do a z on all ngb(v) \ ngb(vb) \ {vb}, and sqy on the friend
  177. self._update_vop(friend, "sqy")
  178. for n in set(self.adj[node]) - set(self.adj[friend]) - {friend}:
  179. self._update_vop(n, "pz")
  180. # Toggle the edges. TODO: Yuk. Just awful!
  181. a = set(self.adj[node].keys())
  182. b = set(self.adj[friend].keys())
  183. self._toggle_edges(a, b)
  184. intersection = a & b
  185. for i, j in it.combinations(intersection, 2):
  186. self._toggle_edge(i, j)
  187. for n in a - {friend}:
  188. self._toggle_edge(friend, n)
  189. return result
  190. def _measure_y(self, node, result):
  191. """ Measure the graph in the Y-basis """
  192. # Do some rotations
  193. for neighbour in self.adj[node]:
  194. self._update_vop(neighbour, "sqz" if result else "msqz")
  195. # A sort of local complementation
  196. vngbh = set(self.adj[node]) | {node}
  197. for i, j in it.combinations(vngbh, 2):
  198. self._toggle_edge(i, j)
  199. self._update_vop(node, 5 if result else 6)
  200. # TODO: naming: # lcoS.herm_adjoint() if result else
  201. # lcoS
  202. return result
  203. def _measure_z(self, node, result):
  204. """ Measure the graph in the Z-basis """
  205. # Disconnect
  206. for neighbour in tuple(self.adj[node]):
  207. self._del_edge(node, neighbour)
  208. if result:
  209. self._update_vop(neighbour, "pz")
  210. # Rotate
  211. if result:
  212. self._update_vop(node, "px")
  213. self._update_vop(node, "hadamard")
  214. else:
  215. self._update_vop(node, "hadamard")
  216. return result
  217. def order(self):
  218. """ Get the number of qubits """
  219. return len(self.node)
  220. def __str__(self):
  221. """ Represent as a string for quick debugging """
  222. s = ""
  223. for key in sorted(self.node.keys()):
  224. s += "{}: {}\t".format(
  225. key, clifford.get_name(self.node[key]["vop"]).replace("YC", "-"))
  226. if self.adj[key]:
  227. s += str(tuple(self.adj[key].keys())).replace(" ", "")
  228. else:
  229. s += "-"
  230. s += "\n"
  231. return s
  232. def to_json(self, stringify=False):
  233. """
  234. Convert the graph to JSON form.
  235. JSON keys must be strings, But sometimes it is useful to have
  236. a JSON-like object whose keys are tuples!
  237. """
  238. if stringify:
  239. node = {str(key): value for key, value in self.node.items()}
  240. adj = {str(key): {str(key): value for key, value in ngbh.items()}
  241. for key, ngbh in self.adj.items()}
  242. return {"node": node, "adj": adj}
  243. else:
  244. return {"node": self.node, "adj": self.adj}
  245. def from_json(self, data):
  246. """ Reconstruct from JSON """
  247. self.__init__([])
  248. # TODO
  249. def to_state_vector(self):
  250. """ Get the full state vector """
  251. if len(self.node) > 15:
  252. raise ValueError("Cannot build state vector: too many qubits")
  253. state = qi.CircuitModel(len(self.node))
  254. for i in range(len(self.node)):
  255. state.act_hadamard(i)
  256. for i, j in self.edgelist():
  257. state.act_cz(i, j)
  258. for i, n in self.node.items():
  259. state.act_local_rotation(i, clifford.unitaries[n["vop"]])
  260. return state
  261. def to_stabilizer(self):
  262. """ Get the stabilizer of this graph """
  263. return
  264. output = {a: {} for a in self.node}
  265. for a, b in it.product(self.node, self.node):
  266. if a == b:
  267. output[a][b] = "X"
  268. elif a in self.adj[b]:
  269. output[a][b] = "Z"
  270. else:
  271. output[a][b] = "I"
  272. # TODO: signs
  273. return output
  274. def __eq__(self, other):
  275. """ Check equality between GraphStates """
  276. return self.adj == other.adj and self.node == other.node
  277. if __name__ == '__main__':
  278. g = GraphState()
  279. g.add_nodes(range(10))
  280. g._add_edge(0, 5)
  281. g.act_local_rotation(6, 10)
  282. print g
  283. print g.to_state_vector()