Skip to content

Make eig/eigvals always return complex eigenvalues #29000

@ev-br

Description

@ev-br

For a real-valued input, NumPy's eigenvalue routines currently may return either real or complex eigenvalues: both eig and eigvals check for imaginary parts being exactly zero, and downcast the output if they are [1].

Other array libraries skip this handholding and always return complex eigenvalues. For instance,

In [10]: torch.linalg.eig(torch.arange(4).reshape(2, 2)*1.0).eigenvalues
Out[10]: tensor([-0.5616+0.j, 3.5616+0.j])

A recent array-api discussion [2], had a question if NumPy would consider changing this value-dependent behavior and always return a complex array for eigenvalues?

What is the downstream effect. Were we starting from scratch, I'd expect that doing real_if_close is not that taxing for a user---and in fact in the vast majority of use cases it's just not needed.

Since we're not starting from scratch, and to roughly assess the blast radius, I applied the following patch (hidden under the fold), and ran tests for scipy, scikit-learn and scikit-image.

Here's the summary:

scipy:

SciPy: 4 failures in scipy.signal, all look trivial to fix

FAILED signal/tests/test_dltisys.py::TestStateSpaceDisc::test_properties - AssertionError: dtypes do not match.
FAILED signal/tests/test_dltisys.py::TestTransferFunction::test_properties - AssertionError: dtypes do not match.
FAILED signal/tests/test_ltisys.py::TestStateSpace::test_properties - AssertionError: dtypes do not match.
FAILED signal/tests/test_ltisys.py::TestTransferFunction::test_properties - AssertionError: dtypes do not match.

scikit-learn:

all tests pass

scikit-image:

FAILED measure/tests/test_fit.py::test_ellipse_model_estimate - TypeError: unsupported operand type(s) for %=: 'numpy.complex128' and 'float'
FAILED measure/tests/test_fit.py::test_ellipse_parameter_stability - TypeError: unsupported operand type(s) for %=: 'numpy.complex128' and 'float'
FAILED measure/tests/test_fit.py::test_ellipse_model_estimate_from_data - TypeError: unsupported operand type(s) for %=: 'numpy.complex128' and 'float'
FAILED measure/tests/test_fit.py::test_ellipse_model_estimate_from_far_shifted_data - TypeError: unsupported operand type(s) for %=: 'numpy.complex128' and 'float'

[1] https://github.com/numpy/numpy/blob/v2.1.0/numpy/linalg/_linalg.py#L1226
[2] data-apis/array-api#935 (comment)

Details
$ git diff
diff --git a/numpy/linalg/_linalg.py b/numpy/linalg/_linalg.py
index d7850c4a02..7b44f73588 100644
--- a/numpy/linalg/_linalg.py
+++ b/numpy/linalg/_linalg.py
@@ -1269,14 +1269,15 @@ def eigvals(a):
                   under='ignore'):
         w = _umath_linalg.eigvals(a, signature=signature)
 
-    if not isComplexType(t):
-        if all(w.imag == 0):
-            w = w.real
-            result_t = _realType(result_t)
-        else:
-            result_t = _complexType(result_t)
 
+    result_t = _complexType(result_t)
    return w.astype(result_t, copy=False)
 
 
 def _eigvalsh_dispatcher(a, UPLO=None):
@@ -1522,13 +1523,14 @@ def eig(a):
                   under='ignore'):
         w, vt = _umath_linalg.eig(a, signature=signature)
 
-    if not isComplexType(t) and all(w.imag == 0.0):
-        w = w.real
-        vt = vt.real
-        result_t = _realType(result_t)
-    else:
-        result_t = _complexType(result_t)
 
+    result_t = _complexType(result_t)
     vt = vt.astype(result_t, copy=False)
     return EigResult(w.astype(result_t, copy=False), wrap(vt))

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions