Repository: quantumlib/Cirq

Issue:
PauliString class sorts qubits by default, which results in confusing semantics
### Description
`PauliString` represents a collection of pauli's applied on different qubits. Since pauli operators applied to different qubits commute with each other, the class sorts all the qubits by default. Eg:

```python
In [3]: cirq.X(q[1]) * cirq.Y(q[2]) * cirq.Z(q[0])
Out[3]: Z(0)*X(1)*Y(2) # Instead of X(1) * Y(2) * Z(0)
```
Sorting qubits is not common in other operations (eg: `ControlledOperations`, `GateOperations` etc.), and has some confusing semantics as shown below:

### Conversion to/from `DensePauliString` results in different gates
`gate_instance == gate_instance.on().gate()`  should generally be true for all gates, but this is not the case with `DensePauliString`. Eg: 
```python
In [4]: cirq.DensePauliString('XYZ', coefficient=(1+0j)).on(q[1], q[2], q[0]).gate
Out[4]: cirq.DensePauliString('ZXY', coefficient=(1+0j))
```

### `with_qubits` returns a different gate applied on different qubits.  
`with_qubits` function is supposed to return the `same operations applied to different qubits` but the underlying gate also changes with qubit ordering, similar to the same issue as above. Eg:
```python
In [5]: ps = cirq.X(q[0]) * cirq.Y(q[1]) * cirq.Z(q[2])
   ...: ps.gate == ps.with_qubits(q[1], q[2], q[0]).gate
Out[5]: False
``` 

### Need for a `matrix()` protocol. 
Since changing the ordering of the qubits (which happens implicitly) changes the underlying "gate" represented by the operation, the unitary matrix of the operation is not necessarily the same as what one would expect. To get the expected representation, one needs to use `.matrix()`, which makes a case for matrix protocol propsoed in #3099. Eg:

```python
In [6]: m_expected = cirq.linalg.kron(*[cirq.unitary(p) for p in [cirq.X, cirq.Y, cirq.Z]])
    ..: m_unitary = cirq.unitary(cirq.X(q[2]) * cirq.Y(q[1]) * cirq.Z(q[0]))
    ..: m_matrix = (cirq.X(q[2]) * cirq.Y(q[1]) * cirq.Z(q[0])).matrix([q[2], q[1], q[0]])
    ..: np.allclose(m_expected, m_unitary), np.allclose(m_expected, m_matrix)
Out[7]: (False, True)
```


### Proposal
Maybe we should store an explicit `QubitOrder` as well to improve consistency between the operator and underlying gate's representations? 
Update: Note that `QubitOrder` is not serializable and hence it might make more sense to use an `OrderedDict` for `qubit_pauli_map` or an explicit `Iterable[raw_types.Qid]` for storing the `qubit_order`. 


Upd: This is part of organizing gate relationships #3242



Discussion:
Adding `QubitOrder` for Paulistring [initialiser](https://github.com/quantumlib/Cirq/blob/c28646feea44b2ea236f254b2b8416acaa74e539/cirq-core/cirq/ops/pauli_string.py#L114) is possible. 
1. This variable will have to be updated each time an operation (`*`  in the above case) is performed on the gateOperation.  
2. This will also need refactor to `*mul*/*multiply*`  [helpers](https://github.com/quantumlib/Cirq/blob/c28646feea44b2ea236f254b2b8416acaa74e539/cirq-core/cirq/ops/pauli_string.py#L160) in PauliString.
3. Use `QubitOrder` to return the [property](https://github.com/quantumlib/Cirq/blob/c28646feea44b2ea236f254b2b8416acaa74e539/cirq-core/cirq/ops/pauli_string.py#L324) `qubits` 
This should be done, let's figure out how to do it. 

Resolving patch:
diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py
--- a/cirq-core/cirq/ops/pauli_string.py
+++ b/cirq-core/cirq/ops/pauli_string.py
@@ -325,7 +325,7 @@ def keys(self) -> KeysView[TKey]:
 
     @property
     def qubits(self) -> Tuple[TKey, ...]:
-        return tuple(sorted(self.keys()))
+        return tuple(self.keys())
 
     def _circuit_diagram_info_(self, args: 'cirq.CircuitDiagramInfoArgs') -> List[str]:
         if not len(self._qubit_pauli_map):


Test patch:
diff --git a/cirq-core/cirq/ops/pauli_string_test.py b/cirq-core/cirq/ops/pauli_string_test.py
--- a/cirq-core/cirq/ops/pauli_string_test.py
+++ b/cirq-core/cirq/ops/pauli_string_test.py
@@ -860,7 +860,7 @@ def test_decompose():
         cirq.X(a),
         cirq.Z(b),
     ]
-    assert cirq.decompose_once(cirq.Y(b) * cirq.Z(a)) == [cirq.Z(a), cirq.Y(b)]
+    assert cirq.decompose_once(cirq.Y(b) * cirq.Z(a)) == [cirq.Y(b), cirq.Z(a)]
 
 
 def test_rejects_non_paulis():
@@ -1350,6 +1350,15 @@ def test_dense():
         _ = p.dense([a, b, d])
 
 
+@pytest.mark.parametrize('qubits', [*itertools.permutations(cirq.LineQubit.range(3))])
+def test_gate_consistent(qubits):
+    g = cirq.DensePauliString('XYZ')
+    assert g == g(*qubits).gate
+    a, b, c = cirq.GridQubit.rect(1, 3)
+    ps = cirq.X(a) * cirq.Y(b) * cirq.Z(c)
+    assert ps.gate == ps.with_qubits(*qubits).gate
+
+
 def test_conjugated_by_incorrectly_powered_cliffords():
     a, b = cirq.LineQubit.range(2)
     p = cirq.PauliString([cirq.X(a), cirq.Z(b)])