Coverage for geometric_kernels/spaces/circle.py: 99%

71 statements  

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

1""" 

2This module provides the :class:`Circle` space and the respective 

3:class:`~.eigenfunctions.Eigenfunctions` subclass :class:`SinCosEigenfunctions`. 

4""" 

5 

6import lab as B 

7from beartype.typing import List, Optional 

8 

9from geometric_kernels.lab_extras import dtype_double, from_numpy 

10from geometric_kernels.spaces.base import DiscreteSpectrumSpace 

11from geometric_kernels.spaces.eigenfunctions import ( 

12 Eigenfunctions, 

13 EigenfunctionsWithAdditionTheorem, 

14) 

15from geometric_kernels.utils.utils import chain 

16 

17 

18class SinCosEigenfunctions(EigenfunctionsWithAdditionTheorem): 

19 """ 

20 Eigenfunctions of the Laplace-Beltrami operator on the circle correspond 

21 to the Fourier basis, i.e. sines and cosines. 

22 

23 Levels are the whole eigenspaces. The zeroth eigenspace is 

24 one-dimensional, all the other eigenspaces are of dimension 2. 

25 

26 :param num_levels: 

27 The number of levels. 

28 """ 

29 

30 def __init__(self, num_levels: int): 

31 if num_levels < 1: 

32 raise ValueError("`num_levels` must be a positive integer.") 

33 

34 self._num_eigenfunctions = num_levels * 2 - 1 

35 self._num_levels = num_levels 

36 

37 def __call__(self, X: B.Numeric, **kwargs) -> B.Numeric: 

38 N = B.shape(X)[0] 

39 theta = X 

40 const = 2.0**0.5 

41 values = [] 

42 for level in range(self.num_levels): 

43 if level == 0: 

44 values.append(B.ones(B.dtype(X), N, 1)) 

45 else: 

46 freq = 1.0 * level 

47 values.append(const * B.cos(freq * theta)) 

48 values.append(const * B.sin(freq * theta)) 

49 

50 return B.concat(*values, axis=1)[:, : self._num_eigenfunctions] # [N, M] 

51 

52 def _addition_theorem( 

53 self, X: B.Numeric, X2: Optional[B.Numeric] = None, **kwargs 

54 ) -> B.Numeric: 

55 r""" 

56 Returns the result of applying the addition theorem to sum over all 

57 the outer products of eigenfunctions within a level, for each level. 

58 

59 .. math:: \sin(l \theta_1) \sin(l \theta_2) + \cos(l \theta_1) \cos(l \theta_2 = N_l \cos(l (\theta_1 - \theta_2)), 

60 

61 where $N_l = 1$ for $l = 0$, else $N_l = 2$. 

62 

63 :param X: 

64 The first of the two batches of points to evaluate the phi 

65 product at. An array of shape [N, <axis>], where N is the 

66 number of points and <axis> is the shape of the arrays that 

67 represent the points in a given space. 

68 :param X2: 

69 The second of the two batches of points to evaluate the phi 

70 product at. An array of shape [N2, <axis>], where N2 is the 

71 number of points and <axis> is the shape of the arrays that 

72 represent the points in a given space. 

73 

74 Defaults to None, in which case X is used for X2. 

75 :param ``**kwargs``: 

76 Any additional parameters. 

77 

78 :return: 

79 An array of shape [N, N2, L]. 

80 """ 

81 theta1, theta2 = X, X2 

82 angle_between = theta1[:, None, :] - theta2[None, :, :] # [N, N2, 1] 

83 freqs = B.range(B.dtype(X), self.num_levels) # [L] 

84 values = B.cos(freqs[None, None, :] * angle_between) # [N, N2, L] 

85 values = ( 

86 B.cast( 

87 B.dtype(X), 

88 from_numpy(X, self.num_eigenfunctions_per_level)[None, None, :], 

89 ) 

90 * values 

91 ) 

92 return values # [N, N2, L] 

93 

94 def _addition_theorem_diag(self, X: B.Numeric, **kwargs) -> B.Numeric: 

95 """ 

96 :return: 

97 Array `result`, such that result[n, l] = 1 if l = 0 or 2 otherwise. 

98 """ 

99 N = X.shape[0] 

100 ones = B.ones(B.dtype(X), N, self.num_levels) # [N, L] 

101 value = ones * B.cast( 

102 B.dtype(X), from_numpy(X, self.num_eigenfunctions_per_level)[None, :] 

103 ) 

104 return value # [N, L] 

105 

106 @property 

107 def num_eigenfunctions(self) -> int: 

108 return self._num_eigenfunctions 

109 

110 @property 

111 def num_levels(self) -> int: 

112 return self._num_levels 

113 

114 @property 

115 def num_eigenfunctions_per_level(self) -> List[int]: 

116 """ 

117 The number of eigenfunctions per level. 

118 

119 :return: 

120 List `result`, such that result[l] = 1 if l = 0 or 2 otherwise. 

121 """ 

122 return [1 if level == 0 else 2 for level in range(self.num_levels)] 

123 

124 

125class Circle(DiscreteSpectrumSpace): 

126 r""" 

127 The GeometricKernels space representing the standard unit circle, denoted 

128 by $\mathbb{S}_1$ (as the one-dimensional hypersphere) or $\mathbb{T}$ (as 

129 the one-dimensional torus). 

130 

131 The elements of this space are represented by angles, 

132 scalars from $0$ to $2 \pi$. 

133 

134 Levels are the whole eigenspaces. The zeroth eigenspace is 

135 one-dimensional, all the other eigenspaces are of dimension 2. 

136 

137 .. note:: 

138 The :doc:`example notebook on the torus </examples/Torus>` involves 

139 this space. 

140 

141 .. admonition:: Citation 

142 

143 If you use this GeometricKernels space in your research, please consider 

144 citing :cite:t:`borovitskiy2020`. 

145 """ 

146 

147 def __str__(self): 

148 return "Circle()" 

149 

150 @property 

151 def dimension(self) -> int: 

152 """ 

153 :return: 

154 1. 

155 """ 

156 return 1 

157 

158 def get_eigenfunctions(self, num: int) -> Eigenfunctions: 

159 """ 

160 Returns the :class:`~.SinCosEigenfunctions` object with `num` levels. 

161 

162 :param num: 

163 Number of levels. 

164 """ 

165 return SinCosEigenfunctions(num) 

166 

167 def get_eigenvalues(self, num: int) -> B.Numeric: 

168 eigenvalues = B.range(num) ** 2 # [num,] 

169 return B.reshape(eigenvalues, -1, 1) # [num, 1] 

170 

171 def get_repeated_eigenvalues(self, num: int) -> B.Numeric: 

172 eigenfunctions = self.get_eigenfunctions(num) 

173 eigenvalues_per_level = B.range(num) ** 2 # [num,] 

174 eigenvalues = chain( 

175 eigenvalues_per_level, 

176 eigenfunctions.num_eigenfunctions_per_level, 

177 ) # [M,] 

178 return B.reshape(eigenvalues, -1, 1) # [M, 1] 

179 

180 def random(self, key: B.RandomState, number: int): 

181 key, random_points = B.random.rand(key, dtype_double(key), number, 1) # [N, 1] 

182 random_points *= 2 * B.pi 

183 return key, random_points 

184 

185 @property 

186 def element_shape(self): 

187 """ 

188 :return: 

189 [1]. 

190 """ 

191 return [1] 

192 

193 @property 

194 def element_dtype(self): 

195 """ 

196 :return: 

197 B.Float. 

198 """ 

199 return B.Float