N-qubit GHZ and Graph state preparation¶
Download this notebook - ghz_and_graph.ipynb
In this example we prepare n-qubit GHZ and graph states and perform stabilizer simulation using Stim. To do this we make use of the fact that Guppy can load in values from the host python program and inject them in to the resulting Guppy program as constants.
import networkx as nx
from collections import Counter
from guppylang import guppy
from guppylang.defs import GuppyFunctionDefinition
from guppylang.std.builtins import array, comptime, result
from guppylang.std.quantum import cx, cz, h, measure_array, qubit
from guppylang.emulator import EmulatorResult
We would like to write one Guppy function for any number of qubits, but we want the number of qubits to be known at Guppy compile time so we can check the number of qubits and avoid using dynamic classical or quantum allocation (which can be slow and induce memory error).
To achieve this we can define a Guppy function that is generic over the size of an array, because arrays have sizes known at compile time.
# Declare generic variable
n = guppy.nat_var("n")
# define guppy function generic over array size
@guppy
def build_ghz_state(q: array[qubit, n]) -> None:
h(q[0])
# array size argument used in range to produce statically sized array
for i in range(n - 1):
cx(q[i], q[i + 1])
def build_ghz_prog(n_qb: int) -> GuppyFunctionDefinition:
"""Build a Guppy program that prepares a GHZ state on `n_qb` qubits."""
# we can define the entry point to the guppy program dependent on
# the number of qubits we want to use.
@guppy
def main() -> None:
# allocate number of qubits specified from outer
# python function using a `comptime` expression.
q = array(qubit() for _ in range(comptime(n_qb)))
build_ghz_state(q)
result("c", measure_array(q))
# return the guppy function
return main
Let’s define a quick utility to help us read out our results as bitstring counts.
def get_counts(shots: EmulatorResult) -> Counter[str]:
"""Counter treating all results from a shot as entries in a single bitstring"""
counter_list = []
for shot in shots:
for e in shot:
bitstring = "".join(str(k) for k in e[1])
counter_list.append(bitstring)
return Counter(counter_list)
We can now run our GHZ prep, we expect to see an even mix of |0..0> and |1..1>.
ghz_prog = build_ghz_prog(6)
shots = ghz_prog.emulator(n_qubits=6).stabilizer_sim().with_seed(2).with_shots(100).run()
get_counts(shots)
Counter({'000000': 51, '111111': 49})
Similarly, we can define a graph state over
an arbitrary graph by first using networkx
to define our graph as a list of edges, and
loading those edge pairs in to Guppy.
Because the Guppy compiler knows the length of the list being pulled in, it can load it in as a statically sized array (just like the qubit array).
def build_graph_state(graph: nx.Graph) -> GuppyFunctionDefinition:
edges = list(graph.edges)
n_qb = graph.number_of_nodes()
@guppy
def main() -> None:
qs = array(qubit() for _ in range(comptime(n_qb)))
for i in range(len(qs)):
h(qs[i])
for i, j in comptime(edges):
# apply CZ along every graph edge
cz(qs[i], qs[j])
result("c", measure_array(qs))
return main
Let’s test out our graph state builder with a \(K_3\) complete graph over 3 nodes. We expect to see an even mix of all 3 qubit basis states when measured.
k3_graph = nx.complete_graph(3)
graph_prog = build_graph_state(k3_graph)
shots = graph_prog.emulator(n_qubits=3).stabilizer_sim().with_seed(2).with_shots(100).run()
sorted(get_counts(shots).items())
[('000', 12),
('001', 14),
('010', 15),
('011', 10),
('100', 16),
('101', 9),
('110', 13),
('111', 11)]