forked from TheAlgorithms/Python
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlanczos_eigenvectors.py
206 lines (166 loc) · 7.66 KB
/
lanczos_eigenvectors.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
"""
Lanczos Method for Finding Eigenvalues and Eigenvectors of a Graph.
This module demonstrates the Lanczos method to approximate the largest eigenvalues
and corresponding eigenvectors of a symmetric matrix represented as a graph's
adjacency list. The method efficiently handles large, sparse matrices by converting
the graph to a tridiagonal matrix, whose eigenvalues and eigenvectors are then
computed.
Key Functions:
- `find_lanczos_eigenvectors`: Computes the k largest eigenvalues and vectors.
- `lanczos_iteration`: Constructs the tridiagonal matrix and orthonormal basis vectors.
- `multiply_matrix_vector`: Multiplies an adjacency list graph with a vector.
Complexity:
- Time: O(k * n), where k is the number of eigenvalues and n is the matrix size.
- Space: O(n), due to sparse representation and tridiagonal matrix structure.
Further Reading:
- Lanczos Algorithm: https://en.wikipedia.org/wiki/Lanczos_algorithm
- Eigenvector Centrality: https://en.wikipedia.org/wiki/Eigenvector_centrality
Example Usage:
Given a graph represented by an adjacency list, the `find_lanczos_eigenvectors`
function returns the largest eigenvalues and eigenvectors. This can be used to
analyze graph centrality.
"""
import numpy as np
def validate_adjacency_list(graph: list[list[int | None]]) -> None:
"""Validates the adjacency list format for the graph.
Args:
graph: A list of lists where each sublist contains the neighbors of a node.
Raises:
ValueError: If the graph is not a list of lists, or if any node has
invalid neighbors (e.g., out-of-range or non-integer values).
>>> validate_adjacency_list([[1, 2], [0], [0, 1]])
>>> validate_adjacency_list([[]]) # No neighbors, valid case
>>> validate_adjacency_list([[1], [2], [-1]]) # Invalid neighbor
Traceback (most recent call last):
...
ValueError: Invalid neighbor -1 in node 2 adjacency list.
"""
if not isinstance(graph, list):
raise ValueError("Graph should be a list of lists.")
for node_index, neighbors in enumerate(graph):
if not isinstance(neighbors, list):
no_neighbors_message: str = (
f"Node {node_index} should have a list of neighbors."
)
raise ValueError(no_neighbors_message)
for neighbor_index in neighbors:
if (
not isinstance(neighbor_index, int)
or neighbor_index < 0
or neighbor_index >= len(graph)
):
invalid_neighbor_message: str = (
f"Invalid neighbor {neighbor_index} in node {node_index} "
f"adjacency list."
)
raise ValueError(invalid_neighbor_message)
def lanczos_iteration(
graph: list[list[int | None]], num_eigenvectors: int
) -> tuple[np.ndarray, np.ndarray]:
"""Constructs the tridiagonal matrix and orthonormal basis vectors using the
Lanczos method.
Args:
graph: The graph represented as a list of adjacency lists.
num_eigenvectors: The number of largest eigenvalues and eigenvectors
to approximate.
Returns:
A tuple containing:
- tridiagonal_matrix: A (num_eigenvectors x num_eigenvectors) symmetric
matrix.
- orthonormal_basis: A (num_nodes x num_eigenvectors) matrix of orthonormal
basis vectors.
Raises:
ValueError: If num_eigenvectors is less than 1 or greater than the number of
nodes.
>>> graph = [[1, 2], [0, 2], [0, 1]]
>>> T, Q = lanczos_iteration(graph, 2)
>>> T.shape == (2, 2) and Q.shape == (3, 2)
True
"""
num_nodes: int = len(graph)
if not (1 <= num_eigenvectors <= num_nodes):
raise ValueError(
"Number of eigenvectors must be between 1 and the number of "
"nodes in the graph."
)
orthonormal_basis: np.ndarray = np.zeros((num_nodes, num_eigenvectors))
tridiagonal_matrix: np.ndarray = np.zeros((num_eigenvectors, num_eigenvectors))
rng = np.random.default_rng()
initial_vector: np.ndarray = rng.random(num_nodes)
initial_vector /= np.sqrt(np.dot(initial_vector, initial_vector))
orthonormal_basis[:, 0] = initial_vector
prev_beta: float = 0.0
for iter_index in range(num_eigenvectors):
result_vector: np.ndarray = multiply_matrix_vector(
graph, orthonormal_basis[:, iter_index]
)
if iter_index > 0:
result_vector -= prev_beta * orthonormal_basis[:, iter_index - 1]
alpha_value: float = np.dot(orthonormal_basis[:, iter_index], result_vector)
result_vector -= alpha_value * orthonormal_basis[:, iter_index]
prev_beta = np.sqrt(np.dot(result_vector, result_vector))
if iter_index < num_eigenvectors - 1 and prev_beta > 1e-10:
orthonormal_basis[:, iter_index + 1] = result_vector / prev_beta
tridiagonal_matrix[iter_index, iter_index] = alpha_value
if iter_index < num_eigenvectors - 1:
tridiagonal_matrix[iter_index, iter_index + 1] = prev_beta
tridiagonal_matrix[iter_index + 1, iter_index] = prev_beta
return tridiagonal_matrix, orthonormal_basis
def multiply_matrix_vector(
graph: list[list[int | None]], vector: np.ndarray
) -> np.ndarray:
"""Performs multiplication of a graph's adjacency list representation with a vector.
Args:
graph: The adjacency list of the graph.
vector: A 1D numpy array representing the vector to multiply.
Returns:
A numpy array representing the product of the adjacency list and the vector.
Raises:
ValueError: If the vector's length does not match the number of nodes in the
graph.
>>> multiply_matrix_vector([[1, 2], [0, 2], [0, 1]], np.array([1, 1, 1]))
array([2., 2., 2.])
>>> multiply_matrix_vector([[1, 2], [0, 2], [0, 1]], np.array([0, 1, 0]))
array([1., 0., 1.])
"""
num_nodes: int = len(graph)
if vector.shape[0] != num_nodes:
raise ValueError("Vector length must match the number of nodes in the graph.")
result: np.ndarray = np.zeros(num_nodes)
for node_index, neighbors in enumerate(graph):
for neighbor_index in neighbors:
result[node_index] += vector[neighbor_index]
return result
def find_lanczos_eigenvectors(
graph: list[list[int | None]], num_eigenvectors: int
) -> tuple[np.ndarray, np.ndarray]:
"""Computes the largest eigenvalues and their corresponding eigenvectors using the
Lanczos method.
Args:
graph: The graph as a list of adjacency lists.
num_eigenvectors: Number of largest eigenvalues and eigenvectors to compute.
Returns:
A tuple containing:
- eigenvalues: 1D array of the largest eigenvalues in descending order.
- eigenvectors: 2D array where each column is an eigenvector corresponding
to an eigenvalue.
Raises:
ValueError: If the graph format is invalid or num_eigenvectors is out of bounds.
>>> eigenvalues, eigenvectors = find_lanczos_eigenvectors(
... [[1, 2], [0, 2], [0, 1]], 2
... )
>>> len(eigenvalues) == 2 and eigenvectors.shape[1] == 2
True
"""
validate_adjacency_list(graph)
tridiagonal_matrix, orthonormal_basis = lanczos_iteration(graph, num_eigenvectors)
eigenvalues, eigenvectors = np.linalg.eigh(tridiagonal_matrix)
return eigenvalues[::-1], np.dot(orthonormal_basis, eigenvectors[:, ::-1])
def main() -> None:
"""
Main driver function for testing the implementation with doctests.
"""
import doctest
doctest.testmod()
if __name__ == "__main__":
main()