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

YGate.power(1/2) behaves differently with scipy 1.14 on macOS #13305

Closed
t-imamichi opened this issue Oct 10, 2024 · 13 comments · Fixed by #13358
Closed

YGate.power(1/2) behaves differently with scipy 1.14 on macOS #13305

t-imamichi opened this issue Oct 10, 2024 · 13 comments · Fixed by #13358
Labels
bug Something isn't working

Comments

@t-imamichi
Copy link
Member

t-imamichi commented Oct 10, 2024

Environment

  • Qiskit version: 1.2.4
  • Python version: 3.12.7
  • Operating system: macOS 15.0.1

What is happening?

YGate.power(1/2) behaves differently with scipy 1.14 on macOS compared with outcomes with other version and OS.

How can we reproduce the issue?

import scipy
from qiskit.circuit.library import YGate

print('scipy', scipy.__version__)
print('Y^0.5', YGate().power(1 / 2))
print('Y^0.5 †', YGate().power(1 / 2).adjoint())

macOS + qiskit 1.2.4 + scipy 1.14.1

scipy 1.14.1
Y^0.5 Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[ 0.5-0.5j,  0.5-0.5j],
       [-0.5+0.5j,  0.5-0.5j]])])
Y^0.5 † Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[ 0.5+0.5j, -0.5-0.5j],
       [ 0.5+0.5j,  0.5+0.5j]])])

macOS + qiskit 1.2.4 + scipy 1.14.0

scipy 1.14.0
Y^0.5 Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[ 0.5-0.5j,  0.5-0.5j],
       [-0.5+0.5j,  0.5-0.5j]])])
Y^0.5 † Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[ 0.5+0.5j, -0.5-0.5j],
       [ 0.5+0.5j,  0.5+0.5j]])])

macOS + qiskit 1.2.4 + scipy 1.13.1

scipy 1.13.1
Y^0.5 Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[ 0.5+0.5j, -0.5-0.5j],
       [ 0.5+0.5j,  0.5+0.5j]])])
Y^0.5 † Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[ 0.5-0.5j,  0.5-0.5j],
       [-0.5+0.5j,  0.5-0.5j]])])

RedHat 9.4 + qiskit 1.2.4 + scipy 1.14.1

scipy 1.14.1
Y^0.5 Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[ 0.5+0.5j, -0.5-0.5j],
       [ 0.5+0.5j,  0.5+0.5j]])])
Y^0.5 † Instruction(name='unitary', num_qubits=1, num_clbits=0, params=[array([[ 0.5-0.5j,  0.5-0.5j],
       [-0.5+0.5j,  0.5-0.5j]])])

What should happen?

Consistent outcomes regardless of scipy version

Any suggestions?

pin scipy version, e.g., scipy<1.14 for macOS?

@t-imamichi t-imamichi added the bug Something isn't working label Oct 10, 2024
@t-imamichi t-imamichi changed the title YGate.power(1/2) behaves differently with qiskit 1.2.4 on macOS YGate.power(1/2) behaves differently with scipy 1.14 on macOS Oct 10, 2024
@jakelishman
Copy link
Member

Hmm, this is kind of weird - I don't think anything should have changed between the 1.2 series and main to change this. At the end of the day, this code should just defer to Scipy to do the decomposition.

Are you using the same versions of Scipy between the macOS 1.2.4 and main tests? Are they both running on the same processor? I can't reproduce on Intel macOS with scipy 1.13.1

@jakelishman
Copy link
Member

Ah, I see you just updated to say it's a scipy 1.14 problem - thanks.

@t-imamichi
Copy link
Member Author

I updated the description just now. I confirmed that it is due to scipy 1.14.

@jakelishman
Copy link
Member

Ok, so Scipy 1.14 started using Accelerate as its default BLAS on macOS. This is quite possibly the difference - it used OpenBLAS before. I need to look a little bit into where something's going wrong, because we want to return the principal square root. I'm hoping we weren't just doing that by chance previously.

@t-imamichi
Copy link
Member Author

Thank you for investigating this issue so quickly. Using Accelerate is a big change.

@jakelishman
Copy link
Member

