Quantum Teleportation with FLEQ

This tutorial introduces the main concepts of FLEQ using quantum teleportation. Quantum teleportation [NC10] allows one actor, Alice, to send quantum information to another actor, Bob, in the form of a qubit state. The protocol proceeds as follows:

  1. Alice and Bob each start with one half of a Bell pair in the state \(\frac{1}{\sqrt{2}}(\ket{00} + \ket{11})\).

  2. Alice prepares her state \(\ket{\varphi} = \alpha \ket{0} + \beta \ket{1}\), resulting in the three-qubit system \(\ket{\varphi} \otimes \frac{1}{\sqrt{2}}(\ket{00} + \ket{11})\).

  3. Alice entangles her state with her half of the Bell pair and measures both qubits, producing bits \(x\) and \(y\), and leaving Bob’s half of the Bell pair in one of the following four states:

    Table 4 Bob’s state after Alice’s local measurement

    \(x\)

    \(y\)

    State

    \(0\)

    \(0\)

    \(\alpha \ket{0} + \beta\ket{1}\)

    \(0\)

    \(1\)

    \(\alpha \ket{0} - \beta\ket{1}\)

    \(1\)

    \(0\)

    \(\alpha \ket{1} + \beta\ket{0}\)

    \(1\)

    \(1\)

    \(\alpha \ket{1} - \beta\ket{0}\)

  4. Finally, Alice sends the classical bits \(x\) and \(y\) to Bob, who uses that information to correct his state to Alice’s original \(\ket{\varphi} = \alpha \ket{0} + \beta \ket{1}\).

One-qubit teleportation with quantum kernel expressions

We start by including the necessary header files: quintrinsics.h for use of the Intel® Quantum SDK, quantum_full_state_simulator_backend.h for a simulator backend, and qexpr.h for quantum kernel expressions. In addition, we include several headers from the C++ standard library that will be useful.

Listing 22 Header files.
1#include <clang/Quantum/quintrinsics.h>
2#include <quantum_full_state_simulator_backend.h>
3#include <clang/Quantum/qexpr.h>
4#include <qexpr_utils.h>
5
6#include <iostream>
7#include <cassert>
8#include <vector>
9#include <random>

To prepare a Bell state using quantum kernel expressions, we write a function that takes as input two qubits, and returns the quantum kernel expression that prepares those qubits in a Bell state.

Listing 23 Quantum kernel expression implementing the Bell state \(\frac{1}{\sqrt{2}} (\ket{00} + \ket{11})\)
1QExpr bell00(qbit& a, qbit& b) {
2    return qexpr::_PrepZ(a)
3         + qexpr::_PrepZ(b)
4         + qexpr::_H(a)
5         + qexpr::_CNOT(a,b);
6}

Notice that this function returns an expression representing a quantum program; unlike the non-FLEQ quantum_kernel functions in the rest of the SDK, FLEQ does not call quantum gates. Instead, it focuses on constructing a quantum kernel expression or QExpr.

In a similar vein, Alice can prepare her state \(\varphi\) by calling a QExpr-returning function prepPhi() on a qubit q, which prepares q by performing an X rotation around a randomly generated angle. The PROTECT modifier prevents inlining, and must be included whenever a QExpr function uses local variables.

Listing 24 Prepare Alice’s state \(\ket{\varphi}\).
 1double randomDoubleBetweenZeroAndTwoPi() {
 2    std::random_device rd;  // Used to seed the random number generator
 3    std::mt19937 gen(rd()); // Mersenne Twister PRNG
 4    std::uniform_real_distribution<double> dis(0.0, 2.0 * M_PI);
 5    return dis(gen);
 6}
 7// Prepare a state |phi> by performing an X rotation around a random angle
 8PROTECT QExpr prepPhi(qbit& q) {
 9    double theta = randomDoubleBetweenZeroAndTwoPi();
10    std::cout << "Using angle " << theta << "\n";
11    return qexpr::_PrepZ(q) + qexpr::_RX(q, theta);
12}

Next, we implement Alice’s half of the teleportation protocol, where she entangles her prepared qubit with her half of the Bell pair. She writes the results to two boolean references x and y.

