All Downloads are FREE. Search and download functionalities are using the official Maven repository.

smile.math.matrix.Lanczos Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (c) 2010-2020 Haifeng Li. All rights reserved.
 *
 * Smile is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * Smile is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Smile.  If not, see .
 ******************************************************************************/
package smile.math.matrix;

import smile.math.MathEx;

/**
 * The Lanczos algorithm is a direct algorithm devised by Cornelius Lanczos
 * that is an adaptation of power methods to find the most useful eigenvalues
 * and eigenvectors of an nth order linear system with a limited
 * number of operations, m, where m is much smaller than n.
 * 

* Although computationally efficient in principle, the method as initially * formulated was not useful, due to its numerical instability. In this * implementation, we use partial reorthogonalization to make the method * numerically stable. * * @author Haifeng Li */ public class Lanczos { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Lanczos.class); /** * Find k largest approximate eigen pairs of a symmetric matrix by the * Lanczos algorithm. * * @param A the matrix supporting matrix vector multiplication operation. * @param k the number of eigenvalues we wish to compute for the input matrix. * This number cannot exceed the size of A. */ public static Matrix.EVD eigen(DMatrix A, int k) { return eigen(A, k, 1.0E-8, 10 * A.nrows()); } /** * Find k largest approximate eigen pairs of a symmetric matrix by the * Lanczos algorithm. * * @param A the matrix supporting matrix vector multiplication operation. * @param k the number of eigenvalues we wish to compute for the input matrix. * This number cannot exceed the size of A. * @param kappa relative accuracy of ritz values acceptable as eigenvalues. * @param maxIter Maximum number of iterations. */ public static Matrix.EVD eigen(DMatrix A, int k, double kappa, int maxIter) { if (A.nrows() != A.ncols()) { throw new IllegalArgumentException(String.format("Matrix is not square: %d x %d", A.nrows(), A.ncols())); } if (k < 1 || k > A.nrows()) { throw new IllegalArgumentException("k is larger than the size of A: " + k + " > " + A.nrows()); } if (kappa <= MathEx.EPSILON) { throw new IllegalArgumentException("Invalid tolerance: kappa = " + kappa); } if (maxIter <= 0) { maxIter = 10 * A.nrows(); } int n = A.nrows(); int intro = 0; // roundoff estimate for dot product of two unit vectors double eps = MathEx.EPSILON * Math.sqrt(n); double reps = Math.sqrt(MathEx.EPSILON); double eps34 = reps * Math.sqrt(reps); kappa = Math.max(kappa, eps34); // Workspace // wptr[0] r[j] // wptr[1] q[j] // wptr[2] q[j-1] // wptr[3] p // wptr[4] p[j-1] // wptr[5] temporary worksapce double[][] wptr = new double[6][n]; // orthogonality estimate of Lanczos vectors at step j double[] eta = new double[n]; // orthogonality estimate of Lanczos vectors at step j-1 double[] oldeta = new double[n]; // the error bounds double[] bnd = new double[n]; // diagonal elements of T double[] alf = new double[n]; // off-diagonal elements of T double[] bet = new double[n + 1]; // basis vectors for the Krylov subspace double[][] q = new double[n][]; // initial Lanczos vectors double[][] p = new double[2][]; // arrays used in the QL decomposition double[] ritz = new double[n + 1]; // eigenvectors calculated in the QL decomposition Matrix z = null; // First step of the Lanczos algorithm. It also does a step of extended // local re-orthogonalization. // get initial vector; default is random double rnm = startv(A, q, wptr, 0); // normalize starting vector double t = 1.0 / rnm; MathEx.scale(t, wptr[0], wptr[1]); MathEx.scale(t, wptr[3]); // take the first step A.mv(wptr[3], wptr[0]); alf[0] = MathEx.dot(wptr[0], wptr[3]); MathEx.axpy(-alf[0], wptr[1], wptr[0]); t = MathEx.dot(wptr[0], wptr[3]); MathEx.axpy(-t, wptr[1], wptr[0]); alf[0] += t; MathEx.copy(wptr[0], wptr[4]); rnm = MathEx.norm(wptr[0]); double anorm = rnm + Math.abs(alf[0]); double tol = reps * anorm; if (0 == rnm) { throw new IllegalStateException("Lanczos method was unable to find a starting vector within range."); } eta[0] = eps; oldeta[0] = eps; // number of ritz values stabilized int neig = 0; // number of intitial Lanczos vectors in local orthog. (has value of 0, 1 or 2) int ll = 0; // start of index through loop int first = 1; // end of index through loop int last = Math.min(k + Math.max(8, k), n); // number of Lanczos steps actually taken int j = 0; // stop flag boolean enough = false; // algorithm iterations int iter = 0; for (; !enough && iter < maxIter; iter++) { if (rnm <= tol) { rnm = 0.0; } // a single Lanczos step for (j = first; j < last; j++) { MathEx.swap(wptr, 1, 2); MathEx.swap(wptr, 3, 4); store(q, j - 1, wptr[2]); if (j - 1 < 2) { p[j - 1] = wptr[4].clone(); } bet[j] = rnm; // restart if invariant subspace is found if (0 == bet[j]) { rnm = startv(A, q, wptr, j); if (rnm < 0.0) { rnm = 0.0; break; } if (rnm == 0) { enough = true; } } if (enough) { // These lines fix a bug that occurs with low-rank matrices MathEx.swap(wptr, 1, 2); break; } // take a lanczos step t = 1.0 / rnm; MathEx.scale(t, wptr[0], wptr[1]); MathEx.scale(t, wptr[3]); A.mv(wptr[3], wptr[0]); MathEx.axpy(-rnm, wptr[2], wptr[0]); alf[j] = MathEx.dot(wptr[0], wptr[3]); MathEx.axpy(-alf[j], wptr[1], wptr[0]); // orthogonalize against initial lanczos vectors if (j <= 2 && (Math.abs(alf[j - 1]) > 4.0 * Math.abs(alf[j]))) { ll = j; } for (int i = 0; i < Math.min(ll, j - 1); i++) { t = MathEx.dot(p[i], wptr[0]); MathEx.axpy(-t, q[i], wptr[0]); eta[i] = eps; oldeta[i] = eps; } // extended local reorthogonalization t = MathEx.dot(wptr[0], wptr[4]); MathEx.axpy(-t, wptr[2], wptr[0]); if (bet[j] > 0.0) { bet[j] = bet[j] + t; } t = MathEx.dot(wptr[0], wptr[3]); MathEx.axpy(-t, wptr[1], wptr[0]); alf[j] = alf[j] + t; MathEx.copy(wptr[0], wptr[4]); rnm = MathEx.norm(wptr[0]); anorm = bet[j] + Math.abs(alf[j]) + rnm; tol = reps * anorm; // update the orthogonality bounds ortbnd(alf, bet, eta, oldeta, j, rnm, eps); // restore the orthogonality state when needed rnm = purge(ll, q, wptr[0], wptr[1], wptr[4], wptr[3], eta, oldeta, j, rnm, tol, eps, reps); if (rnm <= tol) { rnm = 0.0; } } if (enough) { j = j - 1; } else { j = last - 1; } first = j + 1; bet[j + 1] = rnm; // analyze T System.arraycopy(alf, 0, ritz, 0, j + 1); System.arraycopy(bet, 0, wptr[5], 0, j + 1); z = new Matrix(j + 1, j + 1); for (int i = 0; i <= j; i++) { z.set(i, i, 1.0); } // compute the eigenvalues and eigenvectors of the // tridiagonal matrix tql2(z, ritz, wptr[5]); for (int i = 0; i <= j; i++) { bnd[i] = rnm * Math.abs(z.get(j, i)); } // massage error bounds for very close ritz values boolean[] ref_enough = {enough}; neig = error_bound(ref_enough, ritz, bnd, j, tol, eps34); enough = ref_enough[0]; // should we stop? if (neig < k) { if (0 == neig) { last = first + 9; intro = first; } else { last = first + Math.max(3, 1 + ((j - intro) * (k - neig)) / Math.max(3,neig)); } last = Math.min(last, n); } else { enough = true; } enough = enough || first >= n; } logger.info("Lanczos: " + iter + " iterations for Matrix of size " + n); store(q, j, wptr[1]); k = Math.min(k, neig); double[] eigenvalues = new double[k]; Matrix eigenvectors = new Matrix(n, k); for (int i = 0, index = 0; i <= j && index < k; i++) { if (bnd[i] <= kappa * Math.abs(ritz[i])) { for (int row = 0; row < n; row++) { for (int l = 0; l <= j; l++) { eigenvectors.add(row, index, q[l][row] * z.get(l, i)); } } eigenvalues[index++] = ritz[i]; } } return new Matrix.EVD(eigenvalues, eigenvectors); } /** * Generate a starting vector in r and returns |r|. It returns zero if the * range is spanned, and throws exception if no starting vector within range * of operator can be found. * @param step starting index for a Lanczos run */ private static double startv(DMatrix A, double[][] q, double[][] wptr, int step) { // get initial vector; default is random double rnm = MathEx.dot(wptr[0], wptr[0]); double[] r = wptr[0]; for (int id = 0; id < 3; id++) { if (id > 0 || step > 0 || rnm == 0) { for (int i = 0; i < r.length; i++) { r[i] = Math.random() - 0.5; } } MathEx.copy(wptr[0], wptr[3]); // apply operator to put r in range (essential if m singular) A.mv(wptr[3], wptr[0]); MathEx.copy(wptr[0], wptr[3]); rnm = MathEx.dot(wptr[0], wptr[3]); if (rnm > 0.0) { break; } } // fatal error if (rnm <= 0.0) { logger.error("Lanczos method was unable to find a starting vector within range."); return -1; } if (step > 0) { for (int i = 0; i < step; i++) { double t = MathEx.dot(wptr[3], q[i]); MathEx.axpy(-t, q[i], wptr[0]); } // make sure q[step] is orthogonal to q[step-1] double t = MathEx.dot(wptr[4], wptr[0]); MathEx.axpy(-t, wptr[2], wptr[0]); MathEx.copy(wptr[0], wptr[3]); t = MathEx.dot(wptr[3], wptr[0]); if (t <= MathEx.EPSILON * rnm) { t = 0.0; } rnm = t; } return Math.sqrt(rnm); } /** * Update the eta recurrence. * @param alf array to store diagonal of the tridiagonal matrix T * @param bet array to store off-diagonal of T * @param eta on input, orthogonality estimate of Lanczos vectors at step j. * On output, orthogonality estimate of Lanczos vectors at step j+1 . * @param oldeta on input, orthogonality estimate of Lanczos vectors at step j-1 * On output orthogonality estimate of Lanczos vectors at step j * @param step dimension of T * @param rnm norm of the next residual vector * @param eps roundoff estimate for dot product of two unit vectors */ private static void ortbnd(double[] alf, double[] bet, double[] eta, double[] oldeta, int step, double rnm, double eps) { if (step < 1) { return; } if (0 != rnm) { if (step > 1) { oldeta[0] = (bet[1] * eta[1] + (alf[0] - alf[step]) * eta[0] - bet[step] * oldeta[0]) / rnm + eps; } for (int i = 1; i <= step - 2; i++) { oldeta[i] = (bet[i + 1] * eta[i + 1] + (alf[i] - alf[step]) * eta[i] + bet[i] * eta[i - 1] - bet[step] * oldeta[i]) / rnm + eps; } } oldeta[step - 1] = eps; for (int i = 0; i < step; i++) { double swap = eta[i]; eta[i] = oldeta[i]; oldeta[i] = swap; } eta[step] = eps; } /** * Examine the state of orthogonality between the new Lanczos * vector and the previous ones to decide whether re-orthogonalization * should be performed. * @param ll number of intitial Lanczos vectors in local orthog. * @param r on input, residual vector to become next Lanczos vector. * On output, residual vector orthogonalized against previous Lanczos. * @param q on input, current Lanczos vector. On Output, current * Lanczos vector orthogonalized against previous ones. * @param ra previous Lanczos vector * @param qa previous Lanczos vector * @param eta state of orthogonality between r and prev. Lanczos vectors * @param oldeta state of orthogonality between q and prev. Lanczos vectors */ private static double purge(int ll, double[][] Q, double[] r, double[] q, double[] ra, double[] qa, double[] eta, double[] oldeta, int step, double rnm, double tol, double eps, double reps) { if (step < ll + 2) { return rnm; } double t, tq, tr; int k = idamax(step - (ll + 1), eta, ll, 1) + ll; if (Math.abs(eta[k]) > reps) { double reps1 = eps / reps; int iteration = 0; boolean flag = true; while (iteration < 2 && flag) { if (rnm > tol) { // bring in a lanczos vector t and orthogonalize both // r and q against it tq = 0.0; tr = 0.0; for (int i = ll; i < step; i++) { t = -MathEx.dot(qa, Q[i]); tq += Math.abs(t); MathEx.axpy(t, Q[i], q); t = -MathEx.dot(ra, Q[i]); tr += Math.abs(t); MathEx.axpy(t, Q[i], r); } MathEx.copy(q, qa); t = -MathEx.dot(r, qa); tr += Math.abs(t); MathEx.axpy(t, q, r); MathEx.copy(r, ra); rnm = Math.sqrt(MathEx.dot(ra, r)); if (tq <= reps1 && tr <= reps1 * rnm) { flag = false; } } iteration++; } for (int i = ll; i <= step; i++) { eta[i] = eps; oldeta[i] = eps; } } return rnm; } /** * Find the index of element having maximum absolute value. */ private static int idamax(int n, double[] dx, int ix0, int incx) { int ix, imax; double dmax; if (n < 1) { return -1; } if (n == 1) { return 0; } if (incx == 0) { return -1; } ix = (incx < 0) ? ix0 + ((-n + 1) * incx) : ix0; imax = ix; dmax = Math.abs(dx[ix]); for (int i = 1; i < n; i++) { ix += incx; double dtemp = Math.abs(dx[ix]); if (dtemp > dmax) { dmax = dtemp; imax = ix; } } return imax; } /** * Massage error bounds for very close ritz values by placing a gap between * them. The error bounds are then refined to reflect this. * @param ritz array to store the ritz values * @param bnd array to store the error bounds * @param enough stop flag */ private static int error_bound(boolean[] enough, double[] ritz, double[] bnd, int step, double tol, double eps34) { double gapl, gap; // massage error bounds for very close ritz values int mid = idamax(step + 1, bnd, 0, 1); for (int i = ((step + 1) + (step - 1)) / 2; i >= mid + 1; i -= 1) { if (Math.abs(ritz[i - 1] - ritz[i]) < eps34 * Math.abs(ritz[i])) { if (bnd[i] > tol && bnd[i - 1] > tol) { bnd[i - 1] = Math.sqrt(bnd[i] * bnd[i] + bnd[i - 1] * bnd[i - 1]); bnd[i] = 0.0; } } } for (int i = ((step + 1) - (step - 1)) / 2; i <= mid - 1; i += 1) { if (Math.abs(ritz[i + 1] - ritz[i]) < eps34 * Math.abs(ritz[i])) { if (bnd[i] > tol && bnd[i + 1] > tol) { bnd[i + 1] = Math.sqrt(bnd[i] * bnd[i] + bnd[i + 1] * bnd[i + 1]); bnd[i] = 0.0; } } } // refine the error bounds int neig = 0; gapl = ritz[step] - ritz[0]; for (int i = 0; i <= step; i++) { gap = gapl; if (i < step) { gapl = ritz[i + 1] - ritz[i]; } gap = Math.min(gap, gapl); if (gap > bnd[i]) { bnd[i] = bnd[i] * (bnd[i] / gap); } if (bnd[i] <= 16.0 * MathEx.EPSILON * Math.abs(ritz[i])) { neig++; if (!enough[0]) { enough[0] = -MathEx.EPSILON < ritz[i] && ritz[i] < MathEx.EPSILON; } } } logger.info("Lancozs method found {} converged eigenvalues of the {}-by-{} matrix", neig, step + 1, step + 1); if (neig != 0) { for (int i = 0; i <= step; i++) { if (bnd[i] <= 16.0 * MathEx.EPSILON * Math.abs(ritz[i])) { logger.info("ritz[{}] = {}", i, ritz[i]); } } } return neig; } /** * Based on the input operation flag, stores to or retrieves from memory a vector. * @param s contains the vector to be stored */ private static void store(double[][] q, int j, double[] s) { if (null == q[j]) { q[j] = s.clone(); } else { MathEx.copy(s, q[j]); } } /** * Tridiagonal QL Implicit routine for computing eigenvalues and eigenvectors of a symmetric, * real, tridiagonal matrix. * * The routine works extremely well in practice. The number of iterations for the first few * eigenvalues might be 4 or 5, say, but meanwhile the off-diagonal elements in the lower right-hand * corner have been reduced too. The later eigenvalues are liberated with very little work. The * average number of iterations per eigenvalue is typically 1.3 - 1.6. The operation count per * iteration is O(n), with a fairly large effective coefficient, say, ~20n. The total operation count * for the diagonalization is then ~20n * (1.3 - 1.6)n = ~30n^2. If the eigenvectors are required, * there is an additional, much larger, workload of about 3n^3 operations. * * @param V on input, it contains the identity matrix. On output, the kth column * of V returns the normalized eigenvector corresponding to d[k]. * @param d on input, it contains the diagonal elements of the tridiagonal matrix. * On output, it contains the eigenvalues. * @param e on input, it contains the subdiagonal elements of the tridiagonal * matrix, with e[0] arbitrary. On output, its contents are destroyed. */ private static void tql2(Matrix V, double[] d, double[] e) { int n = V.nrows(); for (int i = 1; i < n; i++) { e[i - 1] = e[i]; } e[n - 1] = 0.0; double f = 0.0; double tst1 = 0.0; for (int l = 0; l < n; l++) { // Find small subdiagonal element tst1 = Math.max(tst1, Math.abs(d[l]) + Math.abs(e[l])); int m = l; for (; m < n; m++) { if (Math.abs(e[m]) <= MathEx.EPSILON * tst1) { break; } } // If m == l, d[l] is an eigenvalue, // otherwise, iterate. if (m > l) { int iter = 0; do { if (++iter >= 30) { throw new RuntimeException("Too many iterations"); } // Compute implicit shift double g = d[l]; double p = (d[l + 1] - g) / (2.0 * e[l]); double r = Math.hypot(p, 1.0); if (p < 0) { r = -r; } d[l] = e[l] / (p + r); d[l + 1] = e[l] * (p + r); double dl1 = d[l + 1]; double h = g - d[l]; for (int i = l + 2; i < n; i++) { d[i] -= h; } f = f + h; // Implicit QL transformation. p = d[m]; double c = 1.0; double c2 = c; double c3 = c; double el1 = e[l + 1]; double s = 0.0; double s2 = 0.0; for (int i = m - 1; i >= l; i--) { c3 = c2; c2 = c; s2 = s; g = c * e[i]; h = c * p; r = Math.hypot(p, e[i]); e[i + 1] = s * r; s = e[i] / r; c = p / r; p = c * d[i] - s * g; d[i + 1] = h + s * (c * g + s * d[i]); // Accumulate transformation. for (int k = 0; k < n; k++) { h = V.get(k, i + 1); V.set(k, i + 1, s * V.get(k, i) + c * h); V.set(k, i, c * V.get(k, i) - s * h); } } p = -s * s2 * c3 * el1 * e[l] / dl1; e[l] = s * p; d[l] = c * p; // Check for convergence. } while (Math.abs(e[l]) > MathEx.EPSILON * tst1); } d[l] = d[l] + f; e[l] = 0.0; } // Sort eigenvalues and corresponding vectors. for (int i = 0; i < n - 1; i++) { int k = i; double p = d[i]; for (int j = i + 1; j < n; j++) { if (d[j] > p) { k = j; p = d[j]; } } if (k != i) { d[k] = d[i]; d[i] = p; for (int j = 0; j < n; j++) { p = V.get(j, i); V.set(j, i, V.get(j, k)); V.set(j, k, p); } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy