Skip to content

Fix #855: don't call cirq.unitary() on non-unitary matrices#857

Merged
mhucka merged 4 commits intomasterfrom
mh-fix-855
Aug 12, 2025
Merged

Fix #855: don't call cirq.unitary() on non-unitary matrices#857
mhucka merged 4 commits intomasterfrom
mh-fix-855

Conversation

@mhucka
Copy link
Copy Markdown
Collaborator

@mhucka mhucka commented Aug 10, 2025

In Cirq 1.6.0, the function unitary(...) in cirq-core/cirq/protocols/unitary_protocol.py changed. Whereas previously, if the value passed in was a NumPy array, the array was returned without doing anything else, unitary(...) was changed in Cirq PR #7536 to test that the NumPy array really is unitary and raise an exception if it isn't. This arguably corrected a fault in unitary(...) because with the previous definition, it would return non-unitary results if given a NumPy array that wasn't already unitary. However, one of the tests in qsim depended on this flaw: the test case knowingly invoked unitary(...) with a non-unitary matrix. This recently led to exceptions being raised in the qsim tests with Cirq 1.6.0 but not with prior Cirq versions.

The solution in this case turned out to be very simple: the call to cirq.unitary(...) on line 1060 was unnecessary because the previous version of unitary(...) simply returned the value untouched anyway. Removing the call kept the previous behavior (which was to use a non-unitary matrix in that situation) and works in both Cirq 1.5.0 and 1.6.0.

(An additional change to reformatting a few lines elsewhere was needed to satisfy the CI checks for formatting.)

In Cirq 1.6.0, the function
[`unitary(...)`](https://github.com/quantumlib/Cirq/blob/fe0dc2187ca3269c178526e8ba41083fa1a467c9/cirq-core/cirq/protocols/unitary_protocol.py#L81)
in `cirq-core/cirq/protocols/unitary_protocol.py` changed. Whereas
previously, if the value passed in was a NumPy array, it the function
returned the array directly without doing anything else, `unitary(...)`
was changed in [PR
the array really _is_ unitary and raise an exception if it isn't. This
arguably corrected a fault in `unitary(...)` because with the previous
definition, it would return non-unitary results if given a NumPy array
that wasn't already unitary. However, in one of the tests in qsim, it
had the result of revealing a small bug: the test case was knowingly
invoking `unitary(...)` with a non-unitary matrix. This led to
exceptions being raised in Cirq 1.6.0 but not in prior versions.

The solution in this case turned out to be very simple: the call to
`cirq.unitary(...)` on line 1060 was unnecessary because the previous
version of `unitary(...)` simply returned it right away. Removing the
call kept the previous behavior (which was to use a non-unitary matrix
in that situation) and works in both Cirq 1.5.0 and 1.6.0.
@github-actions github-actions Bot added the size: XS <10 lines changed label Aug 10, 2025
@github-actions github-actions Bot added size: S 10< lines changed <50 and removed size: XS <10 lines changed labels Aug 10, 2025
@mhucka mhucka marked this pull request as ready for review August 10, 2025 17:52
@mhucka
Copy link
Copy Markdown
Collaborator Author

mhucka commented Aug 10, 2025

Note: the docker build failure is due to an unrelated issue.


def _mixture_(self):
return [(prob, cirq.unitary(op)) for prob, op, in self._prob_op_pairs]
return [(prob, op) for prob, op, in self._prob_op_pairs]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is _mixture a standard protocol for qsim or is this only used for this test? This is changing the return type for this method.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't realize op was already a np.array.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. It's true that the 2nd element in the tuples in self._prob_op_pairs are actually NoiseStep objects. Lemme see if I can improve this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, with the way this all routed between different functions, I can't find a way to avoid changing this return type without rewriting test_multi_qubit_noise(). Given that this object class is only used in this file and only for that one function, that seems like it would be poor ROI.

I added a comment explaning what is happening. @dstrain115 can you check that it makes sense?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't realize op was already a np.array.

Actuallly, I was wrong about that in my earlier comment (which I deleted too slowly to save you typing that reply -- sorry about that). At this point in the flow of things, it's not an ndarray. The whole flow is a bit more complicated, which hopefully the new comment clarifies.

Comment thread qsimcirq_tests/qsimcirq_test.py Outdated
def _mixture_(self):
return [(prob, cirq.unitary(op)) for prob, op, in self._prob_op_pairs]
# Cirq's mixture() function in mixture_protocol.py returns tuples of
# the form (probability, unitary peration). It does this by applying
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

peration --> operation

Copy link
Copy Markdown
Collaborator

@pavoljuhas pavoljuhas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with 1-char typo fixed

@pavoljuhas
Copy link
Copy Markdown
Collaborator

@mhucka, @dstrain115 - should we remove the is_unitary introduced in quantumlib/Cirq#7419? (or we could replace it with a check for a square matrix)

As it is cirq.unitary(val) returns val._unitary_() without any check, but raises ValueError when val is a non-unitary array. cirq_unitary(cirq.unitary(val)) can thus fail when cirq.unitary(val) works.

@mhucka
Copy link
Copy Markdown
Collaborator Author

mhucka commented Aug 12, 2025

@mhucka, @dstrain115 - should we remove the is_unitary introduced in quantumlib/Cirq#7419? (or we could replace it with a check for a square matrix)

As it is cirq.unitary(val) returns val._unitary_() without any check, but raises ValueError when val is a non-unitary array. cirq_unitary(cirq.unitary(val)) can thus fail when cirq.unitary(val) works.

By "remove is_unitary I assume you mean remove the addition of the checks using linalg.is_unitary()? (I can't find where it introduced a function is_unitary, but maybe I missed it.)

IMHO, removing the check for whether it is unitary would go against the purpose of unitary(…). That seems wrong too. Instead, shouldn't _strat_unitary_from_unitary(…) check that the value it gets from _unitary_ actually is unitary? (Similarly for the other _strat* methods.)

@mhucka
Copy link
Copy Markdown
Collaborator Author

mhucka commented Aug 12, 2025

I opend quantumlib/Cirq#7572 to follow-up on the issue that cirq.unitary(…) may return non-unitary values.

@mhucka mhucka merged commit 2acea90 into master Aug 12, 2025
48 of 49 checks passed
@mhucka mhucka deleted the mh-fix-855 branch August 12, 2025 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: S 10< lines changed <50

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants