""" Test functions for the sparse.linalg._eigen.lobpcg module
"""

import itertools
import platform
import sys
import pytest
import numpy as np
from numpy import ones, r_, diag
from numpy.testing import (assert_almost_equal, assert_equal,
                           assert_allclose, assert_array_less)

from scipy import sparse
from scipy.linalg import eig, eigh, toeplitz, orth
from scipy.sparse import spdiags, diags, eye, csr_matrix
from scipy.sparse.linalg import eigs, LinearOperator
from scipy.sparse.linalg._eigen.lobpcg import lobpcg
from scipy.sparse.linalg._eigen.lobpcg.lobpcg import _b_orthonormalize

_IS_32BIT = (sys.maxsize < 2**32)

INT_DTYPES = {np.intc, np.int_, np.longlong, np.uintc, np.uint, np.ulonglong}
# np.half is unsupported on many test systems so excluded
REAL_DTYPES = {np.single, np.double, np.longdouble}
COMPLEX_DTYPES = {np.csingle, np.cdouble, np.clongdouble}
# use sorted tuple to ensure fixed order of tests
VDTYPES = tuple(sorted(REAL_DTYPES ^ COMPLEX_DTYPES, key=str))
MDTYPES = tuple(sorted(INT_DTYPES ^ REAL_DTYPES ^ COMPLEX_DTYPES, key=str))


def sign_align(A, B):
    """Align signs of columns of A match those of B: column-wise remove
    sign of A by multiplying with its sign then multiply in sign of B.
    """
    return np.array([col_A * np.sign(col_A[0]) * np.sign(col_B[0])
                     for col_A, col_B in zip(A.T, B.T)]).T

def ElasticRod(n):
    """Build the matrices for the generalized eigenvalue problem of the
    fixed-free elastic rod vibration model.
    """
    L = 1.0
    le = L/n
    rho = 7.85e3
    S = 1.e-4
    E = 2.1e11
    mass = rho*S*le/6.
    k = E*S/le
    A = k*(diag(r_[2.*ones(n-1), 1])-diag(ones(n-1), 1)-diag(ones(n-1), -1))
    B = mass*(diag(r_[4.*ones(n-1), 2])+diag(ones(n-1), 1)+diag(ones(n-1), -1))
    return A, B


def MikotaPair(n):
    """Build a pair of full diagonal matrices for the generalized eigenvalue
    problem. The Mikota pair acts as a nice test since the eigenvalues are the
    squares of the integers n, n=1,2,...
    """
    x = np.arange(1, n+1)
    B = diag(1./x)
    y = np.arange(n-1, 0, -1)
    z = np.arange(2*n-1, 0, -2)
    A = diag(z)-diag(y, -1)-diag(y, 1)
    return A, B


def compare_solutions(A, B, m):
    """Check eig vs. lobpcg consistency.
    """
    n = A.shape[0]
    rnd = np.random.RandomState(0)
    V = rnd.random((n, m))
    X = orth(V)
    eigvals, _ = lobpcg(A, X, B=B, tol=1e-2, maxiter=50, largest=False)
    eigvals.sort()
    w, _ = eig(A, b=B)
    w.sort()
    assert_almost_equal(w[:int(m/2)], eigvals[:int(m/2)], decimal=2)


def test_Small():
    A, B = ElasticRod(10)
    with pytest.warns(UserWarning, match="The problem size"):
        compare_solutions(A, B, 10)
    A, B = MikotaPair(10)
    with pytest.warns(UserWarning, match="The problem size"):
        compare_solutions(A, B, 10)


def test_ElasticRod():
    A, B = ElasticRod(20)
    with pytest.warns(UserWarning, match="Exited at iteration"):
        compare_solutions(A, B, 2)


def test_MikotaPair():
    A, B = MikotaPair(20)
    compare_solutions(A, B, 2)


