def test_pos_semidef_inv(ndim, dtype, n, deficient, reduce_rank, psdef, func): """Test positive semidefinite matrix inverses.""" if LooseVersion(np.__version__) >= LooseVersion('1.19'): svd = np.linalg.svd else: from mne.fixes import svd # make n-dimensional matrix n_extra = 2 # how many we add along the other dims rng = np.random.RandomState(73) shape = (n_extra, ) * (ndim - 2) + (n, n) mat = rng.randn(*shape) + 1j * rng.randn(*shape) proj = np.eye(n) if deficient: vec = np.ones(n) / np.sqrt(n) proj -= np.outer(vec, vec) with pytest.warns(None): # intentionally discard imag mat = mat.astype(dtype) # now make it conjugate symmetric or positive semi-definite if psdef: mat = np.matmul(mat, mat.swapaxes(-2, -1).conj()) else: mat += mat.swapaxes(-2, -1).conj() assert_allclose(mat, mat.swapaxes(-2, -1).conj(), atol=1e-6) s = svd(mat, hermitian=True)[1] assert (s >= 0).all() # make it rank deficient (maybe) if deficient: mat = np.matmul(np.matmul(proj, mat), proj) # if the dtype is complex, the conjugate transpose != transpose kwargs = dict(atol=1e-10, rtol=1e-10) orig_eq_t = np.allclose(mat, mat.swapaxes(-2, -1), **kwargs) t_eq_ct = np.allclose(mat.swapaxes(-2, -1), mat.conj().swapaxes(-2, -1), **kwargs) if np.iscomplexobj(mat): assert not orig_eq_t assert not t_eq_ct else: assert t_eq_ct assert orig_eq_t assert mat.shape == shape # ensure pos-semidef s = np.linalg.svd(mat, compute_uv=False) assert s.shape == shape[:-1] rank = (s > s[..., :1] * 1e-12).sum(-1) want_rank = n - deficient assert_array_equal(rank, want_rank) # assert equiv with NumPy mat_pinv = np.linalg.pinv(mat) if func is _sym_mat_pow: if not psdef: with pytest.raises(ValueError, match='not positive semi-'): func(mat, -1) return mat_symv = func(mat, -1, reduce_rank=reduce_rank) mat_sqrt = func(mat, 0.5) if ndim == 2: mat_sqrt_scipy = linalg.sqrtm(mat) assert_allclose(mat_sqrt, mat_sqrt_scipy, atol=1e-6) mat_2 = np.matmul(mat_sqrt, mat_sqrt) assert_allclose(mat, mat_2, atol=1e-6) mat_symv_2 = func(mat, -0.5, reduce_rank=reduce_rank) mat_symv_2 = np.matmul(mat_symv_2, mat_symv_2) assert_allclose(mat_symv_2, mat_symv, atol=1e-6) else: assert func is _reg_pinv mat_symv, _, _ = func(mat, rank=None) assert_allclose(mat_pinv, mat_symv, **kwargs) want = np.dot(proj, np.eye(n)) if deficient: want -= want.mean(axis=0) for _ in range(ndim - 2): want = np.repeat(want[np.newaxis], n_extra, axis=0) assert_allclose(np.matmul(mat_symv, mat), want, **kwargs) assert_allclose(np.matmul(mat, mat_symv), want, **kwargs)
def _reg_pinv(x, reg=0, rank='full', rcond=1e-15): """Compute a regularized pseudoinverse of Hermitian matrices. Regularization is performed by adding a constant value to each diagonal element of the matrix before inversion. This is known as "diagonal loading". The loading factor is computed as ``reg * np.trace(x) / len(x)``. The pseudo-inverse is computed through SVD decomposition and inverting the singular values. When the matrix is rank deficient, some singular values will be close to zero and will not be used during the inversion. The number of singular values to use can either be manually specified or automatically estimated. Parameters ---------- x : ndarray, shape (..., n, n) Square, Hermitian matrices to invert. reg : float Regularization parameter. Defaults to 0. rank : int | None | 'full' This controls the effective rank of the covariance matrix when computing the inverse. The rank can be set explicitly by specifying an integer value. If ``None``, the rank will be automatically estimated. Since applying regularization will always make the covariance matrix full rank, the rank is estimated before regularization in this case. If 'full', the rank will be estimated after regularization and hence will mean using the full rank, unless ``reg=0`` is used. Defaults to 'full'. rcond : float | 'auto' Cutoff for detecting small singular values when attempting to estimate the rank of the matrix (``rank='auto'``). Singular values smaller than the cutoff are set to zero. When set to 'auto', a cutoff based on floating point precision will be used. Defaults to 1e-15. Returns ------- x_inv : ndarray, shape (..., n, n) The inverted matrix. loading_factor : float Value added to the diagonal of the matrix during regularization. rank : int If ``rank`` was set to an integer value, this value is returned, else the estimated rank of the matrix, before regularization, is returned. """ from ..rank import _estimate_rank_from_s if rank is not None and rank != 'full': rank = int(operator.index(rank)) if x.ndim < 2 or x.shape[-2] != x.shape[-1]: raise ValueError('Input matrix must be square.') if not np.allclose(x, x.conj().swapaxes(-2, -1)): raise ValueError('Input matrix must be Hermitian (symmetric)') assert x.ndim >= 2 and x.shape[-2] == x.shape[-1] n = x.shape[-1] # Decompose the matrix, not necessarily positive semidefinite from mne.fixes import svd U, s, Vh = svd(x, hermitian=True) # Estimate the rank before regularization tol = 'auto' if rcond == 'auto' else rcond * s[..., :1] rank_before = _estimate_rank_from_s(s, tol) # Decompose the matrix again after regularization loading_factor = reg * np.mean(s, axis=-1) if reg: U, s, Vh = svd(x + loading_factor[..., np.newaxis, np.newaxis] * np.eye(n), hermitian=True) # Estimate the rank after regularization tol = 'auto' if rcond == 'auto' else rcond * s[..., :1] rank_after = _estimate_rank_from_s(s, tol) # Warn the user if both all parameters were kept at their defaults and the # matrix is rank deficient. if (rank_after < n).any() and reg == 0 and \ rank == 'full' and rcond == 1e-15: warn('Covariance matrix is rank-deficient and no regularization is ' 'done.') elif isinstance(rank, int) and rank > n: raise ValueError('Invalid value for the rank parameter (%d) given ' 'the shape of the input matrix (%d x %d).' % (rank, x.shape[0], x.shape[1])) # Pick the requested number of singular values mask = np.arange(s.shape[-1]).reshape((1, ) * (x.ndim - 2) + (-1, )) if rank is None: cmp = ret = rank_before elif rank == 'full': cmp = rank_after ret = rank_before else: cmp = ret = rank mask = mask < np.asarray(cmp)[..., np.newaxis] mask &= s > 0 # Invert only non-zero singular values s_inv = np.zeros(s.shape) s_inv[mask] = 1. / s[mask] # Compute the pseudo inverse x_inv = np.matmul(U * s_inv[..., np.newaxis, :], Vh) return x_inv, loading_factor, ret