View on QuantumAI | Run in Google Colab | View source on GitHub | Download notebook |

This tutorial shows how to prepare circuits to run on the Quantum Computing Service (QCS) and optimize them to improve performace, showing an example of the procedure outlined in the Best practices guide. This is an "advanced" tutorial where you will learn to perform the following optimization techniques:

- Converting to target gateset
- Ejecting single-qubit operations
- Aligning gates in moments
- Inserting spin echoes to reduce leakage & cross-talk.

## Setup

```
try:
import cirq
except ImportError:
print("installing cirq...")
!pip install --quiet cirq --pre
print("installed cirq.")
```

```
import matplotlib.pyplot as plt
import numpy as np
import cirq
import cirq_google as cg
```

Getting OAuth2 credentials. Press enter after entering the verification code. Authentication complete. Successful authentication to Google Cloud.

## Preparing circuits to run on QCS

For the sake of this tutorial, we will use a circuit structure created by the `create_benchmark_circuit`

function defined above.

```
"""Create an example circuit."""
qubits = cirq.GridQubit.rect(2, 3) # [cirq.GridQubit(x, y) for (x, y) in [(3, 2), (4, 2), (4, 1), (5, 1), (6, 1), (6, 2), (5, 2)]]
circuit = create_benchmark_circuit(qubits, twoq_gate=cirq.ISWAP, cycles=3, seed=1)
print("Example benchmark circuit:\n")
circuit
```

Example benchmark circuit:

This circuit divides the qubits into two registers: a single ancilla (top qubit) as the first register, and the remaining qubits as the second register. First, the ancilla is excited into the \(|1\rangle\) state and coupled to the second register. Then, a Loschmidt echo is performed on the second register. Last, the ancilla is uncoupled from the second register and measured. Without any noise, the only measurement result should be \(1\).

```
"""Without noise, only the 1 state is measured."""
result = cirq.Simulator().run(circuit, repetitions=1000)
result.histogram(key="z")
```

Counter({1: 1000})

We choose this circuit as an example for two reasons:

Each gate in the circuit is in its own

`cirq.Moment`

, so this is a poor circuit structure to run on devices without any optimization / alignment.The ancilla qubit is idle except at the start and end of the circuit, so this is a prime example where adding spin echoes can improve performance.

A similar circuit was used in Information Scrambling in Computationally Complex Quantum Circuits (see Fig. S10) to benchmark the performance of spin echoes.

Starting from this circuit, we show how to optimize gates, align moments, and insert spin echoes to improve the performance on a real device.

### Convert to target gateset

To run on a device, all gates in the circuit will be converted to a gateset supported by that device. (See the Device specifications guide for information on supported gatesets.)

We will use the \(\sqrt{\text{iSWAP} }\) gateset in this tutorial. You can see this gateset and others as follows.

```
# Create an Engine object to use.
spec = cg.Engine(project_id).get_processor(processor_id).get_device_specification()
# Iterate through each gate set valid on the device.
for gateset in spec.valid_gate_sets:
print(gateset.name)
print('-------')
# Prints each gate valid in the set with its duration
for gate in gateset.valid_gates:
print('%s %d' % (gate.id, gate.gate_duration_picos))
print()
```

sycamore ------- syc 12000 xy 25000 xy_pi 25000 xy_half_pi 25000 z 0 xyz 25000 meas 4000000 wait 0 sqrt_iswap ------- fsim_pi_4 32000 inv_fsim_pi_4 32000 xy 25000 z 0 xyz 25000 meas 4000000 wait 0 fsim ------- fsim 0 xy 25000 z 0 xyz 25000 meas 4000000 wait 0 xmon ------- xy 25000 z 0 xyz 25000 cz 0 meas 4000000

To convert gates to this gateset, use the `cirq.MergeInteractionsToSqrtIswap`

optimizer. This optimizer merges all consecutive (one- and two-qubit) interactions on two qubits into a unitary matrix and then decomposes this unitary using \(\sqrt{\text{iSWAP} }\) gates in an attempt to (a) convert to the target gateset and (b) reduce the circuit depth by reducing the number of operations.

