Skip to content

Commit 564bb77

Browse files
committed
Improve comments on LOBPCG
1 parent e9f9f6a commit 564bb77

File tree

1 file changed

+75
-60
lines changed

1 file changed

+75
-60
lines changed

src/lobpcg/lobpcg.rs

Lines changed: 75 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ fn ndarray_mask<A: Scalar>(matrix: ArrayView2<A>, mask: &[bool]) -> Array2<A> {
7373
/// This functions takes a matrix `v` and constraint matrix `y` and orthogonalize the `v` to `y`.
7474
fn apply_constraints<A: Scalar + Lapack>(
7575
mut v: ArrayViewMut<A, Ix2>,
76-
fact_yy: &CholeskyFactorized<OwnedRepr<A>>,
76+
cholesky_yy: &CholeskyFactorized<OwnedRepr<A>>,
7777
y: ArrayView2<A>,
7878
) {
7979
let gram_yv = y.t().dot(&v);
@@ -82,7 +82,7 @@ fn apply_constraints<A: Scalar + Lapack>(
8282
.gencolumns()
8383
.into_iter()
8484
.map(|x| {
85-
let res = fact_yy.solvec(&x).unwrap();
85+
let res = cholesky_yy.solvec(&x).unwrap();
8686

8787
res.to_vec()
8888
})
@@ -120,23 +120,22 @@ fn orthonormalize<T: Scalar + Lapack>(v: Array2<T>) -> Result<(Array2<T>, Array2
120120
///
121121
/// # Arguments
122122
/// * `a` - An operator defining the problem, usually a sparse (sometimes also dense) matrix
123-
/// multiplication. Also called the "Stiffness matrix".
124-
/// * `x` - Initial approximation to the k eigenvectors. If `a` has shape=(n,n), then `x` should
123+
/// multiplication. Also called the "stiffness matrix".
124+
/// * `x` - Initial approximation of the k eigenvectors. If `a` has shape=(n,n), then `x` should
125125
/// have shape=(n,k).
126-
/// * `m` - Preconditioner to `a`, by default the identity matrix. In the optimal case `m`
127-
/// approximates the inverse of `a`.
126+
/// * `m` - Preconditioner to `a`, by default the identity matrix. Should approximate the inverse
127+
/// of `a`.
128128
/// * `y` - Constraints of (n,size_y), iterations are performed in the orthogonal complement of the
129129
/// column-space of `y`. It must be full rank.
130-
/// * `tol` - The tolerance values defines at which point the solver stops the optimization. The l2-norm
131-
/// of the residual is compared to this value and the eigenvalue approximation returned if below
132-
/// the threshold.
130+
/// * `tol` - The tolerance values defines at which point the solver stops the optimization. The approximation
131+
/// of a eigenvalue stops when then l2-norm of the residual is below this threshold.
133132
/// * `maxiter` - The maximal number of iterations
134133
/// * `order` - Whether to solve for the largest or lowest eigenvalues
135134
///
136135
/// The function returns an `EigResult` with the eigenvalue/eigenvector and achieved residual norm
137136
/// for it. All iterations are tracked and the optimal solution returned. In case of an error a
138137
/// special variant `EigResult::NotConverged` additionally carries the error. This can happen when
139-
/// the precision of the matrix is too low (switch from `f32` to `f64` for example).
138+
/// the precision of the matrix is too low (switch then from `f32` to `f64` for example).
140139
pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default, F: Fn(ArrayView2<A>) -> Array2<A>, G: Fn(ArrayViewMut2<A>)>(
141140
a: F,
142141
mut x: Array2<A>,
@@ -163,37 +162,37 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
163162
// cap the number of iteration
164163
let mut iter = usize::min(n * 10, maxiter);
165164

166-
// factorize yy for later use
167-
let fact_yy = match y {
168-
Some(ref y) => {
169-
let fact_yy = y.t().dot(y).factorizec(UPLO::Lower).unwrap();
165+
// calculate cholesky factorization of YY' and apply constraints to initial guess
166+
let cholesky_yy = y.as_ref().map(|y| {
167+
let cholesky_yy = y.t().dot(y).factorizec(UPLO::Lower).unwrap();
168+
apply_constraints(x.view_mut(), &cholesky_yy, y.view());
169+
cholesky_yy
170+
});
170171

171-
apply_constraints(x.view_mut(), &fact_yy, y.view());
172-
Some(fact_yy)
173-
}
174-
None => None,
175-
};
176-
177-
// orthonormalize the initial guess and calculate matrices AX and XAX
172+
// orthonormalize the initial guess
178173
let (x, _) = match orthonormalize(x) {
179174
Ok(x) => x,
180175
Err(err) => return EigResult::NoResult(err),
181176
};
182177

178+
// calculate AX and XAX for Rayleigh quotient
183179
let ax = a(x.view());
184180
let xax = x.t().dot(&ax);
185181

186-
// perform eigenvalue decomposition of XAX as initialization
182+
// perform eigenvalue decomposition of XAX
187183
let (mut lambda, eig_block) = match sorted_eig(xax.view(), None, size_x, &order) {
188184
Ok(x) => x,
189185
Err(err) => return EigResult::NoResult(err),
190186
};
191187

192-
// initiate X and AX with eigenvector
188+
// initiate approximation of the eigenvector
193189
let mut x = x.dot(&eig_block);
194190
let mut ax = ax.dot(&eig_block);
195191

192+
// track residual below threshold
196193
let mut activemask = vec![true; size_x];
194+
195+
// track residuals and best result
197196
let mut residual_norms_history = Vec::new();
198197
let mut best_result = None;
199198

@@ -203,7 +202,7 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
203202
let ident0: Array2<A> = Array2::eye(size_x);
204203
let two: A = NumCast::from(2.0).unwrap();
205204

206-
let mut ap: Option<(Array2<A>, Array2<A>)> = None;
205+
let mut previous_p_ap: Option<(Array2<A>, Array2<A>)> = None;
207206
let mut explicit_gram_flag = true;
208207

209208
let final_norm = loop {
@@ -245,8 +244,8 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
245244
// apply preconditioner
246245
m(active_block_r.view_mut());
247246
// apply constraints to the preconditioned residuals
248-
if let (Some(ref y), Some(ref fact_yy)) = (&y, &fact_yy) {
249-
apply_constraints(active_block_r.view_mut(), fact_yy, y.view());
247+
if let (Some(ref y), Some(ref cholesky_yy)) = (&y, &cholesky_yy) {
248+
apply_constraints(active_block_r.view_mut(), cholesky_yy, y.view());
250249
}
251250
// orthogonalize the preconditioned residual to x
252251
active_block_r -= &x.dot(&x.t().dot(&active_block_r));
@@ -273,6 +272,9 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
273272
let xar = x.t().dot(&ar);
274273
let mut rar = r.t().dot(&ar);
275274

275+
// for small residuals calculate covariance matrices explicitely, otherwise approximate
276+
// them such that X is orthogonal and uncorrelated to the residual R and use eigenvalues of
277+
// previous decomposition
276278
let (xax, xx, rr, xr) = if explicit_gram_flag {
277279
rar = (&rar + &rar.t()) / two;
278280
let xax = x.t().dot(&ax);
@@ -292,14 +294,16 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
292294
)
293295
};
294296

295-
let p_ap = ap.as_ref()
297+
// mask and orthonormalize P and AP
298+
let p_ap = previous_p_ap.as_ref()
296299
.and_then(|(p, ap)| {
297300
let active_p = ndarray_mask(p.view(), &activemask);
298301
let active_ap = ndarray_mask(ap.view(), &activemask);
299302

300303
orthonormalize(active_p).map(|x| (active_ap, x)).ok()
301304
})
302305
.and_then(|(active_ap, (active_p, p_r))| {
306+
// orthonormalize AP with R^{-1} of A
303307
let active_ap = active_ap.reversed_axes();
304308
p_r.solve_triangular(UPLO::Lower, Diag::NonUnit, &active_ap)
305309
.map(|active_ap| (active_p, active_ap.reversed_axes()))
@@ -351,52 +355,63 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
351355
)
352356
};
353357

358+
// apply Rayleigh-Ritz method for (A - lambda) to calculate optimal expansion coefficients
354359
let (new_lambda, eig_vecs) = match sorted_eig(gram_a.view(), Some(gram_b.view()), size_x, &order) {
355360
Ok(x) => x,
356361
Err(err) => {
357362
// restart if the eigproblem decomposition failed
358-
if ap.is_some() {
359-
ap = None;
363+
if previous_p_ap.is_some() {
364+
previous_p_ap = None;
360365
continue;
361-
} else {
366+
} else { // or break if restart is not possible
362367
break Err(err);
363368
}
364369
}
365370
};
366371
lambda = new_lambda;
367372

368-
let (pp, app, eig_x) = if let Some((active_p, active_ap)) = p_ap
373+
// approximate eigenvector X and conjugate vectors P with solution of eigenproblem
374+
let (p, ap, tau) = if let Some((active_p, active_ap)) = p_ap
369375
{
370-
let eig_x = eig_vecs.slice(s![..size_x, ..]);
371-
let eig_r = eig_vecs.slice(s![size_x..size_x + current_block_size, ..]);
372-
let eig_p = eig_vecs.slice(s![size_x + current_block_size.., ..]);
373-
374-
let pp = r.dot(&eig_r) + active_p.dot(&eig_p);
375-
let app = ar.dot(&eig_r) + active_ap.dot(&eig_p);
376-
377-
(pp, app, eig_x)
376+
// tau are eigenvalues to basis of X
377+
let tau = eig_vecs.slice(s![..size_x, ..]);
378+
// alpha are eigenvalues to basis of R
379+
let alpha = eig_vecs.slice(s![size_x..size_x + current_block_size, ..]);
380+
// gamma are eigenvalues to basis of P
381+
let gamma = eig_vecs.slice(s![size_x + current_block_size.., ..]);
382+
383+
// update AP and P in span{R, P} as linear combination
384+
let updated_p = r.dot(&alpha) + active_p.dot(&gamma);
385+
let updated_ap = ar.dot(&alpha) + active_ap.dot(&gamma);
386+
387+
(updated_p, updated_ap, tau)
378388
} else {
379-
let eig_x = eig_vecs.slice(s![..size_x, ..]);
380-
let eig_r = eig_vecs.slice(s![size_x.., ..]);
389+
// tau are eigenvalues to basis of X
390+
let tau = eig_vecs.slice(s![..size_x, ..]);
391+
// alpha are eigenvalues to basis of R
392+
let alpha = eig_vecs.slice(s![size_x.., ..]);
381393

382-
let pp = r.dot(&eig_r);
383-
let app = ar.dot(&eig_r);
394+
// update AP and P as linear combination of the residual matrix R
395+
let updated_p = r.dot(&alpha);
396+
let updated_ap = ar.dot(&alpha);
384397

385-
(pp, app, eig_x)
398+
(updated_p, updated_ap, tau)
386399
};
387400

388-
x = x.dot(&eig_x) + &pp;
389-
ax = ax.dot(&eig_x) + &app;
401+
// update approximation of X as linear combinations of span{X, P, R}
402+
x = x.dot(&tau) + &p;
403+
ax = ax.dot(&tau) + &ap;
390404

391-
ap = Some((pp, app));
405+
previous_p_ap = Some((p, ap));
392406

393407
iter -= 1;
394408
};
395409

410+
// retrieve best result and convert norm into `A`
396411
let (vals, vecs, rnorm) = best_result.unwrap();
397412
let rnorm = rnorm.into_iter().map(|x| Scalar::from_real(x)).collect();
398413

399-
//dbg!(&residual_norms_history);
414+
dbg!(&residual_norms_history);
400415

401416
match final_norm {
402417
Ok(_) => EigResult::Ok(vals, vecs, rnorm),
@@ -468,21 +483,22 @@ mod tests {
468483
let n = a.len_of(Axis(0));
469484
let x: Array2<f64> = Array2::random((n, num), Uniform::new(0.0, 1.0));
470485

471-
let result = lobpcg(|y| a.dot(&y), x, |_| {}, None, 1e-5, n, order);
486+
let result = lobpcg(|y| a.dot(&y), x, |_| {}, None, 1e-5, n * 2, order);
487+
dbg!(&result);
472488
match result {
473489
EigResult::Ok(vals, _, r_norms) | EigResult::Err(vals, _, r_norms, _) => {
474490
// check convergence
475491
for (i, norm) in r_norms.into_iter().enumerate() {
476-
if norm > 0.01 {
492+
if norm > 1e-5 {
477493
println!("==== Assertion Failed ====");
478-
println!("The {} eigenvalue estimation did not converge!", i);
494+
println!("The {}th eigenvalue estimation did not converge!", i);
479495
panic!("Too large deviation of residual norm: {} > 0.01", norm);
480496
}
481497
}
482498

483499
// check correct order of eigenvalues
484500
if ground_truth_eigvals.len() == num {
485-
close_l2(&Array1::from(ground_truth_eigvals.to_vec()), &vals, 5e-2)
501+
close_l2(&Array1::from(ground_truth_eigvals.to_vec()), &vals, num as f64 * 5e-4)
486502
}
487503
}
488504
EigResult::NoResult(err) => panic!("Did not converge: {:?}", err),
@@ -519,30 +535,29 @@ mod tests {
519535
}
520536

521537
#[test]
522-
fn test_eigsolver_constrainted() {
538+
fn test_eigsolver_constrained() {
523539
let diag = arr1(&[1., 2., 3., 4., 5., 6., 7., 8., 9., 10.]);
524540
let a = Array2::from_diag(&diag);
525541
let x: Array2<f64> = Array2::random((10, 1), Uniform::new(0.0, 1.0));
526-
let y: Array2<f64> = arr2(&[[1.0, 0., 0., 0., 0., 0., 0., 0., 0., 0.]]).reversed_axes();
542+
let y: Array2<f64> = arr2(&[[1.0, 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 1.0, 0., 0., 0., 0., 0., 0., 0., 0.]]).reversed_axes();
527543

528-
let result = lobpcg(|y| a.dot(&y), x, |_| {}, Some(y), 1e-10, 100, Order::Smallest);
529-
dbg!(&result);
544+
let result = lobpcg(|y| a.dot(&y), x, |_| {}, Some(y), 1e-10, 50, Order::Smallest);
530545
match result {
531546
EigResult::Ok(vals, vecs, r_norms) | EigResult::Err(vals, vecs, r_norms, _) => {
532547
// check convergence
533548
for (i, norm) in r_norms.into_iter().enumerate() {
534549
if norm > 0.01 {
535550
println!("==== Assertion Failed ====");
536-
println!("The {} eigenvalue estimation did not converge!", i);
551+
println!("The {}th eigenvalue estimation did not converge!", i);
537552
panic!("Too large deviation of residual norm: {} > 0.01", norm);
538553
}
539554
}
540555

541-
// should be the second eigenvalue
542-
close_l2(&vals, &Array1::from(vec![2.0]), 1e-2);
556+
// should be the third eigenvalue
557+
close_l2(&vals, &Array1::from(vec![3.0]), 1e-10);
543558
close_l2(
544559
&vecs.column(0).mapv(|x| x.abs()),
545-
&arr1(&[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
560+
&arr1(&[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
546561
1e-5,
547562
);
548563
}

0 commit comments

Comments
 (0)