Implemented Cullum/Willoughby method for removing spurious eigenvalues.#143
Implemented Cullum/Willoughby method for removing spurious eigenvalues.#143wdj wants to merge 1 commit into
Conversation
|
Can one of the admins verify this patch? |
|
ok to test |
|
OK to test |
1 similar comment
|
OK to test |
dalg24
left a comment
There was a problem hiding this comment.
Make sure to remove the print statements to the standard output and cleanup the test before we merge.
| // an eigenvalue of T2, then it is considered spurious. | ||
| // | ||
| // The C/W algorithm has other nuances which may necessitate more | ||
| // revisions of the simplified algoithm implemented here. |
|
|
||
| // The following tolerance may need adjustment, however this doesn't | ||
| // seem critical (cf. C/W vol. 1). Cullum/Willoughby use 1e-12. | ||
| const double tol2 = 5.e-12; |
There was a problem hiding this comment.
Any reason why you went for 5e-12 instead of 1e-12?
| is_repeated[i] = (i>0 && evals[i-1] >= evals[i] - tol2) || | ||
| (i<n-1 && evals[i+1] <= evals[i] + tol2); | ||
| is_marked[i] = 0==i || evals[i-1] < evals[i] - tol2; | ||
| } // i |
There was a problem hiding this comment.
Readability How about instead of declaring tol2 you introduce something like:
auto const is_strictly_ordered = [] (double eval1, double eval2) { return eval1 + 5e-12 < eval2; };And then in the body of the for-loop you do:
is_repeated = (i > 0 && !is_strictly_ordered(evals[i-1], evals[i]) ||
(i < n-1 && !is_strictly_ordered(evals[i], evals[i+1]);
is_marked = (0 == i) || !is_strictly_ordered(evals[i-1], evals[i]);I am not sure about the name yet but I do see that every time tol2 appears we are checking strict ascending order on eigenvalues and I find it hard to read in the current form (mixing eval1 >= eval2 - eps, eval2 <= eval1 + eps, etc.)
| // (only need evals, not evecs, so a different LAPACK call may be adequate) | ||
|
|
||
| std::vector<double> evals2; | ||
| std::vector<double> evecs2; |
| std::cout << std::endl; | ||
| std::cout << std::endl; | ||
| #endif | ||
| return std::make_tuple(evals, evecs); |
There was a problem hiding this comment.
Do not shadow these variables. Clear the vectors before you return or explicitly make a tuple from default constructed vectors.
| evals2.erase(evals2.begin()); | ||
| std::vector<double> sub_diagonal_aux = sub_diagonal; | ||
| sub_diagonal_aux.erase(sub_diagonal_aux.begin()); | ||
| std::vector<double> evecs_aux(n2 * n2); |
There was a problem hiding this comment.
Shadowing variables sub_diagonal_aux and evecs_aux .
| #endif | ||
|
|
||
| // Loop over original eigenvalues to check for spuriousness.. | ||
| // NOTE: assuming here evals and evals2 are in ascending order. |
There was a problem hiding this comment.
No need it is guaranteed by Lapack
| // | ||
| // The C/W algorithm has other nuances which may necessitate more | ||
| // revisions of the simplified algoithm implemented here. | ||
|
|
There was a problem hiding this comment.
Please do
ASSERT(std::is_sorted(evals.begin(), evals.end()), "eigenvalues are not in ascending order");before the next for-loop.
|
|
||
| } // n2 | ||
|
|
||
| } // n |
There was a problem hiding this comment.
I feel like it might be more readable with STL algorithms:
auto first = evals.begin();
auto const last = evals.end();
for (int i = 0; i < n; ++i) {
if (is_repeated[i]) continue;
first = std::find_if(first, last,
[&is_strictly_ordered, &evals, i](double x) {
return !is_strictly_ordered(x, evals[i]);
});
if (first != last && !is_strictly_ordered(evals[i], *first)) is_spurious[i] = true;
}@aprokop @Rombur Any opinion on this?
Disclaimer I have not actually compiled and tested the pseudo code I wrote :)
There was a problem hiding this comment.
One thing about the issue of handling the (a-tol, a+tol) interval. In the CW algorithm some attention is given to making the intervals exactly consistent regarding the multiplicity test and the spuriousness test. it is possible but I think doubtful that the inclusion or exclusion of the endpoints of the interval would make much difference. So I think the simplification of the code is probably ok --
|
@wdj I will take care of this PR. |
|
Running for 60 eigenvalues (non-deflated mode, multiplicity 1) produces correct eigenvalues but wrong eigenvectors. When looking for 15 or 30 eigenvectors, everything seems to be fine. which is completely off for a diagonal matrix. |
|
Unfortunately, it seems that the new approach also has issues with multiplicity > 1 cases. For example, searching for 20 eigenvectors in a diagonal matrix with multiplicity 2 eigenvalues results in a weird situation where during the second pass of deflation all tridiagonal eigenvalues are declared to be spurious. And, in fact, the eigenvalues of both Lanczos |
|
Reading more about Cullum, it seems that instead of computing the |
|
Further checking reveals that for multiplicity > 1 the wrong eigenvectors are coming from the first Lanczos solve. When searching for 10 eigenvalues with a single Lanczos solve for a multiplicity 2 diagonal matrix, the found eigenvalues are correct, but eigenvectors are not. |
There are two problems here. First there is a bug such that if max iterations is hit, the code does not recover gracefully but gives garbage. Adding the following line immediately after the lanczos iteration loop fixes this: "it = it < maxit ? it : maxit;". Second, the convergence tolerance is much smaller than the solver can handle. I was able to get convergence for these combinations of tolerances and required iterations: 1e-5/703, 1e-6/739, 1e-7/1791, 1e-8/2651. So 1e-11 is unfortunately far smaller than what can be handled practically. Not sure immediately whether this is due to the eigenproblem itself, the algorithm, or the implementation. Hopefully a smaller tolerance will meet the needs for the mfmg algorithm - ? |
|
So using the coarser tolerance, are the eigenvectors correct? |
|
Yes, the test passes (eigenvalues, eigenvectors) -- |
|
With the change, if max its is reached without convergence, the best approximate eigenvectors are returned, rather than garbage -- |
|
OK very nice! Thanks Wayne. @aprokop can you take a look at this PR |
|
I haven't checked the multiplicity problem yet, this may or may not fix it -- |
|
Looking at it right now. |
|
Multiplicity 1 works (I used 1e-4 tolerance), tested for up to 100 eigenvalues/eigenvectors. Multiplicity > 1 with deflation breaks down for larger number of eigenvalues (for example, multiplicity 2 works with 40 distinct eigenvalues, but breaks down with 50). The spurious eigenvalues are there, and present an interesting case of having some eigenvalues be correct but with a higher multiplicity than in the original matrix. This is certainly an improvement, and the non-deflated version of the method can be considered ready. |
|
One problem with the deflated method is that for some reason it is hard to drive the eigenpair residual norm low enough when in a deflation cycle. However I'm having some success solving this problem with the code below, which picks, from a set of Lanczos tridiagonal eigenpairs with (artificial) multiplicity, the eigenvalue from the cluster of repeats with the lowest residual error estimate. |
|
The other problem is that the deflation is not "perfect" -- a zero eigenvalue is being introduced from the projection -- there is some "noise" from iterating on the projected operator that introduces components in the null space. I have some ideas for solving this. I need to ask, can it be assumed that the operator is positive semidefinite (or, even better, positive definite)? |
It is positive semidefinite but zero is a valid eigenvalue. |
|
Got it. If in Lanczos the original operator is shifted up by 1 (times identity), then in that case the spectrum of the original operator will be clearly distinguished from a zero eigenvalue, then after the solve and removal of spurious zero one can shift down by 1 to correct. I'm going to experiment with something like that. |
See attached files for details.
The Lanczos unit tests pass, even with a new, tighter convergence tolerance and use of the percent overshoot option.
spurious_eigenvalues_fix.txt