Files
lammps/src/math_eigen_impl.h
Axel Kohlmeyer a8220a8502 cosmetic changes
2021-09-15 16:08:53 -04:00

1375 lines
47 KiB
C++

// 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 <cassert>
#include <numeric>
#include <complex>
#include <limits>
#include <cmath>
#include <vector>
#include <random>
#include <functional>
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<typename Entry>
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<typename Entry>
void Dealloc2D(Entry ***paaX); // pointer to 2D multidimensional array
// --- Complex numbers ---
/// @brief
/// "realTypeMap" struct is used to the define "real_t<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<T>" returns the C++ type corresponding to the real component of T.
///
/// @details
/// For example, suppose you have a matrix of type std::complex<double>**.
/// The eigenvalues calculated by "LambdaLanczos" and "PEigenDense" should be
/// of type "double" (which is the same as "real_T<std::complex<double>>"),
/// not "std::complex<double>". (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<T>" is defined using C++ template
/// specializations.
template <typename T>
struct realTypeMap {
typedef T type;
};
template <typename T>
struct realTypeMap<std::complex<T>> {
typedef T type;
};
template <typename T>
using real_t = typename realTypeMap<T>::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 <typename T>
T inner_prod(const std::vector<T>& v1, const std::vector<T>& v2);
// Compute the sum of the absolute values of the entries in v
// returns a real number (of type real_t<T>).
template <typename T>
real_t<T> l1_norm(const std::vector<T>& v);
// Calculate the l2_norm (Euclidian length) of vector v.
// returns a real number (of type real_t<T>).
template <typename T>
real_t<T> l2_norm(const std::vector<T>& v);
/// Multiply a vector (v) by a scalar (c).
template <typename T1, typename T2>
void scalar_mul(T1 c, std::vector<T2>& v);
/// Divide vector "v" in-place by it's length (l2_norm(v)).
template <typename T>
void normalize(std::vector<T>& 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<typename Scalar,
typename Vector,
typename Matrix,
typename ConstMatrix=Matrix>
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,//!<sort results?
bool calc_evecs=true, //!< calculate the eigenvectors?
int max_num_sweeps=50 //!< limit the number of iterations
);
// An alternative constructor is provided, if, for some reason,
// you want to avoid allocating memory on the heap during
// initialization. In that case, you can allocate space for the "M"
// and "max_idx_row" arrays on the stack in advance, and pass them
// to the constructor. (The vast majority of users probably
// do not need this feature. Unless you do, I encourage you
// to use the regular constructor instead.)
// n the size (ie. number of rows) of the (square) matrix.
// M optional preallocated n x n array
// max_idx_row optional preallocated array of size n
// note If either the "M" or "max_idx_row" arguments are specified,
// they both must be specified.
Jacobi(int n, Scalar **M, int *max_idx_row);
private:
bool is_preallocated;
// (Descriptions of private functions can be found in their implementation.)
void _Jacobi(int n, Scalar **M, int *max_idx_row);
void CalcRot(Scalar const *const *M, int i, int j);
void ApplyRot(Scalar **M, int i, int j);
void ApplyRotLeft(Matrix E, int i, int j);
int MaxEntryRow(Scalar const *const *M, int i) const;
void MaxEntry(Scalar const *const *M, int& i_max, int& j_max) const;
void SortRows(Vector v, Matrix M, int n, SortCriteria s=SORT_DECREASING_EVALS) const;
void Init();
void Alloc(int n);
void Dealloc();
public:
// C++ boilerplate: copy and move constructor, swap, and assignment operator
Jacobi(const Jacobi<Scalar, Vector, Matrix, ConstMatrix>& source);
Jacobi(Jacobi<Scalar, Vector, Matrix, ConstMatrix>&& other);
void swap(Jacobi<Scalar, Vector, Matrix, ConstMatrix> &other);
Jacobi<Scalar, Vector, Matrix, ConstMatrix>& operator = (Jacobi<Scalar, Vector, Matrix, ConstMatrix> 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 <typename T>
struct VectorRandomInitializer {
public:
static void init(std::vector<T>&);
};
template <typename T>
struct VectorRandomInitializer<std::complex<T>> {
public:
static void init(std::vector<std::complex<T>>&);
};
// Return the number of significant decimal digits of type T.
template <typename T>
inline constexpr int sig_decimal_digit() {
return (int)(std::numeric_limits<T>::digits *
std::log10(std::numeric_limits<T>::radix));
}
// Return 10^-n where n=number of significant decimal digits of type T.
template <typename T>
inline constexpr T minimum_effective_decimal() {
return std::pow(10, -sig_decimal_digit<T>());
}
/// @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 <typename T>
class LambdaLanczos {
public:
LambdaLanczos();
LambdaLanczos(std::function<void(const std::vector<T>&, std::vector<T>&)> mv_mul, int matrix_size, bool find_maximum);
LambdaLanczos(std::function<void(const std::vector<T>&, std::vector<T>&)> 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<T>&, std::vector<T>&) 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<void(const std::vector<T>&, std::vector<T>&)> 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<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<T> eps = minimum_effective_decimal<real_t<T>>() * 1e3;
real_t<T> tridiag_eps_ratio = 1e-1;
int initial_vector_size = 200;
std::function<void(std::vector<T>&)> init_vector =
VectorRandomInitializer<T>::init;
// (for those who prefer using "Set" functions...)
int SetSize(int matrix_size);
void SetMul(std::function<void(const std::vector<T>&,
std::vector<T>&)> mv_mul);
void SetInitVec(std::function<void(std::vector<T>&)> 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<T>&, const std::vector<std::vector<T>>&);
real_t<T> find_minimum_eigenvalue(const std::vector<real_t<T>>&,
const std::vector<real_t<T>>&) const;
real_t<T> find_maximum_eigenvalue(const std::vector<real_t<T>>&,
const std::vector<real_t<T>>&) const;
static real_t<T> tridiagonal_eigen_limit(const std::vector<real_t<T>>&,
const std::vector<real_t<T>>&);
static int num_of_eigs_smaller_than(real_t<T>,
const std::vector<real_t<T>>&,
const std::vector<real_t<T>>&);
real_t<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<typename Scalar, typename Vector, typename ConstMatrix>
class PEigenDense
{
size_t n; // the size of the matrix
std::vector<Scalar> 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<Scalar>
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<typename Entry>
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<nrows; iy++)
(*paaX)[iy] = (*paaX)[0] + iy*ncols;
// The caller can access the contents using (*paaX)[i][j]
}
template<typename Entry>
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 <typename T>
struct ConjugateProduct {
public:
static T prod(T a, T b) { return a*b; }
};
template <typename T>
struct ConjugateProduct<std::complex<T>> {
public:
static std::complex<T> prod(std::complex<T> a, std::complex<T> b) {
return std::conj(a)*b;
}
};
// --- Implementation: Operations on vectors (of real and complex numbers) ---
template <typename T>
inline T inner_prod(const std::vector<T>& v1, const std::vector<T>& v2) {
return std::inner_product(std::begin(v1), std::end(v1),
std::begin(v2), T(),
[](T a, T b) -> T { return a+b; },
ConjugateProduct<T>::prod);
// T() means zero value of type T
// This spec is required because std::inner_product calculates
// v1*v2 not conj(v1)*v2
}
template <typename T>
inline real_t<T> l2_norm(const std::vector<T>& vec) {
return std::sqrt(std::real(inner_prod(vec, vec)));
// The norm of any complex vector <v|v> is real by definition.
}
template <typename T1, typename T2>
inline void scalar_mul(T1 a, std::vector<T2>& vec) {
int n = vec.size();
for (int i = 0;i < n;i++)
vec[i] *= a;
}
template <typename T>
inline void normalize(std::vector<T>& vec) {
scalar_mul(1.0/l2_norm(vec), vec);
}
template <typename T>
inline real_t<T> l1_norm(const std::vector<T>& vec) {
real_t<T> norm = real_t<T>(); // Zero initialization
for (const T& element : vec)
norm += std::abs(element);
return norm;
}
// --- Implementation: Eigendecomposition of small dense matrices ---
template<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
Jacobi(int n) {
_Jacobi(n, nullptr, nullptr);
}
template<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void
Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
_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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
~Jacobi() {
if (! is_preallocated)
Dealloc();
}
template<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
int Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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 i<j. The max_idx_row[] array is also updated.
/// To save time, since the matrix is symmetric, the elements
/// below the diagonal (ie. M[u][v] where u>v) 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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
int Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
Init() {
n = 0;
M = nullptr;
max_idx_row = nullptr;
is_preallocated = false;
}
template<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
SetSize(int n) {
Dealloc();
Alloc(n);
}
// Implementation: Jacobi memory management:
template<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
Alloc(int n) {
//assert(! is_preallocated);
this->n = n;
if (n > 0) {
max_idx_row = new int[n];
Alloc2D(n, n, &M);
}
}
template<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
Jacobi(const Jacobi<Scalar, Vector, Matrix, ConstMatrix>& 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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
void Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
swap(Jacobi<Scalar, Vector, Matrix, ConstMatrix> &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<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
Jacobi(Jacobi<Scalar, Vector, Matrix, ConstMatrix>&& other) {
Init();
this->swap(other);
}
// Using the "copy-swap" idiom for the assignment operator
template<typename Scalar,typename Vector,typename Matrix,typename ConstMatrix>
Jacobi<Scalar, Vector, Matrix, ConstMatrix>&
Jacobi<Scalar, Vector, Matrix, ConstMatrix>::
operator = (Jacobi<Scalar, Vector, Matrix, ConstMatrix> source) {
this->swap(source);
return *this;
}
// --- Implementation: Eigendecomposition of large matrices ----
template <typename T>
inline LambdaLanczos<T>::LambdaLanczos() {
this->matrix_size = 0;
this->max_iteration = 0;
this->find_maximum = false;
}
template <typename T>
inline LambdaLanczos<T>::
LambdaLanczos(std::function<void(const std::vector<T>&,
std::vector<T>&)> 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 <typename T>
inline int LambdaLanczos<T>::
run(real_t<T>& eigvalue, std::vector<T>& eigvec) const
{
//assert(matrix_size > 0);
//assert(0 < this->tridiag_eps_ratio && this->tridiag_eps_ratio < 1);
std::vector<std::vector<T>> u; // Lanczos vectors
std::vector<real_t<T>> alpha; // Diagonal elements of an approximated tridiagonal matrix
std::vector<real_t<T>> 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<T>(n, 0.0))
std::vector<T> vk(n, 0.0);
real_t<T> alphak = 0.0;
alpha.push_back(alphak);
real_t<T> betak = 0.0;
beta.push_back(betak);
std::vector<T> uk(n);
this->init_vector(uk);
normalize(uk);
u.push_back(uk);
real_t<T> ev, pev; // Calculated eigenvalue and previous one
pev = std::numeric_limits<real_t<T>>::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 <uk|vk> is real.
// Proof:
// <uk|vk> = <uk|A|uk>
// On the other hand its complex conjugate is
// <uk|vk>^* = <vk|uk> = <uk|A^*|uk> = <uk|A|uk>
// here the condition that matrix A is a symmetric (Hermitian) is used.
// Therefore
// <uk|vk> = <vk|uk>^*
// <uk|vk> 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<T> zero_threshold = minimum_effective_decimal<real_t<T>>()*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<T> 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 <typename T>
inline void LambdaLanczos<T>::
schmidt_orth(std::vector<T>& uorth, const std::vector<std::vector<T>>& 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 <typename T>
inline real_t<T> LambdaLanczos<T>::
find_minimum_eigenvalue(const std::vector<real_t<T>>& alpha,
const std::vector<real_t<T>>& beta) const
{
real_t<T> eps = this->eps * this->tridiag_eps_ratio;
real_t<T> pmid = std::numeric_limits<real_t<T>>::max();
real_t<T> r = tridiagonal_eigen_limit(alpha, beta);
real_t<T> lower = -r;
real_t<T> upper = r;
real_t<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 <typename T>
inline real_t<T> LambdaLanczos<T>::
find_maximum_eigenvalue(const std::vector<real_t<T>>& alpha,
const std::vector<real_t<T>>& beta) const
{
real_t<T> eps = this->eps * this->tridiag_eps_ratio;
real_t<T> pmid = std::numeric_limits<real_t<T>>::max();
real_t<T> r = tridiagonal_eigen_limit(alpha, beta);
real_t<T> lower = -r;
real_t<T> upper = r;
real_t<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 <typename T>
inline real_t<T> LambdaLanczos<T>::
tridiagonal_eigen_limit(const std::vector<real_t<T>>& alpha,
const std::vector<real_t<T>>& beta)
{
real_t<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 <typename T>
inline int LambdaLanczos<T>::
num_of_eigs_smaller_than(real_t<T> c,
const std::vector<real_t<T>>& alpha,
const std::vector<real_t<T>>& beta)
{
real_t<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<real_t<T>>();
}
}
return count;
}
template <typename T>
inline void LambdaLanczos<T>::ChooseOffset() {
const auto n = this->matrix_size;
std::vector<T> unit_vec_j(n);
std::vector<T> matrix_column_j(n);
real_t<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<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 <typename T>
inline int LambdaLanczos<T>::SetSize(int matrix_size)
{
this->matrix_size = matrix_size;
this->max_iteration = matrix_size;
return matrix_size;
}
template <typename T>
inline void LambdaLanczos<T>::SetMul(std::function<void(const std::vector<T>&, std::vector<T>&)> mv_mul)
{
this->mv_mul = mv_mul;
}
template <typename T>
inline void LambdaLanczos<T>::SetInitVec(std::function<void(std::vector<T>&)> init_vector)
{
this->init_vector = init_vector;
}
template <typename T>
inline void LambdaLanczos<T>::SetFindMax(bool find_maximum) {
this->find_maximum = find_maximum;
}
template <typename T>
inline void LambdaLanczos<T>::SetEvalOffset(T offset)
{
this->eigenvalue_offset = offset;
}
template <typename T>
inline void LambdaLanczos<T>::SetEpsilon(T epsilon)
{
this->eps = epsilon;
}
template <typename T>
inline void LambdaLanczos<T>::SetTriEpsRatio(T tri_eps_ratio)
{
this->tridiag_eps_ratio = tri_eps_ratio;
}
template <typename T>
inline void VectorRandomInitializer<T>::
init(std::vector<T>& v)
{
std::random_device dev;
std::mt19937 mt(dev());
std::uniform_real_distribution<T> 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 <typename T>
inline void VectorRandomInitializer<std::complex<T>>::
init(std::vector<std::complex<T>>& v)
{
std::random_device dev;
std::mt19937 mt(dev());
std::uniform_real_distribution<T> rand((T)(-1.0), (T)(1.0));
int n = v.size();
for (int i = 0;i < n;i++) {
v[i] = std::complex<T>(rand(mt), rand(mt));
}
normalize(v);
}
// --- Implementation of PEigenDense
template<typename Scalar, typename Vector, typename ConstMatrix>
void PEigenDense<Scalar, Vector, ConstMatrix>::
SetSize(int matrix_size) {
n = matrix_size;
evec.resize(n);
}
template<typename Scalar, typename Vector, typename ConstMatrix>
PEigenDense<Scalar, Vector, ConstMatrix>::
PEigenDense(int matrix_size):evec(matrix_size) {
SetSize(matrix_size);
}
template<typename Scalar, typename Vector, typename ConstMatrix>
real_t<Scalar> PEigenDense<Scalar, Vector, ConstMatrix>::
PrincipalEigen(ConstMatrix matrix,
Vector eigenvector,
bool find_max)
{
//assert(n > 0);
auto matmul = [&](const std::vector<Scalar>& in, std::vector<Scalar>& 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<Scalar>& vec) {
for (int i = 0; i < n; i++)
vec[i] = 0.0;
vec[0] = 1.0;
};
// "ll_engine" calculates the eigenvalue and eigenvector.
LambdaLanczos<Scalar> 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