Averaged Circuit Eigenvalue Sampling with Cirq Superstaq

Open in Colab Launch Binder

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 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
import itertools

# 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()

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. If method="noise-sim", then optional arguments noise and error_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 = service.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]:
'c4428c69-5048-43f9-99e2-f005620d4134'

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 = service.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

\[\tilde{G}(P) = \Lambda_{G, P} G(P)\]

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 = service.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]:
'd0dfbdd2-6945-4930-bbea-f6538f517004'
[10]:
noise_result = service.process_aces(noise_job_id)
[11]:
np.array(noise_result)
[11]:
array([1.        , 1.        , 0.43729089, 1.        , 1.        ,
       1.        , 0.37156509, 1.        , 0.62674301, 0.85195808,
       1.        , 0.93597888, 0.91775969, 1.        , 1.        ,
       1.        , 0.33541873, 1.        , 1.        , 1.        ,
       0.95034448, 1.        , 1.        , 1.        , 1.        ,
       0.40710778, 1.        , 1.        , 1.        , 0.87884797,
       1.        , 0.84014817, 0.34949293, 1.        , 0.66028861,
       1.        , 1.        , 1.        , 0.49340862, 1.        ,
       1.        , 0.21858458, 1.        , 0.25613083, 1.        ,
       1.        , 1.        , 0.4963732 , 1.        , 1.        ,
       0.89530943])

The output is now much more interesting, since we have a more diverse set of eigenvalues due to the noise channel we have introduced.