Listing 25 Entangle q and a and measure both, writing the results to x and y respectively.
1QExpr alice(qbit& q, qbit& a, bool& x, bool& y) {
2    return qexpr::_CNOT(q, a)
3         + qexpr::_H(q)
4         + qexpr::_MeasZ(q, x)
5         + qexpr::_MeasZ(a, y);
6}

Finally, we implement Bob’s piece of the protocol, which uses x and y to apply corrections to his qubit, b.

Listing 26 Use x and y to apply corrections to Bob’s qubit b.
1PROTECT QExpr bob(qbit& b, bool &x, bool &y) {
2    return qexpr::cIf(y, qexpr::_X(b), qexpr::identity())
3         + qexpr::cIf(x, qexpr::_Z(b), qexpr::identity());
4}

Unlike the other QExpr functions up until now, bob() does not correspond to a straightforward quantum circuit. Instead, it uses the classical conditional blocks cIf to change which quantum kernel expression will be applied based on the runtime values of x and y. In particular, if y and x are both true at runtime, evaluating bob() will invoke the quantum operation X(b) followed by the operation Z(b). On the other hand, if y is false but x is true, evaluating bob() will only invoke Z(b). See the FLEQ Guide and Reference (Branching) for more details on classical conditionals.

To put all of these components together, we will implement 1-qubit teleportation in a top-level classical function, teleport1().

Listing 27 Implement the 1-qubit teleportation protocol
 1void teleport1(iqsdk::FullStateSimulator& device) {
 2
 3    qbit q;
 4    qbit a;
 5    qbit b;
 6
 7    // Prepare qubits a and b in a bell state
 8    qexpr::eval_hold(bell00(a,b));
 9
10    // Alice prepares her qubit q in the state |phi>
11    qexpr::eval_hold(prepPhi(q));
12
13    // Record the state Alice prepared
14    auto q_ref = to_ref_wrappers(qlist::QList(q));
15    auto probabilitiesBefore = device.getProbabilities(q_ref);
16
17    // Alice entangles her state q with a, and sends measurement
18    // results x and y to Bob
19    bool x;
20    bool y;
21    qexpr::eval_hold(alice(q, a, x, y));
22
23    // Bob uses x and y to correct his qubit b
24    qexpr::eval_hold(bob(b, x, y));
25
26    // At the end, b should be in the state |phi>, up to a global phase
27    auto b_ref = to_ref_wrappers(qlist::QList(b));
28    auto probabilitiesAfter = device.getProbabilities(b_ref);
29
30    std::cout << "Before teleportation, qubit q has distribution:\n";
31    iqsdk::FullStateSimulator::displayProbabilities(probabilitiesBefore, q_ref);
32    std::cout << "After teleportation, qubit b has distribution:\n";
33    iqsdk::FullStateSimulator::displayProbabilities(probabilitiesAfter, b_ref);
34}

The function takes as input a full state simulator device, which we assume has been properly initialized. Lines 3-5 of teleport1() declare three local qubits: q is Alice’s state; a is Alice’s half of the Bell pair; and b is Bob’s half of the Bell pair. The variables a and b are initialized in line 8 by evaluating the quantum kernel expression bell00 with the eval_hold() function.

Line 11 prepares Alice’s qubit q in state \(\ket{\varphi}\) by evaluating the quantum kernel expression prepPhi(q). Because this state is different every iteration, line 15 calls getProbabilities() to record what \(\ket{\varphi}\) is before teleportation. The function getProbabilities() produces a data structure that maps qubit states to the probability associated with that state at the current point in the computation. The argument to getProbabilities() specifies the subset and order of qubits whose probabilities should be considered. In this case, we are asking for only the qubit q. See the Measurements using Simulated Quantum Backends for more details.

To achieve quantum teleportation, in line 21, Alice measures her qubits to boolean values x and y by evaluating the QExpr function alice(). On line 24, Bob uses these values to correct his state b by evaluating bob(). Finally, line 28 invokes getProbabilities() once more to determine the state of b after teleportation, and prints out both probability distributions to compare them for equality.

