@@ -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`.
7474fn 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).
140139pub 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