Skip to content

ENH: linalg: return complex arrays from eigvals/eigvecs#30411

Merged
mattip merged 5 commits intonumpy:mainfrom
ev-br:eig_cmplx
Feb 18, 2026
Merged

ENH: linalg: return complex arrays from eigvals/eigvecs#30411
mattip merged 5 commits intonumpy:mainfrom
ev-br:eig_cmplx

Conversation

@ev-br
Copy link
Copy Markdown
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
Copy Markdown
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-image 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).

Details
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
Copy Markdown
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)

@ev-br ev-br marked this pull request as draft December 10, 2025 19:26
@mattip
Copy link
Copy Markdown
Member

mattip commented Jan 26, 2026

Reformatted the release note

@ev-br
Copy link
Copy Markdown
Contributor Author

ev-br commented Feb 14, 2026

Updated to maintain strict backwards compatibility of root-finding routines. The reasoning is: np.roots et al use of companion matrices is an implementation detail, so it's better to not leak it and keep strict backwards compat. This also limits the scope of the change.

The remaining CI failure in armhf looks like an unrelated fluke.And indeed, is gone on flushing the CI.

I tested locally that scipy and scikit-learn are not affected by this change (scikit-learn has some tests that rely on numpy's complex_array > 0 returning True (?); this can be easily fixed but ISTM scikit-learn ain't broken don't fix)

The scikit-image fix is scikit-image/scikit-image#8054

Apologies @mattip --- I rebased/force-pushed without checking your edits for the release note; tried recreating your edits manually.

Types: @jorenham I've to admit I'm lost in the sea of overloads. What is the difference between _ArrayLike[np.complex64] and _AsArrayC128 and _ToArrayF64.

@jorenham
Copy link
Copy Markdown
Member

Types: @jorenham I've to admit I'm lost in the sea of overloads. What is the difference between _ArrayLike[np.complex64] and _AsArrayC128 and _ToArrayF64.

unlike _ArrayLike[...], _AsArrayC128_1d also accepts list[complex] (but not list[float] because lists are invariant), and _ToArrayF64_1d also accepts Sequence[float] (so also list[int] and list[bool] because of covariance).

Copy link
Copy Markdown
Member

@jorenham jorenham left a comment

Choose a reason for hiding this comment

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

the stub changes are ok

(although the type-tests also need to be updated, see the mypy errors)

@mattip mattip merged commit be5ad98 into numpy:main Feb 18, 2026
79 checks passed
@mattip
Copy link
Copy Markdown
Member

mattip commented Feb 18, 2026

Thanks @ev-br

@mattip
Copy link
Copy Markdown
Member

mattip commented Feb 18, 2026

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).

Let's keep an eye out for breaking issues

stefanv added a commit to scikit-image/scikit-image that referenced this pull request Mar 6, 2026
NumPy plans to change it's `linalg.eig` routine to always return complex
results. Currently, it returns either complex or real values,
depending on whether the eigenvalues lie on the real axis or not.
See numpy/numpy#30411

The only effect on scikit-image AFAICS, is in the ellipse estimation
routines, which require that eigenvalues are real. In fact, the story
seems to be somewhat convoluted:
- some datasets produce non-zero imaginary parts, which break `EllipseModel.fit`,
#7013
- the current failure mode is a TypeError from an in-place modulo operation,
`phi %= np.pi`, where `phi` is constructed from eigenvalues/eigenvectors
- #7013 (comment)
asks for a reproducer, and
numpy/numpy#29000 (comment) has
some discussion of potential fixes/enhancements

This PR makes a minimal fix to make `EllipseModel.fit`: take the real
part of eigenvalues/eigenvectors explicitly, and raise an error if
either of them has non-zero imaginary parts.

---------

Co-authored-by: Stefan van der Walt <stefanv@berkeley.edu>
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.

3 participants