Module cgc.LinAlg

Expand source code
import numpy as np

import numba

from itertools import product

"""
The expm calculations has to be performed fast; scipy does not provide required speed,
so we instead implement a version using the numba library.

Modified version of:
https://github.com/michael-hartmann/expm  
"""


# Relevant values from Table 10.2 in Functions of Matrices, Higham (2008)
# Defines the values used to determine the optimal degree of the
# calculation
theta3  = 1.5e-2
theta5  = 2.5e-1
theta7  = 9.5e-1
theta9  = 2.1e0
theta13 = 5.4e0

# And the coefficients for equation 10.33 corresponding to the degree
# values in the above dictionary. The number of coefficients is the degree+1 (eg. b3 has 3+1 elements)
b3  = [120,60,12,1]
b5  = [30240,15120,3360,420,30,1]
b7  = [17297280, 8648640,1995840, 277200, 25200,1512, 56,1]
b9  = [17643225600,8821612800,2075673600,302702400,30270240, 2162160,110880,3960,90,1]
b13 = [64764752532480000,32382376266240000,7771770303897600,1187353796428800,129060195264000,10559470521600,670442572800,33522128640,1323241920,40840800,960960,16380,182,1]


@numba.jit(nopython=True) 
def _expm_pade(A,M):
    """
    Compute the exponential of a square matrix using the Pade approximation, as described in
    _Functions of Matrices: Theory and Computation_i [1], algorithm 10.3, lines 1-6.

    Parameters
    ----------

    A : numpy.mat
        A square matrix to the compute the exponential of

    M : positive integer
        The degree of the matrix calculation (see [1])
    """
    dtype = A.dtype
    dim, dim = A.shape
   
    # The coefficients for solving equation 10.33 from [1] based on the degree
    # Unfortunately, numba doesn't play well with dictionaries, so we have to do
    # this the ugly way
    if M == 3:
        b = [120,60,12,1]
    elif M == 5:
        b = [30240,15120,3360,420,30,1]
    elif M == 7:
        b = [17297280, 8648640,1995840, 277200, 25200,1512, 56,1]
    elif M == 9:
        b = [17643225600,8821612800,2075673600,302702400,30270240, 2162160,110880,3960,90,1]
    elif M == 13:
        b = [64764752532480000,32382376266240000,7771770303897600,1187353796428800,129060195264000,10559470521600,670442572800,33522128640,1323241920,40840800,960960,16380,182,1]

    # The list of supported numpy functions can be found here:
    # https://numba.pydata.org/numba-doc/dev/reference/numpysupported.html
    # It lists np.identity as being supported, but throws an error whenever
    # you actually try to use it, so eye is entirely equivalent and _does_ work
    U = b[1]*np.eye(dim, dtype=dtype)
    V = b[0]*np.eye(dim, dtype=dtype)

    A2 = np.dot(A,A)
    A2n = np.eye(dim, dtype=dtype)

    # evaluate (10.33)
    for i in range(1,M//2+1):
        A2n = np.dot(A2n,A2)
        U += b[2*i+1]*A2n
        V += b[2*i]  *A2n

    #del A2,A2n
    U = np.dot(A,U)

    return np.linalg.solve(V-U, V+U)


@numba.jit(nopython=True) 
def _expm_ss(A,norm):
    """
    Compute the exponential of a square matrix using the scaling and squaring method, as described in
    _Functions of Matrices: Theory and Computation_i [1], algorithm 10.3, lines 7-13.

    Parameters
    ----------

    A : numpy.mat
        A square matrix to the compute the exponential of

    norm : double
        The norm of the matrix A
        
    """

    dim, dim = A.shape

    # The coefficients for m=13 used to solve equation 10.33 from [1]
    b = [64764752532480000,32382376266240000,7771770303897600,1187353796428800,129060195264000,10559470521600,670442572800,33522128640,1323241920,40840800,960960,16380,182,1]

    # Ensure that A/2^s <= theta13
    s = max(0, int(np.ceil(np.log(norm/theta13)/np.log(2))))
    if s > 0:
        A /= 2**s

    Id = np.eye(dim)
    A2 = np.dot(A,A)
    A4 = np.dot(A2,A2)
    A6 = np.dot(A2,A4)

    U = np.dot(A, np.dot(A6, b[13]*A6+b[11]*A4+b[9]*A2)+b[7]*A6+b[5]*A4+b[3]*A2+b[1]*Id)
    V = np.dot(A6, b[12]*A6+b[10]*A4+b[8]*A2) + b[6]*A6 + b[4]*A4 + b[2]*A2 + b[0]*Id

    # We'll get a warning from numba that dot is slow on non-contiguous arrays here if
    # we don't convert first. Not sure how much it actually speeds anything up, but
    # it gets rid of the warning. (NOTE: without conversion, array is of type 'F')
    r13 = np.ascontiguousarray(np.linalg.solve(V-U, V+U))

    return np.linalg.matrix_power(r13, 2**s)

@numba.jit(nopython=True) 
def expm(A):
    r"""
    Calculate the matrix exponential of a square matrix A

    This module implements algorithm 10.20 from _Functions of Matrices: Theory and Computation_ [1].
    The matrix exponential is calculated using either the scaling and squaring method or the Pade approximation,
    depending on which will be more accurate.

    Per [1], the error on the calculation is of order \(10^{-16}\).

    References
    ----------

    [1] Higham, N. J. (2008). _Functions of matrices: Theory and computation_. Society for Industrial and Applied Mathematics.

    """
    # Calculate the norm of A
    norm = np.linalg.norm(A, ord=1)

    # Decide which numerical method to use given the norm of the matrix
    if   norm < theta3:
        return _expm_pade(A,3)
    elif norm < theta5:
        return _expm_pade(A,5)
    elif norm < theta7:
        return _expm_pade(A,7)
    elif norm < theta9:
        return _expm_pade(A,9)
    else:
        return _expm_ss(A,norm)


"""
The following two methods are used to generate a set of
basis matrices used in calculating the Wilson Line

Modified version of:
https://github.com/CQuIC/pysme/blob/master/src/pysme/gellmann.py
originally made by Jonathan Gross
"""

def gen_gellmann(j, k, d):
    r"""
    Returns a generalized Gell-Mann matrix of dimension d according to Bertlmann & Krammer (2008).

    Adapted from Jonathan Gross's [pysme library](https://github.com/CQuIC/pysme/blob/master/src/pysme/gellmann.py).

    For generation of all generalized Gell-Mann matrices for a given dimension, see `cgc.LinAlg.get_basis`.


    Parameters
    ----------
    j : positive integer
        Index for generalized Gell-Mann matrix
    k : positive integer
        Index for generalized Gell-Mann matrix
    d : positive integer
        Dimension of the generalized Gell-Mann matrix

    Returns
    -------
    numpy.array
        A genereralized Gell-Mann matrix

    References
    ----------

    Bertlmann, R. A., & Krammer, P. (2008). Bloch vectors for qudits. Journal of Physics A: Mathematical and Theoretical, 41(23), 235303. [10.1088/1751-8113/41/23/235303](https://doi.org/10.1088/1751-8113/41/23/235303)
    """

    if j > k:
        gjkd = np.zeros((d, d), dtype='complex')
        gjkd[j - 1][k - 1] = 1
        gjkd[k - 1][j - 1] = 1
    elif k > j:
        gjkd = np.zeros((d, d), dtype='complex')
        gjkd[j - 1][k - 1] = -1.j
        gjkd[k - 1][j - 1] = 1.j
    elif j == k and j < d:
        gjkd = np.sqrt(2/(j*(j + 1)))*np.diag([1 + 0.j if n <= j
                                               else (-j + 0.j if n == (j + 1)
                                                     else 0 + 0.j)
                                               for n in range(1, d + 1)])
    else:
        gjkd = np.diag([1 + 0.j for n in range(1, d + 1)])*np.sqrt(2/d)

    return gjkd


def get_basis(d):
    r"""Return a Hermitian and traceless set of basis matrices for \(SU(d)\), as well
    as the identity. The former matrices satisfy:

    The basis is made up of \(d^2 - 1\) generalized Gell-Mann matrices, and then the identity
    as the last matrix.

    $$ tr( t^a t^b) = \frac{1}{2} \delta_{ab} $$

    For individual generation information, see `cgc.LinAlg.gen_gellmann`

    Parameters
    ----------

    d : positive integer
        The dimension of the Hilbert space

    Returns
    -------

    list of numpy.ndarray
        The basis matrices

    """

    return np.array([gen_gellmann(j, k, d)/2 for j, k in product(range(1, d + 1), repeat=2)])

Functions

def expm(A)

Calculate the matrix exponential of a square matrix A

This module implements algorithm 10.20 from Functions of Matrices: Theory and Computation [1]. The matrix exponential is calculated using either the scaling and squaring method or the Pade approximation, depending on which will be more accurate.

Per [1], the error on the calculation is of order 10^{-16}.

References

[1] Higham, N. J. (2008). Functions of matrices: Theory and computation. Society for Industrial and Applied Mathematics.

Expand source code
@numba.jit(nopython=True) 
def expm(A):
    r"""
    Calculate the matrix exponential of a square matrix A

    This module implements algorithm 10.20 from _Functions of Matrices: Theory and Computation_ [1].
    The matrix exponential is calculated using either the scaling and squaring method or the Pade approximation,
    depending on which will be more accurate.

    Per [1], the error on the calculation is of order \(10^{-16}\).

    References
    ----------

    [1] Higham, N. J. (2008). _Functions of matrices: Theory and computation_. Society for Industrial and Applied Mathematics.

    """
    # Calculate the norm of A
    norm = np.linalg.norm(A, ord=1)

    # Decide which numerical method to use given the norm of the matrix
    if   norm < theta3:
        return _expm_pade(A,3)
    elif norm < theta5:
        return _expm_pade(A,5)
    elif norm < theta7:
        return _expm_pade(A,7)
    elif norm < theta9:
        return _expm_pade(A,9)
    else:
        return _expm_ss(A,norm)
def gen_gellmann(j, k, d)

Returns a generalized Gell-Mann matrix of dimension d according to Bertlmann & Krammer (2008).

Adapted from Jonathan Gross's pysme library.

For generation of all generalized Gell-Mann matrices for a given dimension, see get_basis().

Parameters

j : positive integer
Index for generalized Gell-Mann matrix
k : positive integer
Index for generalized Gell-Mann matrix
d : positive integer
Dimension of the generalized Gell-Mann matrix

Returns

numpy.array
A genereralized Gell-Mann matrix

References

Bertlmann, R. A., & Krammer, P. (2008). Bloch vectors for qudits. Journal of Physics A: Mathematical and Theoretical, 41(23), 235303. 10.1088/1751-8113/41/23/235303

Expand source code
def gen_gellmann(j, k, d):
    r"""
    Returns a generalized Gell-Mann matrix of dimension d according to Bertlmann & Krammer (2008).

    Adapted from Jonathan Gross's [pysme library](https://github.com/CQuIC/pysme/blob/master/src/pysme/gellmann.py).

    For generation of all generalized Gell-Mann matrices for a given dimension, see `cgc.LinAlg.get_basis`.


    Parameters
    ----------
    j : positive integer
        Index for generalized Gell-Mann matrix
    k : positive integer
        Index for generalized Gell-Mann matrix
    d : positive integer
        Dimension of the generalized Gell-Mann matrix

    Returns
    -------
    numpy.array
        A genereralized Gell-Mann matrix

    References
    ----------

    Bertlmann, R. A., & Krammer, P. (2008). Bloch vectors for qudits. Journal of Physics A: Mathematical and Theoretical, 41(23), 235303. [10.1088/1751-8113/41/23/235303](https://doi.org/10.1088/1751-8113/41/23/235303)
    """

    if j > k:
        gjkd = np.zeros((d, d), dtype='complex')
        gjkd[j - 1][k - 1] = 1
        gjkd[k - 1][j - 1] = 1
    elif k > j:
        gjkd = np.zeros((d, d), dtype='complex')
        gjkd[j - 1][k - 1] = -1.j
        gjkd[k - 1][j - 1] = 1.j
    elif j == k and j < d:
        gjkd = np.sqrt(2/(j*(j + 1)))*np.diag([1 + 0.j if n <= j
                                               else (-j + 0.j if n == (j + 1)
                                                     else 0 + 0.j)
                                               for n in range(1, d + 1)])
    else:
        gjkd = np.diag([1 + 0.j for n in range(1, d + 1)])*np.sqrt(2/d)

    return gjkd
def get_basis(d)

Return a Hermitian and traceless set of basis matrices for SU(d), as well as the identity. The former matrices satisfy:

The basis is made up of d^2 - 1 generalized Gell-Mann matrices, and then the identity as the last matrix.

tr( t^a t^b) = \frac{1}{2} \delta_{ab}

For individual generation information, see gen_gellmann()

Parameters

d : positive integer
The dimension of the Hilbert space

Returns

list of numpy.ndarray
The basis matrices
Expand source code
def get_basis(d):
    r"""Return a Hermitian and traceless set of basis matrices for \(SU(d)\), as well
    as the identity. The former matrices satisfy:

    The basis is made up of \(d^2 - 1\) generalized Gell-Mann matrices, and then the identity
    as the last matrix.

    $$ tr( t^a t^b) = \frac{1}{2} \delta_{ab} $$

    For individual generation information, see `cgc.LinAlg.gen_gellmann`

    Parameters
    ----------

    d : positive integer
        The dimension of the Hilbert space

    Returns
    -------

    list of numpy.ndarray
        The basis matrices

    """

    return np.array([gen_gellmann(j, k, d)/2 for j, k in product(range(1, d + 1), repeat=2)])