Coverage for /usr/lib/python3/dist-packages/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py: 3%
435 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1"""
2Locally Optimal Block Preconditioned Conjugate Gradient Method (LOBPCG).
4References
5----------
6.. [1] A. V. Knyazev (2001),
7 Toward the Optimal Preconditioned Eigensolver: Locally Optimal
8 Block Preconditioned Conjugate Gradient Method.
9 SIAM Journal on Scientific Computing 23, no. 2,
10 pp. 517-541. :doi:`10.1137/S1064827500366124`
12.. [2] A. V. Knyazev, I. Lashuk, M. E. Argentati, and E. Ovchinnikov (2007),
13 Block Locally Optimal Preconditioned Eigenvalue Xolvers (BLOPEX)
14 in hypre and PETSc. :arxiv:`0705.2626`
16.. [3] A. V. Knyazev's C and MATLAB implementations:
17 https://github.com/lobpcg/blopex
18"""
20import warnings
21import numpy as np
22from scipy.linalg import (inv, eigh, cho_factor, cho_solve,
23 cholesky, LinAlgError)
24from scipy.sparse.linalg import LinearOperator
25from scipy.sparse import issparse
27__all__ = ["lobpcg"]
30def _report_nonhermitian(M, name):
31 """
32 Report if `M` is not a Hermitian matrix given its type.
33 """
34 from scipy.linalg import norm
36 md = M - M.T.conj()
37 nmd = norm(md, 1)
38 tol = 10 * np.finfo(M.dtype).eps
39 tol = max(tol, tol * norm(M, 1))
40 if nmd > tol:
41 warnings.warn(
42 f"Matrix {name} of the type {M.dtype} is not Hermitian: "
43 f"condition: {nmd} < {tol} fails.",
44 UserWarning, stacklevel=4
45 )
47def _as2d(ar):
48 """
49 If the input array is 2D return it, if it is 1D, append a dimension,
50 making it a column vector.
51 """
52 if ar.ndim == 2:
53 return ar
54 else: # Assume 1!
55 aux = np.array(ar, copy=False)
56 aux.shape = (ar.shape[0], 1)
57 return aux
60def _makeMatMat(m):
61 if m is None:
62 return None
63 elif callable(m):
64 return lambda v: m(v)
65 else:
66 return lambda v: m @ v
69def _matmul_inplace(x, y, verbosityLevel=0):
70 """Perform 'np.matmul' in-place if possible.
72 If some sufficient conditions for inplace matmul are met, do so.
73 Otherwise try inplace update and fall back to overwrite if that fails.
74 """
75 if x.flags["CARRAY"] and x.shape[1] == y.shape[1] and x.dtype == y.dtype:
76 # conditions where we can guarantee that inplace updates will work;
77 # i.e. x is not a view/slice, x & y have compatible dtypes, and the
78 # shape of the result of x @ y matches the shape of x.
79 np.matmul(x, y, out=x)
80 else:
81 # ideally, we'd have an exhaustive list of conditions above when
82 # inplace updates are possible; since we don't, we opportunistically
83 # try if it works, and fall back to overwriting if necessary
84 try:
85 np.matmul(x, y, out=x)
86 except Exception:
87 if verbosityLevel:
88 warnings.warn(
89 "Inplace update of x = x @ y failed, "
90 "x needs to be overwritten.",
91 UserWarning, stacklevel=3
92 )
93 x = x @ y
94 return x
97def _applyConstraints(blockVectorV, factYBY, blockVectorBY, blockVectorY):
98 """Changes blockVectorV in-place."""
99 YBV = blockVectorBY.T.conj() @ blockVectorV
100 tmp = cho_solve(factYBY, YBV)
101 blockVectorV -= blockVectorY @ tmp
104def _b_orthonormalize(B, blockVectorV, blockVectorBV=None,
105 verbosityLevel=0):
106 """in-place B-orthonormalize the given block vector using Cholesky."""
107 if blockVectorBV is None:
108 if B is None:
109 blockVectorBV = blockVectorV
110 else:
111 try:
112 blockVectorBV = B(blockVectorV)
113 except Exception as e:
114 if verbosityLevel:
115 warnings.warn(
116 f"Secondary MatMul call failed with error\n"
117 f"{e}\n",
118 UserWarning, stacklevel=3
119 )
120 return None, None, None
121 if blockVectorBV.shape != blockVectorV.shape:
122 raise ValueError(
123 f"The shape {blockVectorV.shape} "
124 f"of the orthogonalized matrix not preserved\n"
125 f"and changed to {blockVectorBV.shape} "
126 f"after multiplying by the secondary matrix.\n"
127 )
129 VBV = blockVectorV.T.conj() @ blockVectorBV
130 try:
131 # VBV is a Cholesky factor from now on...
132 VBV = cholesky(VBV, overwrite_a=True)
133 VBV = inv(VBV, overwrite_a=True)
134 blockVectorV = _matmul_inplace(
135 blockVectorV, VBV,
136 verbosityLevel=verbosityLevel
137 )
138 if B is not None:
139 blockVectorBV = _matmul_inplace(
140 blockVectorBV, VBV,
141 verbosityLevel=verbosityLevel
142 )
143 return blockVectorV, blockVectorBV, VBV
144 except LinAlgError:
145 if verbosityLevel:
146 warnings.warn(
147 "Cholesky has failed.",
148 UserWarning, stacklevel=3
149 )
150 return None, None, None
153def _get_indx(_lambda, num, largest):
154 """Get `num` indices into `_lambda` depending on `largest` option."""
155 ii = np.argsort(_lambda)
156 if largest:
157 ii = ii[:-num - 1:-1]
158 else:
159 ii = ii[:num]
161 return ii
164def _handle_gramA_gramB_verbosity(gramA, gramB, verbosityLevel):
165 if verbosityLevel:
166 _report_nonhermitian(gramA, "gramA")
167 _report_nonhermitian(gramB, "gramB")
170def lobpcg(
171 A,
172 X,
173 B=None,
174 M=None,
175 Y=None,
176 tol=None,
177 maxiter=None,
178 largest=True,
179 verbosityLevel=0,
180 retLambdaHistory=False,
181 retResidualNormsHistory=False,
182 restartControl=20,
183):
184 """Locally Optimal Block Preconditioned Conjugate Gradient Method (LOBPCG).
186 LOBPCG is a preconditioned eigensolver for large real symmetric and complex
187 Hermitian definite generalized eigenproblems.
189 Parameters
190 ----------
191 A : {sparse matrix, ndarray, LinearOperator, callable object}
192 The Hermitian linear operator of the problem, usually given by a
193 sparse matrix. Often called the "stiffness matrix".
194 X : ndarray, float32 or float64
195 Initial approximation to the ``k`` eigenvectors (non-sparse).
196 If `A` has ``shape=(n,n)`` then `X` must have ``shape=(n,k)``.
197 B : {sparse matrix, ndarray, LinearOperator, callable object}
198 Optional. By default ``B = None``, which is equivalent to identity.
199 The right hand side operator in a generalized eigenproblem if present.
200 Often called the "mass matrix". Must be Hermitian positive definite.
201 M : {sparse matrix, ndarray, LinearOperator, callable object}
202 Optional. By default ``M = None``, which is equivalent to identity.
203 Preconditioner aiming to accelerate convergence.
204 Y : ndarray, float32 or float64, default: None
205 An ``n-by-sizeY`` ndarray of constraints with ``sizeY < n``.
206 The iterations will be performed in the ``B``-orthogonal complement
207 of the column-space of `Y`. `Y` must be full rank if present.
208 tol : scalar, optional
209 The default is ``tol=n*sqrt(eps)``.
210 Solver tolerance for the stopping criterion.
211 maxiter : int, default: 20
212 Maximum number of iterations.
213 largest : bool, default: True
214 When True, solve for the largest eigenvalues, otherwise the smallest.
215 verbosityLevel : int, optional
216 By default ``verbosityLevel=0`` no output.
217 Controls the solver standard/screen output.
218 retLambdaHistory : bool, default: False
219 Whether to return iterative eigenvalue history.
220 retResidualNormsHistory : bool, default: False
221 Whether to return iterative history of residual norms.
222 restartControl : int, optional.
223 Iterations restart if the residuals jump ``2**restartControl`` times
224 compared to the smallest recorded in ``retResidualNormsHistory``.
225 The default is ``restartControl=20``, making the restarts rare for
226 backward compatibility.
228 Returns
229 -------
230 lambda : ndarray of the shape ``(k, )``.
231 Array of ``k`` approximate eigenvalues.
232 v : ndarray of the same shape as ``X.shape``.
233 An array of ``k`` approximate eigenvectors.
234 lambdaHistory : ndarray, optional.
235 The eigenvalue history, if `retLambdaHistory` is ``True``.
236 ResidualNormsHistory : ndarray, optional.
237 The history of residual norms, if `retResidualNormsHistory`
238 is ``True``.
240 Notes
241 -----
242 The iterative loop runs ``maxit=maxiter`` (20 if ``maxit=None``)
243 iterations at most and finishes earler if the tolerance is met.
244 Breaking backward compatibility with the previous version, LOBPCG
245 now returns the block of iterative vectors with the best accuracy rather
246 than the last one iterated, as a cure for possible divergence.
248 If ``X.dtype == np.float32`` and user-provided operations/multiplications
249 by `A`, `B`, and `M` all preserve the ``np.float32`` data type,
250 all the calculations and the output are in ``np.float32``.
252 The size of the iteration history output equals to the number of the best
253 (limited by `maxit`) iterations plus 3: initial, final, and postprocessing.
255 If both `retLambdaHistory` and `retResidualNormsHistory` are ``True``,
256 the return tuple has the following format
257 ``(lambda, V, lambda history, residual norms history)``.
259 In the following ``n`` denotes the matrix size and ``k`` the number
260 of required eigenvalues (smallest or largest).
262 The LOBPCG code internally solves eigenproblems of the size ``3k`` on every
263 iteration by calling the dense eigensolver `eigh`, so if ``k`` is not
264 small enough compared to ``n``, it makes no sense to call the LOBPCG code.
265 Moreover, if one calls the LOBPCG algorithm for ``5k > n``, it would likely
266 break internally, so the code calls the standard function `eigh` instead.
267 It is not that ``n`` should be large for the LOBPCG to work, but rather the
268 ratio ``n / k`` should be large. It you call LOBPCG with ``k=1``
269 and ``n=10``, it works though ``n`` is small. The method is intended
270 for extremely large ``n / k``.
272 The convergence speed depends basically on three factors:
274 1. Quality of the initial approximations `X` to the seeking eigenvectors.
275 Randomly distributed around the origin vectors work well if no better
276 choice is known.
278 2. Relative separation of the desired eigenvalues from the rest
279 of the eigenvalues. One can vary ``k`` to improve the separation.
281 3. Proper preconditioning to shrink the spectral spread.
282 For example, a rod vibration test problem (under tests
283 directory) is ill-conditioned for large ``n``, so convergence will be
284 slow, unless efficient preconditioning is used. For this specific
285 problem, a good simple preconditioner function would be a linear solve
286 for `A`, which is easy to code since `A` is tridiagonal.
288 References
289 ----------
290 .. [1] A. V. Knyazev (2001),
291 Toward the Optimal Preconditioned Eigensolver: Locally Optimal
292 Block Preconditioned Conjugate Gradient Method.
293 SIAM Journal on Scientific Computing 23, no. 2,
294 pp. 517-541. :doi:`10.1137/S1064827500366124`
296 .. [2] A. V. Knyazev, I. Lashuk, M. E. Argentati, and E. Ovchinnikov
297 (2007), Block Locally Optimal Preconditioned Eigenvalue Xolvers
298 (BLOPEX) in hypre and PETSc. :arxiv:`0705.2626`
300 .. [3] A. V. Knyazev's C and MATLAB implementations:
301 https://github.com/lobpcg/blopex
303 Examples
304 --------
305 Our first example is minimalistic - find the largest eigenvalue of
306 a diagonal matrix by solving the non-generalized eigenvalue problem
307 ``A x = lambda x`` without constraints or preconditioning.
309 >>> import numpy as np
310 >>> from scipy.sparse import spdiags
311 >>> from scipy.sparse.linalg import LinearOperator, aslinearoperator
312 >>> from scipy.sparse.linalg import lobpcg
314 The square matrix size is
316 >>> n = 100
318 and its diagonal entries are 1, ..., 100 defined by
320 >>> vals = np.arange(1, n + 1).astype(np.int16)
322 The first mandatory input parameter in this test is
323 the sparse diagonal matrix `A`
324 of the eigenvalue problem ``A x = lambda x`` to solve.
326 >>> A = spdiags(vals, 0, n, n)
327 >>> A = A.astype(np.int16)
328 >>> A.toarray()
329 array([[ 1, 0, 0, ..., 0, 0, 0],
330 [ 0, 2, 0, ..., 0, 0, 0],
331 [ 0, 0, 3, ..., 0, 0, 0],
332 ...,
333 [ 0, 0, 0, ..., 98, 0, 0],
334 [ 0, 0, 0, ..., 0, 99, 0],
335 [ 0, 0, 0, ..., 0, 0, 100]], dtype=int16)
337 The second mandatory input parameter `X` is a 2D array with the
338 row dimension determining the number of requested eigenvalues.
339 `X` is an initial guess for targeted eigenvectors.
340 `X` must have linearly independent columns.
341 If no initial approximations available, randomly oriented vectors
342 commonly work best, e.g., with components normally distributed
343 around zero or uniformly distributed on the interval [-1 1].
344 Setting the initial approximations to dtype ``np.float32``
345 forces all iterative values to dtype ``np.float32`` speeding up
346 the run while still allowing accurate eigenvalue computations.
348 >>> k = 1
349 >>> rng = np.random.default_rng()
350 >>> X = rng.normal(size=(n, k))
351 >>> X = X.astype(np.float32)
353 >>> eigenvalues, _ = lobpcg(A, X, maxiter=60)
354 >>> eigenvalues
355 array([100.])
356 >>> eigenvalues.dtype
357 dtype('float32')
359 LOBPCG needs only access the matrix product with `A` rather
360 then the matrix itself. Since the matrix `A` is diagonal in
361 this example, one can write a function of the product
362 ``A @ X`` using the diagonal values ``vals`` only, e.g., by
363 element-wise multiplication with broadcasting
365 >>> A_f = lambda X: vals[:, np.newaxis] * X
367 and use the handle ``A_f`` to this callable function as an input
369 >>> eigenvalues, _ = lobpcg(A_f, X, maxiter=60)
370 >>> eigenvalues
371 array([100.])
373 The next example illustrates computing 3 smallest eigenvalues of
374 the same matrix given by the function handle ``A_f`` with
375 constraints and preconditioning.
377 >>> k = 3
378 >>> X = rng.normal(size=(n, k))
380 Constraints - an optional input parameter is a 2D array comprising
381 of column vectors that the eigenvectors must be orthogonal to
383 >>> Y = np.eye(n, 3)
385 The preconditioner acts as the inverse of `A` in this example, but
386 in the reduced precision ``np.float32`` even though the initial `X`
387 and thus all iterates and the output are in full ``np.float64``.
389 >>> inv_vals = 1./vals
390 >>> inv_vals = inv_vals.astype(np.float32)
391 >>> M = lambda X: inv_vals[:, np.newaxis] * X
393 Let us now solve the eigenvalue problem for the matrix `A` first
394 without preconditioning requesting 80 iterations
396 >>> eigenvalues, _ = lobpcg(A_f, X, Y=Y, largest=False, maxiter=80)
397 >>> eigenvalues
398 array([4., 5., 6.])
399 >>> eigenvalues.dtype
400 dtype('float64')
402 With preconditioning we need only 20 iterations from the same `X`
404 >>> eigenvalues, _ = lobpcg(A_f, X, Y=Y, M=M, largest=False, maxiter=20)
405 >>> eigenvalues
406 array([4., 5., 6.])
408 Note that the vectors passed in `Y` are the eigenvectors of the 3
409 smallest eigenvalues. The results returned above are orthogonal to those.
411 Finally, the primary matrix `A` may be indefinite, e.g., after shifting
412 ``vals`` by 50 from 1, ..., 100 to -49, ..., 50, we still can compute
413 the 3 smallest or largest eigenvalues.
415 >>> vals = vals - 50
416 >>> X = rng.normal(size=(n, k))
417 >>> eigenvalues, _ = lobpcg(A_f, X, largest=False, maxiter=99)
418 >>> eigenvalues
419 array([-49., -48., -47.])
420 >>> eigenvalues, _ = lobpcg(A_f, X, largest=True, maxiter=99)
421 >>> eigenvalues
422 array([50., 49., 48.])
424 """
425 blockVectorX = X
426 bestblockVectorX = blockVectorX
427 blockVectorY = Y
428 residualTolerance = tol
429 if maxiter is None:
430 maxiter = 20
432 bestIterationNumber = maxiter
434 sizeY = 0
435 if blockVectorY is not None:
436 if len(blockVectorY.shape) != 2:
437 warnings.warn(
438 f"Expected rank-2 array for argument Y, instead got "
439 f"{len(blockVectorY.shape)}, "
440 f"so ignore it and use no constraints.",
441 UserWarning, stacklevel=2
442 )
443 blockVectorY = None
444 else:
445 sizeY = blockVectorY.shape[1]
447 # Block size.
448 if blockVectorX is None:
449 raise ValueError("The mandatory initial matrix X cannot be None")
450 if len(blockVectorX.shape) != 2:
451 raise ValueError("expected rank-2 array for argument X")
453 n, sizeX = blockVectorX.shape
455 # Data type of iterates, determined by X, must be inexact
456 if not np.issubdtype(blockVectorX.dtype, np.inexact):
457 warnings.warn(
458 f"Data type for argument X is {blockVectorX.dtype}, "
459 f"which is not inexact, so casted to np.float32.",
460 UserWarning, stacklevel=2
461 )
462 blockVectorX = np.asarray(blockVectorX, dtype=np.float32)
464 if retLambdaHistory:
465 lambdaHistory = np.zeros((maxiter + 3, sizeX),
466 dtype=blockVectorX.dtype)
467 if retResidualNormsHistory:
468 residualNormsHistory = np.zeros((maxiter + 3, sizeX),
469 dtype=blockVectorX.dtype)
471 if verbosityLevel:
472 aux = "Solving "
473 if B is None:
474 aux += "standard"
475 else:
476 aux += "generalized"
477 aux += " eigenvalue problem with"
478 if M is None:
479 aux += "out"
480 aux += " preconditioning\n\n"
481 aux += "matrix size %d\n" % n
482 aux += "block size %d\n\n" % sizeX
483 if blockVectorY is None:
484 aux += "No constraints\n\n"
485 else:
486 if sizeY > 1:
487 aux += "%d constraints\n\n" % sizeY
488 else:
489 aux += "%d constraint\n\n" % sizeY
490 print(aux)
492 if (n - sizeY) < (5 * sizeX):
493 warnings.warn(
494 f"The problem size {n} minus the constraints size {sizeY} "
495 f"is too small relative to the block size {sizeX}. "
496 f"Using a dense eigensolver instead of LOBPCG iterations."
497 f"No output of the history of the iterations.",
498 UserWarning, stacklevel=2
499 )
501 sizeX = min(sizeX, n)
503 if blockVectorY is not None:
504 raise NotImplementedError(
505 "The dense eigensolver does not support constraints."
506 )
508 # Define the closed range of indices of eigenvalues to return.
509 if largest:
510 eigvals = (n - sizeX, n - 1)
511 else:
512 eigvals = (0, sizeX - 1)
514 try:
515 if isinstance(A, LinearOperator):
516 A = A(np.eye(n, dtype=int))
517 elif callable(A):
518 A = A(np.eye(n, dtype=int))
519 if A.shape != (n, n):
520 raise ValueError(
521 f"The shape {A.shape} of the primary matrix\n"
522 f"defined by a callable object is wrong.\n"
523 )
524 elif issparse(A):
525 A = A.toarray()
526 else:
527 A = np.asarray(A)
528 except Exception as e:
529 raise Exception(
530 f"Primary MatMul call failed with error\n"
531 f"{e}\n")
533 if B is not None:
534 try:
535 if isinstance(B, LinearOperator):
536 B = B(np.eye(n, dtype=int))
537 elif callable(B):
538 B = B(np.eye(n, dtype=int))
539 if B.shape != (n, n):
540 raise ValueError(
541 f"The shape {B.shape} of the secondary matrix\n"
542 f"defined by a callable object is wrong.\n"
543 )
544 elif issparse(B):
545 B = B.toarray()
546 else:
547 B = np.asarray(B)
548 except Exception as e:
549 raise Exception(
550 f"Secondary MatMul call failed with error\n"
551 f"{e}\n")
553 try:
554 vals, vecs = eigh(A,
555 B,
556 subset_by_index=eigvals,
557 check_finite=False)
558 if largest:
559 # Reverse order to be compatible with eigs() in 'LM' mode.
560 vals = vals[::-1]
561 vecs = vecs[:, ::-1]
563 return vals, vecs
564 except Exception as e:
565 raise Exception(
566 f"Dense eigensolver failed with error\n"
567 f"{e}\n"
568 )
570 if (residualTolerance is None) or (residualTolerance <= 0.0):
571 residualTolerance = np.sqrt(np.finfo(blockVectorX.dtype).eps) * n
573 A = _makeMatMat(A)
574 B = _makeMatMat(B)
575 M = _makeMatMat(M)
577 # Apply constraints to X.
578 if blockVectorY is not None:
580 if B is not None:
581 blockVectorBY = B(blockVectorY)
582 if blockVectorBY.shape != blockVectorY.shape:
583 raise ValueError(
584 f"The shape {blockVectorY.shape} "
585 f"of the constraint not preserved\n"
586 f"and changed to {blockVectorBY.shape} "
587 f"after multiplying by the secondary matrix.\n"
588 )
589 else:
590 blockVectorBY = blockVectorY
592 # gramYBY is a dense array.
593 gramYBY = blockVectorY.T.conj() @ blockVectorBY
594 try:
595 # gramYBY is a Cholesky factor from now on...
596 gramYBY = cho_factor(gramYBY, overwrite_a=True)
597 except LinAlgError as e:
598 raise ValueError("Linearly dependent constraints") from e
600 _applyConstraints(blockVectorX, gramYBY, blockVectorBY, blockVectorY)
602 ##
603 # B-orthonormalize X.
604 blockVectorX, blockVectorBX, _ = _b_orthonormalize(
605 B, blockVectorX, verbosityLevel=verbosityLevel)
606 if blockVectorX is None:
607 raise ValueError("Linearly dependent initial approximations")
609 ##
610 # Compute the initial Ritz vectors: solve the eigenproblem.
611 blockVectorAX = A(blockVectorX)
612 if blockVectorAX.shape != blockVectorX.shape:
613 raise ValueError(
614 f"The shape {blockVectorX.shape} "
615 f"of the initial approximations not preserved\n"
616 f"and changed to {blockVectorAX.shape} "
617 f"after multiplying by the primary matrix.\n"
618 )
620 gramXAX = blockVectorX.T.conj() @ blockVectorAX
622 _lambda, eigBlockVector = eigh(gramXAX, check_finite=False)
623 ii = _get_indx(_lambda, sizeX, largest)
624 _lambda = _lambda[ii]
625 if retLambdaHistory:
626 lambdaHistory[0, :] = _lambda
628 eigBlockVector = np.asarray(eigBlockVector[:, ii])
629 blockVectorX = _matmul_inplace(
630 blockVectorX, eigBlockVector,
631 verbosityLevel=verbosityLevel
632 )
633 blockVectorAX = _matmul_inplace(
634 blockVectorAX, eigBlockVector,
635 verbosityLevel=verbosityLevel
636 )
637 if B is not None:
638 blockVectorBX = _matmul_inplace(
639 blockVectorBX, eigBlockVector,
640 verbosityLevel=verbosityLevel
641 )
643 ##
644 # Active index set.
645 activeMask = np.ones((sizeX,), dtype=bool)
647 ##
648 # Main iteration loop.
650 blockVectorP = None # set during iteration
651 blockVectorAP = None
652 blockVectorBP = None
654 smallestResidualNorm = np.abs(np.finfo(blockVectorX.dtype).max)
656 iterationNumber = -1
657 restart = True
658 forcedRestart = False
659 explicitGramFlag = False
660 while iterationNumber < maxiter:
661 iterationNumber += 1
663 if B is not None:
664 aux = blockVectorBX * _lambda[np.newaxis, :]
665 else:
666 aux = blockVectorX * _lambda[np.newaxis, :]
668 blockVectorR = blockVectorAX - aux
670 aux = np.sum(blockVectorR.conj() * blockVectorR, 0)
671 residualNorms = np.sqrt(np.abs(aux))
672 if retResidualNormsHistory:
673 residualNormsHistory[iterationNumber, :] = residualNorms
674 residualNorm = np.sum(np.abs(residualNorms)) / sizeX
676 if residualNorm < smallestResidualNorm:
677 smallestResidualNorm = residualNorm
678 bestIterationNumber = iterationNumber
679 bestblockVectorX = blockVectorX
680 elif residualNorm > 2**restartControl * smallestResidualNorm:
681 forcedRestart = True
682 blockVectorAX = A(blockVectorX)
683 if blockVectorAX.shape != blockVectorX.shape:
684 raise ValueError(
685 f"The shape {blockVectorX.shape} "
686 f"of the restarted iterate not preserved\n"
687 f"and changed to {blockVectorAX.shape} "
688 f"after multiplying by the primary matrix.\n"
689 )
690 if B is not None:
691 blockVectorBX = B(blockVectorX)
692 if blockVectorBX.shape != blockVectorX.shape:
693 raise ValueError(
694 f"The shape {blockVectorX.shape} "
695 f"of the restarted iterate not preserved\n"
696 f"and changed to {blockVectorBX.shape} "
697 f"after multiplying by the secondary matrix.\n"
698 )
700 ii = np.where(residualNorms > residualTolerance, True, False)
701 activeMask = activeMask & ii
702 currentBlockSize = activeMask.sum()
704 if verbosityLevel:
705 print(f"iteration {iterationNumber}")
706 print(f"current block size: {currentBlockSize}")
707 print(f"eigenvalue(s):\n{_lambda}")
708 print(f"residual norm(s):\n{residualNorms}")
710 if currentBlockSize == 0:
711 break
713 activeBlockVectorR = _as2d(blockVectorR[:, activeMask])
715 if iterationNumber > 0:
716 activeBlockVectorP = _as2d(blockVectorP[:, activeMask])
717 activeBlockVectorAP = _as2d(blockVectorAP[:, activeMask])
718 if B is not None:
719 activeBlockVectorBP = _as2d(blockVectorBP[:, activeMask])
721 if M is not None:
722 # Apply preconditioner T to the active residuals.
723 activeBlockVectorR = M(activeBlockVectorR)
725 ##
726 # Apply constraints to the preconditioned residuals.
727 if blockVectorY is not None:
728 _applyConstraints(activeBlockVectorR,
729 gramYBY,
730 blockVectorBY,
731 blockVectorY)
733 ##
734 # B-orthogonalize the preconditioned residuals to X.
735 if B is not None:
736 activeBlockVectorR = activeBlockVectorR - (
737 blockVectorX @
738 (blockVectorBX.T.conj() @ activeBlockVectorR)
739 )
740 else:
741 activeBlockVectorR = activeBlockVectorR - (
742 blockVectorX @
743 (blockVectorX.T.conj() @ activeBlockVectorR)
744 )
746 ##
747 # B-orthonormalize the preconditioned residuals.
748 aux = _b_orthonormalize(
749 B, activeBlockVectorR, verbosityLevel=verbosityLevel)
750 activeBlockVectorR, activeBlockVectorBR, _ = aux
752 if activeBlockVectorR is None:
753 warnings.warn(
754 f"Failed at iteration {iterationNumber} with accuracies "
755 f"{residualNorms}\n not reaching the requested "
756 f"tolerance {residualTolerance}.",
757 UserWarning, stacklevel=2
758 )
759 break
760 activeBlockVectorAR = A(activeBlockVectorR)
762 if iterationNumber > 0:
763 if B is not None:
764 aux = _b_orthonormalize(
765 B, activeBlockVectorP, activeBlockVectorBP,
766 verbosityLevel=verbosityLevel
767 )
768 activeBlockVectorP, activeBlockVectorBP, invR = aux
769 else:
770 aux = _b_orthonormalize(B, activeBlockVectorP,
771 verbosityLevel=verbosityLevel)
772 activeBlockVectorP, _, invR = aux
773 # Function _b_orthonormalize returns None if Cholesky fails
774 if activeBlockVectorP is not None:
775 activeBlockVectorAP = _matmul_inplace(
776 activeBlockVectorAP, invR,
777 verbosityLevel=verbosityLevel
778 )
779 restart = forcedRestart
780 else:
781 restart = True
783 ##
784 # Perform the Rayleigh Ritz Procedure:
785 # Compute symmetric Gram matrices:
787 if activeBlockVectorAR.dtype == "float32":
788 myeps = 1
789 else:
790 myeps = np.sqrt(np.finfo(activeBlockVectorR.dtype).eps)
792 if residualNorms.max() > myeps and not explicitGramFlag:
793 explicitGramFlag = False
794 else:
795 # Once explicitGramFlag, forever explicitGramFlag.
796 explicitGramFlag = True
798 # Shared memory assingments to simplify the code
799 if B is None:
800 blockVectorBX = blockVectorX
801 activeBlockVectorBR = activeBlockVectorR
802 if not restart:
803 activeBlockVectorBP = activeBlockVectorP
805 # Common submatrices:
806 gramXAR = np.dot(blockVectorX.T.conj(), activeBlockVectorAR)
807 gramRAR = np.dot(activeBlockVectorR.T.conj(), activeBlockVectorAR)
809 gramDtype = activeBlockVectorAR.dtype
810 if explicitGramFlag:
811 gramRAR = (gramRAR + gramRAR.T.conj()) / 2
812 gramXAX = np.dot(blockVectorX.T.conj(), blockVectorAX)
813 gramXAX = (gramXAX + gramXAX.T.conj()) / 2
814 gramXBX = np.dot(blockVectorX.T.conj(), blockVectorBX)
815 gramRBR = np.dot(activeBlockVectorR.T.conj(), activeBlockVectorBR)
816 gramXBR = np.dot(blockVectorX.T.conj(), activeBlockVectorBR)
817 else:
818 gramXAX = np.diag(_lambda).astype(gramDtype)
819 gramXBX = np.eye(sizeX, dtype=gramDtype)
820 gramRBR = np.eye(currentBlockSize, dtype=gramDtype)
821 gramXBR = np.zeros((sizeX, currentBlockSize), dtype=gramDtype)
823 if not restart:
824 gramXAP = np.dot(blockVectorX.T.conj(), activeBlockVectorAP)
825 gramRAP = np.dot(activeBlockVectorR.T.conj(), activeBlockVectorAP)
826 gramPAP = np.dot(activeBlockVectorP.T.conj(), activeBlockVectorAP)
827 gramXBP = np.dot(blockVectorX.T.conj(), activeBlockVectorBP)
828 gramRBP = np.dot(activeBlockVectorR.T.conj(), activeBlockVectorBP)
829 if explicitGramFlag:
830 gramPAP = (gramPAP + gramPAP.T.conj()) / 2
831 gramPBP = np.dot(activeBlockVectorP.T.conj(),
832 activeBlockVectorBP)
833 else:
834 gramPBP = np.eye(currentBlockSize, dtype=gramDtype)
836 gramA = np.block(
837 [
838 [gramXAX, gramXAR, gramXAP],
839 [gramXAR.T.conj(), gramRAR, gramRAP],
840 [gramXAP.T.conj(), gramRAP.T.conj(), gramPAP],
841 ]
842 )
843 gramB = np.block(
844 [
845 [gramXBX, gramXBR, gramXBP],
846 [gramXBR.T.conj(), gramRBR, gramRBP],
847 [gramXBP.T.conj(), gramRBP.T.conj(), gramPBP],
848 ]
849 )
851 _handle_gramA_gramB_verbosity(gramA, gramB, verbosityLevel)
853 try:
854 _lambda, eigBlockVector = eigh(gramA,
855 gramB,
856 check_finite=False)
857 except LinAlgError as e:
858 # raise ValueError("eigh failed in lobpcg iterations") from e
859 if verbosityLevel:
860 warnings.warn(
861 f"eigh failed at iteration {iterationNumber} \n"
862 f"with error {e} causing a restart.\n",
863 UserWarning, stacklevel=2
864 )
865 # try again after dropping the direction vectors P from RR
866 restart = True
868 if restart:
869 gramA = np.block([[gramXAX, gramXAR], [gramXAR.T.conj(), gramRAR]])
870 gramB = np.block([[gramXBX, gramXBR], [gramXBR.T.conj(), gramRBR]])
872 _handle_gramA_gramB_verbosity(gramA, gramB, verbosityLevel)
874 try:
875 _lambda, eigBlockVector = eigh(gramA,
876 gramB,
877 check_finite=False)
878 except LinAlgError as e:
879 # raise ValueError("eigh failed in lobpcg iterations") from e
880 warnings.warn(
881 f"eigh failed at iteration {iterationNumber} with error\n"
882 f"{e}\n",
883 UserWarning, stacklevel=2
884 )
885 break
887 ii = _get_indx(_lambda, sizeX, largest)
888 _lambda = _lambda[ii]
889 eigBlockVector = eigBlockVector[:, ii]
890 if retLambdaHistory:
891 lambdaHistory[iterationNumber + 1, :] = _lambda
893 # Compute Ritz vectors.
894 if B is not None:
895 if not restart:
896 eigBlockVectorX = eigBlockVector[:sizeX]
897 eigBlockVectorR = eigBlockVector[sizeX:
898 sizeX + currentBlockSize]
899 eigBlockVectorP = eigBlockVector[sizeX + currentBlockSize:]
901 pp = np.dot(activeBlockVectorR, eigBlockVectorR)
902 pp += np.dot(activeBlockVectorP, eigBlockVectorP)
904 app = np.dot(activeBlockVectorAR, eigBlockVectorR)
905 app += np.dot(activeBlockVectorAP, eigBlockVectorP)
907 bpp = np.dot(activeBlockVectorBR, eigBlockVectorR)
908 bpp += np.dot(activeBlockVectorBP, eigBlockVectorP)
909 else:
910 eigBlockVectorX = eigBlockVector[:sizeX]
911 eigBlockVectorR = eigBlockVector[sizeX:]
913 pp = np.dot(activeBlockVectorR, eigBlockVectorR)
914 app = np.dot(activeBlockVectorAR, eigBlockVectorR)
915 bpp = np.dot(activeBlockVectorBR, eigBlockVectorR)
917 blockVectorX = np.dot(blockVectorX, eigBlockVectorX) + pp
918 blockVectorAX = np.dot(blockVectorAX, eigBlockVectorX) + app
919 blockVectorBX = np.dot(blockVectorBX, eigBlockVectorX) + bpp
921 blockVectorP, blockVectorAP, blockVectorBP = pp, app, bpp
923 else:
924 if not restart:
925 eigBlockVectorX = eigBlockVector[:sizeX]
926 eigBlockVectorR = eigBlockVector[sizeX:
927 sizeX + currentBlockSize]
928 eigBlockVectorP = eigBlockVector[sizeX + currentBlockSize:]
930 pp = np.dot(activeBlockVectorR, eigBlockVectorR)
931 pp += np.dot(activeBlockVectorP, eigBlockVectorP)
933 app = np.dot(activeBlockVectorAR, eigBlockVectorR)
934 app += np.dot(activeBlockVectorAP, eigBlockVectorP)
935 else:
936 eigBlockVectorX = eigBlockVector[:sizeX]
937 eigBlockVectorR = eigBlockVector[sizeX:]
939 pp = np.dot(activeBlockVectorR, eigBlockVectorR)
940 app = np.dot(activeBlockVectorAR, eigBlockVectorR)
942 blockVectorX = np.dot(blockVectorX, eigBlockVectorX) + pp
943 blockVectorAX = np.dot(blockVectorAX, eigBlockVectorX) + app
945 blockVectorP, blockVectorAP = pp, app
947 if B is not None:
948 aux = blockVectorBX * _lambda[np.newaxis, :]
949 else:
950 aux = blockVectorX * _lambda[np.newaxis, :]
952 blockVectorR = blockVectorAX - aux
954 aux = np.sum(blockVectorR.conj() * blockVectorR, 0)
955 residualNorms = np.sqrt(np.abs(aux))
956 # Use old lambda in case of early loop exit.
957 if retLambdaHistory:
958 lambdaHistory[iterationNumber + 1, :] = _lambda
959 if retResidualNormsHistory:
960 residualNormsHistory[iterationNumber + 1, :] = residualNorms
961 residualNorm = np.sum(np.abs(residualNorms)) / sizeX
962 if residualNorm < smallestResidualNorm:
963 smallestResidualNorm = residualNorm
964 bestIterationNumber = iterationNumber + 1
965 bestblockVectorX = blockVectorX
967 if np.max(np.abs(residualNorms)) > residualTolerance:
968 warnings.warn(
969 f"Exited at iteration {iterationNumber} with accuracies \n"
970 f"{residualNorms}\n"
971 f"not reaching the requested tolerance {residualTolerance}.\n"
972 f"Use iteration {bestIterationNumber} instead with accuracy \n"
973 f"{smallestResidualNorm}.\n",
974 UserWarning, stacklevel=2
975 )
977 if verbosityLevel:
978 print(f"Final iterative eigenvalue(s):\n{_lambda}")
979 print(f"Final iterative residual norm(s):\n{residualNorms}")
981 blockVectorX = bestblockVectorX
982 # Making eigenvectors "exactly" satisfy the blockVectorY constrains
983 if blockVectorY is not None:
984 _applyConstraints(blockVectorX,
985 gramYBY,
986 blockVectorBY,
987 blockVectorY)
989 # Making eigenvectors "exactly" othonormalized by final "exact" RR
990 blockVectorAX = A(blockVectorX)
991 if blockVectorAX.shape != blockVectorX.shape:
992 raise ValueError(
993 f"The shape {blockVectorX.shape} "
994 f"of the postprocessing iterate not preserved\n"
995 f"and changed to {blockVectorAX.shape} "
996 f"after multiplying by the primary matrix.\n"
997 )
998 gramXAX = np.dot(blockVectorX.T.conj(), blockVectorAX)
1000 blockVectorBX = blockVectorX
1001 if B is not None:
1002 blockVectorBX = B(blockVectorX)
1003 if blockVectorBX.shape != blockVectorX.shape:
1004 raise ValueError(
1005 f"The shape {blockVectorX.shape} "
1006 f"of the postprocessing iterate not preserved\n"
1007 f"and changed to {blockVectorBX.shape} "
1008 f"after multiplying by the secondary matrix.\n"
1009 )
1011 gramXBX = np.dot(blockVectorX.T.conj(), blockVectorBX)
1012 _handle_gramA_gramB_verbosity(gramXAX, gramXBX, verbosityLevel)
1013 gramXAX = (gramXAX + gramXAX.T.conj()) / 2
1014 gramXBX = (gramXBX + gramXBX.T.conj()) / 2
1015 try:
1016 _lambda, eigBlockVector = eigh(gramXAX,
1017 gramXBX,
1018 check_finite=False)
1019 except LinAlgError as e:
1020 raise ValueError("eigh has failed in lobpcg postprocessing") from e
1022 ii = _get_indx(_lambda, sizeX, largest)
1023 _lambda = _lambda[ii]
1024 eigBlockVector = np.asarray(eigBlockVector[:, ii])
1026 blockVectorX = np.dot(blockVectorX, eigBlockVector)
1027 blockVectorAX = np.dot(blockVectorAX, eigBlockVector)
1029 if B is not None:
1030 blockVectorBX = np.dot(blockVectorBX, eigBlockVector)
1031 aux = blockVectorBX * _lambda[np.newaxis, :]
1032 else:
1033 aux = blockVectorX * _lambda[np.newaxis, :]
1035 blockVectorR = blockVectorAX - aux
1037 aux = np.sum(blockVectorR.conj() * blockVectorR, 0)
1038 residualNorms = np.sqrt(np.abs(aux))
1040 if retLambdaHistory:
1041 lambdaHistory[bestIterationNumber + 1, :] = _lambda
1042 if retResidualNormsHistory:
1043 residualNormsHistory[bestIterationNumber + 1, :] = residualNorms
1045 if retLambdaHistory:
1046 lambdaHistory = lambdaHistory[
1047 : bestIterationNumber + 2, :]
1048 if retResidualNormsHistory:
1049 residualNormsHistory = residualNormsHistory[
1050 : bestIterationNumber + 2, :]
1052 if np.max(np.abs(residualNorms)) > residualTolerance:
1053 warnings.warn(
1054 f"Exited postprocessing with accuracies \n"
1055 f"{residualNorms}\n"
1056 f"not reaching the requested tolerance {residualTolerance}.",
1057 UserWarning, stacklevel=2
1058 )
1060 if verbosityLevel:
1061 print(f"Final postprocessing eigenvalue(s):\n{_lambda}")
1062 print(f"Final residual norm(s):\n{residualNorms}")
1064 if retLambdaHistory:
1065 lambdaHistory = np.vsplit(lambdaHistory, np.shape(lambdaHistory)[0])
1066 lambdaHistory = [np.squeeze(i) for i in lambdaHistory]
1067 if retResidualNormsHistory:
1068 residualNormsHistory = np.vsplit(residualNormsHistory,
1069 np.shape(residualNormsHistory)[0])
1070 residualNormsHistory = [np.squeeze(i) for i in residualNormsHistory]
1072 if retLambdaHistory:
1073 if retResidualNormsHistory:
1074 return _lambda, blockVectorX, lambdaHistory, residualNormsHistory
1075 else:
1076 return _lambda, blockVectorX, lambdaHistory
1077 else:
1078 if retResidualNormsHistory:
1079 return _lambda, blockVectorX, residualNormsHistory
1080 else:
1081 return _lambda, blockVectorX