What happens when I use DDD?#

The workflow of Digital Dynamical Decoupling (DDD) in Mitiq is represented in the figure below.

../_images/ddd_workflow.png

Workflow of the DDD technique in Mitiq.#

  • The user provides a QPROGRAM, (i.e. a quantum circuit defined via any of the supported frontends).

  • Mitiq modifies the input circuit with the insertion of DDD gate sequences in idle windows.

  • The modified circuit is executed via a user-defined Executor.

  • The error mitigated expectation value is returned to the user.

With respect to the workflows of other error-mitigation techniques (e.g. ZNE or PEC), DDD involves the generation and the execution of a single modified circuit. For this reason, there is no need to combine the results of multiple circuits and the final inference step which is necessary for other techniques is instead trivial for DDD.

Note

When setting the num_trials option to a value larger than one, multiple circuits are actually generated by Mitiq and the associated results are averaged to obtain the final expectation value. This more general case is not shown in the figure since it can be considered as an average of independent single-circuit workflows.

As shown in How do I use DDD?, the function execute_with_ddd() applies DDD behind the scenes and directly returns the error-mitigated expectation value. In the next sections instead, we show how one can apply DDD at a lower level, i.e., by:

  • Characterizing all the slack windows in a circuit;

  • Inserting DDD sequences in all slack windows;

  • Executing the modified circuit.

Analysis of idle windows (optional step)#

In this section we show how one can determine all the idle windows (often called slack windows) in a circuit and how long they are, i.e. how many single-qubit gates can fit each window. This is an optional step, since it is actually not necessary to apply DDD with Mitiq. Nonetheless, it provides additional information on the gate structure of the circuit that can be useful, especially for research purposes.

The circuit mask#

A quantum circuit can be visualized as a 2D grid where the horizontal axis represents discrete time steps (often called moments) and the vertical axis represents the qubits of the circuit. Each gate occupies one or more grid cells, depending on the number of qubits it acts on.

This 2D grid is essentially what we get each time we print a circuit out.

from cirq import Circuit, X, SWAP, LineQubit

qreg = LineQubit.range(8)
x_layer = Circuit(X.on_each(qreg))
cnots_layer = Circuit(SWAP.on(q, q + 1) for q in qreg[:-1])
circuit = x_layer + cnots_layer + x_layer
circuit
0: ───X───×───────────────────────────X───
          │
1: ───X───×───×───────────────────────X───
              │
2: ───X───────×───×───────────────────X───
                  │
3: ───X───────────×───×───────────────X───
                      │
4: ───X───────────────×───×───────────X───
                          │
5: ───X───────────────────×───×───────X───
                              │
6: ───X───────────────────────×───×───X───
                                  │
7: ───X───────────────────────────×───X───

The grid structure of a circuit can be expressed as a mask matrix with \(1\) entries in cells that are occupied by gates and \(0\) entries in empty cells. So, for example, the mask matrix of the above circuit is the following:

from mitiq import ddd

mask_matrix = ddd.insertion._get_circuit_mask(circuit)
mask_matrix
array([[1, 1, 0, 0, 0, 0, 0, 0, 1],
       [1, 1, 1, 0, 0, 0, 0, 0, 1],
       [1, 0, 1, 1, 0, 0, 0, 0, 1],
       [1, 0, 0, 1, 1, 0, 0, 0, 1],
       [1, 0, 0, 0, 1, 1, 0, 0, 1],
       [1, 0, 0, 0, 0, 1, 1, 0, 1],
       [1, 0, 0, 0, 0, 0, 1, 1, 1],
       [1, 0, 0, 0, 0, 0, 0, 1, 1]])

A slack window is an horizontal and contiguous sequence of zeros in the mask matrix, corresponding to a qubit which is idling for a finite amount of time.

The slack matrix#

To analyze the structure of idle windows, it is more convenient to define a slack matrix, i.e., a matrix of positive integers that are placed at the beginning of each slack window and whose value represent the time length of that window. For example, in our simple example, we get:

