Skip to content

Commit f8b792b

Browse files
committed
Add sketch of run.py file, add script for profiling, numba for power method
1 parent 41b5c04 commit f8b792b

File tree

9 files changed

+166
-46
lines changed

9 files changed

+166
-46
lines changed

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,4 @@ format:
1919
python -m black .
2020

2121
all: tests docs format
22-
@echo "Tests completed, documentation generated, and code formatted."
23-
22+
@echo "Tests completed, documentation generated, and code formatted."

experiments/config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dimension: 1000
2+
3+
density: 0.1

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Pygments==2.19.1
3030
pyparsing==3.2.1
3131
pytest==8.3.4
3232
python-dateutil==2.9.0.post0
33+
PyYAML==6.0.2
3334
requests==2.32.3
3435
scipy==1.15.2
3536
six==1.17.0

scripts/run.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from pyclassify import (
2+
eigenvalues_np,
3+
eigenvalues_sp,
4+
power_method,
5+
power_method_numba,
6+
)
7+
from pyclassify.utils import make_symmetric, read_config
8+
import numpy as np
9+
import scipy.sparse as sp
10+
import random
11+
import argparse
12+
13+
14+
random.seed(226)
15+
16+
17+
parser = argparse.ArgumentParser()
18+
parser.add_argument("--config", type=str, required=False, help="config file:")
19+
20+
21+
args = parser.parse_args()
22+
filename = (
23+
args.config if args.config else "./experiments/config"
24+
) # automatic choice if no argument is passed
25+
26+
27+
kwargs = read_config(filename)
28+
dim = kwargs["dimension"]
29+
density = kwargs["density"]
30+
31+
32+
matrix = sp.random(dim, dim, density=density, format="csr")
33+
matrix = make_symmetric(matrix)
34+
35+
eigs_np = eigenvalues_np(matrix.toarray(), symmetric=True)
36+
eigs_sp = eigenvalues_sp(matrix, symmetric=True)
37+
38+
index_np = np.argmax(np.abs(eigs_np))
39+
index_sp = np.argmax(np.abs(eigs_sp))
40+
41+
biggest_eigenvalue_np = eigs_np[index_np]
42+
biggest_eigenvalue_sp = eigs_sp[index_sp]
43+
44+
biggest_eigenvalue_pm = power_method(matrix)
45+
biggest_eigenvalue_pm_numba = power_method_numba(matrix.toarray())

shell/profile.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
python -m kernprof -l -o logs/profile_eigenvalues.dat scripts/run.py --config=experiments/config
3+
python -m line_profiler -rmt "logs/profile_eigenvalues.dat" > logs/eigenvalues.txt

src/pyclassify/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
__all__ = [
2+
'eigenvalues_np',
3+
'eigenvalues_sp',
4+
'power_method',
5+
'power_method_numba',
6+
]
7+
8+
from .eigenvalues import eigenvalues_np, eigenvalues_sp, power_method, power_method_numba

src/pyclassify/eigenvalues.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import numpy as np
2+
import scipy.sparse as sp
3+
from line_profiler import profile
4+
from numpy.linalg import eig, eigh
5+
from pyclassify.utils import check_A_square_matrix, power_method_numba_helper
6+
7+
@profile
8+
def eigenvalues_np(A, symmetric=True):
9+
check_A_square_matrix(A)
10+
eigenvalues, _ = eigh(A) if symmetric else eig(A)
11+
return eigenvalues
12+
13+
14+
@profile
15+
def eigenvalues_sp(A, symmetric=True):
16+
check_A_square_matrix(A)
17+
eigenvalues, _ = (
18+
sp.linalg.eigsh(A, k=A.shape[0] - 1)
19+
if symmetric
20+
else sp.linalg.eigs(A, k=A.shape[0] - 1)
21+
)
22+
return eigenvalues
23+
24+
25+
@profile
26+
def power_method(A, max_iter=500, tol=1e-4, x=None):
27+
check_A_square_matrix(A)
28+
if x is None:
29+
x = np.random.rand(A.shape[0])
30+
x /= np.linalg.norm(x)
31+
x_old = x
32+
33+
iteration = 0
34+
update_norm = tol + 1
35+
36+
while iteration < max_iter and update_norm > tol:
37+
x = A @ x
38+
x /= np.linalg.norm(x)
39+
update_norm = np.linalg.norm(x - x_old) / np.linalg.norm(x_old)
40+
x_old = x.copy()
41+
iteration += 1
42+
43+
return x @ A @ x
44+
45+
46+
47+
@profile
48+
def power_method_numba(A):
49+
return power_method_numba_helper(A)

src/pyclassify/utils.py

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,33 @@
11
import numpy as np
22
import scipy.sparse as sp
3-
from numpy.linalg import eig, eigh
3+
import numba
4+
import os
5+
import yaml
6+
from line_profiler import profile
47

8+
# from numba.pycc import CC
59

610