jakelishman commented Oct 10, 2024

Ok, so digging a little into the Operator code, I feel like we might actually have some problems going on here, and Accelerate's behaviour is not ideal for us, but not wrong.

Let's handle the immediate problem first: at the end of the day, the trouble is that Accelerate is finding that the eigenvalues of $Y$ to be $(1, -(1 + \text{eps}\times i))$ where eps is ~$10^{-32}$. The OpenBLAS implementation found them to be 1 and -1 exactly. This unfortunately means that when we then take powers of the diagonal matrix, Accelerate's negative eigenvalue happens to fall on the other side of the principal-root branch cut.

Now, an additional problem: the way Operator.power works, it's strongly assuming that the internal matrix is unitary, because it's assuming that it can calculate fractional matrix powers by taking the Schur decomposition taking the matrix power of the Schur form then applying the unitary transformation (ok so far), except it assumes that the matrix power of the Schur form is the power of its diagonal. The Schur form is upper-triangular, but not necessarily diagonal (roughly, I think it's diagonal in the case that the input is a scaled unitary). So our Operator.power code fails for non-unitary operators:

>>> from qiskit.quantum_info import Operator
>>> import scipy.linalg
>>> # Not unitary.
>>> data = [[1, 1], [0, -1]]
>>> print(Operator(data).power(0.5).data)
[[1.000000e+00+0.j 0.000000e+00+0.j]
 [0.000000e+00+0.j 6.123234e-17+1.j]]
>>> print(scipy.linalg.fractional_matrix_power(data, 0.5)))
[[1.00000000e+00+0.j  5.00000000e-01-0.5j]
 [0.00000000e+00+0.j  4.93096709e-17+1.j ]]

The ability to do fractional matrix powers of Operator is relatively recent (#11534), and it seems to have pulled the code from Gate.power, which was empowered to make that assumption. We'll need to fix Operator.power for non-unitary operators, but that's a separate issue to this one.

Unfortunately for us, scipy.linalg.fractional_matrix_power has the same trouble with the branch cut between OpenBLAS and Accelerate (unsurprisingly - it's using a Schur-based method internally, and I wouldn't be surprised if the difference comes in in the QR step, which is the base of basically everything). I don't immediately have a way to mitigate this branch cut effect that's actually going to be reliable - it's an annoying common problem in computational linear algebra, but I'm not sure I've got enough experience to see the best way out of the situation.

@jakelishman
Copy link
Member

Fwiw, while we're seeing the problem with Accelerate here with the default Scipy 1.14 wheels, in principle it can happen with any Scipy version if the user switches out the BLAS implementation it's built against. So pinning Scipy would just unfortunately squashing a single symptom.

If we can't come up with a fix to this that at least makes it less surprisingly, I guess we'll at least need to heavily document in Operator.power and Gate.power that the results of fractional powers may well fall afoul of branch-cuts in the complex plane depending on the BLAS version, available SIMD instructions, etc.

@t-imamichi
Copy link
Member Author

Thank you for your detailed investigation. I will report the issue of scipy.linalg.fractional_matrix_power to scipy repo. I hope I get some information of mitigation.

@t-imamichi
Copy link
Member Author

I reported it to scipy scipy/scipy#21687

@jakelishman
Copy link
Member

Oh, I don't think it's a problem with the function per se - it's just one of these problems we have to deal with with imprecise numerical linear algebra.

@t-imamichi
Copy link
Member Author

I reported only fractional_matrix_power case because I felt it could be inconvenient for other users too. Let's wait for their response.

@jakelishman
Copy link
Member

Imamichi-san: in case you didn't see it, #13358 includes the "fix" for this, which is that we've done some (slightly questionable) numerical trickery to shift the branch-cut point of the fraction-root function slightly away from the really common -1 point. It'll make roots of common operators reliable, at the cost that an operator that truly has an eigenvalue a very small rotation anticlockwise around from -1 might have its root put on the other side of the branch cut. That's controllable with Operator.power(branch_cut_rotation=...) now.

@t-imamichi
Copy link
Member Author

Thank you for the fix and information, Jake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants