Repository: quantumlib/Cirq

Issue:
PauliString repr does not preserve qubit order.
**Description of the issue**

If you create a PauliString 
```
p = cirq.PauliString({q2: cirq.Z, q1: cirq.Y, q0: cirq.X})
```
and round trip it via its repr, the qubit order does not match.
```
eval(repr(p)).qubits == p.qubits
>>> False
```

PauliString having ordered qubits was introduced in #4270



Discussion:

Resolving patch:
diff --git a/cirq-core/cirq/contrib/paulistring/clifford_optimize.py b/cirq-core/cirq/contrib/paulistring/clifford_optimize.py
--- a/cirq-core/cirq/contrib/paulistring/clifford_optimize.py
+++ b/cirq-core/cirq/contrib/paulistring/clifford_optimize.py
@@ -14,7 +14,7 @@
 
 from typing import Tuple, cast
 
-from cirq import ops, circuits
+from cirq import circuits, ops, protocols
 from cirq.contrib.paulistring.convert_gate_set import converted_gate_set
 
 
@@ -87,10 +87,13 @@ def try_merge_clifford(cliff_op: ops.GateOperation, start_i: int) -> bool:
             merge_i, merge_op, num_passed = find_merge_point(start_i, string_op, quarter_turns == 2)
             assert merge_i > start_i
             assert len(merge_op.pauli_string) == 1, 'PauliString length != 1'
+            assert not protocols.is_parameterized(merge_op.pauli_string)
+            coefficient = merge_op.pauli_string.coefficient
+            assert isinstance(coefficient, complex)
 
             qubit, pauli = next(iter(merge_op.pauli_string.items()))
             quarter_turns = round(merge_op.exponent_relative * 2)
-            quarter_turns *= int(merge_op.pauli_string.coefficient.real)
+            quarter_turns *= int(coefficient.real)
             quarter_turns %= 4
             part_cliff_gate = ops.SingleQubitCliffordGate.from_quarter_turns(pauli, quarter_turns)
 
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
@@ -391,7 +391,7 @@ def _repr_pretty_(self, p: Any, cycle: bool) -> None:
             p.text(str(self))
 
     def __repr__(self) -> str:
-        ordered_qubits = sorted(self.qubits)
+        ordered_qubits = self.qubits
         prefix = ''
 
         factors = []
diff --git a/cirq-core/cirq/ops/pauli_string_phasor.py b/cirq-core/cirq/ops/pauli_string_phasor.py
--- a/cirq-core/cirq/ops/pauli_string_phasor.py
+++ b/cirq-core/cirq/ops/pauli_string_phasor.py
@@ -12,7 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from typing import AbstractSet, cast, Dict, Iterable, Union, TYPE_CHECKING, Sequence, Iterator
+from typing import (
+    AbstractSet,
+    cast,
+    Dict,
+    Iterable,
+    Iterator,
+    Optional,
+    Sequence,
+    TYPE_CHECKING,
+    Union,
+)
+
 import numbers
 
 import sympy
@@ -35,16 +46,25 @@
 
 @value.value_equality(approximate=True)
 class PauliStringPhasor(gate_operation.GateOperation):
-    """An operation that phases the eigenstates of a Pauli string.
+    r"""An operation that phases the eigenstates of a Pauli string.
+
+    This class takes `PauliString`, which is a sequence of non-identity
+    Pauli operators, potentially with a $\pm 1$ valued coefficient,
+    acting on qubits.
 
     The -1 eigenstates of the Pauli string will have their amplitude multiplied
     by e^(i pi exponent_neg) while +1 eigenstates of the Pauli string will have
     their amplitude multiplied by e^(i pi exponent_pos).
