Commit 8a6d320b authored by Anton Akhmerov's avatar Anton Akhmerov
Browse files

Merge branch 'sparse-modes' into 'master'

Clean up linear algebra (related to modes)

See merge request !389
parents a8947abf 5832d028
Pipeline #74710 passed with stages
in 9 minutes and 44 seconds
# Copyright 2011-2013 Kwant authors.
# Copyright 2011-2021 Kwant authors.
#
# This file is part of Kwant. It is subject to the license terms in the file
# LICENSE.rst found in the top-level directory of this distribution and at
......@@ -10,10 +10,6 @@ __all__ = ['lapack']
from . import lapack
# Merge the public interface of the other submodules.
from .decomp_lu import *
from .decomp_schur import *
from .decomp_ev import *
__all__.extend([decomp_lu.__all__,
decomp_ev.__all__,
decomp_schur.__all__])
__all__.extend([decomp_schur.__all__])
# Copyright 2011-2013 Kwant authors.
#
# This file is part of Kwant. It is subject to the license terms in the file
# LICENSE.rst found in the top-level directory of this distribution and at
# https://kwant-project.org/license. A list of Kwant authors can be found in
# the file AUTHORS.rst at the top-level directory of this distribution and at
# https://kwant-project.org/authors.
__all__ = ['gen_eig']
from . import lapack
def gen_eig(a, b, left=False, right=True, overwrite_ab=False):
"""Compute the eigenvalues and -vectors of the matrix pencil (a,b), i.e. of
the generalized (unsymmetric) eigenproblem a v = lambda b v where a and b
are square (unsymmetric) matrices, v the eigenvector and lambda the
eigenvalues.
The eigenvalues are returned as numerator alpha and denominator beta,
i.e. lambda = alpha/beta. This is advantageous, as lambda can be infinity
which is well-defined in this case as beta = 0.
Parameters
----------
a : array, shape (M, M)
b : array, shape (M, M)
`a` and `b` are the two matrices defining the generalized eigenproblem
left : boolean
Whether to calculate and return left eigenvectors
right : boolean
Whether to calculate and return right eigenvectors
overwrite_ab : boolean
Whether to overwrite data in `a` and `b` (may improve performance)
Returns
-------
alpha : complex array, shape (M,)
beta : real or complex array, shape (M,)
The eigenvalues in the form ``alpha/beta``
(if left == True)
vl : double or complex array, shape (M, M)
The left eigenvector corresponding to the eigenvalue
``alpha[i]/beta[i]`` is the column ``vl[:,i]``.
(if right == True)
vr : double or complex array, shape (M, M)
The right eigenvector corresponding to the eigenvalue
``alpha[i]/beta[i]`` is the column ``vr[:,i]``.
"""
a, b = lapack.prepare_for_lapack(overwrite_ab, a, b)
return lapack.ggev(a, b, left, right)
# Copyright 2011-2013 Kwant authors.
#
# This file is part of Kwant. It is subject to the license terms in the file
# LICENSE.rst found in the top-level directory of this distribution and at
# https://kwant-project.org/license. A list of Kwant authors can be found in
# the file AUTHORS.rst at the top-level directory of this distribution and at
# https://kwant-project.org/authors.
__all__ = ['lu_factor', 'lu_solve', 'rcond_from_lu']
import numpy as np
from . import lapack
def lu_factor(a, overwrite_a=False):
"""Compute the LU factorization of a matrix A = P * L * U. The function
returns a tuple (lu, p, singular), where lu contains the LU factorization
storing the unit lower triangular matrix L in the strictly lower triangle
(the unit diagonal is not stored) and the upper triangular matrix U in the
upper triangle. p is a vector of pivot indices, and singular a Boolean
value indicating whether the matrix A is singular up to machine precision.
NOTE: This function mimics the behavior of scipy.linalg.lu_factor (except
that it has in addition the flag singular). The main reason is that
lu_factor in SciPy has a bug that depending on the type of NumPy matrix
passed to it, it would not return what was descirbed in the
documentation. This bug will be (probably) fixed in 0.10.0 but until this
is standard, this version is better to use.
Parameters
----------
a : array, shape (M, M)
Matrix to factorize
overwrite_a : boolean
Whether to overwrite data in a (may increase performance)
Returns
-------
lu : array, shape (N, N)
Matrix containing U in its upper triangle, and L in its lower triangle.
The unit diagonal elements of L are not stored.
piv : array, shape (N,)
Pivot indices representing the permutation matrix P:
row i of matrix was interchanged with row piv[i].
singular : boolean
Whether the matrix a is singular (up to machine precision)
"""
a = lapack.prepare_for_lapack(overwrite_a, a)
return lapack.getrf(a)
def lu_solve(matrix_factorization, b):
"""Solve a linear system of equations, a x = b, given the LU
factorization of a
Parameters
----------
matrix_factorization
Factorization of the coefficient matrix a, as given by lu_factor
b : array (vector or matrix)
Right-hand side
Returns
-------
x : array (vector or matrix)
Solution to the system
"""
(lu, ipiv, singular) = matrix_factorization
if singular:
raise RuntimeWarning("In lu_solve: the flag singular indicates "
"a singular matrix. Result of solve step "
"are probably unreliable")
lu, b = lapack.prepare_for_lapack(False, lu, b)
ipiv = np.ascontiguousarray(np.asanyarray(ipiv), dtype=lapack.int_dtype)
return lapack.getrs(lu, ipiv, b)
def rcond_from_lu(matrix_factorization, norm_a, norm="1"):
"""Compute the reciprocal condition number from the LU decomposition as
returned from lu_factor(), given additionally the norm of the matrix a in
norm_a.
The reciprocal condition number is given as 1/(||A||*||A^-1||), where
||...|| is a matrix norm.
Parameters
----------
matrix_factorization
Factorization of the matrix a, as given by lu_factor
norm_a : float or complex
norm of the original matrix a (type of norm is specified in norm)
norm : {'1', 'I'}, optional
type of matrix norm which should be used to compute the condition
number ("1": 1-norm, "I": infinity norm). Default: '1'.
Returns
-------
rcond : float or complex
reciprocal condition number of a with respect to the type of matrix
norm specified in norm
"""
(lu, ipiv, singular) = matrix_factorization
norm = norm.encode('utf8') # lapack expects bytes
lu = lapack.prepare_for_lapack(False, lu)
return lapack.gecon(lu, norm_a, norm)
......@@ -302,8 +302,7 @@ def gen_schur(a, b, calc_q=True, calc_z=True, calc_ev=True,
problem (the entries of the diagonal of the complex Schur form are the
eigenvalues of the matrix, for example), and the routine can optionally
also return the generalized eigenvalues in the form (alpha, beta), such
that alpha/beta is a generalized eigenvalue of the pencil (a, b) (see also
gen_eig()).
that alpha/beta is a generalized eigenvalue of the pencil (a, b).
Parameters
----------
......
......@@ -8,9 +8,7 @@
"""Low-level access to LAPACK functions. """
__all__ = ['getrf',
'getrs',
'gecon',
__all__ = ['gecon',
'ggev',
'gees',
'trsen',
......@@ -92,85 +90,6 @@ cdef l_int lwork_from_qwork(scalar qwork):
return <l_int>qwork.real
def getrf(np.ndarray[scalar, ndim=2] A):
cdef l_int M, N, info
cdef np.ndarray[l_int] ipiv
assert_fortran_mat(A)
M = A.shape[0]
N = A.shape[1]
ipiv = np.empty(min(M,N), dtype = int_dtype)
if scalar is float:
lapack.sgetrf(&M, &N, <float *>A.data, &M,
<l_int *>ipiv.data, &info)
elif scalar is double:
lapack.dgetrf(&M, &N, <double *>A.data, &M,
<l_int *>ipiv.data, &info)
elif scalar is float_complex:
lapack.cgetrf(&M, &N, <float complex *>A.data, &M,
<l_int *>ipiv.data, &info)
elif scalar is double_complex:
lapack.zgetrf(&M, &N, <double complex *>A.data, &M,
<l_int *>ipiv.data, &info)
assert info >= 0, "Argument error in getrf"
return (A, ipiv, info > 0 or M != N)
def getrs(np.ndarray[scalar, ndim=2] LU, np.ndarray[l_int] IPIV,
np.ndarray B):
cdef l_int N, NRHS, info
assert_fortran_mat(LU)
# Consistency checks for LU and B
if B.descr.type_num != LU.descr.type_num:
raise TypeError('B must have same dtype as LU')
# Workaround for 1x1-Fortran bug in NumPy < v2.0
if ((B.ndim == 2 and (B.shape[0] > 1 or B.shape[1] > 1) and
not B.flags["F_CONTIGUOUS"])):
raise ValueError("B must be Fortran ordered")
if B.ndim > 2:
raise ValueError("B must be a vector or matrix")
if LU.shape[0] != B.shape[0]:
raise ValueError('LU and B have incompatible shapes')
N = LU.shape[0]
if B.ndim == 1:
NRHS = 1
elif B.ndim == 2:
NRHS = B.shape[1]
if scalar is float:
lapack.sgetrs("N", &N, &NRHS, <float *>LU.data, &N,
<l_int *>IPIV.data, <float *>B.data, &N,
&info)
elif scalar is double:
lapack.dgetrs("N", &N, &NRHS, <double *>LU.data, &N,
<l_int *>IPIV.data, <double *>B.data, &N,
&info)
elif scalar is float_complex:
lapack.cgetrs("N", &N, &NRHS, <float complex *>LU.data, &N,
<l_int *>IPIV.data, <float complex *>B.data, &N,
&info)
elif scalar is double_complex:
lapack.zgetrs("N", &N, &NRHS, <double complex *>LU.data, &N,
<l_int *>IPIV.data, <double complex *>B.data, &N,
&info)
assert info == 0, "Argument error in getrs"
return B
def gecon(np.ndarray[scalar, ndim=2] LU, double normA, char *norm = b"1"):
cdef l_int N, info
cdef float srcond, snormA
......@@ -264,155 +183,6 @@ def ggev_postprocess(dtype, alphar, alphai, vl_r=None, vr_r=None):
return (alpha, vl, vr)
def ggev(np.ndarray[scalar, ndim=2] A, np.ndarray[scalar, ndim=2] B,
left=False, right=True):
cdef l_int N, info, lwork
# Parameter checks
assert_fortran_mat(A, B)
if A.ndim != 2 or A.ndim != 2:
raise ValueError("gen_eig requires both a and be to be matrices")
if A.shape[0] != A.shape[1]:
raise ValueError("gen_eig requires square matrix input")
if A.shape[0] != B.shape[0] or A.shape[1] != B.shape[1]:
raise ValueError("A and B do not have the same shape")
# Allocate workspaces
N = A.shape[0]
cdef np.ndarray[scalar] alphar, alphai
if scalar in cmplx:
alphar = np.empty(N, dtype=A.dtype)
alphai = None
else:
alphar = np.empty(N, dtype=A.dtype)
alphai = np.empty(N, dtype=A.dtype)
cdef np.ndarray[scalar] beta = np.empty(N, dtype=A.dtype)
cdef np.ndarray rwork = None
if scalar is float_complex:
rwork = np.empty(8 * N, dtype=np.float32)
elif scalar is double_complex:
rwork = np.empty(8 * N, dtype=np.float64)
cdef np.ndarray vl
cdef scalar *vl_ptr
cdef char *jobvl
if left:
vl = np.empty((N,N), dtype=A.dtype, order='F')
vl_ptr = <scalar *>vl.data
jobvl = "V"
else:
vl = None
vl_ptr = NULL
jobvl = "N"
cdef np.ndarray vr
cdef scalar *vr_ptr
cdef char *jobvr
if right:
vr = np.empty((N,N), dtype=A.dtype, order='F')
vr_ptr = <scalar *>vr.data
jobvr = "V"
else:
vr = None
vr_ptr = NULL
jobvr = "N"
# Workspace query
# Xggev expects &qwork as a <scalar *> (even though it's an integer)
lwork = -1
cdef scalar qwork
if scalar is float:
lapack.sggev(jobvl, jobvr, &N, <float *>A.data, &N,
<float *>B.data, &N,
<float *>alphar.data, <float *> alphai.data,
<float *>beta.data,
vl_ptr, &N, vr_ptr, &N,
&qwork, &lwork, &info)
elif scalar is double:
lapack.dggev(jobvl, jobvr, &N, <double *>A.data, &N,
<double *>B.data, &N,
<double *>alphar.data, <double *> alphai.data,
<double *>beta.data,
vl_ptr, &N, vr_ptr, &N,
&qwork, &lwork, &info)
elif scalar is float_complex:
lapack.cggev(jobvl, jobvr, &N, <float complex *>A.data, &N,
<float complex *>B.data, &N,
<float complex *>alphar.data, <float complex *>beta.data,
vl_ptr, &N, vr_ptr, &N,
&qwork, &lwork,
<float *>rwork.data, &info)
elif scalar is double_complex:
lapack.zggev(jobvl, jobvr, &N, <double complex *>A.data, &N,
<double complex *>B.data, &N,
<double complex *>alphar.data, <double complex *>beta.data,
vl_ptr, &N, vr_ptr, &N,
&qwork, &lwork,
<double *>rwork.data, &info)
assert info == 0, "Argument error in ggev"
lwork = lwork_from_qwork(qwork)
cdef np.ndarray[scalar] work = np.empty(lwork, dtype=A.dtype)
# The actual calculation
if scalar is float:
lapack.sggev(jobvl, jobvr, &N, <float *>A.data, &N,
<float *>B.data, &N,
<float *>alphar.data, <float *> alphai.data,
<float *>beta.data,
vl_ptr, &N, vr_ptr, &N,
<float *>work.data, &lwork, &info)
elif scalar is double:
lapack.dggev(jobvl, jobvr, &N, <double *>A.data, &N,
<double *>B.data, &N,
<double *>alphar.data, <double *> alphai.data,
<double *>beta.data,
vl_ptr, &N, vr_ptr, &N,
<double *>work.data, &lwork, &info)
elif scalar is float_complex:
lapack.cggev(jobvl, jobvr, &N, <float complex *>A.data, &N,
<float complex *>B.data, &N,
<float complex *>alphar.data, <float complex *>beta.data,
vl_ptr, &N, vr_ptr, &N,
<float complex *>work.data, &lwork,
<float *>rwork.data, &info)
elif scalar is double_complex:
lapack.zggev(jobvl, jobvr, &N, <double complex *>A.data, &N,
<double complex *>B.data, &N,
<double complex *>alphar.data, <double complex *>beta.data,
vl_ptr, &N, vr_ptr, &N,
<double complex *>work.data, &lwork,
<double *>rwork.data, &info)
if info > 0:
raise LinAlgError("QZ iteration failed to converge in sggev")
assert info == 0, "Argument error in ggev"
if scalar is float:
post_dtype = np.complex64
elif scalar is double:
post_dtype = np.complex128
cdef np.ndarray alpha
alpha = alphar
if scalar in floating:
alpha, vl, vr = ggev_postprocess(post_dtype, alphar, alphai, vl, vr)
return filter_args((True, True, left, right), (alpha, beta, vl, vr))
def gees(np.ndarray[scalar, ndim=2] A, calc_q=True, calc_ev=True):
cdef l_int N, lwork, sdim, info
......
......@@ -6,353 +6,194 @@
# the file AUTHORS.rst at the top-level directory of this distribution and at
# https://kwant-project.org/authors.
from kwant.linalg import (
lu_factor, lu_solve, rcond_from_lu, gen_eig, schur,
convert_r2c_schur, order_schur, evecs_from_schur, gen_schur,
convert_r2c_gen_schur, order_gen_schur, evecs_from_gen_schur)
import pytest
import numpy as np
from kwant.linalg import (
schur, convert_r2c_schur, order_schur, evecs_from_schur, gen_schur,
convert_r2c_gen_schur, order_gen_schur, evecs_from_gen_schur
)
from ._test_utils import _Random, assert_array_almost_equal
def test_gen_eig():
def _test_gen_eig(dtype):
rand = _Random()
a = rand.randmat(4, 4, dtype)
b = rand.randmat(4, 4, dtype)
(alpha, beta, vl, vr) = gen_eig(a, b, True, True)
assert_array_almost_equal(dtype, np.dot(np.dot(a, vr), beta),
np.dot(np.dot(b, vr), alpha))
assert_array_almost_equal(dtype,
np.dot(beta, np.dot(np.conj(vl.T), a)),
np.dot(alpha, np.dot(np.conj(vl.T), b)))
_test_gen_eig(np.float32)
_test_gen_eig(np.float64)
_test_gen_eig(np.complex64)
_test_gen_eig(np.complex128)
#int should be propagated to float64
_test_gen_eig(np.int32)
def test_lu():
def _test_lu(dtype):
rand = _Random()
a = rand.randmat(4, 4, dtype)
bmat = rand.randmat(4, 4, dtype)
bvec = rand.randvec(4, dtype)
lu = lu_factor(a)
xmat = lu_solve(lu, bmat)
xvec = lu_solve(lu, bvec)
assert_array_almost_equal(dtype, np.dot(a, xmat), bmat)
assert_array_almost_equal(dtype, np.dot(a, xvec), bvec)
_test_lu(np.float32)
_test_lu(np.float64)
_test_lu(np.complex64)
_test_lu(np.complex128)
#int should be propagated to float64
_test_lu(np.int32)
def test_rcond_from_lu():
def _test_rcond_from_lu(dtype):
rand = _Random()
a = rand.randmat(10, 10, dtype)
norm1_a = np.linalg.norm(a, 1)
normI_a = np.linalg.norm(a, np.inf)
lu = lu_factor(a)
rcond1 = rcond_from_lu(lu, norm1_a, '1')
rcondI = rcond_from_lu(lu, normI_a, 'I')
err1 = abs(rcond1 -
1/(norm1_a * np.linalg.norm(np.linalg.inv(a), 1)))
errI = abs(rcondI -
1/(normI_a * np.linalg.norm(np.linalg.inv(a), np.inf)))
#rcond_from_lu returns an estimate for the reciprocal
#condition number only; hence we shouldn't be too strict about
#the assertions here
#Note: in my experience the estimate is excellent for somewhat
#larger matrices
assert err1/rcond1 < 0.1
assert errI/rcondI < 0.1
_test_rcond_from_lu(np.float32)
_test_rcond_from_lu(np.float64)
_test_rcond_from_lu(np.complex64)
_test_rcond_from_lu(np.complex128)
#int should be propagated to float64
_test_rcond_from_lu(np.int32)
def test_schur():
def _test_schur(dtype):
rand = _Random()
a = rand.randmat(5, 5, dtype)
t, q = schur(a)[:2]
assert_array_almost_equal(dtype, np.dot(np.dot(q, t), np.conj(q.T)), a)
_test_schur(np.float32)
_test_schur(np.float64)
_test_schur(np.complex64)
_test_schur(np.complex128)
#int should be propagated to float64
_test_schur(np.int32)
def test_convert_r2c_schur():
def _test_convert_r2c_schur(dtype):
rand = _Random()
a = rand.randmat(10, 10, dtype)
t, q = schur(a)[:2]
t2, q2 = convert_r2c_schur(t, q)
assert_array_almost_equal(dtype, np.dot(np.dot(q, t), np.conj(q.T)), a)
assert_array_almost_equal(dtype, np.dot(np.dot(q2, t2), np.conj(q2.T)),
a)
_test_convert_r2c_schur(np.float32)
_test_convert_r2c_schur(np.float64)
#in the complex case the function should actually just copy
_test_convert_r2c_schur(np.complex64)
_test_convert_r2c_schur(np.complex128)
#int should be propagated to float64
_test_convert_r2c_schur(np.int32)
def test_order_schur():
def _test_order_schur(dtype):
rand = _Random()
a = rand.randmat(10, 10, dtype)
t, q, ev = schur(a)
t2, q2, ev2 = order_schur(lambda i: i>2 and i<7, t, q)
assert_array_almost_equal(dtype, np.dot(np.dot(q, t), np.conj(q.T)), a)
assert_array_almost_equal(dtype, np.dot(np.dot(q2, t2), np.conj(q2.T)),
a)
assert_array_almost_equal(dtype, np.sort(ev), np.sort(ev2))
assert_array_almost_equal(dtype, np.sort(ev[3:7]), np.sort(ev2[:4]))
sel = [False, False, 0, True, True, True, 1, False, False, False]
t3, q3 = order_schur(sel, t, q)[:2]
assert_array_almost_equal(dtype, np.dot(np.dot(q3, t3), np.conj(q3.T)),
a)
assert_array_almost_equal(dtype, t2, t3)
assert_array_almost_equal(dtype, q2, q3)
_test_order_schur(np.float32)
_test_order_schur(np.float64)
_test_order_schur(np.complex64)
_test_order_schur(np.complex128)
#int should be propagated to float64