```
cirq.MergeInteractionsToSqrtIswap().optimize_circuit(circuit)
circuit
```

```
"""Compile an arbitrary two-qubit operation to the sqrt_iswap gateset."""
ops = cirq.two_qubit_matrix_to_sqrt_iswap_operations(
q0=qubits[0], q1=qubits[1], mat=cirq.testing.random_unitary(dim=4, random_state=1)
)
cirq.Circuit(ops)
```

### Eject single-qubit operations

After converting to a target gateset, you can use various circuit optimizers to attempt to reduce the number of gates as shown below.

The `cirq.eject_phased_paulis`

optimizer pushes `cirq.X`

, `cirq.Y`

, and `cirq.PhasedXPowGate`

gates towards the end of the circuit.

```
circuit = cirq.eject_phased_paulis(circuit)
circuit
```

Note that, for example, the back-to-back `cirq.X`

gates on the ancilla have been removed from the start of the circuit.

You can also use the `cirq.eject_z`

optimizer to attempt to push `cirq.Z`

gates towards the end of the circuit.

```
circuit = cirq.eject_z(circuit)
circuit
```

Note that, for example, the `cirq.Z`

gate immediately before the ancilla measurement has been removed.

### Align gates in moments

After optimizing, gates should be aligned into `cirq.Moment`

s to satisfy the following criteria:

The fewer moments the better (generally speaking).

- Each moment is a discrete time slice, so fewer moments means shorter circuit execution time.

Moments should consist of gates with similar durations.

- Otherwise some qubits will be idle for part of the moment.
- It's best to align one-qubit gates in their own moment and two-qubit gates in their own moment if possible.

All measurements should be terminal and in a single moment.

- Intermediate measurements are not currently supported, and measurement operation times are roughly two orders of magnitude longer than other gate times (see the above cell which prints out gatesets and gate times).

To align gates into moments and push them as far left as possible, use `cirq.align_left`

.

```
left_aligned_circuit = cirq.align_left(circuit)
left_aligned_circuit
```

Note how many fewer moments this aligned circuit has.

```
print(f"Original circuit has {len(circuit)} moments.")
print(f"Aligned circuit has {len(left_aligned_circuit)} moments.")
```

Original circuit has 54 moments. Aligned circuit has 14 moments.

You can also align gates and push them to the right with `cirq.align_right`

.

```
right_aligned_circuit = cirq.align_right(circuit)
right_aligned_circuit
```

Also, you can use `cirq.stratified_circuit`

to align operations into similar categories. For example, you can align single-qubit and two-qubit operations in separate moments as follows.

```
circuit = cirq.stratified_circuit(
circuit, categories=[lambda op : len(op.qubits) == 1, lambda op : len(op.qubits) == 2]
)
circuit
```

Note that each moment now only contains single-qubit gates or two-qubit gates.

#### Drop moments

To drop moments that have a tiny effect or moments that are empty, you can use the following optimizers.

```
circuit = cirq.drop_negligible_operations(circuit)
circuit = cirq.drop_empty_moments(circuit)
circuit
```

#### Synchronize terminal measurements

You can use the `cirq.synchronize_terminal_measurements`

to move all measurements to the final moment if it can accommodate them (without overlapping with other operations).

```
circuit = cirq.synchronize_terminal_measurements(circuit)
circuit
```

## Adding spin echoes

Dynamical decoupling applies a series of spin echoes to otherwise idle qubits to reduce decoherent effects. As mentioned above, spin echoes were used as an effective error mitigation technique in Information Scrambling in Computationally Complex Quantum Circuits, and the performance of any circuit with idle qubits can potentially be improved by adding spin echoes.

The following codeblock shows how to insert spin echoes on the ancilla qubit.

