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/forms.py DELETED
@@ -1,168 +0,0 @@
1
- """
2
- Module defining the LinearForm class.
3
- """
4
-
5
- from __future__ import annotations
6
- from typing import Callable, Optional, Any, TYPE_CHECKING
7
-
8
- import numpy as np
9
-
10
- # This block only runs for type checkers, not at runtime
11
- if TYPE_CHECKING:
12
- from .hilbert_space import HilbertSpace, EuclideanSpace
13
- from .operators import LinearOperator
14
-
15
-
16
- class LinearForm:
17
- """
18
- Represents a linear form, which is a linear functional that maps
19
- vectors from a Hilbert space to a scalar value (a real number).
20
- """
21
-
22
- def __init__(
23
- self,
24
- domain: "HilbertSpace",
25
- /,
26
- *,
27
- mapping: Optional[Callable[[Any], float]] = None,
28
- components: Optional[np.ndarray] = None,
29
- ) -> None:
30
- """
31
- Initializes the LinearForm.
32
-
33
- A form can be defined either by its mapping or its component vector.
34
-
35
- Args:
36
- domain (HilbertSpace): The Hilbert space on which the form is defined.
37
- mapping (callable, optional): A function defining the action of the form.
38
- components (np.ndarray, optional): The component representation of
39
- the form.
40
- """
41
-
42
- self._domain: "HilbertSpace" = domain
43
- self._components: Optional[np.ndarray] = components
44
- self._mapping: Callable[[Any], float]
45
-
46
- if components is None:
47
- if mapping is None:
48
- raise AssertionError("Neither mapping nor components specified.")
49
- else:
50
- self._mapping = mapping
51
- else:
52
- if mapping is None:
53
- self._mapping = self._mapping_from_components
54
- else:
55
- self._mapping = mapping
56
-
57
- @staticmethod
58
- def from_linear_operator(operator: "LinearOperator") -> "LinearForm":
59
- """
60
- Creates a LinearForm from an operator that maps to a 1D Euclidean space.
61
- """
62
- from .hilbert_space import EuclideanSpace
63
-
64
- assert operator.codomain == EuclideanSpace(1)
65
- return LinearForm(operator.domain, mapping=lambda x: operator(x)[0])
66
-
67
- @property
68
- def domain(self) -> "HilbertSpace":
69
- """The Hilbert space on which the form is defined."""
70
- return self._domain
71
-
72
- @property
73
- def components_stored(self) -> bool:
74
- """True if the form's component vector is cached."""
75
- return self._components is not None
76
-
77
- @property
78
- def components(self) -> np.ndarray:
79
- """
80
- The component vector of the form.
81
-
82
- The components are computed and cached on first access if not
83
- provided during initialization.
84
- """
85
- if self.components_stored:
86
- return self._components
87
- else:
88
- self.store_components()
89
- return self.components
90
-
91
- def store_components(self) -> None:
92
- """Computes and caches the component vector of the form."""
93
- if not self.components_stored:
94
- self._components = np.zeros(self.domain.dim)
95
- cx = np.zeros(self.domain.dim)
96
- for i in range(self.domain.dim):
97
- cx[i] = 1
98
- x = self.domain.from_components(cx)
99
- self._components[i] = self(x)
100
- cx[i] = 0
101
-
102
- @property
103
- def as_linear_operator(self) -> "LinearOperator":
104
- """
105
- Represents the linear form as a LinearOperator mapping to a
106
- 1D Euclidean space.
107
- """
108
- from .hilbert_space import EuclideanSpace
109
- from .operators import LinearOperator
110
-
111
- return LinearOperator(
112
- self.domain,
113
- EuclideanSpace(1),
114
- lambda x: np.array([self(x)]),
115
- dual_mapping=lambda y: y * self,
116
- )
117
-
118
- def __call__(self, x: Any) -> float:
119
- """Applies the linear form to a vector."""
120
- return self._mapping(x)
121
-
122
- def __neg__(self) -> "LinearForm":
123
- """Returns the additive inverse of the form."""
124
- if self.components_stored:
125
- return LinearForm(self.domain, components=-self._components)
126
- else:
127
- return LinearForm(self.domain, mapping=lambda x: -self(x))
128
-
129
- def __mul__(self, a: float) -> "LinearForm":
130
- """Returns the product of the form and a scalar."""
131
- if self.components_stored:
132
- return LinearForm(self.domain, components=a * self._components)
133
- else:
134
- return LinearForm(self.domain, mapping=lambda x: a * self(x))
135
-
136
- def __rmul__(self, a: float) -> "LinearForm":
137
- """Returns the product of the form and a scalar."""
138
- return self * a
139
-
140
- def __truediv__(self, a: float) -> "LinearForm":
141
- """Returns the division of the form by a scalar."""
142
- return self * (1.0 / a)
143
-
144
- def __add__(self, other: "LinearForm") -> "LinearForm":
145
- """Returns the sum of this form and another."""
146
- if self.components_stored and other.components_stored:
147
- return LinearForm(
148
- self.domain, components=self.components + other.components
149
- )
150
- else:
151
- return LinearForm(self.domain, mapping=lambda x: self(x) + other(x))
152
-
153
- def __sub__(self, other: "LinearForm") -> "LinearForm":
154
- """Returns the difference between this form and another."""
155
- if self.components_stored and other.components_stored:
156
- return LinearForm(
157
- self.domain, components=self.components - other.components
158
- )
159
- else:
160
- return LinearForm(self.domain, mapping=lambda x: self(x) - other(x))
161
-
162
- def __str__(self) -> str:
163
- """Returns the string representation of the form's components."""
164
- return self.components.__str__()
165
-
166
- def _mapping_from_components(self, x: Any) -> float:
167
- """Implements the action of the form using its cached components."""
168
- return np.dot(self._components, self.domain.to_components(x))
@@ -1,384 +0,0 @@
1
- """
2
- Sobolev spaces for functions on a line.
3
- """
4
-
5
- from __future__ import annotations
6
- from typing import Callable, Any, Tuple, Optional
7
- import matplotlib.pyplot as plt
8
- import numpy as np
9
-
10
-
11
- from matplotlib.figure import Figure
12
- from matplotlib.axes import Axes
13
- from pygeoinf.operators import LinearOperator
14
- from pygeoinf.forms import LinearForm
15
- from pygeoinf.gaussian_measure import GaussianMeasure
16
-
17
- from pygeoinf.operators import LinearOperator
18
- from pygeoinf.gaussian_measure import GaussianMeasure
19
- from pygeoinf.symmetric_space.symmetric_space import SymmetricSpaceSobolev
20
- from pygeoinf.symmetric_space.circle import Sobolev as CircleSobolev
21
-
22
-
23
- class Sobolev(SymmetricSpaceSobolev):
24
- """
25
- Implementation of the Sobolev space H^s on a finite line interval.
26
-
27
- This class models functions on an interval [x0, x1] by mapping the problem
28
- to a periodic domain (a circle) with padding. This avoids explicit
29
- boundary conditions by using smooth tapers.
30
- """
31
-
32
- def __init__(
33
- self,
34
- kmax: int,
35
- order: float,
36
- scale: float,
37
- /,
38
- *,
39
- x0: float = 0.0,
40
- x1: float = 1.0,
41
- ) -> None:
42
- """
43
- Args:
44
- kmax: The maximum Fourier degree for the underlying circle representation.
45
- order: The Sobolev order, controlling function smoothness.
46
- scale: The Sobolev length-scale.
47
- x0: The left boundary of the interval. Defaults to 0.0.
48
- x1: The right boundary of the interval. Defaults to 1.0.
49
-
50
- Raises:
51
- ValueError: If `x0 >= x1` or if `scale <= 0` when `order` is non-zero.
52
- """
53
-
54
- if x0 >= x1:
55
- raise ValueError("Invalid interval parameters: x0 must be less than x1.")
56
- if order != 0 and scale <= 0:
57
- raise ValueError("Length-scale must be positive for non-L2 spaces.")
58
-
59
- self._kmax: int = kmax
60
- self._x0: float = x0
61
- self._x1: float = x1
62
-
63
- # Work out the padding.
64
- padding_scale: float = 5 * scale if scale > 0 else 0.1 * (x1 - x0)
65
- number_of_points: int = 2 * kmax
66
- width: float = x1 - x0
67
- self._start_index: int = int(
68
- number_of_points * padding_scale / (width + 2 * padding_scale)
69
- )
70
- self._finish_index: int = 2 * kmax - self._start_index + 1
71
- self._padding_length: float = (
72
- self._start_index * width / (number_of_points - 2 * self._start_index)
73
- )
74
-
75
- self._jac: float = (width + 2 * self._padding_length) / (2 * np.pi)
76
- self._ijac: float = 1.0 / self._jac
77
- self._sqrt_jac: float = np.sqrt(self._jac)
78
- self._isqrt_jac: float = 1.0 / self._sqrt_jac
79
-
80
- # Set up the related Sobolev space on the unit circle.
81
- circle_scale: float = scale * self._ijac
82
- self._circle_space: CircleSobolev = CircleSobolev(kmax, order, circle_scale)
83
-
84
- super().__init__(
85
- order,
86
- scale,
87
- self._circle_space.dim,
88
- self._to_components,
89
- self._from_components,
90
- self._inner_product,
91
- self._to_dual,
92
- self._from_dual,
93
- vector_multiply=lambda u1, u2: u1 * u2,
94
- )
95
-
96
- @staticmethod
97
- def from_sobolev_parameters(
98
- order: float,
99
- scale: float,
100
- /,
101
- *,
102
- x0: float = 0.0,
103
- x1: float = 1.0,
104
- rtol: float = 1e-8,
105
- power_of_two: bool = False,
106
- ) -> "Sobolev":
107
- """
108
- Creates an instance with `kmax` selected based on the Sobolev parameters.
109
-
110
- Args:
111
- order: The Sobolev order.
112
- scale: The Sobolev length-scale.
113
- x0: The left boundary of the interval. Defaults to 0.0.
114
- x1: The right boundary of the interval. Defaults to 1.0.
115
- rtol: Relative tolerance for truncation error assessment.
116
- power_of_two: If True, `kmax` is set to the next power of two.
117
-
118
- Returns:
119
- An instance of the Sobolev class with an appropriate `kmax`.
120
- """
121
- if x0 >= x1:
122
- raise ValueError("Invalid interval parameters")
123
-
124
- circle_scale = scale / (x1 - x0)
125
- circle_space = CircleSobolev.from_sobolev_parameters(
126
- order, circle_scale, rtol=rtol, power_of_two=power_of_two
127
- )
128
- kmax = circle_space.kmax
129
- return Sobolev(kmax, order, scale, x0=x0, x1=x1)
130
-
131
- @property
132
- def kmax(self) -> int:
133
- """The maximum Fourier degree of the underlying circle representation."""
134
- return self._kmax
135
-
136
- @property
137
- def x0(self) -> float:
138
- """The left boundary point of the interval."""
139
- return self._x0
140
-
141
- @property
142
- def x1(self) -> float:
143
- """The right boundary point of the interval."""
144
- return self._x1
145
-
146
- @property
147
- def width(self) -> float:
148
- """The width of the interval, `x1 - x0`."""
149
- return self._x1 - self._x0
150
-
151
- @property
152
- def point_spacing(self) -> float:
153
- """The spacing between grid points on the interval."""
154
- return self._circle_space.angle_spacing * self._jac
155
-
156
- def computational_points(self) -> np.ndarray:
157
- """Returns the grid points on the full computational domain, including padding."""
158
- return self._x0 - self._padding_length + self._jac * self._circle_space.angles()
159
-
160
- def points(self) -> np.ndarray:
161
- """Returns the grid points within the primary interval `[x0, x1]`."""
162
- return self.computational_points()[self._start_index : self._finish_index]
163
-
164
- def project_function(self, f: Callable[[float], float]) -> np.ndarray:
165
- """
166
- Returns an element of the space by projecting a given function.
167
-
168
- The function `f` is evaluated at the computational grid points and
169
- multiplied by a smooth tapering function.
170
-
171
- Args:
172
- f: A function that takes a position `x` and returns a value.
173
- """
174
- return np.fromiter(
175
- (f(x) * self._taper(x) for x in self.computational_points()), float
176
- )
177
-
178
- def random_point(self) -> float:
179
- """Returns a random point within the interval `[x0, x1]`."""
180
- return np.random.uniform(self._x0, self._x1)
181
-
182
- def plot(
183
- self,
184
- u: np.ndarray,
185
- fig: Optional[Figure] = None,
186
- ax: Optional[Axes] = None,
187
- /,
188
- *,
189
- computational_domain: bool = False,
190
- **kwargs,
191
- ) -> Tuple[Figure, Axes]:
192
- """
193
- Makes a simple plot of a function from the space.
194
-
195
- Args:
196
- u: The vector representing the function to be plotted.
197
- fig: An existing Matplotlib Figure object. Defaults to None.
198
- ax: An existing Matplotlib Axes object. Defaults to None.
199
- computational_domain: If True, plot the whole computational
200
- domain including the tapered padding. Defaults to False.
201
- **kwargs: Keyword arguments forwarded to `ax.plot()`.
202
-
203
- Returns:
204
- A tuple (figure, axes) containing the plot objects.
205
- """
206
- figsize = kwargs.pop("figsize", (10, 8))
207
-
208
- if fig is None:
209
- fig = plt.figure(figsize=figsize)
210
- if ax is None:
211
- ax = fig.add_subplot()
212
-
213
- if computational_domain:
214
- ax.plot(self.computational_points(), u, **kwargs)
215
- else:
216
- ax.plot(self.points(), u[self._start_index : self._finish_index], **kwargs)
217
-
218
- return fig, ax
219
-
220
- def plot_pointwise_bounds(
221
- self,
222
- u: np.ndarray,
223
- u_bound: np.ndarray,
224
- fig: Optional[Figure] = None,
225
- ax: Optional[Axes] = None,
226
- /,
227
- *,
228
- computational_domain: bool = False,
229
- **kwargs,
230
- ) -> Tuple[Figure, Axes]:
231
- """
232
- Plots a function with pointwise error bounds.
233
-
234
- Args:
235
- u: The vector representing the mean function.
236
- u_bound: A vector giving pointwise standard deviations.
237
- fig: An existing Matplotlib Figure object. Defaults to None.
238
- ax: An existing Matplotlib Axes object. Defaults to None.
239
- computational_domain: If True, plot the whole computational domain.
240
- **kwargs: Keyword arguments forwarded to `ax.fill_between()`.
241
-
242
- Returns:
243
- A tuple (figure, axes) containing the plot objects.
244
- """
245
- figsize = kwargs.pop("figsize", (10, 8))
246
-
247
- if fig is None:
248
- fig = plt.figure(figsize=figsize)
249
- if ax is None:
250
- ax = fig.add_subplot()
251
-
252
- if computational_domain:
253
- ax.fill_between(
254
- self.computational_points(), u - u_bound, u + u_bound, **kwargs
255
- )
256
- else:
257
- ax.fill_between(
258
- self.points(),
259
- u[self._start_index : self._finish_index]
260
- - u_bound[self._start_index : self._finish_index],
261
- u[self._start_index : self._finish_index]
262
- + u_bound[self._start_index : self._finish_index],
263
- **kwargs,
264
- )
265
-
266
- return fig, ax
267
-
268
- def invariant_automorphism(self, f: Callable[[float], float]) -> "LinearOperator":
269
- A = self._circle_space.invariant_automorphism(lambda k: f(self._ijac * k))
270
- return LinearOperator.formally_self_adjoint(self, A)
271
-
272
- def invariant_gaussian_measure(
273
- self, f: Callable[[float], float], /, *, expectation: Optional[Any] = None
274
- ) -> "GaussianMeasure":
275
- mu = self._circle_space.invariant_gaussian_measure(
276
- lambda k: f(self._ijac * k), expectation=expectation
277
- )
278
- covariance = LinearOperator.self_adjoint(self, mu.covariance)
279
- return GaussianMeasure(
280
- covariance=covariance, expectation=expectation, sample=mu.sample
281
- )
282
-
283
- def dirac(self, point: float) -> "LinearForm":
284
- theta = self._inverse_transformation(point)
285
- up = self._circle_space.dirac(theta)
286
- cp = self._circle_space.dual.to_components(up) * self._isqrt_jac
287
- return self.dual.from_components(cp)
288
-
289
- def __eq__(self, other: object) -> bool:
290
- """
291
- Checks for mathematical equality with another Sobolev space on a line.
292
-
293
- Two spaces are considered equal if they are of the same type and have
294
- the same defining parameters (kmax, order, scale, x0, and x1).
295
- """
296
-
297
- if not isinstance(other, Sobolev):
298
- return NotImplemented
299
-
300
- return (
301
- self.kmax == other.kmax
302
- and self.order == other.order
303
- and self.scale == other.scale
304
- and self.x0 == other.x0
305
- and self.x1 == other.x1
306
- )
307
-
308
- # =============================================================#
309
- # Private methods #
310
- # =============================================================#
311
-
312
- def _step(self, x: float) -> float:
313
- if x > 0:
314
- return np.exp(-1.0 / x)
315
- else:
316
- return 0.0
317
-
318
- def _bump_up(self, x: float, x1: float, x2: float) -> float:
319
- s1 = self._step(x - x1)
320
- s2 = self._step(x2 - x)
321
- return s1 / (s1 + s2)
322
-
323
- def _bump_down(self, x: float, x1: float, x2: float) -> float:
324
- s1 = self._step(x2 - x)
325
- s2 = self._step(x - x1)
326
- return s1 / (s1 + s2)
327
-
328
- def _taper(self, x: float) -> float:
329
- s1 = self._bump_up(x, self._x0 - self._padding_length, self._x0)
330
- s2 = self._bump_down(x, self._x1, self._x1 + self._padding_length)
331
- return s1 * s2
332
-
333
- def _transformation(self, th: float) -> float:
334
- return self._x0 - self._padding_length + self._jac * th
335
-
336
- def _inverse_transformation(self, x: float) -> float:
337
- return (x - self._x0 + self._padding_length) * self._ijac
338
-
339
- def _to_components(self, u: np.ndarray) -> np.ndarray:
340
- c = self._circle_space.to_components(u)
341
- c *= self._sqrt_jac
342
- return c
343
-
344
- def _from_components(self, c: np.ndarray) -> np.ndarray:
345
- u = self._circle_space.from_components(c)
346
- u *= self._isqrt_jac
347
- return u
348
-
349
- def _inner_product(self, u1: np.ndarray, u2: np.ndarray) -> float:
350
- return self._jac * self._circle_space.inner_product(u1, u2)
351
-
352
- def _to_dual(self, u: np.ndarray) -> "LinearForm":
353
- up = self._circle_space.to_dual(u)
354
- cp = self._circle_space.dual.to_components(up) * self._sqrt_jac
355
- return self.dual.from_components(cp)
356
-
357
- def _from_dual(self, up: "LinearForm") -> np.ndarray:
358
- cp = self.dual.to_components(up)
359
- vp = self._circle_space.dual.from_components(cp) * self._isqrt_jac
360
- return self._circle_space.from_dual(vp)
361
-
362
-
363
- class Lebesgue(Sobolev):
364
- """
365
- Implementation of the Lebesgue space L^2 on a line.
366
-
367
- This is a special case of the Sobolev space with order `s=0`.
368
- """
369
-
370
- def __init__(
371
- self,
372
- kmax: int,
373
- /,
374
- *,
375
- x0: float = 0.0,
376
- x1: float = 1.0,
377
- ) -> None:
378
- """
379
- Args:
380
- kmax: The maximum Fourier degree for the underlying representation.
381
- x0: The left boundary of the interval. Defaults to 0.0.
382
- x1: The right boundary of the interval. Defaults to 1.0.
383
- """
384
- super().__init__(kmax, 0.0, 1.0, x0=x0, x1=x1)
@@ -1,21 +0,0 @@
1
- pygeoinf/__init__.py,sha256=PsUfD2hfc-eoIz6fHSC-Mn3MHflaU07W2mHT6fLYKr0,1076
2
- pygeoinf/direct_sum.py,sha256=_ZMtK8EQ02nWpU0yYSqeEieR3ZHZgIyN8fgo9jqkm5k,15867
3
- pygeoinf/forms.py,sha256=X3vftbjkUKb4m21BDg-Z5umlc1ibAl47ml40i1Fv33E,5815
4
- pygeoinf/forward_problem.py,sha256=4V40qvHN4qAu8CXsxGAuJ5F9oukqXbXbGCARFpBYaD0,9546
5
- pygeoinf/gaussian_measure.py,sha256=UEFViRtLYU730U4upV33KB_ksg78tpW0dwGVFgt1hOw,21595
6
- pygeoinf/hilbert_space.py,sha256=m0wbzm2XIITs8KenwrNrgvRKilsf-KrX6SR0ylKyhLg,14342
7
- pygeoinf/inversion.py,sha256=0O1ZVZSyvHaS63XovMtAmhwQwnl0OBfiSD9JwEt95bg,2560
8
- pygeoinf/linear_bayesian.py,sha256=9PQWq3nzNN2KR5fDbDqE8rv0mszIe53_7M40jeD_26U,8374
9
- pygeoinf/linear_optimisation.py,sha256=C2kA1QZC4BlSF0txfpr2_c3A2lCAIklijqY8sgs5OZE,10316
10
- pygeoinf/linear_solvers.py,sha256=ieRscHbI7On_4E0h_U-CAKOGlMYKwB2W-dR-Gh4Iwp4,17481
11
- pygeoinf/operators.py,sha256=SZRmzUL6KiJA1fh5PoWqLwyDYWDo5ooJ_CbMD3TCvFA,24912
12
- pygeoinf/random_matrix.py,sha256=d6apC9Sd5ud-QgLnvvXaURdar3LiYJPwz-CYbDojH54,7545
13
- pygeoinf/symmetric_space/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- pygeoinf/symmetric_space/circle.py,sha256=TWjQwX42EvZ4-zOIJKATo8lVg0YbXRaQgxVzj6xXYlM,12467
15
- pygeoinf/symmetric_space/line.py,sha256=FoZL5qVEVWzOkWwhbmwsg3D_0ceX9Nj8L8OJXQevtr4,13203
16
- pygeoinf/symmetric_space/sphere.py,sha256=YezUyB3TfsY6Bb0M6hVVXSA00HDkcyO8I-FMy_I0lOA,24215
17
- pygeoinf/symmetric_space/symmetric_space.py,sha256=E-8lXZmW5WWaHgX-YTu--NwV6dXP3M7hIP3J3H6uv3U,9067
18
- pygeoinf-1.0.8.dist-info/LICENSE,sha256=GrTQnKJemVi69FSbHprq60KN0OJGsOSR-joQoTq-oD8,1501
19
- pygeoinf-1.0.8.dist-info/METADATA,sha256=9kra1ONFxBefYI7LkLzHM2NYkax8RwhHtrGbJT3lZY4,14375
20
- pygeoinf-1.0.8.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
21
- pygeoinf-1.0.8.dist-info/RECORD,,