Check if a large matrix is diagonal matrix in python

Question:

I have compute a very large matrix M with lots of degenerate eigenvectors(different eigenvectors with same eigenvalues). I use QR decomposition to make sure these eigenvectors are orthonormal, so the Q is the orthonormal eigenvectors of M, and Q^{-1}MQ = D, where D is diagonal matrix. Now I want to check if D is truly diagonal matrix, but when I print D, the matrix is too large to show all of them, so how can I know if it is truly diagonal matrix?

Asked By: JoeJackJessieJames

||

Answers:

I afraid if this is the most efficient way of doing this, But the idea is to mask the diagonal elements and check if all the other elements are zero. I guess this is sufficient check to label a matrix as diagonal matrix.

So we create a dummy array with same size as input matrix, initialized with ones. and then replace the diagonal elements with zeros. Now we perform element wise multiplication of input matrix and dummy matrix. So here we replace the diagonal elements of input matrix with zero and leave the other elements as it is.

Now finally we check if there are any non zero elements.

def is_diagonal(matrix):
    #create a dummy matrix
    dummy_matrix = np.ones(matrix.shape, dtype=np.uint8)
    # Fill the diagonal of dummy matrix with 0.
    np.fill_diagonal(dummy_matrix, 0)

    return np.count_nonzero(np.multiply(dummy_matrix, matrix)) == 0

diagonal_matrix = np.array([[3, 0, 0],
                            [0, 7, 0],
                            [0, 0, 4]])
print is_diagonal(diagonal_matrix)
>>> True

random_matrix = np.array([[3, 8, 0],
                          [1, 7, 8],
                          [5, 0, 4]])
print is_diagonal(random_matrix)
>>> False
Answered By: ZdaR

Remove the diagonal and count the non zero elements:

np.count_nonzero(x - np.diag(np.diagonal(x)))
Answered By: donkopotamus

Not sure how fast this is compared to the others, but:

def isDiag(M):
    i, j = np.nonzero(M)
    return np.all(i == j)

EDIT Let’s time things:

M = np.random.randint(0, 10, 1000) * np.eye(1000)

def a(M):  #donkopotamus solution
    return np.count_nonzero(M - np.diag(np.diagonal(M)))

%timeit a(M) 
100 loops, best of 3: 11.5 ms per loop

%timeit is_diagonal(M)
100 loops, best of 3: 10.4 ms per loop

%timeit isDiag(M)
100 loops, best of 3: 12.5 ms per loop

Hmm, that’s slower, probably from constructing i and j

Let’s try to improve the @donkopotamus solution by removing the subtraction step:

def b(M):
    return np.all(M == np.diag(np.diagonal(M)))

%timeit b(M)
100 loops, best of 3: 4.48 ms per loop

That’s a bit better.

EDIT2 I came up with an even faster method:

def isDiag2(M):
    i, j = M.shape
    assert i == j 
    test = M.reshape(-1)[:-1].reshape(i-1, j+1)
    return ~np.any(test[:, 1:])

This isn’t doing any calculations, just reshaping. Turns out reshaping to +1 rows on a diagonal matrix puts all the data in the first column. You can then check a contiguous block for any nonzeros which is much fatser for numpy Let’s check times:

def Make42(m):
    b = np.zeros(m.shape)
    np.fill_diagonal(b, m.diagonal())
    return np.all(m == b)


%timeit b(M)
%timeit Make42(M)
%timeit isDiag2(M)

100 loops, best of 3: 4.88 ms per loop
100 loops, best of 3: 5.73 ms per loop
1000 loops, best of 3: 1.84 ms per loop

Seems my original is faster than @Make42 for smaller sets

M = np.diag(np.random.randint(0,10,10000))
%timeit b(M)
%timeit Make42(M)
%timeit isDiag2(M)


The slowest run took 35.58 times longer than the fastest. This could mean that an intermediate result is being cached.
1 loop, best of 3: 335 ms per loop

<MemoryError trace removed>

10 loops, best of 3: 76.5 ms per loop

And @Make42 gives memory error on the larger set. But then I don’t seem to have as much RAM as they do.

Answered By: Daniel F

I believe this is the most succinct way:

np.allclose(np.diag(np.diag(a)), a)
Answered By: liwt31

We can actually do quite a bit better than what Daniel F suggested:

import numpy as np
import time

a = np.diag(np.random.random(19999))

t1 = time.time()
np.all(a == np.diag(np.diagonal(a)))
print(time.time()-t1)

t1 = time.time()
b = np.zeros(a.shape)
np.fill_diagonal(b, a.diagonal())
np.all(a == b)
print(time.time()-t1)

results in

2.5737204551696777
0.6501829624176025

One tricks is that np.diagonal(a) actually uses a.diagonal(), so we use that one directly. But what takes the cake the the fast build of b, combined with the in-place operation on b.

Answered By: Make42

quick and dirty way to get the truth. works in a reasonable amount of time

for i in range(0, len(matrix[0])): 
    for j in range(0, len(matrix[0])): 
        if ((i != j) and
         (matrix[i][j] != 0)) : 
            return False

return True
Answered By: Michael Alemu
import numpy as np

is_diagonal = (np.trace(mat) == np.sum(mat))
Answered By: user3166559

