Skip to content

Commit 3eaff2f

Browse files
committed
Keeping up to date wrt final_project branch
2 parents 394fa49 + e4953bf commit 3eaff2f

File tree

7 files changed

+206
-20
lines changed

7 files changed

+206
-20
lines changed

docs/Documentation.ipynb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Documentation \n",
8+
"**Disclaimer**: *this document is not meant to be neither a formal nor an exhaustive description of the iterative method to solve the eigenvalue problem. The aim of this text is only to provide the necessary and minimal information needed for the reader to understand the code implementation. We will use these pages to document, explain and justify the choice made from a numerical and scientific computing standpoint*.\n",
9+
"\n",
10+
"## Problem statement\n",
11+
"Given a matrix $\\boldsymbol{A} \\in \\mathbb{C}^{n, n}$, with $n \\in \\mathbb{N}$ the matrix dimension, the eigenvalue problem can be formulated as finding the eigenpair $\\{(\\lambda_i, \\boldsymbol{v}_i)\\}_{i=1} ^ n$, with $\\lambda_i \\in \\mathbb{C}$, $\\boldsymbol{v}_i \\in \\mathbb{C}^{n ,1}$ and $\\boldsymbol{v}_i \\ne \\boldsymbol{0}$ such that \n",
12+
"$$\\boldsymbol{A} \\boldsymbol{v}_i = \\lambda_i \\boldsymbol{v}_i, \\quad i=1, 2, ..., n $$\n",
13+
"\n",
14+
"$\\lambda_i$ are called eigenvalue, while $\\boldsymbol{v}_i$ are the corresponding eigenvectors. Theoretically, finding the matrix's eigenvalue is possible by imposing the following condition:\n",
15+
"$$ \\det(A-\\lambda I)=0 $$\n",
16+
"which lead to the well known characteristic polynomial of degree $n$. The eigenvalue are the roots of the characteristic polynomial. Although correct, this apporach is not viable, due to the difficulties in both expressing the characteristic polynomial and find its roots, when the Matrix gets very large.\n",
17+
"\n",
18+
"Numerical methods takle the eigenproblem using a different strategy, most of the time being a iterative methods. Rather than aspire to get the exact solution, they seek after an approximation of the solution, which hopefully, under a set of conditions, converges to the exact solution.\n",
19+
"Among all the method devolped to solve the eigenproblem, the power method, along with its variants suct that the inverse power method and power method with shift, and the QR are the widest spread and the most used.\n",
20+
"\n",
21+
"## Power method\n",
22+
"Let $\\boldsymbol{A}$ being a matric and $\\lambda _i$ the eigenvalue already sorted accordingly to their module. If\n",
23+
"$$ |\\lambda_1| > |\\lambda _i | \\quad i=2, 3, ..., n$$\n",
24+
"then the power method allow to recover the eigenpair $(\\lambda_1, \\boldsymbol{v}_1)$ by applying iteratively, the following steps\n",
25+
"\n",
26+
"\n",
27+
"$$\n",
28+
"\\begin{array}{l}\n",
29+
"\\textbf{Power Method Algorithm} \\\\\n",
30+
"\\textbf{Input:} A \\in \\mathbb{C}^{n \\times n}, \\text{initial vector } x_0, \\text{ tolerance } \\text{tol}, \\text{ max iterations } \\text{max\\_iter} \\\\\n",
31+
"\\textbf{Output:} \\lambda \\text{ (dominant eigenvalue approximation), } x \\text{ (eigenvector approximation)} \\\\\n",
32+
"\n",
33+
"1.\\ \\text{Initialize } x = x_0 \\text{ (random or chosen guess)} \\\\\n",
34+
"2.\\ \\text{Normalize } x \\text{: } y^{(1)} = x/ \\| x\\| \\\\\n",
35+
"3.\\ \\lambda_{\\text{old}} = 0 \\\\\n",
36+
"\n",
37+
"4.\\ \\textbf{for } k = 1 \\textbf{ to } \\text{max\\_iter} \\textbf{ do} \\\\\n",
38+
"\\quad 5.\\ x^{(k+1)} = A \\cdot y^{(k)} \\\\\n",
39+
"\\quad 6.\\ y^{(k+1)} = x^{(k+1)}/ \\| x^{(k+1)}\\| \\\\\n",
40+
"\\quad 7.\\ \\lambda^{(k+1)} = (y^{(k+1)})^H \\boldsymbol{A} y^{(k+1)} \\\\\n",
41+
"\\quad 8.\\ \\textbf{if } |\\lambda^{(k+1)} - \\lambda^{(k)}| < \\text{tol} \\textbf{ then} \\\\\n",
42+
"\\quad \\quad \\text{Break (convergence reached)} \\\\\n",
43+
"\n",
44+
"9.\\ \\textbf{Return } \\lambda, x\n",
45+
"\\end{array}\n",
46+
"$$\n",
47+
"\n",
48+
"By exploiting Matrix properties, it is also possible to find the eigenvalue with the smallest norm, and its associated eigenvector, or either the the eigenvalue closest to a given number.\n",
49+
"\n",
50+
"## QR algorithm\n",
51+
"Being the problem we are interested in to solve incolve a symmetric Matrix, we will start introduce the QR alforithm in a general way and specialize it for the case of symmetric matrices as soon as we have the chance.\n",
52+
"\n",
53+
"The QR algorithm can be divide in to two stages: during the first stage, the matrix is reduced, by means of similar trasformation, into a Hessemberg matrix, or, for Hermetian matrix, into a tridiagonal matrix. Although the QR algorithm can be applied directly to a full matrix, it converges faster if it is applied to Hessemberg Matrix or triagular matrices, with also a smaller computational cost.\n"
54+
]
55+
}
56+
],
57+
"metadata": {
58+
"kernelspec": {
59+
"display_name": "Python 3",
60+
"language": "python",
61+
"name": "python3"
62+
},
63+
"language_info": {
64+
"name": "python",
65+
"version": "3.12.7"
66+
}
67+
},
68+
"nbformat": 4,
69+
"nbformat_minor": 2
70+
}

