// clang-format off /* -*- c++ -*- ---------------------------------------------------------- LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator https://www.lammps.org/, Sandia National Laboratories Steve Plimpton, sjplimp@sandia.gov Copyright (2003) Sandia Corporation. Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains certain rights in this software. This software is distributed under the GNU General Public License. (Some of the code in this file is also available using a more premissive license. See below for details.) See the README file in the top-level LAMMPS directory. ------------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- Contributing authors: Yuya Kurebayashi (Tohoku University, Lanczos algorithm) Andrew Jewett (Scripps Research, Jacobi algorithm) ------------------------------------------------------------------------- */ #ifndef LMP_MATH_EIGEN_IMPL_H #define LMP_MATH_EIGEN_IMPL_H // This file contains a library of functions and classes which can // efficiently perform eigendecomposition for an extremely broad // range of matrix types: both real and complex, dense and sparse. // Matrices need not be of type "double **", for example. // In principle, almost any type of C++ container can be used. // Some general C++11 compatible functions for allocating matrices and // calculating norms of real and complex vectors are also provided. // note // The "Jacobi" and "PEigenDense" classes are used for calculating // eigenvalues and eigenvectors of conventional dense square matrices. // note // The "LambdaLanczos" class can calculate eigenalues and eigenvectors // of more general types of matrices, especially large, sparse matrices. // It uses C++ lambda expressions to simplify and generalize the way // matrices can be represented. This allows it to be applied to // nearly any kind of sparse (or dense) matrix representation. // note // The source code for Jacobi and LambdaLanczos is also available at: // https://github.com/jewettaij/jacobi_pd (CC0-1.0 license) // https://github.com/mrcdr/lambda-lanczos (MIT license) //#include #include #include #include #include #include #include #include namespace MathEigen { // --- Memory allocation for matrices --- // Allocate an arbitrary 2-dimensional array. (Uses row-major order.) // note This function was intended for relatively small matrices (eg 4x4). // For large arrays, please use the 2d create() function from "memory.h" template void Alloc2D(size_t nrows, // size of the array (number of rows) size_t ncols, // size of the array (number of columns) Entry ***paaX); // pointer to a 2D C-style array // Deallocate arrays that were created using Alloc2D(). template void Dealloc2D(Entry ***paaX); // pointer to 2D multidimensional array // --- Complex numbers --- /// @brief /// "realTypeMap" struct is used to the define "real_t" type mapper. /// The "real_t" type mapper is used by the "LambdaLanczos" and "PEigenDense" /// classes, so it is documented here to help users understand those classes. /// "real_t" returns the C++ type corresponding to the real component of T. /// /// @details /// For example, suppose you have a matrix of type std::complex**. /// The eigenvalues calculated by "LambdaLanczos" and "PEigenDense" should be /// of type "double" (which is the same as "real_T>"), /// not "std::complex". (This is because the algorithm assumes the /// matrix is Hermitian, and the eigenvalues of a Hermitian matrix are always /// real. So if you attempt to pass a reference to a complex number as the /// first argument to LambdaLanczos::run(), the compiler will complain.) /// /// Implementation details: "real_t" is defined using C++ template /// specializations. template struct realTypeMap { typedef T type; }; template struct realTypeMap> { typedef T type; }; template using real_t = typename realTypeMap::type; // --- Operations on vectors (of real and complex numbers) --- // Calculate the inner product of two vectors. // (For vectors of complex numbers, std::conj() is used.) template T inner_prod(const std::vector& v1, const std::vector& v2); // Compute the sum of the absolute values of the entries in v // returns a real number (of type real_t). template real_t l1_norm(const std::vector& v); // Calculate the l2_norm (Euclidian length) of vector v. // returns a real number (of type real_t). template real_t l2_norm(const std::vector& v); /// Multiply a vector (v) by a scalar (c). template void scalar_mul(T1 c, std::vector& v); /// Divide vector "v" in-place by it's length (l2_norm(v)). template void normalize(std::vector& v); // ---- Eigendecomposition of small dense symmetric matrices ---- /// @class Jacobi /// @brief Calculate the eigenvalues and eigenvectors of a symmetric matrix /// using the Jacobi eigenvalue algorithm. This code (along with tests /// and benchmarks) is available free of license restrictions at: /// https://github.com/jewettaij/jacobi_pd /// @note The "Vector", "Matrix", and "ConstMatrix" type arguments can be any /// C or C++ object that supports indexing, including pointers or vectors. template class Jacobi { int n; //!< the size of the matrices you want to diagonalize Scalar **M; //!< local copy of the current matrix being analyzed // Precomputed cosine, sine, and tangent of the most recent rotation angle: Scalar c; //!< = cos(θ) Scalar s; //!< = sin(θ) Scalar t; //!< = tan(θ), (note |t|<=1) int *max_idx_row; //!< = keep track of the the maximum element in row i (>i) public: /// @brief Specify the size of the matrices you want to diagonalize later. /// @param n the size (ie. number of rows) of the (square) matrix. Jacobi(int n); ~Jacobi(); /// @brief Change the size of the matrices you want to diagonalize. /// @param n the size (ie. number of rows) of the (square) matrix. void SetSize(int n); // @typedef choose the criteria for sorting eigenvalues and eigenvectors typedef enum eSortCriteria { DO_NOT_SORT, SORT_DECREASING_EVALS, SORT_INCREASING_EVALS, SORT_DECREASING_ABS_EVALS, SORT_INCREASING_ABS_EVALS } SortCriteria; /// @brief Calculate the eigenvalues and eigenvectors of a symmetric matrix /// using the Jacobi eigenvalue algorithm. /// @returns 0 if the algorithm converged, /// 1 if the algorithm failed to converge. (IE, if the number of /// pivot iterations exceeded max_num_sweeps * iters_per_sweep, /// where iters_per_sweep = (n*(n-1)/2)) /// @note To reduce the computation time further, set calc_evecs=false. int Diagonalize(ConstMatrix mat, //!< the matrix you wish to diagonalize (size n) Vector eval, //!< store the eigenvalues here Matrix evec, //!< store the eigenvectors here (in rows) SortCriteria sort_criteria=SORT_DECREASING_EVALS,//!& source); Jacobi(Jacobi&& other); void swap(Jacobi &other); Jacobi& operator = (Jacobi source); }; // class Jacobi // ---- Eigendecomposition of large sparse (or dense) matrices ---- // The "LambdaLanczos" is a class useful for calculating eigenvalues // and eigenvectors of large sparse matrices. Unfortunately, before the // LambdaLanczos class can be declared, several additional expressions, // classes and functions that it depends on must be declared first. // Create random vectors used at the beginning of the Lanczos algorithm. // note "Partially specialization of function" is not allowed, so // it is mimicked by wrapping the "init" function with a class template. template struct VectorRandomInitializer { public: static void init(std::vector&); }; template struct VectorRandomInitializer> { public: static void init(std::vector>&); }; // Return the number of significant decimal digits of type T. template inline constexpr int sig_decimal_digit() { return (int)(std::numeric_limits::digits * std::log10(std::numeric_limits::radix)); } // Return 10^-n where n=number of significant decimal digits of type T. template inline constexpr T minimum_effective_decimal() { return std::pow(10, -sig_decimal_digit()); } /// @class LambdaLanczos /// @brief The LambdaLanczos class provides a general way to calculate /// the smallest or largest eigenvalue and the corresponding eigenvector /// of a Hermitian matrix using the Lanczos algorithm. /// The characteristic feature of LambdaLanczos is that the matrix-vector /// multiplication routine used in the Lanczos algorithm is adaptable. /// This code (along with automatic unit tests) is also distributed /// under the MIT license at https://github.com/mrcdr/lambda-lanczos /// /// @note /// If the matrices you want to analyze are ordinary square matrices, (as in /// the example) it might be easier to use "PEigenDense" instead. (It is a /// wrapper which takes care of all of the LambdaLanczos details for you.) /// /// @note /// IMPORTANT: /// Unless the matrix you are solving is positive or negative definite, you /// MUST set the "eigenvalue_offset" parameter. See the "pg_developer" docs. template class LambdaLanczos { public: LambdaLanczos(); LambdaLanczos(std::function&, std::vector&)> mv_mul, int matrix_size, bool find_maximum); LambdaLanczos(std::function&, std::vector&)> mv_mul, int matrix_size) : LambdaLanczos(mv_mul, matrix_size, true) {} /// @brief Calculate the principal (largest or smallest) eigenvalue /// of the matrix (and its corresponding eigenvector). int run(real_t&, std::vector&) const; // --- public data members --- /// @brief Specify the size of the matrix you will analyze. /// (This equals the size of the eigenvector which will be returned.) int matrix_size; /// @brief Specify the function used for matrix*vector multiplication /// used by the Lanczos algorithm. For an ordinary dense matrix, /// this function is the ordinary matrix*vector product. (See the /// example above. For a sparse matrix, it will be something else.) std::function&, std::vector&)> mv_mul; /// @brief Are we searching for the maximum or minimum eigenvalue? /// @note (Usually, you must also specify eigenvalue_offset.) bool find_maximum = false; /// @brief Shift all the eigenvalues by "eigenvalue_offset" during the Lanczos /// iteration (ie. during LambdaLanczos::run()). The goal is to insure /// that the correct eigenvalue is selected (the one with the maximum /// magnitude). /// @note Unless your matrix is positive definite or negative definite, you /// MUST specify eigenvalue_offset. See comment above for details. /// @note After LambdaLanczos::run() has finished, it will subtract this /// number from the eigenvalue before it is returned to the caller. real_t eigenvalue_offset = 0.0; /// @brief This function sets "eigenvalue_offset" automatically. /// @note Using this function is not recommended because it is very slow. /// For efficiency, set the "eigenvalue_offset" yourself. void ChooseOffset(); // The remaining data members usually can be left alone: int max_iteration; real_t eps = minimum_effective_decimal>() * 1e3; real_t tridiag_eps_ratio = 1e-1; int initial_vector_size = 200; std::function&)> init_vector = VectorRandomInitializer::init; // (for those who prefer using "Set" functions...) int SetSize(int matrix_size); void SetMul(std::function&, std::vector&)> mv_mul); void SetInitVec(std::function&)> init_vector); void SetFindMax(bool find_maximum); void SetEvalOffset(T eigenvalue_offset); void SetEpsilon(T eps); void SetTriEpsRatio(T tridiag_eps_ratio); private: static void schmidt_orth(std::vector&, const std::vector>&); real_t find_minimum_eigenvalue(const std::vector>&, const std::vector>&) const; real_t find_maximum_eigenvalue(const std::vector>&, const std::vector>&) const; static real_t tridiagonal_eigen_limit(const std::vector>&, const std::vector>&); static int num_of_eigs_smaller_than(real_t, const std::vector>&, const std::vector>&); real_t UpperBoundEvals() const; }; /// @class PEigenDense /// @brief /// PEigenDense is a class containing only one useful member function: /// PrincipalEigen(). This function calculates the principal (largest /// or smallest) eigenvalue and corresponding eigenvector of a square /// n x n matrix. This can be faster than diagonalizing the entire matrix. /// @note /// This code is a wrapper. Internally, it uses the "LambdaLanczos" class. /// @note /// For dense matrices smaller than 13x13, Jacobi::Diagonalize(), /// is usually faster than PEigenDense::PrincipleEigen(). template class PEigenDense { size_t n; // the size of the matrix std::vector evec; // preallocated vector public: PEigenDense(int matrix_size=0); /// @brief Calculate the principal eigenvalue and eigenvector of a matrix. /// @return Return the principal eigenvalue of the matrix. /// If you want the eigenvector, pass a non-null "evector" argument. real_t PrincipalEigen(ConstMatrix matrix, //!< the input matrix Vector evector, //!< the eigenvector is stored here bool find_max=false); //!< want the max or min eigenvalue? void SetSize(int matrix_size); //change matrix size after instantiation }; // class PEigenDense // -------------------------------------- // ----------- IMPLEMENTATION ----------- // -------------------------------------- // --- Implementation: Memory allocation for matrices --- template void Alloc2D(size_t nrows, // size of the array (number of rows) size_t ncols, // size of the array (number of columns) Entry ***paaX) // pointer to a 2D C-style array { //assert(paaX); *paaX = new Entry* [nrows]; //conventional 2D C array (pointer-to-pointer) (*paaX)[0] = new Entry [nrows * ncols]; // 1D C array (contiguous memor) for (size_t iy=0; iy void Dealloc2D(Entry ***paaX) // pointer to a 2D C-style array { if (paaX && *paaX) { delete [] (*paaX)[0]; delete [] (*paaX); *paaX = nullptr; } } /// @brief ConjugateProduct::prod(a,b) is a function which returns a*b by /// default. If the arguments are complex numbers, it returns conj(a)*b instead. template struct ConjugateProduct { public: static T prod(T a, T b) { return a*b; } }; template struct ConjugateProduct> { public: static std::complex prod(std::complex a, std::complex b) { return std::conj(a)*b; } }; // --- Implementation: Operations on vectors (of real and complex numbers) --- template inline T inner_prod(const std::vector& v1, const std::vector& v2) { return std::inner_product(std::begin(v1), std::end(v1), std::begin(v2), T(), [](T a, T b) -> T { return a+b; }, ConjugateProduct::prod); // T() means zero value of type T // This spec is required because std::inner_product calculates // v1*v2 not conj(v1)*v2 } template inline real_t l2_norm(const std::vector& vec) { return std::sqrt(std::real(inner_prod(vec, vec))); // The norm of any complex vector is real by definition. } template inline void scalar_mul(T1 a, std::vector& vec) { int n = vec.size(); for (int i = 0;i < n;i++) vec[i] *= a; } template inline void normalize(std::vector& vec) { scalar_mul(1.0/l2_norm(vec), vec); } template inline real_t l1_norm(const std::vector& vec) { real_t norm = real_t(); // Zero initialization for (const T& element : vec) norm += std::abs(element); return norm; } // --- Implementation: Eigendecomposition of small dense matrices --- template Jacobi:: Jacobi(int n) { _Jacobi(n, nullptr, nullptr); } template Jacobi:: Jacobi(int n, Scalar **M, int *max_idx_row) { _Jacobi(n, M, max_idx_row); } // _Jacobi() is a function which is invoked by the two constructors and it // does all of the work. (Splitting the constructor into multiple functions // was technically unnecessary, but it makes documenting the code easier.) template void Jacobi:: _Jacobi(int n, Scalar **M, int *max_idx_row) { Init(); if (M) { // if caller supplies their own "M" array, don't allocate M is_preallocated = true; this->n = n; this->M = M; this->max_idx_row = max_idx_row; //assert(this->max_idx_row); } else { is_preallocated = false; SetSize(n); // allocate the "M" and "max_int_row" arrays } } template Jacobi:: ~Jacobi() { if (! is_preallocated) Dealloc(); } template int Jacobi:: Diagonalize(ConstMatrix mat, // the matrix you wish to diagonalize (size n) Vector eval, // store the eigenvalues here Matrix evec, // store the eigenvectors here (in rows) SortCriteria sort_criteria, // sort results? bool calc_evec, // calculate the eigenvectors? int max_num_sweeps) // limit the number of iterations ("sweeps") { // -- Initialization -- for (int i = 0; i < n; i++) for (int j = i; j < n; j++) //copy mat[][] into M[][] M[i][j] = mat[i][j]; //(M[][] is a local copy we can modify) if (calc_evec) for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) evec[i][j] = (i==j) ? 1.0 : 0.0; //Set evec equal to the identity matrix for (int i = 0; i < n-1; i++) //Initialize the "max_idx_row[]" array max_idx_row[i] = MaxEntryRow(M, i); //(which is needed by MaxEntry()) // -- Iteration -- int n_iters; int max_num_iters = max_num_sweeps*n*(n-1)/2; //"sweep" = n*(n-1)/2 iters for (n_iters=0; n_iters < max_num_iters; n_iters++) { int i,j; MaxEntry(M, i, j); // Find the maximum entry in the matrix. Store in i,j // If M[i][j] is small compared to M[i][i] and M[j][j], set it to 0. if ((M[i][i] + M[i][j] == M[i][i]) && (M[j][j] + M[i][j] == M[j][j])) { M[i][j] = 0.0; max_idx_row[i] = MaxEntryRow(M,i); //must also update max_idx_row[i] } if (M[i][j] == 0.0) break; // Otherwise, apply a rotation to make M[i][j] = 0 CalcRot(M, i, j); // Calculate the parameters of the rotation matrix. ApplyRot(M, i, j); // Apply this rotation to the M matrix. if (calc_evec) // Optional: If the caller wants the eigenvectors, then ApplyRotLeft(evec,i,j); // apply the rotation to the eigenvector matrix } //for (int n_iters=0; n_iters < max_num_iters; n_iters++) // -- Post-processing -- for (int i = 0; i < n; i++) eval[i] = M[i][i]; // Optional: Sort results by eigenvalue. SortRows(eval, evec, n, sort_criteria); return (n_iters == max_num_iters); } /// brief Calculate the components of a rotation matrix which performs a /// rotation in the i,j plane by an angle (θ) that (when multiplied on /// both sides) will zero the ij'th element of M, so that afterwards /// M[i][j] = 0. The results will be stored in c, s, and t /// (which store cos(θ), sin(θ), and tan(θ), respectively). template void Jacobi:: CalcRot(Scalar const *const *M, //!< matrix int i, //!< row index int j) //!< column index { t = 1.0; // = tan(θ) Scalar M_jj_ii = (M[j][j] - M[i][i]); if (M_jj_ii != 0.0) { // kappa = (M[j][j] - M[i][i]) / (2*M[i][j]) Scalar kappa = M_jj_ii; t = 0.0; Scalar M_ij = M[i][j]; if (M_ij != 0.0) { kappa /= (2.0*M_ij); // t satisfies: t^2 + 2*t*kappa - 1 = 0 // (choose the root which has the smaller absolute value) t = 1.0 / (std::sqrt(1 + kappa*kappa) + std::abs(kappa)); if (kappa < 0.0) t = -t; } } //assert(std::abs(t) <= 1.0); c = 1.0 / std::sqrt(1 + t*t); s = c*t; } /// brief Perform a similarity transformation by multiplying matrix M on both /// sides by a rotation matrix (and its transpose) to eliminate M[i][j]. /// details This rotation matrix performs a rotation in the i,j plane by /// angle θ. This function assumes that c=cos(θ). s=som(θ), t=tan(θ) /// have been calculated previously (using the CalcRot() function). /// It also assumes that iv) are not computed. /// /// verbatim /// /// M' = R^T * M * R /// where R the rotation in the i,j plane and ^T denotes the transpose. /// i j /// _ _ /// | 1 | /// | . | /// | . | /// | 1 | /// | c ... s | /// | . . . | /// R = | . 1 . | /// | . . . | /// | -s ... c | /// | 1 | /// | . | /// | . | /// |_ 1 _| /// /// endverbatim /// /// Let M' denote the matrix M after multiplication by R^T and R. /// The components of M' are: /// M'_uv = Σ_w Σ_z R_wu * M_wz * R_zv /// /// note /// The rotation at location i,j will modify all of the matrix /// elements containing at least one index which is either i or j /// such as: M[w][i], M[i][w], M[w][j], M[j][w]. /// Check and see whether these modified matrix elements exceed the /// corresponding values in max_idx_row[] array for that row. /// If so, then update max_idx_row for that row. /// This is somewhat complicated by the fact that we must only consider /// matrix elements in the upper-right triangle strictly above the diagonal. /// (ie. matrix elements whose second index is > the first index). template void Jacobi:: ApplyRot(Scalar **M, // matrix int i, // row index int j) // column index { // Recall that c = cos(θ), s = sin(θ), t = tan(θ) (and t <= 1.0) // Compute the diagonal elements of M which have changed: M[i][i] -= t * M[i][j]; M[j][j] += t * M[i][j]; //Update the off-diagonal elements of M which will change (above the diagonal) //assert(i < j); M[i][j] = 0.0; //compute M[w][i] and M[i][w] for all w!=i,considering above-diagonal elements for (int w=0; w < i; w++) { // 0 <= w < i < j < n M[i][w] = M[w][i]; //backup the previous value. store below diagonal (i>w) M[w][i] = c*M[w][i] - s*M[w][j]; //M[w][i], M[w][j] from previous iteration if (i == max_idx_row[w]) max_idx_row[w] = MaxEntryRow(M, w); else if (std::abs(M[w][i])>std::abs(M[w][max_idx_row[w]])) max_idx_row[w]=i; //assert(max_idx_row[w] == MaxEntryRow(M, w)); } for (int w=i+1; w < j; w++) { // 0 <= i < w < j < n M[w][i] = M[i][w]; //backup the previous value. store below diagonal (w>i) M[i][w] = c*M[i][w] - s*M[w][j]; //M[i][w], M[w][j] from previous iteration } for (int w=j+1; w < n; w++) { // 0 <= i < j+1 <= w < n M[w][i] = M[i][w]; //backup the previous value. store below diagonal (w>i) M[i][w] = c*M[i][w] - s*M[j][w]; //M[i][w], M[j][w] from previous iteration } // now that we're done modifying row i, we can update max_idx_row[i] max_idx_row[i] = MaxEntryRow(M, i); //compute M[w][j] and M[j][w] for all w!=j,considering above-diagonal elements for (int w=0; w < i; w++) { // 0 <= w < i < j < n M[w][j] = s*M[i][w] + c*M[w][j]; //M[i][w], M[w][j] from previous iteration if (j == max_idx_row[w]) max_idx_row[w] = MaxEntryRow(M, w); else if (std::abs(M[w][j])>std::abs(M[w][max_idx_row[w]])) max_idx_row[w]=j; //assert(max_idx_row[w] == MaxEntryRow(M, w)); } for (int w=i+1; w < j; w++) { // 0 <= i+1 <= w < j < n M[w][j] = s*M[w][i] + c*M[w][j]; //M[w][i], M[w][j] from previous iteration if (j == max_idx_row[w]) max_idx_row[w] = MaxEntryRow(M, w); else if (std::abs(M[w][j])>std::abs(M[w][max_idx_row[w]])) max_idx_row[w]=j; //assert(max_idx_row[w] == MaxEntryRow(M, w)); } for (int w=j+1; w < n; w++) { // 0 <= i < j < w < n M[j][w] = s*M[w][i] + c*M[j][w]; //M[w][i], M[j][w] from previous iteration } // now that we're done modifying row j, we can update max_idx_row[j] max_idx_row[j] = MaxEntryRow(M, j); } //Jacobi::ApplyRot() /// brief Multiply matrix E on the left by the (previously calculated) /// rotation matrix. /// /// details /// Multiply matrix M on the LEFT side by a transposed rotation matrix, R^T. /// This matrix performs a rotation in the i,j plane by angle θ /// (where the arguments "s" and "c" refer to cos(θ) and sin(θ), respectively). /// /// verbatim /// E'_uv = Σ_w R_wu * E_wv /// endverbatim template void Jacobi:: ApplyRotLeft(Matrix E, // matrix int i, // row index int j) // column index { // recall that c = cos(θ) and s = sin(θ) for (int v = 0; v < n; v++) { Scalar Eiv = E[i][v]; //backup E[i][v] E[i][v] = c*E[i][v] - s*E[j][v]; E[j][v] = s*Eiv + c*E[j][v]; } } /// brief Find the off-diagonal index in row i whose absolute value is largest template int Jacobi:: MaxEntryRow(Scalar const *const *M, int i) const { int j_max = i+1; for (int j = i+2; j < n; j++) if (std::abs(M[i][j]) > std::abs(M[i][j_max])) j_max = j; return j_max; } /// brief Find the indices (i_max, j_max) marking the location of the /// entry in the matrix with the largest absolute value. This /// uses the max_idx_row[] array to find the answer in O(n) time. /// returns This function does not return a avalue. However after it is /// invoked, the location of the largest matrix element will be /// stored in the i_max and j_max arguments. template void Jacobi:: MaxEntry(Scalar const *const *M, int& i_max, int& j_max) const { // find the maximum entry in the matrix M in O(n) time i_max = 0; j_max = max_idx_row[i_max]; Scalar max_entry = std::abs(M[i_max][j_max]); int nm1 = n-1; for (int i=1; i < nm1; i++) { int j = max_idx_row[i]; if (std::abs(M[i][j]) > max_entry) { max_entry = std::abs(M[i][j]); i_max = i; j_max = j; } } } /// brief Sort the rows in matrix "evec" according to the numbers in "eval". template void Jacobi:: SortRows(Vector eval, // vector containing the keys used for sorting Matrix evec, // matrix whose rows will be sorted according to v int n, // size of the vector and matrix SortCriteria sort_criteria) const // sort eigenvalues? { for (int i = 0; i < n-1; i++) { int i_max = i; for (int j = i+1; j < n; j++) { // find the "maximum" element in the array starting at position i+1 switch (sort_criteria) { case SORT_DECREASING_EVALS: if (eval[j] > eval[i_max]) i_max = j; break; case SORT_INCREASING_EVALS: if (eval[j] < eval[i_max]) i_max = j; break; case SORT_DECREASING_ABS_EVALS: if (std::abs(eval[j]) > std::abs(eval[i_max])) i_max = j; break; case SORT_INCREASING_ABS_EVALS: if (std::abs(eval[j]) < std::abs(eval[i_max])) i_max = j; break; default: break; } } std::swap(eval[i], eval[i_max]); // sort "eval" for (int k = 0; k < n; k++) std::swap(evec[i][k], evec[i_max][k]); // sort "evec" } } template void Jacobi:: Init() { n = 0; M = nullptr; max_idx_row = nullptr; is_preallocated = false; } template void Jacobi:: SetSize(int n) { Dealloc(); Alloc(n); } // Implementation: Jacobi memory management: template void Jacobi:: Alloc(int n) { //assert(! is_preallocated); this->n = n; if (n > 0) { max_idx_row = new int[n]; Alloc2D(n, n, &M); } } template void Jacobi:: Dealloc() { //assert(! is_preallocated); Dealloc2D(&M); delete[] max_idx_row; max_idx_row = nullptr; Init(); } // Jacobi copy and move constructor, swap, and assignment operator: template Jacobi:: Jacobi(const Jacobi& source) { Init(); SetSize(source.n); //assert(n == source.n); // The following lines aren't really necessary, because the contents // of source.M and source.max_idx_row are not needed (since they are // overwritten every time Jacobi::Diagonalize() is invoked). std::copy(source.max_idx_row, source.max_idx_row + n, max_idx_row); for (int i = 0; i < n; i++) std::copy(source.M[i], source.M[i] + n, M[i]); } template void Jacobi:: swap(Jacobi &other) { std::swap(n, other.n); std::swap(is_preallocated, other.is_preallocated); std::swap(max_idx_row, other.max_idx_row); std::swap(M, other.M); } // Move constructor (C++11) template Jacobi:: Jacobi(Jacobi&& other) { Init(); this->swap(other); } // Using the "copy-swap" idiom for the assignment operator template Jacobi& Jacobi:: operator = (Jacobi source) { this->swap(source); return *this; } // --- Implementation: Eigendecomposition of large matrices ---- template inline LambdaLanczos::LambdaLanczos() { this->matrix_size = 0; this->max_iteration = 0; this->find_maximum = false; } template inline LambdaLanczos:: LambdaLanczos(std::function&, std::vector&)> mv_mul, int matrix_size, bool find_maximum) { this->mv_mul = mv_mul; this->matrix_size = matrix_size; this->max_iteration = matrix_size; this->find_maximum = find_maximum; } template inline int LambdaLanczos:: run(real_t& eigvalue, std::vector& eigvec) const { //assert(matrix_size > 0); //assert(0 < this->tridiag_eps_ratio && this->tridiag_eps_ratio < 1); std::vector> u; // Lanczos vectors std::vector> alpha; // Diagonal elements of an approximated tridiagonal matrix std::vector> beta; // Subdiagonal elements of an approximated tridiagonal matrix const int n = this->matrix_size; u.reserve(this->initial_vector_size); alpha.reserve(this->initial_vector_size); beta.reserve(this->initial_vector_size); u.emplace_back(n, 0.0); // Same as u.push_back(std::vector(n, 0.0)) std::vector vk(n, 0.0); real_t alphak = 0.0; alpha.push_back(alphak); real_t betak = 0.0; beta.push_back(betak); std::vector uk(n); this->init_vector(uk); normalize(uk); u.push_back(uk); real_t ev, pev; // Calculated eigenvalue and previous one pev = std::numeric_limits>::max(); int itern = this->max_iteration; for (int k = 1;k <= this->max_iteration;k++) { // vk = (A + offset*E)uk, here E is the identity matrix for (int i = 0;i < n;i++) { vk[i] = uk[i]*this->eigenvalue_offset; } this->mv_mul(uk, vk); alphak = std::real(inner_prod(u.back(), vk)); // The inner product is real. // Proof: // = // On the other hand its complex conjugate is // ^* = = = // here the condition that matrix A is a symmetric (Hermitian) is used. // Therefore // = ^* // is real. alpha.push_back(alphak); for (int i = 0;i < n; i++) { uk[i] = vk[i] - betak*u[k-1][i] - alphak*u[k][i]; } schmidt_orth(uk, u); betak = l2_norm(uk); beta.push_back(betak); if (this->find_maximum) { ev = find_maximum_eigenvalue(alpha, beta); } else { ev = find_minimum_eigenvalue(alpha, beta); } const real_t zero_threshold = minimum_effective_decimal>()*1e-1; if (betak < zero_threshold) { u.push_back(uk); // This element will never be accessed, // but this "push" guarantees u to always have one more element than // alpha and beta do. itern = k; break; } normalize(uk); u.push_back(uk); if (abs(ev-pev) < std::min(abs(ev), abs(pev))*this->eps) { itern = k; break; } else { pev = ev; } } eigvalue = ev - this->eigenvalue_offset; int m = alpha.size(); std::vector cv(m+1); cv[0] = 0.0; cv[m] = 0.0; cv[m-1] = 1.0; beta[m-1] = 0.0; if (eigvec.size() < n) { eigvec.resize(n); } for (int i = 0;i < n;i++) { eigvec[i] = cv[m-1]*u[m-1][i]; } for (int k = m-2;k >= 1;k--) { cv[k] = ((ev - alpha[k+1])*cv[k+1] - beta[k+1]*cv[k+2])/beta[k]; for (int i = 0;i < n;i++) { eigvec[i] += cv[k]*u[k][i]; } } normalize(eigvec); return itern; } //LambdaLancos::run() template inline void LambdaLanczos:: schmidt_orth(std::vector& uorth, const std::vector>& u) { // Vectors in u must be normalized, but uorth doesn't have to be. int n = uorth.size(); for (int k = 0;k < u.size();k++) { T innprod = inner_prod(uorth, u[k]); for (int i = 0;i < n;i++) uorth[i] -= innprod * u[k][i]; } } template inline real_t LambdaLanczos:: find_minimum_eigenvalue(const std::vector>& alpha, const std::vector>& beta) const { real_t eps = this->eps * this->tridiag_eps_ratio; real_t pmid = std::numeric_limits>::max(); real_t r = tridiagonal_eigen_limit(alpha, beta); real_t lower = -r; real_t upper = r; real_t mid; int nmid; // Number of eigenvalues smaller than the "mid" while (upper-lower > std::min(abs(lower), abs(upper))*eps) { mid = (lower+upper)/2.0; nmid = num_of_eigs_smaller_than(mid, alpha, beta); if (nmid >= 1) { upper = mid; } else { lower = mid; } if (mid == pmid) { break; // This avoids an infinite loop due to zero matrix } pmid = mid; } return lower; // The "lower" almost equals the "upper" here. } template inline real_t LambdaLanczos:: find_maximum_eigenvalue(const std::vector>& alpha, const std::vector>& beta) const { real_t eps = this->eps * this->tridiag_eps_ratio; real_t pmid = std::numeric_limits>::max(); real_t r = tridiagonal_eigen_limit(alpha, beta); real_t lower = -r; real_t upper = r; real_t mid; int nmid; // Number of eigenvalues smaller than the "mid" int m = alpha.size() - 1; // Number of eigenvalues of the approximated // triangular matrix, which equals the rank of it while (upper-lower > std::min(abs(lower), abs(upper))*eps) { mid = (lower+upper)/2.0; nmid = num_of_eigs_smaller_than(mid, alpha, beta); if (nmid < m) { lower = mid; } else { upper = mid; } if (mid == pmid) { break; // This avoids an infinite loop due to zero matrix } pmid = mid; } return lower; // The "lower" almost equals the "upper" here. } /// @brief /// Compute the upper bound of the absolute value of eigenvalues /// by Gerschgorin theorem. This routine gives a rough upper bound, /// but it is sufficient because the bisection routine using /// the upper bound converges exponentially. template inline real_t LambdaLanczos:: tridiagonal_eigen_limit(const std::vector>& alpha, const std::vector>& beta) { real_t r = l1_norm(alpha); r += 2*l1_norm(beta); return r; } // Algorithm from // Peter Arbenz et al. // "High Performance Algorithms for Structured Matrix Problems" // Nova Science Publishers, Inc. template inline int LambdaLanczos:: num_of_eigs_smaller_than(real_t c, const std::vector>& alpha, const std::vector>& beta) { real_t q_i = 1.0; int count = 0; int m = alpha.size(); for (int i = 1;i < m;i++){ q_i = alpha[i] - c - beta[i-1]*beta[i-1]/q_i; if (q_i < 0){ count++; } if (q_i == 0){ q_i = minimum_effective_decimal>(); } } return count; } template inline void LambdaLanczos::ChooseOffset() { const auto n = this->matrix_size; std::vector unit_vec_j(n); std::vector matrix_column_j(n); real_t eval_upper_bound = 0.0; /// According to Gershgorin theorem, the maximum (magnitude) eigenvalue should /// not exceed max_j{Σ_i|Mij|}. We can infer the contents of each column in /// the matrix by multiplying it by different unit vectors. This is slow. for (int j = 0; j < n; j++) { std::fill(unit_vec_j.begin(), unit_vec_j.end(), 0); // fill with zeros unit_vec_j[j] = 1.0; // = jth element is 1, all other elements are 0 // Multiplying the matrix by a unit vector (a vector containing only one // non-zero element at position j) extracts the jth column of the matrix. this->mv_mul(unit_vec_j, matrix_column_j); real_t sum_column = 0.0; // compute Σ_i|Mij| for (int i = 0; i < n; i++) sum_column += std::abs(matrix_column_j[i]); if (eval_upper_bound < sum_column) eval_upper_bound = sum_column; // compute max_j{Σ_i|Mij|} } if (find_maximum) this->eigenvalue_offset = eval_upper_bound; else this->eigenvalue_offset = -eval_upper_bound; } template inline int LambdaLanczos::SetSize(int matrix_size) { this->matrix_size = matrix_size; this->max_iteration = matrix_size; return matrix_size; } template inline void LambdaLanczos::SetMul(std::function&, std::vector&)> mv_mul) { this->mv_mul = mv_mul; } template inline void LambdaLanczos::SetInitVec(std::function&)> init_vector) { this->init_vector = init_vector; } template inline void LambdaLanczos::SetFindMax(bool find_maximum) { this->find_maximum = find_maximum; } template inline void LambdaLanczos::SetEvalOffset(T offset) { this->eigenvalue_offset = offset; } template inline void LambdaLanczos::SetEpsilon(T epsilon) { this->eps = epsilon; } template inline void LambdaLanczos::SetTriEpsRatio(T tri_eps_ratio) { this->tridiag_eps_ratio = tri_eps_ratio; } template inline void VectorRandomInitializer:: init(std::vector& v) { std::random_device dev; std::mt19937 mt(dev()); std::uniform_real_distribution rand((T)(-1.0), (T)(1.0)); int n = v.size(); for (int i = 0;i < n;i++) { v[i] = rand(mt); } normalize(v); } template inline void VectorRandomInitializer>:: init(std::vector>& v) { std::random_device dev; std::mt19937 mt(dev()); std::uniform_real_distribution rand((T)(-1.0), (T)(1.0)); int n = v.size(); for (int i = 0;i < n;i++) { v[i] = std::complex(rand(mt), rand(mt)); } normalize(v); } // --- Implementation of PEigenDense template void PEigenDense:: SetSize(int matrix_size) { n = matrix_size; evec.resize(n); } template PEigenDense:: PEigenDense(int matrix_size):evec(matrix_size) { SetSize(matrix_size); } template real_t PEigenDense:: PrincipalEigen(ConstMatrix matrix, Vector eigenvector, bool find_max) { //assert(n > 0); auto matmul = [&](const std::vector& in, std::vector& out) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { out[i] += matrix[i][j]*in[j]; } } }; auto init_vec = [&](std::vector& vec) { for (int i = 0; i < n; i++) vec[i] = 0.0; vec[0] = 1.0; }; // "ll_engine" calculates the eigenvalue and eigenvector. LambdaLanczos ll_engine(matmul, n, find_max); // The Lanczos algorithm selects the eigenvalue with the largest magnitude. // In order to insure that this is the one we want (maxima or minima), we can // add a constant to all of the eigenvalues by setting "eigenvalue_offset". Scalar eval_upper_bound = 0.0; for (int i = 0; i < n; i++) { Scalar sum_row = 0.0; for (int j = 0; j < n; i++) sum_row += std::abs(matrix[i][j]); if (eval_upper_bound < sum_row) eval_upper_bound = sum_row; } if (find_max) ll_engine.eigenvalue_offset = eval_upper_bound; else ll_engine.eigenvalue_offset = -eval_upper_bound; ll_engine.init_vector = init_vec; Scalar eval; // This line does all of the hard work: ll_engine.run(eval, evec); for (int i = 0; i < n; i++) eigenvector[i] = evec[i]; return eval; } } //namespace MathEigen #endif //#ifndef LMP_MATH_EIGEN_IMPL_H