The output of running teleport1() is the following:

 1 $ ./qexpr_teleport
 2   Using angle 4.75947
 3   Bob received 1 and 0.
 4   Before teleportation, qubit q has distribution:
 5   Printing probability register of size 2
 6   |0⟩   : 0.5236                        |1⟩   : 0.4764
 7
 8   After teleportation, qubit b has distribution:
 9   Printing probability register of size 2
10   |0⟩   : 0.5235                        |1⟩   : 0.4765

Line 2 indicates that Alice’s state \(\ket{\varphi}\) was prepared with angle 4.75947. Line 3 indicates that Alice measured bits x and y as 1 and 0, respectively. Finally, lines 4-10 show that Bob’s state after teleportation matches Alice’s state before teleportation (up to a rounding error).

A single quantum kernel expression

The function teleport1() above contains multiple evaluation calls to eval_hold(); it itself is a classical function that interacts with the quantum runtime. If a user does not need to report the output of the first state, can they implement teleportation as a single QExpr function?

An initial incorrect attempt in doing this is to write a QExpr function that simply joins the three modular components of the teleportation protocol:

Listing 28 Incorrect attempt to implement teleportation as a single quantum kernel expression.
1PROTECT QExpr teleport1_join(qbit& q, qbit& a, qbit& b) {
2    bool x = false;
3    bool y = false;
4    return bell00(a,b)
5            + alice(q, a, x, y)
6            + bob(b,x,y);
7}

To use this function, Alice prepares her qubit q in state \(\ket{\varphi}\) and combines that with the teleportation procedure. Below, Alice prepares the state \(\ket{1}\).

Listing 29 Incorrect attempt to implement teleportation as a single quantum kernel expression.
 1void teleport1_bad(iqsdk::FullStateSimulator& device) {
 2    qbit q;
 3    qbit a;
 4    qbit b;
 5
 6    qexpr::eval_hold(qexpr::_PrepZ(q) + qexpr::_X(q) + teleport1_join(q,a,b));
 7
 8    // At the end, b should be in the state |1>
 9    auto b_ref = to_ref_wrappers(qlist::QList(b));
10    auto probabilitiesAfter = device.getProbabilities(b_ref);
11
12    std::cout << "Expecting state |1>\n";
13    std::cout << "After teleportation, Bob obtains state:\n";
14    iqsdk::FullStateSimulator::displayProbabilities(probabilitiesAfter, b_ref);
15}

When we try to run this algorithm, half the time we will observe b in the state \(\ket{0}\) and half the time we will observe it in the state \(\ket{1}\). This is an indication that the corrections in Bob’s part of the protocol are not being applied correctly. Indeed, if we were to print out the values of x and y before Bob performs his corrections, we would see that the values of x and y are always 0.

The reason for this is that the measurement in Alice’s protocol (inside the QExpr function alice()) is occurring within the same Quantum Basic Block (QBB) as the conditional in bob(). However, measurement results are not written to classical variables x and y until the end of a QBB. Thus, the measurement results do not propagate to x and y before Bob tries to use them. See FLEQ Guide and Reference (Barriers and binding) for more details.

The solution is to insert a barrier between Alice’s protocol and Bob’s protocol to ensure they happen within separate QBBs. This can be achieved via the bind function, an analogue of the usual join function that combines two quantum kernel expressions. Where join takes two quantum kernel expressions and combines them sequentially in the same quantum basic block, bind produces separate QBBs, executing one after the other. Analogous to the notation e1 + e2 for joining quantum kernel expressions in sequence, users can write e1 << e2 for binding quantum kernel expressions in sequence. See FLEQ Guide and Reference (Barriers and binding) for more details.

In this case, the QExpr teleportation function teleport1_join() should be replaced by a version that uses << in place of +.

Listing 30 A quantum kernel function that implements quantum teleportation using bind().
1PROTECT QExpr teleport1_bind(qbit& q, qbit& a, qbit& b) {
2    bool x = false;
3    bool y = false;
4    return (bell00(a,b) + alice(q, a, x, y))
5            << bob(b,x,y);
6}

