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)]