def orthogonalise(self, vectors): """ Orthogonalise the passed vectors with each other and return orthonormal vectors. """ if len(vectors) == 0: return [] subspace = [evaluate(vectors[0] / np.sqrt(vectors[0] @ vectors[0]))] for v in vectors[1:]: w = self.orthogonalise_against(v, subspace) subspace.append(evaluate(w / np.sqrt(w @ w))) return subspace
def symmetrise(self, new_vectors): """ Symmetrise a set of new vectors to be added to the subspace. new_vectors Vectors to symmetrise (updated in-place) Returns: The updated new_vectors """ if isinstance(new_vectors, AmplitudeVector): return self.symmetrise([new_vectors])[0] for vec in new_vectors: if not isinstance(vec, AmplitudeVector): raise TypeError("new_vectors has to be an " "iterable of AmplitudeVector") for b in vec.blocks_ph: if b not in self.symmetrisation_functions: continue vec[b] = evaluate(self.symmetrisation_functions[b](vec[b])) return new_vectors
def qr(self, vectors): """ A simple (and inefficient / inaccurate) QR decomposition based on Gram-Schmidt. Use only if no alternatives. vectors : list List of vectors representing the input matrix to decompose. """ if len(vectors) == 0: return [] elif len(vectors) == 1: norm_v = np.sqrt(vectors[0] @ vectors[0]) return [evaluate(vectors[0] / norm_v)], np.array([[norm_v]]) else: n_vec = len(vectors) Q = self.orthogonalise(vectors) R = np.zeros((n_vec, n_vec)) for i in range(n_vec): for j in range(i, n_vec): R[i, j] = Q[i] @ vectors[j] return Q, R
def conjugate_gradient(matrix, rhs, x0=None, conv_tol=1e-9, max_iter=100, callback=None, Pinv=None, cg_type="polak_ribiere", explicit_symmetrisation=IndexSymmetrisation): """An implementation of the conjugate gradient algorithm. This algorithm implements the "flexible" conjugate gradient using the Polak-Ribière formula, but allows to employ the "traditional" Fletcher-Reeves formula as well. It solves `matrix @ x = rhs` for `x` by minimising the residual `matrix @ x - rhs`. Parameters ---------- matrix Matrix object. Should be an ADC matrix. rhs Right-hand side, source. x0 Initial guess conv_tol : float Convergence tolerance on the l2 norm of residuals to consider them converged. max_iter : int Maximum number of iterations callback Callback to call after each iteration Pinv Preconditioner to A, typically an estimate for A^{-1} cg_type : string Identifier to select between polak_ribiere and fletcher_reeves 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(matrix) if x0 is None: # Start with random guess raise NotImplementedError("Random guess is not yet implemented.") else: x0 = copy(x0) if Pinv is None: Pinv = PreconditionerIdentity() if Pinv is not None and isinstance(Pinv, type): Pinv = Pinv(matrix) def is_converged(state): state.converged = state.residual_norm < conv_tol return state.converged state = State() # Initialise iterates state.solution = x0 state.residual = evaluate(rhs - matrix @ state.solution) state.n_applies += 1 state.residual_norm = np.sqrt(state.residual @ state.residual) pk = zk = Pinv @ state.residual if explicit_symmetrisation: # TODO Not sure this is the right spot ... also this syntax is ugly pk = explicit_symmetrisation.symmetrise(pk) callback(state, "start") while state.n_iter < max_iter: state.n_iter += 1 # Update ak and iterated solution # TODO This needs to be modified for general optimisations, # i.e. where A is non-linear # https://en.wikipedia.org/wiki/Nonlinear_conjugate_gradient_method Apk = matrix @ pk state.n_applies += 1 res_dot_zk = dot(state.residual, zk) ak = float(res_dot_zk / dot(pk, Apk)) state.solution = evaluate(state.solution + ak * pk) residual_old = state.residual state.residual = evaluate(residual_old - ak * Apk) state.residual_norm = np.sqrt(state.residual @ state.residual) callback(state, "next_iter") if is_converged(state): state.converged = True callback(state, "is_converged") return state if state.n_iter == max_iter: raise la.LinAlgError("Maximum number of iterations (== " + str(max_iter) + " reached in conjugate " "gradient procedure.") zk = evaluate(Pinv @ state.residual) if explicit_symmetrisation: # TODO Not sure this is the right spot ... also this syntax is ugly zk = explicit_symmetrisation.symmetrise(zk) if cg_type == "fletcher_reeves": bk = float(dot(zk, state.residual) / res_dot_zk) elif cg_type == "polak_ribiere": bk = float(dot(zk, (state.residual - residual_old)) / res_dot_zk) pk = zk + bk * pk
def guesses_from_diagonal_singles(matrix, n_guesses, spin_change=0, spin_block_symmetrisation="none", degeneracy_tolerance=1e-14): motrans = MoIndexTranslation(matrix.mospaces, matrix.axis_spaces["ph"]) if n_guesses == 0: return [] # Create a result vector of zero vectors with appropriate symmetry setup ret = [ guess_zero(matrix, spin_change=spin_change, spin_block_symmetrisation=spin_block_symmetrisation) for _ in range(n_guesses) ] # Search of the smallest elements # This predicate checks an index is an allowed element for the singles # part of the guess vectors and has the requested spin-change def pred_singles(telem): return (ret[0].ph.is_allowed(telem.index) and telem.spin_change == spin_change) elements = find_smallest_matching_elements( pred_singles, matrix.diagonal().ph, motrans, n_guesses, degeneracy_tolerance=degeneracy_tolerance) if len(elements) == 0: return [] # By construction of find_smallest_elements the returned elements # are already sorted such that adjacent vectors of equal value # only differ in spin indices (or they consider excitations from # degenerate orbitals). We want to form spin-adapted linear combinations # for the case of an unrestricted reference in the following # and therefore to exclude the spatial degeneracies we sort explicitly # only by value, and spatial indices (and not by spin). def telem_nospin(telem): return (telem.value, telem.block_index_spatial, telem.inblock_index) ivec = 0 for value, group in groupby(elements, key=telem_nospin): if ivec >= len(ret): break group = list(group) if len(group) == 1: # Just add the single vector ret[ivec].ph[group[0].index] = 1.0 ivec += 1 elif len(group) == 2: # Since these two are grouped together, their # spatial parts must be identical. # Add the positive linear combination ... ret[ivec].ph[group[0].index] = 1 ret[ivec].ph[group[1].index] = 1 ivec += 1 # ... and the negative linear combination if ivec < n_guesses: ret[ivec].ph[group[0].index] = +1 ret[ivec].ph[group[1].index] = -1 ivec += 1 else: raise AssertionError("group size > 3 should not occur " "when setting up single guesse.") assert ivec <= n_guesses # Resize in case less guesses found than requested # and normalise vectors return [evaluate(v / np.sqrt(v @ v)) for v in ret[:ivec]]
def __next__(self): """Advance the iterator, i.e. extend the Lanczos subspace""" if self.n_iter == 0: # Initialise Lanczos subspace v = self.ortho.orthogonalise(self.residual) self.lanczos_subspace = v r = evaluate(self.matrix @ v) alpha = np.empty((self.n_block, self.n_block)) for p in range(self.n_block): alpha[p, :] = v[p] @ r # r = r - v * alpha - Y * Sigma Sigma, Y = self.ritz_overlaps, self.ritz_vectors r = [ lincomb(np.hstack(([1], -alpha[:, p], -Sigma[:, p])), [r[p]] + v + Y, evaluate=True) for p in range(self.n_block) ] # r = r - Y * Y'r (Full reorthogonalisation) for p in range(self.n_block): r[p] = self.ortho.orthogonalise_against( r[p], self.ritz_vectors) self.residual = r self.n_iter = 1 self.n_applies = self.n_block self.alphas = [alpha] # Diagonal matrix block of subspace matrix self.betas = [] # Side-diagonal matrix blocks return LanczosSubspace(self) # Iteration 1 and onwards: q = self.lanczos_subspace[-self.n_block:] v, beta = self.ortho.qr(self.residual) if np.linalg.norm(beta) < np.finfo(float).eps * self.n_problem: # No point to go on ... new vectors will be decoupled from old ones raise StopIteration() # r = A * v - q * beta^T self.n_applies += self.n_block r = self.matrix @ v r = [ lincomb(np.hstack(([1], -(beta.T)[:, p])), [r[p]] + q, evaluate=True) for p in range(self.n_block) ] # alpha = v^T * r alpha = np.empty((self.n_block, self.n_block)) for p in range(self.n_block): alpha[p, :] = v[p] @ r # r = r - v * alpha r = [ lincomb(np.hstack(([1], -alpha[:, p])), [r[p]] + v, evaluate=True) for p in range(self.n_block) ] # Full reorthogonalisation for p in range(self.n_block): r[p] = self.ortho.orthogonalise_against( r[p], self.lanczos_subspace + self.ritz_vectors) # Commit results self.n_iter += 1 self.lanczos_subspace.extend(v) self.residual = r self.alphas.append(alpha) self.betas.append(beta) return LanczosSubspace(self)
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