@pytest.mark.parametrize("n", [50])
@pytest.mark.parametrize("m", [1, 2, 10])
@pytest.mark.parametrize("Vdtype", REAL_DTYPES)
@pytest.mark.parametrize("Bdtype", REAL_DTYPES)
@pytest.mark.parametrize("BVdtype", REAL_DTYPES)
def test_b_orthonormalize(n, m, Vdtype, Bdtype, BVdtype):
    """Test B-orthonormalization by Cholesky with callable 'B'.
    The function '_b_orthonormalize' is key in LOBPCG but may
    lead to numerical instabilities. The input vectors are often
    badly scaled, so the function needs scale-invariant Cholesky;
    see https://netlib.org/lapack/lawnspdf/lawn14.pdf.
    """
    rnd = np.random.RandomState(0)
    X = rnd.standard_normal((n, m)).astype(Vdtype)
    Xcopy = np.copy(X)
    vals = np.arange(1, n+1, dtype=float)
    B = diags([vals], [0], (n, n)).astype(Bdtype)
    BX = B @ X
    BX = BX.astype(BVdtype)
    dtype = min(X.dtype, B.dtype, BX.dtype)
    # np.longdouble tol cannot be achieved on most systems
    atol = m * n * max(np.finfo(dtype).eps, np.finfo(np.double).eps)

    Xo, BXo, _ = _b_orthonormalize(lambda v: B @ v, X, BX)
    # Check in-place.
    assert_equal(X, Xo)
    assert_equal(id(X), id(Xo))
    assert_equal(BX, BXo)
    assert_equal(id(BX), id(BXo))
    # Check BXo.
    assert_allclose(B @ Xo, BXo, atol=atol, rtol=atol)
    # Check B-orthonormality
    assert_allclose(Xo.T.conj() @ B @ Xo, np.identity(m),
                    atol=atol, rtol=atol)
    # Repeat without BX in outputs
    X = np.copy(Xcopy)
    Xo1, BXo1, _ = _b_orthonormalize(lambda v: B @ v, X)
    assert_allclose(Xo, Xo1, atol=atol, rtol=atol)
    assert_allclose(BXo, BXo1, atol=atol, rtol=atol)
    # Check in-place.
    assert_equal(X, Xo1)
    assert_equal(id(X), id(Xo1))
    # Check BXo1.
    assert_allclose(B @ Xo1, BXo1, atol=atol, rtol=atol)

    # Introduce column-scaling in X.
    scaling = 1.0 / np.geomspace(10, 1e10, num=m)
    X = Xcopy * scaling
    X = X.astype(Vdtype)
    BX = B @ X
    BX = BX.astype(BVdtype)
    # Check scaling-invariance of Cholesky-based orthonormalization
    Xo1, BXo1, _ = _b_orthonormalize(lambda v: B @ v, X, BX)
    # The output should be the same, up the signs of the columns.
    Xo1 =  sign_align(Xo1, Xo)
    assert_allclose(Xo, Xo1, atol=atol, rtol=atol)
    BXo1 =  sign_align(BXo1, BXo)
    assert_allclose(BXo, BXo1, atol=atol, rtol=atol)


@pytest.mark.filterwarnings("ignore:Exited at iteration 0")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_nonhermitian_warning(capsys):
    """Check the warning of a Ritz matrix being not Hermitian
    by feeding a non-Hermitian input matrix.
    Also check stdout since verbosityLevel=1 and lack of stderr.
    """
    n = 10
    X = np.arange(n * 2).reshape(n, 2).astype(np.float32)
    A = np.arange(n * n).reshape(n, n).astype(np.float32)
    with pytest.warns(UserWarning, match="Matrix gramA"):
        _, _ = lobpcg(A, X, verbosityLevel=1, maxiter=0)
    out, err = capsys.readouterr()  # Capture output
    assert out.startswith("Solving standard eigenvalue")  # Test stdout
    assert err == ''  # Test empty stderr
    # Make the matrix symmetric and the UserWarning dissappears.
    A += A.T
    _, _ = lobpcg(A, X, verbosityLevel=1, maxiter=0)
    out, err = capsys.readouterr()  # Capture output
    assert out.startswith("Solving standard eigenvalue")  # Test stdout
    assert err == ''  # Test empty stderr


