Averaged Circuit Eigenvalue Sampling with Qiskit Superstaq
This notebook demonstrates how to characterize a quantum device using the averaged circuit eigenvalue sampling (ACES) protocol through Superstaq. This protocol is integrated into Superstaq following the original paper by Steven T. Flammia and can be accessed using qiskit-superstaq
.
Imports and API Token
This example tutorial notebook uses qiskit-superstaq
, our Superstaq client for Qiskit,
[1]:
try:
import qiskit
import qiskit_superstaq as qss
except ImportError:
print("Installing qiskit-superstaq...")
%pip install --quiet 'qiskit-superstaq[examples]'
print("Installed qiskit-superstaq.")
print("You may need to restart the kernel to import newly installed packages.")
import qiskit
import qiskit_superstaq as qss
[2]:
# Required imports
import numpy as np
import itertools
# Optional imports
import os # Used if setting a token as an environment variable
To interface Superstaq via Qiskit, we must first instantiate a provider in qiskit-superstaq
with SuperstaqProvider()
. We then supply a Superstaq API token (or key) by either providing the API token as an argument of qss.SuperstaqProvider()
or by setting it as an environment variable (see more details here).
[3]:
# Get the qiskit superstaq provider for Superstaq backend
provider = qss.SuperstaqProvider()
ACES
To submit an ACES job, you will need the following information:
target
: device that you want to characterize.qubits
: indices of the qubits to characterize.shots
: number of shots to use per circuit to run.num_circuits
: number of random circuits to sample in order to get the gate eigenvalues.mirror_depth
: for each circuit, how many mirrored moments to include.extra_depth
: for each circuit, how many random moments to include.method
: the type of execution method. Ifmethod="noise-sim"
, then optional argumentsnoise
anderror_prob
must be passed as well.
With this information, you can submit an ACES job through the submit_aces
method. This will take care of constructing the circuits needed to perform the protocol and will execute them.
[4]:
job_id = provider.submit_aces(
target="ss_unconstrained_simulator",
qubits=[0, 1],
shots=100,
num_circuits=5,
mirror_depth=4,
extra_depth=7,
method="dry-run",
)
[5]:
job_id
[5]:
'68f5d0a8-92ff-4430-b6ee-5d4200b07a58'
We note that the circuits created by this protocol can take long to execute, especially if submitted to a real device, so it might be convenient to save the job id somewhere. Once the jobs have finished running, calling process_aces
and passing it the job id will compute the individual gate eigenvalues.
[6]:
result = provider.process_aces(job_id)
[7]:
np.array(result)
[7]:
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
A gate eigenvalue is a measure of how well a gate is executed by the device we are characterizing. It is given by the following equation
where \(\tilde{G}\) is the noisy implementation of the ideal gate \(G\), \(P\) is a Pauli operator, and \(\Lambda_{G, P}\) is the gate eigenvalue corresponding to that gate and Pauli pair. \(G(P)\) is the conjugation of \(P\) by \(G\), i.e., \(G P G^\dagger\).
The output is a list of estimated circuit eigenvalues. For each qubit, we consider six Clifford gates, given by the XZ maps: XZ, ZX, -YZ, -XY, ZY, and YX. For each of these, there are three eigenvalues: X, Y, and Z. All the one-qubit eigenvalues are returned first. Then, the only two-qubit gate considered is the CZ in linear connectivity. For this gate, there are 15 eigenvalues: XX, XY, XZ, XI, YX, YY, YZ, YI, ZX, ZY, ZZ, ZI, IX, IY, and IZ. Therefore, for the above example of two qubits, there are \(18\cdot2 + 14 = 51\) eigenvalues.
Since we used "ss_unconstrained_simulator"
as a target, the results are not very interesting because all the circuits are simulated without a noise model, so we get that every circuit eigenvalue is 1 since every gate is simulated perfectly.
ACES with noise
A more interesting example is given when we use method="noise-sim"
. We now have to specify the arguments noise
and error_prob
(see the documentation for more information on these). Here, we are going to use an asymmetric depolarizing channel, with the error probabilities 0.05, 0.11, and 0.08 for the X, Y, and Z gates respectively.
[8]:
noise_job_id = provider.submit_aces(
target="ss_unconstrained_simulator",
qubits=[0, 1],
shots=100,
num_circuits=5,
mirror_depth=4,
extra_depth=7,
method="noise-sim",
noise="asymmetric_depolarize",
error_prob=(0.05, 0.11, 0.08),
)
[9]:
noise_job_id
[9]:
'5c04c005-ac3c-478f-b3e9-4ae2c8e775b3'
[10]:
noise_result = provider.process_aces(noise_job_id)
[11]:
np.array(noise_result)
[11]:
array([0.22506795, 0.75664239, 1. , 1. , 0.87474481,
1. , 1. , 0.94793303, 0.86153607, 0.58656859,
1. , 1. , 1. , 0.6490385 , 0.80166365,
1. , 1. , 1. , 0.51738399, 1. ,
0.72798301, 1. , 1. , 1. , 1. ,
0.65592175, 1. , 1. , 1. , 1. ,
0.95206672, 0.93797758, 1. , 0.59267365, 1. ,
1. , 0.35966265, 0.88632776, 0.63868724, 1. ,
0.39035178, 1. , 0.27895623, 1. , 1. ,
0.35345374, 1. , 0.74321218, 1. , 0.78580319,
0.76095787])
The output is now much more interesting, since we have a more diverse set of eigenvalues due to the noise channel we have introduced.