PauliSum reorders the qubits, possibly in a non-deterministic way
Description of the issue
When retrieving the terms of a PauliSum, the resulting PauliString may have their qubits reordered compared to what was their original order when added to the sum.
What is happening behind the scenes is that the Pauli string cirq.DensePauliString('XY').on(q0, q1) is added to the PauliSum, but may be turned into a cirq.DensePauliString('YX').on(q1, q0) when retrieving the first term of the sum. They represent the same operation indeed, but do not have the same representation and unitary.
As a result, everything based on PauliSum produces consistent results at the circuit level, but x and PauliSum() + x are not guaranteed to be equivalent at the representation and unitary level. In addition, PauliSum() + x might have an inconsistent representation over several calls.
How to reproduce the issue
import cirq
import numpy as np
qubits = cirq.LineQubit.range(2)
for string in ["XY", "YX"]:
original_ps = cirq.DensePauliString(string).on(*qubits)
in_sum_ps = list(cirq.PauliSum() + original_ps)[0]
print(original_ps.dense(original_ps.qubits), in_sum_ps.dense(in_sum_ps.qubits))
print(np.allclose(cirq.unitary(original_ps), cirq.unitary(in_sum_ps)))
print(np.allclose(
cirq.unitary(cirq.Circuit(original_ps)),
cirq.unitary(cirq.Circuit(in_sum_ps)))
)
Output
(cirq-py3) vagrant@ubuntu-focal:/vagrant/Cirq$ python3 pauli_sum_bug.py
+XY +YX
False
True
+YX +YX
True
True
(cirq-py3) vagrant@ubuntu-focal:/vagrant/Cirq$ python3 pauli_sum_bug.py
+XY +YX
False
True
+YX +YX
True
True
(cirq-py3) vagrant@ubuntu-focal:/vagrant/Cirq$ python3 pauli_sum_bug.py
+XY +XY
True
True
+YX +XY
False
True
(cirq-py3) vagrant@ubuntu-focal:/vagrant/Cirq$ python3 pauli_sum_bug.py
+XY +XY
True
True
+YX +YX
True
True
Output in Colab
+XY +YX
False
True
+YX +XY
False
True
Cirq version
Observed on 1.5.0.dev with python 3.11.9 (main, Apr 6 2024, 17:59:24) [GCC 9.4.0] in a dev environement. Non-deterministic behavior.
Observed on 1.5.0.dev20240531223815 with python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] running in Colab. Deterministic behavior, but qubit order consistently swapped in the string repr and unitary.
Adding some pointers:
Each term of the PauliSum is turned back into a PauliString in _pauli_string_from_unit. The element of the Pauli string are stored in a frozenset which is not guaranteed to return items over several runs (just use the system hash, not stable with some python implementations). My suggestion is to always return the qubits in sorted order.
Suggested fix: https://github.com/quantumlib/Cirq/compare/main...burlemarxiste:Cirq:qubit_order_in_sum
cirq-sync: it makes sense to maintain the original qubit order. to do this we need to keep track of the original qubit order and use it when computing the unitary or decomposition
I had a somewhat similar issue using the all_qubits() function on circuits. For example, this snippet
import cirq
qubits = [*cirq.LineQubit.range(10), cirq.NamedQubit("ancilla")]
circ = cirq.Circuit()
for q in qubits:
circ.append(cirq.X(q))
print(list(circ.all_qubits()))
prints [cirq.LineQubit(6), cirq.LineQubit(1), cirq.LineQubit(9), cirq.LineQubit(4), cirq.LineQubit(0), cirq.LineQubit(7), cirq.LineQubit(2), cirq.LineQubit(8), cirq.LineQubit(3), cirq.NamedQubit('ancilla'), cirq.LineQubit(5)].
Using sorted() on the above list returns a nicely ordered qubit list:
[cirq.LineQubit(0), cirq.LineQubit(1), cirq.LineQubit(2), cirq.LineQubit(3), cirq.LineQubit(4), cirq.LineQubit(5), cirq.LineQubit(6), cirq.LineQubit(7), cirq.LineQubit(8), cirq.LineQubit(9), cirq.NamedQubit('ancilla')]
It feels like the second option should be the default. These issues are possibly unrelated. I can open another issue if so.