@@ -73,7 +73,7 @@ fn ndarray_mask<A: Scalar>(matrix: ArrayView2<A>, mask: &[bool]) -> Array2<A> {
73
73
/// This functions takes a matrix `v` and constraint matrix `y` and orthogonalize the `v` to `y`.
74
74
fn apply_constraints < A : Scalar + Lapack > (
75
75
mut v : ArrayViewMut < A , Ix2 > ,
76
- fact_yy : & CholeskyFactorized < OwnedRepr < A > > ,
76
+ cholesky_yy : & CholeskyFactorized < OwnedRepr < A > > ,
77
77
y : ArrayView2 < A > ,
78
78
) {
79
79
let gram_yv = y. t ( ) . dot ( & v) ;
@@ -82,7 +82,7 @@ fn apply_constraints<A: Scalar + Lapack>(
82
82
. gencolumns ( )
83
83
. into_iter ( )
84
84
. map ( |x| {
85
- let res = fact_yy . solvec ( & x) . unwrap ( ) ;
85
+ let res = cholesky_yy . solvec ( & x) . unwrap ( ) ;
86
86
87
87
res. to_vec ( )
88
88
} )
@@ -120,23 +120,22 @@ fn orthonormalize<T: Scalar + Lapack>(v: Array2<T>) -> Result<(Array2<T>, Array2
120
120
///
121
121
/// # Arguments
122
122
/// * `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
125
125
/// 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`.
128
128
/// * `y` - Constraints of (n,size_y), iterations are performed in the orthogonal complement of the
129
129
/// 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.
133
132
/// * `maxiter` - The maximal number of iterations
134
133
/// * `order` - Whether to solve for the largest or lowest eigenvalues
135
134
///
136
135
/// The function returns an `EigResult` with the eigenvalue/eigenvector and achieved residual norm
137
136
/// for it. All iterations are tracked and the optimal solution returned. In case of an error a
138
137
/// 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).
140
139
pub fn lobpcg < A : Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default , F : Fn ( ArrayView2 < A > ) -> Array2 < A > , G : Fn ( ArrayViewMut2 < A > ) > (
141
140
a : F ,
142
141
mut x : Array2 < A > ,
@@ -163,37 +162,37 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
163
162
// cap the number of iteration
164
163
let mut iter = usize:: min ( n * 10 , maxiter) ;
165
164
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
+ } ) ;
170
171
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
178
173
let ( x, _) = match orthonormalize ( x) {
179
174
Ok ( x) => x,
180
175
Err ( err) => return EigResult :: NoResult ( err) ,
181
176
} ;
182
177
178
+ // calculate AX and XAX for Rayleigh quotient
183
179
let ax = a ( x. view ( ) ) ;
184
180
let xax = x. t ( ) . dot ( & ax) ;
185
181
186
- // perform eigenvalue decomposition of XAX as initialization
182
+ // perform eigenvalue decomposition of XAX
187
183
let ( mut lambda, eig_block) = match sorted_eig ( xax. view ( ) , None , size_x, & order) {
188
184
Ok ( x) => x,
189
185
Err ( err) => return EigResult :: NoResult ( err) ,
190
186
} ;
191
187
192
- // initiate X and AX with eigenvector
188
+ // initiate approximation of the eigenvector
193
189
let mut x = x. dot ( & eig_block) ;
194
190
let mut ax = ax. dot ( & eig_block) ;
195
191
192
+ // track residual below threshold
196
193
let mut activemask = vec ! [ true ; size_x] ;
194
+
195
+ // track residuals and best result
197
196
let mut residual_norms_history = Vec :: new ( ) ;
198
197
let mut best_result = None ;
199
198
@@ -203,7 +202,7 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
203
202
let ident0: Array2 < A > = Array2 :: eye ( size_x) ;
204
203
let two: A = NumCast :: from ( 2.0 ) . unwrap ( ) ;
205
204
206
- let mut ap : Option < ( Array2 < A > , Array2 < A > ) > = None ;
205
+ let mut previous_p_ap : Option < ( Array2 < A > , Array2 < A > ) > = None ;
207
206
let mut explicit_gram_flag = true ;
208
207
209
208
let final_norm = loop {
@@ -245,8 +244,8 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
245
244
// apply preconditioner
246
245
m ( active_block_r. view_mut ( ) ) ;
247
246
// 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 ( ) ) ;
250
249
}
251
250
// orthogonalize the preconditioned residual to x
252
251
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,
273
272
let xar = x. t ( ) . dot ( & ar) ;
274
273
let mut rar = r. t ( ) . dot ( & ar) ;
275
274
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
276
278
let ( xax, xx, rr, xr) = if explicit_gram_flag {
277
279
rar = ( & rar + & rar. t ( ) ) / two;
278
280
let xax = x. t ( ) . dot ( & ax) ;
@@ -292,14 +294,16 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
292
294
)
293
295
} ;
294
296
295
- let p_ap = ap. as_ref ( )
297
+ // mask and orthonormalize P and AP
298
+ let p_ap = previous_p_ap. as_ref ( )
296
299
. and_then ( |( p, ap) | {
297
300
let active_p = ndarray_mask ( p. view ( ) , & activemask) ;
298
301
let active_ap = ndarray_mask ( ap. view ( ) , & activemask) ;
299
302
300
303
orthonormalize ( active_p) . map ( |x| ( active_ap, x) ) . ok ( )
301
304
} )
302
305
. and_then ( |( active_ap, ( active_p, p_r) ) | {
306
+ // orthonormalize AP with R^{-1} of A
303
307
let active_ap = active_ap. reversed_axes ( ) ;
304
308
p_r. solve_triangular ( UPLO :: Lower , Diag :: NonUnit , & active_ap)
305
309
. map ( |active_ap| ( active_p, active_ap. reversed_axes ( ) ) )
@@ -351,52 +355,63 @@ pub fn lobpcg<A: Float + Scalar + Lapack + ScalarOperand + PartialOrd + Default,
351
355
)
352
356
} ;
353
357
358
+ // apply Rayleigh-Ritz method for (A - lambda) to calculate optimal expansion coefficients
354
359
let ( new_lambda, eig_vecs) = match sorted_eig ( gram_a. view ( ) , Some ( gram_b. view ( ) ) , size_x, & order) {
355
360
Ok ( x) => x,
356
361
Err ( err) => {
357
362
// 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 ;
360
365
continue ;
361
- } else {
366
+ } else { // or break if restart is not possible
362
367
break Err ( err) ;
363
368
}
364
369
}
365
370
} ;
366
371
lambda = new_lambda;
367
372
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
369
375
{
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)
378
388
} 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.., ..] ) ;
381
393
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) ;
384
397
385
- ( pp , app , eig_x )
398
+ ( updated_p , updated_ap , tau )
386
399
} ;
387
400
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;
390
404
391
- ap = Some ( ( pp , app ) ) ;
405
+ previous_p_ap = Some ( ( p , ap ) ) ;
392
406
393
407
iter -= 1 ;
394
408
} ;
395
409
410
+ // retrieve best result and convert norm into `A`
396
411
let ( vals, vecs, rnorm) = best_result. unwrap ( ) ;
397
412
let rnorm = rnorm. into_iter ( ) . map ( |x| Scalar :: from_real ( x) ) . collect ( ) ;
398
413
399
- // dbg!(&residual_norms_history);
414
+ dbg ! ( & residual_norms_history) ;
400
415
401
416
match final_norm {
402
417
Ok ( _) => EigResult :: Ok ( vals, vecs, rnorm) ,
@@ -468,21 +483,22 @@ mod tests {
468
483
let n = a. len_of ( Axis ( 0 ) ) ;
469
484
let x: Array2 < f64 > = Array2 :: random ( ( n, num) , Uniform :: new ( 0.0 , 1.0 ) ) ;
470
485
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) ;
472
488
match result {
473
489
EigResult :: Ok ( vals, _, r_norms) | EigResult :: Err ( vals, _, r_norms, _) => {
474
490
// check convergence
475
491
for ( i, norm) in r_norms. into_iter ( ) . enumerate ( ) {
476
- if norm > 0.01 {
492
+ if norm > 1e-5 {
477
493
println ! ( "==== Assertion Failed ====" ) ;
478
- println ! ( "The {} eigenvalue estimation did not converge!" , i) ;
494
+ println ! ( "The {}th eigenvalue estimation did not converge!" , i) ;
479
495
panic ! ( "Too large deviation of residual norm: {} > 0.01" , norm) ;
480
496
}
481
497
}
482
498
483
499
// check correct order of eigenvalues
484
500
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 )
486
502
}
487
503
}
488
504
EigResult :: NoResult ( err) => panic ! ( "Did not converge: {:?}" , err) ,
@@ -519,30 +535,29 @@ mod tests {
519
535
}
520
536
521
537
#[ test]
522
- fn test_eigsolver_constrainted ( ) {
538
+ fn test_eigsolver_constrained ( ) {
523
539
let diag = arr1 ( & [ 1. , 2. , 3. , 4. , 5. , 6. , 7. , 8. , 9. , 10. ] ) ;
524
540
let a = Array2 :: from_diag ( & diag) ;
525
541
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 ( ) ;
527
543
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 ) ;
530
545
match result {
531
546
EigResult :: Ok ( vals, vecs, r_norms) | EigResult :: Err ( vals, vecs, r_norms, _) => {
532
547
// check convergence
533
548
for ( i, norm) in r_norms. into_iter ( ) . enumerate ( ) {
534
549
if norm > 0.01 {
535
550
println ! ( "==== Assertion Failed ====" ) ;
536
- println ! ( "The {} eigenvalue estimation did not converge!" , i) ;
551
+ println ! ( "The {}th eigenvalue estimation did not converge!" , i) ;
537
552
panic ! ( "Too large deviation of residual norm: {} > 0.01" , norm) ;
538
553
}
539
554
}
540
555
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 ) ;
543
558
close_l2 (
544
559
& 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 ] ) ,
546
561
1e-5 ,
547
562
) ;
548
563
}
0 commit comments