Source code for pyamg.aggregation.adaptive

"""Adaptive Smoothed Aggregation."""

from warnings import warn
import numpy as np
from scipy.sparse import csr_matrix, bsr_matrix, isspmatrix_csr, \
    isspmatrix_csc, isspmatrix_bsr, eye, SparseEfficiencyWarning

from ..multilevel import MultilevelSolver
from ..strength import symmetric_strength_of_connection, \
    classical_strength_of_connection, evolution_strength_of_connection
from ..krylov import gmres
from ..util.linalg import norm, approximate_spectral_radius
from ..util.utils import amalgamate, levelize_strength_or_aggregation, \
    levelize_smooth_or_improve_candidates
from ..relaxation.smoothing import change_smoothers, rho_D_inv_A
from ..relaxation.relaxation import gauss_seidel, gauss_seidel_nr, \
    gauss_seidel_ne, gauss_seidel_indexed, jacobi, polynomial
from .aggregation import smoothed_aggregation_solver
from .aggregate import standard_aggregation, lloyd_aggregation
from .smooth import jacobi_prolongation_smoother, \
    energy_prolongation_smoother, richardson_prolongation_smoother
from .tentative import fit_candidates


def eliminate_local_candidates(x, AggOp, A, T, thresh=1.0, **kwargs):
    """Eliminate candidates locally.

    Helper function that determines where to eliminate candidates locally
    on a per aggregate basis.

    Parameters
    ----------
    x : array
        n x 1 vector of new candidate
    AggOp : CSR or CSC sparse matrix
        Aggregation operator for the level that x was generated for
    A : sparse matrix
        Operator for the level that x was generated for
    T : sparse matrix
        Tentative prolongation operator for the level that x was generated for
    thresh : scalar
        Constant threshold parameter to decide when to drop candidates

    Returns
    -------
    Nothing, x is modified in place

    """
    if not (isspmatrix_csr(AggOp) or isspmatrix_csc(AggOp)):
        raise TypeError('AggOp must be a CSR or CSC matrix')

    if kwargs:  # process any needed kwargs for elimination
        pass

    AggOp = AggOp.tocsc()
    ndof = max(x.shape)
    npde = int(ndof/AggOp.shape[0])

    def aggregate_wise_inner_product(z, AggOp, npde, ndof):
        """Inner products per aggregate.

        Helper function that calculates <z, z>_i, i.e., the
        inner product of z only over aggregate i
        Returns a vector of length num_aggregates where entry i is <z, z>_i

        """
        z = np.ravel(z)*np.ravel(z)
        innerp = np.zeros((1, AggOp.shape[1]), dtype=z.dtype)
        for j in range(npde):
            innerp += z[slice(j, ndof, npde)].reshape(1, -1) * AggOp

        return innerp.reshape(-1, 1)

    def get_aggregate_weights(AggOp, A, z, npde, ndof):
        """Weights per aggregate.

        Calculate local aggregate quantities
        Return a vector of length num_aggregates where entry i is
        (card(agg_i)/A.shape[0]) ( <Az, z>/rho(A) )

        """
        _ = ndof
        rho = approximate_spectral_radius(A)
        zAz = np.dot(z.reshape(1, -1), A*z.reshape(-1, 1))
        card = npde * (AggOp.indptr[1:]-AggOp.indptr[:-1])
        weights = (np.ravel(card)*zAz)/(A.shape[0]*rho)
        return weights.reshape(-1, 1)

    # Run test 1, which finds where x is small relative to its energy
    weights = thresh * get_aggregate_weights(AggOp, A, x, npde, ndof)
    mask1 = aggregate_wise_inner_product(x, AggOp, npde, ndof) <= weights

    # Run test 2, which finds where x is already approximated
    # accurately by the existing T
    projected_x = x - T*(T.T*x)
    mask2 = aggregate_wise_inner_product(projected_x,
                                         AggOp, npde, ndof) <= weights

    # Combine masks and zero out corresponding aggregates in x
    mask = np.ravel(mask1 + mask2).nonzero()[0]
    if mask.shape[0] > 0:
        mask = npde * AggOp[:, mask].indices
        for j in range(npde):
            x[mask+j] = 0.0