Appproach #1 : Using NumPy strides/np.lib.stride_tricks.as_strided

We can leverage NumPy strides to give us the off-diag elements as a view. So, no memory overhead there and virtually free runtime! This idea has been explored before in this post.

Thus, we have –

# https://stackoverflow.com/a/43761941/ @Divakar
def nodiag_view(a):
    m = a.shape[0]
    p,q = a.strides
    return np.lib.stride_tricks.as_strided(a[:,1:], (m-1,m), (p+q,q))

Sample run to showcase its usage –

In [175]: a
Out[175]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])

In [176]: nodiag_view(a)
Out[176]: 
array([[ 1,  2,  3,  4],
       [ 6,  7,  8,  9],
       [11, 12, 13, 14]])

Let’s verify the free runtime and no memory overhead claims, by using it on a large array –

In [182]: a = np.zeros((10000,10000), dtype=int)
     ...: np.fill_diagonal(a,np.arange(len(a)))

In [183]: %timeit nodiag_view(a)
6.42 µs ± 48.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [184]: np.shares_memory(a, nodiag_view(a))
Out[184]: True

Now, how do we use it here? Simply check if all nodiag_view elements are 0s, signalling a diagonal matrix!

Hence, to solve our case here, for an input array a, it would be –

isdiag = (nodiag_view(a)==0).all()

Appproach #2 : Hacky way

For completeness, one hacky way would be to temporarily save diag elements, assign 0s there, check for all elements to be 0s. If so, signalling a diagonal matrix. Finally assign back the diag elements.

The implementation would be –

def hacky_way(a):
    diag_elem = np.diag(a).copy()
    np.fill_diagonal(a,0)
    out = (a==0).all()
    np.fill_diagonal(a,diag_elem)
    return out

Benchmarking

Let’s time on a large array and see how these compare on performance –

In [3]: a = np.zeros((10000,10000), dtype=int)
   ...: np.fill_diagonal(a,np.arange(len(a)))

In [4]: %timeit (nodiag_view(a)==0).all()
52.3 ms ± 393 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [5]: %timeit hacky_way(a)
51.8 ms ± 250 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Other approaches from @Daniel F’s post that captured other approaches –

# @donkopotamus solution improved by @Daniel F
def b(M):
    return np.all(M == np.diag(np.diagonal(M)))

# @Daniel F's soln without assert check
def isDiag2(M):
    i, j = M.shape
    test = M.reshape(-1)[:-1].reshape(i-1, j+1)
    return ~np.any(test[:, 1:])

# @Make42's soln
def Make42(m):
    b = np.zeros(m.shape)
    np.fill_diagonal(b, m.diagonal())
    return np.all(m == b)

Timings with same setup as earlier –

In [6]: %timeit b(a)
   ...: %timeit Make42(a)
   ...: %timeit isDiag2(a)
218 ms ± 1.68 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
302 ms ± 1.25 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
67.1 ms ± 1.35 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Answered By: Divakar

The solutions nodiag_view and hacky_way by @Divakar and the solution isDiag2 by @Daniel F are efficient. The other solutions are pretty slow.

The accepted solution that is by @donkopotamus, which is easy to implement, is the second slowest and 7.55 times slower than the fastest answer.

from timeit import timeit

setup = '''
import numpy as np

d = np.zeros((10000,10000), dtype=int)
np.fill_diagonal(d,np.arange(len(d)))

def nodiag_view(a):
    m = a.shape[0]
    p,q = a.strides
    return (np.lib.stride_tricks.as_strided(a[:,1:], (m-1,m), (p+q,q)) == 0).all()

def hacky_way(a):
    diag_elem = np.diag(a).copy()
    np.fill_diagonal(a,0)
    out = (a==0).all()
    np.fill_diagonal(a,diag_elem)
    return out

def isDiag(M):
    i, j = np.nonzero(M)
    return np.all(i == j)

def isDiag2(M):
    i, j = M.shape
    assert i == j
    test = M.reshape(-1)[:-1].reshape(i-1, j+1)
    return ~np.any(test[:, 1:])

def Make42(m):
    b = np.zeros(m.shape)
    np.fill_diagonal(b, m.diagonal())
    return np.all(m == b)

def by_donkopotamus(a):
    return np.count_nonzero(a - np.diag(np.diag(a))) == 0

def by_liwt31(a):
    np.allclose(np.diag(np.diag(a)), a)

'''
test0 = '''nodiag_view(d)'''
test1 = '''hacky_way(d)'''
test2 = '''isDiag(d)'''
test3 = '''isDiag2(d)'''
test4 = '''Make42(d)'''
test5 = '''by_donkopotamus(d)'''
test6 = '''by_liwt31(d)'''

print('test0:', timeit(test0, setup, number=100))
print('test1:', timeit(test1, setup, number=100))
print('test2:', timeit(test2, setup, number=100))
print('test3:', timeit(test3, setup, number=100))
print('test4:', timeit(test4, setup, number=100))
print('test5:', timeit(test5, setup, number=100))
print('test6:', timeit(test6, setup, number=100))

Results:

test0: 4.194842008990236
test1: 4.11843847198179
test2: 28.11888137299684
test3: 5.095675196003867
test4: 22.56097131301067
test5: 31.05823188900831
test6: 106.19386338599725
Answered By: Sun Bear
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.