docs/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ help:
1717
# Catch-all target: route all unknown targets to Sphinx using the new
1818
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
1919
%: Makefile
20+
export LC_ALL=C
2021
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

requirements.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
alabaster==1.0.0
2+
asttokens==3.0.0
23
babel==2.17.0
34
black==25.1.0
45
certifi==2025.1.31
@@ -7,27 +8,38 @@ click==8.1.8
78
contourpy==1.3.1
89
cupy-cuda12x==13.4.0
910
cycler==0.12.1
11+
decorator==5.2.1
1012
docutils==0.21.2
1113
exceptiongroup==1.2.2
14+
executing==2.2.0
1215
fastrlock==0.8.3
1316
fonttools==4.56.0
1417
idna==3.10
1518
imagesize==1.4.1
1619
iniconfig==2.0.0
20+
ipython==9.0.1
21+
ipython_pygments_lexers==1.1.1
22+
jedi==0.19.2
1723
Jinja2==3.1.5
1824
kiwisolver==1.4.8
1925
line_profiler==4.2.0
2026
llvmlite==0.44.0
2127
MarkupSafe==3.0.2
2228
matplotlib==3.10.0
29+
matplotlib-inline==0.1.7
2330
mypy-extensions==1.0.0
2431
numba==0.61.0
2532
numpy==2.1.3
2633
packaging==24.2
34+
parso==0.8.4
2735
pathspec==0.12.1
36+
pexpect==4.9.0
2837
pillow==11.1.0
2938
platformdirs==4.3.6
3039
pluggy==1.5.0
40+
prompt_toolkit==3.0.50
41+
ptyprocess==0.7.0
42+
pure_eval==0.2.3
3143
Pygments==2.19.1
3244
pyparsing==3.2.1
3345
pytest==8.3.4
@@ -47,7 +59,11 @@ sphinxcontrib-jquery==4.1
4759
sphinxcontrib-jsmath==1.0.1
4860
sphinxcontrib-qthelp==2.0.0
4961
sphinxcontrib-serializinghtml==2.0.0
62+
stack-data==0.6.3
63+
tokenize_rt==6.1.0
5064
tomli==2.2.1
65+
traitlets==5.14.3
5166
typing_extensions==4.12.2
5267
urllib3==2.3.0
68+
wcwidth==0.2.13
5369
wheel==0.45.1

shell/submit.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
module load cuda/12.1
4+
module use cuda/12.1
5+
6+
pytest -v > output_pytest.txt

src/pyclassify/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"power_method",
66
"power_method_numba",
77
"power_method_cp",
8+
"Lanczos_PRO",
9+
"QR_method",
810
]
911

1012
from .eigenvalues import (
@@ -14,4 +16,6 @@
1416
power_method,
1517
power_method_numba,
1618
power_method_cp,
19+
Lanczos_PRO,
20+
QR_method,
1721
)

src/pyclassify/eigenvalues.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def power_method(A, max_iter=500, tol=1e-4, x=None):
137137

138138

139139
@profile
140-
def power_method_numba(A):
140+
def power_method_numba(A, max_iter=500, tol=1e-4, x=None):
141141
"""
142142
Compute the dominant eigenvalue of a matrix using the power method, with Numba optimization.
143143
@@ -154,7 +154,7 @@ def power_method_numba(A):
154154
Returns:
155155
float: The approximated dominant eigenvalue of the matrix `A`.
156156
"""
157-
return power_method_numba_helper(A)
157+
return power_method_numba_helper(A, max_iter, tol, x)
158158

