QSCOUT Optimizations with Cirq

Open in Colab Launch Binder

Below is a brief tutorial on Superstaq optimizations for Quantum Scientific Computing Open User Testbed (QSCOUT), a trapped ion quantum computing testbed at Sandia National Laboratories. For more information on QSCOUT, visit their website here.

Imports and API Token

This example tutorial notebook uses cirq-superstaq, our Superstaq client for Cirq; you can try it out by running pip install cirq-superstaq:

[1]:
# Required imports
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

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).

[2]:
# Get cirq superstaq service for Superstaq backend
service = css.Service()

Single Circuit Compilation

Let us start by creating an example qiskit circuit that we will then compile and optimize for the QSCOUT trapped-ion testbed at Sandia National Laboratories.

[3]:
# Create a two-qubit cirq circuit
theta = np.random.uniform(0, 4 * np.pi)
qubits = cirq.LineQubit.range(2)
circuit1 = cirq.Circuit(
    cirq.CX(qubits[0], qubits[1]),
    cirq.rz(theta).on(qubits[1]),
    cirq.CX(qubits[0], qubits[1]),
    cirq.measure(qubits[0], qubits[1]),
)

# Draw circuit for visualization
print(circuit1)
0: ───@───────────────@───M───
      │               │   │
1: ───X───Rz(1.11π)───X───M───

Using the same circuit from above as input, we will now compile it for QSCOUT and visualize the differences by printing the compiled circuit.

[4]:
# Compile with qscout compile
compiler_output = service.qscout_compile(circuit1)

# Call circuit from the compiler output to get the corresponding output circuit
output_circuit = compiler_output.circuit

# Visualize the compiled circuit
print(compiler_output.circuit)
0: ───PhX(-0.5)^0.5───Z───MS(0.056π)───PhX(-0.5)^0.5───│───M───
                          │                            │   │
1: ───PhX(-0.5)^0.5───Z───MS(0.056π)───PhX(-0.5)^0.5───│───M───

The resulting output is now a circuit compiled and optimized to QSCOUT’s native operations. But there’s more! With Superstaq’s compilation, users can also get the corresponding Jaqal (see Just another quantum assembly language) program for their compiled circuit. The Jaqal program is a useful representation that highlights the sequence of pulse level single and two-qubit gates that have to be executed on the trapped ions to realize the user’s circuit. To view the jaqal program, users simply have to call jaqal_program on their compiler_output, and print to list it in a readable format.

[5]:
# Get jaqal program
print(compiler_output.jaqal_program)
from qscout.v1.std usepulses *

register allqubits[2]

prepare_all
<
        R allqubits[0] -1.5707963267948966 1.5707963267948966
        R allqubits[1] -1.5707963267948966 1.5707963267948966
>
<
        Rz allqubits[0] -3.141592653589793
        Rz allqubits[1] -3.141592653589793
>
MS allqubits[0] allqubits[1] 0 0.3525105540780351
<
        R allqubits[0] -1.5707963267948966 1.5707963267948966
        R allqubits[1] -1.5707963267948966 1.5707963267948966
>
measure_all