+
+    The class also takes a list of qubits, which can be a superset of those
+    acted on by the provided `PauliString`.  Those extra qubits are assumed to be
+    acted upon via identity.
     """
 
     def __init__(
         self,
         pauli_string: ps.PauliString,
+        qubits: Optional[Sequence['cirq.Qid']] = None,
         *,
         exponent_neg: Union[int, float, sympy.Expr] = 1,
         exponent_pos: Union[int, float, sympy.Expr] = 0,
@@ -54,20 +74,36 @@ def __init__(
         Args:
             pauli_string: The PauliString defining the positive and negative
                 eigenspaces that will be independently phased.
+            qubits: The qubits upon which the PauliStringPhasor acts. This
+                must be a superset of the qubits of `pauli_string`.
+                If None, it will use the qubits from `pauli_string`
+                The `pauli_string` contains only the non-identity component
+                of the phasor, while the qubits supplied here and not in
+                `pauli_string` are acted upon by identity. The order of
+                these qubits must match the order in `pauli_string`.
             exponent_neg: How much to phase vectors in the negative eigenspace,
                 in the form of the t in (-1)**t = exp(i pi t).
             exponent_pos: How much to phase vectors in the positive eigenspace,
                 in the form of the t in (-1)**t = exp(i pi t).
 
         Raises:
-            ValueError: If coefficient is not 1 or -1.
+            ValueError: If coefficient is not 1 or -1 or the qubits of
+                `pauli_string` are not a subset of `qubits`.
         """
+        if qubits is not None:
+            it = iter(qubits)
+            if any(not any(q0 == q1 for q1 in it) for q0 in pauli_string.qubits):
+                raise ValueError(
+                    f"PauliStringPhasor's pauli string qubits ({pauli_string.qubits}) "
+                    f"are not an ordered subset of the explicit qubits ({qubits})."
+                )
+        else:
+            qubits = pauli_string.qubits
+        # Use qubits below instead of `qubits or pauli_string.qubits`
         gate = PauliStringPhasorGate(
-            pauli_string.dense(pauli_string.qubits),
-            exponent_neg=exponent_neg,
-            exponent_pos=exponent_pos,
+            pauli_string.dense(qubits), exponent_neg=exponent_neg, exponent_pos=exponent_pos
         )
-        super().__init__(gate, pauli_string.qubits)
+        super().__init__(gate, qubits)
         self._pauli_string = gate.dense_pauli_string.on(*self.qubits)
 
     @property
@@ -76,17 +112,17 @@ def gate(self) -> 'cirq.PauliStringPhasorGate':
         return cast(PauliStringPhasorGate, self._gate)
 
     @property
-    def exponent_neg(self):
+    def exponent_neg(self) -> Union[int, float, sympy.Expr]:
         """The negative exponent."""
         return self.gate.exponent_neg
 
     @property
-    def exponent_pos(self):
+    def exponent_pos(self) -> Union[int, float, sympy.Expr]:
         """The positive exponent."""
         return self.gate.exponent_pos
 
     @property
-    def pauli_string(self):
+    def pauli_string(self) -> 'cirq.PauliString':
         """The underlying pauli string."""
         return self._pauli_string
 
@@ -96,27 +132,48 @@ def exponent_relative(self) -> Union[int, float, sympy.Expr]:
         return self.gate.exponent_relative
 
     def _value_equality_values_(self):
-        return (self.pauli_string, self.exponent_neg, self.exponent_pos)
+        return (self.pauli_string, self.qubits, self.exponent_neg, self.exponent_pos)
 
-    def equal_up_to_global_phase(self, other):
+    def equal_up_to_global_phase(self, other: 'PauliStringPhasor') -> bool:
         """Checks equality of two PauliStringPhasors, up to global phase."""
         if isinstance(other, PauliStringPhasor):
-            rel1 = self.exponent_relative
-            rel2 = other.exponent_relative
-            return rel1 == rel2 and self.pauli_string == other.pauli_string
+            return (
+                self.exponent_relative == other.exponent_relative
+                and self.pauli_string == other.pauli_string
+                and self.qubits == other.qubits
+            )
         return False
 
-    def map_qubits(self, qubit_map: Dict[raw_types.Qid, raw_types.Qid]):
-        """Maps the qubits inside the PauliString."""
+    def map_qubits(self, qubit_map: Dict[raw_types.Qid, raw_types.Qid]) -> 'PauliStringPhasor':
+        """Maps the qubits inside the PauliStringPhasor.
+
+        Args:
+            qubit_map: A map from the qubits in the phasor to new qubits.
+
+        Returns:
+            A new PauliStringPhasor with remapped qubits.
+
+        Raises:
+            ValueError: If the map does not contain an entry for all
+                the qubits in the phasor.
+        """
+        if not set(self.qubits) <= qubit_map.keys():
+            raise ValueError(
+                "qubit_map must have a key for every qubit in the phasors qubits. "
+                f"keys: {qubit_map.keys()} phasor qubits: {self.qubits}"
+            )
         return PauliStringPhasor(
-            self.pauli_string.map_qubits(qubit_map),
+            pauli_string=self.pauli_string.map_qubits(qubit_map),
+            qubits=[qubit_map[q] for q in self.qubits],
             exponent_neg=self.exponent_neg,
             exponent_pos=self.exponent_pos,
         )
 
     def can_merge_with(self, op: 'PauliStringPhasor') -> bool:
         """Checks whether the underlying PauliStrings can be merged."""
