Skip to content

Commit 8a85570

Browse files
committed
CuPy QR + various minor changes
1 parent d7063c6 commit 8a85570

File tree

3 files changed

+273
-45
lines changed

3 files changed

+273
-45
lines changed

src/pyclassify/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"Lanczos_PRO",
99
"QR_method",
1010
"QR",
11+
"Lanczos_PRO_cp",
12+
"QR_method_cp",
13+
"QR_cp",
1114
]
1215

1316
from .eigenvalues import (
@@ -20,4 +23,7 @@
2023
Lanczos_PRO,
2124
QR_method,
2225
QR,
26+
Lanczos_PRO_cp,
27+
QR_method_cp,
28+
QR_cp,
2329
)

src/pyclassify/eigenvalues.py

Lines changed: 231 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def power_method_cp(A, max_iter=500, tol=1e-4, x=None):
197197

198198

199199
@jit(nopython=True)
200-
def Lanczos_PRO(A, q=None, m=None, toll=np.sqrt(np.finfo(float).eps)):
200+
def Lanczos_PRO(A, q=None, m=None, tol=1e-8):
201201
r"""
202202
Perform the Lanczos algorithm for symmetric matrices.
203203
@@ -210,7 +210,7 @@ def Lanczos_PRO(A, q=None, m=None, toll=np.sqrt(np.finfo(float).eps)):
210210
q (np.ndarray): Initial vector of size n.
211211
m (int, optional): Number of eigenvalues to compute. Must be less than or equal to n.
212212
If None, defaults to the size of A.
213-
toll (float, optional): Tolerance for orthogonality checks (default is sqrt(machine epsilon)).
213+
tol (float, optional): Tolerance for orthogonality checks (default is sqrt(machine epsilon)).
214214
215215
Returns:
216216
tuple: A tuple (Q, alpha, beta) where:
@@ -251,7 +251,7 @@ def Lanczos_PRO(A, q=None, m=None, toll=np.sqrt(np.finfo(float).eps)):
251251

252252
for j in range(1, m):
253253
q = r / beta[j - 1]
254-
if np.any(np.abs(q @ Q[: j - 1].T) > toll):
254+
if np.any(np.abs(q @ Q[: j - 1].T) > tol):
255255
for q_bbasis in Q[: j - 1]:
256256
q = q - (q @ q_bbasis) * q_bbasis
257257

@@ -263,9 +263,8 @@ def Lanczos_PRO(A, q=None, m=None, toll=np.sqrt(np.finfo(float).eps)):
263263
beta.append(np.linalg.norm(r))
264264

265265
if np.abs(beta[j]) < 1e-15:
266-
267-
return Q, alpha, beta[:-1]
268-
return Q, alpha, beta[:-1]
266+
return Q, np.array(alpha), np.array(beta[:-1])
267+
return Q, np.array(alpha), np.array(beta[:-1])
269268

270269

271270
@jit(nopython=True)
@@ -297,15 +296,15 @@ def QR_method(diag, off_diag, tol=1e-8, max_iter=100):
297296
Matrix_trigonometric = np.zeros((n - 1, 2))
298297

299298
iter = 0
300-
#eigenvalues_old = np.array(diag)
299+
# eigenvalues_old = np.array(diag)
301300

302301
r, c, s = 0, 0, 0
303302
d, mu = 0, 0 # mu: Wilkinson shift
304303
a_m, b_m_1 = 0, 0
305-
#tmp = 0
304+
# tmp = 0
306305
x, y = 0, 0
307306
m = n - 1
308-
toll_equivalence = 1e-10
307+
tol_equivalence = 1e-10
309308
w, z = 0, 0
310309

311310
while iter < max_iter and m > 0:
@@ -314,7 +313,7 @@ def QR_method(diag, off_diag, tol=1e-8, max_iter=100):
314313
b_m_1 = off_diag[m - 1]
315314
d = (diag[m - 1] - a_m) * 0.5
316315

317-
if np.abs(d) < toll_equivalence:
316+
if np.abs(d) < tol_equivalence:
318317
mu = diag[m] - np.abs(b_m_1)
319318
else:
320319
mu = a_m - b_m_1 * b_m_1 / (
@@ -347,7 +346,7 @@ def QR_method(diag, off_diag, tol=1e-8, max_iter=100):
347346
off_diag[i + 1] = c * off_diag[i + 1]
348347

349348
else:
350-
if abs(d) < toll_equivalence:
349+
if abs(d) < tol_equivalence:
351350
if off_diag[0] * d > 0:
352351
c = np.sqrt(2) / 2
353352
s = -np.sqrt(2) / 2
@@ -364,7 +363,9 @@ def QR_method(diag, off_diag, tol=1e-8, max_iter=100):
364363
err_rel = 1
365364
iter_newton = 0
366365
while err_rel > 1e-10 and iter_newton < 1000:
367-
x_new = x_0 - np.cos(x_0) * np.cos(x_0) * (np.tan(x_0) + b_2 / d)
366+
x_new = x_0 - np.cos(x_0) * np.cos(x_0) * (
367+
np.tan(x_0) + b_2 / d
368+
)
368369
err_rel = np.abs((x_new - x_0) / x_new)
369370
x_0 = x_new
370371
iter_newton += 1
@@ -383,7 +384,7 @@ def QR_method(diag, off_diag, tol=1e-8, max_iter=100):
383384
diag[1] = c * c * diag[1] + s * s * a_0 + 2 * s * c * b_1
384385

385386
# Uncomment to compute the eigenvalue
386-
#Q[:, :m] = Q[:, :m] @ Matrix_trigonometric[:m, :]
387+
# Q[:, :m] = Q[:, :m] @ Matrix_trigonometric[:m, :]
387388

388389
iter += 1
389390
if abs(off_diag[m - 1]) < tol * (np.abs(diag[m]) + np.abs(diag[m - 1])):
@@ -413,7 +414,221 @@ def QR(A, q0=None, tol=1e-8, max_iter=100):
413414
Raises:
414415
ValueError: If the input matrix is not square.
415416
"""
416-
_, alpha, beta = Lanczos_PRO(A, q=q0, m=None, toll=1e-8)
417-
alpha = np.array(alpha)
418-
beta = np.array(beta)
417+
_, alpha, beta = Lanczos_PRO(A, q=q0, m=None, tol=1e-8)
419418
return QR_method(alpha, beta, tol=tol, max_iter=max_iter)
419+
420+
421+
def Lanczos_PRO_cp(A, q=None, m=None, tol=1e-8):
422+
r"""
423+
Perform the Lanczos algorithm for symmetric matrices.
424+
425+
This function computes an orthogonal matrix Q and tridiagonal matrix T such that A is approximately
426+
equal to Q * T * Q.T, where A is a symmetric matrix. The algorithm is useful for finding a few
427+
eigenvalues and eigenvectors of large symmetric matrices.
428+
429+
Args:
430+
A (cp.ndarray or cpsp.spmatrix): A symmetric square matrix of size n x n.
431+
q (cp.ndarray): Initial vector of size n.
432+
m (int, optional): Number of eigenvalues to compute. Must be less than or equal to n.
433+
If None, defaults to the size of A.
434+
tol (float, optional): Tolerance for orthogonality checks (default is sqrt(machine epsilon)).
435+
436+
Returns:
437+
tuple: A tuple (Q, alpha, beta) where:
438+
- Q (cp.ndarray): Orthogonal matrix of size n x m.
439+
- alpha (cp.ndarray): Vector of size m containing the diagonal elements of the tridiagonal matrix.
440+
- beta (cp.ndarray): Vector of size m-1 containing the off-diagonal elements of the tridiagonal matrix.
441+
442+
Raises:
443+
ValueError: If the input matrix A is not square or if m is greater than the size of A.
444+
"""
445+
if q is None:
446+
q = cp.random.rand(A.shape[0])
447+
if q[0] == 0:
448+
q[0] += 1
449+
450+
if m == None:
451+
m = A.shape[0]
452+
453+
check_symm_square(A)
454+
455+
if A.shape[0] != q.shape[0]:
456+
raise ValueError("Input vector q must have the same size as the matrix A.")
457+
458+
q = q / cp.linalg.norm(q)
459+
# Q=np.array([q])
460+
Q = cp.zeros((m, A.shape[0]))
461+
Q[0] = q
462+
r = A @ q
463+
alpha = []
464+
beta = []
465+
alpha.append(q @ r)
466+
r = r - alpha[0] * q
467+
beta.append(cp.linalg.norm(r))
468+
469+
for j in range(1, m):
470+
q = r / beta[j - 1]
471+
if cp.any(cp.abs(q @ Q[: j - 1].T) > tol):
472+
for q_bbasis in Q[: j - 1]:
473+
q = q - (q @ q_bbasis) * q_bbasis
474+
475+
q = q / cp.linalg.norm(q)
476+
Q[j] = q
477+
r = A @ q - beta[j - 1] * Q[j - 1]
478+
alpha.append(q @ r)
479+
r = r - alpha[j] * q
480+
beta.append(cp.linalg.norm(r))
481+
482+
if cp.abs(beta[j]) < 1e-15:
483+
484+
return cp.array(alpha), cp.array(beta[:-1])
485+
return Q, cp.array(alpha), cp.array(beta[:-1])
486+
487+
488+
def QR_method_cp(diag, off_diag, tol=1e-8, max_iter=100):
489+
"""
490+
Compute the eigenvalues of a tridiagonal matrix using the QR algorithm.
491+
492+
This function uses the QR decomposition method to iteratively compute the eigenvalues of a given tridiagonal matrix.
493+
The QR algorithm is an iterative method that computes the eigenvalues of a matrix by decomposing it into a product
494+
of an orthogonal matrix Q and an upper triangular matrix R, and then updating the matrix as the product of R and Q.
495+
496+
Args:
497+
diag (cp.ndarray): Diagonal elements of the tridiagonal matrix.
498+
off_diag (cp.ndarray): Off-diagonal elements of the tridiagonal matrix.
499+
tol (float, optional): Tolerance for convergence based on the off-diagonal elements (default is 1e-10).
500+
max_iter (int, optional): Maximum number of iterations to perform (default is 100).
501+
502+
Returns:
503+
tuple: A tuple (eigenvalues, Q) where:
504+
- eigenvalues (cp.ndarray): An array containing the eigenvalues of the matrix.
505+
- Q (cp.ndarray): The orthogonal matrix Q from the final QR decomposition.
506+
507+
Raises:
508+
ValueError: If the input matrix is not square.
509+
"""
510+
n = diag.shape[0]
511+
Q = cp.eye(n)
512+
513+
Matrix_trigonometric = cp.zeros((n - 1, 2))
514+
515+
iter = 0
516+
# eigenvalues_old = np.array(diag)
517+
518+
r, c, s = 0, 0, 0
519+
d, mu = 0, 0 # mu: Wilkinson shift
520+
a_m, b_m_1 = 0, 0
521+
x, y = 0, 0
522+
m = n - 1
523+
tol_equivalence = 1e-10
524+
w, z = 0, 0
525+
526+
while iter < max_iter and m > 0:
527+
# prefetching most used value to avoid call overhead
528+
a_m = diag[m]
529+
b_m_1 = off_diag[m - 1]
530+
d = (diag[m - 1] - a_m) * 0.5
531+
532+
if cp.abs(d) < tol_equivalence:
533+
mu = diag[m] - cp.abs(b_m_1)
534+
else:
535+
mu = a_m - b_m_1 * b_m_1 / (
536+
d * (1 + cp.sqrt(d * d + b_m_1 * b_m_1) / cp.abs(d))
537+
)
538+
539+
x = diag[0] - mu
540+
y = off_diag[0]
541+
542+
for i in range(m):
543+
if m > 1:
544+
r = np.sqrt(x * x + y * y)
545+
c = x / r
546+
s = -y / r
547+
Matrix_trigonometric[i][0] = c
548+
Matrix_trigonometric[i][1] = s
549+
550+
w = c * x - s * y
551+
d = diag[i] - diag[i + 1]
552+
z = (2 * c * off_diag[i] + d * s) * s
553+
diag[i] -= z
554+
diag[i + 1] += z
555+
off_diag[i] = d * c * s + (c * c - s * s) * off_diag[i]
556+
x = off_diag[i]
557+
if i > 0:
558+
off_diag[i - 1] = w
559+
560+
if i < m - 1:
561+
y = -s * off_diag[i + 1]
562+
off_diag[i + 1] = c * off_diag[i + 1]
563+
564+
else:
565+
if abs(d) < tol_equivalence:
566+
if off_diag[0] * d > 0:
567+
c = cp.sqrt(2) / 2
568+
s = -cp.sqrt(2) / 2
569+
else:
570+
c = s = cp.sqrt(2) / 2
571+
572+
else:
573+
b_2 = off_diag[0]
574+
if off_diag[0] * d > 0:
575+
x_0 = -cp.pi / 4
576+
else:
577+
x_0 = cp.pi / 4
578+
579+
err_rel = 1
580+
iter_newton = 0
581+
while err_rel > 1e-10 and iter_newton < 1000:
582+
x_new = x_0 - cp.cos(x_0) * cp.cos(x_0) * (
583+
cp.tan(x_0) + b_2 / d
584+
)
585+
err_rel = cp.abs((x_new - x_0) / x_new)
586+
x_0 = x_new
587+
iter_newton += 1
588+
589+
c = cp.cos(x_new / 2)
590+
s = cp.sin(x_new / 2)
591+
592+
Matrix_trigonometric[i][0] = c
593+
Matrix_trigonometric[i][1] = s
594+
595+
a_0 = diag[0]
596+
b_1 = off_diag[0]
597+
598+
off_diag[0] = 0 # c * s * (a_0 - diag[1]) + b_1 * (c * c - s * s)
599+
diag[0] = c * c * a_0 + s * s * diag[1] - 2 * s * c * b_1
600+
diag[1] = c * c * diag[1] + s * s * a_0 + 2 * s * c * b_1
601+
602+
# Uncomment to compute the eigenvalue
603+
# Q[:, :m] = Q[:, :m] @ Matrix_trigonometric[:m, :]
604+
605+
iter += 1
606+
if abs(off_diag[m - 1]) < tol * (cp.abs(diag[m]) + cp.abs(diag[m - 1])):
607+
m -= 1
608+
609+
return diag, Q
610+
611+
612+
@profile
613+
def QR_cp(A, q0=None, tol=1e-8, max_iter=100):
614+
"""
615+
Compute the eigenvalues of a square matrix using the QR algorithm.
616+
Done using the Lanczos algorithm to compute the tridiagonal matrix and then the QR
617+
algorithm to compute the eigenvalues.
618+
619+
Args:
620+
A (cp.ndarray or cpsp.spmatrix): A square matrix whose eigenvalues are to be computed.
621+
q0 (cp.ndarray, optional): An initial vector for the Lanczos process. If None, a random vector is used.
622+
tol (float, optional): Convergence tolerance for the QR algorithm. Default is 1e-8
623+
max_iter (int, optional): Maximum number of iterations for the QR algorithm. Deault is 100.
624+
625+
Returns:
626+
tuple: A tuple (eigenvalues, Q) where:
627+
- eigenvalues (cp.ndarray): An array containing the eigenvalues of the matrix.
628+
- Q (cp.ndarray): The orthogonal matrix Q from the final QR decomposition.
629+
630+
Raises:
631+
ValueError: If the input matrix is not square.
632+
"""
633+
_, alpha, beta = Lanczos_PRO_cp(A, q=q0, m=None, tol=1e-8)
634+
return QR_method_cp(alpha, beta, tol=tol, max_iter=max_iter)

0 commit comments

Comments
 (0)