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

Hello Guillermo,
For Documentation “Using Pennylane” Example:

np.random.seed(1)
dev = qml.device(“default.qubit”, wires=1)
@qml.qnode(dev)
def circuit():
qml.Hadamard(wires=0)
return qml.expval(qml.PauliZ(0))

Running the simulator with shots=None returns the exact expectation.
circuit(shots=None) 0.0
Now we set the device to return stochastic results, and increase the number of shots starting from 10.
circuit(shots=10) 0.2
circuit(shots=1000) -0.062
circuit(shots=100000) 0.00056
The result converges to the exact expectation.

Questions:

  1. When “shots=None” for quantum simulators: are all quantum mechanical properties preserved when running quantum algorithms vs. on real hardware?

  2. Will there be near-term quantum hardware that can productively perform complex machine learning tasks (minimizing shots and minimizing time for each epoch, while coming closer to the exact expectation)?

Thank You

Reference:
https://docs.pennylane.ai/en/stable/introduction/measurements.html

Hi @kevinkawchak ! :wave:

Regarding the first question, you could work with shots = None for develop any quantum simulation. However, you have to be careful. On the one hand, you may design an algorithm that to achieve certain guarantees, the number of shots you would need to calculate the value would grow exponentially and this would go unnoticed by working in an analytical way. On the other hand, there are differentiation methods that will not work if we set shots (and therefore will not work on real hardware).

The second question is something that is not known with certainty. But from my point of view, if we get there, methods that use fewer circuits or fewer shots will start to be interesting, since these technologies generally do not seem to be free :smile:

1 Like

Thank you, I appreciate the response.