PauliGate and PauliString equality works only on odd exponents
Describe the issue
There's an inconsistency in how Pauli ops handle exponentiation and multiplication with respect to equality. While equality holds when the exponent is odd, it doesn't when the exponent is even.
import cirq
x = cirq.X(cirq.LineQubit(0))
print(1 * x == x) # True
print(x * x == x**2) # False
print(x * x * x == x**3) # True
print(x * x * x * x == x**4) # False
print(x * x * x * x * x == x**5) # True
print(x * x * x * x * x * x == x**6) # False
print(x * x * x * x * x * x * x == x**7) # True
There may not be a way around this. The reason it doesn't hold for even exponents is that x * x evaluates to an empty PauliString(), as PauliString silently drops any identity qubits, whereas x**2 is still an XPowGate on a qubit.
The options I can think of would be (no particular order):
- Modify PauliString to retain identity qubits, but I assume there's a reason it was designed the way it is, and/or would be a breaking change.
- Change it so that PauliString and GateOperation[X/Y/ZPowGate] are not comparable, so both even and odd exponents return False.
- Allow e.g.
(X**2==PauliString()) == True. This would run into transitivity issues because we'd also need(Y**2==PauliString()) == Trueetc., butX**2 != Y**2. If we were to go with this option, we would likely need to create a universalIdentityequality value, and have all "identity" ops be equal. On the surface, that actually seems kind of nice, an easy way to checkif op==cirq.Igenerically, but note it would implycirq.I(q1) == cirq.I(q2)for q1 != q2, which seems wrong. - Change it so that PauliString and GateOperation[X/Y/ZPowGate] are only comparable when the exponent is one. This also causes transitivity problems because
x==x**3==x**5==.... So ifx == PauliString(x)butx**3 != PauliString(x), that could lead to problems. - Leave things as-is.
Tell us the version of Cirq where this happens
1.6.1
Hi @daxfohl! I'd like to give it a try, looking at the code, I think the problem is that the multiplication and power operations are either returning different types or not implemented. Feel free to assign to me.
@ToastCheng yeah that was my thought too. I started looking at it here https://github.com/quantumlib/Cirq/compare/main...daxfohl:fix-pauli-exponent, and really it just came down to implementing SingleQubitPauliStringGateOperation.__pow__, and having that and __mul__ return SingleQubitPauliStringGateOperation when possible. That actually made it possible to clean up a bunch of other code that worked around the lack of consistency in those operations.
The main challenge was with equality checking, as for backward consistency it was necessary to have some special handling for SingleQubitPauliStringGateOperation comparisons to raw PauliString, to raw GateOperations, to a special NotImplementedChecker and to have them all hash correctly.
I finally came up with something that worked for all existing tests (the linked branch), but as I started thinking about it more, something was nagging at me. It finally occurred to me that having even number exponents evaluate as equal may not be well-formed. If they do, then X**2 == X * X == PauliString() == Y * Y == Y**2. But X**2 != Y**2 in Cirq, and I don't think it'd be possible without some big changes that are probably breaking, so transitivity of equality would be lost, equal things would have different hashes, etc. Note this isn't a problem with odd exponents because their PauliString representations are all distinct.
So, IDK if that would be worse than the inconsistency presented in the issue or not. It kind of makes me think that maybe something is more fundamentally broken here. But, more eyes can help. Maybe there's an easy option I'm missing. Feel free to take a look!
@ToastCheng I went ahead and split this into two issues. I updated this one so it's just about equality of even/odd exponents, and it may end up being a "will not fix", given each of the alternatives seem to have concerns. (Though feel free to propose other options if you can think of any)! I'll unassign you from this issue for now since I think we need to establish consensus on whether we're going to do anything about it first.
The issue related to the TypeError exceptions, I extracted to #7588. AFAICT that should be tractable. Post something on that issue if you'd like to take it.
Discussed in Cirq Cynq 2025-08-20:
- Consensus is that this shouldn't be left as-is. Question is what the best fix would be.
- Dax notes: making a change to overload the equality would likely have a lot of impact, based on past experiences with changing behavior of equality
- Nour: maybe PauliString should be changed?
- Dax: has been thinking about a “canonicalize” operation of some sort. But this would be a big project, similar to decomposition.
Marked as both triage/accepted and triage/discuss to remind us to review @ next meeting.
So as I was saying IRL, I kind of lean toward option 2. Here's the code that allows them to be equal: https://github.com/quantumlib/Cirq/blob/a165e2dc1c79e07d9a585f4638e3350d11580801/cirq-core/cirq/ops/pauli_string.py#L204-L207
The _value_equality_values_ function returns the PS as a GateOperation if it's a single-qubit op. If that is removed, then all the examples in the issue would be considered not-equal.
My own general approach to these types of things is to avoid getting too clever with __eq__ in general. Especially when considering that it also requires __hash__ to be equal, and can lead to surprises when using things as keys in a dictionary. (I generally try to make things obey x == y implies f(x) == f(y) (except if f is like id() or other runtime-level thing), so I'd have probably not even had x == x**3, since x.exponent != (x**3).exponent, and also someone may want to use x and x**3 as different keys in a dictionary). If there's a need to define some "equivalence" between two things, I think it's usually better if that is defined independently from __eq__. But that's just my personal take, so, can be taken with a grain of salt.
Another reason not to expand equality in this case, is that the next issue would be "parity gates don't equal PauliStrings"; i.e. cirq.XX(q1, q2) != cirq.PauliString({q1: 'x', q2: 'x'}). It just kind of opens up a can of worms.
Thanks for the explanation! Agreed that it has more cases to handle rather than a simple equivalent fix.. I could start with #7588 first.
TODO @pavoljuhas - check if anything needs to be done here (are there some impacts on cirq use from this multiplication / power inconsistency).