def unpack_arg(v):
    """Use for local methods."""
    if isinstance(v, tuple):
        return v[0], v[1]
    return v, {}


[docs] def adaptive_sa_solver(A, initial_candidates=None, symmetry='hermitian', pdef=True, num_candidates=1, candidate_iters=5, improvement_iters=0, epsilon=0.1, max_levels=10, max_coarse=10, aggregate='standard', prepostsmoother=('gauss_seidel', {'sweep': 'symmetric'}), smooth=('jacobi', {}), strength='symmetric', coarse_solver='pinv', eliminate_local=(False, {'thresh': 1.0}), keep=False, **kwargs): """Create a multilevel solver using Adaptive Smoothed Aggregation (aSA). Parameters ---------- A : csr_matrix, bsr_matrix Square matrix in CSR or BSR format initial_candidates : None, n x m dense matrix If a matrix, then this forms the basis for the first m candidates. Also in this case, the initial setup stage is skipped, because this provides the first candidate(s). If None, then a random initial guess and relaxation are used to inform the initial candidate. symmetry : string 'symmetric' refers to both real and complex symmetric 'hermitian' refers to both complex Hermitian and real Hermitian Note that for the strictly real case, these two options are the same Note that this flag does not denote definiteness of the operator pdef : bool True or False, whether A is known to be positive definite. num_candidates : integer Number of near-nullspace candidates to generate candidate_iters : integer Number of smoothing passes/multigrid cycles used at each level of the adaptive setup phase improvement_iters : integer Number of times each candidate is improved epsilon : float Target convergence factor max_levels : integer Maximum number of levels to be used in the multilevel solver. max_coarse : integer Maximum number of variables permitted on the coarse grid. prepostsmoother : string or dict Pre- and post-smoother used in the adaptive method strength : ['symmetric', 'classical', 'evolution', None] Method used to determine the strength of connection between unknowns of the linear system. See smoothed_aggregation_solver(...) documentation. Predefined strength may be used with ('predefined', {'C': csr_matrix}). aggregate : ['standard', 'lloyd', 'naive', ('predefined', {'AggOp': csr_matrix})] Method used to aggregate nodes. See smoothed_aggregation_solver(...) documentation. smooth : ['jacobi', 'richardson', 'energy', None] Method used used to smooth the tentative prolongator. See smoothed_aggregation_solver(...) documentation coarse_solver : ['splu', 'lu', 'cholesky, 'pinv', 'gauss_seidel', ... ] Solver used at the coarsest level of the MG hierarchy. Optionally, may be a tuple (fn, args), where fn is a string such as ['splu', 'lu', ...] or a callable function, and args is a dictionary of arguments to be passed to fn. eliminate_local : tuple Length 2 tuple. If the first entry is True, then eliminate candidates where they aren't needed locally, using the second entry of the tuple to contain arguments to local elimination routine. Given the rigid sparse data structures, this doesn't help much, if at all, with complexity. Its more of a diagnostic utility. keep : bool Flag to indicate keeping extra operators in the hierarchy for diagnostics. For example, if True, then strength of connection (C), tentative prolongation (T), and aggregation (AggOp) are kept. Returns ------- MultilevelSolver : MultilevelSolver Smoothed aggregation solver with adaptively generated candidates Notes ----- - Floating point value representing the 'work' required to generate the solver. This value is the total cost of just relaxation, relative to the fine grid. The relaxation method used is assumed to symmetric Gauss-Seidel. - Unlike the standard Smoothed Aggregation (SA) method, adaptive SA does not require knowledge of near-nullspace candidate vectors. Instead, an adaptive procedure computes one or more candidates 'from scratch'. This approach is useful when no candidates are known or the candidates have been invalidated due to changes to matrix A. Examples -------- >>> from pyamg.gallery import stencil_grid >>> from pyamg.aggregation import adaptive_sa_solver >>> import numpy as np >>> A=stencil_grid([[-1,-1,-1],[-1,8.0,-1],[-1,-1,-1]], (31,31),format='csr') >>> [asa,work] = adaptive_sa_solver(A,num_candidates=1) >>> res=[] >>> x=asa.solve(b=np.ones((A.shape[0],)), x0=np.ones((A.shape[0],)), residuals=res) References ---------- .. [1] Brezina, Falgout, MacLachlan, Manteuffel, McCormick, and Ruge "Adaptive Smoothed Aggregation (alpha SA) Multigrid" SIAM Review Volume 47, Issue 2 (2005) """ if not (isspmatrix_csr(A) or isspmatrix_bsr(A)): try: A = csr_matrix(A) warn('Implicit conversion of A to CSR', SparseEfficiencyWarning) except Exception as e: raise TypeError('Argument A must have type csr_matrix or ' 'bsr_matrix, or be convertible to csr_matrix') from e A = A.asfptype() if A.shape[0] != A.shape[1]: raise ValueError('expected square matrix') # Track work in terms of relaxation work = np.zeros((1,)) # Levelize the user parameters, so that they become lists describing the # desired user option on each level. max_levels, max_coarse, strength =\ levelize_strength_or_aggregation(strength, max_levels, max_coarse) max_levels, max_coarse, aggregate =\ levelize_strength_or_aggregation(aggregate, max_levels, max_coarse) smooth = levelize_smooth_or_improve_candidates(smooth, max_levels) # Develop initial candidate(s). Note that any predefined aggregation is # preserved. if initial_candidates is None: B, aggregate, strength =\ initial_setup_stage(A, symmetry, pdef, candidate_iters, epsilon, max_levels, max_coarse, aggregate, prepostsmoother, smooth, strength, work) # Normalize B B = (1.0/norm(B, 'inf')) * B num_candidates -= 1 else: # Otherwise, use predefined candidates B = initial_candidates num_candidates -= B.shape[1] # Generate Aggregation and Strength Operators (the brute force way) sa = smoothed_aggregation_solver(A, B=B, symmetry=symmetry, presmoother=prepostsmoother, postsmoother=prepostsmoother, smooth=smooth, strength=strength, max_levels=max_levels, max_coarse=max_coarse, aggregate=aggregate, coarse_solver=coarse_solver, improve_candidates=None, keep=True, **kwargs) if len(sa.levels) > 1: # Set strength-of-connection and aggregation aggregate = [('predefined', {'AggOp': sa.levels[i].AggOp.tocsr()}) for i in range(len(sa.levels) - 1)] strength = [('predefined', {'C': sa.levels[i].C.tocsr()}) for i in range(len(sa.levels) - 1)] # Develop additional candidates for i in range(num_candidates): x = general_setup_stage( smoothed_aggregation_solver(A, B=B, symmetry=symmetry, presmoother=prepostsmoother, postsmoother=prepostsmoother, smooth=smooth, coarse_solver=coarse_solver, aggregate=aggregate, strength=strength, improve_candidates=None, keep=True, **kwargs), symmetry, candidate_iters, prepostsmoother, smooth, eliminate_local, coarse_solver, work) # Normalize x and add to candidate list x = x/norm(x, 'inf') if np.isinf(x[0]) or np.isnan(x[0]): raise ValueError(f'The {i}th adaptive candidate is all 0.') B = np.hstack((B, x.reshape(-1, 1))) # Improve candidates if B.shape[1] > 1 and improvement_iters > 0: b = np.zeros((A.shape[0], 1), dtype=A.dtype) for _i in range(improvement_iters): for j in range(B.shape[1]): # Run a V-cycle built on everything except candidate j, while # using candidate j as the initial guess x0 = B[:, 0] B = B[:, 1:] sa_temp =\ smoothed_aggregation_solver(A, B=B, symmetry=symmetry, presmoother=prepostsmoother, postsmoother=prepostsmoother, smooth=smooth, coarse_solver=coarse_solver, aggregate=aggregate, strength=strength, improve_candidates=None, keep=True, **kwargs) x = sa_temp.solve(b, x0=x0, tol=1e-20, maxiter=candidate_iters, cycle='V') work[:] += 2 * sa_temp.operator_complexity() *\ sa_temp.levels[0].A.nnz * candidate_iters # Apply local elimination elim, elim_kwargs = unpack_arg(eliminate_local) if elim is True: x = x/norm(x, 'inf') eliminate_local_candidates(x, sa_temp.levels[0].AggOp, A, sa_temp.levels[0].T, **elim_kwargs) # Normalize x and add to candidate list x = x/norm(x, 'inf') if np.isinf(x[0]) or np.isnan(x[0]): raise ValueError(f'The {j}th improved adaptive candidate is all 0.') B = np.hstack((B, x.reshape(-1, 1))) elif improvement_iters > 0: # Special case for improving a single candidate max_levels = len(aggregate) + 1 max_coarse = 0 for _i in range(improvement_iters): B, aggregate, strength =\ initial_setup_stage(A, symmetry, pdef, candidate_iters, epsilon, max_levels, max_coarse, aggregate, prepostsmoother, smooth, strength, work, initial_candidate=B) # Normalize B B = (1.0/norm(B, 'inf'))*B return [smoothed_aggregation_solver(A, B=B, symmetry=symmetry, presmoother=prepostsmoother, postsmoother=prepostsmoother, smooth=smooth, coarse_solver=coarse_solver, aggregate=aggregate, strength=strength, improve_candidates=None, keep=keep, **kwargs), work[0]/A.nnz]
def initial_setup_stage(A, symmetry, pdef, candidate_iters, epsilon, max_levels, max_coarse, aggregate, prepostsmoother, smooth, strength, work, initial_candidate=None): """Compute aggregation and the first near-nullspace candidate. Parameters ---------- candidate_iters number of test relaxation iterations epsilon minimum acceptable relaxation convergence factor Notes ----- This follows Algorithm 3 in Brezina et al. References ---------- .. [1] Brezina, Falgout, MacLachlan, Manteuffel, McCormick, and Ruge "Adaptive Smoothed Aggregation (aSA) Multigrid" SIAM Review Volume 47, Issue 2 (2005) http://www.cs.umn.edu/~maclach/research/aSA2.pdf """ # Define relaxation routine def relax(A, x): fn, _ = unpack_arg(prepostsmoother) if fn == 'gauss_seidel': gauss_seidel(A, x, np.zeros_like(x), iterations=candidate_iters, sweep='symmetric') elif fn == 'gauss_seidel_nr': gauss_seidel_nr(A, x, np.zeros_like(x), iterations=candidate_iters, sweep='symmetric') elif fn == 'gauss_seidel_ne': gauss_seidel_ne(A, x, np.zeros_like(x), iterations=candidate_iters, sweep='symmetric') elif fn == 'jacobi': jacobi(A, x, np.zeros_like(x), iterations=1, omega=1.0 / rho_D_inv_A(A)) elif fn == 'richardson': polynomial(A, x, np.zeros_like(x), iterations=1, coefficients=[1.0/approximate_spectral_radius(A)]) elif fn == 'gmres': x[:] = (gmres(A, np.zeros_like(x), x0=x, maxiter=candidate_iters)[0]).reshape(x.shape) else: raise TypeError('Unrecognized smoother') # flag for skipping steps f-i in step 4 skip_f_to_i = True # step 1 A_l = A if initial_candidate is None: x = np.random.rand(A_l.shape[0], 1).astype(A_l.dtype) # The following type check matches the usual 'complex' type, # but also numpy data types such as 'complex64', 'complex128' # and 'complex256'. if A_l.dtype.name.startswith('complex'): x = x + 1.0j*np.random.rand(A_l.shape[0], 1) else: x = np.array(initial_candidate, dtype=A_l.dtype) # step 2 relax(A_l, x) work[:] += A_l.nnz * candidate_iters*2 # step 3 # not advised to stop the iteration here: often the first relaxation pass # _is_ good, but the remaining passes are poor # if x_A_x/x_A_x_old < epsilon: # # relaxation alone is sufficient # print 'relaxation alone works: %g'%(x_A_x/x_A_x_old) # return x, [] # step 4 As = [A] xs = [x] Ps = [] AggOps = [] StrengthOps = [] while A.shape[0] > max_coarse and max_levels > 1: # The real check to break from the while loop is below # Begin constructing next level fn, kwargs = unpack_arg(strength[len(As)-1]) # step 4b if fn == 'symmetric': C_l = symmetric_strength_of_connection(A_l, **kwargs) # Diagonal must be nonzero C_l = C_l + eye(C_l.shape[0], C_l.shape[1], format='csr') elif fn == 'classical': C_l = classical_strength_of_connection(A_l, **kwargs) # Diagonal must be nonzero C_l = C_l + eye(C_l.shape[0], C_l.shape[1], format='csr') if isspmatrix_bsr(A_l): C_l = amalgamate(C_l, A_l.blocksize[0]) elif fn in ('ode', 'evolution'): C_l = evolution_strength_of_connection(A_l, np.ones( (A_l.shape[0], 1), dtype=A.dtype), **kwargs) elif fn == 'predefined': C_l = kwargs['C'].tocsr() elif fn is None: C_l = A_l.tocsr() else: raise ValueError(f'Unrecognized strength of connection method: {str(fn)}') # In SA, strength represents 'distance', so we take magnitude of # complex values if C_l.dtype.name.startswith('complex'): C_l.data = np.abs(C_l.data) # Create a unified strength framework so that large values represent # strong connections and small values represent weak connections if fn in ('ode', 'evolution', 'energy_based'): C_l.data = 1.0 / C_l.data # aggregation fn, kwargs = unpack_arg(aggregate[len(As) - 1]) if fn == 'standard': AggOp = standard_aggregation(C_l, **kwargs)[0] elif fn == 'lloyd': AggOp = lloyd_aggregation(C_l, **kwargs)[0] elif fn == 'predefined': AggOp = kwargs['AggOp'].tocsr() else: raise ValueError(f'Unrecognized aggregation method {str(fn)}') T_l, x = fit_candidates(AggOp, x) # step 4c fn, kwargs = unpack_arg(smooth[len(As)-1]) # step 4d if fn == 'jacobi': P_l = jacobi_prolongation_smoother(A_l, T_l, C_l, x, **kwargs) elif fn == 'richardson': P_l = richardson_prolongation_smoother(A_l, T_l, **kwargs) elif fn == 'energy': P_l = energy_prolongation_smoother(A_l, T_l, C_l, x, None, (False, {}), **kwargs) elif fn is None: P_l = T_l else: raise ValueError(f'Unrecognized prolongation smoother method: {str(fn)}') # R should reflect A's structure # step 4e if symmetry == 'symmetric': A_l = P_l.T.asformat(P_l.format) * A_l * P_l elif symmetry == 'hermitian': A_l = P_l.H.asformat(P_l.format) * A_l * P_l else: raise ValueError(f'aSA not implemented for symmetry={symmetry}.') StrengthOps.append(C_l) AggOps.append(AggOp) Ps.append(P_l) As.append(A_l) # skip to step 5 as in step 4e if (A_l.shape[0] <= max_coarse) or (len(AggOps) + 1 >= max_levels): break if not skip_f_to_i: x_hat = x.copy() # step 4g relax(A_l, x) # step 4h work[:] += A_l.nnz*candidate_iters*2 if pdef is True: x_A_x = np.dot(np.conjugate(x).T, A_l*x) xhat_A_xhat = np.dot(np.conjugate(x_hat).T, A_l*x_hat) err_ratio = (x_A_x/xhat_A_xhat)**(1.0/candidate_iters) else: # use A.H A inner-product Ax = A_l * x # Axhat = A_l * x_hat x_A_x = np.dot(np.conjugate(Ax).T, Ax) xhat_A_xhat = np.dot(np.conjugate(x_hat).T, A_l*x_hat) err_ratio = (x_A_x/xhat_A_xhat)**(1.0/candidate_iters) if err_ratio < epsilon: # step 4i # print 'sufficient convergence, skipping' skip_f_to_i = True if x_A_x == 0: x = x_hat # need to restore x else: # just carry out relaxation, don't check for convergence relax(A_l, x) # step 4h work[:] += 2 * A_l.nnz * candidate_iters # store xs for diagnostic use and for use in step 5 xs.append(x) # step 5 # Extend coarse-level candidate to the finest level # --> note that we start with the x from the second coarsest level x = xs[-1] # make sure that xs[-1] has been relaxed by step 4h, i.e. relax(As[-2], x) for lev in range(len(Ps)-2, -1, -1): # lev = coarsest ... finest-1 P = Ps[lev] # I: lev --> lev+1 A = As[lev] # A on lev+1 x = P * x relax(A, x) work[:] += A.nnz*candidate_iters*2 # Set predefined strength of connection and aggregation if len(AggOps) > 1: aggregate = [('predefined', {'AggOp': AggOps[i]}) for i in range(len(AggOps))] strength = [('predefined', {'C': StrengthOps[i]}) for i in range(len(StrengthOps))] return x, aggregate, strength # first candidate def general_setup_stage(ml, symmetry, candidate_iters, prepostsmoother, smooth, eliminate_local, coarse_solver, work): """Compute additional candidates and improvements. Parameters ---------- candidate_iters number of test relaxation iterations epsilon minimum acceptable relaxation convergence factor Notes ----- Follows Algorithm 4 in Brezina et al. References ---------- .. [1] Brezina, Falgout, MacLachlan, Manteuffel, McCormick, and Ruge "Adaptive Smoothed Aggregation (alphaSA) Multigrid" SIAM Review Volume 47, Issue 2 (2005) http://www.cs.umn.edu/~maclach/research/aSA2.pdf """ def make_bridge(T): M, N = T.shape K = T.blocksize[0] bnnz = T.indptr[-1] # the K+1 represents the new dof introduced by the new candidate. the # bridge 'T' ignores this new dof and just maps zeros there data = np.zeros((bnnz, K+1, K), dtype=T.dtype) data[:, :-1, :] = T.data return bsr_matrix((data, T.indices, T.indptr), shape=((K + 1) * int(M / K), N)) def expand_candidates(B_old, nodesize): # pylint: disable=unused-variable # insert a new dof that is always zero, to create NullDim+1 dofs per # node in B NullDim = B_old.shape[1] nnodes = int(B_old.shape[0] / nodesize) Bnew = np.zeros((nnodes, nodesize+1, NullDim), dtype=B_old.dtype) Bnew[:, :-1, :] = B_old.reshape(nnodes, nodesize, NullDim) return Bnew.reshape(-1, NullDim) levels = ml.levels x = np.random.rand(levels[0].A.shape[0], 1) if levels[0].A.dtype.name.startswith('complex'): x = x + 1.0j*np.random.rand(levels[0].A.shape[0], 1) b = np.zeros_like(x) x = ml.solve(b, x0=x, tol=1e-20, maxiter=candidate_iters) work[:] += ml.operator_complexity()*ml.levels[0].A.nnz*candidate_iters*2 T0 = levels[0].T.copy() # TEST FOR CONVERGENCE HERE for i in range(len(ml.levels) - 2): # alpha-SA paper does local elimination here, but after talking # to Marian, its not clear that this helps things # fn, kwargs = unpack_arg(eliminate_local) # if fn == True: # eliminate_local_candidates(x,levels[i].AggOp,levels[i].A, # levels[i].T, **kwargs) # add candidate to B B = np.hstack((levels[i].B, x.reshape(-1, 1))) # construct Ptent T, R = fit_candidates(levels[i].AggOp, B) levels[i].T = T x = R[:, -1].reshape(-1, 1) # smooth P fn, kwargs = unpack_arg(smooth[i]) if fn == 'jacobi': levels[i].P = jacobi_prolongation_smoother(levels[i].A, T, levels[i].C, R, **kwargs) elif fn == 'richardson': levels[i].P = richardson_prolongation_smoother(levels[i].A, T, **kwargs) elif fn == 'energy': levels[i].P = energy_prolongation_smoother(levels[i].A, T, levels[i].C, R, None, (False, {}), **kwargs) x = R[:, -1].reshape(-1, 1) elif fn is None: levels[i].P = T else: raise ValueError(f'Unrecognized prolongation smoother method: {str(fn)}') # construct R if symmetry == 'symmetric': # R should reflect A's structure levels[i].R = levels[i].P.T.asformat(levels[i].P.format) elif symmetry == 'hermitian': levels[i].R = levels[i].P.H.asformat(levels[i].P.format) # construct coarse A levels[i+1].A = levels[i].R * levels[i].A * levels[i].P # construct bridging P T_bridge = make_bridge(levels[i+1].T) R_bridge = levels[i+2].B # smooth bridging P fn, kwargs = unpack_arg(smooth[i+1]) if fn == 'jacobi': levels[i+1].P = jacobi_prolongation_smoother(levels[i+1].A, T_bridge, levels[i+1].C, R_bridge, **kwargs) elif fn == 'richardson': levels[i+1].P = richardson_prolongation_smoother(levels[i+1].A, T_bridge, **kwargs) elif fn == 'energy': levels[i+1].P = energy_prolongation_smoother(levels[i+1].A, T_bridge, levels[i+1].C, R_bridge, None, (False, {}), **kwargs) elif fn is None: levels[i+1].P = T_bridge else: raise ValueError(f'Unrecognized prolongation smoother method {str(fn)}') # construct the 'bridging' R if symmetry == 'symmetric': # R should reflect A's structure levels[i+1].R = levels[i+1].P.T.asformat(levels[i+1].P.format) elif symmetry == 'hermitian': levels[i+1].R = levels[i+1].P.H.asformat(levels[i+1].P.format) # run solver on candidate solver = MultilevelSolver(levels[i+1:], coarse_solver=coarse_solver) change_smoothers(solver, presmoother=prepostsmoother, postsmoother=prepostsmoother) x = solver.solve(np.zeros_like(x), x0=x, tol=1e-20, maxiter=candidate_iters) work[:] += 2 * solver.operator_complexity() * solver.levels[0].A.nnz *\ candidate_iters*2 # update values on next level levels[i+1].B = R[:, :-1].copy() levels[i+1].T = T_bridge # note that we only use the x from the second coarsest level fn, kwargs = unpack_arg(prepostsmoother) for lvl in reversed(levels[:-2]): x = lvl.P * x work[:] += lvl.A.nnz*candidate_iters*2 if fn == 'gauss_seidel': # only relax at nonzeros, so as not to mess up any locally dropped # candidates indices = np.ravel(x).nonzero()[0] gauss_seidel_indexed(lvl.A, x, np.zeros_like(x), indices, iterations=candidate_iters, sweep='symmetric') elif fn == 'gauss_seidel_ne': gauss_seidel_ne(lvl.A, x, np.zeros_like(x), iterations=candidate_iters, sweep='symmetric') elif fn == 'gauss_seidel_nr': gauss_seidel_nr(lvl.A, x, np.zeros_like(x), iterations=candidate_iters, sweep='symmetric') elif fn == 'jacobi': jacobi(lvl.A, x, np.zeros_like(x), iterations=1, omega=1.0 / rho_D_inv_A(lvl.A)) elif fn == 'richardson': polynomial(lvl.A, x, np.zeros_like(x), iterations=1, coefficients=[1.0/approximate_spectral_radius(lvl.A)]) elif fn == 'gmres': x[:] = (gmres(lvl.A, np.zeros_like(x), x0=x, maxiter=candidate_iters)[0]).reshape(x.shape) else: raise TypeError('Unrecognized smoother') # x will be dense again, so we have to drop locally again elim, elim_kwargs = unpack_arg(eliminate_local) if elim is True: x = x/norm(x, 'inf') eliminate_local_candidates(x, levels[0].AggOp, levels[0].A, T0, **elim_kwargs) return x.reshape(-1, 1)