Coverage for geometric_kernels/kernels/hodge_compositional.py: 84%

43 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-16 21:43 +0000

1""" 

2This module provides the :class:`~.kernels.MaternHodgeCompositionalKernel` 

3kernel, for discrete spectrum spaces with Hodge decomposition, subclasses 

4of :class:`~.spaces.HodgeDiscreteSpectrumSpace`. 

5""" 

6 

7import lab as B 

8import numpy as np 

9from beartype.typing import Dict, Optional 

10 

11from geometric_kernels.kernels.base import BaseGeometricKernel 

12from geometric_kernels.kernels.karhunen_loeve import MaternKarhunenLoeveKernel 

13from geometric_kernels.spaces import HodgeDiscreteSpectrumSpace 

14from geometric_kernels.utils.utils import _check_1_vector, _check_field_in_params 

15 

16 

17class MaternHodgeCompositionalKernel(BaseGeometricKernel): 

18 r""" 

19 This class is similar to :class:`~.kernels.MaternKarhunenLoeveKernel`, but 

20 provides a more expressive family of kernels on the spaces where Hodge 

21 decomposition is available. 

22 

23 The resulting kernel is a sum of three kernels, 

24 

25 .. math:: k(x, x') = a k_{\text{harmonic}}(x, x') + b k_{\text{gradient}}(x, x') + c k_{\text{curl}}(x, x'), 

26 

27 where $a, b, c$ are weights $a, b, c \geq 0$ and $a + b + c = 1$, and 

28 $k_{\text{harmonic}}$, $k_{\text{gradient}}$, $k_{\text{curl}}$ are the 

29 instances of :class:`~.kernels.MaternKarhunenLoeveKernel` that only use the 

30 eigenpairs of the Laplacian corresponding to a single part of the Hodge 

31 decomposition. 

32 

33 The parameters of this kernel are represented by a dict with three keys: 

34 `"harmonic"`, `"gradient"`, `"curl"`, each corresponding to a dict with 

35 keys `"logit"`, `"nu"`, `"lengthscale"`, where `"nu"` and `"lengthscale"` 

36 are the parameters of the respective :class:`~.kernels.MaternKarhunenLoeveKernel`, 

37 while the `"logit"` parameters determine the weights $a, b, c$ in the 

38 formula above: $a, b, c$ are the `"logit"` parameters normalized to 

39 satisfy $a + b + c = 1$. 

40 

41 Same as for :class:`~.kernels.MaternKarhunenLoeveKernel`, these kernels can sometimes 

42 be computed more efficiently using addition theorems. 

43 

44 .. note:: 

45 A brief introduction into the theory behind 

46 :class:`~.kernels.MaternHodgeCompositionalKernel` can be found in 

47 :doc:`this </theory/graphs>` documentation page. 

48 

49 :param space: 

50 The space to define the kernel upon, a subclass of :class:`~.spaces.HodgeDiscreteSpectrumSpace`. 

51 

52 :param num_levels: 

53 Number of levels to include in the summation. 

54 

55 :param normalize: 

56 Whether to normalize kernel to have unit average variance. 

57 """ 

58 

59 def __init__( 

60 self, 

61 space: HodgeDiscreteSpectrumSpace, 

62 num_levels: int, 

63 normalize: bool = True, 

64 ): 

65 super().__init__(space) 

66 self.num_levels = num_levels # in code referred to as `L`. 

67 for hodge_type in ["harmonic", "curl", "gradient"]: 

68 eigenvalues = self.space.get_eigenvalues(self.num_levels, hodge_type) 

69 eigenfunctions = self.space.get_eigenfunctions(self.num_levels, hodge_type) 

70 num_levels_per_type = len(eigenvalues) 

71 setattr( 

72 self, 

73 f"kernel_{hodge_type}", 

74 MaternKarhunenLoeveKernel( 

75 space, 

76 num_levels_per_type, 

77 normalize, 

78 eigenvalues_laplacian=eigenvalues, 

79 eigenfunctions=eigenfunctions, 

80 ), 

81 ) 

82 

