How is noise implemented in `default.mixed` device?

Currently, default.qubit is used for ideal simulations and default.mixed is used for noisy simulations.

What is the difference between the two that prevents the former device from implementing noise models?

When I use a noise gate like qml.DepolarizingChannel, what is being implemented in terms of gates on the circuit?

The documentation refers to Kraus operators relating to each Pauli operator. Can the noise gate be simulated by probabilistically applying the Pauli gates on the default.qubit device?
For example, is the below code correctly applying the noise channel?

@qml.qnode(dev)
def circ():
    qml.RY(.5, wires=0)
    
    # Depolarising Noise
    if np.random.choice([True, False], p=[d / 3, 1 - d / 3]):
        qml.PauliX(0)
    if np.random.choice([True, False], p=[d / 3, 1 - d / 3]):
        qml.PauliY(0)
    if np.random.choice([True, False], p=[d / 3, 1 - d / 3]):
        qml.PauliZ(0)

    return qml.expval(qml.PauliX(0))

Hi @ankit27kh,

With default.mixed the density matrix remains the same and the probabilistic nature of the DepolarizingChannel is taken into account within the device. This allows for a more complex study of error and error correction.

Your approach would be equivalent to using default.mixed if you don’t need any deep analysis of error, but note that the state of the circuit and the density matrix will be different every time you run your circuit since you are essentially creating a different deterministic circuit every time, instead of creating one probabilistic circuit.

As an example:

After your code run

print('circ v1',circ())
qml.draw_mpl(circ)()
print('circ v2',circ())
qml.draw_mpl(circ)()

You will notice that you essentially build a different circuit every time.

Instead, if you run the following code you will basically get the same circuit every time:

dev2 = qml.device('default.mixed', wires = 1)

@qml.qnode(dev2)
def circ2():
    qml.RY(.5, wires=0)
    qml.DepolarizingChannel(d, wires=0)
    return qml.density_matrix([0])

print('circ2 v1',circ2())
qml.draw_mpl(circ2)()
print('circ2 v2',circ2())
qml.draw_mpl(circ2)()

Please let me know if this is clear or if you need any other help!

Hey @CatalinaAlbornoz, thanks for the response.
I have some further questions regarding using the default.qubit device for simulating noise.

  1. How are the measurements returned when using the gates directly? For example, when calculating expectation values with a given number of shots, does it consider the probability I have supplied for each gate?

  2. What happens when using None shots with the probabilistically applied gates?

  3. At least when using None shots, shouldn’t the result from both devices match if they are doing the same computation?

Hey @ankit27kh , let me answer these questions :slightly_smiling_face: When working with probabilities in this way inside the circuit there is no vector that correctly defines the state. You can see that if you calculate qml.state each time you will get a different thing. However, the default.mixed has only one way to represent that state through the density matrix.

If you are going to return qml.probs or qml.sample you will see no real difference between using one device or the other, regardless of the number of shots you put in. In particular when you put shots = None , it is solved analytically (as if there were infinite shots) so you will see no difference.

In short, if you are not going to use qml.state() you should not notice any difference between the devices (although in the mixed you have some gates already coded that can save you work)

Hey @Guillermo_Alonso, thank you for this! It covers all of my questions.
But I am not getting identical results from the two devices.
Consider the code below:

import pennylane as qml
import pennylane.numpy as np

shots = 100
dev1 = qml.device('default.qubit', wires=1, shots=shots)
dev2 = qml.device('default.mixed', wires=1, shots=shots)

d = .7

@qml.qnode(dev2)
def circ2():
    qml.RY(1, wires=0)

    qml.DepolarizingChannel(d, 0)

    return qml.expval(qml.PauliX(wires=0))

@qml.qnode(dev1)
def circ1():
    qml.RY(1, wires=0)

    if np.random.choice([True, False], p=[d / 3, 1 - d / 3]):
        qml.PauliX(0)
    if np.random.choice([True, False], p=[d / 3, 1 - d / 3]):
        qml.PauliY(0)
    if np.random.choice([True, False], p=[d / 3, 1 - d / 3]):
        qml.PauliZ(0)

    return qml.expval(qml.PauliX(wires=0))

print("default.mixed")
for _ in range(3):
    np.random.seed(42)
    print(circ2())

print("default.qubit")
for _ in range(3):
    np.random.seed(42)
    print(circ1())

This results in the output:

default.mixed
0.14
0.14
0.14
default.qubit
0.86
0.86
0.86

As you can see, these don’t match. I am also resetting the seed every time. You can also use None shots. It does not match even then.

The reason for trying this instead of just using the default.mixed device is that this device does not support JAX. So if I can replicate the results with default.qubit device, I can then use JAX-JIT to reduce my computation time.

Theoretically it should work but I don’t see the problem :thinking:I have to check the Depolarization structure, to see what is going on. As you can see, with another operator it works.

import pennylane as qml
import pennylane.numpy as np

shots = 1000

dev1 = qml.device('default.qubit', wires=1)
dev2 = qml.device('default.mixed', wires=1)

d = 0.7 



@qml.qnode(dev1)
def circ1():
    qml.RY(2, wires=0)

    if np.random.rand() < d:
        qml.PauliX(0)

    return qml.expval(qml.PauliX(wires=0))


print("default.qubit:", circ1())
    
    
@qml.qnode(dev2)
def circ2():
    
    qml.RY(2, wires=0)

    qml.BitFlip(d, 0)

    return qml.expval(qml.PauliX(wires=0))

print("default.mixed", circ2())

If I find out anything I will write you here

1 Like