Breaking down the printed Jaqal program, we see that we begin a 2-qubit quantum register with allqubits[2] in correspondance to our 2-qubit circuit. The prepare_all command prepares the state of all the qubits in the register in the \(z\) basis as the standard. Next, the program describes the pulse-level gates to be used following the format outlined and described here. For a quick reference, here is a brief description of the pulse-level gates and operations that are used in a Jaqal program:

  • R <qubit> <axis-angle> <rotation-angle> : Performs a counter-clockwise rotation around an axis in the equatorial plane of the Bloch sphere defined by <axis-angle> , measured counter-clockwise from the \(x\) axis, by the angle defined by <rotation-angle>.

  • Rx <qubit> <rotation-angle> : Performs a counter-clockwise rotation around the \(x\) axis, by the angle defined by <rotation angle>.

  • Ry <qubit> <rotation-angle> : Performs a counter-clockwise rotation around the \(y\) axis, by the angle defined by <rotation-angle>.

  • Rz <qubit> <angle> : Performs a counter-clockwise rotation around the \(z\) axis, by the angle defined by <rotation-angle>.

  • Px <qubit> : Performs a counter-clockwise rotation around the \(x\) axis, by \(\pi\). In other words, a Pauli \(X\) gate.

  • Py <qubit>> : Performs a counter-clockwise rotation around the \(y\) axis, by \(\pi\). In other words, Pauli \(Y\) gate.

  • Pz <qubit> : Performs a counter-clockwise rotation around the \(z\) axis, by \(\pi\). In other words, Pauli \(Z\) gate.

  • Sx <qubit> : Performs a counter-clockwise rotation around the \(x\) axis, by \(\pi/2\). This results in a \(\sqrt{X}\) gate.

  • Sy <qubit> : Performs a counter-clockwise rotation around the \(y\) axis, by \(\pi/2\). This results in a \(\sqrt{Y}\) gate.

  • Sz <qubit> : Performs a counter-clockwise rotation around the \(z\) axis, by \(\pi/2\). This results in a \(\sqrt{Z}\) gate.

  • Sxd <qubit> : Performs a clockwise rotation around the \(x\) axis, by \(\pi/2\). That is, a \(\sqrt{X}^\dagger\) gate.

  • Syd <qubit> : Performs a clockwise rotation around the \(y\) axis, by \(\pi/2\). That is, a \(\sqrt{Y}^\dagger\) gate.

  • Szd <qubit> : Performs a clockwise rotation around the \(z\) axis, by \(\pi/2\). That is, a \(\sqrt{Z}^\dagger\) gate.

  • MS <qubit> <qubit> <axis-angle> <rotation-angle> : This is the general two-qubit Mølmer–Sørensen gate used for entanglement. Supposing that \(\theta\) and \(\varphi\) represent the <rotation-angle> and <axis-angle> respectively, the general MS gate is given by,

    \[\exp\left(-i\left(\frac{\theta}{2}\right)(\cos \varphi X + \sin \varphi Y)^{\otimes 2}\right).\]
  • Sxx <qubit> <qubit> : This is the XX version of two-qubit Mølmer–Sørensen gate,

    \[\exp \left(-i\left(\frac{\pi}{4}\right) X\otimes X\right).\]
  • measure_all : Command to measure all qubits of the quantum register in the \(z\) basis.

The Jaqal program output is also very useful is showcasing when multiple gates are combined into a single gate block for execution – or in the case of parallel gate blocks – executed at the same time. These gate blocks are marked by the angle brakets like so,

```
<
  // pulse-level gates
>
```

providing the user more valuable context between the device execution and the original circuit representation. Further details on the verstaility of the Jaqal program can be found at this reference.

Multiple Circuits Compilation

All the functionalities we have seen so far can also be used on a multiple circuits input as well. To illustrate this, let us create a different, example two-qubit circuit (say, a Bell-state circuit):

[6]:
# Create second circuit
qubits = cirq.LineQubit.range(2)
circuit2 = cirq.Circuit(
    cirq.H(qubits[0]),
    cirq.CX(qubits[0], qubits[1]),
    cirq.measure(qubits[0], qubits[1]),
)

# Printing second circuit for visualization
print(circuit2)
0: ───H───@───M───
          │   │
1: ───────X───M───

By passing multiple circuits as a list to the qscout_compile endpoint, we can compile all of them individually with a single call to qscout_compile. This will return all the corresponding compiled circuits and Jaqal programs back as a list, like so:

[7]:
# Create list of circuits
circuit_list = [circuit1, circuit2]

# Compile a list of circuits and their respective jaqal programs
compiler_output_list = service.qscout_compile(circuit_list)
jaqal_output_list = compiler_output_list.jaqal_programs
[8]:
# To get the list of compiled output circuits from the compiler output list, call `circuits` instead of just `circuit` that is called for a single circuit input
output_circuits = compiler_output_list.circuits

# Visualize and get the jaqal program of the first compiled circuit
print("-" * 65, "\n")
print(jaqal_output_list[0])
print("-" * 65, "\n")
print("Compiled circuit 1 \n")
print(output_circuits[0])
-----------------------------------------------------------------

from qscout.v1.std usepulses *

register allqubits[2]

prepare_all
<
        R allqubits[0] -1.5707963267948966 1.5707963267948966
        R allqubits[1] -1.5707963267948966 1.5707963267948966
>
<
        Rz allqubits[0] -3.141592653589793
        Rz allqubits[1] -3.141592653589793
