Coverage for geometric_kernels/utils/kernel_formulas/hyperbolic.py: 89%

57 statements  

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

1""" 

2Implements alternative formulas for the heat kernel on the hyperbolic manifold. 

3 

4More specifically, the function :func:`hyperbolic_heat_kernel_odd` implements 

5analytic formulas for the heat kernel on odd-dimensional hyperbolic spaces. The 

6function :func:`hyperbolic_heat_kernel_even` implements a semi-analytic formula 

7for the heat kernel on even-dimensional hyperbolic spaces. 

8 

9The implementation is provided mainly for testing purposes. Hypothetically, the 

10odd-dimensional formula could be used in practice, but the even-dimensional one 

11is not recommended due to its inefficiency and numerical instability. 

12""" 

13 

14import lab as B 

15import numpy as np 

16import scipy 

17from beartype.typing import Optional 

18 

19from geometric_kernels.lab_extras import cosh, sinh 

20from geometric_kernels.utils.manifold_utils import hyperbolic_distance 

21 

22 

23def hyperbolic_heat_kernel_odd( 

24 dim: int, 

25 t: float, 

26 X: B.Numeric, 

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

28) -> B.Numeric: 

29 """ 

30 The analytic formula for the heat kernel on the hyperbolic space of odd 

31 dimension, normalized to have k(x, x) = 1 for all x. 

32 

33 :param t: 

34 The time parameter, a positive float. 

35 :param X: 

36 A batch of inputs, an array of shape [N, dim+1]. 

37 :param X2: 

38 A batch of inputs, an array of shape [N2, dim+1]. If None, defaults to X. 

39 

40 :return: 

41 The kernel matrix, an array of shape [N, N2]. The kernel is normalized, 

42 i.e. k(x, x) = 1 for all x. 

43 """ 

44 

45 if dim % 2 == 0: 

46 raise ValueError( 

47 "This function is only defined for odd-dimensional hyperbolic spaces. For even-dimensional spaces, use `hyperbolic_heat_kernel_even`." 

48 ) 

49 

50 if X2 is None: 

51 X2 = X 

52 

53 dists = hyperbolic_distance(X, X2) 

54 dists_are_small = dists < 0.1 

55 

56 if dim == 3: 

57 # This formula is taken from :cite:t:`azangulov2024b`, Equation (42). 

58 analytic_expr = dists / sinh(dists) * B.exp(-(dists**2) / (4 * t)) 

59 

60 # To get the Taylor expansion below, which gives a stable way to compute 

61 # the kernel for small distances, use the following Mathematica code: 

62 # > Subscript[F, 3][r_, t_] := r/Sinh[r]*Exp[-r^2/(4*t)] 

63 # > Series[Subscript[F, 3][r, t],{r, 0, 5}] 

64 taylor = 1 - (1 / 6 + 1 / (4 * t)) * dists**2 

65 

66 return B.where(dists_are_small, taylor, analytic_expr) 

67 elif dim == 5: 

68 # The following expression follows from the recursive formula in Equation 

69 # (43) of :cite:t:`azangulov2024b`. In order to get the form below, you 

70 # can continue the Mathematica code above with the following: 

71 # > Subscript[U, 5][r_,t_] := -1/Sinh[r]*(D[Subscript[F, 3][rr,t ], rr] /. rr -> r) 

72 # > Subscript[C, 5][t_] := Limit[Subscript[U, 5][r, t], r -> 0] 

73 # > Subscript[F, 5][r_, t_] := Subscript[U, 5][r, t]/Subscript[C, 5][t] 

74 # > Simplify[Subscript[F, 5][r, t]] 

75 a = 3 * B.exp(-(dists**2) / (4 * t)) / (3 + 2 * t) 

76 b = dists**2 - 2 * t + 2 * dists * t * cosh(dists) / sinh(dists) 

77 c = 1.0 / sinh(dists) ** 2 

78 analytic_expr = a * b * c 

79 

80 # To get the Taylor expansion below, which gives a stable way to compute 

81 # the kernel for small distances, use the following Mathematica code: 

82 # > Series[Subscript[F, 5][r, t], {r, 0, 5}] 

83 taylor = 1 - (15 - 30 * t - 16 * t**2) / (20 * t * (3 + 2 * t)) * dists**2 

84 

85 return B.where(dists_are_small, taylor, analytic_expr) 

86 elif dim == 7: 

87 # The following expression follows from the recursive formula in Equation 

88 # (43) of :cite:t:`azangulov2024b`. In order to get the form below, you 

89 # can continue the Mathematica code above with the following: 

90 # > Subscript[U, 7][r_, t_] := -1/Sinh[r]*(D[Subscript[F, 5][rr, t ], rr] /. rr -> r) 

91 # > Subscript[C, 7][t_]:=Limit[Subscript[U, 7][r, t],r->0] 

92 # > Subscript[F, 7][r_, t_] := Subscript[U, 7][r, t]/Subscript[C, 7][t] 

93 # > TrigFactor[Subscript[F, 7][r,t]] 

