1 // Copyright (c) 2017-2021, Lawrence Livermore National Security, LLC and
2 // other Axom Project Developers. See the top-level LICENSE file for details.
3 //
4 // SPDX-License-Identifier: (BSD-3-Clause)
5 
6 #ifndef AXOM_JACOBI_EIGENSOLVE_HPP_
7 #define AXOM_JACOBI_EIGENSOLVE_HPP_
8 
9 #include "axom/core/Macros.hpp"  // for AXOM_STATIC_ASSERT
10 
11 #include "axom/core/numerics/Matrix.hpp"      // for numerics::Matrix
12 #include "axom/core/numerics/eigen_sort.hpp"  // for numerics::eigen_sort()
13 #include "axom/core/utilities/Utilities.hpp"  // for abs(), isNearlyEqual()
14 
15 namespace axom
16 {
17 namespace numerics
18 {
19 constexpr double JACOBI_DEFAULT_TOLERANCE = 1.e-18;
20 constexpr int JACOBI_DEFAULT_MAX_ITERATIONS = 20;
21 constexpr int JACOBI_EIGENSOLVE_SUCCESS = 0;
22 constexpr int JACOBI_EIGENSOLVE_FAILURE = -1;
23 
24 /*!
25  * \brief Computes the eigenvalues and eigenvectors of a real symmetric matrix
26  *  using the Jacobi iteration method.
27  *
28  * \param [in]  A the input matrix whose eigenpairs will be computed
29  * \param [out] V matrix to store the eigenvectors of the input matrix
30  * \param [out] lambdas buffer where the computed eigenvalues will be stored.
31  * \param [in] maxIterations the maximum number of iterations (optional)
32  * \param [out] numIterations the number of actual iterations (optional)
33  * \param [in] TOL convergence tolerance. Default is set to 1.e-18 (optional)
34  *
35  * \return status returns JACOBI_EIGENSOLVE_SUCCESS on success, otherwise,
36  *  JACOBI_EIGENSOLVE_FAILURE is returned.
37  *
38  * \note A return status of JACOBI_EIGENSOLVE_FAILURE, can indicate:
39  *  ( a ) a problem with the supplied input, e.g., nullptr etc.
40  *  ( b ) the method did not converge in the specified number of max iterations.
41  *
42  * \note The supplied matrix is expected to be a real symmetric matrix.
43  *
44  * \note The Jacobi method is absolutely foolproof for all real symmetric
45  *  matrices. It is efficient for moderate size matrices and generally can
46  *  evaluate the smaller eigenvalues with better relative accuracy than other
47  *  methods that convert the matrix to tridiagonal form, e.g., QR. However,
48  *  for matrices of order greater than, e.g., 20, the Jacobi algorithm is
49  *  generally slower by a significant constant factor.
50  *
51  * \note The Jacobi iteration will generally converge within 6-15 iterations.
52  *  The default max number of iterations is set to 20, but, the caller may
53  *  specify a different value if necessary.
54  *
55  * \note The implementation of this method is based on the algorithm described
56  *  in Numerical Recipes, Chapter 11.1, "Jacobi Transformations of a Symmetric
57  *  Matrix", p. 570.
58  *
59  * \pre A.isSquare() == true
60  * \pre V.isSquare() == true
61  * \pre lambdas != nullptr
62  */
63 template <typename T>
64 int jacobi_eigensolve(Matrix<T> A,
65                       Matrix<T>& V,
66                       T* lambdas,
67                       int maxIterations = JACOBI_DEFAULT_MAX_ITERATIONS,
68                       int* numIterations = nullptr,
69                       T TOL = JACOBI_DEFAULT_TOLERANCE);
70 
71 //------------------------------------------------------------------------------
72 // IMPLEMENTATION
73 //------------------------------------------------------------------------------
74 template <typename T>
jacobi_eigensolve(Matrix<T> A,Matrix<T> & V,T * lambdas,int maxIterations,int * numIterations,T TOL)75 int jacobi_eigensolve(Matrix<T> A,
76                       Matrix<T>& V,
77                       T* lambdas,
78                       int maxIterations,
79                       int* numIterations,
80                       T TOL)
81 {
82   bool converged = false;
83   const int n = A.getNumRows();
84 
85   AXOM_STATIC_ASSERT_MSG(std::is_floating_point<T>::value,
86                          "pre: T is a floating point type");
87   assert("pre: input matrix must be square" && A.isSquare());
88   assert("pre: can't have more eigenvectors than rows" && (n <= A.getNumRows()));
89   assert("pre: lambdas vector is null" && (lambdas != nullptr));
90 
91   if(!A.isSquare() || !V.isSquare())
92   {
93     return JACOBI_EIGENSOLVE_FAILURE;
94   }
95 
96   T* bw = axom::allocate<T>(n);
97   T* zw = axom::allocate<T>(n);
98 
99   // initialize
100   for(int i = 0; i < n; ++i)
101   {
102     lambdas[i] = A(i, i);
103     bw[i] = lambdas[i];
104     zw[i] = 0.0;
105 
106     for(int j = 0; j < n; ++j)
107     {
108       V(i, j) = (i == j) ? 1.0 : 0.0;
109     }
110   }
111 
112   // Jacobi solve
113   for(int iter = 0; iter < maxIterations; ++iter)
114   {
115     // compute the sum of all elements in the upper triangular portion of A.
116     // The convergence criterion (thresh) is based on the absolute value of
117     // this sum.
118     T sum = 0.0;
119 
120     for(int i = 0; i < n; ++i)
121     {
122       for(int j = i + 1; j < n; ++j)
123       {
124         sum += A(i, j) * A(i, j);
125       }
126     }
127 
128     const T thresh = sqrt(sum) / (4.0 * static_cast<T>(n));
129 
130     if(utilities::isNearlyEqual(thresh, 0.0, TOL))
131     {
132       converged = true;
133       break;
134     }
135 
136     // if not converged, then perform next iteration.
137     for(int p = 0; p < n; ++p)
138     {
139       for(int q = p + 1; q < n; ++q)
140       {
141         T gapq = 10.0 * utilities::abs(A(p, q));
142         T termp = gapq + utilities::abs(lambdas[p]);
143         T termq = gapq + utilities::abs(lambdas[q]);
144 
145         // the Jacobi iteration ignores off diagonal elements close to zero
146         if(4 < iter && termp == utilities::abs(lambdas[p]) &&
147            termq == utilities::abs(lambdas[q]))
148         {
149           A(p, q) = 0.0;
150         }
151         else if(thresh <= utilities::abs(A(p, q)))
152         {
153           T h = lambdas[q] - lambdas[p];
154           T term = utilities::abs(h) + gapq;
155 
156           T t;
157 
158           if(term == utilities::abs(h))
159           {
160             t = A(p, q) / h;
161           }
162           else
163           {
164             T theta = 0.5 * h / A(p, q);
165             t = 1.0 / (utilities::abs(theta) + sqrt(1.0 + theta * theta));
166 
167             if(theta < 0.0)
168             {
169               t = -t;
170             }
171           }
172 
173           // compute Givens rotation terms: c = cos(theta), s = sin(theta)
174           T c = 1.0 / sqrt(1.0 + t * t);
175           T s = t * c;
176           T tau = s / (1.0 + c);
177           h = t * A(p, q);
178 
179           // accumulate corrections to diagonals
180           zw[p] += -h;
181           zw[q] += h;
182           lambdas[p] += -h;
183           lambdas[q] += h;
184 
185           A(p, q) = 0.0;
186 
187           // perform the rotation using information from upper triangle of A
188           for(int j = 0; j < p; ++j)
189           {
190             T g1 = A(j, p);
191             T g2 = A(j, q);
192             A(j, p) = g1 - s * (g2 + g1 * tau);
193             A(j, q) = g2 + s * (g1 - g2 * tau);
194           }
195 
196           for(int j = p + 1; j < q; ++j)
197           {
198             T g1 = A(p, j);
199             T g2 = A(j, q);
200             A(p, j) = g1 - s * (g2 + g1 * tau);
201             A(j, q) = g2 + s * (g1 - g2 * tau);
202           }
203 
204           for(int j = q + 1; j < n; ++j)
205           {
206             T g1 = A(p, j);
207             T g2 = A(q, j);
208             A(p, j) = g1 - s * (g2 + g1 * tau);
209             A(q, j) = g2 + s * (g1 - g2 * tau);
210           }
211 
212           // accumulate results into eigenvector matrix
213           for(int j = 0; j < n; ++j)
214           {
215             T g1 = V(j, p);
216             T g2 = V(j, q);
217             V(j, p) = g1 - s * (g2 + g1 * tau);
218             V(j, q) = g2 + s * (g1 - g2 * tau);
219           }
220 
221         }  // END else if
222       }    // END for all q
223     }      // END for all p
224 
225     for(int i = 0; i < n; ++i)
226     {
227       bw[i] += zw[i];
228       lambdas[i] = bw[i];
229       zw[i] = 0.0;
230     }
231 
232     // update number of actual iterations (if specified)
233     if(numIterations != nullptr)
234     {
235       (*numIterations)++;
236     }
237 
238   }  // END for all iterations
239 
240   // sort eigenvalues in ascending order
241   eigen_sort(lambdas, V);
242 
243   axom::deallocate(bw);
244   axom::deallocate(zw);
245 
246   return ((converged) ? JACOBI_EIGENSOLVE_SUCCESS : JACOBI_EIGENSOLVE_FAILURE);
247 }
248 
249 } /* end namespace numerics */
250 } /* end namespace axom */
251 
252 #endif /* AXOM_JACOBI_EIGENSOLVE_HPP_ */
253