>
MS allqubits[0] allqubits[1] 0 0.3525105540780351
<
        R allqubits[0] -1.5707963267948966 1.5707963267948966
        R allqubits[1] -1.5707963267948966 1.5707963267948966
>
measure_all

-----------------------------------------------------------------

Compiled circuit 1

0: ───PhX(-0.5)^0.5───Z───MS(0.056π)───PhX(-0.5)^0.5───│───M───
                          │                            │   │
1: ───PhX(-0.5)^0.5───Z───MS(0.056π)───PhX(-0.5)^0.5───│───M───
[9]:
# Visualize and get the jaqal program of second compiled circuit
print("-" * 65, "\n")
print(jaqal_output_list[1])
print("-" * 65, "\n")
print("Compiled circuit 2 \n")
print(output_circuits[1])
-----------------------------------------------------------------

from qscout.v1.std usepulses *

register allqubits[2]

prepare_all
<
        R allqubits[0] 3.141592653589793 3.141592653589793
        R allqubits[1] 3.141592653589793 3.141592653589793
>
Sxx allqubits[0] allqubits[1]
<
        R allqubits[0] -1.5707963267948972 1.5707963267948966
        R allqubits[1] 0.0 1.570796326794898
>
measure_all

-----------------------------------------------------------------

Compiled circuit 2

0: ───PhX(1)───MS(0.25π)───PhX(-0.5)^0.5───│───M───
               │                           │   │
1: ───PhX(1)───MS(0.25π)───PhX(0)^0.5──────│───M───

Entangling Basis Compilation

When compiling to the QSCOUT gateset, we can also specify the type of entangling basis gate to utilize during the compilation – either a \(XX\) or \(ZZ\). Let’s consider the first circuit example for earlier but with a different, random \(R_Z\) angle and single measurement:

[10]:
# Create circuit
theta = np.random.uniform(0, 4 * np.pi)
qubits = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
    cirq.CX(qubits[0], qubits[1]),
    cirq.rz(theta).on(qubits[1]),
    cirq.CX(qubits[0], qubits[1]),
    cirq.measure(qubits[0], qubits[1]),
)

# Print circuit for visualization
print(circuit)
0: ───@─────────────────@───M───
      │                 │   │
1: ───X───Rz(-0.834π)───X───M───
[11]:
# Compile with XX entangling basis
compiler_output = service.qscout_compile(circuit, base_entangling_gate="xx")

# Visualize and get jaqal program of the compiled circuit
print("-" * 70, "\n")
print(compiler_output.jaqal_program)
print("-" * 70, "\n")
print(compiler_output.circuit)
----------------------------------------------------------------------

from qscout.v1.std usepulses *

register allqubits[2]

prepare_all
<
        R allqubits[0] -1.5707963267948966 1.5707963267948966
        R allqubits[1] -1.5707963267948966 1.5707963267948966
>
<
        Rz allqubits[0] -3.141592653589793
        Rz allqubits[1] -3.141592653589793
>
MS allqubits[0] allqubits[1] 0 0.5219919608324424
<
        R allqubits[0] -1.5707963267948966 1.5707963267948966
        R allqubits[1] -1.5707963267948966 1.5707963267948966
>
measure_all

----------------------------------------------------------------------

0: ───PhX(-0.5)^0.5───Z───MS(0.083π)───PhX(-0.5)^0.5───│───M───
                          │                            │   │
1: ───PhX(-0.5)^0.5───Z───MS(0.083π)───PhX(-0.5)^0.5───│───M───

As we can see above, we get the same compiled gate structure as before. This is because, by default, the \(XX\) interaction is used, and we observe that it uses an \(MS\) gate as the base entangling gate in the compiled circuit. Similarly, let’s take a look at the compiled circuit if we now specify the compiler to use the \(ZZ\) interaction instead:

[12]:
# Compile with zz entangling basis
compiler_output = service.qscout_compile(circuit, base_entangling_gate="zz")

# Get jaqal program and print circuit
print("-" * 70, "\n")
print(compiler_output.jaqal_program)
print("-" * 70, "\n")
print(compiler_output.circuit)
----------------------------------------------------------------------

from qscout.v1.std usepulses *

register allqubits[2]

prepare_all
ZZ allqubits[0] allqubits[1] 0.5219919608324424
measure_all

----------------------------------------------------------------------

