Lab 3 - Quantum Protocols¶
(Ex. 1-3: Due Nov. 15, Ex. 4 & 5: Due Nov. 27, Complete Lab due Dec 4)¶
This lab demonstrates interesting properties of entangled qubits. In particular, we will consider two experiments:
- CHSH Inequality Violation - this shows that quantum mechanics cannot be explained by a local hidden variable theory
- Teleportation - teleport an arbitrary quantum state using an entangled qubit pair as a resource
In particular, this lab demonstrates how to use new features from IBM Quantum
- Primitives - abstract measurement and error mitigation for scalable quantum computing
- Dynamic Circuits - mid-circuit measurement and feed-forward within the qubits' coherence time
Getting Started¶
Start by importing some libraries we need, including the Sampler
and Estimator
primitives from Qiskit. While the primitives from qiskit.providers
use a local statevector simulator by default, the syntax within this lab is easily generalizable to running experiments on real systems.
To run on real hardware requires a Qiskit Runtime service instance. If you haven't done so already, follow the instructions in the Qiskit Getting started guide to set one up. TODO: include video links and such. After setup, import the Sampler
and Estimator
primitives from qiskit_ibm_runtime
instead. Additionally we will need QiskitRuntimeService
and Session
, which form the interface between Qiskit and Qiskit IBM Runtime. Then the below exercises can be run on real systems by instantiating the primitives in this way (as opposed to from qiskit.primitives
):
from qiskit_ibm_runtime import QiskitRuntimeService, Session, Sampler, Estimator
service = QiskitRuntimeService()
backend = service.get_backend('...')
session = Session(service=service, backend=backend)
sampler = Sampler(session=session)
estimator = Estimator(session=session)
where additional options can be specified in the Sampler
and Estimator
with the Options
class. See this how-to for using Primitives with Runtime Sessions.
from qiskit.circuit import QuantumCircuit
from qiskit.primitives import Estimator, Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit.visualization import plot_histogram
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('dark_background') # optional
CHSH Inequality Violation¶
Warm Up¶
Create circuits that put the qubit in the excited $|1\rangle$ and superposition $|+\rangle$ states, respectivly, and measure them in different bases. This is done first with the Sampler
primitive (which is most similar to the backend.run()
used in the previous lab), and then with the Estimator
primitive to show how measurement is abstracted in that we do not need to worry about rotating the qubit into the appropriate measurement basis. The primitives will be executed withing the Session
context which allows efficiency to optimize workloads.
# create a qubit in the excited |1> state
qc_1 =
# create a different qubit in the superposition |+> state
qc_plus =
Introduction to Sampler Primitive¶
First use the Sampler
to measure qubits in the $Z$-basis (the physical basis in which qubits are measured). The Sampler
will count the number of outcomes of the $|0\rangle$ state and $|1\rangle$ state, normalized by the number of shots (experiments performed). The Sampler
also offers the ability to easily perform error mitigation (which is covered in a followup Lab), which modifies this calculation, and hence the outcomes are refered to as quasi-probabilities.
Measurments must be present in the circuit when using the Sampler
primitive. Then the Session
context is opened, the Sampler
is instantiated, and sampler.run()
is used to send the circuits to the backend, similar to the backend.run()
syntax you may already be familiar with.
qc_1.measure_all()
qc_plus.measure_all()
sampler = Sampler()
job_1 = sampler.run(qc_1)
job_plus = sampler.run(qc_plus)
job_1.result().quasi_dists
[{0: 1.0}]
job_plus.result().quasi_dists
[{0: 1.0}]
legend = ["Excited State", "Plus State"] # TODO: Excited State does not appear
plot_histogram([job_1.result().quasi_dists[0], job_plus.result().quasi_dists[0]], legend=legend)
The result for the excited state is always $|1\rangle$ wheres it is roughly half $|0\rangle$ and half $|1\rangle$ for the plus superposition state. This is because the $|0\rangle$ and $|1\rangle$ states are eigenstates of the $Z$ operator (with $+1$ and $-1$ eigenvalues, respectively).
Let's switch and measure in the $X$ basis. Using the Sampler
we must rotate the qubit from the $X$-basis to the $Z$-basis for measurement (because that is the only basis we can actually perform measurement in).
qc_1.remove_final_measurements()
qc_plus.remove_final_measurements()
# rotate into the X-basis
qc_1.measure_all()
qc_plus.measure_all()
sampler = Sampler()
job_1 = sampler.run(qc_1)
job_plus = sampler.run(qc_plus)
plot_histogram([job_1.result().quasi_dists[0], job_plus.result().quasi_dists[0]], legend=legend)
Explanation:¶
# your explanation here
Remark: This is good to remember when considering how the Estimator
works in the next subsection.
Estimator Primitive¶
The Qiskit Runtime Primitives allow us to abstract measurement into the Estimator
primitive, where it is specified as an observable. In particular, we can construct the same circuits, the excited $|1\rangle$ and superposition $|+\rangle$ as before. However, in the case of the Estimator
, we do not add measurements to the circuit. Instead, specify a list of observables which take the form of Pauli strings. In our case for a measurement of a single qubit, we specify 'Z'
for the $Z$-basis and 'X'
for the $X$-basis.
qc2_1 = QuantumCircuit(1)
# repat the same here i.e., create a qubit in the excited |1> state
qc2_plus = QuantumCircuit(1)
# similarly here i.e., create different qubit in the superposition |+> state
obsvs = list(SparsePauliOp(['Z', 'X']))
Us of the Estimator primitive
estimator = Estimator()
job2_1 = estimator.run([qc2_1]*len(obsvs), observables=obsvs)
#job2_1.result()
job2_plus = estimator.run([qc2_plus]*len(obsvs), observables=obsvs)
#job2_plus.result()
EstimatorResult(values=array([-1., 0.]), metadata=[{}, {}])
# TODO: make this into module that outputs a nice table
print(f' | <Z> | <X> ')
print(f'----|------------------')
print(f'|1> | {job2_1.result().values[0]} | {job2_1.result().values[1]}')
print(f'|+> | {job2_plus.result().values[0]} | {job2_plus.result().values[1]}')
| <Z> | <X> ----|------------------ |1> | -1.0 | 0.0 |+> | 0.0 | 0.9999999999999998
Just as before, we see the $|1\rangle$ state expectation in the $Z$-basis is $-1$ (corresponding to its eigenvalue) and around zero in the $X$-basis (average over $+1$ and $-1$ eigenvalues), and vice-versa for the $|+\rangle$ state (although its eigenvalue of the $X$ operators is $+1$).
CHSH Inequality¶
Imagine Alice and Bob are given each one part of a bipartite entangled system. Each of them then performs two measurements on their part in two different bases. Let's call Alice's bases A and a and Bob's B and b. What is the expectation value of the quantity
$$ \langle CHSH \rangle = \langle AB \rangle - \langle Ab \rangle + \langle aB \rangle + \langle ab \rangle ? $$
Now, Alice and Bob have one qubit each, so any measurement they perform on their system (qubit) can only yield one of two possible outcomes: +1 or -1. Note that whereas we typically refer to the two qubit states as $|0\rangle$ and $|1\rangle$, these are eigenstates, and a projective measurement will yield their eigenvalues, +1 and -1, respectively.
Therefore, if any measurement of A, a, B, and b can only yield $\pm 1$, the quantities $(B-b)$ and $(B+b)$ can only be 0 or $\pm 2$. And thus, the quantity $A(B-b) + a(B+b)$ can only be either +2 or -2, which means that there should be a bound for the expectation value of the quantity we have called
$$ |\langle CHSH \rangle| = |\langle AB \rangle - \langle Ab \rangle + \langle aB \rangle + \langle ab \rangle| \le 2. $$
Now, the above discussion is oversimplified, because we could consider that the outcome on any set of measurements from Alice and Bob could depend on a set of local hidden variables, but it can be shown with some math that, even when that is the case, the expectation value of the quantity $CHSH$ should be bounded by 2 if local realism held.
But what happens when we do these experiments with an entangled system? Let's try it!
The first step is to build the observable
$$
CHSH = A(B-b) + a(B+b) = AB - Ab + aB +ab
$$
where $A, a$ are each one of $\{IX, IZ\}$ for qubit 0 and $B, b$ are each one of $\{XI, ZI\}$ for qubit 1 (corresponding to little-endian notation). Paulis on different qubits can be composed by specifying order with a Pauli string, for example instantiating a SparsePauliOp
with the 'ZX'
argument implies a measurement of $\langle X \rangle$ on q0
and $\langle Z \rangle$ on q1
. This tensor product (combining operations on different qubits) can be explicitly stated using the .tensor()
method. Additionally, combining operations on the same qubit(s) uses the compositional product with the .compose()
method. For example, all these statements create the same Pauli operator:
from qiskit.quantum_info import SparsePauliOp
ZX = SparsePauliOp('ZX')
ZX = SparsePauliOp(['ZX'], coeffs=[1.]) # extendable to a sum of Paulis
ZX = SparsePauliOp('Z').tensor(SparsePauliOp('X')) # extendable to a tensor product of Paulis
ZX = SparsePauliOp('XZ').compose(SparsePauliOp('YY')) # extendable to a compositional product of Paulis
obsv = # create operator for chsh witness
# Print your answer To validate your expression is entered correctly!
Create Entangled Qubit Pair¶
Next we want to test the $CHSH$ observable on an entangled pair, for example the maximally-entangled Bell state
$$
|\Phi\rangle = \frac{1}{\sqrt{2}} \left(|00\rangle + |11\rangle \right)
$$
which is created with a Hadamard gate followed by a CNOT with the target on the same qubit as the Hadamard. Due to the simplifaction of measuring in just the $X$- and $Z$-bases as discussed above, we will rotate the Bell state around the Bloch sphere which is equivalant to changing the measurement basis as demonstrated in the Warmup section. This can be done by applying an $R_y(\theta)$ gate where $\theta$ is a Parameter
to be specified at the Estimator
API call. This produces the state
$$
|\psi\rangle = \frac{1}{\sqrt{2}} \left(\cos(\theta/2) |00\rangle + \sin(\theta/2)|11\rangle \right)
$$
from qiskit.circuit import Parameter
theta = Parameter('θ')
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.ry(theta, 0)
qc.draw('mpl')
Next we need to specify a Sequence
of Parameter
s that show a clear violation of the CHSH Inequality, namely
$$
|\langle CHSH \rangle| > 2.
$$
Let's make sure we have at least three points in violation.
Hint: Note the type
for the parameter_values
argument is Sequence[Sequence[float]]
.
angles = # create a parameterization of angles that will violate the inequality
Validation:¶
Test your angles and observable by running with the Estimator
.
estimator = Estimator()
job = estimator.run([qc]*len(angles), observables=[obsv]*len(angles), parameter_values=angles)
exps = job.result().values
plt.plot(angles, exps, marker='x', ls='-', color='green')
plt.plot(angles, [2]*len(angles), ls='--', color='red', label='Classical Bound')
plt.plot(angles, [-2]*len(angles), ls='--', color='red')
plt.xlabel('angle (rad)')
plt.ylabel('CHSH Witness')
plt.legend(loc=4)
Did you see at least 3 points outside the red dashed lines? If so, you prooved (experimentaly) clear violation of the CHSH Inequality!
Teleportation¶
Quantum information cannot be copied due to the No Cloning Theorem, however it can be "teleported" in the sense that a qubit can be entangled with a quantum resource, and via a protocol of measurements and classical communication of their results, the original quantum state can be reconstructed on a different qubit. This process destroys the information in the original qubit via measurement.
In this exercise, we will construct a particular qubit state and then transfer that state to another qubit using the teleportation protocol. Here we will be looking at specific classical and quantum registers, so we need to import those.
from qiskit.circuit import ClassicalRegister, QuantumRegister
Create the circuit¶
Define an angle $\theta$ to rotate our qubit by. This will allow us to easily make comparisons for the original state and the teleported state.
theta = Parameter('θ')
qr = QuantumRegister(1, 'q')
qc = QuantumCircuit(qr)
qc.ry(theta, 0)
qc.draw('mpl')
Alice possesses the quantum information $|\psi\rangle$ in the state of $q$ and wishes to transfer it to Bob. The resource they share is a special entangled state called a Bell state
$$
|\Phi^+\rangle = \frac{1}{2} \left( |00\rangle + |11\rangle \right)
$$
with the first of the pair going to Alice and the second to Bob. Hence Alice has a 2-qubit register ($q$ and $Bell_0$) and Bob has a single-qubit register ($Bell_1$). We will construct the circuit by copying the original qc
and adding the appropriate registers.
tele_qc = qc.copy()
bell = QuantumRegister(2, 'Bell')
alice = ClassicalRegister(2, 'Alice')
bob = ClassicalRegister(1, 'Bob')
tele_qc.add_register(bell, alice, bob)
tele_qc.draw('mpl')
Now create the Bell pair with $Bell_0$ going to Alice and $Bell_1$ going to Bob. This is done by using a Hadamard gate to put $Bell_0$ in the $|+\rangle$ state and then performing a CNOT with the same qubit as the control. After they receive their respective qubit, they part ways.
# create Bell state with other two qubits
tele_qc.barrier()
# Add your code here ...
tele_qc.barrier()
tele_qc.draw('mpl')
Next, Alice performs a CNOT controlled by $q$ on $Bell_0$, which maps information about the state onto it. She then applies a Hadamard gate on $q$.
# alice operates on her qubits
# Add your code here ...
tele_qc.barrier()
tele_qc.draw('mpl')
Now Alice measures her qubits and saves the results to her register.
tele_qc.measure([qr[0], bell[0]], alice)
tele_qc.draw('mpl')
Bob's qubit now has the information $|\psi\rangle$ from Alice's qubit $q$ encoded in $Bell_1$, but he does not know what basis to measure in to extract it. Accordingly, Alice must share the information in her register over a classical communication channel (this is why teleportation does not violate special relativity, no matter how far Alice and Bob are apart). She instructs Bob to perform an X gate on his qubit if her measurement of $Bell_0$ yields a 1 outcome, followed by a Z gate if her measurement of $q$ yields a 1.
The applications of these gates can be conditioned on the measurement outcomes in two ways:
- the
.c_if()
instruction, which applies the gate it modifies if the value of theClassicalRegister
index is equal to the value specified. Note that this works only on simulators. - the
.if_test()
context which operates similarly, but generalizes the syntax to allow for nested conditionals. This works on both simulators and actual hardware.
graded_qc = tele_qc.copy()
##############################
# add gates to graded_qc here
##############################
graded_qc.draw('mpl')
Finally, Bob can measure his qubit, which would yield results with the same probabilities as had Alice measured it originally.
graded_qc.barrier()
graded_qc.measure(bell[1], bob)
graded_qc.draw('mpl')
The statevector simulator cannot work with dynamic circuits because measurement is not a unitary operation. Therefore we import the Sampler
primitive from qiskit_aer
to use the AerSimulator
. We choose our angle to be $5\pi/7$, which will yield a 1 result about 80% of the time and 0 result about 20% of the time. Then we run both circuits: the original one Alice had and the teleported one Bob receives.
from qiskit_aer.primitives import Sampler
angle = 5*np.pi/7
sampler = Sampler()
qc.measure_all()
job_static = sampler.run(qc.bind_parameters({theta: angle}))
job_dynamic = sampler.run(graded_qc.bind_parameters({theta: angle}))
print(f"Original Dists: {job_static.result().quasi_dists[0].binary_probabilities()}")
print(f"Teleported Dists: {job_dynamic.result().quasi_dists[0].binary_probabilities()}")
Original Dists: {'0': 0.1904296875, '1': 0.8095703125} Teleported Dists: {'011': 0.041015625, '111': 0.2265625, '001': 0.0458984375, '100': 0.2001953125, '110': 0.1943359375, '101': 0.193359375, '010': 0.048828125, '000': 0.0498046875}
Wait, we see different results! While measuring Alice's original $q$ yields the expected ratio of outcomes, the teleported distributions have many more values. This is because the teleported circuit includes Alice's measurements of $q$ and $Bell_0$, whereas we only wish to see Bob's measurements of $Bell_1$ yield the same distribution.
In order to rectify this, we must take the marginal counts, meaning we combine results in which Bob measures a 0 and all the results in which Bob measures a 1 over all the possible combinations. This is done with the marginal_counts
method from qiskit.result
, which combines results over measurement indices.
Hint: Remember that bit strings are reported in the little-endian convention.
from qiskit.result import marginal_counts
tele_counts = # marginalize counts
Validation:¶
If we marginalized correctly, we will see that the quasi-distributions from Alice's measurement and Bob's measurement are nearly identical, demonstrating that teleportation was successful!
legend = ['Original State', 'Teleported State']
plot_histogram([job_static.result().quasi_dists[0].binary_probabilities(), tele_counts], legend=legend)
import qiskit.tools.jupyter
%qiskit_version_table
Version Information
Qiskit Software | Version |
---|---|
qiskit-terra | 0.24.2 |
qiskit-aer | 0.12.2 |
qiskit-ignis | 0.7.1 |
qiskit-ibmq-provider | 0.20.2 |
qiskit | 0.43.3 |
qiskit-nature | 0.4.2 |
qiskit-machine-learning | 0.4.0 |
System information | |
Python version | 3.9.12 |
Python compiler | Clang 12.0.0 |
Python build | main, Apr 5 2022 01:53:17 |
OS | Darwin |
CPUs | 4 |
Memory (Gb) | 32.0 |
Fri Oct 27 10:19:23 2023 EDT |
pip install qiskit --upgrade