pasqal-io / qadence

Digital-analog quantum programming interface
https://pasqal-io.github.io/qadence/latest/
Apache License 2.0
72 stars 21 forks source link

[Feature] Add digital noise from PyQ #563

Closed jpmoutinho closed 1 month ago

jpmoutinho commented 2 months ago

Description

Ongoing work in adding the noise from PyQ. Supersedes https://github.com/pasqal-io/qadence/pull/469.

Currently in this branch the basics should be working:

from qadence import NoiseType, DigitalNoise
from qadence import RX, run

noise = DigitalNoise(NoiseType.BITFLIP, error_probability = 0.2)
noise = DigitalNoise.bitflip(error_probability = 0.2) # equivalent

op = RX(0, torch.pi, noise = noise)

run(op)

---

DensityMatrix([[[0.2000+0.0000e+00j, 0.0000+3.6739e-17j],
                [0.0000-3.6739e-17j, 0.8000+0.0000e+00j]]])

Note that currently the DigitalNoise is simply a name alias for the NoiseProtocol class directly from PyQ, which you can find here: https://github.com/pasqal-io/pyqtorch/blob/main/pyqtorch/noise/protocol.py.

The approach in https://github.com/pasqal-io/qadence/pull/469 was instead to change the Noise dataclass in qadence.noise.protocols to include the same options, but then there was already an option there called DEPOLARIZING which got changed to DEPOLARIZING_PYQ... I am not so sure what is the best approach here, this should be revised.

Furthermore, I have also drafted a transpilation function to add noise to gates within a composite block or circuit. It works like this:

from qadence import RX, DigitalNoise, set_noise, chain, run

n_qubits = 2

block = chain(RX(i, f"theta_{i}") for i in range(n_qubits))

noise = DigitalNoise.bitflip(error_probability = 0.1)

# The function changes the block in place:
set_noise(block, noise)

run(block)

The code above should add the defined bitflip protocol to every gate in the block. It should also work if it is a QuantumCircuit. There is an extra optional argument to specify the type of block we want to apply noise to. E.g., let's say we want to apply noise only to X gates, then it will run an if isinstance(block, target_class): ... at every leaf block in the block tree:

from qadence import RX, X, DigitalNoise, set_noise, chain, run

n_qubits = 2

block = chain(RX(i, f"theta_{i}") for i in range(n_qubits))

noise = DigitalNoise.bitflip(error_probability = 0.1)

# The function changes the block in place:
set_noise(block, noise, target_class = X)

run(block)

The snippet above will just return a statevector because there exists no X gates in that composite block. We can also try to print the noise for each of the gates and see that it works:

from qadence import RX, X, DigitalNoise, set_noise, chain, run

n_qubits = 2

block = chain(RX(0, "theta"), X(0))

noise = DigitalNoise.bitflip(error_probability = 0.1)

set_noise(block, noise, target_class = X)

for block in block.blocks:
    print(block.noise)

---

> None
> BitFlip(prob: 0.1)

What is left to do: