Search code examples
pythonnumpyeigenvaluediagonal

Simultaneously diagonalize matrices with numpy


I have a m × n × n numpy.ndarray of m simultaneously diagonalizable square matrices and would like to use numpy to obtain their simultaneous eigenvalues.

For example, if I had

from numpy import einsum, diag, array, linalg, random
U = linalg.svd(random.random((3,3)))[2]

M = einsum(
    "ij, ajk, lk",
    U, [diag([2,2,0]), diag([1,-1,1])], U)

the two matrices in M are simultaneously diagonalizable, and I am looking for a way to obtain the array

array([[2.,  1.],
       [2., -1.],
       [0.,  1.]])

(up to permutation of the lines) from M. Is there a built-in or easy way to get this?


Solution

  • I am sure there is significant room for improvement in my solution, but I have come up with the following set of three functions doing the calculation for me in a semi-robust way.

    def clusters(array,
                 orig_indices = None,
                 start = 0,
                 rtol=numpy.allclose.__defaults__[0],
                 atol=numpy.allclose.__defaults__[1]):
        """For an array, return a permutation that sorts the numbers and the sizes of the resulting blocks of identical numbers."""
        array = numpy.asarray(array)
        if not len(array):
            return numpy.array([]),[]
        if orig_indices is None:
            orig_indices = numpy.arange(len(array))
        x = array[0]
        close = abs(array-x) <= (atol + rtol*abs(x))
        first = sum(close)
        r_perm, r_sizes = clusters(
            array[~close],
            orig_indices[~close],
            start+first,
            rtol, atol)
        r_sizes.insert(0, first)
        return numpy.concatenate((orig_indices[close], r_perm)), r_sizes
    
    def permutation_matrix(permutation, dtype=dtype):
        n = len(permutation)
        P = numpy.zeros((n,n), dtype)
        for i,j in enumerate(permutation):
            P[j,i]=1
        return P
    
    def simultaneously_diagonalize(tensor, atol=numpy.allclose.__defaults__[1]):
        tensor = numpy.asarray(tensor)
        old_shape = tensor.shape
        size = old_shape[-1]
        tensor = tensor.reshape((-1, size, size))
        diag_mask = 1-numpy.eye(size)
    
        eigvalues, diagonalizer = numpy.linalg.eig(tensor[0])
        diagonalization = numpy.dot(
            numpy.dot(
                matrix.linalg.inv(diagonalizer),
                tensor).swapaxes(0,-2),
            diagonalizer)
        if numpy.allclose(diag_mask*diagonalization, 0):
            return diagonalization.diagonal(axis1=-2, axis2=-1).reshape(old_shape[:-1])
        else:
            perm, cluster_sizes = clusters(diagonalization[0].diagonal())
            perm_matrix = permutation_matrix(perm)
            diagonalization = numpy.dot(
                numpy.dot(
                    perm_matrix.T,
                    diagonalization).swapaxes(0,-2),
                perm_matrix)
            mask = 1-scipy.linalg.block_diag(
                *list(
                    numpy.ones((blocksize, blocksize))
                    for blocksize in cluster_sizes))
            print(diagonalization)
            assert(numpy.allclose(
                    diagonalization*mask,
                    0)) # Assert that the matrices are co-diagonalizable
            blocks = numpy.cumsum(cluster_sizes)
            start = 0
            other_part = []
            for block in blocks:
                other_part.append(
                    simultaneously_diagonalize(
                        diagonalization[1:, start:block, start:block]))
                start = block
            return numpy.vstack(
                (diagonalization[0].diagonal(axis1=-2, axis2=-1),
                 numpy.hstack(other_part)))