94 a = 15 * B.exp(-(dists**2) / (4 * t)) / (2 * (15 + 30 * t + 16 * t**2)) 

95 b1 = -12 * t**2 * sinh(2 * dists) 

96 b2 = (6 * t + 16 * t**2 + (8 * t**2 - 6 * t) * cosh(2 * dists)) * dists 

97 b3 = 6 * t * sinh(2 * dists) * dists**2 

98 b4 = (cosh(2 * dists) - 1) * dists**3 

99 b = b1 + b2 + b3 + b4 

100 analytic_expr = a * b / sinh(dists) ** 5 

101 

102 # To get the Taylor expansion below, which gives a stable way to compute 

103 # the kernel for small distances, use the following Mathematica code: 

104 # > Series[Subscript[F, 7][r, t],{r, 0, 5}] 

105 taylor = ( 

106 1 

107 - 3 

108 * (35 + 140 * t + 196 * t**2 + 96 * t**3) 

109 / (28 * t * (15 + 30 * t + 16 * t**2)) 

110 * dists**2 

111 ) 

112 

113 return B.where(dists_are_small, taylor, analytic_expr) 

114 else: 

115 raise NotImplementedError( 

116 f"Odd-dimensional hyperbolic space of dimension {dim} is not supported." 

117 ) 

118 

119 

120def _hyperbolic_heat_kernel_2d_unnormalized(t: float, rho: float) -> float: 

121 def integrand(t: float, s: float, rho: float) -> float: 

122 result = (s + rho) * np.exp(-((s + rho) ** 2) / (4 * t)) 

123 result /= np.sqrt((np.cosh(s + rho) - np.cosh(rho))) 

124 return result 

125 

126 integral, error = scipy.integrate.quad(lambda s: integrand(t, s, rho), 0, np.inf) 

127 

128 return integral 

129 

130 

131def hyperbolic_heat_kernel_even( 

132 dim: int, 

133 t: float, 

134 X: B.NPNumeric, 

135 X2: Optional[B.NPNumeric] = None, 

136) -> B.NPNumeric: 

137 

138 if dim % 2 != 0: 

139 raise ValueError( 

140 "This function is only defined for even-dimensional hyperbolic spaces. For odd-dimensional spaces, use `hyperbolic_heat_kernel_odd`." 

141 ) 

142 elif dim != 2: 

143 # The integrand in higher dimensions may be obtained from the 

144 # recursive formula in Equation (43) of :cite:t:`azangulov2024b`. 

145 # 

146 # For example, to get the integrand in dimension 4, you can use the 

147 # following Mathematica code: 

148 # > Subscript[F, 2][r_,t_,s_]:=((r+s)*Exp[-(r+s)^2/(4*t)])/(Cosh[r+s]-Cosh[r])^(1/2) 

149 # > Subscript[F, 4][r_,t_,s_]:=-(1/Sinh[r])*(D[Subscript[F, 2][rr,t,s ], rr] /. rr -> r) 

150 # > Simplify[Subscript[F, 4][r, t, s]] 

151 # 

152 # However, in higher dimensions, these integrands become a huge pain 

153 # to work with. For example, the integrand in dimension 4 looks like 

154 # 

155 # | *** 

156 # | * ** 

157 # | * *** 

158 # | * *** 

159 # | * **** 

160 # -|- * ------------------------- 

161 # | * 

162 # | * 

163 # | * 

164 # 

165 # This function is very hard to numerically integrate because 

166 # * it has a singularity at s=0 (it explodes to -inf), 

167 # * it changes sign, 

168 # * the domain of integration is infinite, and for large s you get 0*inf 

169 # situations all the time (=> you have to use an asymptotic for s->inf), 

170 # * finally, it also behaves very poorly for small rho (=> you have to 

171 # use another asymptotic for rho->0). 

172 # 

173 # This is why we don't provide the integrand in higher dimensions. 

174 # 

175 # To whoever wants to implement it in higher dimensions in the future: 

176 # Don't. It's not worth it. The Fourier feature approximation is already 

177 # very accurate and, crucially, it is numerically stable. Furthermore, 

178 # any implementation of the integrand in higher dimensions will be very 

179 # hacky, thus diminishing its value for testing purposes. 

180 raise NotImplementedError( 

181 f"Even-dimensional hyperbolic space of dimension {dim} is not supported. See the comments in the code for more details on why." 

182 ) 

183 

184 if X2 is None: 

185 X2 = X 

186 

187 normalization = _hyperbolic_heat_kernel_2d_unnormalized(t, 0) 

188 

189 result = np.zeros((X.shape[0], X2.shape[0])) 

190 

191 # This is a very inefficient implementation, but it will do for tests. 

192 for i, x in enumerate(X): 

193 for j, x2 in enumerate(X2): 

194 rho = hyperbolic_distance(x, x2).squeeze() 

195 cur_result = _hyperbolic_heat_kernel_2d_unnormalized(t, rho) 

196 result[i, j] = cur_result / normalization 

197 

198 return result