Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add argument assume_unitary to Operator.power. #13319

Merged
merged 3 commits into from
Nov 7, 2024

Conversation

alexanderivrii
Copy link
Contributor

@alexanderivrii alexanderivrii commented Oct 14, 2024

Summary

Fixes #13307 following @jakelishman's suggestion in the description.

Details and comments

After a quick round of profiling on my laptop (Python 10, Windows 11), the old (Schur-based) way to raise an operator to a power is 3x-5x faster than calling scipy.linalg.fractional_matrix_power, so it makes sense to use it when the operator is known to be unitary.

I have renamed the old method to be called Operator.power_if_unitary, this assumes that the given operator is unitary. The Operator.is_power does not make this assumption (and calls fractional_matrix_power). The gate class uses the power_if_unitary method.

P.S. In addition, for the power method and an integer power, I don't see much runtime difference when calling fractional_matrix_power and matrix_power. For the power_if_unitary method calling matrix_power is faster than calling the Schur-based implementation.

@alexanderivrii alexanderivrii added the Changelog: Bugfix Include in the "Fixed" section of the changelog label Oct 14, 2024
@alexanderivrii alexanderivrii added this to the 1.3.0 milestone Oct 14, 2024
@alexanderivrii alexanderivrii requested a review from a team as a code owner October 14, 2024 12:26
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@coveralls
Copy link

coveralls commented Oct 14, 2024

Pull Request Test Coverage Report for Build 11717584033

Details

  • 9 of 9 (100.0%) changed or added relevant lines in 2 files are covered.
  • 12 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.007%) to 88.81%

Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/unitary_synthesis.rs 1 92.2%
crates/qasm2/src/lex.rs 5 91.98%
crates/qasm2/src/parse.rs 6 97.62%
Totals Coverage Status
Change from base Build 11713604924: -0.007%
Covered Lines: 77811
Relevant Lines: 87615

💛 - Coveralls

Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

This generally seems sensible to me, thanks for the fix.

A minor question (and I'm fine either way): do you prefer adding a separate power_if_unitary method, or adding an assume_unitary=False flag to the existing power method? Either's fine, I'm just making sure we've thought about the interface we want.

qiskit/quantum_info/operators/operator.py Outdated Show resolved Hide resolved
@alexanderivrii alexanderivrii changed the title Fix handling of Operator.power for non-unitary operators. Add argument assume_unitary to Operator.power. Nov 3, 2024
@alexanderivrii
Copy link
Contributor Author

alexanderivrii commented Nov 3, 2024

@jakelishman, thanks for fixing the original issue. Based on review comments, I have added the assume_unitary argument to Operator.power(...) instead of introducing a separate function (it looks much cleaner and I don't know why I did not think of this myself) and applied the "branch-cut protection trick" for the Schur-based implementation as well. Please take a look if this is what you had in mind.

@alexanderivrii
Copy link
Contributor Author

Hmm, I have made a mistake of renaming the release notes file, while keeping the hash, now I have no idea how to make the tests pass.

@jakelishman
Copy link
Member

Yeah, reno doesn't like it when it can't track what happened to a note. Might be easiest to rebase this on top of main, and squash the commits together - I think that one way or another, the history needs rewriting because reno needs each commit in the series to work, and we can't fix a broken commit just by adding more children.

This is approximately a 3x performance improvement over the generalised
`fractional_matrix_power` method, but only works for unitary matrices.
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

This looks good thanks. The one thing I just thought of now is that I'm not 100% certain the condition for success is whether the matrix is unitary-like - I think actually the condition is that the matrix is normal (i.e. $A A^\dagger = A^\dagger A$). That obviously is true for unitaries, scaled unitaries and Hermitian matrices.

Maybe in that sense, the argument should be called assume_normal?

Comment on lines 577 to 578
assume_unitary (bool): if `True`, the operator is assumed to be unitary. In this case,
for fractional powers we employ a faster implementation based on Schur's decomposition.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
assume_unitary (bool): if `True`, the operator is assumed to be unitary. In this case,
for fractional powers we employ a faster implementation based on Schur's decomposition.
assume_unitary (bool): if ``True``, the operator is assumed to be unitary. In this case,
for fractional powers we employ a faster implementation based on Schur's decomposition.

It might be worth mentioning that the output will just be wrong if this is set True and the matrix isn't unitary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A added a note, please see 47f7f90.

) * scipy.linalg.fractional_matrix_power(
cmath.rect(1, -branch_cut_rotation) * self.data, n
)
if not assume_unitary:
Copy link
Member

Choose a reason for hiding this comment

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

Since this is an if/else, it might be slightly cleaner to have the positive case (if assume_unitary) first, so there's no "double negative" implied by the else.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 47f7f90.

@alexanderivrii
Copy link
Contributor Author

alexanderivrii commented Nov 7, 2024

Thanks @jakelishman, I think that you are fully correct that the matrix only needs to be normal for our implementation to work. I feel that the name "assume_normal" might be a bit less intuitive to users than "assume_unitary", so I have kept the name "assume_unitary" but briefly mentioned this in the docstring. I am happy to change it, if you prefer.

Copy link
Contributor

@ElePT ElePT left a comment

Choose a reason for hiding this comment

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

I agree with @alexanderivrii, I think it's safe to leave assume_unitary (as it's more restrictive than normal) and a bit more expected in this context. I don't think many users with non-unitary-normal-operators would be affected by not being fully general in the name (the output would still be correct, just a bit less efficient), and at least it's documented in the docstring. I do have one comment for @alexanderivrii and it's to add a bugfix reno on top of the feature reno mentioning that the issue with non-unitary inputs has been fixed. Other than that LGTM.

@alexanderivrii
Copy link
Contributor Author

Thanks @ElePT. Technically, the bug has been fixed by @jakelishman in #13358 (along with the branch cut improvement), and (I've double-checked) the bugfix note already appears there.

Copy link
Contributor

@ElePT ElePT left a comment

Choose a reason for hiding this comment

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

Ahaa, noted. Then let's go!

@ElePT ElePT added this pull request to the merge queue Nov 7, 2024
Merged via the queue into Qiskit:main with commit 3cbab95 Nov 7, 2024
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: Bugfix Include in the "Fixed" section of the changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Operator.power does not handle non-unitary inputs correctly
5 participants