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

1import lab as B 

2import numpy as np 

3import pytest 

4 

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 

14 

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 

26 

27 

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): 

39 

40 type_reference = create_random_state(backend) 

41 

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 ) 

49 

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 ) 

57 

58 

59@pytest.mark.parametrize("backend", ["numpy", "tensorflow", "torch", "jax"]) 

60def test_laplacian(backend): 

61 

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 ) 

71 

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 ) 

81 

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 ) 

91 

92 

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 ) 

106 

107 def eigendiff(or_e, or_t): 

108 graph_edges = GraphEdges(TEST_GRAPH_EDGES_NUM_NODES, or_e, or_t) 

109 

110 eigenvalue_mat = B.diag_construct(graph_edges.get_eigenvalues(L)[:, 0]) 

111 eigenvectors = graph_edges.get_eigenvectors(L) 

112 

113 laplace_x_eigvecs = hodge_laplacian @ eigenvectors 

114 eigvals_x_eigvecs = eigenvectors @ eigenvalue_mat 

115 return laplace_x_eigvecs - eigvals_x_eigvecs 

116 

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 ) 

124 

125 

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 

148 

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) 

154 

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) 

158 

159 eigenvectors = graph_edges.get_eigenvectors(L, hodge_type=hodge_type) 

160 

161 result = proj_mat @ eigenvectors 

162 return result 

163 

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 ) 

174 

175 

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): 

181 

182 hodge_laplacian = TEST_GRAPH_EDGES_UP_LAPLACIAN + TEST_GRAPH_EDGES_DOWN_LAPLACIAN 

183 num_edges = hodge_laplacian.shape[0] 

184 

185 evals_np, evecs_np = np.linalg.eigh(hodge_laplacian) 

186 evecs_np *= np.sqrt(hodge_laplacian.shape[0]) 

187 

188 type_reference = create_random_state(backend) 

189 

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 ) 

197 

198 if hodge_compositional: 

199 kernel = MaternHodgeCompositionalKernel(graph_edges, num_levels=num_edges) 

200 

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. 

204 

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 ) 

216 

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 ) 

228 

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 ) 

240 

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} 

249 

250 return kernel.K(params, xs) 

251 

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()) 

261 

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 ) 

273 

274 

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 

291 

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) 

304 

305 kernel = MaternGeometricKernel(graph_edges) 

306 

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 } 

312 

313 return projection_matrix @ kernel.K(params, xs) 

314 

315 num_edges = TEST_GRAPH_EDGES_ORIENTED_EDGES.shape[0] 

316 

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 )