def test_regression():
    """Check the eigenvalue of the identity matrix is one.
    """
    # https://mail.python.org/pipermail/scipy-user/2010-October/026944.html
    n = 10
    X = np.ones((n, 1))
    A = np.identity(n)
    w, _ = lobpcg(A, X)
    assert_allclose(w, [1])


@pytest.mark.filterwarnings("ignore:The problem size")
@pytest.mark.parametrize('n, m, m_excluded', [(30, 4, 3), (4, 2, 0)])
def test_diagonal(n, m, m_excluded):
    """Test ``m - m_excluded`` eigenvalues and eigenvectors of
    diagonal matrices of the size ``n`` varying matrix formats:
    dense array, spare matrix, and ``LinearOperator`` for both
    matrixes in the generalized eigenvalue problem ``Av = cBv``
    and for the preconditioner.
    """
    rnd = np.random.RandomState(0)

    # Define the generalized eigenvalue problem Av = cBv
    # where (c, v) is a generalized eigenpair,
    # A is the diagonal matrix whose entries are 1,...n,
    # B is the identity matrix.
    vals = np.arange(1, n+1, dtype=float)
    A_s = diags([vals], [0], (n, n))
    A_a = A_s.toarray()

    def A_f(x):
        return A_s @ x

    A_lo = LinearOperator(matvec=A_f,
                          matmat=A_f,
                          shape=(n, n), dtype=float)

    B_a = eye(n)
    B_s = csr_matrix(B_a)

    def B_f(x):
        return B_a @ x

    B_lo = LinearOperator(matvec=B_f,
                          matmat=B_f,
                          shape=(n, n), dtype=float)

    # Let the preconditioner M be the inverse of A.
    M_s = diags([1./vals], [0], (n, n))
    M_a = M_s.toarray()

    def M_f(x):
        return M_s @ x

    M_lo = LinearOperator(matvec=M_f,
                          matmat=M_f,
                          shape=(n, n), dtype=float)

    # Pick random initial vectors.
    X = rnd.normal(size=(n, m))

    # Require that the returned eigenvectors be in the orthogonal complement
    # of the first few standard basis vectors.
    if m_excluded > 0:
        Y = np.eye(n, m_excluded)
    else:
        Y = None

    for A in [A_a, A_s, A_lo]:
        for B in [B_a, B_s, B_lo]:
            for M in [M_a, M_s, M_lo]:
                eigvals, vecs = lobpcg(A, X, B, M=M, Y=Y,
                                       maxiter=40, largest=False)

                assert_allclose(eigvals, np.arange(1+m_excluded,
                                                   1+m_excluded+m))
                _check_eigen(A, eigvals, vecs, rtol=1e-3, atol=1e-3)


def _check_eigen(M, w, V, rtol=1e-8, atol=1e-14):
    """Check if the eigenvalue residual is small.
    """
    mult_wV = np.multiply(w, V)
    dot_MV = M.dot(V)
    assert_allclose(mult_wV, dot_MV, rtol=rtol, atol=atol)