0: ───ZZ─────────│───M───
      │          │   │
1: ───ZZ^(1/6)───│───M───

Looking at the compiled circuit above, we can see that by specifying the \(ZZ\) entangling basis, we use \(R_{ZZ}\) gate as the base entangling gate and the remaining single-qubit gates are compiled and optimized accordingly.

Swap Mirroring

In addition to specifying the type of base entangling gate to use for the compilation and optimization, we can also specify whether to use swap mirroring to help reduce the two-qubit gate overhead of the circuit. By default, it is not enabled; but we will see the differences in circuit compilation by compiling to a random quantum volume model circuit. You can learn more about quantum volume and randomized circuits here.

Create random Quantum Volume (QV) circuit

[13]:
from cirq.contrib import quantum_volume
[14]:
# Generate a random QV circuit
circ = quantum_volume.generate_model_circuit(
    num_qubits=2, depth=2, random_state=np.random.RandomState(seed=123)
)
circ.append([cirq.measure(*cirq.LineQubit.range(2))])

Compile without swap mirroring

[15]:
# Compile with no swap mirroring
output_nsm = service.qscout_compile(circ, mirror_swaps=False, base_entangling_gate="zz")

# Visualize the circuit
circ_nsm = output_nsm.circuit
print(circ_nsm)
0: ───PhX(-0.548)^0.918───ZZ──────────PhX(-0.184)^0.5───ZZ───────────PhX(0.316)^0.5────ZZ─────────PhX(0.385)^0.775──────│───M───
                          │                             │                              │                                │   │
1: ───PhX(0.408)^0.551────ZZ^-0.476───PhX(-0.702)^0.5───ZZ^(-4/13)───PhX(-0.202)^0.5───ZZ^0.052───PhX(-0.259)^(11/16)───│───M───

Compile with swap mirroring

[16]:
# Compile with swap mirroring
output_wsm = service.qscout_compile(circ, mirror_swaps=True, base_entangling_gate="zz")

# Visualize the circuit
circ_wsm = output_wsm.circuit
print(circ_wsm)
0: ───PhX(-0.409)^0.426────ZZ──────────PhX(0.0567)^0.5───ZZ──────────PhX(0.557)^0.5───ZZ──────────PhX(-0.254)^0.547───│───M────────────────
                           │                             │                            │                               │   │
1: ───PhX(-0.0731)^0.391───ZZ^-0.448───PhX(-0.128)^0.5───ZZ^-0.193───PhX(0.372)^0.5───ZZ^-0.024───PhX(0.094)^0.544────│───M('q(0),q(1)')───

With the use of swap mirroring, we note that the angle of the entangling gate has now been reduced and the classical bits associated with each measurement have now been swapped in the compiled circuit. This is also displayed with the measurement indices in the above compiled circuit compared to the previous compiled circuit that does not have swap mirroring enabled.

Return final logical to physical qubit mapping

[17]:
# Return final qubit map for non swap-mirrored circuit
map_nsm = output_nsm.final_logical_to_physical
print("Non swap-mirrored mapping:", map_nsm)

# Return final qubit map for swap-mirrored circuit
map_wsm = output_wsm.final_logical_to_physical
print("Swap-mirrored mapping:", map_wsm)
Non swap-mirrored mapping: {cirq.LineQubit(0): cirq.LineQubit(0), cirq.LineQubit(1): cirq.LineQubit(1)}
Swap-mirrored mapping: {cirq.LineQubit(0): cirq.LineQubit(1), cirq.LineQubit(1): cirq.LineQubit(0)}

Using Superstaq Simulator

Lastly, we will go over how to submit a circuit to a backend and simulate it. This feature is available to free trial users, and can be done by passing the "dry-run" method parameter when calling create_job() to instruct Superstaq to simulate the circuit.

[18]:
# Example Bell state circuit
qubits = cirq.LineQubit.range(2)
qc = cirq.Circuit(
    cirq.H(qubits[0]),
    cirq.CX(qubits[0], qubits[1]),
    cirq.measure(qubits[0], qubits[1]),
)

# Get qscout backend from service provider
job = service.create_job(
    qc, repetitions=100, target="qscout_peregrine_qpu", method="dry-run"
)  # specify "dry-run" as the method to run Superstaq simulation

# Get the counts from the measurement
print(job.counts())
{'00': 56, '11': 44}