Coverage for tests/spaces/test_graph_edges.py: 100%
85 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-16 21:43 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-16 21:43 +0000
1import lab as B
2import numpy as np
3import pytest
5from geometric_kernels.jax import * # noqa
6from geometric_kernels.kernels import (
7 MaternGeometricKernel,
8 MaternHodgeCompositionalKernel,
9 MaternKarhunenLoeveKernel,
10)
11from geometric_kernels.spaces import GraphEdges
12from geometric_kernels.tensorflow import * # noqa
13from geometric_kernels.torch import * # noqa
15from ..data import (
16 TEST_GRAPH_EDGES_ADJACENCY,
17 TEST_GRAPH_EDGES_DOWN_LAPLACIAN,
18 TEST_GRAPH_EDGES_NUM_NODES,
19 TEST_GRAPH_EDGES_ORIENTED_EDGES,
20 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES,
21 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES_AUTO,
22 TEST_GRAPH_EDGES_TRIANGLES,
23 TEST_GRAPH_EDGES_UP_LAPLACIAN,
24)
25from ..helper import check_function_with_backend, create_random_state, np_to_backend
28@pytest.mark.parametrize(
29 "triangles, oriented_triangles",
30 [
31 (None, TEST_GRAPH_EDGES_ORIENTED_TRIANGLES_AUTO),
32 (TEST_GRAPH_EDGES_TRIANGLES, TEST_GRAPH_EDGES_ORIENTED_TRIANGLES),
33 ],
34)
35@pytest.mark.parametrize(
36 "backend", ["numpy", "tensorflow", "torch", "jax", "scipy_sparse"]
37)
38def test_from_adjacency(backend, triangles, oriented_triangles):
40 type_reference = create_random_state(backend)
42 # check that oriented_edges array is correctly computed from adjacency
43 B.all(
44 TEST_GRAPH_EDGES_ORIENTED_EDGES
45 == GraphEdges.from_adjacency(
46 TEST_GRAPH_EDGES_ADJACENCY, type_reference, triangles=triangles
47 ).oriented_edges
48 )
50 # check that oriented_triangles array is correctly computed from adjacency
51 B.all(
52 oriented_triangles
53 == GraphEdges.from_adjacency(
54 TEST_GRAPH_EDGES_ADJACENCY, type_reference, triangles=triangles
55 ).oriented_triangles
56 )
59@pytest.mark.parametrize("backend", ["numpy", "tensorflow", "torch", "jax"])
60def test_laplacian(backend):
62 check_function_with_backend(
63 backend,
64 TEST_GRAPH_EDGES_UP_LAPLACIAN,
65 lambda or_e, or_t: GraphEdges(
66 TEST_GRAPH_EDGES_NUM_NODES, or_e, or_t
67 )._up_laplacian,
68 TEST_GRAPH_EDGES_ORIENTED_EDGES,
69 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES,
70 )
72 check_function_with_backend(
73 backend,
74 TEST_GRAPH_EDGES_DOWN_LAPLACIAN,
75 lambda or_e, or_t: GraphEdges(
76 TEST_GRAPH_EDGES_NUM_NODES, or_e, or_t
77 )._down_laplacian,
78 TEST_GRAPH_EDGES_ORIENTED_EDGES,
79 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES,
80 )
82 check_function_with_backend(
83 backend,
84 TEST_GRAPH_EDGES_UP_LAPLACIAN + TEST_GRAPH_EDGES_DOWN_LAPLACIAN,
85 lambda or_e, or_t: GraphEdges(
86 TEST_GRAPH_EDGES_NUM_NODES, or_e, or_t
87 )._hodge_laplacian,
88 TEST_GRAPH_EDGES_ORIENTED_EDGES,
89 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES,
90 )
93@pytest.mark.parametrize(
94 "L",
95 [
96 TEST_GRAPH_EDGES_ORIENTED_EDGES.shape[0],
97 TEST_GRAPH_EDGES_ORIENTED_EDGES.shape[0] // 2,
98 ],
99)
100@pytest.mark.parametrize("backend", ["numpy", "tensorflow", "torch", "jax"])
101def test_eigendecomposition(L, backend):
102 hodge_laplacian = np_to_backend(
103 TEST_GRAPH_EDGES_UP_LAPLACIAN + TEST_GRAPH_EDGES_DOWN_LAPLACIAN,
104 backend,
105 )
107 def eigendiff(or_e, or_t):
108 graph_edges = GraphEdges(TEST_GRAPH_EDGES_NUM_NODES, or_e, or_t)
110 eigenvalue_mat = B.diag_construct(graph_edges.get_eigenvalues(L)[:, 0])
111 eigenvectors = graph_edges.get_eigenvectors(L)
113 laplace_x_eigvecs = hodge_laplacian @ eigenvectors
114 eigvals_x_eigvecs = eigenvectors @ eigenvalue_mat
115 return laplace_x_eigvecs - eigvals_x_eigvecs
117 check_function_with_backend(
118 backend,
119 np.zeros((TEST_GRAPH_EDGES_ORIENTED_EDGES.shape[0], L)),
120 eigendiff,
121 TEST_GRAPH_EDGES_ORIENTED_EDGES,
122 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES,
123 )
126@pytest.mark.parametrize(
127 "hodge_type, projection_matrix, projection_matrix_id",
128 [
129 ("harmonic", TEST_GRAPH_EDGES_UP_LAPLACIAN, "up"),
130 ("harmonic", TEST_GRAPH_EDGES_DOWN_LAPLACIAN, "down"),
131 ("gradient", TEST_GRAPH_EDGES_DOWN_LAPLACIAN, "down"),
132 ("curl", TEST_GRAPH_EDGES_UP_LAPLACIAN, "up"),
133 ],
134 ids=lambda x: x if isinstance(x, str) else "",
135)
136@pytest.mark.parametrize(
137 "L",
138 [
139 TEST_GRAPH_EDGES_ORIENTED_EDGES.shape[0],
140 TEST_GRAPH_EDGES_ORIENTED_EDGES.shape[0] // 2,
141 ],
142)
143@pytest.mark.parametrize("backend", ["numpy", "tensorflow", "torch", "jax"])
144def test_hodge_decomposition(
145 hodge_type, projection_matrix, projection_matrix_id, L, backend
146):
147 # projection_matrix_id we only use for the ids of the test
149 n = GraphEdges(
150 TEST_GRAPH_EDGES_NUM_NODES,
151 TEST_GRAPH_EDGES_ORIENTED_EDGES,
152 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES,
153 ).get_number_of_eigenpairs(L, hodge_type=hodge_type)
155 def proj(or_e, or_t, proj_mat):
156 graph_edges = GraphEdges(TEST_GRAPH_EDGES_NUM_NODES, or_e, or_t)
157 proj_mat = np_to_backend(proj_mat, backend)
159 eigenvectors = graph_edges.get_eigenvectors(L, hodge_type=hodge_type)
161 result = proj_mat @ eigenvectors
162 return result
164 # Check that the eigenvectors of type `hodge_type` lie in the null space
165 # of the projection matrix (are harmonic / divergence-free / curl-free).
166 check_function_with_backend(
167 backend,
168 np.zeros((TEST_GRAPH_EDGES_ORIENTED_EDGES.shape[0], n)),
169 proj,
170 TEST_GRAPH_EDGES_ORIENTED_EDGES,
171 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES,
172 projection_matrix,
173 )
176@pytest.mark.parametrize("nu, lengthscale", [(1.0, 1.0), (2.0, 1.0), (np.inf, 1.0)])
177@pytest.mark.parametrize("sparse_adj", [True, False])
178@pytest.mark.parametrize("hodge_compositional", [True, False])
179@pytest.mark.parametrize("backend", ["numpy", "tensorflow", "torch", "jax"])
180def test_matern_kernels(nu, lengthscale, hodge_compositional, sparse_adj, backend):
182 hodge_laplacian = TEST_GRAPH_EDGES_UP_LAPLACIAN + TEST_GRAPH_EDGES_DOWN_LAPLACIAN
183 num_edges = hodge_laplacian.shape[0]
185 evals_np, evecs_np = np.linalg.eigh(hodge_laplacian)
186 evecs_np *= np.sqrt(hodge_laplacian.shape[0])
188 type_reference = create_random_state(backend)
190 def evaluate_kernel(nu, lengthscale, xs):
191 adj = TEST_GRAPH_EDGES_ADJACENCY
192 if sparse_adj:
193 adj = np_to_backend(B.to_numpy(TEST_GRAPH_EDGES_ADJACENCY), "scipy_sparse")
194 graph_edges = GraphEdges.from_adjacency(
195 adj, type_reference, triangles=TEST_GRAPH_EDGES_TRIANGLES
196 )
198 if hodge_compositional:
199 kernel = MaternHodgeCompositionalKernel(graph_edges, num_levels=num_edges)
201 # We want MaternHodgeCompositionalKernel to coincide with
202 # MaternKarhunenLoeveKernel in this case. For this, we need to
203 # set the right coefficients for the three hodge types.
205 a = B.reshape(
206 B.sum(
207 MaternKarhunenLoeveKernel.spectrum(
208 graph_edges.get_eigenvalues(num_edges, hodge_type="harmonic"),
209 nu,
210 lengthscale,
211 0,
212 )
213 ),
214 1,
215 )
217 b = B.reshape(
218 B.sum(
219 MaternKarhunenLoeveKernel.spectrum(
220 graph_edges.get_eigenvalues(num_edges, hodge_type="gradient"),
221 nu,
222 lengthscale,
223 0,
224 )
225 ),
226 1,
227 )
229 c = B.reshape(
230 B.sum(
231 MaternKarhunenLoeveKernel.spectrum(
232 graph_edges.get_eigenvalues(num_edges, hodge_type="curl"),
233 nu,
234 lengthscale,
235 0,
236 )
237 ),
238 1,
239 )
241 params = {
242 "harmonic": {"logit": a, "nu": nu, "lengthscale": lengthscale},
243 "gradient": {"logit": b, "nu": nu, "lengthscale": lengthscale},
244 "curl": {"logit": c, "nu": nu, "lengthscale": lengthscale},
245 }
246 else:
247 kernel = MaternKarhunenLoeveKernel(graph_edges, num_levels=num_edges)
248 params = {"nu": nu, "lengthscale": lengthscale}
250 return kernel.K(params, xs)
252 if nu < np.inf:
253 K = (
254 evecs_np
255 @ np.diag(np.power(evals_np + 2 * nu / lengthscale**2, -nu))
256 @ evecs_np.T
257 )
258 else:
259 K = evecs_np @ np.diag(np.exp(-(lengthscale**2) / 2 * evals_np)) @ evecs_np.T
260 K = K / np.mean(K.diagonal())
262 # Check that the kernel matrix is correctly computed. For the Hodge
263 # compositional kernel, we only check the case when the hyperparameters
264 # make it coincide with the Karhunen-Loève kernel.
265 check_function_with_backend(
266 backend,
267 K,
268 evaluate_kernel,
269 np.array([nu]),
270 np.array([lengthscale]),
271 np.arange(1, num_edges + 1)[:, None],
272 )
275@pytest.mark.parametrize(
276 "coeffs, projection_matrix, projection_matrix_id",
277 [
278 ([1.0, 0.0, 0.0], TEST_GRAPH_EDGES_UP_LAPLACIAN, "up"),
279 ([1.0, 0.0, 0.0], TEST_GRAPH_EDGES_DOWN_LAPLACIAN, "down"),
280 ([0.0, 1.0, 0.0], TEST_GRAPH_EDGES_DOWN_LAPLACIAN, "down"),
281 ([0.0, 0.0, 1.0], TEST_GRAPH_EDGES_UP_LAPLACIAN, "up"),
282 ],
283 ids=lambda x: x if isinstance(x, str) else "",
284)
285@pytest.mark.parametrize("nu, lengthscale", [(1.0, 1.0), (2.0, 1.0), (np.inf, 1.0)])
286@pytest.mark.parametrize("backend", ["numpy", "tensorflow", "torch", "jax"])
287def test_kernels_hodge_type(
288 coeffs, projection_matrix, projection_matrix_id, nu, lengthscale, backend
289):
290 # projection_matrix_id we only use for the ids of the test
292 def proj_kernel(
293 or_e,
294 or_t,
295 coeff_harmonic,
296 coeff_gradient,
297 coeff_curl,
298 nu,
299 lengthscale,
300 xs,
301 projection_matrix,
302 ):
303 graph_edges = GraphEdges(TEST_GRAPH_EDGES_NUM_NODES, or_e, or_t)
305 kernel = MaternGeometricKernel(graph_edges)
307 params = {
308 "harmonic": {"logit": coeff_harmonic, "nu": nu, "lengthscale": lengthscale},
309 "gradient": {"logit": coeff_gradient, "nu": nu, "lengthscale": lengthscale},
310 "curl": {"logit": coeff_curl, "nu": nu, "lengthscale": lengthscale},
311 }
313 return projection_matrix @ kernel.K(params, xs)
315 num_edges = TEST_GRAPH_EDGES_ORIENTED_EDGES.shape[0]
317 # Check that the image of the Hodge compositional kernel lies in the null
318 # space of the projection matrix. Makes sure that buy setting the
319 # coefficients of the other two types to zero, the kernel is harmonic /
320 # divergence-free / curl-free.
321 check_function_with_backend(
322 backend,
323 np.zeros((num_edges, num_edges)),
324 proj_kernel,
325 TEST_GRAPH_EDGES_ORIENTED_EDGES,
326 TEST_GRAPH_EDGES_ORIENTED_TRIANGLES,
327 np.array([coeffs[0]]),
328 np.array([coeffs[1]]),
329 np.array([coeffs[2]]),
330 np.array([nu]),
331 np.array([lengthscale]),
332 np.arange(1, num_edges + 1)[:, None],
333 projection_matrix,
334 )