""" This module implements Anders and Briegel's method for fast simulation of Clifford circuits. """ import itertools as it import json, random import qi, clifford, util class GraphState(object): """ This is the main class used to model stabilizer states. Internally it uses the same dictionary-of-dictionaries data structure as ``networkx``. """ def __init__(self, nodes=[], deterministic=False): """ Construct a ``GraphState`` :param nodes: An iterable of nodes used to construct the graph. :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. """ self.adj, self.node = {}, {} self.add_nodes(nodes) self.deterministic = deterministic def add_node(self, node, **kwargs): """ Add a node. :param node: The name of the node, e.g. ``9``, ``start`` :type node: Any hashable type :param kwargs: Any extra node attributes Example of using node attributes :: >>> g.add_node(0, label="fred", position=(1,2,3)) >>> g.node[0]["label"] fred """ assert not node in self.node, "Node {} already exists".format(v) self.adj[node] = {} self.node[node] = {"vop": clifford.by_name["hadamard"]} self.node[node].update(kwargs) def add_nodes(self, nodes): """ Add many nodes in one shot. """ for n in nodes: self.add_node(n) def act_circuit(self, circuit): """ Run many gates in one call. :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``. Example (makes a Bell pair):: >>> g.act_circuit([(0, "hadamard"), (1, "hadamard"), ((0, 1), "cz")]) """ for node, operation in circuit: if operation == "cz": self.act_cz(*node) else: self.act_local_rotation(node, operation) def _add_edge(self, v1, v2, data={}): """ Add an edge between two vertices """ self.adj[v1][v2] = data self.adj[v2][v1] = data def _del_edge(self, v1, v2): """ Delete an edge between two vertices """ del self.adj[v1][v2] del self.adj[v2][v1] def has_edge(self, v1, v2): """ Test existence of an edge between two vertices """ return v2 in self.adj[v1] def _toggle_edge(self, v1, v2): """ Toggle an edge between two vertices """ if self.has_edge(v1, v2): self._del_edge(v1, v2) else: self._add_edge(v1, v2) def edgelist(self): """ Describe a graph as an edgelist # TODO: inefficient """ edges = set(tuple(sorted((i, n))) for i, v in self.adj.items() for n in v) return tuple(edges) def remove_vop(self, a, avoid): """ Reduces VOP[a] to the identity """ others = set(self.adj[a]) - {avoid} if self.deterministic: swap_qubit = min(others) if others else avoid else: swap_qubit = others.pop() if others else avoid for v in reversed(clifford.decompositions[self.node[a]["vop"]]): if v == "x": self.local_complementation(a, "U ->") else: self.local_complementation(swap_qubit, "V ->") def local_complementation(self, v, prefix=""): """ As defined in LISTING 1 of Anders & Briegel """ for i, j in it.combinations(self.adj[v], 2): self._toggle_edge(i, j) self.node[v]["vop"] = clifford.times_table[ self.node[v]["vop"], clifford.by_name["msqx_h"]] for i in self.adj[v]: self.node[i]["vop"] = clifford.times_table[ self.node[i]["vop"], clifford.by_name["sqz_h"]] def act_local_rotation(self, node, operation): """ Act a local rotation on a qubit :param node: The index of the node to act on :param operation: The Clifford-group operation to perform. """ rotation = clifford.by_name[str(operation)] self.node[node]["vop"] = clifford.times_table[ rotation, self.node[node]["vop"]] def _update_vop(self, v, op): """ Update a VOP - only used internally""" rotation = clifford.by_name[str(op)] self.node[v]["vop"] = clifford.times_table[ self.node[v]["vop"], rotation] def act_hadamard(self, qubit): """ Shorthand for ``self.act_local_rotation(qubit, "hadamard")`` """ self.act_local_rotation(qubit, 10) def _lonely(self, a, b): """ Is this qubit _lonely ? """ return len(self.adj[a]) > (b in self.adj[a]) def act_cz(self, a, b): """ Act a controlled-phase gate on two qubits :param a: The first qubit :param b: The second qubit """ if self._lonely(a, b): self.remove_vop(a, b) if self._lonely(b, a): self.remove_vop(b, a) if self._lonely(a, b) and not clifford.is_diagonal(self.node[a]["vop"]): self.remove_vop(a, b) edge = self.has_edge(a, b) va = self.node[a]["vop"] vb = self.node[b]["vop"] new_edge, self.node[a]["vop"], self.node[b]["vop"] = \ clifford.cz_table[int(edge), va, vb] if new_edge != edge: self._toggle_edge(a, b) def measure(self, node, basis, force=None): """ Measure in an arbitrary basis :param node: The name of the qubit to measure. :param basis: The basis in which to measure. :type basis: :math:`\in` ``{"px", "py", "pz"}`` :param force: Measurements in quantum mechanics are probabilistic. If you want to force a particular outcome, use the ``force``. :type force: boolean """ basis = clifford.by_name[basis] ha = clifford.conjugation_table[self.node[node]["vop"]] basis, phase = clifford.conjugate(basis, ha) # Flip a coin result = force if force != None else random.choice([0, 1]) # Flip the result if we have negative phase if phase == -1: result = not result if basis == clifford.by_name["px"]: result = self._measure_x(node, result) elif basis == clifford.by_name["py"]: result = self._measure_y(node, result) elif basis == clifford.by_name["pz"]: result = self._measure_z(node, result) else: raise ValueError("You can only measure in {X,Y,Z}") # Flip the result if we have negative phase if phase == -1: result = not result return result def _toggle_edges(self, a, b): """ Toggle edges between vertex sets a and b """ # TODO: i'm pretty sure this is just a single-line it.combinations or # equiv done = set() for i, j in it.product(a, b): if i != j and not (i, j) in done: done.add((i, j)) done.add((j, i)) self._toggle_edge(i, j) def _measure_x(self, node, result): """ Measure the graph in the X-basis """ if len(self.adj[node]) == 0: return 0 # Pick a vertex if self.deterministic: friend = sorted(self.adj[node].keys())[0] else: friend = next(self.adj[node].iterkeys()) # Update the VOPs. TODO: pretty ugly if result: # Do a z on all ngb(vb) \ ngb(v) \ {v}, and some other stuff self._update_vop(friend, "msqy") self._update_vop(node, "pz") for n in set(self.adj[friend]) - set(self.adj[node]) - {node}: self._update_vop(n, "pz") else: # Do a z on all ngb(v) \ ngb(vb) \ {vb}, and sqy on the friend self._update_vop(friend, "sqy") for n in set(self.adj[node]) - set(self.adj[friend]) - {friend}: self._update_vop(n, "pz") # Toggle the edges. TODO: Yuk. Just awful! a = set(self.adj[node].keys()) b = set(self.adj[friend].keys()) self._toggle_edges(a, b) intersection = a & b for i, j in it.combinations(intersection, 2): self._toggle_edge(i, j) for n in a - {friend}: self._toggle_edge(friend, n) return result def _measure_y(self, node, result): """ Measure the graph in the Y-basis """ # Do some rotations for neighbour in self.adj[node]: self._update_vop(neighbour, "sqz" if result else "msqz") # A sort of local complementation vngbh = set(self.adj[node]) | {node} for i, j in it.combinations(vngbh, 2): self._toggle_edge(i, j) self._update_vop(node, 5 if result else 6) # TODO: naming: # lcoS.herm_adjoint() if result else # lcoS return result def _measure_z(self, node, result): """ Measure the graph in the Z-basis """ # Disconnect for neighbour in tuple(self.adj[node]): self._del_edge(node, neighbour) if result: self._update_vop(neighbour, "pz") # Rotate if result: self._update_vop(node, "px") self._update_vop(node, "hadamard") else: self._update_vop(node, "hadamard") return result def order(self): """ Get the number of qubits """ return len(self.node) def __str__(self): """ Represent as a string for quick debugging """ s = "" for key in sorted(self.node.keys()): s += "{}: {}\t".format( key, clifford.get_name(self.node[key]["vop"]).replace("YC", "-")) if self.adj[key]: s += str(tuple(self.adj[key].keys())).replace(" ", "") else: s += "-" s += "\n" return s def to_json(self, stringify=False): """ Convert the graph to JSON-like form. :param stringify: JSON keys must be strings, But sometimes it is useful to have a JSON-like object whose keys are tuples. If you want to dump a graph do disk, do something like this:: >>> import json >>> with open("graph.json") as f: json.dump(graph.to_json(True), f) """ if stringify: node = {str(key): value for key, value in self.node.items()} adj = {str(key): {str(key): value for key, value in ngbh.items()} for key, ngbh in self.adj.items()} return {"node": node, "adj": adj} else: return {"node": self.node, "adj": self.adj} def from_json(self, data): """ Reconstruct from JSON """ self.__init__([]) # TODO def to_state_vector(self): """ Get the full state vector corresponding to this stabilizer state. Useful for debugging, interface with other simulators. The output state is represented as a ``abp.qi.CircuitModel``:: >>> print g.to_state_vector() |00000>: 0.18+0.00j |00001>: 0.18+0.00j ... .. warning:: Obviously this method becomes very slow for more than about ten qubits! """ if len(self.node) > 15: raise ValueError("Cannot build state vector: too many qubits") state = qi.CircuitModel(len(self.node)) for i in range(len(self.node)): state.act_hadamard(i) for i, j in self.edgelist(): state.act_cz(i, j) for i, n in self.node.items(): state.act_local_rotation(i, clifford.unitaries[n["vop"]]) return state def to_stabilizer(self): """ Get the stabilizer tableau. Work in progress! """ return output = {a: {} for a in self.node} for a, b in it.product(self.node, self.node): if a == b: output[a][b] = "X" elif a in self.adj[b]: output[a][b] = "Z" else: output[a][b] = "I" # TODO: signs return output def __eq__(self, other): """ Check equality between GraphStates """ return self.adj == other.adj and self.node == other.node if __name__ == '__main__': g = GraphState() g.add_nodes(range(10)) g._add_edge(0, 5) g.act_local_rotation(6, 10) print g print g.to_state_vector()