-
-
Notifications
You must be signed in to change notification settings - Fork 12.2k
Make eig/eigvals always return complex eigenvalues #29000
Description
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))