7-
def make_symmetric(A):
8-
if not sp.isspmatrix(A):
9-
raise TypeError("Input must be a scipy sparse matrix")
10-
11-
A_sym = (A + A.T) / 2
12-
return A_sym
13-
14-
15-
16-
def eigenvalues_np(A, symmetric=True):
17-
if A.shape[0] != A.shape[1]:
18-
raise ValueError("Matrix must be square!")
11+
def check_A_square_matrix(A):
1912
if not isinstance(A, (np.ndarray, sp.spmatrix)):
2013
raise TypeError("Input matrix must be a NumPy array or a SciPy sparse matrix!")
21-
22-
eigenvalues, _ = eigh(A) if symmetric else eig(A)
23-
return eigenvalues
24-
25-
26-
27-
def eigenvalues_sp(A, symmetric=True):
2814
if A.shape[0] != A.shape[1]:
2915
raise ValueError("Matrix must be square!")
30-
if not isinstance(A, (np.ndarray, sp.spmatrix)):
31-
raise TypeError("Input matrix must be a NumPy array or a SciPy sparse matrix!")
32-
33-
eigenvalues, _ = sp.linalg.eigsh(A, k=A.shape[0] - 1) if symmetric else sp.linalg.eigs(A, k=A.shape[0] - 1)
34-
return eigenvalues
3516

3617

18+
@profile
19+
def make_symmetric(A):
20+
check_A_square_matrix(A)
21+
A_sym = (A + A.T) / 2
22+
return A_sym
3723

38-
def power_method(A, max_iter=1000, tol=1e-4):
39-
assert A.shape[0] == A.shape[1], "Matrix must be square"
4024

41-
x = np.random.rand(A.shape[0])
25+
@numba.njit(nogil=True, parallel=True)
26+
def power_method_numba_helper(A, max_iter=500, tol=1e-4, x=None):
27+
if A.shape[0] != A.shape[1]:
28+
raise ValueError("Matrix must be square!") # explain why re-written
29+
if x is None:
30+
x = np.random.rand(A.shape[0])
4231
x /= np.linalg.norm(x)
4332
x_old = x
4433

@@ -48,8 +37,22 @@ def power_method(A, max_iter=1000, tol=1e-4):
4837
while iteration < max_iter and update_norm > tol:
4938
x = A @ x
5039
x /= np.linalg.norm(x)
51-
update_norm = np.linalg.norm(x - x_old)/np.linalg.norm(x_old)
40+
update_norm = np.linalg.norm(x - x_old) / np.linalg.norm(x_old)
5241
x_old = x.copy()
5342
iteration += 1
5443

55-
return x @ A @ x
44+
return x @ A @ x
45+
46+
47+
def read_config(file: str) -> dict:
48+
"""
49+
To read the desired configuration file, passed in input as a string
50+
Input:
51+
file: str (representing the location of file)
52+
Returns:
53+
dict (containing the configuration parameters needed)
54+
"""
55+
filepath = os.path.abspath(f"{file}.yaml")
56+
with open(filepath, "r") as stream:
57+
kwargs = yaml.safe_load(stream)
58+
return kwargs

test/test_.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,47 @@
11
import numpy as np
22
import scipy.sparse as sp
33
import pytest
4-
from pyclassify.utils import eigenvalues_np, eigenvalues_sp, power_method, make_symmetric
4+
from pyclassify import (
5+
eigenvalues_np,
6+
eigenvalues_sp,
7+
power_method,
8+
power_method_numba,
9+
)
10+
from pyclassify.utils import check_A_square_matrix, make_symmetric
511

612

713
np.random.seed(42)
8-
sizes = [10, 100, 1000]
9-
densities = [0.1, 0.5]
1014

1115

16+
def test_checks_on_A():
17+
ugly_nonsquare_matrix = np.random.rand(5, 3)
18+
19+
with pytest.raises(ValueError):
20+
check_A_square_matrix(ugly_nonsquare_matrix)
21+
with pytest.raises(TypeError):
22+
check_A_square_matrix("definitely_not_a_matrix")
23+
24+
25+
sizes = [10, 100, 1000]
26+
densities = [0.1, 0.3]
27+
1228

1329
@pytest.mark.parametrize("size", sizes)
1430
@pytest.mark.parametrize("density", densities)
1531
def test_make_symmetric(size, density):
1632
matrix = sp.random(size, size, density=density, format="csr")
1733
symmetric_matrix = make_symmetric(matrix)
18-
34+
1935
assert np.allclose(symmetric_matrix.toarray(), symmetric_matrix.toarray().T)
2036
assert symmetric_matrix.shape == matrix.shape
2137

2238
with pytest.raises(TypeError):
2339
_ = make_symmetric("banana")
2440

2541

26-
2742
@pytest.mark.parametrize("size", sizes)
2843
@pytest.mark.parametrize("density", densities)
29-
def test_power_method(size, density):
44+
def test_implementations_power_method(size, density):
3045
matrix = sp.random(size, size, density=density, format="csr")
3146
matrix = make_symmetric(matrix)
3247

@@ -40,14 +55,8 @@ def test_power_method(size, density):
4055
biggest_eigenvalue_sp = eigs_sp[index_sp]
4156

4257
biggest_eigenvalue_pm = power_method(matrix)
58+
biggest_eigenvalue_pm_numba = power_method_numba(matrix.toarray())
4359

4460
assert np.isclose(biggest_eigenvalue_np, biggest_eigenvalue_sp, rtol=1e-4)
4561
assert np.isclose(biggest_eigenvalue_pm, biggest_eigenvalue_sp, rtol=1e-4)
46-
47-
ugly_nonsquare_matrix = np.random.rand(5, 3)
48-
49-
with pytest.raises(ValueError):
50-
_ = eigenvalues_np(eigenvalues_np(ugly_nonsquare_matrix, symmetric=True))
51-
52-
with pytest.raises(ValueError):
53-
_ = eigenvalues_sp(eigenvalues_sp(ugly_nonsquare_matrix, symmetric=True))
62+
assert np.isclose(biggest_eigenvalue_pm_numba, biggest_eigenvalue_sp, rtol=1e-4)

0 commit comments

Comments
 (0)