pygeoinf 1.0.8__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygeoinf/__init__.py +5 -1
- pygeoinf/direct_sum.py +113 -85
- pygeoinf/forward_problem.py +33 -32
- pygeoinf/gaussian_measure.py +97 -71
- pygeoinf/hilbert_space.py +517 -241
- pygeoinf/inversion.py +16 -4
- pygeoinf/linear_bayesian.py +57 -36
- pygeoinf/linear_forms.py +169 -0
- pygeoinf/linear_optimisation.py +34 -23
- pygeoinf/linear_solvers.py +74 -247
- pygeoinf/operators.py +175 -36
- pygeoinf/random_matrix.py +36 -32
- pygeoinf/symmetric_space/circle.py +347 -202
- pygeoinf/symmetric_space/sphere.py +335 -435
- pygeoinf/symmetric_space/symmetric_space.py +330 -142
- {pygeoinf-1.0.8.dist-info → pygeoinf-1.1.0.dist-info}/METADATA +1 -1
- pygeoinf-1.1.0.dist-info/RECORD +20 -0
- pygeoinf/forms.py +0 -168
- pygeoinf/symmetric_space/line.py +0 -384
- pygeoinf-1.0.8.dist-info/RECORD +0 -21
- {pygeoinf-1.0.8.dist-info → pygeoinf-1.1.0.dist-info}/LICENSE +0 -0
- {pygeoinf-1.0.8.dist-info → pygeoinf-1.1.0.dist-info}/WHEEL +0 -0
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Provides concrete implementations of function spaces on the circle (S¹).
|
|
3
|
+
|
|
4
|
+
This module uses the abstract framework from the symmetric space module to create
|
|
5
|
+
fully-featured `Lebesgue` (L²) and `Sobolev` (Hˢ) Hilbert spaces for functions
|
|
6
|
+
defined on a circle.
|
|
7
|
+
|
|
8
|
+
The core representation for a function is a truncated real Fourier series,
|
|
9
|
+
and the module provides efficient methods to transform between the spatial domain
|
|
10
|
+
(function values on a grid) and the frequency domain (Fourier coefficients)
|
|
11
|
+
using the Fast Fourier Transform (FFT). This allows for the construction of
|
|
12
|
+
differential operators and rotationally-invariant Gaussian measures on the
|
|
13
|
+
circle, which are diagonal in the Fourier basis.
|
|
14
|
+
|
|
15
|
+
Key Classes
|
|
16
|
+
-----------
|
|
17
|
+
- `CircleHelper`: A mixin class providing the core geometry, FFT machinery, and
|
|
18
|
+
plotting utilities.
|
|
19
|
+
- `Lebesgue`: A concrete implementation of the L²(S¹) space of square-integrable
|
|
20
|
+
functions.
|
|
21
|
+
- `Sobolev`: A concrete implementation of the Hˢ(S¹) space of functions with a
|
|
22
|
+
specified degree of smoothness.
|
|
3
23
|
"""
|
|
4
24
|
|
|
5
25
|
from __future__ import annotations
|
|
26
|
+
|
|
6
27
|
from typing import Callable, Tuple, Optional
|
|
7
28
|
import matplotlib.pyplot as plt
|
|
8
29
|
import numpy as np
|
|
@@ -12,117 +33,45 @@ from scipy.sparse import diags
|
|
|
12
33
|
|
|
13
34
|
from matplotlib.figure import Figure
|
|
14
35
|
from matplotlib.axes import Axes
|
|
15
|
-
from pygeoinf.operators import LinearOperator
|
|
16
|
-
from pygeoinf.forms import LinearForm
|
|
17
|
-
from pygeoinf.gaussian_measure import GaussianMeasure
|
|
18
36
|
|
|
19
|
-
from pygeoinf.hilbert_space import
|
|
20
|
-
|
|
37
|
+
from pygeoinf.hilbert_space import (
|
|
38
|
+
HilbertModule,
|
|
39
|
+
MassWeightedHilbertModule,
|
|
40
|
+
)
|
|
41
|
+
from pygeoinf.operators import LinearOperator
|
|
42
|
+
from pygeoinf.linear_forms import LinearForm
|
|
43
|
+
from .symmetric_space import (
|
|
44
|
+
AbstractInvariantLebesgueSpace,
|
|
45
|
+
AbstractInvariantSobolevSpace,
|
|
46
|
+
)
|
|
21
47
|
|
|
22
48
|
|
|
23
|
-
class
|
|
49
|
+
class CircleHelper:
|
|
24
50
|
"""
|
|
25
|
-
|
|
51
|
+
A mixin class providing common functionality for function spaces on the circle.
|
|
26
52
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
53
|
+
This helper is not intended to be instantiated directly. It provides the core
|
|
54
|
+
geometry (radius, grid points), the FFT transform machinery, and plotting
|
|
55
|
+
utilities that are shared by both the `Lebesgue` and `Sobolev` space classes.
|
|
30
56
|
"""
|
|
31
57
|
|
|
32
|
-
def __init__(
|
|
33
|
-
self,
|
|
34
|
-
kmax: int,
|
|
35
|
-
order: float,
|
|
36
|
-
scale: float,
|
|
37
|
-
/,
|
|
38
|
-
*,
|
|
39
|
-
radius: float = 1.0,
|
|
40
|
-
):
|
|
58
|
+
def __init__(self, kmax: int, radius: float):
|
|
41
59
|
"""
|
|
42
60
|
Args:
|
|
43
|
-
kmax: The maximum Fourier degree to be represented
|
|
44
|
-
|
|
45
|
-
scale: The Sobolev length-scale.
|
|
46
|
-
radius: The radius of the circle. Defaults to 1.0.
|
|
61
|
+
kmax: The maximum Fourier degree to be represented.
|
|
62
|
+
radius: Radius of the circle.
|
|
47
63
|
"""
|
|
48
64
|
self._kmax: int = kmax
|
|
49
65
|
self._radius: float = radius
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
order,
|
|
53
|
-
scale,
|
|
54
|
-
2 * kmax,
|
|
55
|
-
self._to_components,
|
|
56
|
-
self._from_components,
|
|
57
|
-
self._inner_product,
|
|
58
|
-
self._to_dual,
|
|
59
|
-
self._from_dual,
|
|
60
|
-
vector_multiply=lambda u1, u2: u1 * u2,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
self._fft_factor: float = np.sqrt(2 * np.pi * radius) / self.dim
|
|
67
|
+
self._fft_factor: float = np.sqrt(2 * np.pi * radius) / (2 * self.kmax)
|
|
64
68
|
self._inverse_fft_factor: float = 1.0 / self._fft_factor
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for k in range(1, self.kmax + 1):
|
|
69
|
-
values[k] = 2 * self._sobolev_function(k)
|
|
70
|
-
|
|
71
|
-
self._metric = diags([values], [0])
|
|
72
|
-
self._inverse_metric = diags([np.reciprocal(values)], [0])
|
|
73
|
-
|
|
74
|
-
@staticmethod
|
|
75
|
-
def from_sobolev_parameters(
|
|
76
|
-
order: float,
|
|
77
|
-
scale: float,
|
|
78
|
-
/,
|
|
79
|
-
*,
|
|
80
|
-
radius: float = 1.0,
|
|
81
|
-
rtol: float = 1e-8,
|
|
82
|
-
power_of_two: bool = False,
|
|
83
|
-
) -> "Sobolev":
|
|
84
|
-
"""
|
|
85
|
-
Creates an instance with `kmax` chosen based on Sobolev parameters.
|
|
86
|
-
|
|
87
|
-
The method estimates the truncation error for the Dirac measure and is
|
|
88
|
-
only applicable for spaces with order > 0.5.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
order: The Sobolev order. Must be > 0.5.
|
|
92
|
-
scale: The Sobolev length-scale.
|
|
93
|
-
radius: The radius of the circle. Defaults to 1.0.
|
|
94
|
-
rtol: Relative tolerance used in assessing truncation error.
|
|
95
|
-
Defaults to 1e-8.
|
|
96
|
-
power_of_two: If True, `kmax` is set to the next power of two.
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
An instance of the Sobolev class with an appropriate `kmax`.
|
|
100
|
-
|
|
101
|
-
Raises:
|
|
102
|
-
ValueError: If order is <= 0.5.
|
|
103
|
-
"""
|
|
104
|
-
if order <= 0.5:
|
|
105
|
-
raise ValueError("This method is only applicable for orders > 0.5")
|
|
106
|
-
|
|
107
|
-
summation = 1.0
|
|
108
|
-
k = 0
|
|
109
|
-
err = 1.0
|
|
110
|
-
while err > rtol:
|
|
111
|
-
k += 1
|
|
112
|
-
term = (1 + (scale * k / radius) ** 2) ** -order
|
|
113
|
-
summation += 2 * term
|
|
114
|
-
err = 2 * term / summation
|
|
115
|
-
if k > 10000:
|
|
116
|
-
raise RuntimeError("Failed to converge on a stable kmax.")
|
|
117
|
-
|
|
118
|
-
if power_of_two:
|
|
119
|
-
n = int(np.log2(k))
|
|
120
|
-
k = 2 ** (n + 1)
|
|
121
|
-
|
|
122
|
-
return Sobolev(k, order, scale, radius=radius)
|
|
70
|
+
def _space(self):
|
|
71
|
+
return self
|
|
123
72
|
|
|
124
73
|
@property
|
|
125
|
-
def kmax(self)
|
|
74
|
+
def kmax(self):
|
|
126
75
|
"""The maximum Fourier degree represented in this space."""
|
|
127
76
|
return self._kmax
|
|
128
77
|
|
|
@@ -134,7 +83,28 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
134
83
|
@property
|
|
135
84
|
def angle_spacing(self) -> float:
|
|
136
85
|
"""The angular spacing between grid points."""
|
|
137
|
-
return
|
|
86
|
+
return np.pi / self.kmax
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def spatial_dimension(self) -> int:
|
|
90
|
+
"""The dimension of the space."""
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def fft_factor(self) -> float:
|
|
95
|
+
"""
|
|
96
|
+
The factor by which the Fourier coefficients are scaled
|
|
97
|
+
in forward transformations.
|
|
98
|
+
"""
|
|
99
|
+
return self._fft_factor
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def inverse_fft_factor(self) -> float:
|
|
103
|
+
"""
|
|
104
|
+
The factor by which the Fourier coefficients are scaled
|
|
105
|
+
in inverse transformations.
|
|
106
|
+
"""
|
|
107
|
+
return self._inverse_fft_factor
|
|
138
108
|
|
|
139
109
|
def random_point(self) -> float:
|
|
140
110
|
"""Returns a random angle in the interval [0, 2*pi)."""
|
|
@@ -143,10 +113,40 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
143
113
|
def angles(self) -> np.ndarray:
|
|
144
114
|
"""Returns a numpy array of the grid point angles."""
|
|
145
115
|
return np.fromiter(
|
|
146
|
-
[i * self.angle_spacing for i in range(self.
|
|
116
|
+
[i * self.angle_spacing for i in range(2 * self.kmax)],
|
|
147
117
|
float,
|
|
148
118
|
)
|
|
149
119
|
|
|
120
|
+
def laplacian_eigenvalue(self, k: int) -> float:
|
|
121
|
+
"""
|
|
122
|
+
Returns the k-th eigenvalue of the Laplacian.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
k: The index of the eigenvalue to return.
|
|
126
|
+
"""
|
|
127
|
+
return (k / self.radius) ** 2
|
|
128
|
+
|
|
129
|
+
def trace_of_invariant_automorphism(self, f: Callable[[float], float]) -> float:
|
|
130
|
+
"""
|
|
131
|
+
Returns the trace of the automorphism of the form f(Δ) with f a function
|
|
132
|
+
that is well-defined on the spectrum of the Laplacian.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
f: A real-valued function that is well-defined on the spectrum
|
|
136
|
+
of the Laplacian.
|
|
137
|
+
|
|
138
|
+
Notes:
|
|
139
|
+
The method takes account of the Nyquist theorem for real fast Fourier transforms,
|
|
140
|
+
this meaning that element at k = -kmax is excluded from the trace.
|
|
141
|
+
"""
|
|
142
|
+
trace = f(self.laplacian_eigenvalue(0))
|
|
143
|
+
if self.kmax > 0:
|
|
144
|
+
trace += f(self.laplacian_eigenvalue(self.kmax))
|
|
145
|
+
trace += 2 * np.sum(
|
|
146
|
+
[f(self.laplacian_eigenvalue(k)) for k in range(1, self.kmax)]
|
|
147
|
+
)
|
|
148
|
+
return float(trace)
|
|
149
|
+
|
|
150
150
|
def project_function(self, f: Callable[[float], float]) -> np.ndarray:
|
|
151
151
|
"""
|
|
152
152
|
Returns an element of the space by projecting a given function.
|
|
@@ -158,6 +158,14 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
158
158
|
"""
|
|
159
159
|
return np.fromiter((f(theta) for theta in self.angles()), float)
|
|
160
160
|
|
|
161
|
+
def to_coefficient(self, u: np.ndarray) -> np.ndarray:
|
|
162
|
+
"""Maps a function vector to its complex Fourier coefficients."""
|
|
163
|
+
return rfft(u) * self.fft_factor
|
|
164
|
+
|
|
165
|
+
def from_coefficient(self, coeff: np.ndarray) -> np.ndarray:
|
|
166
|
+
"""Maps complex Fourier coefficients to a function vector."""
|
|
167
|
+
return irfft(coeff, n=2 * self.kmax) * self._inverse_fft_factor
|
|
168
|
+
|
|
161
169
|
def plot(
|
|
162
170
|
self,
|
|
163
171
|
u: np.ndarray,
|
|
@@ -218,154 +226,291 @@ class Sobolev(SymmetricSpaceSobolev):
|
|
|
218
226
|
ax.fill_between(self.angles(), u - u_bound, u + u_bound, **kwargs)
|
|
219
227
|
return fig, ax
|
|
220
228
|
|
|
221
|
-
def
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
229
|
+
def _coefficient_to_component(self, coeff: np.ndarray) -> np.ndarray:
|
|
230
|
+
"""Packs complex Fourier coefficients into a real component vector."""
|
|
231
|
+
# For a real-valued input, the output of rfft (real FFT) has
|
|
232
|
+
# conjugate symmetry. This implies that the imaginary parts of the
|
|
233
|
+
# zero-frequency (k=0) and Nyquist-frequency (k=kmax) components
|
|
234
|
+
# are always zero. We omit them from the component vector to create
|
|
235
|
+
# a minimal, non-redundant representation.
|
|
236
|
+
return np.concatenate((coeff.real, coeff.imag[1 : self.kmax]))
|
|
226
237
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
def _component_to_coefficient(self, c: np.ndarray) -> np.ndarray:
|
|
239
|
+
"""Unpacks a real component vector into complex Fourier coefficients."""
|
|
240
|
+
# This is the inverse of `_coefficient_to_component`. It reconstructs
|
|
241
|
+
# the full complex coefficient array that irfft expects. We re-insert
|
|
242
|
+
# the known zeros for the imaginary parts of the zero-frequency (k=0)
|
|
243
|
+
# and Nyquist-frequency (k=kmax) components, which were removed to
|
|
244
|
+
# create the minimal real-valued representation.
|
|
245
|
+
coeff_real = c[: self.kmax + 1]
|
|
246
|
+
coeff_imag = np.concatenate([[0], c[self.kmax + 1 :], [0]])
|
|
247
|
+
return coeff_real + 1j * coeff_imag
|
|
231
248
|
|
|
232
|
-
return LinearOperator.formally_self_adjoint(self, mapping)
|
|
233
249
|
|
|
234
|
-
|
|
250
|
+
class Lebesgue(CircleHelper, HilbertModule, AbstractInvariantLebesgueSpace):
|
|
251
|
+
"""
|
|
252
|
+
Implementation of the Lebesgue space L² on a circle.
|
|
253
|
+
|
|
254
|
+
This class represents square-integrable functions on a circle. A function is
|
|
255
|
+
represented by its values on an evenly spaced grid. The L² inner product
|
|
256
|
+
is correctly implemented by accounting for the non-orthonormality of the
|
|
257
|
+
real Fourier basis functions.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def __init__(
|
|
235
261
|
self,
|
|
236
|
-
|
|
262
|
+
kmax: int,
|
|
237
263
|
/,
|
|
238
264
|
*,
|
|
239
|
-
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
265
|
+
radius: float = 1.0,
|
|
266
|
+
):
|
|
267
|
+
"""
|
|
268
|
+
Args:
|
|
269
|
+
kmax: The maximum Fourier degree to be represented.
|
|
270
|
+
radius: Radius of the circle. Defaults to 1.0.
|
|
271
|
+
"""
|
|
246
272
|
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
if kmax < 0:
|
|
274
|
+
raise ValueError("kmax must be non-negative")
|
|
249
275
|
|
|
250
|
-
|
|
251
|
-
coeff = self._component_to_coefficient(c)
|
|
252
|
-
coeff = matrix @ coeff
|
|
253
|
-
return self.from_coefficient(coeff)
|
|
276
|
+
self._dim = 2 * kmax
|
|
254
277
|
|
|
255
|
-
|
|
256
|
-
coeff = self.to_coefficient(u)
|
|
257
|
-
coeff = matrix @ coeff
|
|
258
|
-
return self._coefficient_to_component(coeff)
|
|
278
|
+
CircleHelper.__init__(self, kmax, radius)
|
|
259
279
|
|
|
260
|
-
|
|
261
|
-
|
|
280
|
+
values = np.fromiter(
|
|
281
|
+
[2 if k > 0 else 1 for k in range(self.kmax + 1)], dtype=float
|
|
262
282
|
)
|
|
283
|
+
self._metric = diags([values], [0])
|
|
284
|
+
self._inverse_metric = diags([np.reciprocal(values)], [0])
|
|
263
285
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
@property
|
|
287
|
+
def dim(self) -> int:
|
|
288
|
+
"""The dimension of the space."""
|
|
289
|
+
return self._dim
|
|
268
290
|
|
|
269
|
-
def
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
for k in range(1, coeff.size):
|
|
274
|
-
coeff[k] = coeff[k - 1] * fac
|
|
275
|
-
coeff *= 1.0 / np.sqrt(2 * np.pi * self.radius)
|
|
276
|
-
coeff[1:] *= 2.0
|
|
277
|
-
cp = self._coefficient_to_component(coeff)
|
|
278
|
-
return LinearForm(self, components=cp)
|
|
291
|
+
def to_components(self, u: np.ndarray) -> np.ndarray:
|
|
292
|
+
"""Converts a function vector to its real component representation."""
|
|
293
|
+
coeff = self.to_coefficient(u)
|
|
294
|
+
return self._coefficient_to_component(coeff)
|
|
279
295
|
|
|
280
|
-
def
|
|
281
|
-
"""
|
|
282
|
-
|
|
296
|
+
def from_components(self, c: np.ndarray) -> np.ndarray:
|
|
297
|
+
"""Converts a real component vector back to a function vector."""
|
|
298
|
+
coeff = self._component_to_coefficient(c)
|
|
299
|
+
return self.from_coefficient(coeff)
|
|
283
300
|
|
|
284
|
-
def
|
|
285
|
-
"""Maps
|
|
286
|
-
|
|
301
|
+
def to_dual(self, u: np.ndarray) -> "LinearForm":
|
|
302
|
+
"""Maps a vector `u` to its dual representation `u*`."""
|
|
303
|
+
coeff = self.to_coefficient(u)
|
|
304
|
+
cp = self._coefficient_to_component(self._metric @ coeff)
|
|
305
|
+
return self.dual.from_components(cp)
|
|
287
306
|
|
|
307
|
+
def from_dual(self, up: "LinearForm") -> np.ndarray:
|
|
308
|
+
"""Maps a dual vector `u*` back to its primal representation `u`."""
|
|
309
|
+
cp = self.dual.to_components(up)
|
|
310
|
+
dual_coeff = self._component_to_coefficient(cp)
|
|
311
|
+
primal_coeff = self._inverse_metric @ dual_coeff
|
|
312
|
+
return self.from_coefficient(primal_coeff)
|
|
313
|
+
|
|
314
|
+
def vector_multiply(self, x1: np.ndarray, x2: np.ndarray) -> np.ndarray:
|
|
315
|
+
"""
|
|
316
|
+
Computes the pointwise product of two vectors.
|
|
317
|
+
"""
|
|
318
|
+
return x1 * x2
|
|
319
|
+
|
|
320
|
+
def eigenfunction_norms(self) -> np.ndarray:
|
|
321
|
+
"""Returns a list of the norms of the eigenfunctions."""
|
|
322
|
+
return np.fromiter(
|
|
323
|
+
[np.sqrt(2) if i > 0 else 1 for i in range(self.dim)],
|
|
324
|
+
dtype=float,
|
|
325
|
+
)
|
|
288
326
|
|
|
289
327
|
def __eq__(self, other: object) -> bool:
|
|
290
328
|
"""
|
|
291
|
-
Checks for mathematical equality with another
|
|
329
|
+
Checks for mathematical equality with another Lebesgue space on a circle.
|
|
292
330
|
|
|
293
331
|
Two spaces are considered equal if they are of the same type and have
|
|
294
332
|
the same defining parameters (kmax, order, scale, and radius).
|
|
295
333
|
"""
|
|
296
|
-
if not isinstance(other,
|
|
334
|
+
if not isinstance(other, Lebesgue):
|
|
297
335
|
return NotImplemented
|
|
298
|
-
|
|
299
|
-
return (self.kmax == other.kmax and
|
|
300
|
-
self.order == other.order and
|
|
301
|
-
self.scale == other.scale and
|
|
302
|
-
self.radius == other.radius)
|
|
303
|
-
|
|
304
|
-
# ================================================================#
|
|
305
|
-
# Private methods #
|
|
306
|
-
# ================================================================#
|
|
307
|
-
|
|
308
|
-
def _sobolev_function(self, k: int) -> float:
|
|
309
|
-
"""Computes the diagonal entries of the Sobolev metric in Fourier space."""
|
|
310
|
-
return (1 + (self.scale * k / self.radius) ** 2) ** self.order
|
|
311
|
-
|
|
312
|
-
def _coefficient_to_component(self, coeff: np.ndarray) -> np.ndarray:
|
|
313
|
-
"""Packs complex Fourier coefficients into a real component vector."""
|
|
314
|
-
return np.concatenate((coeff.real, coeff.imag[1 : self.kmax]))
|
|
315
336
|
|
|
316
|
-
|
|
317
|
-
"""Unpacks a real component vector into complex Fourier coefficients."""
|
|
318
|
-
coeff_real = c[: self.kmax + 1]
|
|
319
|
-
coeff_imag = np.concatenate([[0], c[self.kmax + 1 :], [0]])
|
|
320
|
-
return coeff_real + 1j * coeff_imag
|
|
337
|
+
return self.kmax == other.kmax and self.radius == other.radius
|
|
321
338
|
|
|
322
|
-
def
|
|
323
|
-
"""
|
|
324
|
-
|
|
325
|
-
|
|
339
|
+
def invariant_automorphism(self, f: Callable[[float], float]):
|
|
340
|
+
"""
|
|
341
|
+
Implements an invariant automorphism of the form f(Δ) using Fourier
|
|
342
|
+
expansions on a circle.
|
|
326
343
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
344
|
+
Args:
|
|
345
|
+
f: A real-valued function that is well-defined on the spectrum
|
|
346
|
+
of the Laplacian, Δ.
|
|
347
|
+
"""
|
|
331
348
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
349
|
+
values = np.fromiter(
|
|
350
|
+
(f(self.laplacian_eigenvalue(k)) for k in range(self.kmax + 1)),
|
|
351
|
+
dtype=float,
|
|
352
|
+
)
|
|
353
|
+
matrix = diags([values], [0])
|
|
337
354
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
return self.dual.from_components(cp)
|
|
355
|
+
def mapping(u):
|
|
356
|
+
coeff = self.to_coefficient(u)
|
|
357
|
+
coeff = matrix @ coeff
|
|
358
|
+
return self.from_coefficient(coeff)
|
|
343
359
|
|
|
344
|
-
|
|
345
|
-
"""Maps a dual vector `u*` back to its primal representation `u`."""
|
|
346
|
-
cp = self.dual.to_components(up)
|
|
347
|
-
coeff = self._component_to_coefficient(cp)
|
|
348
|
-
c = self._coefficient_to_component(self._inverse_metric @ coeff)
|
|
349
|
-
return self.from_components(c)
|
|
360
|
+
return LinearOperator.self_adjoint(self, mapping)
|
|
350
361
|
|
|
351
362
|
|
|
352
|
-
class
|
|
363
|
+
class Sobolev(
|
|
364
|
+
CircleHelper,
|
|
365
|
+
MassWeightedHilbertModule,
|
|
366
|
+
AbstractInvariantSobolevSpace,
|
|
367
|
+
):
|
|
353
368
|
"""
|
|
354
|
-
Implementation of the
|
|
369
|
+
Implementation of the Sobolev space Hˢ on a circle.
|
|
355
370
|
|
|
356
|
-
This
|
|
371
|
+
This class represents functions with a specified degree of smoothness. It is
|
|
372
|
+
constructed as a `MassWeightedHilbertModule` over the `Lebesgue` space, where
|
|
373
|
+
the mass operator weights the Fourier coefficients to enforce smoothness. This
|
|
374
|
+
is the primary class for defining smooth, random function fields on the circle.
|
|
357
375
|
"""
|
|
358
376
|
|
|
359
377
|
def __init__(
|
|
360
378
|
self,
|
|
361
379
|
kmax: int,
|
|
380
|
+
order: float,
|
|
381
|
+
scale: float,
|
|
362
382
|
/,
|
|
363
383
|
*,
|
|
364
384
|
radius: float = 1.0,
|
|
365
385
|
):
|
|
366
386
|
"""
|
|
367
387
|
Args:
|
|
368
|
-
|
|
369
|
-
|
|
388
|
+
kmax: The maximum Fourier degree to be represented.
|
|
389
|
+
order: The Sobolev order, controlling the smoothness of functions.
|
|
390
|
+
scale: The Sobolev length-scale.
|
|
391
|
+
radius: Radius of the circle. Defaults to 1.0.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
CircleHelper.__init__(self, kmax, radius)
|
|
395
|
+
AbstractInvariantSobolevSpace.__init__(self, order, scale)
|
|
396
|
+
|
|
397
|
+
lebesgue = Lebesgue(kmax, radius=radius)
|
|
398
|
+
|
|
399
|
+
mass_operator = lebesgue.invariant_automorphism(self.sobolev_function)
|
|
400
|
+
inverse_mass_operator = lebesgue.invariant_automorphism(
|
|
401
|
+
lambda k: 1.0 / self.sobolev_function(k)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
MassWeightedHilbertModule.__init__(
|
|
405
|
+
self, lebesgue, mass_operator, inverse_mass_operator
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
@staticmethod
|
|
409
|
+
def from_sobolev_parameters(
|
|
410
|
+
order: float,
|
|
411
|
+
scale: float,
|
|
412
|
+
/,
|
|
413
|
+
*,
|
|
414
|
+
radius: float = 1.0,
|
|
415
|
+
rtol: float = 1e-6,
|
|
416
|
+
power_of_two: bool = False,
|
|
417
|
+
) -> "Sobolev":
|
|
370
418
|
"""
|
|
371
|
-
|
|
419
|
+
Creates an instance with `kmax` chosen based on Sobolev parameters.
|
|
420
|
+
|
|
421
|
+
The method estimates the truncation error for the Dirac measure and is
|
|
422
|
+
only applicable for spaces with order > 0.5.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
order: The Sobolev order. Must be > 0.5.
|
|
426
|
+
scale: The Sobolev length-scale.
|
|
427
|
+
radius: The radius of the circle. Defaults to 1.0.
|
|
428
|
+
rtol: Relative tolerance used in assessing truncation error.
|
|
429
|
+
Defaults to 1e-8.
|
|
430
|
+
power_of_two: If True, `kmax` is set to the next power of two.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
An instance of the Sobolev class with an appropriate `kmax`.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
ValueError: If order is <= 0.5.
|
|
437
|
+
"""
|
|
438
|
+
if order <= 0.5:
|
|
439
|
+
raise ValueError("This method is only applicable for orders > 0.5")
|
|
440
|
+
|
|
441
|
+
summation = 1.0
|
|
442
|
+
k = 0
|
|
443
|
+
err = 1.0
|
|
444
|
+
while err > rtol:
|
|
445
|
+
k += 1
|
|
446
|
+
term = (1 + (scale * k / radius) ** 2) ** -order
|
|
447
|
+
summation += 2 * term
|
|
448
|
+
err = 2 * term / summation
|
|
449
|
+
if k > 100000:
|
|
450
|
+
raise RuntimeError("Failed to converge on a stable kmax.")
|
|
451
|
+
|
|
452
|
+
if power_of_two:
|
|
453
|
+
n = int(np.log2(k))
|
|
454
|
+
k = 2 ** (n + 1)
|
|
455
|
+
|
|
456
|
+
return Sobolev(k, order, scale, radius=radius)
|
|
457
|
+
|
|
458
|
+
def __eq__(self, other: object) -> bool:
|
|
459
|
+
"""
|
|
460
|
+
Checks for mathematical equality with another Sobolev space on a circle.
|
|
461
|
+
|
|
462
|
+
Two spaces are considered equal if they are of the same type and have
|
|
463
|
+
the same defining parameters (kmax, order, scale, and radius).
|
|
464
|
+
"""
|
|
465
|
+
if not isinstance(other, Sobolev):
|
|
466
|
+
return NotImplemented
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
self.kmax == other.kmax
|
|
470
|
+
and self.radius == other.radius
|
|
471
|
+
and self.order == other.order
|
|
472
|
+
and self.scale == other.scale
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def eigenfunction_norms(self) -> np.ndarray:
|
|
476
|
+
"""Returns a list of the norms of the eigenfunctions."""
|
|
477
|
+
values = self.underlying_space.eigenfunction_norms()
|
|
478
|
+
|
|
479
|
+
i = 0
|
|
480
|
+
for k in range(self.kmax + 1):
|
|
481
|
+
values[i] *= np.sqrt(self.sobolev_function(self.laplacian_eigenvalue(k)))
|
|
482
|
+
i += 1
|
|
483
|
+
|
|
484
|
+
for k in range(1, self.kmax):
|
|
485
|
+
values[i] *= np.sqrt(self.sobolev_function(self.laplacian_eigenvalue(k)))
|
|
486
|
+
i += 1
|
|
487
|
+
|
|
488
|
+
return values
|
|
489
|
+
|
|
490
|
+
def dirac(self, point: float) -> LinearForm:
|
|
491
|
+
"""
|
|
492
|
+
Returns the linear functional corresponding to a point evaluation.
|
|
493
|
+
|
|
494
|
+
This represents the action of the Dirac delta measure based at the given
|
|
495
|
+
point.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
point: The angle for the point at which the measure is based.
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
ValueError: If the Sobolev order is less than 1/2.
|
|
502
|
+
"""
|
|
503
|
+
if self.order <= 1 / 2:
|
|
504
|
+
raise NotImplementedError(
|
|
505
|
+
"This method is only applicable for orders >= 1/2"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
coeff = np.zeros(self.kmax + 1, dtype=complex)
|
|
509
|
+
fac = np.exp(-1j * point)
|
|
510
|
+
coeff[0] = 1.0
|
|
511
|
+
for k in range(1, coeff.size):
|
|
512
|
+
coeff[k] = coeff[k - 1] * fac
|
|
513
|
+
coeff *= 1.0 / np.sqrt(2 * np.pi * self.radius)
|
|
514
|
+
coeff[1:] *= 2.0
|
|
515
|
+
cp = self._coefficient_to_component(coeff)
|
|
516
|
+
return LinearForm(self, components=cp)
|