83 self.kernel_harmonic: MaternKarhunenLoeveKernel # for mypy to know the type 

84 self.kernel_gradient: MaternKarhunenLoeveKernel # for mypy to know the type 

85 self.kernel_curl: MaternKarhunenLoeveKernel # for mypy to know the type 

86 

87 self.normalize = normalize 

88 

89 @property 

90 def space(self) -> HodgeDiscreteSpectrumSpace: 

91 """ 

92 The space on which the kernel is defined. 

93 """ 

94 self._space: HodgeDiscreteSpectrumSpace 

95 return self._space 

96 

97 def init_params(self) -> Dict[str, Dict[str, B.NPNumeric]]: 

98 """ 

99 Initialize the three sets of parameters for the three kernels. 

100 """ 

101 params = dict( 

102 harmonic=dict( 

103 logit=np.array([1.0]), 

104 nu=np.array([np.inf]), 

105 lengthscale=np.array([1.0]), 

106 ), 

107 gradient=dict( 

108 logit=np.array([1.0]), 

109 nu=np.array([np.inf]), 

110 lengthscale=np.array([1.0]), 

111 ), 

112 curl=dict( 

113 logit=np.array([1.0]), 

114 nu=np.array([np.inf]), 

115 lengthscale=np.array([1.0]), 

116 ), 

117 ) 

118 

119 return params 

120 

121 def K( 

122 self, 

123 params: Dict[str, Dict[str, B.NPNumeric]], 

124 X: B.Numeric, 

125 X2: Optional[B.Numeric] = None, 

126 **kwargs, 

127 ) -> B.Numeric: 

128 """ 

129 Compute the cross-covariance matrix between two batches of vectors of 

130 inputs, or batches of matrices of inputs, depending on the space. 

131 """ 

132 

133 for key in ("harmonic", "gradient", "curl"): 

134 _check_field_in_params(params, key) 

135 _check_1_vector(params[key]["logit"], f'params["{key}"]["logit"]') 

136 

137 # Copy the parameters to avoid modifying the original dict. 

138 params = {key: params[key].copy() for key in ["harmonic", "gradient", "curl"]} 

139 

140 coeffs = B.stack( 

141 *[params[key].pop("logit") for key in ["harmonic", "gradient", "curl"]], 

142 axis=0, 

143 ) 

144 coeffs = coeffs / B.sum(coeffs) 

145 

146 return ( 

147 coeffs[0] * self.kernel_harmonic.K(params["harmonic"], X, X2, **kwargs) 

148 + coeffs[1] * self.kernel_gradient.K(params["gradient"], X, X2, **kwargs) 

149 + coeffs[2] * self.kernel_curl.K(params["curl"], X, X2, **kwargs) 

150 ) 

151 

152 def K_diag( 

153 self, params: Dict[str, Dict[str, B.NPNumeric]], X: B.Numeric, **kwargs 

154 ) -> B.Numeric: 

155 """ 

156 Returns the diagonal of the covariance matrix `self.K(params, X, X)`, 

157 typically in a more efficient way than actually computing the full 

158 covariance matrix with `self.K(params, X, X)` and then extracting its 

159 diagonal. 

160 """ 

161 

162 for key in ("harmonic", "gradient", "curl"): 

163 _check_field_in_params(params, key) 

164 _check_1_vector(params[key]["logit"], f'params["{key}"]["logit"]') 

165 

166 # Copy the parameters to avoid modifying the original dict. 

167 params = {key: params[key].copy() for key in ["harmonic", "gradient", "curl"]} 

168 

169 coeffs = B.stack( 

170 *[params[key].pop("logit") for key in ["harmonic", "gradient", "curl"]], 

171 axis=0, 

172 ) 

173 coeffs = coeffs / B.sum(coeffs) 

174 

175 return ( 

176 coeffs[0] * self.kernel_harmonic.K_diag(params["harmonic"], X, **kwargs) 

177 + coeffs[1] * self.kernel_gradient.K_diag(params["gradient"], X, **kwargs) 

178 + coeffs[2] * self.kernel_curl.K_diag(params["curl"], X, **kwargs) 

179 )