159159

160160
@profile
@@ -219,6 +219,15 @@ def Lanczos_PRO(A, q, m=None, toll=np.sqrt(np.finfo(float).eps)):
219219
Raises:
220220
ValueError: If the input matrix A is not square or if m is greater than the size of A.
221221
"""
222+
if m == None:
223+
m = A.shape[0]
224+
225+
if A.shape[0] != A.shape[1]:
226+
raise ValueError("Input matrix A must be square.")
227+
228+
if A.shape[0] != q.shape[0]:
229+
raise ValueError("Input vector q must have the same size as the matrix A.")
230+
222231
q = q / np.linalg.norm(q)
223232
Q = np.array([q])
224233
r = A @ q
@@ -271,14 +280,12 @@ def QR_method(A_copy, tol=1e-10, max_iter=100):
271280
"""
272281

273282
A = A_copy.copy()
274-
T = A.copy()
275283
A = np.array(A)
276284
iter = 0
285+
Q = np.array([])
277286

278287
while np.linalg.norm(np.diag(A, -1), np.inf) > tol and iter < max_iter:
279288
Matrix_trigonometry = np.array([])
280-
QQ, RR = np.linalg.qr(T)
281-
T = RR @ QQ
282289
for i in range(A.shape[0] - 1):
283290
c = A[i, i] / np.sqrt(A[i, i] ** 2 + A[i + 1, i] ** 2)
284291
s = -A[i + 1, i] / np.sqrt(A[i, i] ** 2 + A[i + 1, i] ** 2)

test/test_.py

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import numpy as np
22
import scipy.sparse as sp
33
import cupyx.scipy.sparse as cpsp
4+
import cupy as cp
5+
import scipy
46
import pytest
57
from pyclassify import (
68
eigenvalues_np,
@@ -9,14 +11,19 @@
911
power_method,
1012
power_method_numba,
1113
power_method_cp,
14+
Lanczos_PRO,
15+
QR_method,
1216
)
1317
from pyclassify.utils import check_square_matrix, make_symmetric, check_symm_square
1418

1519

16-
np.random.seed(104)
20+
@pytest.fixture(autouse=True)
21+
def set_random_seed():
22+
seed = 1422
23+
np.random.seed(seed)
1724

1825

19-
sizes = [10, 100, 1000]
26+
sizes = [10, 100]
2027
densities = [0.1, 0.3]
2128

2229

@@ -34,8 +41,7 @@ def test_checks_on_A(size, density):
3441
symmetric_matrix = make_symmetric(matrix)
3542
check_square_matrix(symmetric_matrix)
3643

37-
# cp_symm_matrix = cpsp.csr_matrix(symmetric_matrix)
38-
# check_square_matrix(cp_symm_matrix)
44+
# regarding the CuPy implementation: see below!
3945

4046
not_so_symmetric_matrix = np.random.rand(5, 5)
4147
if not_so_symmetric_matrix[1, 2] == not_so_symmetric_matrix[2, 1]:
@@ -64,36 +70,112 @@ def test_make_symmetric(size, density):
6470
def test_implementations_power_method(size, density):
6571
matrix = sp.random(size, size, density=density, format="csr")
6672
matrix = make_symmetric(matrix)
67-
# cp_matrix = cpsp.csr_matrix(matrix)
6873

6974
eigs_np = eigenvalues_np(matrix.toarray(), symmetric=True)
7075
eigs_sp = eigenvalues_sp(matrix, symmetric=True)
71-
# eigs_cp = eigenvalues_cp(cp_matrix)
7276

7377
index_np = np.argmax(np.abs(eigs_np))
7478
index_sp = np.argmax(np.abs(eigs_sp))
75-
# index_cp = np.argmax(np.abs(eigs_cp))
7679

7780
biggest_eigenvalue_np = eigs_np[index_np]
7881
biggest_eigenvalue_sp = eigs_sp[index_sp]
79-
# biggest_eigenvalue_cp = eigs_cp[index_cp]
8082

8183
biggest_eigenvalue_pm = power_method(matrix)
8284
biggest_eigenvalue_pm_numba = power_method_numba(matrix.toarray())
83-
# biggest_eigenvalue_pm_cp = power_method_cp(cp_matrix)
8485