def _check_fiedler(n, p):
    """Check the Fiedler vector computation.
    """
    # This is not necessarily the recommended way to find the Fiedler vector.
    col = np.zeros(n)
    col[1] = 1
    A = toeplitz(col)
    D = np.diag(A.sum(axis=1))
    L = D - A
    # Compute the full eigendecomposition using tricks, e.g.
    # http://www.cs.yale.edu/homes/spielman/561/2009/lect02-09.pdf
    tmp = np.pi * np.arange(n) / n
    analytic_w = 2 * (1 - np.cos(tmp))
    analytic_V = np.cos(np.outer(np.arange(n) + 1/2, tmp))
    _check_eigen(L, analytic_w, analytic_V)
    # Compute the full eigendecomposition using eigh.
    eigh_w, eigh_V = eigh(L)
    _check_eigen(L, eigh_w, eigh_V)
    # Check that the first eigenvalue is near zero and that the rest agree.
    assert_array_less(np.abs([eigh_w[0], analytic_w[0]]), 1e-14)
    assert_allclose(eigh_w[1:], analytic_w[1:])

    # Check small lobpcg eigenvalues.
    X = analytic_V[:, :p]
    lobpcg_w, lobpcg_V = lobpcg(L, X, largest=False)
    assert_equal(lobpcg_w.shape, (p,))
    assert_equal(lobpcg_V.shape, (n, p))
    _check_eigen(L, lobpcg_w, lobpcg_V)
    assert_array_less(np.abs(np.min(lobpcg_w)), 1e-14)
    assert_allclose(np.sort(lobpcg_w)[1:], analytic_w[1:p])

    # Check large lobpcg eigenvalues.
    X = analytic_V[:, -p:]
    lobpcg_w, lobpcg_V = lobpcg(L, X, largest=True)
    assert_equal(lobpcg_w.shape, (p,))
    assert_equal(lobpcg_V.shape, (n, p))
    _check_eigen(L, lobpcg_w, lobpcg_V)
    assert_allclose(np.sort(lobpcg_w), analytic_w[-p:])

    # Look for the Fiedler vector using good but not exactly correct guesses.
    fiedler_guess = np.concatenate((np.ones(n//2), -np.ones(n-n//2)))
    X = np.vstack((np.ones(n), fiedler_guess)).T
    lobpcg_w, _ = lobpcg(L, X, largest=False)
    # Mathematically, the smaller eigenvalue should be zero
    # and the larger should be the algebraic connectivity.
    lobpcg_w = np.sort(lobpcg_w)
    assert_allclose(lobpcg_w, analytic_w[:2], atol=1e-14)


def test_fiedler_small_8():
    """Check the dense workaround path for small matrices.
    """
    # This triggers the dense path because 8 < 2*5.
    with pytest.warns(UserWarning, match="The problem size"):
        _check_fiedler(8, 2)


def test_fiedler_large_12():
    """Check the dense workaround path avoided for non-small matrices.
    """
    # This does not trigger the dense path, because 2*5 <= 12.
    _check_fiedler(12, 2)


@pytest.mark.filterwarnings("ignore:Failed at iteration")
@pytest.mark.filterwarnings("ignore:Exited at iteration")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_failure_to_run_iterations():
    """Check that the code exits gracefully without breaking. Issue #10974.
    The code may or not issue a warning, filtered out. Issue #15935, #17954.
    """
    rnd = np.random.RandomState(0)
    X = rnd.standard_normal((100, 10))
    A = X @ X.T
    Q = rnd.standard_normal((X.shape[0], 4))
    eigenvalues, _ = lobpcg(A, Q, maxiter=40, tol=1e-12)
    assert np.max(eigenvalues) > 0


def test_failure_to_run_iterations_nonsymmetric():
    """Check that the code exists gracefully without breaking
    if the matrix in not symmetric.
    """
    A = np.zeros((10, 10))
    A[0, 1] = 1
    Q = np.ones((10, 1))
    with pytest.warns(UserWarning, match="Exited at iteration 2"):
        eigenvalues, _ = lobpcg(A, Q, maxiter=20)
    assert np.max(eigenvalues) > 0


@pytest.mark.filterwarnings("ignore:The problem size")
def test_hermitian():
    """Check complex-value Hermitian cases.
    """
    rnd = np.random.RandomState(0)

    sizes = [3, 12]
    ks = [1, 2]
    gens = [True, False]

    for s, k, gen, dh, dx, db in (
        itertools.product(sizes, ks, gens, gens, gens, gens)
    ):
        H = rnd.random((s, s)) + 1.j * rnd.random((s, s))
        H = 10 * np.eye(s) + H + H.T.conj()
        H = H.astype(np.complex128) if dh else H.astype(np.complex64)

        X = rnd.standard_normal((s, k))
        X = X + 1.j * rnd.standard_normal((s, k))
        X = X.astype(np.complex128) if dx else X.astype(np.complex64)

        if not gen:
            B = np.eye(s)
            w, v = lobpcg(H, X, maxiter=99, verbosityLevel=0)
            # Also test mixing complex H with real B.
            wb, _ = lobpcg(H, X, B, maxiter=99, verbosityLevel=0)
            assert_allclose(w, wb, rtol=1e-6)
            w0, _ = eigh(H)
        else:
            B = rnd.random((s, s)) + 1.j * rnd.random((s, s))
            B = 10 * np.eye(s) + B.dot(B.T.conj())
            B = B.astype(np.complex128) if db else B.astype(np.complex64)
            w, v = lobpcg(H, X, B, maxiter=99, verbosityLevel=0)
            w0, _ = eigh(H, B)

        for wx, vx in zip(w, v.T):
            # Check eigenvector
            assert_allclose(np.linalg.norm(H.dot(vx) - B.dot(vx) * wx)
                            / np.linalg.norm(H.dot(vx)),
                            0, atol=5e-2, rtol=0)

            # Compare eigenvalues
            j = np.argmin(abs(w0 - wx))
            assert_allclose(wx, w0[j], rtol=1e-4)


# The n=5 case tests the alternative small matrix code path that uses eigh().
@pytest.mark.filterwarnings("ignore:The problem size")
@pytest.mark.parametrize('n, atol', [(20, 1e-3), (5, 1e-8)])
def test_eigs_consistency(n, atol):
    """Check eigs vs. lobpcg consistency.
    """
    vals = np.arange(1, n+1, dtype=np.float64)
    A = spdiags(vals, 0, n, n)
    rnd = np.random.RandomState(0)
    X = rnd.standard_normal((n, 2))
    lvals, lvecs = lobpcg(A, X, largest=True, maxiter=100)
    vals, _ = eigs(A, k=2)

    _check_eigen(A, lvals, lvecs, atol=atol, rtol=0)
    assert_allclose(np.sort(vals), np.sort(lvals), atol=1e-14)


def test_verbosity():
    """Check that nonzero verbosity level code runs.
    """
    rnd = np.random.RandomState(0)
    X = rnd.standard_normal((10, 10))
    A = X @ X.T
    Q = rnd.standard_normal((X.shape[0], 1))
    with pytest.warns(UserWarning, match="Exited at iteration"):
        _, _ = lobpcg(A, Q, maxiter=3, verbosityLevel=9)


@pytest.mark.xfail(_IS_32BIT and sys.platform == 'win32',
                   reason="tolerance violation on windows")
@pytest.mark.xfail(platform.machine() == 'ppc64le',
                   reason="fails on ppc64le")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_tolerance_float32():
    """Check lobpcg for attainable tolerance in float32.
    """
    rnd = np.random.RandomState(0)
    n = 50
    m = 3
    vals = -np.arange(1, n + 1)
    A = diags([vals], [0], (n, n))
    A = A.astype(np.float32)
    X = rnd.standard_normal((n, m))
    X = X.astype(np.float32)
    eigvals, _ = lobpcg(A, X, tol=1.25e-5, maxiter=50, verbosityLevel=0)
    assert_allclose(eigvals, -np.arange(1, 1 + m), atol=2e-5, rtol=1e-5)


@pytest.mark.parametrize("vdtype", VDTYPES)
@pytest.mark.parametrize("mdtype", MDTYPES)
@pytest.mark.parametrize("arr_type", [np.array,
                                      sparse.csr_matrix,
                                      sparse.coo_matrix])
def test_dtypes(vdtype, mdtype, arr_type):
    """Test lobpcg in various dtypes.
    """
    rnd = np.random.RandomState(0)
    n = 12
    m = 2
    A = arr_type(np.diag(np.arange(1, n + 1)).astype(mdtype))
    X = rnd.random((n, m))
    X = X.astype(vdtype)
    eigvals, eigvecs = lobpcg(A, X, tol=1e-2, largest=False)
    assert_allclose(eigvals, np.arange(1, 1 + m), atol=1e-1)
    # eigenvectors must be nearly real in any case
    assert_allclose(np.sum(np.abs(eigvecs - eigvecs.conj())), 0, atol=1e-2)


@pytest.mark.filterwarnings("ignore:Exited at iteration")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_inplace_warning():
    """Check lobpcg gives a warning in '_b_orthonormalize'
    that in-place orthogonalization is impossible due to dtype mismatch.
    """
    rnd = np.random.RandomState(0)
    n = 6
    m = 1
    vals = -np.arange(1, n + 1)
    A = diags([vals], [0], (n, n))
    A = A.astype(np.cdouble)
    X = rnd.standard_normal((n, m))
    with pytest.warns(UserWarning, match="Inplace update"):
        eigvals, _ = lobpcg(A, X, maxiter=2, verbosityLevel=1)


def test_maxit():
    """Check lobpcg if maxit=maxiter runs maxiter iterations and
    if maxit=None runs 20 iterations (the default)
    by checking the size of the iteration history output, which should
    be the number of iterations plus 3 (initial, final, and postprocessing)
    typically when maxiter is small and the choice of the best is passive.
    """
    rnd = np.random.RandomState(0)
    n = 50
    m = 4
    vals = -np.arange(1, n + 1)
    A = diags([vals], [0], (n, n))
    A = A.astype(np.float32)
    X = rnd.standard_normal((n, m))
    X = X.astype(np.float64)
    for maxiter in range(1, 4):
        with pytest.warns(UserWarning, match="Exited at iteration"):
            _, _, l_h, r_h = lobpcg(A, X, tol=1e-8, maxiter=maxiter,
                                    retLambdaHistory=True,
                                    retResidualNormsHistory=True)
        assert_allclose(np.shape(l_h)[0], maxiter+3)
        assert_allclose(np.shape(r_h)[0], maxiter+3)
    with pytest.warns(UserWarning, match="Exited at iteration"):
        l, _, l_h, r_h = lobpcg(A, X, tol=1e-8,
                                retLambdaHistory=True,
                                retResidualNormsHistory=True)
    assert_allclose(np.shape(l_h)[0], 20+3)
    assert_allclose(np.shape(r_h)[0], 20+3)
    # Check that eigenvalue output is the last one in history
    assert_allclose(l, l_h[-1])
    # Make sure that both history outputs are lists
    assert isinstance(l_h, list)
    assert isinstance(r_h, list)
    # Make sure that both history lists are arrays-like
    assert_allclose(np.shape(l_h), np.shape(np.asarray(l_h)))
    assert_allclose(np.shape(r_h), np.shape(np.asarray(r_h)))


@pytest.mark.slow
@pytest.mark.parametrize("n", [15])
@pytest.mark.parametrize("m", [1, 2])
@pytest.mark.filterwarnings("ignore:Exited at iteration")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_diagonal_data_types(n, m):
    """Check lobpcg for diagonal matrices for all matrix types.
    Constraints are imposed, so a dense eigensolver eig cannot run.
    """
    rnd = np.random.RandomState(0)
    # Define the generalized eigenvalue problem Av = cBv
    # where (c, v) is a generalized eigenpair,
    # and where we choose A  and B to be diagonal.
    vals = np.arange(1, n + 1)

    # list_sparse_format = ['bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil']
    list_sparse_format = ['coo']
    sparse_formats = len(list_sparse_format)
    for s_f_i, s_f in enumerate(list_sparse_format):

        As64 = diags([vals * vals], [0], (n, n), format=s_f)
        As32 = As64.astype(np.float32)
        Af64 = As64.toarray()
        Af32 = Af64.astype(np.float32)

        def As32f(x):
            return As32 @ x
        As32LO = LinearOperator(matvec=As32f,
                                matmat=As32f,
                                shape=(n, n),
                                dtype=As32.dtype)

        listA = [Af64, As64, Af32, As32, As32f, As32LO, lambda v: As32 @ v]

        Bs64 = diags([vals], [0], (n, n), format=s_f)
        Bf64 = Bs64.toarray()
        Bs32 = Bs64.astype(np.float32)

        def Bs32f(x):
            return Bs32 @ x
        Bs32LO = LinearOperator(matvec=Bs32f,
                                matmat=Bs32f,
                                shape=(n, n),
                                dtype=Bs32.dtype)
        listB = [Bf64, Bs64, Bs32, Bs32f, Bs32LO, lambda v: Bs32 @ v]

        # Define the preconditioner function as LinearOperator.
        Ms64 = diags([1./vals], [0], (n, n), format=s_f)

        def Ms64precond(x):
            return Ms64 @ x
        Ms64precondLO = LinearOperator(matvec=Ms64precond,
                                       matmat=Ms64precond,
                                       shape=(n, n),
                                       dtype=Ms64.dtype)
        Mf64 = Ms64.toarray()

        def Mf64precond(x):
            return Mf64 @ x
        Mf64precondLO = LinearOperator(matvec=Mf64precond,
                                       matmat=Mf64precond,
                                       shape=(n, n),
                                       dtype=Mf64.dtype)
        Ms32 = Ms64.astype(np.float32)

        def Ms32precond(x):
            return Ms32 @ x
        Ms32precondLO = LinearOperator(matvec=Ms32precond,
                                       matmat=Ms32precond,
                                       shape=(n, n),
                                       dtype=Ms32.dtype)
        Mf32 = Ms32.toarray()

        def Mf32precond(x):
            return Mf32 @ x
        Mf32precondLO = LinearOperator(matvec=Mf32precond,
                                       matmat=Mf32precond,
                                       shape=(n, n),
                                       dtype=Mf32.dtype)
        listM = [None, Ms64, Ms64precondLO, Mf64precondLO, Ms64precond,
                 Ms32, Ms32precondLO, Mf32precondLO, Ms32precond]

        # Setup matrix of the initial approximation to the eigenvectors
        # (cannot be sparse array).
        Xf64 = rnd.random((n, m))
        Xf32 = Xf64.astype(np.float32)
        listX = [Xf64, Xf32]

        # Require that the returned eigenvectors be in the orthogonal complement
        # of the first few standard basis vectors (cannot be sparse array).
        m_excluded = 3
        Yf64 = np.eye(n, m_excluded, dtype=float)
        Yf32 = np.eye(n, m_excluded, dtype=np.float32)
        listY = [Yf64, Yf32]

        tests = list(itertools.product(listA, listB, listM, listX, listY))
        # This is one of the slower tests because there are >1,000 configs
        # to test here, instead of checking product of all input, output types
        # test each configuration for the first sparse format, and then
        # for one additional sparse format. this takes 2/7=30% as long as
        # testing all configurations for all sparse formats.
        if s_f_i > 0:
            tests = tests[s_f_i - 1::sparse_formats-1]

        for A, B, M, X, Y in tests:
            eigvals, _ = lobpcg(A, X, B=B, M=M, Y=Y, tol=1e-4,
                                maxiter=100, largest=False)
            assert_allclose(eigvals,
                            np.arange(1 + m_excluded, 1 + m_excluded + m),
                            atol=1e-5)
