Source code for mpnum.utils.extmath

# encoding: utf-8
"""Additional math functions for dealing with dense arrays"""

from __future__ import division, print_function

import numpy as np
from scipy import linalg
from scipy.sparse.linalg import aslinearoperator
from six.moves import range, zip

__all__ = ['block_diag', 'matdot', 'mkron', 'partial_trace',
           'truncated_svd', 'randomized_svd']


[docs]def partial_trace(array, traceout): """Return the partial trace of an array over the sites given in traceout. :param np.ndarray array: Array in global form (see :func:`global_to_local` above) with exactly 2 legs per site :param traceout: List of sites to trace out, must be in _ascending_ order :returns: Partial trace over input array """ if len(traceout) == 0: return array sites, rem = divmod(array.ndim, 2) assert rem == 0, 'ndim={} is not a multiple of 2'.format(array.ndim) # Recursively trace out the last site given array_red = np.trace(array, axis1=traceout[-1], axis2=traceout[-1] + sites) return partial_trace(array_red, traceout=traceout[:-1])
[docs]def matdot(A, B, axes=((-1,), (0,))): """np.tensordot with sane defaults for matrix multiplication""" return np.tensordot(A, B, axes=axes)
[docs]def mkron(*args): """np.kron() with an arbitrary number of n >= 1 arguments""" if len(args) == 1: return args[0] return mkron(np.kron(args[0], args[1]), *args[2:])
[docs]def block_diag(summands, axes=(0, 1)): """Block-diagonal sum for n-dimensional arrays. Perform something like a block diagonal sum (if len(axes) == 2) along the specified axes. All other axes must have identical sizes. :param axes: Along these axes, perform a block-diagonal sum. Can be negative. >>> a = np.arange(8).reshape((2, 2, 2)) >>> b = np.arange(8, 16).reshape((2, 2, 2)) >>> a array([[[0, 1], [2, 3]], <BLANKLINE> [[4, 5], [6, 7]]]) >>> b array([[[ 8, 9], [10, 11]], <BLANKLINE> [[12, 13], [14, 15]]]) >>> block_diag((a, b), axes=(1, -1)) array([[[ 0, 1, 0, 0], [ 2, 3, 0, 0], [ 0, 0, 8, 9], [ 0, 0, 10, 11]], <BLANKLINE> [[ 4, 5, 0, 0], [ 6, 7, 0, 0], [ 0, 0, 12, 13], [ 0, 0, 14, 15]]]) """ axes = np.array(axes) axes += (axes < 0) * summands[0].ndim nr_axes = len(axes) axes_order = list(axes) axes_order += [i for i in range(summands[0].ndim) if i not in axes] summands = [array.transpose(axes_order) for array in summands] shapes = np.array([array.shape[:nr_axes] for array in summands]) res = np.zeros(tuple(shapes.sum(axis=0)) + summands[0].shape[nr_axes:], dtype=summands[0].dtype) startpos = np.zeros(nr_axes, dtype=int) for array, shape in zip(summands, shapes): endpos = startpos + shape pos = [slice(start, end) for start, end in zip(startpos, endpos)] res[pos] += array startpos = endpos old_axes_order = np.argsort(axes_order) res = res.transpose(old_axes_order) return res
[docs]def truncated_svd(A, k): """Compute the truncated SVD of the matrix `A` i.e. the `k` largest singular values as well as the corresponding singular vectors. It might return less singular values/vectors, if one dimension of `A` is smaller than `k`. In the background it performs a full SVD. Therefore, it might be inefficient when `k` is much smaller than the dimensions of `A`. :param A: A real or complex matrix :param k: Number of singular values/vectors to compute :returns: u, s, v, where u: left-singular vectors s: singular values in descending order v: right-singular vectors """ u, s, v = np.linalg.svd(A) k_prime = min(k, len(s)) return u[:, :k_prime], s[:k_prime], v[:k_prime]
#################### # Randomized SVD # #################### def _standard_normal(shape, randstate=np.random, dtype=np.float_): """Generates a standard normal numpy array of given shape and dtype, i.e. this function is equivalent to `randstate.randn(*shape)` for real dtype and `randstate.randn(*shape) + 1.j * randstate.randn(shape)` for complex dtype. :param tuple shape: Shape of array to be returned :param randstate: An instance of :class:`numpy.random.RandomState` (default is ``np.random``)) :param dtype: ``np.float_`` (default) or `np.complex_` Returns ------- A: An array of given shape and dtype with standard normal entries """ if dtype == np.float_: return randstate.randn(*shape) elif dtype == np.complex_: return randstate.randn(*shape) + 1.j * randstate.randn(*shape) else: raise ValueError('{} is not a valid dtype.'.format(dtype)) def approx_range_finder(A, sketch_size, n_iter, piter_normalizer='auto', randstate=np.random): """Computes an orthonormal matrix whose range approximates the range of A. Parameters ---------- :param A: The input data matrix, can be any type that can be converted into a :class:`scipy.linalg.LinarOperator`, e.g. :class:`numpy.ndarray`, or a sparse matrix. :param int sketch_size: Size of the return array :param int n_iter: Number of power iterations used to stabilize the result :param str piter_normalizer: ``'auto'`` (default), ``'QR'``, ``'LU'``, ``'none'``. Whether the power iterations are normalized with step-by-step QR factorization (the slowest but most accurate), 'none' (the fastest but numerically unstable when `n_iter` is large, e.g. typically 5 or larger), or 'LU' factorization (numerically stable but can lose slightly in accuracy). The 'auto' mode applies no normalization if `n_iter`<=2 and switches to LU otherwise. :param randstate: An instance of :class:`numpy.random.RandomState` (default is ``np.random``)) Returns ------- :returns: :class:`numpy.ndarray` A (A.shape[0] x sketch_size) projection matrix, the range of which approximates well the range of the input matrix A. Notes ----- Follows Algorithm 4.3/4.4 of Finding structure with randomness: Stochastic algorithms for constructing approximate matrix decompositions Halko, et al., 2009 (arXiv:909) http://arxiv.org/pdf/0909.4061 An implementation of a randomized algorithm for principal component analysis A. Szlam et al. 2014 Original implementation from scikit-learn. """ A = aslinearoperator(A) # note that real normal vectors might actually be sufficient Q = _standard_normal((A.shape[1], sketch_size), randstate=randstate, dtype=A.dtype) # Deal with "auto" mode if piter_normalizer == 'auto': if n_iter <= 2: piter_normalizer = 'none' else: piter_normalizer = 'LU' # Perform power iterations with Q to further 'imprint' the top # singular vectors of A in Q for i in range(n_iter): if piter_normalizer == 'none': Q = A * Q Q = A.H * Q elif piter_normalizer == 'LU': Q, _ = linalg.lu(A * Q, permute_l=True) Q, _ = linalg.lu(A.H * Q, permute_l=True) elif piter_normalizer == 'QR': Q, _ = linalg.qr(A * Q, mode='economic') Q, _ = linalg.qr(A.H * Q, mode='economic') # Sample the range of A using by linear projection of Q # Extract an orthonormal basis Q, _ = linalg.qr(A * Q, mode='economic') return Q
[docs]def randomized_svd(M, n_components, n_oversamples=10, n_iter='auto', piter_normalizer='auto', transpose='auto', randstate=np.random): """Computes a truncated randomized SVD. Uses the same convention as :func:`scipy.sparse.linalg.svds`. However, we guarantee to return the singular values in descending order. :param M: The input data matrix, can be any type that can be converted into a :class:`scipy.linalg.LinarOperator`, e.g. :class:`numpy.ndarray`, or a sparse matrix. :param int n_components: Number of singular values and vectors to extract. :param int n_oversamples: Additional number of random vectors to sample the range of `M` so as to ensure proper conditioning. The total number of random vectors used to find the range of M is ``n_components + n_oversamples``. Smaller number can improve speed but can negatively impact the quality of approximation of singular vectors and singular values. (default 10) :param n_iter: Number of power iterations. It can be used to deal with very noisy problems. When ``'auto'``, it is set to 4, unless ``n_components`` is small (``< .1 * min(X.shape)``). Then, ``n_iter`` is set to 7. This improves precision with few components. (default ``'auto'``) :param str piter_normalizer: ``'auto'`` (default), ``'QR'``\ , ``'LU'``\ , ``'none'``\ . Whether the power iterations are normalized with step-by-step QR factorization (the slowest but most accurate), ``'none'`` (the fastest but numerically unstable when `n_iter` is large, e.g. typically 5 or larger), or ``'LU'`` factorization (numerically stable but can lose slightly in accuracy). The 'auto' mode applies no normalization if ``n_iter <= 2`` and switches to LU otherwise. :param transpose: ``True``, ``False`` or ``'auto'`` Whether the algorithm should be applied to ``M.T`` instead of ``M``. The result should approximately be the same. The ``'auto'`` mode will trigger the transposition if ``M.shape[1] > M.shape[0]`` since then the computational overhead in the randomized SVD is generally smaller. (default ``'auto'``). :param randstate: An instance of :class:`numpy.random.RandomState` (default is ``np.random``)) .. rubric:: Notes This algorithm finds a (usually very good) approximate truncated singular value decomposition using randomization to speed up the computations. It is particularly fast on large matrices on which you wish to extract only a small number of components. In order to obtain further speed up, ``n_iter`` can be set <=2 (at the cost of loss of precision). .. rubric:: References * Finding structure with randomness: Stochastic algorithms for constructing approximate matrix decompositions Halko, et al., 2009 http://arxiv.org/abs/arXiv:0909.4061 * A randomized algorithm for the decomposition of matrices Per-Gunnar Martinsson, Vladimir Rokhlin and Mark Tygert * An implementation of a randomized algorithm for principal component analysis A. Szlam et al. 2014 """ M = aslinearoperator(M) sketch_size = n_components + n_oversamples if n_iter == 'auto': # Checks if the number of iterations is explicitely specified # Adjust n_iter. 7 was found a good compromise for PCA. n_iter = 7 if n_components < .1 * min(M.shape) else 4 if transpose == 'auto': transpose = M.shape[0] < M.shape[1] if transpose: M = M.H Q = approx_range_finder(M, sketch_size, n_iter, piter_normalizer, randstate) # project M to the (k + p) dimensional space using the basis vectors # B = Q.H * M B = (M.H * Q).conj().T # compute the SVD on the thin matrix: (k + p) wide Uhat, s, V = linalg.svd(B, full_matrices=False) del B U = np.dot(Q, Uhat) sel = slice(None, n_components, 1) if transpose: # transpose back the results according to the input convention return (V[sel].conj().T, s[sel], U[:, sel].conj().T) else: return U[:, sel], s[sel], V[sel, :]