-        return self.pauli_string.equal_up_to_coefficient(op.pauli_string)
+        return (
+            self.pauli_string.equal_up_to_coefficient(op.pauli_string) and self.qubits == op.qubits
+        )
 
     def merged_with(self, op: 'PauliStringPhasor') -> 'PauliStringPhasor':
         """Merges two PauliStringPhasors."""
@@ -124,13 +181,21 @@ def merged_with(self, op: 'PauliStringPhasor') -> 'PauliStringPhasor':
             raise ValueError(f'Cannot merge operations: {self}, {op}')
         pp = self.exponent_pos + op.exponent_pos
         pn = self.exponent_neg + op.exponent_neg
-        return PauliStringPhasor(self.pauli_string, exponent_pos=pp, exponent_neg=pn)
+        return PauliStringPhasor(
+            self.pauli_string, qubits=self.qubits, exponent_pos=pp, exponent_neg=pn
+        )
 
     def _circuit_diagram_info_(
         self, args: 'cirq.CircuitDiagramInfoArgs'
     ) -> 'cirq.CircuitDiagramInfo':
         qubits = self.qubits if args.known_qubits is None else args.known_qubits
-        syms = tuple(f'[{self.pauli_string[qubit]}]' for qubit in qubits)
+
+        def sym(qubit):
+            if qubit in self.pauli_string:
+                return f'[{self.pauli_string[qubit]}]'
+            return '[I]'
+
+        syms = tuple(sym(qubit) for qubit in qubits)
         return protocols.CircuitDiagramInfo(wire_symbols=syms, exponent=self.exponent_relative)
 
     def pass_operations_over(
@@ -170,6 +235,7 @@ def pass_operations_over(
     def __repr__(self) -> str:
         return (
             f'cirq.PauliStringPhasor({self.pauli_string!r}, '
+            f'qubits={self.qubits!r}, '
             f'exponent_neg={proper_repr(self.exponent_neg)}, '
             f'exponent_pos={proper_repr(self.exponent_pos)})'
         )
@@ -182,7 +248,19 @@ def __str__(self) -> str:
         return f'({self.pauli_string})**{self.exponent_relative}'
 
     def _json_dict_(self):
-        return protocols.obj_to_dict_helper(self, ['pauli_string', 'exponent_neg', 'exponent_pos'])
+        return protocols.obj_to_dict_helper(
+            self, ['pauli_string', 'qubits', 'exponent_neg', 'exponent_pos']
+        )
+
+    @classmethod
+    def _from_json_dict_(cls, pauli_string, exponent_neg, exponent_pos, **kwargs):
+        qubits = kwargs['qubits'] if 'qubits' in kwargs else None
+        return PauliStringPhasor(
+            pauli_string=pauli_string,
+            qubits=qubits,
+            exponent_neg=exponent_neg,
+            exponent_pos=exponent_pos,
+        )
 
 
 @value.value_equality(approximate=True)
@@ -234,24 +312,24 @@ def exponent_relative(self) -> Union[int, float, sympy.Expr]:
         return value.canonicalize_half_turns(self.exponent_neg - self.exponent_pos)
 
     @property
-    def exponent_neg(self):
+    def exponent_neg(self) -> Union[int, float, sympy.Expr]:
         """The negative exponent."""
         return self._exponent_neg
 
     @property
-    def exponent_pos(self):
+    def exponent_pos(self) -> Union[int, float, sympy.Expr]:
         """The positive exponent."""
         return self._exponent_pos
 
     @property
-    def dense_pauli_string(self):
+    def dense_pauli_string(self) -> 'cirq.DensePauliString':
         """The underlying DensePauliString."""
         return self._dense_pauli_string
 
     def _value_equality_values_(self):
         return (self.dense_pauli_string, self.exponent_neg, self.exponent_pos)
 
-    def equal_up_to_global_phase(self, other):
+    def equal_up_to_global_phase(self, other: 'cirq.PauliStringPhasorGate') -> bool:
         """Checks equality of two PauliStringPhasors, up to global phase."""
         if isinstance(other, PauliStringPhasorGate):
             rel1 = self.exponent_relative
@@ -266,7 +344,7 @@ def __pow__(self, exponent: Union[float, sympy.Symbol]) -> 'PauliStringPhasorGat
             return NotImplemented
         return PauliStringPhasorGate(self.dense_pauli_string, exponent_neg=pn, exponent_pos=pp)
 
-    def _has_unitary_(self):
+    def _has_unitary_(self) -> bool:
         return not self._is_parameterized_()
 
     def _to_z_basis_ops(self, qubits: Sequence['cirq.Qid']) -> Iterator[raw_types.Operation]:
@@ -352,6 +430,7 @@ def on(self, *qubits: 'cirq.Qid') -> 'cirq.PauliStringPhasor':
         """Creates a PauliStringPhasor on the qubits."""
         return PauliStringPhasor(
             self.dense_pauli_string.on(*qubits),
+            qubits=qubits,
             exponent_pos=self.exponent_pos,
             exponent_neg=self.exponent_neg,
         )


Test patch:
diff --git a/cirq-core/cirq/ops/gate_operation_test.py b/cirq-core/cirq/ops/gate_operation_test.py
--- a/cirq-core/cirq/ops/gate_operation_test.py
+++ b/cirq-core/cirq/ops/gate_operation_test.py
@@ -488,6 +488,7 @@ def all_subclasses(cls):
 
     skip_classes = {
         # Abstract or private parent classes.
+        cirq.ArithmeticGate,
         cirq.BaseDensePauliString,
         cirq.EigenGate,
         cirq.Pauli,
@@ -503,17 +504,10 @@ def all_subclasses(cls):
         # Interop gates
         cirq.interop.quirk.QuirkQubitPermutationGate,
         cirq.interop.quirk.QuirkArithmeticGate,
-        # No reason given for missing json.
-        # TODO(#5353): Serialize these gates.
-        cirq.ArithmeticGate,
     }
 
-    # Gates that do not satisfy the contract.
-    # TODO(#5167): Fix this case.
-    exceptions = {cirq.PauliStringPhasorGate}
-
     skipped = set()
-    for gate_cls in gate_subclasses - exceptions:
+    for gate_cls in gate_subclasses:
         filename = test_module_spec.test_data_path.joinpath(f"{gate_cls.__name__}.json")
         if pathlib.Path(filename).is_file():
             gates = cirq.read_json(filename)
diff --git a/cirq-core/cirq/ops/pauli_string_phasor_test.py b/cirq-core/cirq/ops/pauli_string_phasor_test.py
--- a/cirq-core/cirq/ops/pauli_string_phasor_test.py
+++ b/cirq-core/cirq/ops/pauli_string_phasor_test.py
@@ -36,7 +36,6 @@ def test_init():
     a = cirq.LineQubit(0)
     with pytest.raises(ValueError, match='eigenvalues'):
         _ = cirq.PauliStringPhasor(1j * cirq.X(a))
-
     v1 = cirq.PauliStringPhasor(-cirq.X(a), exponent_neg=0.25, exponent_pos=-0.5)
     assert v1.pauli_string == cirq.X(a)
     assert v1.exponent_neg == -0.5
@@ -48,8 +47,20 @@ def test_init():
     assert v2.exponent_pos == -0.125
 
 
+def test_qubit_order_mismatch():
+    q0, q1 = cirq.LineQubit.range(2)
+    with pytest.raises(ValueError, match='are not an ordered subset'):
+        _ = cirq.PauliStringPhasor(1j * cirq.X(q0), qubits=[q1])
+    with pytest.raises(ValueError, match='are not an ordered subset'):
+        _ = cirq.PauliStringPhasor(1j * cirq.X(q0) * cirq.X(q1), qubits=[q1])
+    with pytest.raises(ValueError, match='are not an ordered subset'):
+        _ = cirq.PauliStringPhasor(1j * cirq.X(q0), qubits=[])
+    with pytest.raises(ValueError, match='are not an ordered subset'):
+        _ = cirq.PauliStringPhasor(1j * cirq.X(q0) * cirq.X(q1), qubits=[q1, q0])
+
+
 def test_eq_ne_hash():
-    q0, q1, q2 = _make_qubits(3)
+    q0, q1, q2, q3 = _make_qubits(4)
     eq = cirq.testing.EqualsTester()
     ps1 = cirq.X(q0) * cirq.Y(q1) * cirq.Z(q2)
     ps2 = cirq.X(q0) * cirq.Y(q1) * cirq.X(q2)
@@ -66,10 +77,11 @@ def test_eq_ne_hash():
     eq.add_equality_group(cirq.PauliStringPhasor(ps2, exponent_neg=0.5))
     eq.add_equality_group(cirq.PauliStringPhasor(-ps2, exponent_neg=-0.5))
     eq.add_equality_group(cirq.PauliStringPhasor(ps1, exponent_neg=sympy.Symbol('a')))
+    eq.add_equality_group(cirq.PauliStringPhasor(ps1, qubits=[q0, q1, q2, q3]))
 
 
 def test_equal_up_to_global_phase():
-    a, b = cirq.LineQubit.range(2)
+    a, b, c = cirq.LineQubit.range(3)
     groups = [
         [
             cirq.PauliStringPhasor(cirq.PauliString({a: cirq.X}), exponent_neg=0.25),
@@ -83,6 +95,11 @@ def test_equal_up_to_global_phase():
         [cirq.PauliStringPhasor(cirq.PauliString({a: cirq.X}))],
         [cirq.PauliStringPhasor(cirq.PauliString({a: cirq.Y}), exponent_neg=0.25)],
         [cirq.PauliStringPhasor(cirq.PauliString({a: cirq.X, b: cirq.Y}), exponent_neg=0.25)],
+        [
+            cirq.PauliStringPhasor(
+                cirq.PauliString({a: cirq.X, b: cirq.Y}), qubits=[a, b, c], exponent_neg=0.25
+            )
+        ],
     ]
     for g1 in groups:
         for e1 in g1:
@@ -93,12 +110,29 @@ def test_equal_up_to_global_phase():
 
 
 def test_map_qubits():
-    q0, q1, q2, q3 = _make_qubits(4)
+    q0, q1, q2, q3, q4, q5 = _make_qubits(6)
     qubit_map = {q1: q2, q0: q3}
     before = cirq.PauliStringPhasor(cirq.PauliString({q0: cirq.Z, q1: cirq.Y}), exponent_neg=0.1)
     after = cirq.PauliStringPhasor(cirq.PauliString({q3: cirq.Z, q2: cirq.Y}), exponent_neg=0.1)
     assert before.map_qubits(qubit_map) == after
 
+    qubit_map = {q1: q3, q0: q4, q2: q5}
+    before = cirq.PauliStringPhasor(
+        cirq.PauliString({q0: cirq.Z, q1: cirq.Y}), qubits=[q0, q1, q2], exponent_neg=0.1
+    )
+    after = cirq.PauliStringPhasor(
+        cirq.PauliString({q4: cirq.Z, q3: cirq.Y}), qubits=[q4, q3, q5], exponent_neg=0.1
+    )
+    assert before.map_qubits(qubit_map) == after
+
+
+def test_map_qubits_missing_qubits():
+    q0, q1, q2 = _make_qubits(3)
+    qubit_map = {q1: q2}
+    before = cirq.PauliStringPhasor(cirq.PauliString({q0: cirq.Z, q1: cirq.Y}), exponent_neg=0.1)
+    with pytest.raises(ValueError, match="have a key"):
+        _ = before.map_qubits(qubit_map)
+
 
 def test_pow():
     a = cirq.LineQubit(0)
@@ -108,12 +142,16 @@ def test_pow():
     with pytest.raises(TypeError, match='unsupported operand'):
         _ = p ** object()
     assert p**1 == p
+    p = cirq.PauliStringPhasor(s, qubits=[a], exponent_neg=0.25, exponent_pos=0.5)
+    assert p**0.5 == cirq.PauliStringPhasor(s, exponent_neg=0.125, exponent_pos=0.25)
 
 
 def test_consistent():
     a, b = cirq.LineQubit.range(2)
     op = np.exp(1j * np.pi / 2 * cirq.X(a) * cirq.X(b))
     cirq.testing.assert_implements_consistent_protocols(op)
+    p = cirq.PauliStringPhasor(cirq.X(a), qubits=[a], exponent_neg=0.25, exponent_pos=0.5)
+    cirq.testing.assert_implements_consistent_protocols(p)
 
 
 def test_pass_operations_over():
@@ -167,7 +205,7 @@ def test_inverse():
 
 
 def test_can_merge_with():
-    (q0,) = _make_qubits(1)
+    q0, q1 = _make_qubits(2)
 
     op1 = cirq.PauliStringPhasor(cirq.PauliString({}), exponent_neg=0.25)
     op2 = cirq.PauliStringPhasor(cirq.PauliString({}), exponent_neg=0.75)
@@ -181,6 +219,12 @@ def test_can_merge_with():
     op2 = cirq.PauliStringPhasor(cirq.PauliString({q0: cirq.Y}, -1), exponent_neg=0.75)
     assert not op1.can_merge_with(op2)
 
+    op1 = cirq.PauliStringPhasor(
+        cirq.PauliString({q0: cirq.X}, +1), qubits=[q0, q1], exponent_neg=0.25
+    )
+    op2 = cirq.PauliStringPhasor(cirq.PauliString({q0: cirq.X}, -1), exponent_neg=0.75)
+    assert not op1.can_merge_with(op2)
+
 
 def test_merge_with():
     (q0,) = _make_qubits(1)
@@ -363,16 +407,17 @@ def test_text_diagram():
             cirq.PauliString({q0: cirq.Z, q1: cirq.Y, q2: cirq.X}, -1),
             exponent_neg=sympy.Symbol('b'),
         ),
+        cirq.PauliStringPhasor(cirq.PauliString({q0: cirq.Z}), qubits=[q0, q1], exponent_neg=0.5),
     )
 
     cirq.testing.assert_has_diagram(
         circuit,
         """
-q0: ───[Z]───[Y]^0.25───[Z]───[Z]────────[Z]─────[Z]────────
-                        │     │          │       │
-q1: ────────────────────[Z]───[Y]────────[Y]─────[Y]────────
+q0: ───[Z]───[Y]^0.25───[Z]───[Z]────────[Z]─────[Z]────────[Z]───────
+                        │     │          │       │          │
+q1: ────────────────────[Z]───[Y]────────[Y]─────[Y]────────[I]^0.5───
                         │     │          │       │
-q2: ────────────────────[Z]───[X]^-0.5───[X]^a───[X]^(-b)───
+q2: ────────────────────[Z]───[X]^-0.5───[X]^a───[X]^(-b)─────────────
 """,
     )
 
@@ -408,6 +453,75 @@ def test_str():
     assert str(np.exp(-0.25j * np.pi * cirq.X(q0) * cirq.Y(q1))) == 'exp(-iπ0.25*X(q0)*Y(q1))'
     assert str(np.exp(0.5j * np.pi * cirq.PauliString())) == 'exp(iπ0.5*I)'
 
+    ps = cirq.PauliStringPhasor(cirq.PauliString({q0: cirq.X}, +1), qubits=[q0, q1]) ** 0.5
+    assert str(ps) == '(X(q0))**0.5'
+
+
+def test_old_json():
+    """Older versions of PauliStringPhasor did not have a qubit field."""
+    old_json = """
+    {
+      "cirq_type": "PauliStringPhasor",
+      "pauli_string": {
+        "cirq_type": "PauliString",
+        "qubit_pauli_map": [
+          [
+            {
+              "cirq_type": "LineQubit",
+              "x": 0
+            },
+            {
+              "cirq_type": "_PauliX",
+              "exponent": 1.0,
+              "global_shift": 0.0
+            }
+          ],
+          [
+            {
+              "cirq_type": "LineQubit",
+              "x": 1
+            },
+            {
+              "cirq_type": "_PauliY",
+              "exponent": 1.0,
+              "global_shift": 0.0
+            }
+          ],
+          [
+            {
+              "cirq_type": "LineQubit",
+              "x": 2
+            },
+            {
+              "cirq_type": "_PauliZ",
+              "exponent": 1.0,
+              "global_shift": 0.0
+            }
+          ]
+        ],
+        "coefficient": {
+          "cirq_type": "complex",
+          "real": 1.0,
+          "imag": 0.0
+        }
+      },
+      "exponent_neg": 0.2,
+      "exponent_pos": 0.1
+    }
+    """
+    phasor = cirq.read_json(json_text=old_json)
+    assert phasor == cirq.PauliStringPhasor(
+        (
+            (1 + 0j)
+            * cirq.X(cirq.LineQubit(0))
+            * cirq.Y(cirq.LineQubit(1))
+            * cirq.Z(cirq.LineQubit(2))
+        ),
+        qubits=(cirq.LineQubit(0), cirq.LineQubit(1), cirq.LineQubit(2)),
+        exponent_neg=0.2,
+        exponent_pos=0.1,
+    )
+
 
 def test_gate_init():
     a = cirq.LineQubit(0)
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
@@ -310,6 +310,18 @@ def test_repr():
     cirq.testing.assert_equivalent_repr(cirq.PauliString())
 
 
+def test_repr_preserves_qubit_order():
+    q0, q1, q2 = _make_qubits(3)
+    pauli_string = cirq.PauliString({q2: cirq.X, q1: cirq.Y, q0: cirq.Z})
+    assert eval(repr(pauli_string)).qubits == pauli_string.qubits
+
+    pauli_string = cirq.PauliString(cirq.X(q2), cirq.Y(q1), cirq.Z(q0))
+    assert eval(repr(pauli_string)).qubits == pauli_string.qubits
+
+    pauli_string = cirq.PauliString(cirq.Z(q0), cirq.Y(q1), cirq.X(q2))
+    assert eval(repr(pauli_string)).qubits == pauli_string.qubits
+
+
 def test_repr_coefficient_of_one():
     pauli_string = cirq.Z(cirq.LineQubit(0)) * 1
     assert type(pauli_string) == type(eval(repr(pauli_string)))
diff --git a/cirq-core/cirq/protocols/json_test_data/PauliStringPhasor.json b/cirq-core/cirq/protocols/json_test_data/PauliStringPhasor.json
--- a/cirq-core/cirq/protocols/json_test_data/PauliStringPhasor.json
+++ b/cirq-core/cirq/protocols/json_test_data/PauliStringPhasor.json
@@ -1,9 +1,6 @@
-{
+ {
   "cirq_type": "PauliStringPhasor",
-  "exponent_neg": 0.2,
-  "exponent_pos": 0.1,
-  "pauli_string":
-  {
+  "pauli_string": {
     "cirq_type": "PauliString",
     "qubit_pauli_map": [
       [
@@ -13,7 +10,7 @@
         },
         {
           "cirq_type": "_PauliX",
-          "exponent": 1,
+          "exponent": 1.0,
           "global_shift": 0.0
         }
       ],
@@ -24,7 +21,18 @@
         },
         {
           "cirq_type": "_PauliY",
-          "exponent": 1,
+          "exponent": 1.0,
+          "global_shift": 0.0
+        }
+      ],
+      [
+        {
+          "cirq_type": "LineQubit",
+          "x": 2
+        },
+        {
+          "cirq_type": "_PauliZ",
+          "exponent": 1.0,
           "global_shift": 0.0
         }
       ]
@@ -34,5 +42,21 @@
       "real": 1.0,
       "imag": 0.0
     }
-  }
-}
\ No newline at end of file
+  },
+  "qubits": [
+    {
+      "cirq_type": "LineQubit",
+      "x": 0
+    },
+    {
+      "cirq_type": "LineQubit",
+      "x": 1
+    },
+    {
+      "cirq_type": "LineQubit",
+      "x": 2
+    }
+  ],
+  "exponent_neg": 0.2,
+  "exponent_pos": 0.1
+}
diff --git a/cirq-core/cirq/protocols/json_test_data/PauliStringPhasor.repr b/cirq-core/cirq/protocols/json_test_data/PauliStringPhasor.repr
--- a/cirq-core/cirq/protocols/json_test_data/PauliStringPhasor.repr
+++ b/cirq-core/cirq/protocols/json_test_data/PauliStringPhasor.repr
@@ -1 +1 @@
-cirq.PauliStringPhasor(cirq.X.on(cirq.LineQubit(0))*cirq.Y.on(cirq.LineQubit(1)), exponent_neg=0.2, exponent_pos=0.1)
\ No newline at end of file
+cirq.PauliStringPhasor(((1+0j)*cirq.X(cirq.LineQubit(0))*cirq.Y(cirq.LineQubit(1))*cirq.Z(cirq.LineQubit(2))), qubits=(cirq.LineQubit(0), cirq.LineQubit(1), cirq.LineQubit(2)), exponent_neg=0.2, exponent_pos=0.1)
\ No newline at end of file