We can now invoke this algorithm by evaluating teleport1_bind() after Alice has prepared her qubit in state \(\ket{1}\). In this case we find that no matter how many times we run the algorithm, Bob always results in a qubit in state \(\ket{1}\), as expected.

Multi-qubit teleportation

In this section we will extend single-qubit quantum teleportation to a protocol that teleports \(N\) qubits for an arbitrary \(N\). The protocol requires \(N\) pairs of qubits in a Bell state and performs the single-qubit teleportation sequence for each qubit.

A first attempt at multi-qubit teleportation would be just to call the teleport1() function \(N\) times in sequence. However, with this approach Alice would prepare \(N\) single-qubit states, and it would not allow for an entangled state to be teleported.

A next attempt would be for Alice to prepare her qubit state and then evaluate teleport1_bind() \(N\) times on each successive qubit. This can be achieved via a recursive function that returns a quantum kernel expression (see FLEQ Guide and Reference (Recursion)).

Listing 31 A function that returns a recursive quantum kernel expression applying the quantum teleportation protocol to each triple of qubits in qs, as, and bs.
1QExpr teleport_sequential(qlist::QList qs, qlist::QList as, qlist::QList bs) {
2    return qexpr::cIf(qs.size() == 0,
3                        // if qs is empty:
4                            qexpr::identity(),
5                        // if qs is non-empty:
6                            teleport1_bind(qs[0], as[0], bs[0])
7                            << teleport_sequential(qs+1, as+1, bs+1)
8    );
9}

The recursive function teleport_sequential uses a classical conditional cIf to distinguish between a base case (when the QList qs is empty) and a recursive case (when qs is non-empty). It assumes that all three QList inputs have the same length. When they are all empty (have size 0), teleportation should do nothing and so returns the qexpr::identity() quantum kernel expression. In the recursive case, we will apply single-qubit teleportation (in the form of the teleport1_bind() function) on qs[0], as[0], and bs[0] and then recursively call teleport_sequential on the tails of the three qubit lists (qs+1, as+1, and bs+1).

For example, if the length of the three qubit lists is 2, then teleport_sequential(qs, as, bs) will be unrolled to the following sequence:

teleport1_bind(qs[0], as[0], bs[0]) + teleport_sequential(qs+1, as+1, bs+1)

Then, because (qs+1).size() == 1, the call to teleport_sequential() will be unrolled again:

teleport1_bind(qs[0], as[0], bs[0])
  + teleport1_bind((qs+1)[0], (as+1)[0], (bs+1)[0])
  + teleport_sequential(qs+2, as+2, bs+2)

where (qs+1)[0] is the same as qs[1]. Finally, because (qs+2).size() == 0, the final call to teleport_sequential() will unroll to the identity. Thus all together the call teleport_sequential(qs,as,bs) becomes

teleport1_bind(qs[0], as[0], bs[0]) + teleport1_bind(qs[1], as[1], bs[1])

Putting this all together, the top-level evaluation call would prepare Alice’s state \(\ket{\phi}\) and call teleport_sequential. Here, we will prepare Alice’s state to be the GHZ state \(\frac{1}{\sqrt{2}}(\ket{0\cdots0} + \ket{1\cdots 1})\) as illustrated in the example qexpr_ghz.cpp (see Code Samples).

Listing 32 An evaluation call of teleport_sequential after preparing \(\ket{\varphi}\) as a GHZ state.
1    const int N = 2;
2    qbit listable(qs, N);
3    qbit listable(as, N);
4    qbit listable(bs, N);
5
6    qexpr::eval_hold(ghz(qs) // Prepare |phi>
7                    + teleport_sequential(qs, as, bs));

Minimizing barriers and map

Because each call to teleport1_bind has a barrier in the form of a bind, teleport_parallel(qs, as, bs) will result in more than \(n\) quantum basic blocks (QBBs) (see FLEQ Guide and Reference (Barriers and binding)). While such barriers are logically valid, they prevent the compiler from optimizing across boundaries, which can make compilation redundant and expensive. It can also result in less-ideal placements (which are determined for each QBB individually) and scheduling. Logically, the \(n\)-qubit teleportation protocol really should have three separate components:

  1. First, Alice and Bob prepare their joint Bell states.

  2. Second, Alice prepares her state \(\ket{\varphi}\) and measures her qubits.

  3. Finally, Bob receives Alice’s measurements and performs his own corrections.

