This notebook teleports a qubit from one side of an IBM Eagle quantum computer chip to the other.
Some effort went into ensuring the code actually does this, and doesn't just teleport from one arbitrary qubit to another.
import qiskit
import qiskit_ibm_runtime
Get a quantum computer. So far this is a simulator, to get the program working without eating real quantum computing resources.
simulate = False
noise = True
if (noise or not simulate):
import qiskit_ibm_runtime as ibm
service = ibm.QiskitRuntimeService()
ibm_backend = service.backend("ibm_kyoto") #pick this fairly noisy one to start.
if simulate:
import qiskit_aer
if not noise:
backend = qiskit_aer.AerSimulator()
else:
backend = qiskit_aer.AerSimulator.from_backend(ibm_backend)
else:
backend = ibm_backend
num_qubits = 17 #17 is the most ibl_kyoto handles with reasonable noise.
The coupling_map tells you the way the qubits are coupled for ecr gates. e.g. [1,0] means you can do ecr with qubit 1 as the control and qubit 0 as the target. If you do ecr in the other direction, it adds local operations and hence noise.
if noise:
coupling_map = ibm_backend.coupling_map
else:
coupling_list = [(i, i+1) for i in range(num_qubits-1)]
coupling_map = qiskit.transpiler.CouplingMap(coupling_list)
coupling_map.draw()
Specify which physical qubits we will use for our logical qubits. Choose them based on their noise, as e.g. Readout Assignment Error for a single qubit can be as high as 40%!
#This is one path across the eagle chip, which seems not too noisy on a qubit and ecr level. Check the latest calibration results on the chip you are using!
longest_layout = [13,12,17,30,29,28,35,47,46,45,54,64,63,62,72,81,80,79,91,98,97,96,109,114,113]
if noise: #We are based on the eagle chip, so can set the layout
initial_layout = longest_layout[0:num_qubits]
else:
initial_layout = range(num_qubits)
if len(initial_layout) != num_qubits:
raise ValueError('Wrong number of qubits in initial layout')
#alternative method for specifying layout
#a, b = qubits
#initial_layout = Layout({a: 5, b: 6})
Build a quantum circuit with n qubits and n classical bits (n odd). The qubits are initialized as |0>'s. We shall teleport from the first qubit to the last.
qubits = qiskit.QuantumRegister(num_qubits, name = "qubits")
#One pair of cbits for each Bell Measurement
bell_cbits = [qiskit.ClassicalRegister(2, name = "bell_cbits_" + str(i)) for i in range(0, num_qubits-1, 2)]
final_cbit = qiskit.ClassicalRegister(1, name = "final_cbit")
qc = qiskit.QuantumCircuit(qubits, *bell_cbits, final_cbit)
Setup a state to be teleported. This is parameterized, so we can teleport many different states. Thus far it's essentially $\frac{1}{\sqrt{2}}(|0> - i e^{i \phi} |1>)$. Use native processor gates for the ibm eagle processor, to minimize the number of gates and hence the error.
phi = qiskit.circuit.Parameter('phi')
qc.sx(0)
qc.rz(phi,0)
<qiskit.circuit.instructionset.InstructionSet at 0x16fa12350>
Create a list of parameters for the initial state.
import numpy
num_phis = 4
phi_values = numpy.linspace(0, numpy.pi/2, num_phis)
Setup the pre-shared entanglement, using the native ecr direction. We get the same entangled state whatever the native direction.
def ecr_order(physical_qubits, edges):
if physical_qubits in edges:
entanglement_order = 0
else:
reversed_qubits = (physical_qubits[1], physical_qubits[0])
if reversed_qubits in edges:
entanglement_order = 1
else:
raise ValueError('These physical qubits are not linked')
return entanglement_order
edges = coupling_map.get_edges()
#Create the entangled pairs for logical qubits (1,2), (3,4) etc.
for i in range(1, qc.num_qubits, 2):
physical_qubits = (initial_layout[i], initial_layout[i+1])
entanglement_order = ecr_order(physical_qubits, edges)
if entanglement_order == 0:
qc.sx(i)
qc.ecr(i,i+1)
else:
qc.sx(i+1)
qc.ecr(i+1,i)
qc.barrier()
CircuitInstruction(operation=Instruction(name='barrier', num_qubits=17, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(17, 'qubits'), 0), Qubit(QuantumRegister(17, 'qubits'), 1), Qubit(QuantumRegister(17, 'qubits'), 2), Qubit(QuantumRegister(17, 'qubits'), 3), Qubit(QuantumRegister(17, 'qubits'), 4), Qubit(QuantumRegister(17, 'qubits'), 5), Qubit(QuantumRegister(17, 'qubits'), 6), Qubit(QuantumRegister(17, 'qubits'), 7), Qubit(QuantumRegister(17, 'qubits'), 8), Qubit(QuantumRegister(17, 'qubits'), 9), Qubit(QuantumRegister(17, 'qubits'), 10), Qubit(QuantumRegister(17, 'qubits'), 11), Qubit(QuantumRegister(17, 'qubits'), 12), Qubit(QuantumRegister(17, 'qubits'), 13), Qubit(QuantumRegister(17, 'qubits'), 14), Qubit(QuantumRegister(17, 'qubits'), 15), Qubit(QuantumRegister(17, 'qubits'), 16)), clbits=())
Display the circuit thus far: a state to be teleported and pre-shared entanglement.
qc.draw(output='mpl')
Perform one Bell Measurement on the initial qubit and the adjacent entangled qubit, and further ones to link the entangled pairs together, moving the state from qubit 0 to the final qubit.
#Arrange the measurements to match the direction of the underlying hardware ecr gates.
measure_orders = []
for i in range(1, qc.num_qubits, 2):
physical_qubits = (initial_layout[i-1], initial_layout[i])
measure_order = ecr_order(physical_qubits, edges)
measure_orders.append(measure_order) #We'll need these when interpreting the outcomes
if measure_order == 0:
qc.ecr(i-1,i)
qc.sx(i-1)
else:
qc.ecr(i,i-1)
qc.sx(i)
qc.measure([i-1, i],[i-1,i])
qc.barrier()
CircuitInstruction(operation=Instruction(name='barrier', num_qubits=17, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(17, 'qubits'), 0), Qubit(QuantumRegister(17, 'qubits'), 1), Qubit(QuantumRegister(17, 'qubits'), 2), Qubit(QuantumRegister(17, 'qubits'), 3), Qubit(QuantumRegister(17, 'qubits'), 4), Qubit(QuantumRegister(17, 'qubits'), 5), Qubit(QuantumRegister(17, 'qubits'), 6), Qubit(QuantumRegister(17, 'qubits'), 7), Qubit(QuantumRegister(17, 'qubits'), 8), Qubit(QuantumRegister(17, 'qubits'), 9), Qubit(QuantumRegister(17, 'qubits'), 10), Qubit(QuantumRegister(17, 'qubits'), 11), Qubit(QuantumRegister(17, 'qubits'), 12), Qubit(QuantumRegister(17, 'qubits'), 13), Qubit(QuantumRegister(17, 'qubits'), 14), Qubit(QuantumRegister(17, 'qubits'), 15), Qubit(QuantumRegister(17, 'qubits'), 16)), clbits=())
Draw the circuit so far:
qc.draw(output='mpl')
Next adjust the target qubit by the outcome of the Bell Measurement. In the regular order, if the outcome is 00 or 10, do Z. If outcome is 10 or 11, do X. These operations commute, so if we do entanglement swapping we can easily add the bell measurement outcomes and apply X or I and Z or I to the final state: no need to do multiply X's, Z's or anything else. Similarly in the reverse order.
target_num = num_qubits-1
num_pairs = round((num_qubits-1)/2)
import qiskit.circuit.classical.expr as expr
#Apply an X gate if x_expr is true, instead of many sequential X gates, potentially one for each pair.
#Similar for z gate.
for i in range(0, num_pairs):
c_register = bell_cbits[i]
if measure_orders[i] == 0:
pair_x_expr = c_register[0]
else:
pair_x_expr = expr.equal(c_register[0], c_register[1]) #Flip if 00 or 11
pair_z_expr = expr.logic_not(c_register[1])
if i==0:
x_expr = pair_x_expr
z_expr = pair_z_expr
else:
x_expr = expr.not_equal(pair_x_expr, x_expr) #two X gates make Identity.
z_expr = expr.not_equal(pair_z_expr, z_expr)
with qc.if_test(x_expr) as else_:
qc.x(target_num)
with qc.if_test(z_expr) as else_:
qc.rz(numpy.pi, target_num)
qc.barrier()
CircuitInstruction(operation=Instruction(name='barrier', num_qubits=17, num_clbits=0, params=[]), qubits=(Qubit(QuantumRegister(17, 'qubits'), 0), Qubit(QuantumRegister(17, 'qubits'), 1), Qubit(QuantumRegister(17, 'qubits'), 2), Qubit(QuantumRegister(17, 'qubits'), 3), Qubit(QuantumRegister(17, 'qubits'), 4), Qubit(QuantumRegister(17, 'qubits'), 5), Qubit(QuantumRegister(17, 'qubits'), 6), Qubit(QuantumRegister(17, 'qubits'), 7), Qubit(QuantumRegister(17, 'qubits'), 8), Qubit(QuantumRegister(17, 'qubits'), 9), Qubit(QuantumRegister(17, 'qubits'), 10), Qubit(QuantumRegister(17, 'qubits'), 11), Qubit(QuantumRegister(17, 'qubits'), 12), Qubit(QuantumRegister(17, 'qubits'), 13), Qubit(QuantumRegister(17, 'qubits'), 14), Qubit(QuantumRegister(17, 'qubits'), 15), Qubit(QuantumRegister(17, 'qubits'), 16)), clbits=())
Finally measure $\sigma_y$ on the target qubit, i.e. in the |0>+i|1> vs |0>-i|1> basis. This is chosen so that it changes with the input state, and can be done with a native .sx operation followed by measuring the computational basis.
qc.sx(target_num)
qc.measure(target_num, target_num)
<qiskit.circuit.instructionset.InstructionSet at 0x32c6259f0>
Display our final logical circuit.
qc.draw(output='mpl')
Convert our circuit into native operations for the backend, specifying which logical qubit is assigned to which physical qubit. To reduce noise, this should have as few layers as possible, and should look the same as our non-converted circuit as we attempted to use only native operations.
qc_transpiled = qiskit.transpile(qc, backend, initial_layout=initial_layout)
qc_transpiled.draw(output='mpl', idle_wires=False)
Finally, we can run our experiment!
load_results = True
if not load_results:
#How do we set max_execution_time (defined in seconds)?
sampler = qiskit_ibm_runtime.SamplerV2(backend, options={"default_shots": 4096})
job = sampler.run([(qc_transpiled, phi_values)])
if not simulate:
print(job.usage_estimation)
The job might be in the queue for a few hours before it runs, so get the job_id so we can retreive it later.
if not load_results:
print(job.job_id())
crk9fvq14ys000888f00
In case we need to load the job later
if load_results:
service = ibm.QiskitRuntimeService()
job = service.job('crk9fvq14ys000888f00'); #The result of job_id()
results = job.result()
Process the results.
#get the measurement output counts just for the final (output) qubit
marginal_counts = [ results[0].data.final_cbit.get_counts(c) for c in range(results[0].data.final_cbit.size)]
#One doc claimed you can't modify this when dynamic=True, but it does change as this shows.
num_shots = sum(marginal_counts[0].values())
print(num_shots)
4096
Calculate the expectation of the $\sigma_y$ operator. $-1 \le E(\sigma_y) \le +1$.
#To do sigma_y we applied sqrt(X) then measured $\sigma_z$. Measurement outcome 0 corresponds to $\sigma_z = +1$, i.e. $\sigma_y = +1$.
E_sigma_y = [qiskit.result.sampled_expectation_value(counts, 'Z') for counts in marginal_counts]
Plot the the output across various input states.
import matplotlib
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 6))
# results from hardware
ax.plot(phi_values / numpy.pi, E_sigma_y, "o-", label="QuantumSim", zorder=3)
#ax.plot(phis / numpy.pi, E_sigma_z, "o-", label="QuantumComp", zorder=3)
# classical bound +-0.5
ax.axhline(y=0.5, color="0.9", linestyle="--")
ax.axhline(y=-0.5, color="0.9", linestyle="--")
# quantum bound, +-1
ax.axhline(1, color="0.9", linestyle="-.")
ax.axhline(-1, color="0.9", linestyle="-.")
ax.fill_between(phi_values / numpy.pi, 1, 0.5, color="0.6", alpha=0.7)
ax.fill_between(phi_values / numpy.pi, -1, -0.5, color="0.6", alpha=0.7)
# set x tick labels to the unit of pi.
ax.xaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter('%g $\\pi$'))
ax.xaxis.set_major_locator(matplotlib.ticker.MultipleLocator(base=0.5))
# set labels, and legend
plt.xlabel("Phi")
plt.ylabel("Teleportation")
plt.legend()
plt.show()
We are finished. Note the quantum computer and library versions as they change often, and the code might not run on other versions.
backend.name
if not simulate:
print(backend.processor_type)
Note: the Eagle chip used in ibm_brisbane is approx 25mm wide.
We naively guess across the qubits inside the chip is perhaps 15mm.
qiskit_ibm_runtime.version.get_version_info()
qiskit.version.get_version_info()