def lanczos(matrix, guesses, n_ep, max_subspace=None, conv_tol=1e-9, which="LM", max_iter=100, callback=None, debug_checks=False, explicit_symmetrisation=IndexSymmetrisation, min_subspace=None): """Lanczos eigensolver for ADC problems Parameters ---------- matrix ADC matrix instance guesses : list Guess vectors (fixes also the Lanczos block size) n_ep : int Number of eigenpairs to be computed max_subspace : int or NoneType, optional Maximal subspace size conv_tol : float, optional Convergence tolerance on the l2 norm squared of residuals to consider them converged which : str, optional Which eigenvectors to converge to (e.g. LM, LA, SM, SA) max_iter : int, optional Maximal number of iterations callback : callable, optional Callback to run after each iteration debug_checks : bool, optional Enable some potentially costly debug checks (Loss of orthogonality etc.) explicit_symmetrisation : optional Explicit symmetrisation to use after orthogonalising the subspace vectors. Allows to correct for loss of index or spin symmetries during orthogonalisation (type or instance). min_subspace : int or NoneType, optional Subspace size to collapse to when performing a thick restart. """ if explicit_symmetrisation is not None and \ isinstance(explicit_symmetrisation, type): explicit_symmetrisation = explicit_symmetrisation(matrix) iterator = LanczosIterator(matrix, guesses, explicit_symmetrisation=explicit_symmetrisation) if not isinstance(guesses, list): guesses = [guesses] if not max_subspace: max_subspace = max(2 * n_ep + len(guesses), 20, 8 * len(guesses)) if not min_subspace: min_subspace = n_ep + 2 * len(guesses) if conv_tol < matrix.shape[1] * np.finfo(float).eps: warnings.warn(la.LinAlgWarning( "Convergence tolerance (== {:5.2g}) lower than " "estimated maximal numerical accuracy (== {:5.2g}). " "Convergence might be hard to achieve." "".format(conv_tol, matrix.shape[1] * np.finfo(float).eps) )) return lanczos_iterations(iterator, n_ep, min_subspace, max_subspace, conv_tol, which, max_iter, callback, debug_checks)
def check_orthogonality(self, tolerance=None): if tolerance is None: tolerance = self.n_problem * np.finfo(float).eps orth = np.array([[SSi @ SSj for SSi in self.subspace] for SSj in self.subspace]) orth -= np.eye(len(self.subspace)) orth = np.max(np.abs(orth)) if orth > tolerance: warnings.warn( la.LinAlgWarning("LanczosSubspace has lost orthogonality. " "Expect inaccurate results.")) return orth
def lanczos_iterations(iterator, n_ep, min_subspace, max_subspace, conv_tol=1e-9, which="LA", max_iter=100, callback=None, debug_checks=False, state=None): """Drive the Lanczos iterations Parameters ---------- iterator : LanczosIterator Iterator generating the Lanczos subspace (contains matrix, guess, residual, Ritz pairs from restart, symmetrisation and orthogonalisation) n_ep : int Number of eigenpairs to be computed min_subspace : int Subspace size to collapse to when performing a thick restart. max_subspace : int Maximal subspace size conv_tol : float, optional Convergence tolerance on the l2 norm squared of residuals to consider them converged which : str, optional Which eigenvectors to converge to (e.g. LM, LA, SM, SA) max_iter : int, optional Maximal number of iterations callback : callable, optional Callback to run after each iteration debug_checks : bool, optional Enable some potentially costly debug checks (Loss of orthogonality etc.) """ if callback is None: def callback(state, identifier): pass # TODO For consistency with the Davidson the conv_tol is interpreted # as the residual norm *squared*. Arnoldi, however, uses the actual norm # to check for convergence and so on. See also the comment in Davidson # around the line computing state.residual_norms # # See also the squaring of the residual norms below tol = np.sqrt(conv_tol) if state is None: state = LanczosState(iterator) callback(state, "start") state.timer.restart("iteration") n_applies_offset = 0 else: n_applies_offset = state.n_applies for subspace in iterator: b = subspace.rayleigh_extension with state.timer.record("rayleigh_ritz"): rvals, rvecs = np.linalg.eigh(subspace.subspace_matrix) if debug_checks: eps = np.finfo(float).eps orthotol = max(tol / 1000, subspace.n_problem * eps) orth = subspace.check_orthogonality(orthotol) state.subspace_orthogonality = orth is_rval_converged, eigenpair_error = check_convergence( subspace, rvals, rvecs, tol) # Update state state.n_iter += 1 state.n_applies = subspace.n_applies + n_applies_offset state.converged = False state.eigenvectors = None # Not computed in Lanczos state.subspace_vectors = subspace.subspace state.subspace_residual = subspace.residual epair_mask = select_eigenpairs(rvals, n_ep, which) state.eigenvalues = rvals[epair_mask] state.residual_norms = eigenpair_error[epair_mask] converged = np.all(is_rval_converged[epair_mask]) # TODO For consistency with the Davidson the residual norms are squared # again to give output in the same order of magnitude. state.residual_norms = state.residual_norms**2 callback(state, "next_iter") state.timer.restart("iteration") if converged: state = amend_true_residuals(state, subspace, rvals, rvecs, epair_mask) state.converged = True callback(state, "is_converged") state.timer.stop("iteration") return state if state.n_iter >= max_iter: warnings.warn( la.LinAlgWarning( f"Maximum number of iterations (== {max_iter}) " "reached in lanczos procedure.")) state = amend_true_residuals(state, subspace, rvals, rvecs, epair_mask) state.timer.stop("iteration") state.converged = False return state if len(rvecs) + subspace.n_block > max_subspace: callback(state, "restart") epair_mask = select_eigenpairs(rvals, min_subspace, which) V = subspace.subspace vn, betan = subspace.ortho.qr(subspace.residual) Y = [ lincomb(rvec, V, evaluate=True) for i, rvec in enumerate(np.transpose(rvecs)) if i in epair_mask ] Theta = rvals[epair_mask] Sigma = rvecs[:, epair_mask].T @ b @ betan.T iterator = LanczosIterator( iterator.matrix, vn, ritz_vectors=Y, ritz_values=Theta, ritz_overlaps=Sigma, explicit_symmetrisation=iterator.explicit_symmetrisation) state.n_restart += 1 return lanczos_iterations(iterator, n_ep, min_subspace, max_subspace, conv_tol, which, max_iter, callback, debug_checks, state) state = amend_true_residuals(state, subspace, rvals, rvecs, epair_mask) state.timer.stop("iteration") state.converged = False warnings.warn( la.LinAlgWarning( "Lanczos procedure found maximal subspace possible. Iteration cannot be " "continued like this and will be aborted without convergence. " "Try a different guess.")) return state
def power_method(A, guess, conv_tol=1e-9, max_iter=70, callback=None, explicit_symmetrisation=IndexSymmetrisation): """Use the power iteration to solve for the largest eigenpair of A. The power method is a very simple diagonalisation method, which solves for the (by magnitude) largest eigenvalue of the matrix `A`. Parameters ---------- A Matrix object. Only the `@` operator needs to be implemented. guess Matrix used as a guess conv_tol : float Convergence tolerance on the l2 norm of residuals to consider them converged. max_iter : int Maximal numer of iterations callback Callback function called after each iteration explicit_symmetrisation Explicit symmetrisation to perform during iteration to ensure obtaining an eigenvector with matching symmetry criteria. """ if callback is None: def callback(state, identifier): pass if explicit_symmetrisation is not None and \ isinstance(explicit_symmetrisation, type): explicit_symmetrisation = explicit_symmetrisation(A) x = guess / np.sqrt(guess @ guess) state = PowerMethodState(A) def is_converged(state): return state.residual_norms[0] < conv_tol callback(state, "start") state.timer.restart("power_method/iteration") for i in range(max_iter): state.n_iter += 1 Ax = A @ x state.n_applies += 1 eigval = x @ (Ax) residual = Ax - eigval * x residual_norm = np.sqrt(residual @ residual) state.eigenvalues = np.array([eigval]) state.eigenvectors = np.array([x]) state.residual_norms = np.array([residual_norm]) callback(state, "next_iter") state.timer.restart("power_method/iteration") if is_converged(state): state.converged = True callback(state, "is_converged") state.timer.stop("power_method/iteration") return state if explicit_symmetrisation: x = explicit_symmetrisation.symmetrise(Ax) else: x = Ax x = x / np.sqrt(x @ x) warnings.warn(la.LinAlgWarning( "Power method not converged. Returning intermediate results.")) return state
def davidson_iterations(matrix, state, max_subspace, max_iter, n_ep, is_converged, which, callback=None, preconditioner=None, preconditioning_method="Davidson", debug_checks=False, residual_min_norm=None, explicit_symmetrisation=None): """ @param matrix Matrix to diagonalise @param state DavidsonState containing the eigenvector guess to propagate @param max_subspace Maximal subspace size @param max_iter Maximal numer of iterations @param n_ep Number of eigenpairs to be computed @param is_converged Function to test for convergence @param callback Callback to run after each iteration @param which Which eigenvectors to converge to. Needs to be compatible with the selected preconditioner. @param preconditioner Preconditioner (type or instance) @param preconditioning_method Precondititoning method. Valid values are "Davidson" or "Sleijpen-van-der-Vorst" @param debug_checks Enable some potentially costly debug checks (loss of orthogonality in subspace etc) @param residual_min_norm Minimal norm a residual needs to have in order to be accepted as a new subspace vector (defaults to 2 * len(matrix) * machine_expsilon) @param explicit_symmetrisation Explicit symmetrisation to perform on new subspace vectors before adding them to the subspace. """ if preconditioning_method not in ["Davidson", "Sleijpen-van-der-Vorst"]: raise ValueError("Only 'Davidson' and 'Sleijpen-van-der-Vorst' " "are valid preconditioner methods") if preconditioning_method == "Sleijpen-van-der-Vorst": raise NotImplementedError("Sleijpen-van-der-Vorst preconditioning " "not yet implemented.") if callback is None: def callback(state, identifier): pass # The problem size n_problem = matrix.shape[1] # The block size n_block = len(state.subspace_vectors) # The current subspace size n_ss_vec = n_block # The current subspace SS = state.subspace_vectors # The matrix A projected into the subspace # as a continuous array. Only the view # Ass[:n_ss_vec, :n_ss_vec] contains valid data. Ass_cont = np.empty((max_subspace, max_subspace)) eps = np.finfo(float).eps if residual_min_norm is None: residual_min_norm = 2 * n_problem * eps callback(state, "start") state.timer.restart("iteration") with state.timer.record("projection"): # Initial application of A to the subspace Ax = matrix @ SS state.n_applies += n_ss_vec while state.n_iter < max_iter: state.n_iter += 1 assert len(SS) >= n_block assert len(SS) <= max_subspace # Project A onto the subspace, keeping in mind # that the values Ass[:-n_block, :-n_block] are already valid, # since they have been computed in the previous iterations already. with state.timer.record("projection"): Ass = Ass_cont[:n_ss_vec, :n_ss_vec] # Increase the work view size for i in range(n_block): Ass[:, -n_block + i] = Ax[-n_block + i] @ SS Ass[-n_block:, :] = np.transpose(Ass[:, -n_block:]) # Compute the which(== largest, smallest, ...) eigenpair of Ass # and the associated ritz vector as well as residual with state.timer.record("rayleigh_ritz"): if Ass.shape == (n_block, n_block): rvals, rvecs = la.eigh(Ass) # Do a full diagonalisation else: # TODO Maybe play with precision a little here # TODO Maybe use previous vectors somehow v0 = None rvals, rvecs = sla.eigsh(Ass, k=n_block, which=which, v0=v0) with state.timer.record("residuals"): # Form residuals, A * SS * v - λ * SS * v = Ax * v + SS * (-λ*v) def form_residual(rval, rvec): Axv = linear_combination(rvec, Ax) Axv.add_linear_combination(-rval * rvec, SS) return Axv residuals = [ form_residual(rvals[i], v) for i, v in enumerate(np.transpose(rvecs)) ] assert len(residuals) == n_block # Update the state's eigenpairs and residuals state.eigenvalues = select_eigenpairs(rvals, n_ep, which) state.residuals = select_eigenpairs(residuals, n_ep, which) state.residual_norms = np.array([r @ r for r in state.residuals]) # TODO This is misleading ... actually residual_norms contains # the norms squared. That's also the used e.g. in adcman to # check for convergence, so using the norm squared is fine, # in theory ... it should just be consistent. I think it is # better to go for the actual norm (no squared) inside the code # TODO # The select_eigenpairs is not that great ... better one makes a # function which returns a mask. In that way one can have one # form_residual function, which also returns the formed Ritz vectors # (i.e. our approximations to the eigenpairs) and one which only # forms the residuals ... this would save some duplicate work # in the is_converged section *and* would allow the callback to do # some stuff with the eigenpairs if desired. callback(state, "next_iter") state.timer.restart("iteration") if is_converged(state): # Build the eigenvectors we desire from the subspace vectors: selected = select_eigenpairs(np.transpose(rvecs), n_ep, which) state.eigenvectors = [linear_combination(v, SS) for v in selected] state.converged = True callback(state, "is_converged") state.timer.stop("iteration") return state if state.n_iter == max_iter: raise la.LinAlgError("Maximum number of iterations (== " + str(max_iter) + " reached in davidson " "procedure.") if n_ss_vec + n_block > max_subspace: callback(state, "restart") with state.timer.record("projection"): # The addition of the preconditioned vectors goes beyond max. # subspace size => Collapse first, ie keep current Ritz vectors # as new subspace SS = [linear_combination(v, SS) for v in np.transpose(rvecs)] state.subspace_vectors = SS Ax = [linear_combination(v, Ax) for v in np.transpose(rvecs)] n_ss_vec = len(SS) # Update projection of ADC matrix A onto subspace Ass = Ass_cont[:n_ss_vec, :n_ss_vec] for i in range(n_ss_vec): Ass[:, i] = Ax[i] @ SS # continue to add residuals to space with state.timer.record("preconditioner"): if preconditioner: if hasattr(preconditioner, "update_shifts"): preconditioner.update_shifts(rvals) preconds = preconditioner.apply(residuals) else: preconds = residuals # Explicitly symmetrise the new vectors if requested if explicit_symmetrisation: explicit_symmetrisation.symmetrise(preconds, SS) # Project the components of the preconditioned vectors away # which are already contained in the subspace. # Then add those, which have a significant norm to the subspace. with state.timer.record("orthogonalisation"): n_ss_added = 0 for i in range(n_block): pvec = preconds[i] # Project out the components of the current subspace pvec = pvec - linear_combination(pvec @ SS, SS) pnorm = np.sqrt(pvec @ pvec) if pnorm > residual_min_norm: # Extend the subspace SS.append(pvec / pnorm) n_ss_added += 1 n_ss_vec = len(SS) if debug_checks: orth = np.array([[SS[i] @ SS[j] for i in range(n_ss_vec)] for j in range(n_ss_vec)]) orth -= np.eye(n_ss_vec) state.subspace_orthogonality = np.max(np.abs(orth)) if state.subspace_orthogonality > n_problem * eps: warnings.warn( la.LinAlgWarning( "Subspace in davidson has lost orthogonality. " "Expect inaccurate results.")) if n_ss_added == 0: state.converged = False raise la.LinAlgError( "Davidson procedure could not generate any further vectors for " "the subpace. Iteration cannot be continued like this and will " "be aborted without convergence. Try a different guess.") with state.timer.record("projection"): Ax.extend(matrix @ SS[-n_ss_added:]) state.n_applies += n_ss_added
def eigsh(matrix, guesses, n_ep=None, max_subspace=None, conv_tol=1e-9, which="SA", max_iter=70, callback=None, preconditioner=None, preconditioning_method="Davidson", debug_checks=False, residual_min_norm=None, explicit_symmetrisation=IndexSymmetrisation): """ Davidson eigensolver for ADC problems @param matrix ADC matrix instance @param guesses Guess vectors (fixes the block size) @param n_ep Number of eigenpairs to be computed @param max_subspace Maximal subspace size @param conv_tol Convergence tolerance on the l2 norm of residuals to consider them converged @param which Which eigenvectors to converge to. Needs to be chosen such that it agrees with the selected preconditioner. @param max_iter Maximal numer of iterations @param callback Callback to run after each iteration @param preconditioner Preconditioner (type or instance) @param preconditioning_method Precondititoning method. Valid values are "Davidson" or "Sleijpen-van-der-Vorst" @param explicit_symmetrisation Explicit symmetrisation to apply to new subspace vectors before adding them to the subspace. Allows to correct for loss of index or spin symmetries (type or instance) @param debug_checks Enable some potentially costly debug checks (Loss of orthogonality etc.) @param residual_min_norm Minimal norm a residual needs to have in order to be accepted as a new subspace vector (defaults to 2 * len(matrix) * machine_expsilon) """ if not isinstance(matrix, AdcMatrix): raise TypeError("matrix is not of type AdcMatrix") for guess in guesses: if not isinstance(guess, AmplitudeVector): raise TypeError( "One of the guesses is not of type AmplitudeVector") if preconditioner is not None and isinstance(preconditioner, type): preconditioner = preconditioner(matrix) if explicit_symmetrisation is not None and \ isinstance(explicit_symmetrisation, type): explicit_symmetrisation = explicit_symmetrisation(matrix) if n_ep is None: n_ep = len(guesses) elif n_ep > len(guesses): raise ValueError("n_ep cannot exceed the number of guess vectors.") if not max_subspace: # TODO Arnoldi uses this: # max_subspace = max(2 * n_ep + 1, 20) max_subspace = max(6 * n_ep, 20, 5 * len(guesses)) def convergence_test(state): state.residuals_converged = state.residual_norms < conv_tol state.converged = np.all(state.residuals_converged) return state.converged if conv_tol < matrix.shape[1] * np.finfo(float).eps: warnings.warn( la.LinAlgWarning( "Convergence tolerance (== {:5.2g}) lower than " "estimated maximal numerical accuracy (== {:5.2g}). " "Convergence might be hard to achieve." "".format(conv_tol, matrix.shape[1] * np.finfo(float).eps))) state = DavidsonState(matrix, guesses) davidson_iterations(matrix, state, max_subspace, max_iter, n_ep=n_ep, is_converged=convergence_test, callback=callback, which=which, preconditioner=preconditioner, preconditioning_method=preconditioning_method, debug_checks=debug_checks, residual_min_norm=residual_min_norm, explicit_symmetrisation=explicit_symmetrisation) return state
def davidson_iterations(matrix, state, max_subspace, max_iter, n_ep, is_converged, which, callback=None, preconditioner=None, preconditioning_method="Davidson", debug_checks=False, residual_min_norm=None, explicit_symmetrisation=None): """Drive the davidson iterations Parameters ---------- matrix Matrix to diagonalise state DavidsonState containing the eigenvector guess max_subspace : int or NoneType, optional Maximal subspace size max_iter : int, optional Maximal number of iterations n_ep : int or NoneType, optional Number of eigenpairs to be computed is_converged Function to test for convergence callback : callable, optional Callback to run after each iteration which : str, optional Which eigenvectors to converge to. Needs to be chosen such that it agrees with the selected preconditioner. preconditioner Preconditioner (type or instance) preconditioning_method : str, optional Precondititoning method. Valid values are "Davidson" or "Sleijpen-van-der-Vorst" debug_checks : bool, optional Enable some potentially costly debug checks (Loss of orthogonality etc.) residual_min_norm : float or NoneType, optional Minimal norm a residual needs to have in order to be accepted as a new subspace vector (defaults to 2 * len(matrix) * machine_expsilon) explicit_symmetrisation Explicit symmetrisation to apply to new subspace vectors before adding them to the subspace. Allows to correct for loss of index or spin symmetries (type or instance) """ if preconditioning_method not in ["Davidson", "Sleijpen-van-der-Vorst"]: raise ValueError("Only 'Davidson' and 'Sleijpen-van-der-Vorst' " "are valid preconditioner methods") if preconditioning_method == "Sleijpen-van-der-Vorst": raise NotImplementedError("Sleijpen-van-der-Vorst preconditioning " "not yet implemented.") if callback is None: def callback(state, identifier): pass # The problem size n_problem = matrix.shape[1] # The block size n_block = len(state.subspace_vectors) # The current subspace size n_ss_vec = n_block # The current subspace SS = state.subspace_vectors # The matrix A projected into the subspace # as a continuous array. Only the view # Ass[:n_ss_vec, :n_ss_vec] contains valid data. Ass_cont = np.empty((max_subspace, max_subspace)) eps = np.finfo(float).eps if residual_min_norm is None: residual_min_norm = 2 * n_problem * eps callback(state, "start") state.timer.restart("iteration") with state.timer.record("projection"): # Initial application of A to the subspace Ax = evaluate(matrix @ SS) state.n_applies += n_ss_vec while state.n_iter < max_iter: state.n_iter += 1 assert len(SS) >= n_block assert len(SS) <= max_subspace # Project A onto the subspace, keeping in mind # that the values Ass[:-n_block, :-n_block] are already valid, # since they have been computed in the previous iterations already. with state.timer.record("projection"): Ass = Ass_cont[:n_ss_vec, :n_ss_vec] # Increase the work view size for i in range(n_block): Ass[:, -n_block + i] = Ax[-n_block + i] @ SS Ass[-n_block:, :] = np.transpose(Ass[:, -n_block:]) # Compute the which(== largest, smallest, ...) eigenpair of Ass # and the associated ritz vector as well as residual with state.timer.record("rayleigh_ritz"): if Ass.shape == (n_block, n_block): rvals, rvecs = la.eigh(Ass) # Do a full diagonalisation else: # TODO Maybe play with precision a little here # TODO Maybe use previous vectors somehow v0 = None rvals, rvecs = sla.eigsh(Ass, k=n_block, which=which, v0=v0) with state.timer.record("residuals"): # Form residuals, A * SS * v - λ * SS * v = Ax * v + SS * (-λ*v) def form_residual(rval, rvec): coefficients = np.hstack((rvec, -rval * rvec)) return lincomb(coefficients, Ax + SS, evaluate=True) residuals = [ form_residual(rvals[i], v) for i, v in enumerate(np.transpose(rvecs)) ] assert len(residuals) == n_block # Update the state's eigenpairs and residuals epair_mask = select_eigenpairs(rvals, n_ep, which) state.eigenvalues = rvals[epair_mask] state.residuals = [residuals[i] for i in epair_mask] state.residual_norms = np.array([r @ r for r in state.residuals]) # TODO This is misleading ... actually residual_norms contains # the norms squared. That's also the used e.g. in adcman to # check for convergence, so using the norm squared is fine, # in theory ... it should just be consistent. I think it is # better to go for the actual norm (no squared) inside the code # # If this adapted, also change the conv_tol to tol conversion # inside the Lanczos procedure. callback(state, "next_iter") state.timer.restart("iteration") if is_converged(state): # Build the eigenvectors we desire from the subspace vectors: state.eigenvectors = [ lincomb(v, SS, evaluate=True) for i, v in enumerate(np.transpose(rvecs)) if i in epair_mask ] state.converged = True callback(state, "is_converged") state.timer.stop("iteration") return state if state.n_iter == max_iter: warnings.warn( la.LinAlgWarning( f"Maximum number of iterations (== {max_iter}) " "reached in davidson procedure.")) state.eigenvectors = [ lincomb(v, SS, evaluate=True) for i, v in enumerate(np.transpose(rvecs)) if i in epair_mask ] state.timer.stop("iteration") state.converged = False return state if n_ss_vec + n_block > max_subspace: callback(state, "restart") with state.timer.record("projection"): # The addition of the preconditioned vectors goes beyond max. # subspace size => Collapse first, ie keep current Ritz vectors # as new subspace SS = [ lincomb(v, SS, evaluate=True) for v in np.transpose(rvecs) ] state.subspace_vectors = SS Ax = [ lincomb(v, Ax, evaluate=True) for v in np.transpose(rvecs) ] n_ss_vec = len(SS) # Update projection of ADC matrix A onto subspace Ass = Ass_cont[:n_ss_vec, :n_ss_vec] for i in range(n_ss_vec): Ass[:, i] = Ax[i] @ SS # continue to add residuals to space with state.timer.record("preconditioner"): if preconditioner: if hasattr(preconditioner, "update_shifts"): # Epsilon factor to make sure that 1 / (shift - diagonal) # does not become ill-conditioned as soon as the shift # approaches the actual diagonal values (which are the # eigenvalues for the ADC(2) doubles part if the coupling # block are absent) rvals_eps = 1e-6 preconditioner.update_shifts(rvals - rvals_eps) preconds = evaluate(preconditioner @ residuals) else: preconds = residuals # Explicitly symmetrise the new vectors if requested if explicit_symmetrisation: explicit_symmetrisation.symmetrise(preconds) # Project the components of the preconditioned vectors away # which are already contained in the subspace. # Then add those, which have a significant norm to the subspace. with state.timer.record("orthogonalisation"): n_ss_added = 0 for i in range(n_block): pvec = preconds[i] # Project out the components of the current subspace # That is form (1 - SS * SS^T) * pvec = pvec + SS * (-SS^T * pvec) coefficients = np.hstack(([1], -(pvec @ SS))) pvec = lincomb(coefficients, [pvec] + SS, evaluate=True) pnorm = np.sqrt(pvec @ pvec) if pnorm > residual_min_norm: # Extend the subspace SS.append(evaluate(pvec / pnorm)) n_ss_added += 1 n_ss_vec = len(SS) if debug_checks: orth = np.array([[SS[i] @ SS[j] for i in range(n_ss_vec)] for j in range(n_ss_vec)]) orth -= np.eye(n_ss_vec) state.subspace_orthogonality = np.max(np.abs(orth)) if state.subspace_orthogonality > n_problem * eps: warnings.warn( la.LinAlgWarning( "Subspace in davidson has lost orthogonality. " "Expect inaccurate results.")) if n_ss_added == 0: state.timer.stop("iteration") state.converged = False state.eigenvectors = [ lincomb(v, SS, evaluate=True) for i, v in enumerate(np.transpose(rvecs)) if i in epair_mask ] warnings.warn( la.LinAlgWarning( "Davidson procedure could not generate any further vectors for " "the subspace. Iteration cannot be continued like this and will " "be aborted without convergence. Try a different guess.")) return state with state.timer.record("projection"): Ax.extend(matrix @ SS[-n_ss_added:]) state.n_applies += n_ss_added