These three states could be achieved using their own recursive functions over the qubit lists, as in teleport_sequential(). But each of these three cases has a similar structure that we can exploit. Consider:

  1. Preparing the joint Bell states takes as input two QList values as and bs and maps bell00() over each pair as[i] and bs[i].

  2. Alice’s preparations involve first preparing the state \(\ket{\varphi}\) and then mapping the function QExpr alice(qbit& q, qbit& a, bool& x, bool& y) over qs[i], as[i], and two boolean arrays xs[i] and ys[i].

  3. Bob’s corrections involve mapping the function QExpr bob(qbit& b, bool x, bool y) over bs[i], xs[i], and ys[i].

Each of these three cases (as well as teleport_sequential() itself) involves mapping a single-qubit QExpr function over one or more QList values or arrays. In fact, this is such a commonly occurring pattern that FLEQ provides a higher-order functional utility called qexpr::map() in the header file qexpr_utils.h (see FLEQ Guide and Reference (Higher-order QExpr functions)).

The first argument to qexpr::map(f, qs, ...) is a function pointer f, which takes at least one qubit argument and returns a QExpr. The next argument is a QList qs, and the remaining arguments are either additional QList variables, array variables, or non-arrays, each of which is passed as an additional argument to f.

The qexpr::map() utility is best understood via example.

  1. qexpr::map(bell00, as, bs) maps bell00() over each pair of qubits in the QList values as and bs.

  2. qexpr::map(alice, qs, as, xs, ys) maps alice() over each tuple (qs[i], as[i], xs[i], ys[i]).

  3. qexpr::map(bob, bs, xs, ys) maps bob() over each tuple (bs[i], xs[i], ys[i]).

Thus, we can lift each component of quantum teleportation to \(n\) qubits easily, without even writing any additional QExpr functions, and put them together smoothly as follows:

Listing 33 A version of \(n\)-qubit teleportation with only two total barriers.
1PROTECT
2QExpr teleport_parallel(QExpr phi, qlist::QList qs, qlist::QList as, qlist::QList bs) {
3    bool xs[qs.size()];
4    bool ys[qs.size()];
5
6    return qexpr::map(bell00, as, bs)
7        << (phi + qexpr::map(alice, qs, as, xs, ys))
8        << qexpr::map(bob, bs, xs, ys);
9}

To test our implementation, we will have Alice prepare a GHZ state on qs and then analyze Bob’s qubits after teleportation. If teleportation succeeds, Bob’s qubits will then be in the original GHZ state.

Listing 34 Evaluating \(n\)-qubit teleportation via teleport_parallel on the Intel® Quantum Simulator.
 1void teleportN(iqsdk::FullStateSimulator& device) {
 2
 3    const int N = 3;
 4    qbit listable(qs, N);
 5    qbit listable(as, N);
 6    qbit listable(bs, N);
 7
 8    // Teleportation with |phi>=1/sqrt(2)(|0...0> + |1...1>)
 9    qexpr::eval_hold(teleport_parallel(ghz(qs), qs, as, bs));
10
11    // At the end, bs should be in the state |phi>=1/sqrt(2)(|0...0> + |1...1>)
12    // (up to a global phase)
13    auto outputRefs = to_ref_wrappers(bs);
14    auto probsAfter = device.getProbabilities(outputRefs, {}, 0.01);
15
16    std::cout << "Expecting GHZ state |0...0> + |1...1>\n";
17    std::cout << "Qubits bs after teleportation:\n";
18    iqsdk::FullStateSimulator::displayProbabilities(probsAfter);
19}

The result of this evaluation call is, as expected:

1 $ ./qexpr_teleport
2   Expecting GHZ state |0...0> + |1...1>
3   Qubits bs after teleportation:
4   Printing probability map of size 2
5   |000⟩ : 0.5                           |111⟩ : 0.5