354 lines
12 KiB
Python
354 lines
12 KiB
Python
from warnings import warn
|
|
|
|
import numpy as np
|
|
from numpy import (atleast_2d, arange, zeros_like, imag, diag,
|
|
iscomplexobj, tril, triu, argsort, empty_like)
|
|
from scipy._lib._util import ComplexWarning
|
|
from ._decomp import _asarray_validated
|
|
from .lapack import get_lapack_funcs, _compute_lwork
|
|
|
|
__all__ = ['ldl']
|
|
|
|
|
|
def ldl(A, lower=True, hermitian=True, overwrite_a=False, check_finite=True):
|
|
""" Computes the LDLt or Bunch-Kaufman factorization of a symmetric/
|
|
hermitian matrix.
|
|
|
|
This function returns a block diagonal matrix D consisting blocks of size
|
|
at most 2x2 and also a possibly permuted unit lower triangular matrix
|
|
``L`` such that the factorization ``A = L D L^H`` or ``A = L D L^T``
|
|
holds. If `lower` is False then (again possibly permuted) upper
|
|
triangular matrices are returned as outer factors.
|
|
|
|
The permutation array can be used to triangularize the outer factors
|
|
simply by a row shuffle, i.e., ``lu[perm, :]`` is an upper/lower
|
|
triangular matrix. This is also equivalent to multiplication with a
|
|
permutation matrix ``P.dot(lu)``, where ``P`` is a column-permuted
|
|
identity matrix ``I[:, perm]``.
|
|
|
|
Depending on the value of the boolean `lower`, only upper or lower
|
|
triangular part of the input array is referenced. Hence, a triangular
|
|
matrix on entry would give the same result as if the full matrix is
|
|
supplied.
|
|
|
|
Parameters
|
|
----------
|
|
A : array_like
|
|
Square input array
|
|
lower : bool, optional
|
|
This switches between the lower and upper triangular outer factors of
|
|
the factorization. Lower triangular (``lower=True``) is the default.
|
|
hermitian : bool, optional
|
|
For complex-valued arrays, this defines whether ``A = A.conj().T`` or
|
|
``A = A.T`` is assumed. For real-valued arrays, this switch has no
|
|
effect.
|
|
overwrite_a : bool, optional
|
|
Allow overwriting data in `A` (may enhance performance). The default
|
|
is False.
|
|
check_finite : bool, optional
|
|
Whether to check that the input matrices contain only finite numbers.
|
|
Disabling may give a performance gain, but may result in problems
|
|
(crashes, non-termination) if the inputs do contain infinities or NaNs.
|
|
|
|
Returns
|
|
-------
|
|
lu : ndarray
|
|
The (possibly) permuted upper/lower triangular outer factor of the
|
|
factorization.
|
|
d : ndarray
|
|
The block diagonal multiplier of the factorization.
|
|
perm : ndarray
|
|
The row-permutation index array that brings lu into triangular form.
|
|
|
|
Raises
|
|
------
|
|
ValueError
|
|
If input array is not square.
|
|
ComplexWarning
|
|
If a complex-valued array with nonzero imaginary parts on the
|
|
diagonal is given and hermitian is set to True.
|
|
|
|
See Also
|
|
--------
|
|
cholesky, lu
|
|
|
|
Notes
|
|
-----
|
|
This function uses ``?SYTRF`` routines for symmetric matrices and
|
|
``?HETRF`` routines for Hermitian matrices from LAPACK. See [1]_ for
|
|
the algorithm details.
|
|
|
|
Depending on the `lower` keyword value, only lower or upper triangular
|
|
part of the input array is referenced. Moreover, this keyword also defines
|
|
the structure of the outer factors of the factorization.
|
|
|
|
.. versionadded:: 1.1.0
|
|
|
|
References
|
|
----------
|
|
.. [1] J.R. Bunch, L. Kaufman, Some stable methods for calculating
|
|
inertia and solving symmetric linear systems, Math. Comput. Vol.31,
|
|
1977. :doi:`10.2307/2005787`
|
|
|
|
Examples
|
|
--------
|
|
Given an upper triangular array ``a`` that represents the full symmetric
|
|
array with its entries, obtain ``l``, 'd' and the permutation vector `perm`:
|
|
|
|
>>> import numpy as np
|
|
>>> from scipy.linalg import ldl
|
|
>>> a = np.array([[2, -1, 3], [0, 2, 0], [0, 0, 1]])
|
|
>>> lu, d, perm = ldl(a, lower=0) # Use the upper part
|
|
>>> lu
|
|
array([[ 0. , 0. , 1. ],
|
|
[ 0. , 1. , -0.5],
|
|
[ 1. , 1. , 1.5]])
|
|
>>> d
|
|
array([[-5. , 0. , 0. ],
|
|
[ 0. , 1.5, 0. ],
|
|
[ 0. , 0. , 2. ]])
|
|
>>> perm
|
|
array([2, 1, 0])
|
|
>>> lu[perm, :]
|
|
array([[ 1. , 1. , 1.5],
|
|
[ 0. , 1. , -0.5],
|
|
[ 0. , 0. , 1. ]])
|
|
>>> lu.dot(d).dot(lu.T)
|
|
array([[ 2., -1., 3.],
|
|
[-1., 2., 0.],
|
|
[ 3., 0., 1.]])
|
|
|
|
"""
|
|
a = atleast_2d(_asarray_validated(A, check_finite=check_finite))
|
|
if a.shape[0] != a.shape[1]:
|
|
raise ValueError('The input array "a" should be square.')
|
|
# Return empty arrays for empty square input
|
|
if a.size == 0:
|
|
return empty_like(a), empty_like(a), np.array([], dtype=int)
|
|
|
|
n = a.shape[0]
|
|
r_or_c = complex if iscomplexobj(a) else float
|
|
|
|
# Get the LAPACK routine
|
|
if r_or_c is complex and hermitian:
|
|
s, sl = 'hetrf', 'hetrf_lwork'
|
|
if np.any(imag(diag(a))):
|
|
warn('scipy.linalg.ldl():\nThe imaginary parts of the diagonal'
|
|
'are ignored. Use "hermitian=False" for factorization of'
|
|
'complex symmetric arrays.', ComplexWarning, stacklevel=2)
|
|
else:
|
|
s, sl = 'sytrf', 'sytrf_lwork'
|
|
|
|
solver, solver_lwork = get_lapack_funcs((s, sl), (a,))
|
|
lwork = _compute_lwork(solver_lwork, n, lower=lower)
|
|
ldu, piv, info = solver(a, lwork=lwork, lower=lower,
|
|
overwrite_a=overwrite_a)
|
|
if info < 0:
|
|
raise ValueError(f'{s.upper()} exited with the internal error "illegal value '
|
|
f'in argument number {-info}". See LAPACK documentation '
|
|
'for the error codes.')
|
|
|
|
swap_arr, pivot_arr = _ldl_sanitize_ipiv(piv, lower=lower)
|
|
d, lu = _ldl_get_d_and_l(ldu, pivot_arr, lower=lower, hermitian=hermitian)
|
|
lu, perm = _ldl_construct_tri_factor(lu, swap_arr, pivot_arr, lower=lower)
|
|
|
|
return lu, d, perm
|
|
|
|
|
|
def _ldl_sanitize_ipiv(a, lower=True):
|
|
"""
|
|
This helper function takes the rather strangely encoded permutation array
|
|
returned by the LAPACK routines ?(HE/SY)TRF and converts it into
|
|
regularized permutation and diagonal pivot size format.
|
|
|
|
Since FORTRAN uses 1-indexing and LAPACK uses different start points for
|
|
upper and lower formats there are certain offsets in the indices used
|
|
below.
|
|
|
|
Let's assume a result where the matrix is 6x6 and there are two 2x2
|
|
and two 1x1 blocks reported by the routine. To ease the coding efforts,
|
|
we still populate a 6-sized array and fill zeros as the following ::
|
|
|
|
pivots = [2, 0, 2, 0, 1, 1]
|
|
|
|
This denotes a diagonal matrix of the form ::
|
|
|
|
[x x ]
|
|
[x x ]
|
|
[ x x ]
|
|
[ x x ]
|
|
[ x ]
|
|
[ x]
|
|
|
|
In other words, we write 2 when the 2x2 block is first encountered and
|
|
automatically write 0 to the next entry and skip the next spin of the
|
|
loop. Thus, a separate counter or array appends to keep track of block
|
|
sizes are avoided. If needed, zeros can be filtered out later without
|
|
losing the block structure.
|
|
|
|
Parameters
|
|
----------
|
|
a : ndarray
|
|
The permutation array ipiv returned by LAPACK
|
|
lower : bool, optional
|
|
The switch to select whether upper or lower triangle is chosen in
|
|
the LAPACK call.
|
|
|
|
Returns
|
|
-------
|
|
swap_ : ndarray
|
|
The array that defines the row/column swap operations. For example,
|
|
if row two is swapped with row four, the result is [0, 3, 2, 3].
|
|
pivots : ndarray
|
|
The array that defines the block diagonal structure as given above.
|
|
|
|
"""
|
|
n = a.size
|
|
swap_ = arange(n)
|
|
pivots = zeros_like(swap_, dtype=int)
|
|
skip_2x2 = False
|
|
|
|
# Some upper/lower dependent offset values
|
|
# range (s)tart, r(e)nd, r(i)ncrement
|
|
x, y, rs, re, ri = (1, 0, 0, n, 1) if lower else (-1, -1, n-1, -1, -1)
|
|
|
|
for ind in range(rs, re, ri):
|
|
# If previous spin belonged already to a 2x2 block
|
|
if skip_2x2:
|
|
skip_2x2 = False
|
|
continue
|
|
|
|
cur_val = a[ind]
|
|
# do we have a 1x1 block or not?
|
|
if cur_val > 0:
|
|
if cur_val != ind+1:
|
|
# Index value != array value --> permutation required
|
|
swap_[ind] = swap_[cur_val-1]
|
|
pivots[ind] = 1
|
|
# Not.
|
|
elif cur_val < 0 and cur_val == a[ind+x]:
|
|
# first neg entry of 2x2 block identifier
|
|
if -cur_val != ind+2:
|
|
# Index value != array value --> permutation required
|
|
swap_[ind+x] = swap_[-cur_val-1]
|
|
pivots[ind+y] = 2
|
|
skip_2x2 = True
|
|
else: # Doesn't make sense, give up
|
|
raise ValueError('While parsing the permutation array '
|
|
'in "scipy.linalg.ldl", invalid entries '
|
|
'found. The array syntax is invalid.')
|
|
return swap_, pivots
|
|
|
|
|
|
def _ldl_get_d_and_l(ldu, pivs, lower=True, hermitian=True):
|
|
"""
|
|
Helper function to extract the diagonal and triangular matrices for
|
|
LDL.T factorization.
|
|
|
|
Parameters
|
|
----------
|
|
ldu : ndarray
|
|
The compact output returned by the LAPACK routing
|
|
pivs : ndarray
|
|
The sanitized array of {0, 1, 2} denoting the sizes of the pivots. For
|
|
every 2 there is a succeeding 0.
|
|
lower : bool, optional
|
|
If set to False, upper triangular part is considered.
|
|
hermitian : bool, optional
|
|
If set to False a symmetric complex array is assumed.
|
|
|
|
Returns
|
|
-------
|
|
d : ndarray
|
|
The block diagonal matrix.
|
|
lu : ndarray
|
|
The upper/lower triangular matrix
|
|
"""
|
|
is_c = iscomplexobj(ldu)
|
|
d = diag(diag(ldu))
|
|
n = d.shape[0]
|
|
blk_i = 0 # block index
|
|
|
|
# row/column offsets for selecting sub-, super-diagonal
|
|
x, y = (1, 0) if lower else (0, 1)
|
|
|
|
lu = tril(ldu, -1) if lower else triu(ldu, 1)
|
|
diag_inds = arange(n)
|
|
lu[diag_inds, diag_inds] = 1
|
|
|
|
for blk in pivs[pivs != 0]:
|
|
# increment the block index and check for 2s
|
|
# if 2 then copy the off diagonals depending on uplo
|
|
inc = blk_i + blk
|
|
|
|
if blk == 2:
|
|
d[blk_i+x, blk_i+y] = ldu[blk_i+x, blk_i+y]
|
|
# If Hermitian matrix is factorized, the cross-offdiagonal element
|
|
# should be conjugated.
|
|
if is_c and hermitian:
|
|
d[blk_i+y, blk_i+x] = ldu[blk_i+x, blk_i+y].conj()
|
|
else:
|
|
d[blk_i+y, blk_i+x] = ldu[blk_i+x, blk_i+y]
|
|
|
|
lu[blk_i+x, blk_i+y] = 0.
|
|
blk_i = inc
|
|
|
|
return d, lu
|
|
|
|
|
|
def _ldl_construct_tri_factor(lu, swap_vec, pivs, lower=True):
|
|
"""
|
|
Helper function to construct explicit outer factors of LDL factorization.
|
|
|
|
If lower is True the permuted factors are multiplied as L(1)*L(2)*...*L(k).
|
|
Otherwise, the permuted factors are multiplied as L(k)*...*L(2)*L(1). See
|
|
LAPACK documentation for more details.
|
|
|
|
Parameters
|
|
----------
|
|
lu : ndarray
|
|
The triangular array that is extracted from LAPACK routine call with
|
|
ones on the diagonals.
|
|
swap_vec : ndarray
|
|
The array that defines the row swapping indices. If the kth entry is m
|
|
then rows k,m are swapped. Notice that the mth entry is not necessarily
|
|
k to avoid undoing the swapping.
|
|
pivs : ndarray
|
|
The array that defines the block diagonal structure returned by
|
|
_ldl_sanitize_ipiv().
|
|
lower : bool, optional
|
|
The boolean to switch between lower and upper triangular structure.
|
|
|
|
Returns
|
|
-------
|
|
lu : ndarray
|
|
The square outer factor which satisfies the L * D * L.T = A
|
|
perm : ndarray
|
|
The permutation vector that brings the lu to the triangular form
|
|
|
|
Notes
|
|
-----
|
|
Note that the original argument "lu" is overwritten.
|
|
|
|
"""
|
|
n = lu.shape[0]
|
|
perm = arange(n)
|
|
# Setup the reading order of the permutation matrix for upper/lower
|
|
rs, re, ri = (n-1, -1, -1) if lower else (0, n, 1)
|
|
|
|
for ind in range(rs, re, ri):
|
|
s_ind = swap_vec[ind]
|
|
if s_ind != ind:
|
|
# Column start and end positions
|
|
col_s = ind if lower else 0
|
|
col_e = n if lower else ind+1
|
|
|
|
# If we stumble upon a 2x2 block include both cols in the perm.
|
|
if pivs[ind] == (0 if lower else 2):
|
|
col_s += -1 if lower else 0
|
|
col_e += 0 if lower else 1
|
|
lu[[s_ind, ind], col_s:col_e] = lu[[ind, s_ind], col_s:col_e]
|
|
perm[[s_ind, ind]] = perm[[ind, s_ind]]
|
|
|
|
return lu, argsort(perm)
|