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