```
# Gates for spin echoes. Note that these gates are self-inverse.
pi_pulses = [
cirq.PhasedXPowGate(phase_exponent=p, exponent=1.0) for p in (-0.5, 0.0, 0.5, 1.0)
]
# Generate spin echoes on ancilla.
num_echoes = 3
random_state = np.random.RandomState(1)
spin_echo = []
for _ in range(num_echoes):
op = random_state.choice(pi_pulses).on(qubits[0])
spin_echo += [op, cirq.inverse(op)]
# Insert spin echo operations to circuit.
optimized_circuit_with_spin_echoes = circuit.copy()
optimized_circuit_with_spin_echoes.insert(5, spin_echo)
# Align single-qubit spin echo gates into other moments of single-qubit gates.
optimized_circuit_with_spin_echoes = cirq.stratified_circuit(
optimized_circuit_with_spin_echoes,
categories=[lambda op : len(op.qubits) == 1, lambda op : len(op.qubits) == 2]
)
optimized_circuit_with_spin_echoes
```

The ancilla now has spin echoes between the two-qubit gates at the start/end of the circuit instead of remaining idle.

## Benchmark

Now that we have discussed how to remove uncessary gates, align gates, and insert spin echoes, we run an experiment to benchmark the results. First we get a line of qubits, list of cycle values (one circuit per cycle value), and set other experimental parameters.

```
"""Set experiment parameters."""
qubits = cg.line_on_device(device_sampler.device, length=7)
cycle_values = range(0, 100 + 1, 4)
nreps = 20_000
seed = 1
```

The `create_benchmark_circuit`

defined at the start of this tutorial has options to optimize the circuit and insert spin echoes on the ancilla as we have discussed above. Without any optimization or spin echoes, an example circuit looks like this:

```
circuit = create_benchmark_circuit(qubits, cycles=2, seed=1)
print(f"Unoptimized circuit ({len(circuit)} moments):\n")
circuit
```

Unoptimized circuit (47 moments):

After removing unnecessary gates (optimization) and aligning gates, the same circuit looks like this:

```
optimized_circuit = create_benchmark_circuit(qubits, cycles=2, seed=1, with_optimization=True, with_alignment=True)
print(f"Circuit with optimization + alignment ({len(optimized_circuit)} moments):\n")
optimized_circuit
```

Circuit with optimization + alignment (15 moments):

And with optimization + alginment + spin echoes on the ancilla, the same circuit looks like this:

```
optimized_circuit_with_spin_echoes = create_benchmark_circuit(qubits, cycles=2, seed=1, with_optimization=True, with_alignment=True, with_spin_echoes=True)
print(f"Circuit with optimization + alignment + spin echoes ({len(optimized_circuit_with_spin_echoes)} moments):\n")
optimized_circuit_with_spin_echoes
```

Circuit with optimization + alignment + spin echoes (15 moments):

Now we create circuits for all cycle values without optimization, with optimization + alignment, and with optimization + alignment + spin echoes.

```
"""Create all circuits."""
batch = [
create_benchmark_circuit(qubits, cycles=c, seed=seed)
for c in cycle_values
]
batch_with_optimization = [
create_benchmark_circuit(qubits, cycles=c, seed=seed, with_optimization=True, with_alignment=True)
for c in cycle_values
]
batch_with_optimization_and_spin_echoes = [
create_benchmark_circuit(qubits, cycles=c, seed=seed, with_optimization=True, with_alignment=True, with_spin_echoes=True)
for c in cycle_values
]
```

The next cell runs them on the device.

```
"""Run all circuits."""
all_probs = []
for b in (batch, batch_with_optimization, batch_with_optimization_and_spin_echoes):
results = device_sampler.sampler.run_batch(b, repetitions=nreps)
all_probs.append([to_survival_prob(*res) for res in results])
```

And the next cell plots the results.

```
"""Plot results."""
labels = ["Unoptimized", "Optimization + Alignment", "Optimization + Alignment + Spin echoes"]
for (probs, label) in zip(all_probs, labels):
plt.plot(cycle_values, probs, "-o", label=label)
plt.xlabel("Cycles")
plt.ylabel("Survival probability")
plt.legend();
```

Recall that without any noise, the survival probability (ratio of \(1\)s measured to all measurements) should be \(1.0\), so higher on this plot is better. The unoptimized circuit performs the worst, the circuit with optimization + alignment performs better, and the circuit with optimization + alignment + spin echoes performs the best.