slack_matrix = ddd.get_slack_matrix_from_circuit_mask(mask_matrix)
slack_matrix
array([[0, 0, 6, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 5, 0, 0, 0, 0, 0],
       [0, 1, 0, 0, 4, 0, 0, 0, 0],
       [0, 2, 0, 0, 0, 3, 0, 0, 0],
       [0, 3, 0, 0, 0, 0, 2, 0, 0],
       [0, 4, 0, 0, 0, 0, 0, 1, 0],
       [0, 5, 0, 0, 0, 0, 0, 0, 0],
       [0, 6, 0, 0, 0, 0, 0, 0, 0]])

This matrix contains the same amount of information as mask_matrix, but it is more convenient for the analysis of idle windows as shown in the next code cell.

import numpy as np 

print(f"The circuit contains {np.count_nonzero(slack_matrix)} slack windows.")
print(f"The maximum slack length is {np.max(slack_matrix)}.")
lengths, counts = np.unique(slack_matrix, return_counts=True)
length_distribution = dict(zip(lengths[1:], counts[1:]))
print(f"The full distribution of slack lengths is {length_distribution}.")
print(f"On average, each qubit spends {np.mean(slack_matrix) :.0%} of time in idle mode.")
The circuit contains 12 slack windows.
The maximum slack length is 6.
The full distribution of slack lengths is {1: 2, 2: 2, 3: 2, 4: 2, 5: 2, 6: 2}.
On average, each qubit spends 58% of time in idle mode.

Inserting DDD sequences#

The DDD error mitigation technique consists of filling the slack windows of a circuit with DDD gate sequences. This can be directly achieved via the function insert_ddd_sequences() function.

xyxy_rule = ddd.rules.xyxy
circuit_with_ddd = ddd.insert_ddd_sequences(circuit, rule=xyxy_rule)
circuit_with_ddd
0: ───X───×───I───X───Y───X───Y───I───X───
          │
1: ───X───×───×───I───X───Y───X───Y───X───
              │
2: ───X───────×───×───X───Y───X───Y───X───
                  │
3: ───X───────────×───×───────────────X───
                      │
4: ───X───────────────×───×───────────X───
                          │
5: ───X───X───Y───X───Y───×───×───────X───
                              │
6: ───X───I───X───Y───X───Y───×───×───X───
                                  │
7: ───X───I───X───Y───X───Y───I───×───X───

Note

In principle, the function insert_ddd_sequences() is all one needs to apply DDD. Indeed, since in DDD there is not a final post-processing step, one can simply insert DDD sequences before running the circuit on a noisy backend. As shown in the next section, this approach is exactly equivalent to the application of the standard Mitiq function execute_with_ddd().

Executing the modified circuit#

from cirq import DensityMatrixSimulator, amplitude_damp

def execute(circuit, noise_level=0.003):
    """Returns Tr[ρ |00..⟩⟨00..|] where ρ is the state prepared by the circuit
    executed with depolarizing noise.
    """
    noisy_circuit = circuit.with_noise(amplitude_damp(noise_level))
    rho = DensityMatrixSimulator().simulate(noisy_circuit).final_density_matrix
    return rho[0, 0].real

If executed on a noiseless backend, circuit_with_ddd and circuit are equivalent. On a real backend, they have a different sensitivity to noise. The core idea of the DDD technique is that, circuit_with_ddd is (hopefully) less sensitive to noise thanks to the particular structure of DDD sequences.

# Ideal result
execute(circuit, noise_level=0)
1.0
# Unmitigated result
execute(circuit)
0.8255487
# Error-mitigated result (with DDD)
execute(circuit_with_ddd)
0.8560556

As a final remark, we stress that the low-level procedure that we have shown is exactly what execute_with_ddd() does behind the scenes. Let’s verify this fact:

np.isclose(
  ddd.execute_with_ddd(circuit, execute, rule=xyxy_rule),
  execute(circuit_with_ddd),
)
True