-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implemented Cullum/Willoughby method for removing spurious eigenvalues. #143
base: master
Are you sure you want to change the base?
Conversation
Can one of the admins verify this patch? |
ok to test |
OK to test |
1 similar comment
OK to test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/algoithm/algorithm/
|
||
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable.
std::cout << std::endl; | ||
std::cout << std::endl; | ||
#endif | ||
return std::make_tuple(evals, evecs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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