8586
assert np.isclose(
8687
biggest_eigenvalue_np, biggest_eigenvalue_sp, rtol=1e-4
8788
) # ensure numpy and scipy implementations are consistent
88-
# assert np.isclose(
89-
# biggest_eigenvalue_cp, biggest_eigenvalue_sp, rtol=1e-4
90-
# ) # ensure cupy and scipy implementations are consistent
9189
assert np.isclose(
9290
biggest_eigenvalue_pm, biggest_eigenvalue_sp, rtol=1e-4
9391
) # ensure power method and scipy implementation are consistent
9492
assert np.isclose(
9593
biggest_eigenvalue_pm_numba, biggest_eigenvalue_sp, rtol=1e-4
9694
) # ensure numba power method and scipy implementation are consistent
97-
# assert np.isclose(
98-
# biggest_eigenvalue_pm_cp, biggest_eigenvalue_sp, rtol=1e-4
99-
# ) # ensure cupy power method and scipy implementation are consistent
95+
96+
97+
@pytest.mark.parametrize("size", sizes)
98+
@pytest.mark.parametrize("density", densities)
99+
def test_cupy(size, density):
100+
try:
101+
if not cp.cuda.is_available():
102+
pytest.skip("Skipping test because CUDA is not available")
103+
except cp.cuda.runtime.CUDARuntimeError as e:
104+
pytest.skip(f"Skipping test due to CUDA driver issues: {str(e)}")
105+
106+
matrix = sp.random(size, size, density=density, format="csr")
107+
symmetric_matrix = make_symmetric(matrix)
108+
cp_symm_matrix = cpsp.csr_matrix(symmetric_matrix)
109+
check_square_matrix(cp_symm_matrix)
110+
111+
eigs_cp = eigenvalues_cp(cp_symm_matrix)
112+
113+
index_cp = np.argmax(np.abs(eigs_cp))
114+
biggest_eigenvalue_cp = eigs_cp[index_cp]
115+
biggest_eigenvalue_pm_cp = power_method_cp(cp_symm_matrix, max_iter=1000, tol=1e-5)
116+
117+
assert np.isclose(
118+
biggest_eigenvalue_cp, biggest_eigenvalue_pm_cp, rtol=1e-4
119+
) # ensure cupy native and cupy power method implementations are consistent
120+
121+
122+
@pytest.mark.parametrize("size", sizes)
123+
def test_Lanczos(size):
124+
matrix = sp.random(size, size, density=0.3, format="csr")
125+
matrix = make_symmetric(matrix)
126+
127+
random_vector = np.random.rand(size)
128+
129+
_, alpha, beta = Lanczos_PRO(matrix, random_vector)
130+
131+
T = np.diag(alpha) + np.diag(beta, 1) + np.diag(beta, -1)
132+
133+
EigVal_T = np.linalg.eig(T)[0]
134+
EigVect_T = np.linalg.eig(T)[1]
135+
136+
EigVal_A = np.linalg.eig(matrix.toarray())[0]
137+
EigVect_A = np.linalg.eig(matrix.toarray())[1]
138+
139+
EigVal_A = np.sort(EigVal_A)
140+
EigVal_T = np.sort(EigVal_T)
141+
142+
assert np.allclose(EigVal_T, EigVal_A, rtol=1e-6)
143+
144+
with pytest.raises(ValueError):
145+
random_matrix = np.random.rand(size, 2 * size)
146+
_ = Lanczos_PRO(random_matrix, random_vector)
147+
148+
with pytest.raises(ValueError):
149+
random_matrix = np.random.rand(size, size)
150+
_ = Lanczos_PRO(random_matrix, np.random.rand(2 * size))
151+
152+
153+
@pytest.mark.parametrize("size", [10, 100])
154+
def test_QR_method(size):
155+
eig = np.arange(1, size + 1)
156+
A = np.diag(eig)
157+
U = scipy.stats.ortho_group.rvs(size)
158+
print(U)
159+
160+
A = U @ A @ U.T
161+
print(A)
162+
A = make_symmetric(A)
163+
eig = np.linalg.eig(A)
164+
print(eig.eigenvalues)
165+
index = np.argsort(eig.eigenvalues)
166+
eig = eig.eigenvalues
167+
eig_vec = np.linalg.eig(A).eigenvectors
168+
eig_vec = eig_vec[index]
169+
eig = eig[index]
170+
eig_vec = eig_vec / np.linalg.norm(eig_vec, axis=0)
171+
172+
random_vector = np.random.rand(size)
173+
_, alpha, beta = Lanczos_PRO(A, random_vector)
174+
175+
T = np.diag(alpha) + np.diag(beta, 1) + np.diag(beta, -1)
176+
177+
eig_valQR, eig_vecQR = QR_method(T, max_iter=100 * size)
178+
index = np.argsort(eig_valQR)
179+
eig_valQR = eig_valQR[index]
180+
181+
assert np.allclose(eig, eig_valQR, rtol=1e-8)

0 commit comments

Comments
 (0)