"""Graph simulator
Graph state simulator, according to
M. Elliot, B. Eastin & C. Caves,
JPhysA 43, 025301 (2010) and PRA 77, 042307 (2008)
"""
import networkx as nx
import numpy as np
from graphix.clifford import CLIFFORD_MUL
from graphix.sim.statevec import Statevec
from graphix.ops import Ops
[docs]class GraphState(nx.Graph):
"""Graph state simulator
Performs Pauli measurements on graph states.
Inherits methods and attributes from networkx.Graph.
ref: M. Elliot, B. Eastin & C. Caves, JPhysA 43, 025301 (2010)
and PRA 77, 042307 (2008)
Each node has attributes:
:`hollow`: True if node is hollow
:`sign`: True if node has negative sign
:`loop`: True if node has loop
"""
[docs] def __init__(self, nodes=None, edges=None):
"""
Parameters
----------
nodes : iterable container
A container of nodes (list, dict, etc)
edges : list
list of tuples (i,j) for pairs to be entangled.
"""
super().__init__()
if nodes is not None and edges is not None:
self.add_nodes_from(nodes)
self.add_edges_from(edges)
elif nodes is not None:
self.add_nodes_from(np.arange(nodes))
elif edges is not None:
self.add_edges_from(edges)
[docs] def add_nodes_from(self, nodes):
"""Add nodes and initialize node properties.
Parameters
----------
nodes : iterable container
A container of nodes (list, dict, etc)
"""
super().add_nodes_from(nodes, loop=False, sign=False, hollow=False)
[docs] def add_edges_from(self, edges):
"""Add edges and initialize node properties of newly added nodes.
Parameters
----------
edges : iterable container
must be given as list of 2-tuples (u, v)
"""
super().add_edges_from(edges)
for i in self.nodes:
if "loop" not in self.nodes[i]:
self.nodes[i]["loop"] = False # True for having loop
self.nodes[i]["sign"] = False # True for minus
self.nodes[i]["hollow"] = False # True for hollow node
[docs] def get_vops(self):
"""Returns a dict containing clifford labels for each nodes.
labels 0 - 23 specify one of 24 single-qubit Clifford gates.
see graphq.clifford for the definition of all unitaries.
Returns
----------
vops : dict
clifford gate indices as defined in `graphq.clifford`.
.. seealso:: :mod:`graphix.clifford`
"""
vops = {}
for i in self.nodes:
vop = 0
if self.nodes[i]["sign"]:
vop = CLIFFORD_MUL[3, vop]
if self.nodes[i]["loop"]:
vop = CLIFFORD_MUL[4, vop]
if self.nodes[i]["hollow"]:
vop = CLIFFORD_MUL[6, vop]
vops[i] = vop
return vops
[docs] def flip_fill(self, node):
"""Flips the fill (local H) of a node.
Parameters
----------
node : int
graph node to flip the fill
"""
self.nodes[node]["hollow"] = not self.nodes[node]["hollow"]
[docs] def flip_sign(self, node):
"""Flips the sign (local Z) of a node.
Note that application of Z gate is different from `flip_sign`
if there exist an edge from the node.
Parameters
----------
node : int
graph node to flip the sign
"""
self.nodes[node]["sign"] = not self.nodes[node]["sign"]
[docs] def advance(self, node):
"""Flips the loop (local S) of a node.
If the loop already exist, sign is also flipped,
reflecting the relation SS=Z.
Note that application of S gate is different from `advance`
if there exist an edge from the node.
Parameters
----------
node : int
graph node to advance the loop.
"""
if self.nodes[node]["loop"]:
self.nodes[node]["loop"] = False
self.flip_sign(node)
else:
self.nodes[node]["loop"] = True
[docs] def h(self, node):
"""Apply H gate to a qubit (node).
Parameters
----------
node : int
graph node to apply H gate
"""
self.flip_fill(node)
[docs] def s(self, node):
"""Apply S gate to a qubit (node).
Parameters
----------
node : int
graph node to apply S gate
"""
if self.nodes[node]["hollow"]:
if self.nodes[node]["loop"]:
self.flip_fill(node)
self.nodes["loop"] = False
self.local_complement(node)
for i in self.neighbors(node):
self.advance(i)
else:
self.local_complement(node)
for i in self.neighbors(node):
self.advance(i)
if self.nodes[node]["sign"]:
for i in self.neighbors(node):
self.flip_sign(i)
else: # solid
self.advance(node)
[docs] def z(self, node):
"""Apply Z gate to a qubit (node).
Parameters
----------
node : int
graph node to apply Z gate
"""
if self.nodes[node]["hollow"]:
for i in self.neighbors(node):
self.flip_sign(i)
if self.nodes[node]["loop"]:
self.flip_sign(node)
else: # solid
self.flip_sign(node)
[docs] def equivalent_graph_E1(self, node):
"""Tranform a graph state to a different graph state
representing the same stabilizer state.
This rule applies only to a node with loop.
Parameters
----------
node1 : int
A graph node with a loop to apply rule E1
"""
assert self.nodes[node]["loop"]
self.flip_fill(node)
self.local_complement(node)
for i in self.neighbors(node):
self.advance(i)
self.flip_sign(node)
if self.nodes[node]["sign"]:
for i in self.neighbors(node):
self.flip_sign(i)
[docs] def equivalent_graph_E2(self, node1, node2):
"""Tranform a graph state to a different graph state
representing the same stabilizer state.
This rule applies only to two connected nodes without loop.
Parameters
----------
node1, node2 : int
connected graph nodes to apply rule E2
"""
assert (node1, node2) in list(self.edges) or (node2, node1) in list(self.edges)
assert not self.nodes[node1]["loop"] and not self.nodes[node2]["loop"]
self.flip_fill(node1)
self.flip_fill(node2)
# local complement along edge between node1, node2
self.local_complement(node1)
self.local_complement(node2)
self.local_complement(node1)
for i in iter(set(self.neighbors(node1)) & set(self.neighbors(node2))):
self.flip_sign(i)
if self.nodes[node1]["sign"] != self.nodes[node2]["sign"]:
if self.nodes[node1]["sign"]:
self.flip_sign(node1)
for i in self.neighbors(node1):
self.flip_sign(i)
elif self.nodes[node2]["sign"]:
self.flip_sign(node2)
for i in self.neighbors(node2):
self.flip_sign(i)
[docs] def local_complement(self, node):
"""Perform local complementation of a graph
Parameters
----------
node : int
chosen node for the local complementation
"""
g = self.subgraph(list(self.neighbors(node)))
g_new = nx.complement(g)
self.remove_edges_from(g.edges)
self.add_edges_from(g_new.edges)
[docs] def equivalent_fill_node(self, node):
"""Fill the chosen node by graph transformation rules E1 and E2,
If the selected node is hollow and isolated, it cannot be filled
and warning is thrown.
Parameters
----------
node : int
node to fill.
Returns
----------
result : int
if the selected node is hollow and isolated, `result` is 1.
if filled and isolated, 2.
otherwise it is 0.
"""
if self.nodes[node]["hollow"]:
if self.nodes[node]["loop"]:
self.equivalent_graph_E1(node)
return 0
else: # node = hollow and loopless
if len(list(self.neighbors(node))) == 0:
return 1
for i in self.neighbors(node):
if not self.nodes[i]["loop"]:
self.equivalent_graph_E2(node, i)
return 0
# if all neighbor has loop, pick one and apply E1, then E1 to the node.
i = next(self.neighbors(node))
self.equivalent_graph_E1(i) # this gives loop to node.
self.equivalent_graph_E1(node)
return 0
else:
if len(list(self.neighbors(node))) == 0:
return 2
else:
return 0
[docs] def measure_x(self, node, choice=0):
"""perform measurement in X basis
According to original paper, we realise X measurement by
applying H gate to the measured node before Z measurement.
Parameters
----------
node : int
qubit index to be measured
choice : int, 0 or 1
choice of measurement outcome. observe (-1)^choice
"""
self.h(node)
return self.measure_z(node, choice=choice)
[docs] def measure_y(self, node, choice=0):
"""perform measurement in Y basis
According to original paper, we realise Y measurement by
applying S,Z and H gate to the measured node before Z measurement.
Parameters
----------
node : int
qubit index to be measured
choice : int, 0 or 1
choice of measurement outcome. observe (-1)^choice
"""
self.s(node)
self.z(node)
self.h(node)
return self.measure_z(node, choice=choice)
[docs] def measure_z(self, node, choice=0):
"""perform measurement in Z basis
To realize the simple Z measurement on undecorated graph state,
we first fill the measured node (remove local H gate)
Parameters
----------
node : int
qubit index to be measured
choice : int, 0 or 1
choice of measurement outcome. observe (-1)^choice
"""
isolated = self.equivalent_fill_node(node)
if choice:
for i in self.neighbors(node):
self.flip_sign(i)
if not isolated:
result = choice
else:
result = int(self.nodes[node]["sign"])
self.remove_node(node)
return result
[docs] def draw(self, fill_color="C0", **kwargs):
"""Draw decorated graph state.
Negative nodes are indicated by negative sign of node labels.
Parameters
----------
fill_color : str, optional
fill color of nodes
kwargs : keyword arguments, optional
additional arguments to supply networkx.draw().
"""
nqubit = len(self.nodes)
nodes = list(self.nodes)
edges = list(self.edges)
labels = {i: i for i in iter(self.nodes)}
colors = [fill_color for i in range(nqubit)]
for i in range(nqubit):
if self.nodes[nodes[i]]["loop"]:
edges.append((nodes[i], nodes[i]))
if self.nodes[nodes[i]]["hollow"]:
colors[i] = "white"
if self.nodes[nodes[i]]["sign"]:
labels[nodes[i]] = -1 * labels[nodes[i]]
g = nx.Graph()
g.add_nodes_from(nodes)
g.add_edges_from(edges)
nx.draw(g, labels=labels, node_color=colors, edgecolors="k", **kwargs)
[docs] def to_statevector(self):
node_list = list(self.nodes)
nqubit = len(self.nodes)
gstate = Statevec(nqubit=nqubit)
# map graph node indices into 0 - (nqubit-1) for qubit indexing in statevec
imapping = {node_list[i]: i for i in range(nqubit)}
mapping = [node_list[i] for i in range(nqubit)]
for i, j in self.edges:
gstate.entangle((imapping[i], imapping[j]))
for i in range(nqubit):
if self.nodes[mapping[i]]["sign"]:
gstate.evolve_single(Ops.z, i)
for i in range(nqubit):
if self.nodes[mapping[i]]["loop"]:
gstate.evolve_single(Ops.s, i)
for i in range(nqubit):
if self.nodes[mapping[i]]["hollow"]:
gstate.evolve_single(Ops.h, i)
return gstate