pythonnumpymathlinear-algebraeigenvalue

numpy computed eigenvalues incorrect?


I am trying to get the eigenvalues of a positive semi-definite matrix in Python using numpy.

All eigenvalues should be non-negative and real. The minimum eigenvalue should be zero. Sometimes, numpy.eig returns complex values as eigenvalues even when they're supposed to be real (although the complex numbers have 0j in the imaginary part). To a related SO question, the accepted answer suggests using numpy.eigh instead of numpy.eig. I tried numpy.eigh but it produces incorrect results: in the second case in the MEW below, the smallest eigenvalue is not zero.

I know that numpy.eigh is for a complex Hermitian or a real symmetric matrix. The second case below is none of these. But it seems numpy.eig is known to return complex eigenvalues even in cases where it shouldn't (see linked question)! What should one do?

MWE (see output for second case):

import numpy as np
from numpy.random import RandomState

rng = RandomState(123456)

num_v = 4

weights = rng.uniform(0, 1, num_v * num_v).reshape((num_v, num_v))
weights = np.tril(weights) + np.triu(weights.T, 1)
np.fill_diagonal(weights, 0)

degrees = weights.sum(axis=0)

degrees_rw = 1 / degrees
D_rw = np.diag(degrees_rw)

degrees = 1 / np.sqrt(degrees)
D = np.diag(degrees)

I = np.eye(num_v)

laplacian = I - np.matmul(np.dot(D, weights), D)
laplacian_rw = I - np.matmul(D_rw, weights)

eigs1, _ = np.linalg.eig(laplacian)
eigs2, _ = np.linalg.eigh(laplacian)

print(np.sort(eigs1)[:10], "\n", np.sort(eigs2)[:10])

eigs1, _ = np.linalg.eig(laplacian_rw)
eigs2, _ = np.linalg.eigh(laplacian_rw)

print("\n", np.sort(eigs1)[:10], "\n", np.sort(eigs2)[:10])

Here's the output:

[0.         1.01712657 1.40644541 1.57642803] 
 [-1.08192365e-16  1.01712657e+00  1.40644541e+00  1.57642803e+00]

 [-2.22044605e-16  1.01712657e+00  1.40644541e+00  1.57642803e+00] 
 [0.08710134 1.01779168 1.38159284 1.51351414]

Could someone please help me with this?


EDIT: previously, the graph in the MWE was directed. I edited the question with an undirected graph.


Solution

  • eig is performing correctly here.

    What you see are the limitations of floating point precision.

    For all intents and purposes on a computer using 64 bit ieee753 floating point numbers, +-1e-16 is 0 when it comes to the results of numeric computations. You only get 15 to 16 decimal digits precision.

    So if you have the external knowledge, that your matrices are positive semidefinite and thus should have positive real eigen values, you can round those values away.

    Numpy has a convenience function for the complex values and its easy to do for the negative ones.

    import numpy as np
    
    # this is the new, recommended numpy random API
    rng = np.random.default_rng(123456)
    
    num_v = 4
    
    weights = rng.uniform(0, 1, num_v * num_v).reshape((num_v, num_v))
    weights = np.tril(weights) + np.triu(weights.T, 1)
    np.fill_diagonal(weights, 0)
    
    degrees = weights.sum(axis=0)
    degrees_rw = 1 / degrees
    D_rw = np.diag(degrees_rw)
    
    I = np.eye(num_v)
    
    laplacian_rw = I - np.matmul(D_rw, weights)
    
    eigvals_rw, eigvecs_rw = np.linalg.eig(laplacian_rw)
    print(eigvals_rw)
    
    
    # cast to real values if the imaginary parts are in floating precision to 0
    eigvals_rw = np.real_if_close(eigvals_rw)
    
    # round values that are at the floating point precision to exactly 0
    # might need more than 2 epsilon, but in this example, it was enough
    small = np.abs(eigvals_rw) < 2 * np.finfo(eigvals_rw.dtype).eps
    eigvals_rw[small] = 0
    
    print(eigvals_rw)
    

    Output:

    [-2.22044605e-16  9.98074713e-01  1.62494665e+00  1.37697864e+00]
    [0.         0.99807471 1.62494665 1.37697864]