[Python] What is wrong with my block GMRES implementation?

Discussão em 'Python' iniciado por Stack, Julho 23, 2025.

  1. Stack

    Stack Membro Participativo

    I'm trying to implement a block GMRES procedure (i.e., GMRES to solve Ax=b, but with b that is not a vector but a n x r matrix, where r << n). My goal is to have a first implementation in Python (where I can compare to the scipy implementation) to be able to translate it in C++ later.

    I have a simple code for GMRES (with restart) that is working quite well:

    def gmres_restart(A, b, x0=None, tol=1e-5, max_iters=100, restart_dim=30):
    n = len(b)
    if x0 is None:
    x = np.zeros(n)
    else:
    x = x0.copy()

    r = b - A @ x
    beta = np.linalg.norm(r)
    if beta < tol:
    return x

    for outer_it in range(0, max_iters, restart_dim):
    # Arnoldi process
    V = np.zeros((n, restart_dim + 1))
    H = np.zeros((restart_dim + 1, restart_dim))
    V[:, 0] = r / beta

    for j in range(restart_dim):
    w = A @ V[:, j]
    for i in range(j + 1):
    H[i, j] = np.dot(V[:, i], w)
    w -= H[i, j] * V[:, i]
    H[j + 1, j] = np.linalg.norm(w)

    if H[j + 1, j] < 1e-14:
    break

    V[:, j + 1] = w / H[j + 1, j]

    # Solve least squares:
    e1 = np.zeros(j + 2)
    e1[0] = beta
    H_small = H[:j + 2, :j + 1]
    y, *_ = np.linalg.lstsq(H_small, e1)

    # Update solution
    x += V[:, :j + 1] @ y

    # New residual
    r = b - A @ x
    beta = np.linalg.norm(r)
    print(f"GMRES iter {outer_it + restart_dim}, residual = {beta:.2e}")
    if beta < tol:
    print("Converged.")
    return x

    print("Did not converge.")
    return x


    It uses least-square fit, as described in Wikipedia.

    "Translating" this code to block GMRES requires to use block Arnoldi, which I implemented like this:

    def block_arnoldi(A, V0, m):
    n, s = V0.shape
    V = np.zeros((n, s * (m + 1)))
    H = np.zeros((s * (m + 1), s * m))

    # Normalize the initial block
    Q, _ = np.linalg.qr(V0, mode='reduced')
    V[:, :s] = Q

    for j in range(m):
    # Compute W = A * V_j
    Vj = V[:, j * s : (j + 1) * s]
    Wj = A @ Vj

    # Orthogonalize against previous blocks
    for i in range(j + 1):
    Vi = V[:, i * s : (i + 1) * s]
    Hij = Vi.T @ Wj
    H[i * s : (i + 1) * s, j * s : (j + 1) * s] = Hij
    Wj -= Vi @ Hij

    # QR decomposition to get next block of orthonormal vectors
    Q, R = np.linalg.qr(Wj, mode='reduced')
    V[:, (j + 1) * s : (j + 2) * s] = Q
    H[(j + 1) * s : (j + 2) * s, j * s : (j + 1) * s] = R

    return V, H


    For those who have it, this is Algorithm 6.8 of "NUMERICAL METHODS FOR LARGE EIGENVALUE PROBLEMS (second edition)" by Y. Saad (page 146).

    And then, here is my implementation for block GMRES:

    def block_gmres_restart(A, B, X0=None, tol=1e-5, max_iters=100, restart_dim=30):
    n, s = B.shape
    if X0 is None:
    X = np.zeros((n, s))
    else:
    X = X0.copy()

    R = B - A @ X
    beta = np.linalg.norm(R, ord='fro')

    if beta < tol:
    return X

    for outer in range(0, max_iters, restart_dim):
    V, H = block_arnoldi(A, R, restart_dim)

    # Solve least-squares
    e1 = np.zeros(((restart_dim + 1) * s, s))
    e1[:s, :] = np.linalg.norm(R, axis=0).reshape(-1, 1) * np.eye(s)
    H_small = H[:(restart_dim + 1) * s, :restart_dim * s]
    Y, *_ = np.linalg.lstsq(H_small, e1)

    # Update solution
    X += V[:, :restart_dim * s] @ Y

    # New residual
    R = B - A @ X
    res_norm = np.linalg.norm(R, ord='fro')
    print(f"Block GMRES iter {outer + restart_dim}, residual = {res_norm:.2e}")

    if res_norm < tol:
    print("Converged.")
    return X

    print("Did not converge.")
    return X


    The problem is the following: it never works for r > 1, and it sometimes works for r = 1. For example, this works:

    np.random.seed(42) # fix randomness

    n = 20
    A = np.diag(np.linspace(1, 100, n)) + 0.001 * np.random.randn(n, n)
    A = (A + A.T) / 2 # Ensure symmetry

    b = np.random.randn(n, 1)

    x = gmres_restart(A, b[:, 0], restart_dim=10)
    x = block_gmres_restart(A, b, restart_dim=10)


    ... But when I set n=30, block_gmres_restart does not converge, while gmres_restart does (residual increase instead of decreasing).

    My understanding at the moment is that X += V[:, :restart_dim * s] @ Y should sometimes be X -= V[:, :restart_dim * s] @ Y. Would anyone have an idea why?

    Continue reading...

Compartilhe esta Página