🌐 AI搜索 & 代理 主页
Skip to content

Conversation

@ev-br
Copy link
Contributor

@ev-br ev-br commented Dec 10, 2025

A (simplest) companion to #29000 : change the dtype of eig/eigvals returns from maybe float if starts align, else complex to always complex.

As an opinionated pitch from the long discussion in gh-29000:

  • in general, eigenvalues of a non-symmetric matrix are complex
  • they may or may not have zero imaginary parts
  • NumPy goes out of its way to force-cast to reals if the imaginary parts happen to be zero

Hence this PR proposes to stop going out our way, and return what the math says: complex arrays.
This is a breaking change

What is the user impact?

Code search shows two kinds of affected usage:

  • Users incorrectly assuming that their matrix has real eigenvalues.

This is typically a sign that the user code genuinely misses a case where the eigenvalues are not on a real line.
Example: scikit-image/scikit-image#7013 (comment)
Note that this scikit-image issue is that the scikit-image code assumes real eigenvalues and fails where they are genuinely not.

The downstream fix could be along the lines of adding in the user code

if w.imag == 0:
     w = w.real

to bring the behavior to what it is today. If wanted, they can add a warning asking a user to report the reproducer, as the scikit-learn issue asks for.

  • A long tail of one-off scripts which do eigvals(covariance_matrix).

For a covariance matrix, we know that it's positive definite and the eigenvalues actually are real-values. Hence the fix is to use eigh instead.
I think the best we can do is to add a note to this effect.


An opinionated summary of alternatives discussed in gh-29000:

  • Make eig emit a warning of impending change.

The warning is extraneous and annoying for those users who are not affected or have already adapted.

  • Add a kwarg to control the return type.

I frankly don't think it's very useful to users. Large, well-maintained users can just make the change. The long tail of incorrect usage is unlikely to be able to adapt twice (add the keyword, adapt to the change, wait for a couple of years, remove the keyword)

  • Add a new (pair of) function(s) to eventually replace eig and eigvals.

Feels like a lot of churn across the ecosystem.

@ev-br
Copy link
Contributor Author

ev-br commented Dec 10, 2025

Here is a quick session of downstream testing: I ran test suites for SciPy, scikit-learn, scikit-image, networkx and pandas.

Short version: scipy has small issues which I'll take care of; scikit-learn has a small issue which I'm volunteering to fix to restore the status quo; scikit-learn and networkx are not affected; pandas is most likely not affected. Testing notes are under the fold.

I'm happy to both run tests for other downstream projects (which ones?), and send downstream patches to restore the status quo after this PR lands (assuming that it is, of course).

Build / install patched numpy
-----------------------------

$ cd repos/numpy
$ python -m build .
$ pip install dist/numpy-2.5.0.dev0-cp312-cp312-linux_x86_64.whl
$ cd ~/temp



scikit-learn
------------

$ python -c'import sklearn; print(sklearn.__file__)'
/home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/sklearn/__init__.py
$ pytest /home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/sklearn/ -v

....

================================================= 37605 passed, 3945 skipped, 150 xfailed, 65 xpassed, 5402 warnings in 532.40s (0:08:52) =================================================


scikit-image
------------

$ python -c'import skimage; print(skimage.__file__)'
/home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/__init__.py
$ pytest /home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/ -v

.....

FAILED ../.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/io/tests/test_pil.py::test_imsave_filelike - ValueError: Unexpected warning: Saving I mode images as PNG is deprecated and will be removed in Pillow 13 (2026-10-15)
FAILED ../.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/io/tests/test_pil.py::test_all_mono - ValueError: Unexpected warning: Saving I mode images as PNG is deprecated and will be removed in Pillow 13 (2026-10-15)
FAILED ../.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/measure/tests/test_fit.py::test_ellipse_model_estimate - TypeError: unsupported operand type(s) for %=: 'numpy.complex128' and 'float'
FAILED ../.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/measure/tests/test_fit.py::test_ellipse_parameter_stability - TypeError: unsupported operand type(s) for %=: 'numpy.complex128' and 'float'
FAILED ../.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/measure/tests/test_fit.py::test_ellipse_model_estimate_from_data - TypeError: unsupported operand type(s) for %=: 'numpy.complex128' and 'float'
FAILED ../.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/measure/tests/test_fit.py::test_ellipse_model_estimate_from_far_shifted_data - TypeError: unsupported operand type(s) for %=: 'numpy.complex128' and 'float'
ERROR ../.conda/envs/numpy-dev/lib/python3.12/site-packages/skimage/io/tests/test_io.py::test_imread_http_url
===================================================== 6 failed, 8404 passed, 130 skipped, 289 warnings, 1 error in 134.77s (0:02:14) =====================================================



networkx
--------

$ python -c'import networkx; print(networkx.__file__)'
/home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/networkx/__init__.py
$ pytest /home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/networkx/ -v

......

==================================================================================== warnings summary ====================================================================================
.conda/envs/numpy-dev/lib/python3.12/site-packages/networkx/algorithms/link_analysis/tests/test_hits.py::TestHITS::test_hits_numpy
  /home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/networkx/algorithms/link_analysis/hits_alg.py:229: ComplexWarning: Casting complex values to real discards the imaginary part
    hubs = dict(zip(G, map(float, h)))

.conda/envs/numpy-dev/lib/python3.12/site-packages/networkx/algorithms/link_analysis/tests/test_hits.py::TestHITS::test_hits_numpy
  /home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/networkx/algorithms/link_analysis/hits_alg.py:230: ComplexWarning: Casting complex values to real discards the imaginary part
    authorities = dict(zip(G, map(float, a)))

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================================================== 6955 passed, 78 skipped, 1 xfailed, 2 warnings in 87.36s (0:01:27) ===========================================================


pandas
------

$ python -c'import pandas; print(pandas.__file__)'
/home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/pandas/__init__.py
$ pytest /home/ev-br/.conda/envs/numpy-dev/lib/python3.12/site-packages/pandas/ -v

...

============================================================ 619 failed, 178346 passed, 26527 skipped, 1017 xfailed, 85 xpassed, 157 warnings, 783 errors in 1271.67s (0:21:11) =============================================================


But, all failures/erros seem to be IO related (files, html etc)

@jorenham
Copy link
Member

jorenham commented Dec 10, 2025

Coincidentally I was just working on the eig[vals] stubs yesterday, and it bothered me that it wasn't possible to accurately describe the return dtype in the stubs, causing them to become rather awkward:

# NOTE: for real input the output dtype (floating/complexfloating) depends on the specific values
@overload # abstract `inexact` and `floating` (excluding concrete types)
def eig(a: NDArray[np.inexact[Never]]) -> EigResult: ...
@overload # ~complex128
def eig(a: _AsArrayC128) -> EigResult[np.complex128]: ...
@overload # +float64
def eig(a: _ToArrayF64) -> EigResult[np.complex128] | EigResult[np.float64]: ...
@overload # ~complex64
def eig(a: _ArrayLike[np.complex64]) -> EigResult[np.complex64]: ...
@overload # ~float32
def eig(a: _ArrayLike[np.float32]) -> EigResult[np.complex64] | EigResult[np.float32]: ...
@overload # fallback
def eig(a: _ArrayLikeComplex_co) -> EigResult: ...

So yea I like this :)

And on that note, it would help if you could also update the stubs accordingly now :)
(You'll have to rebase on main first though)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants