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.

494 lines
18KB

  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. """
  4. This module implements Anders and Briegel's method for fast simulation of Clifford circuits.
  5. """
  6. import itertools as it
  7. import json, random
  8. import qi, clifford, util
  9. from stabilizer import Stabilizer
  10. class GraphState(object):
  11. """
  12. This is the main class used to model stabilizer states.
  13. Internally it uses the same dictionary-of-dictionaries data structure as ``networkx``.
  14. """
  15. def __init__(self, data=(), deterministic=False, vop="identity"):
  16. """ Construct a ``GraphState``
  17. :param data: An iterable of nodes used to construct the graph, or an integer -- the number of nodes, or a ``nx.Graph``.
  18. :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.
  19. :param vop: The default VOP for new qubits. Setting ``vop="identity"`` initializes qubits in :math:`|+\\rangle`. Setting ``vop="hadamard"`` initializes qubits in :math:`|0\\rangle`.
  20. """
  21. self.deterministic = deterministic
  22. self.adj, self.node = {}, {}
  23. try:
  24. # Cloning from a networkx graph
  25. self.adj = data.adj.copy()
  26. self.node = data.node.copy()
  27. for key, value in self.node.items():
  28. self.node[key]["vop"] = data.node[key].get("vop", clifford.identity)
  29. except AttributeError:
  30. try:
  31. # Provided with a list of node names?
  32. for n in data:
  33. self._add_node(n, vop=vop)
  34. except TypeError:
  35. # Provided with an integer?
  36. for n in range(data):
  37. self._add_node(n, vop=vop)
  38. def add_node(self, *args, **kwargs):
  39. """ Add a node """
  40. self._add_node(self, *args, **kwargs)
  41. def _del_node(self, node):
  42. """ Remove a node. TODO: this is a hack right now! """
  43. if not node in self.node:
  44. return
  45. del self.node[node]
  46. for k in self.adj[node]:
  47. del self.adj[k][node]
  48. del self.adj[node]
  49. def _add_node(self, node, **kwargs):
  50. """ Add a node. By default, nodes are initialized with ``vop=``:math:`I`, i.e. they are in the :math:`|+\\rangle` state.
  51. :param node: The name of the node, e.g. ``9``, ``start``
  52. :type node: Any hashable type
  53. :param kwargs: Any extra node attributes
  54. Example of using node attributes ::
  55. >>> g.add_node(0, label="fred", position=(1,2,3))
  56. >>> g.node[0]["label"]
  57. fred
  58. """
  59. if node in self.node:
  60. print "Warning: node {} already exists".format(node)
  61. return
  62. default = kwargs.get("default", "identity")
  63. self.adj[node] = {}
  64. self.node[node] = {}
  65. kwargs["vop"] = clifford.by_name[str(kwargs.get("vop", "identity"))] #TODO: ugly
  66. self.node[node].update(kwargs)
  67. def add_qubit(self, name, **kwargs):
  68. """ Add a qubit to the state.
  69. :param name: The name of the node, e.g. ``9``, ``start``.
  70. :type name: Any hashable type
  71. :param kwargs: Any extra node attributes
  72. By default, qubits are initialized in the :math:`|0\\rangle` state. Provide the optional ``vop`` argument to set the initial state.
  73. """
  74. kwargs["vop"] = clifford.by_name[str(kwargs.get("vop", "hadamard"))] #TODO: ugly
  75. self._add_node(name, **kwargs)
  76. def act_circuit(self, circuit):
  77. """ Run many gates in one call.
  78. :param circuit: An iterable containing tuples of the form ``(node, operation)``. If ``operation`` is a name for a local operation (e.g. ``6``, ``hadamard``) then that operation is performed on ``node``. If ``operation`` is ``cz`` then a CZ is performed on the two nodes in ``node``.
  79. Example (makes a Bell pair)::
  80. >>> g.act_circuit([(0, "hadamard"), (1, "hadamard"), ((0, 1), "cz")])
  81. """
  82. for node, operation in circuit:
  83. if operation == "cz":
  84. self.act_cz(*node)
  85. else:
  86. self.act_local_rotation(node, operation)
  87. def _add_edge(self, v1, v2, data={}):
  88. """ Add an edge between two vertices """
  89. self.adj[v1][v2] = data
  90. self.adj[v2][v1] = data
  91. def _del_edge(self, v1, v2):
  92. """ Delete an edge between two vertices """
  93. del self.adj[v1][v2]
  94. del self.adj[v2][v1]
  95. def has_edge(self, v1, v2):
  96. """ Test existence of an edge between two vertices """
  97. return v2 in self.adj[v1]
  98. def _toggle_edge(self, v1, v2):
  99. """ Toggle an edge between two vertices """
  100. if self.has_edge(v1, v2):
  101. self._del_edge(v1, v2)
  102. else:
  103. self._add_edge(v1, v2)
  104. def edgelist(self):
  105. """ Describe a graph as an edgelist # TODO: inefficient """
  106. edges = set(tuple(sorted((i, n)))
  107. for i, v in self.adj.items()
  108. for n in v)
  109. return tuple(edges)
  110. def remove_vop(self, node, avoid):
  111. """ Attempts to remove the vertex operator on a particular qubit.
  112. :param node: The node whose vertex operator should be reduced to the identity.
  113. :param avoid: We will try to leave this node alone during the process (if possible).
  114. """
  115. others = set(self.adj[node]) - {avoid}
  116. if self.deterministic:
  117. swap_qubit = min(others) if others else avoid
  118. else:
  119. swap_qubit = others.pop() if others else avoid
  120. for v in reversed(clifford.decompositions[self.node[node]["vop"]]):
  121. if v == "x":
  122. self.local_complementation(node)
  123. else:
  124. self.local_complementation(swap_qubit)
  125. def local_complementation(self, v):
  126. """ As defined in LISTING 1 of Anders & Briegel """
  127. for i, j in it.combinations(self.adj[v], 2):
  128. self._toggle_edge(i, j)
  129. self.node[v]["vop"] = clifford.times_table[
  130. self.node[v]["vop"], clifford.msqx_h]
  131. for i in self.adj[v]:
  132. self.node[i]["vop"] = clifford.times_table[
  133. self.node[i]["vop"], clifford.sqz_h]
  134. def act_local_rotation(self, node, operation):
  135. """ Act a local rotation on a qubit
  136. :param node: The index of the node to act on
  137. :param operation: The Clifford-group operation to perform. You can use any of the names in the :ref:`Clifford group alias table <clifford>`.
  138. """
  139. rotation = clifford.by_name[str(operation)]
  140. self.node[node]["vop"] = clifford.times_table[
  141. rotation, self.node[node]["vop"]]
  142. def _update_vop(self, v, op):
  143. """ Update a VOP - only used internally"""
  144. rotation = clifford.by_name[str(op)]
  145. self.node[v]["vop"] = clifford.times_table[
  146. self.node[v]["vop"], rotation]
  147. def act_hadamard(self, qubit):
  148. """ Shorthand for ``self.act_local_rotation(qubit, "hadamard")`` """
  149. self.act_local_rotation(qubit, 10)
  150. def _lonely(self, a, b):
  151. """ Is this qubit _lonely ? """
  152. return len(self.adj[a]) > (b in self.adj[a])
  153. def act_cz(self, a, b):
  154. """ Act a controlled-phase gate on two qubits
  155. :param a: The first qubit
  156. :param b: The second qubit
  157. """
  158. if self._lonely(a, b):
  159. self.remove_vop(a, b)
  160. if self._lonely(b, a):
  161. self.remove_vop(b, a)
  162. if self._lonely(a, b) and not clifford.is_diagonal(self.node[a]["vop"]):
  163. self.remove_vop(a, b)
  164. edge = self.has_edge(a, b)
  165. va = self.node[a]["vop"]
  166. vb = self.node[b]["vop"]
  167. new_edge, self.node[a]["vop"], self.node[b]["vop"] = \
  168. clifford.cz_table[int(edge), va, vb]
  169. if new_edge != edge:
  170. self._toggle_edge(a, b)
  171. def measure(self, node, basis, force=None, detail=False, friend=None):
  172. """ Measure in an arbitrary basis
  173. :param node: The name of the qubit to measure.
  174. :param basis: The basis in which to measure.
  175. :param friend: Specify a node to toggle about when performing an :math:`X` measurement.
  176. :type friend: Any neighbour of ``node``.
  177. :type basis: :math:`\in` ``{"px", "py", "pz"}``
  178. :param force: Forces the measurement outcome.
  179. :type force: boolean
  180. :param detail: Get detailed information.
  181. :type detail: boolean
  182. Measurements in quantum mechanics are probabilistic. If you want to force a particular outcome :math:`\in\{0, 1\}`, use ``force``.
  183. You can get more information by setting ``detail=True``, in which case ``measure()`` returns a dictionary with the following keys:
  184. - ``outcome``: the measurement outcome.
  185. - ``determinate``: indicates whether the outcome was determinate or random. For example, measuring :math:`|0\\rangle` in :math:`\sigma_x` always gives a deterministic outcome. ``determinate`` is overridden by ``force`` -- forced outcomes are always determinate.
  186. - ``conjugated_basis``: The index of the measurement operator, rotated by the vertex operator of the measured node, i.e. :math:`U_\\text{vop} \sigma_m U_\\text{vop}^\dagger`.
  187. - ``phase``: The phase of the cojugated basis, :math:`\pm 1`.
  188. - ``node``: The name of the measured node.
  189. - ``force``: The value of ``force``.
  190. """
  191. basis = clifford.by_name[basis]
  192. ha = clifford.conjugation_table[self.node[node]["vop"]]
  193. basis, phase = clifford.conjugate(basis, ha)
  194. # Flip a coin
  195. result = force if force != None else random.choice([0, 1])
  196. # Flip the result if we have negative phase
  197. if phase == -1:
  198. result = not result
  199. if basis == clifford.px:
  200. result, determinate = self._measure_graph_x(node, result, friend)
  201. elif basis == clifford.py:
  202. result, determinate = self._measure_graph_y(node, result)
  203. elif basis == clifford.pz:
  204. result, determinate = self._measure_graph_z(node, result)
  205. else:
  206. raise ValueError("You can only measure in {X,Y,Z}")
  207. # Flip the result if we have negative phase
  208. if phase == -1:
  209. result = not result
  210. if detail:
  211. return {"outcome": int(result),
  212. "determinate": (determinate or force!=None),
  213. "conjugated_basis": basis,
  214. "phase": phase,
  215. "node": node,
  216. "force": force}
  217. else:
  218. return int(result)
  219. def measure_x(self, node, force=None, detail=False, friend=None):
  220. """ Measure in the X basis
  221. :param node: The name of the qubit to measure.
  222. :param force: Measurements in quantum mechanics are probabilistic. If you want to force a particular outcome, use the ``force``.
  223. :type force: boolean
  224. :param detail: Provide detailed information
  225. :type detail: boolean
  226. :param friend: Specify a node to toggle about
  227. :type friend: Any neighbour of ``node``.
  228. """
  229. return self.measure(node, "px", force, detail, friend)
  230. def measure_y(self, node, force=None, detail=False):
  231. """ Measure in the Y basis
  232. :param node: The name of the qubit to measure.
  233. :param force: Measurements in quantum mechanics are probabilistic. If you want to force a particular outcome, use the ``force``.
  234. :type force: boolean
  235. :param detail: Provide detailed information
  236. :type detail: boolean
  237. """
  238. return self.measure(node, "py", force, detail)
  239. def measure_z(self, node, force=None, detail=False):
  240. """ Measure in the Z basis
  241. :param node: The name of the qubit to measure.
  242. :param force: Measurements in quantum mechanics are probabilistic. If you want to force a particular outcome, use the ``force``.
  243. :type force: boolean
  244. :param detail: Provide detailed information
  245. :type detail: boolean
  246. """
  247. return self.measure(node, "pz", force, detail)
  248. def _toggle_edges(self, a, b):
  249. """ Toggle edges between vertex sets a and b """
  250. # TODO: i'm pretty sure this is just a single-line it.combinations or
  251. # equiv
  252. done = set()
  253. for i, j in it.product(a, b):
  254. if i != j and not (i, j) in done:
  255. done.add((i, j))
  256. done.add((j, i))
  257. self._toggle_edge(i, j)
  258. def _measure_graph_x(self, node, result, friend=None):
  259. """ Measure the bare graph in the X-basis """
  260. if len(self.adj[node]) == 0:
  261. return 0, True
  262. # Pick a friend vertex
  263. if friend == None:
  264. if self.deterministic:
  265. friend = sorted(self.adj[node].keys())[0]
  266. else:
  267. friend = next(self.adj[node].iterkeys())
  268. else:
  269. assert friend in self.adj[node].keys() # TODO: unnecessary assert
  270. # Update the VOPs. TODO: pretty ugly
  271. if result:
  272. # Do a z on all ngb(vb) \ ngb(v) \ {v}, and some other stuff
  273. self._update_vop(friend, "msqy")
  274. self._update_vop(node, "pz")
  275. for n in set(self.adj[friend]) - set(self.adj[node]) - {node}:
  276. self._update_vop(n, "pz")
  277. else:
  278. # Do a z on all ngb(v) \ ngb(vb) \ {vb}, and sqy on the friend
  279. self._update_vop(friend, "sqy")
  280. for n in set(self.adj[node]) - set(self.adj[friend]) - {friend}:
  281. self._update_vop(n, "pz")
  282. # Toggle the edges. TODO: Yuk. Just awful!
  283. a = set(self.adj[node].keys())
  284. b = set(self.adj[friend].keys())
  285. self._toggle_edges(a, b)
  286. intersection = a & b
  287. for i, j in it.combinations(intersection, 2):
  288. self._toggle_edge(i, j)
  289. for n in a - {friend}:
  290. self._toggle_edge(friend, n)
  291. return result, False
  292. def _measure_graph_y(self, node, result):
  293. """ Measure the bare graph in the Y-basis """
  294. # Do some rotations
  295. for neighbour in self.adj[node]:
  296. self._update_vop(neighbour, "sqz" if result else "msqz")
  297. # A sort of local complementation
  298. vngbh = set(self.adj[node]) | {node}
  299. for i, j in it.combinations(vngbh, 2):
  300. self._toggle_edge(i, j)
  301. # TODO: naming: # lcoS.herm_adjoint() if result else lcoS
  302. self._update_vop(node, 5 if result else 6)
  303. return result, False
  304. def _measure_graph_z(self, node, result):
  305. """ Measure the bare graph in the Z-basis """
  306. # Disconnect
  307. for neighbour in tuple(self.adj[node]):
  308. self._del_edge(node, neighbour)
  309. if result:
  310. self._update_vop(neighbour, "pz")
  311. # Rotate
  312. if result:
  313. self._update_vop(node, "px")
  314. self._update_vop(node, "hadamard")
  315. else:
  316. self._update_vop(node, "hadamard")
  317. return result, False
  318. def order(self):
  319. """ Get the number of qubits """
  320. return len(self.node)
  321. def __str__(self):
  322. """ Represent as a string for quick debugging """
  323. s = ""
  324. for key in sorted(self.node.keys()):
  325. s += "{}: {}\t".format(
  326. key, clifford.get_name(self.node[key]["vop"]).replace("YC", "-"))
  327. if self.adj[key]:
  328. s += str(tuple(self.adj[key].keys())).replace(" ", "")
  329. else:
  330. s += "-"
  331. s += "\n"
  332. return s
  333. def to_json(self, stringify=False):
  334. """ Convert the graph to JSON-like form.
  335. :param stringify: JSON keys must be strings, But sometimes it is useful to have a JSON-like object whose keys are tuples.
  336. If you want to dump a graph do disk, do something like this::
  337. >>> import json
  338. >>> with open("graph.json") as f:
  339. json.dump(graph.to_json(True), f)
  340. """
  341. if stringify:
  342. node = {str(key): value for key, value in self.node.items()}
  343. adj = {str(key): {str(key): value for key, value in ngbh.items()}
  344. for key, ngbh in self.adj.items()}
  345. return {"node": node, "adj": adj}
  346. else:
  347. return {"node": self.node, "adj": self.adj}
  348. def from_json(self, data):
  349. """ Construct the graph from JSON data
  350. :param data: JSON data to be read.
  351. """
  352. self.node = data["node"]
  353. self.adj = data["adj"]
  354. def to_state_vector(self):
  355. """ Get the full state vector corresponding to this stabilizer state. Useful for debugging, interface with other simulators.
  356. This method becomes very slow for more than about ten qubits!
  357. The output state is represented as a ``abp.qi.CircuitModel``::
  358. >>> print g.to_state_vector()
  359. |00000❭: 0.18+0.00j
  360. |00001❭: 0.18+0.00j ...
  361. """
  362. if len(self.node) > 15:
  363. raise ValueError("Cannot build state vector: too many qubits")
  364. state = qi.CircuitModel(len(self.node))
  365. mapping = {node: i for i, node in enumerate(sorted(self.node))}
  366. for n in self.node:
  367. state.act_hadamard(mapping[n])
  368. for i, j in self.edgelist():
  369. state.act_cz(mapping[i], mapping[j])
  370. for i, n in self.node.items():
  371. state.act_local_rotation(mapping[i], clifford.unitaries[n["vop"]])
  372. return state
  373. def to_stabilizer(self):
  374. """
  375. Get the stabilizer representation of the state::
  376. >>> print g.to_stabilizer()
  377. 0 1 2 3 100 200
  378. ------------------------------
  379. X Z Z X
  380. Z X Z
  381. Z Z X
  382. - Z Z
  383. X Z
  384. Z X
  385. """
  386. return Stabilizer(self)
  387. def __eq__(self, other):
  388. """ Check equality between GraphStates """
  389. return self.adj == other.adj and self.node == other.node
  390. def copy(self):
  391. """ Make a copy of this graphstate """
  392. g = GraphState()
  393. g.node = self.node.copy()
  394. g.adj = self.adj.copy()
  395. g.deterministic = self.deterministic
  396. return g