qiboteam / qibo

A framework for quantum computing
https://qibo.science
Apache License 2.0
275 stars 55 forks source link

ZNE doesn't repeat RX or CNOT gates when there is no RX or CNOT in the circuit #1305

Open mho291 opened 2 months ago

mho291 commented 2 months ago

Describe the bug Zero Noise Extrapolation (ZNE) is a quantum error mitigation technique that creates a few circuits with increasing amounts of noise in each circuit. Then it computes the expectation value of the observable and extrapolates backwards to the zero-noise limit.

I have noticed that the function get_noisy_circuit is used to artificially create noisy circuits by repeating some (U * U^dagger) pairs. It looks like this get_noisy_circuit(circuit, num_insertions, insertion_gate). The insertion_gate can either be RX or CNOT while num_insertion >= 1.

I've ran various scenarios:

  1. When the circuit contains n number of CNOT gates, and we have insertion_gate = CNOT, each of the n CNOTs has an extra num_insertion (CNOT * CNOT)s appended. This is ok.
  2. When the circuit contains n number of RX gates, and we have insertion_gate = RX, we expect to see num_insertion (RX * RX`^dagger) gates being appended. However, none appears.
  3. When the circuit contains no RX or CNOT gates and insertion_gate = RX or insertion_gate = CNOT, we do not see num_insertion (RX RX^dagger) or num_insertion (CNOT CNOT) gates being appended.

Scenarios 2 and 3 appear to have some issue.

Code to reproduce scenarios 1, 2

from qibo import Circuit, gates
import numpy as np
from qibo.models.error_mitigation import get_noisy_circuit

circuit = Circuit(2)
circuit.add(gates.H(0))
circuit.add(gates.RX(0, np.pi/3))
circuit.add(gates.CNOT(0, 1))
circuit.add(gates.CZ(0, 1))
circuit.add(gates.iSWAP(0, 1))
circuit.add(gates.H(0))
circuit.add(gates.H(1))
circuit.add(gates.CNOT(1, 0))
circuit.add(gates.M(0, 1))

print('Original circuit')
print(circuit.draw())

ZNE_circuit = get_noisy_circuit(circuit, num_insertions = 1, insertion_gate = "CNOT")
print('ZNE_circuit')
print(ZNE_circuit.draw())

ZNE_circuit2 = get_noisy_circuit(circuit, num_insertions = 1, insertion_gate = "RX")
print('ZNE_circuit2')
print(ZNE_circuit2.draw())

Running this code gives this output:

Original circuit:
q0: ─H─RX─o─o─i─H─X─M─
q1: ──────X─Z─i─H─o─M─

ZNE_circuit:
q0: ─H─RX─o─o─o─o─i─H─X─X─X─M─
q1: ──────X─X─X─Z─i─H─o─o─o─M─

ZNE_circuit2:
q0: ─H─RX─o─o─i─H─X─M─
q1: ──────X─Z─i─H─o─M─

Code to reproduce scenario 3

circuit = Circuit(2)
circuit.add(gates.H(0))
circuit.add(gates.CZ(0, 1))
circuit.add(gates.M(0, 1))

print('Original circuit')
print(circuit.draw())

ZNE_circuit3 = get_noisy_circuit(circuit, num_insertions = 1, insertion_gate = "CNOT")

print('ZNE circuit3:')
print(ZNE_circuit3.draw())

ZNE_circuit4 = get_noisy_circuit(circuit, num_insertions = 1, insertion_gate = "RX")
print('ZNE circuit4:')
print(ZNE_circuit4.draw())

Running this code gives:

Original circuit
q0: ─H─o─M─
q1: ───Z─M─

ZNE circuit3:
q0: ─H─o─M─
q1: ───Z─M─

ZNE circuit4:
q0: ─H─o─M─
q1: ───Z─M─
mho291 commented 1 month ago

Update; Fixing this with a global unitary folding under pull request #1327.