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:
Alice and Bob each start with one half of a Bell pair in the state \(\frac{1}{\sqrt{2}}(\ket{00} + \ket{11})\).
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})\).
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}\)
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.
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.
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.
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
.
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
.
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()
.
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:
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}\).
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 +
.
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)).
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).
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:
First, Alice and Bob prepare their joint Bell states.
Second, Alice prepares her state \(\ket{\varphi}\) and measures her qubits.
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:
Preparing the joint Bell states takes as input two
QList
valuesas
andbs
and mapsbell00()
over each pairas[i]
andbs[i]
.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)
overqs[i]
,as[i]
, and two boolean arraysxs[i]
andys[i]
.Bob’s corrections involve mapping the function
QExpr bob(qbit& b, bool x, bool y)
overbs[i]
,xs[i]
, andys[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.
qexpr::map(bell00, as, bs)
mapsbell00()
over each pair of qubits in theQList
valuesas
andbs
.qexpr::map(alice, qs, as, xs, ys)
mapsalice()
over each tuple(qs[i], as[i], xs[i], ys[i])
.qexpr::map(bob, bs, xs, ys)
mapsbob()
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:
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.
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