Inconsistent ordering of simulation results with Evolution operators

Unless I am mistaken, Pennylane uses the big endian format, while Qiskit uses the little endian format. We can observe this by running the following simple code:

(1) Pennylane:

import pennylane as qml
with qml.tape.QuantumTape() as tape:
    qml.PauliX(1)
    qml.probs(wires=[0, 1])
simulator = qml.device("default.qubit", wires=2)
qml.execute([tape], simulator)

The result is:

(array([0., 1., 0., 0.]),)

(2) Qiskit:

from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import Statevector
qc = QuantumCircuit(2, name="xgate")
qc.x(1)
state = Statevector.from_instruction(qc)
np.abs(state.data) ** 2

The result is:

array([0., 0., 1., 0.])

The ordering of the simulation results is compatible with Pennylane using big endian format and Qiskit using little endian format.

Now I am comparing the results from using Evolution gate:

(1) Pennylane:

import pennylane as qml
H =  2.1 * qml.PauliX(0) - qml.PauliY(1)
time = 1
with qml.tape.QuantumTape() as tape:
    qml.evolve(H, time, num_steps=1).queue()
    qml.probs(wires=[0, 1])
simulator = qml.device("default.qubit", wires=2)
qml.execute([tape], simulator)

The result is:

(array([0.07440321, 0.18046638, 0.21752337, 0.52760704]),)

(2) Qiskit:

from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit.library import PauliEvolutionGate
X = SparsePauliOp("X")
Y = SparsePauliOp("Y")
I = SparsePauliOp("I")
operator = 2.1 * (X ^ I) - (I ^ Y)
evo = PauliEvolutionGate(operator, time=1)
circuit = QuantumCircuit(2)
circuit.append(evo, [0,1])
state = Statevector.from_instruction(circuit)
np.abs(state.data) ** 2

The result is:

array([0.07440321, 0.18046638, 0.21752337, 0.52760704])

This time one can observe that the two results are the same, despite I expected that the order of entries in the two results should be different. Of course in principle the problem can be either in Qiskit or in Pennylane but, based on some independent result, I suspect that perhaps the entries in Pennylane result are not ordered correctly in this case.

Thanks for your help!

Name: PennyLane
Version: 0.41.0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /home/radu/.pyenv/versions/pennylane-ionq/lib/python3.11/site-packages
Requires: appdirs, autograd, autoray, cachetools, diastatic-malt, networkx, numpy, packaging, pennylane-lightning, requests, rustworkx, scipy, tomlkit, typing-extensions
Required-by: PennyLane-IonQ, PennyLane_Lightning

Platform info:           Linux-6.8.0-57-generic-x86_64-with-glibc2.39
Python version:          3.11.0
Numpy version:           2.0.2
Scipy version:           1.15.1
Installed devices:
- default.clifford (PennyLane-0.41.0)
- default.gaussian (PennyLane-0.41.0)
- default.mixed (PennyLane-0.41.0)
- default.qubit (PennyLane-0.41.0)
- default.qutrit (PennyLane-0.41.0)
- default.qutrit.mixed (PennyLane-0.41.0)
- default.tensor (PennyLane-0.41.0)
- null.qubit (PennyLane-0.41.0)
- reference.qubit (PennyLane-0.41.0)
- lightning.qubit (PennyLane_Lightning-0.41.0)
- ionq.qpu (PennyLane-IonQ-0.40.0.dev0)
- ionq.simulator (PennyLane-IonQ-0.40.0.dev0)

Thanks for your question @Radu_Marginean !

I’ll do some tests to understand what’s going on.

Is there a reason why you’re choosing to use tapes instead of qnodes?

If you’re looking to learn more about PennyLane I can recommend our Codebook module on PennyLane Fundamentals. It can help you learn a lot of cool things about PennyLane!

There is no particular reason for using tapes rather than qnodes. Thank you.

Hi @Radu_Marginean ,

It looks like PennyLane’s output was correct. I’ll explain this with the code below. However if you still feel something is off please let us know.

When you use PennyLane the recommended way is to use qnodes. This doesn’t really change anything with respect to your ordering question, but it might make it easier to understand what’s going on.

import pennylane as qml

# We create an instance of the device (or backend) that the circuit will run on
simulator = qml.device("default.qubit", wires=2)

# Using @qml.qnode tells the program where to run the circuit that is defined immediately below

@qml.qnode(simulator)
def circuit(): # Your circuit is defined as a Python function
  # Add your gates within your circuit and specify which wire(s) they will act on
  
  qml.PauliX(wires=1) # Did you know you can use qml.X()? It's a shorthand for qml.PauliX()
  
  # The output will show the probabilities for the states in lexicographic order 00, 01, 10, 11.
  return qml.probs(wires=[0, 1]) 

# The output in this case indicates 100% probability of measuring '0' in the 
# first qubit and '1' in the second one. So 100% probability of obtaining '01.
circuit()

Out: array([0., 1., 0., 0.])

Now let’s use a very similar circuit but using qml.evolve().

# Choose the operator you want to evolve
H = qml.PauliX(wires=1) # Let's choose PauliX applied on wire 1 for simplicity

# Make your qnode like before
@qml.qnode(simulator)
def circuit(time): # Make the circuit depend on the time for the evolution
  # Evolve your operator
  qml.evolve(H, time, num_steps=1)
  return qml.probs(wires=[0, 1]) # Return a measurement

# Run the circuit for time=0 (meaning you remain in the initial state, which is |00>)
print("t=0: ", circuit(time=0))
# Same as before, the output shows the probabilities for the states in lexicographic order 00, 01, 10, 11.
# As expected we measure '00' with 100% probability, meaning nothing has happened.


# Now run the circuit for time=1
# Remember you've only evolved one operator which is the PauliX gate on the second qubit
print("t=1: ", circuit(time=1))
# As expected the first qubit (wire 0) is always measured as '0' since we didn't add any gates to it.
# The second qubit (wire 1) shows an evolution from '0' to '1' which is what we expected.
# This output shows we have 70% chance of measuring '01'.
Out: 
t=0:  [1. 0. 0. 0.]
t=1:  [0.29192658 0.70807342 0.         0.        ]

My guess here is that the recent changes in Qiskit ordering may have affected some parts of the code but not others. This is just a guess though, I have no evidence more than the example you showed.

I hope this helps!

Hi @CatalinaAlbornoz,

It makes sense. Thank you very much for looking into this.

1 Like

No problem @Radu_Marginean !

Enjoy using PennyLane, and let us know if you have any further questions in the future :smiley: