Skip to content

Commit 29c603d

Browse files
JerryChen97Copilot
andcommitted
Fix Pow/Pow2 eigvals branch cut on Python 3.14
Python 3.14 implements C99 mixed-mode real/complex arithmetic (python/cpython#69639), so `complex * float` is now componentwise instead of promoting the float to `complex(x, +0.0)`: 3.13: (1+0j) * -1.0 -> (-1+0j) 3.14: (1+0j) * -1.0 -> (-1-0j) The negative zero imaginary part puts negative eigenvalues on the wrong side of the branch cut of `**`, so `Pow.eigvals()` and `Pow2.eigvals()` returned the complex conjugate of the correct eigenvalues for non-integer `z`, disagreeing with `qml.matrix`. Replace the `(1 + 0j) *` cast idiom introduced in #9802 with `math.cast(..., "complex128")`, and assert in both tests that the eigenvalues match the diagonal of the matrix so branch-cut flips are caught on any Python version. Authored with AI assistance (GitHub Copilot CLI). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 1f51ae2 commit 29c603d

5 files changed

Lines changed: 18 additions & 2 deletions

File tree

‎doc/releases/changelog-dev.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,6 +1596,12 @@
15961596
* Various decomposition rules are updated so that they accept positionally passed arguments.
15971597
[(#10088)](https://github.com/PennyLaneAI/pennylane/pull/10088)
15981598

1599+
* Fixed :class:`~pennylane.ops.op_math.Pow` and ``Pow2`` returning the complex conjugate of the
1600+
correct eigenvalues for fractional powers of operators with negative eigenvalues on Python 3.14.
1601+
Python 3.14 implements C99 mixed-mode arithmetic, so multiplying a negative real by ``1 + 0j`` now
1602+
yields a negative zero imaginary part, which selects the wrong branch of the power function.
1603+
[(#10139)](https://github.com/PennyLaneAI/pennylane/pull/10139)
1604+
15991605
<h3>Contributors ✍️</h3>
16001606

16011607
This release contains contributions from (in alphabetical order):

‎pennylane/ops/op_math/pow.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,11 @@ def diagonalizing_gates(self):
336336
return self.base.diagonalizing_gates()
337337

338338
def eigvals(self):
339+
# Cast rather than multiply by ``1 + 0j``: since Python 3.14 implements C99 mixed-mode
340+
# arithmetic, ``(1 + 0j) * -1.0`` is ``-1 - 0j``, and the negative zero imaginary part
341+
# puts negative eigenvalues on the wrong side of the branch cut of ``**``.
339342
base_eigvals = self.base.eigvals()
340-
return [((1 + 0j) * value) ** self.z for value in base_eigvals]
343+
return math.cast(base_eigvals, "complex128") ** self.z
341344

342345
# pylint: disable=arguments-renamed, invalid-overridden-method
343346
@property

‎pennylane/ops/op_math/pow2.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,11 @@ def compute_diagonalizing_gates(base, z): # pylint: disable=unused-argument
241241

242242
@override
243243
def eigvals(self):
244+
# Cast rather than multiply by ``1 + 0j``: since Python 3.14 implements C99 mixed-mode
245+
# arithmetic, ``(1 + 0j) * -1.0`` is ``-1 - 0j``, and the negative zero imaginary part
246+
# puts negative eigenvalues on the wrong side of the branch cut of ``**``.
244247
base_eigvals = self.base.eigvals()
245-
return [((1 + 0j) * value) ** self.z for value in base_eigvals]
248+
return math.cast(base_eigvals, "complex128") ** self.z
246249

247250
# pylint: disable=arguments-renamed, invalid-overridden-method
248251
@property

‎tests/ops/op_math/test_pow2.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ def test_eigvals_fractional_power_negative_eigenvalue_2(z, power_method):
356356
expected_eigvals = np.array([1.0**z, (-1.0 + 0j) ** z])
357357

358358
assert np.allclose(eigvals, expected_eigvals)
359+
# the eigenvalues must sit on the same branch of ``**`` as the matrix
360+
assert np.allclose(eigvals, np.diag(qp.matrix(op)))
359361

360362

361363
# pylint: disable-next=too-few-public-methods

‎tests/ops/op_math/test_pow_op.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,8 @@ def test_eigvals_fractional_power_negative_eigenvalue(self, z, power_method):
446446
expected_eigvals = np.array([1.0**z, (-1.0 + 0j) ** z])
447447

448448
assert np.allclose(eigvals, expected_eigvals)
449+
# the eigenvalues must sit on the same branch of ``**`` as the matrix
450+
assert np.allclose(eigvals, np.diag(qp.matrix(op)))
449451

450452

451453
class TestSimplify:

0 commit comments

Comments
 (0)