Direct Fidelity Estimation with Cirq Superstaq
This notebook demonstrates how to run a estimate the fidelity between two quantum states prepared in different devices using Superstaq. The direct fidelity estimation protocol is integrated into Superstaq following Cross-Platform Verification of Intermediate Scale Quantum Devices and can be accessed using cirq-superstaq
.
Imports and API Token
This example tutorial notebook uses cirq-superstaq
, our Superstaq client for Cirq,
[1]:
try:
import cirq
import cirq_superstaq as css
except ImportError:
print("Installing cirq-superstaq...")
%pip install --quiet 'cirq-superstaq[examples]'
print("Installed cirq-superstaq.")
print("You may need to restart the kernel to import newly installed packages.")
import cirq
import cirq_superstaq as css
[2]:
# Required imports
import numpy as np
# Optional imports
import os # Used if setting a token as an environment variable
To interface Superstaq via Cirq, we must first instantiate a service provider in cirq-superstaq
with Service()
. We then supply a Superstaq API token (or key) by either providing the API token as an argument of css.Service()
or by setting it as an environment variable (see more details here).
[3]:
# Get cirq-superstaq service for Superstaq backend
service = css.Service()
Randomized measurements background
The core idea behind this protocol is the use of random measurements to measure the overlap between two states \(\rho_1\) and \(\rho_2\), defined as \(\mathrm{Tr}(\rho_1 \rho_2)\). To do this, we measure both states in the same randomized Clifford product bases of the form \(C_1 \otimes \cdots \otimes C_N\) where each \(C_i\) is a one qubit Clifford and we are comparing \(N\)-qubit (sub)systems. We then compare the distributions over all random measurements to get estimate the overlap between the two states.
Succintly, the protocol goes as follows:
Apply the same random unitary \(U_j = C_1 \otimes \cdots \otimes C_N\) to both states.
Measure both states in the standard computational basis \(\left(|0\rangle^{\otimes N}, |1\rangle^{\otimes N}\right)\).
Repeat these measurementes for a fixed random basis to get an estimate of \(P_{U_j}^i(x) = \mathrm{Tr}\left( U_j \rho_i U_j^\dagger |x\rangle\langle x| \right)\) for all \(x \in \{0, 1\}^{\otimes N}\).
Repeat steps 1-3 for a collection of random unitaries \(U = \{U_1, \cdots U_M\}\).
With this data, we can calculate the overlap between the two states as:
Where \(M = |U|\) is the number of random unitaries, and \(\mathcal{D}(x, x')\) is the hamming distance between two bitstrings (i.e., the number of positions at which the bits are different). Fidelity is then computed as:
Where we compute the purities in the denominator with the same formula as the overlap but setting both \(\rho_i\) to be the same.
Running DFE
Identical states
To run a DFE protocol, we have to define circuits that prepare the states we want to compare.
[4]:
equal_superposition = cirq.Circuit(cirq.H(cirq.q(0)))
We also have to specify the target in which we want to prepare the states. These two pieces of information are what make up a state to be passed to submit_dfe
, which is a tuple with the circuit that prepares the state as its first element and the target as its second element.
[5]:
target = "ss_unconstrained_simulator"
rho = (equal_superposition, target)
rho
[5]:
(0: ───H───, 'ss_unconstrained_simulator')
With this, we can run the protocol comparing the state we defined to itself.
[6]:
ids = service.submit_dfe(
rho_1=rho,
rho_2=rho,
num_random_bases=50,
shots=1000,
)
result = service.process_dfe(ids)
[7]:
print(result)
0.9987554770271813
As we can see above, we get a fidelity very close to 1, as expected for identical states.
Orthogonal states
To test our protocol is giving sensible results, we can run it on orthogonal states.
[8]:
rho_1 = (cirq.Circuit(cirq.I(cirq.q(0))), target) # |0>
rho_1
[8]:
(0: ───I───, 'ss_unconstrained_simulator')
[9]:
rho_2 = (cirq.Circuit(cirq.X(cirq.q(0))), target) # |1>
rho_2
[9]:
(0: ───X───, 'ss_unconstrained_simulator')
To get an idea of how many measurements and shots should be used depending on the number of qubits and any given information about the states, refer to Figure 2 and related text in the paper linked at the beginning of this tutorial.
[10]:
ids = service.submit_dfe(
rho_1=rho_1,
rho_2=rho_2,
num_random_bases=50,
shots=1000,
)
result = service.process_dfe(ids)
[11]:
print(result)
0.05222673354848918
We get a fidelity close to 0, expected for orthogonal states.
A more interesting example
Let’s say we want to compare how two different devices prepare two different states. To do this, we can simply set the target for each state to be whatever device we want (as long as you have access to it). We will set method="dry-run"
for now to simulate the results, but if this argument is removed the circuits will be submitted to the real backend.
[12]:
state_1 = cirq.Circuit(
cirq.H(cirq.q(0)),
cirq.CX(cirq.q(0), cirq.q(1)),
)
state_1
[12]:
0: ───H───@─── │ 1: ───────X───
[13]:
state_2 = cirq.Circuit(
cirq.H(cirq.q(0)),
cirq.I(cirq.q(1)),
)
state_2
[13]:
0: ───H─── 1: ───I───
[14]:
rho_1 = (state_1, "ibmq_belem_qpu")
rho_2 = (state_2, "ibmq_manila_qpu")
[15]:
ids = service.submit_dfe(
rho_1=rho_1,
rho_2=rho_2,
num_random_bases=50,
shots=5000,
method="dry-run", # Remove this argument to run on real devices
)
result = service.process_dfe(ids)
[16]:
print(result)
0.25214091972457
We can see how our estimation compares to the ideal value by using the formula for fidelity between pure states.
[17]:
np.trace(cirq.final_density_matrix(state_1) @ cirq.final_density_matrix(state_2))
[17]:
(0.24999997+0j)