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
« 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"""
6import lab as B
7from beartype.typing import List, Optional
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
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.
23 Levels are the whole eigenspaces. The zeroth eigenspace is
24 one-dimensional, all the other eigenspaces are of dimension 2.
26 :param num_levels:
27 The number of levels.
28 """
30 def __init__(self, num_levels: int):
31 if num_levels < 1:
32 raise ValueError("`num_levels` must be a positive integer.")
34 self._num_eigenfunctions = num_levels * 2 - 1
35 self._num_levels = num_levels
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))
50 return B.concat(*values, axis=1)[:, : self._num_eigenfunctions] # [N, M]
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.
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)),
61 where $N_l = 1$ for $l = 0$, else $N_l = 2$.
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.
74 Defaults to None, in which case X is used for X2.
75 :param ``**kwargs``:
76 Any additional parameters.
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]
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]
106 @property
107 def num_eigenfunctions(self) -> int:
108 return self._num_eigenfunctions
110 @property
111 def num_levels(self) -> int:
112 return self._num_levels
114 @property
115 def num_eigenfunctions_per_level(self) -> List[int]:
116 """
117 The number of eigenfunctions per level.
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)]
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).
131 The elements of this space are represented by angles,
132 scalars from $0$ to $2 \pi$.
134 Levels are the whole eigenspaces. The zeroth eigenspace is
135 one-dimensional, all the other eigenspaces are of dimension 2.
137 .. note::
138 The :doc:`example notebook on the torus </examples/Torus>` involves
139 this space.
141 .. admonition:: Citation
143 If you use this GeometricKernels space in your research, please consider
144 citing :cite:t:`borovitskiy2020`.
145 """
147 def __str__(self):
148 return "Circle()"
150 @property
151 def dimension(self) -> int:
152 """
153 :return:
154 1.
155 """
156 return 1
158 def get_eigenfunctions(self, num: int) -> Eigenfunctions:
159 """
160 Returns the :class:`~.SinCosEigenfunctions` object with `num` levels.
162 :param num:
163 Number of levels.
164 """
165 return SinCosEigenfunctions(num)
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]
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]
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
185 @property
186 def element_shape(self):
187 """
188 :return:
189 [1].
190 """
191 return [1]
193 @property
194 def element_dtype(self):
195 """
196 :return:
197 B.Float.
198 """
199 return B.Float