pygeoinf 1.0.9__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 +101 -75
- 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 -448
- pygeoinf/symmetric_space/symmetric_space.py +330 -142
- {pygeoinf-1.0.9.dist-info → pygeoinf-1.1.0.dist-info}/METADATA +1 -2
- pygeoinf-1.1.0.dist-info/RECORD +20 -0
- pygeoinf/forms.py +0 -128
- pygeoinf/symmetric_space/line.py +0 -384
- pygeoinf-1.0.9.dist-info/RECORD +0 -21
- {pygeoinf-1.0.9.dist-info → pygeoinf-1.1.0.dist-info}/LICENSE +0 -0
- {pygeoinf-1.0.9.dist-info → pygeoinf-1.1.0.dist-info}/WHEEL +0 -0
pygeoinf/hilbert_space.py
CHANGED
|
@@ -1,140 +1,238 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Defines the foundational abstractions for working with Hilbert spaces.
|
|
3
|
+
|
|
4
|
+
This module provides the core `HilbertSpace` abstract base class (ABC), which
|
|
5
|
+
serves as a mathematical abstraction for real vector spaces equipped with an
|
|
6
|
+
inner product. The design separates abstract vector operations from their
|
|
7
|
+
concrete representations (e.g., as NumPy arrays), allowing for generic and
|
|
8
|
+
reusable implementations of linear operators and algorithms.
|
|
9
|
+
|
|
10
|
+
The inner product of a space is defined by its Riesz representation map
|
|
11
|
+
(`to_dual` and `from_dual` methods), which connects the space to its dual.
|
|
12
|
+
Concrete subclasses must implement the abstract methods to define a specific
|
|
13
|
+
type of space.
|
|
14
|
+
|
|
15
|
+
Key Classes
|
|
16
|
+
-----------
|
|
17
|
+
- `HilbertSpace`: The primary ABC defining the interface for all Hilbert spaces.
|
|
18
|
+
- `DualHilbertSpace`: A wrapper class representing the dual of a Hilbert space.
|
|
19
|
+
- `HilbertModule`: An ABC for Hilbert spaces that also support vector multiplication.
|
|
20
|
+
- `EuclideanSpace`: A concrete implementation for R^n using NumPy arrays.
|
|
21
|
+
- `MassWeightedHilbertSpace`: A space whose inner product is weighted by a
|
|
22
|
+
mass operator relative to an underlying space.
|
|
3
23
|
"""
|
|
4
24
|
|
|
5
25
|
from __future__ import annotations
|
|
6
|
-
|
|
26
|
+
|
|
27
|
+
from abc import ABC, abstractmethod
|
|
28
|
+
from typing import (
|
|
29
|
+
TypeVar,
|
|
30
|
+
List,
|
|
31
|
+
Optional,
|
|
32
|
+
Any,
|
|
33
|
+
TYPE_CHECKING,
|
|
34
|
+
final,
|
|
35
|
+
)
|
|
7
36
|
|
|
8
37
|
import numpy as np
|
|
9
38
|
|
|
10
39
|
# This block only runs for type checkers, not at runtime
|
|
11
40
|
if TYPE_CHECKING:
|
|
12
41
|
from .operators import LinearOperator
|
|
13
|
-
from .
|
|
14
|
-
|
|
42
|
+
from .linear_forms import LinearForm
|
|
15
43
|
|
|
16
44
|
# Define a generic type for vectors in a Hilbert space
|
|
17
|
-
|
|
45
|
+
Vector = TypeVar("Vector")
|
|
18
46
|
|
|
19
47
|
|
|
20
|
-
class HilbertSpace:
|
|
48
|
+
class HilbertSpace(ABC):
|
|
21
49
|
"""
|
|
22
|
-
|
|
50
|
+
An abstract base class for real Hilbert spaces.
|
|
23
51
|
|
|
24
|
-
This class provides a mathematical abstraction for vector
|
|
25
|
-
with an inner product. It
|
|
26
|
-
their concrete representation (e.g., as
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
implementations for converting vectors to/from their component
|
|
30
|
-
representations, as well as the inner product and Riesz maps.
|
|
52
|
+
This class provides a mathematical abstraction for a vector space equipped
|
|
53
|
+
with an inner product. It defines a formal interface that separates
|
|
54
|
+
abstract vector operations from their concrete representation (e.g., as
|
|
55
|
+
NumPy arrays). Subclasses must implement all abstract methods to be
|
|
56
|
+
instantiable.
|
|
31
57
|
"""
|
|
32
58
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
subtract: Optional[Callable[[T_vec, T_vec], T_vec]] = None,
|
|
45
|
-
multiply: Optional[Callable[[float, T_vec], T_vec]] = None,
|
|
46
|
-
ax: Optional[Callable[[float, T_vec], None]] = None,
|
|
47
|
-
axpy: Optional[Callable[[float, T_vec, T_vec], None]] = None,
|
|
48
|
-
copy: Optional[Callable[[T_vec], T_vec]] = None,
|
|
49
|
-
vector_multiply: Optional[Callable[[T_vec, T_vec], T_vec]] = None,
|
|
50
|
-
base: Optional[HilbertSpace] = None,
|
|
51
|
-
):
|
|
59
|
+
# ------------------------------------------------------------------- #
|
|
60
|
+
# Abstract methods that must be provided #
|
|
61
|
+
# ------------------------------------------------------------------- #
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def dim(self) -> int:
|
|
66
|
+
"""The finite dimension of the space."""
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def to_dual(self, x: Vector) -> Any:
|
|
52
70
|
"""
|
|
53
|
-
|
|
71
|
+
Maps a vector to its canonical dual vector (a linear functional).
|
|
72
|
+
|
|
73
|
+
This method, along with `from_dual`, defines the Riesz representation
|
|
74
|
+
map and implicitly defines the inner product of the space.
|
|
54
75
|
|
|
55
76
|
Args:
|
|
56
|
-
|
|
57
|
-
to_components (callable): A function mapping vectors to their
|
|
58
|
-
NumPy component arrays.
|
|
59
|
-
from_components (callable): A function mapping NumPy component
|
|
60
|
-
arrays back to vectors.
|
|
61
|
-
inner_product (callable): The inner product defined on the space.
|
|
62
|
-
to_dual (callable): The Riesz map from the space to its dual.
|
|
63
|
-
from_dual (callable): The Riesz map from the dual space back
|
|
64
|
-
to the primal space.
|
|
65
|
-
add (callable, optional): Custom vector addition.
|
|
66
|
-
subtract (callable, optional): Custom vector subtraction.
|
|
67
|
-
multiply (callable, optional): Custom scalar multiplication.
|
|
68
|
-
ax (callable, optional): Custom in-place scaling x := a*x.
|
|
69
|
-
axpy (callable, optional): Custom in-place operation y := a*x + y.
|
|
70
|
-
copy (callable, optional): Custom deep copy for vectors.
|
|
71
|
-
base (HilbertSpace, optional): Used internally for creating
|
|
72
|
-
dual spaces. Should not be set by the user.
|
|
73
|
-
"""
|
|
74
|
-
self._dim: int = dim
|
|
75
|
-
self.__to_components: Callable[[T_vec], np.ndarray] = to_components
|
|
76
|
-
self.__from_components: Callable[[np.ndarray], T_vec] = from_components
|
|
77
|
-
self.__inner_product: Callable[[T_vec, T_vec], float] = inner_product
|
|
78
|
-
self.__from_dual: Callable[[Any], T_vec] = from_dual
|
|
79
|
-
self.__to_dual: Callable[[T_vec], Any] = to_dual
|
|
80
|
-
self._base: Optional[HilbertSpace] = base
|
|
81
|
-
self._add: Callable[[T_vec, T_vec], T_vec] = self.__add if add is None else add
|
|
82
|
-
self._subtract: Callable[[T_vec, T_vec], T_vec] = (
|
|
83
|
-
self.__subtract if subtract is None else subtract
|
|
84
|
-
)
|
|
85
|
-
self._multiply: Callable[[float, T_vec], T_vec] = (
|
|
86
|
-
self.__multiply if multiply is None else multiply
|
|
87
|
-
)
|
|
88
|
-
self._ax: Callable[[float, T_vec], None] = self.__ax if ax is None else ax
|
|
89
|
-
self._axpy: Callable[[float, T_vec, T_vec], None] = (
|
|
90
|
-
self.__axpy if axpy is None else axpy
|
|
91
|
-
)
|
|
92
|
-
self._copy: Callable[[T_vec], T_vec] = self.__copy if copy is None else copy
|
|
93
|
-
self._vector_multiply: Optional[Callable[[T_vec, T_vec], T_vec]] = (
|
|
94
|
-
vector_multiply
|
|
95
|
-
)
|
|
77
|
+
x: A vector in the primal space.
|
|
96
78
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"""
|
|
100
|
-
return self._dim
|
|
79
|
+
Returns:
|
|
80
|
+
The corresponding vector in the dual space.
|
|
81
|
+
"""
|
|
101
82
|
|
|
102
|
-
@
|
|
103
|
-
def
|
|
104
|
-
"""
|
|
105
|
-
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def from_dual(self, xp: Any) -> Vector:
|
|
85
|
+
"""
|
|
86
|
+
Maps a dual vector back to its representative in the primal space.
|
|
87
|
+
|
|
88
|
+
This is the inverse of the Riesz representation map defined by `to_dual`.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
xp: A vector in the dual space.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The corresponding vector in the primal space.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def to_components(self, x: Vector) -> np.ndarray:
|
|
99
|
+
"""
|
|
100
|
+
Maps a vector to its representation as a NumPy component array.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
x: A vector in the space.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The components of the vector as a NumPy array.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def from_components(self, c: np.ndarray) -> Vector:
|
|
111
|
+
"""
|
|
112
|
+
Maps a NumPy component array back to a vector in the space.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
c: The components of the vector as a NumPy array.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The corresponding vector in the space.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------- #
|
|
122
|
+
# Default implementations that can be overridden #
|
|
123
|
+
# ------------------------------------------------------------------- #
|
|
106
124
|
|
|
107
125
|
@property
|
|
108
126
|
def dual(self) -> HilbertSpace:
|
|
109
127
|
"""
|
|
110
|
-
The dual of
|
|
128
|
+
The dual of this Hilbert space.
|
|
111
129
|
|
|
112
130
|
The dual space is the space of all continuous linear functionals
|
|
113
|
-
that map vectors from
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
self.dim,
|
|
118
|
-
self._dual_to_components,
|
|
119
|
-
self._dual_from_components,
|
|
120
|
-
self._dual_inner_product,
|
|
121
|
-
self.from_dual,
|
|
122
|
-
self.to_dual,
|
|
123
|
-
base=self,
|
|
124
|
-
)
|
|
125
|
-
else:
|
|
126
|
-
return self._base
|
|
131
|
+
(i.e., `LinearForm` objects) that map vectors from this space to
|
|
132
|
+
real numbers. This implementation returns a `DualHilbertSpace` wrapper.
|
|
133
|
+
"""
|
|
134
|
+
return DualHilbertSpace(self)
|
|
127
135
|
|
|
128
136
|
@property
|
|
129
|
-
def zero(self) ->
|
|
130
|
-
"""
|
|
137
|
+
def zero(self) -> Vector:
|
|
138
|
+
"""The zero vector (additive identity) of the space."""
|
|
131
139
|
return self.from_components(np.zeros((self.dim)))
|
|
132
140
|
|
|
141
|
+
def is_element(self, x: Any) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Checks if an object is a valid element of the space.
|
|
144
|
+
|
|
145
|
+
Note: The default implementation checks the object's type against the
|
|
146
|
+
type of the `zero` vector. This may not be robust for all vector
|
|
147
|
+
representations and can be overridden if needed.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
x: The object to check.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
True if the object is an element of the space, False otherwise.
|
|
154
|
+
"""
|
|
155
|
+
return isinstance(x, type(self.zero))
|
|
156
|
+
|
|
157
|
+
def duality_product(self, xp: LinearForm, x: Vector) -> float:
|
|
158
|
+
"""
|
|
159
|
+
Computes the duality product <xp, x>.
|
|
160
|
+
|
|
161
|
+
This evaluates the linear functional `xp` (an element of the dual space)
|
|
162
|
+
at the vector `x` (an element of the primal space).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
xp: The linear functional from the dual space.
|
|
166
|
+
x: The vector from the primal space.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The result of the evaluation xp(x).
|
|
170
|
+
"""
|
|
171
|
+
return xp(x)
|
|
172
|
+
|
|
173
|
+
def add(self, x: Vector, y: Vector) -> Vector:
|
|
174
|
+
"""Computes the sum of two vectors. Defaults to `x + y`."""
|
|
175
|
+
return x + y
|
|
176
|
+
|
|
177
|
+
def subtract(self, x: Vector, y: Vector) -> Vector:
|
|
178
|
+
"""Computes the difference of two vectors. Defaults to `x - y`."""
|
|
179
|
+
return x - y
|
|
180
|
+
|
|
181
|
+
def multiply(self, a: float, x: Vector) -> Vector:
|
|
182
|
+
"""Computes scalar multiplication. Defaults to `a * x`."""
|
|
183
|
+
return a * x
|
|
184
|
+
|
|
185
|
+
def negative(self, x: Vector) -> Vector:
|
|
186
|
+
"""Computes the additive inverse of a vector. Defaults to `-1 * x`."""
|
|
187
|
+
return -1 * x
|
|
188
|
+
|
|
189
|
+
def ax(self, a: float, x: Vector) -> None:
|
|
190
|
+
"""Performs in-place scaling `x := a*x`. Defaults to `x *= a`."""
|
|
191
|
+
x *= a
|
|
192
|
+
|
|
193
|
+
def axpy(self, a: float, x: Vector, y: Vector) -> None:
|
|
194
|
+
"""Performs in-place operation `y := y + a*x`. Defaults to `y += a*x`."""
|
|
195
|
+
y += a * x
|
|
196
|
+
|
|
197
|
+
def copy(self, x: Vector) -> Vector:
|
|
198
|
+
"""Returns a deep copy of a vector. Defaults to `x.copy()`."""
|
|
199
|
+
return x.copy()
|
|
200
|
+
|
|
201
|
+
def random(self) -> Vector:
|
|
202
|
+
"""
|
|
203
|
+
Generates a random vector from the space.
|
|
204
|
+
|
|
205
|
+
The vector's components are drawn from a standard normal distribution.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
A new random vector.
|
|
209
|
+
"""
|
|
210
|
+
return self.from_components(np.random.randn(self.dim))
|
|
211
|
+
|
|
212
|
+
def __eq__(self, other: object) -> bool:
|
|
213
|
+
"""
|
|
214
|
+
Defines equality between Hilbert spaces.
|
|
215
|
+
|
|
216
|
+
For dual spaces, equality is determined by the equality of their
|
|
217
|
+
underlying primal spaces. For non-dual spaces, this is not implemented,
|
|
218
|
+
requiring concrete subclasses to define a meaningful comparison.
|
|
219
|
+
"""
|
|
220
|
+
if isinstance(self, DualHilbertSpace):
|
|
221
|
+
return (
|
|
222
|
+
isinstance(other, DualHilbertSpace)
|
|
223
|
+
and self.underlying_space == other.underlying_space
|
|
224
|
+
)
|
|
225
|
+
return NotImplemented
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------- #
|
|
228
|
+
# Final (Non-Overridable) Methods #
|
|
229
|
+
# ------------------------------------------------------------------- #
|
|
230
|
+
|
|
231
|
+
@final
|
|
133
232
|
@property
|
|
134
|
-
def coordinate_inclusion(self) ->
|
|
233
|
+
def coordinate_inclusion(self) -> LinearOperator:
|
|
135
234
|
"""
|
|
136
|
-
|
|
137
|
-
in this Hilbert space.
|
|
235
|
+
The linear operator mapping R^n component vectors into this space.
|
|
138
236
|
"""
|
|
139
237
|
from .operators import LinearOperator
|
|
140
238
|
|
|
@@ -144,7 +242,7 @@ class HilbertSpace:
|
|
|
144
242
|
cp = self.dual.to_components(xp)
|
|
145
243
|
return domain.to_dual(cp)
|
|
146
244
|
|
|
147
|
-
def adjoint_mapping(y:
|
|
245
|
+
def adjoint_mapping(y: Vector) -> np.ndarray:
|
|
148
246
|
yp = self.to_dual(y)
|
|
149
247
|
return self.dual.to_components(yp)
|
|
150
248
|
|
|
@@ -156,11 +254,11 @@ class HilbertSpace:
|
|
|
156
254
|
adjoint_mapping=adjoint_mapping,
|
|
157
255
|
)
|
|
158
256
|
|
|
257
|
+
@final
|
|
159
258
|
@property
|
|
160
|
-
def coordinate_projection(self) ->
|
|
259
|
+
def coordinate_projection(self) -> LinearOperator:
|
|
161
260
|
"""
|
|
162
|
-
|
|
163
|
-
coordinate vectors in R^n.
|
|
261
|
+
The linear operator projecting vectors from this space to R^n.
|
|
164
262
|
"""
|
|
165
263
|
from .operators import LinearOperator
|
|
166
264
|
|
|
@@ -170,7 +268,7 @@ class HilbertSpace:
|
|
|
170
268
|
c = codomain.from_dual(cp)
|
|
171
269
|
return self.dual.from_components(c)
|
|
172
270
|
|
|
173
|
-
def adjoint_mapping(c: np.ndarray) ->
|
|
271
|
+
def adjoint_mapping(c: np.ndarray) -> Vector:
|
|
174
272
|
xp = self.dual.from_components(c)
|
|
175
273
|
return self.from_dual(xp)
|
|
176
274
|
|
|
@@ -182,138 +280,131 @@ class HilbertSpace:
|
|
|
182
280
|
adjoint_mapping=adjoint_mapping,
|
|
183
281
|
)
|
|
184
282
|
|
|
283
|
+
@final
|
|
185
284
|
@property
|
|
186
|
-
def riesz(self) ->
|
|
187
|
-
"""
|
|
188
|
-
Returns the Riesz map (dual to primal) as a LinearOperator.
|
|
189
|
-
"""
|
|
285
|
+
def riesz(self) -> LinearOperator:
|
|
286
|
+
"""The Riesz map (dual to primal) as a `LinearOperator`."""
|
|
190
287
|
from .operators import LinearOperator
|
|
191
288
|
|
|
192
289
|
return LinearOperator.self_dual(self.dual, self.from_dual)
|
|
193
290
|
|
|
291
|
+
@final
|
|
194
292
|
@property
|
|
195
|
-
def inverse_riesz(self) ->
|
|
196
|
-
"""
|
|
197
|
-
Returns the inverse Riesz map (primal to dual) as a LinearOperator.
|
|
198
|
-
"""
|
|
293
|
+
def inverse_riesz(self) -> LinearOperator:
|
|
294
|
+
"""The inverse Riesz map (primal to dual) as a `LinearOperator`."""
|
|
199
295
|
from .operators import LinearOperator
|
|
200
296
|
|
|
201
297
|
return LinearOperator.self_dual(self, self.to_dual)
|
|
202
298
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
299
|
+
@final
|
|
300
|
+
def inner_product(self, x1: Vector, x2: Vector) -> float:
|
|
301
|
+
"""
|
|
302
|
+
Computes the inner product of two vectors, `(x1, x2)`.
|
|
303
|
+
|
|
304
|
+
This is defined via the duality product as `<R(x1), x2>`, where `R` is
|
|
305
|
+
the Riesz map (`to_dual`).
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
x1: The first vector.
|
|
309
|
+
x2: The second vector.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
The inner product as a float.
|
|
313
|
+
"""
|
|
314
|
+
return self.duality_product(self.to_dual(x1), x2)
|
|
315
|
+
|
|
316
|
+
@final
|
|
317
|
+
def squared_norm(self, x: Vector) -> float:
|
|
318
|
+
"""
|
|
319
|
+
Computes the squared norm of a vector, `||x||^2`.
|
|
206
320
|
|
|
207
|
-
|
|
208
|
-
|
|
321
|
+
Args:
|
|
322
|
+
x: The vector.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
The squared norm of the vector.
|
|
326
|
+
"""
|
|
209
327
|
return self.inner_product(x, x)
|
|
210
328
|
|
|
211
|
-
|
|
212
|
-
|
|
329
|
+
@final
|
|
330
|
+
def norm(self, x: Vector) -> float:
|
|
331
|
+
"""
|
|
332
|
+
Computes the norm of a vector, `||x||`.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
x: The vector.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
The norm of the vector.
|
|
339
|
+
"""
|
|
213
340
|
return np.sqrt(self.squared_norm(x))
|
|
214
341
|
|
|
215
|
-
|
|
342
|
+
@final
|
|
343
|
+
def gram_schmidt(self, vectors: List[Vector]) -> List[Vector]:
|
|
216
344
|
"""
|
|
217
345
|
Orthonormalizes a list of vectors using the Gram-Schmidt process.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
vectors: A list of linearly independent vectors.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
A list of orthonormalized vectors spanning the same subspace.
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
ValueError: If not all items in the list are elements of the space.
|
|
218
355
|
"""
|
|
219
356
|
if not all(self.is_element(vector) for vector in vectors):
|
|
220
357
|
raise ValueError("Not all vectors are elements of the space")
|
|
221
358
|
|
|
222
|
-
orthonormalised_vectors: List[
|
|
359
|
+
orthonormalised_vectors: List[Vector] = []
|
|
223
360
|
for i, vector in enumerate(vectors):
|
|
224
361
|
vec_copy = self.copy(vector)
|
|
225
362
|
for j in range(i):
|
|
226
363
|
product = self.inner_product(vec_copy, orthonormalised_vectors[j])
|
|
227
364
|
self.axpy(-product, orthonormalised_vectors[j], vec_copy)
|
|
228
365
|
norm = self.norm(vec_copy)
|
|
366
|
+
if norm < 1e-12:
|
|
367
|
+
raise ValueError("Vectors are not linearly independent.")
|
|
229
368
|
self.ax(1 / norm, vec_copy)
|
|
230
369
|
orthonormalised_vectors.append(vec_copy)
|
|
231
370
|
|
|
232
371
|
return orthonormalised_vectors
|
|
233
372
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return self.__to_dual(x)
|
|
237
|
-
|
|
238
|
-
def from_dual(self, xp: Any) -> T_vec:
|
|
239
|
-
"""Maps a dual vector to its representative in the primal space."""
|
|
240
|
-
return self.__from_dual(xp)
|
|
241
|
-
|
|
242
|
-
def _dual_inner_product(self, xp1: Any, xp2: Any) -> float:
|
|
243
|
-
return self.inner_product(self.from_dual(xp1), self.from_dual(xp2))
|
|
244
|
-
|
|
245
|
-
def is_element(self, x: Any) -> bool:
|
|
373
|
+
@final
|
|
374
|
+
def basis_vector(self, i: int) -> Vector:
|
|
246
375
|
"""
|
|
247
|
-
|
|
376
|
+
Returns the i-th standard basis vector.
|
|
248
377
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"""
|
|
252
|
-
return isinstance(x, type(self.zero))
|
|
378
|
+
This is the vector whose component array is all zeros except for a one
|
|
379
|
+
at index `i`.
|
|
253
380
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return self._add(x, y)
|
|
257
|
-
|
|
258
|
-
def subtract(self, x: T_vec, y: T_vec) -> T_vec:
|
|
259
|
-
"""Subtracts two vectors."""
|
|
260
|
-
return self._subtract(x, y)
|
|
261
|
-
|
|
262
|
-
def multiply(self, a: float, x: T_vec) -> T_vec:
|
|
263
|
-
"""Performs scalar multiplication, returning a new vector."""
|
|
264
|
-
return self._multiply(a, x)
|
|
265
|
-
|
|
266
|
-
def negative(self, x: T_vec) -> T_vec:
|
|
267
|
-
"""Returns the additive inverse of a vector."""
|
|
268
|
-
return self.multiply(-1.0, x)
|
|
269
|
-
|
|
270
|
-
def ax(self, a: float, x: T_vec) -> None:
|
|
271
|
-
"""Performs the in-place scaling operation x := a*x."""
|
|
272
|
-
self._ax(a, x)
|
|
273
|
-
|
|
274
|
-
def axpy(self, a: float, x: T_vec, y: T_vec) -> None:
|
|
275
|
-
"""Performs the in-place vector operation y := y + a*x."""
|
|
276
|
-
self._axpy(a, x, y)
|
|
277
|
-
|
|
278
|
-
def copy(self, x: T_vec) -> T_vec:
|
|
279
|
-
"""Returns a deep copy of a vector."""
|
|
280
|
-
return self._copy(x)
|
|
381
|
+
Args:
|
|
382
|
+
i: The index of the basis vector.
|
|
281
383
|
|
|
282
|
-
|
|
384
|
+
Returns:
|
|
385
|
+
The i-th basis vector.
|
|
283
386
|
"""
|
|
284
|
-
Returns the product of two elements of the space, if defined.
|
|
285
|
-
"""
|
|
286
|
-
if self._vector_multiply is None:
|
|
287
|
-
raise NotImplementedError(
|
|
288
|
-
"Vector multiplication not defined on this space."
|
|
289
|
-
)
|
|
290
|
-
return self._vector_multiply(x1, x2)
|
|
291
|
-
|
|
292
|
-
def to_components(self, x: T_vec) -> np.ndarray:
|
|
293
|
-
"""Maps a vector to its NumPy component array."""
|
|
294
|
-
return self.__to_components(x)
|
|
295
|
-
|
|
296
|
-
def from_components(self, c: np.ndarray) -> T_vec:
|
|
297
|
-
"""Maps a NumPy component array to a vector."""
|
|
298
|
-
return self.__from_components(c)
|
|
299
|
-
|
|
300
|
-
def basis_vector(self, i: int) -> T_vec:
|
|
301
|
-
"""Returns the i-th standard basis vector."""
|
|
302
387
|
c = np.zeros(self.dim)
|
|
303
388
|
c[i] = 1
|
|
304
389
|
return self.from_components(c)
|
|
305
390
|
|
|
306
|
-
|
|
391
|
+
@final
|
|
392
|
+
def sample_expectation(self, vectors: List[Vector]) -> Vector:
|
|
307
393
|
"""
|
|
308
|
-
|
|
394
|
+
Computes the sample mean of a list of vectors.
|
|
309
395
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
396
|
+
Args:
|
|
397
|
+
vectors: A list of vectors from the space.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
The sample mean (average) vector.
|
|
313
401
|
|
|
314
|
-
|
|
315
|
-
|
|
402
|
+
Raises:
|
|
403
|
+
TypeError: If not all items in the list are elements of the space.
|
|
404
|
+
"""
|
|
316
405
|
n = len(vectors)
|
|
406
|
+
if not n > 0:
|
|
407
|
+
raise ValueError("Cannot compute expectation of an empty list.")
|
|
317
408
|
if not all(self.is_element(x) for x in vectors):
|
|
318
409
|
raise TypeError("Not all items in list are elements of the space.")
|
|
319
410
|
xbar = self.zero
|
|
@@ -321,25 +412,29 @@ class HilbertSpace:
|
|
|
321
412
|
self.axpy(1 / n, x, xbar)
|
|
322
413
|
return xbar
|
|
323
414
|
|
|
324
|
-
|
|
325
|
-
|
|
415
|
+
@final
|
|
416
|
+
def identity_operator(self) -> LinearOperator:
|
|
417
|
+
"""Returns the identity operator `I` on the space."""
|
|
326
418
|
from .operators import LinearOperator
|
|
327
419
|
|
|
328
420
|
return LinearOperator(
|
|
329
421
|
self,
|
|
330
422
|
self,
|
|
331
423
|
lambda x: x,
|
|
332
|
-
dual_mapping=lambda yp: yp,
|
|
333
424
|
adjoint_mapping=lambda y: y,
|
|
334
425
|
)
|
|
335
426
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
) -> "LinearOperator":
|
|
427
|
+
@final
|
|
428
|
+
def zero_operator(self, codomain: Optional[HilbertSpace] = None) -> LinearOperator:
|
|
339
429
|
"""
|
|
340
|
-
Returns the zero operator from this space to a codomain.
|
|
430
|
+
Returns the zero operator `0` from this space to a codomain.
|
|
341
431
|
|
|
342
|
-
|
|
432
|
+
Args:
|
|
433
|
+
codomain: The target space of the operator. If None, the operator
|
|
434
|
+
maps to this space itself.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
The zero linear operator.
|
|
343
438
|
"""
|
|
344
439
|
from .operators import LinearOperator
|
|
345
440
|
|
|
@@ -352,65 +447,246 @@ class HilbertSpace:
|
|
|
352
447
|
adjoint_mapping=lambda y: self.zero,
|
|
353
448
|
)
|
|
354
449
|
|
|
355
|
-
def _dual_to_components(self, xp: "LinearForm") -> np.ndarray:
|
|
356
|
-
return xp.components
|
|
357
450
|
|
|
358
|
-
|
|
359
|
-
|
|
451
|
+
class DualHilbertSpace(HilbertSpace):
|
|
452
|
+
"""
|
|
453
|
+
A wrapper class representing the dual of a `HilbertSpace`.
|
|
454
|
+
|
|
455
|
+
An element of a dual space is a continuous linear functional, represented
|
|
456
|
+
in this library by the `LinearForm` class. This wrapper provides a full
|
|
457
|
+
`HilbertSpace` interface for these `LinearForm` objects, allowing them to be
|
|
458
|
+
treated as vectors in their own right.
|
|
459
|
+
"""
|
|
360
460
|
|
|
361
|
-
|
|
461
|
+
def __init__(self, space: HilbertSpace):
|
|
462
|
+
"""
|
|
463
|
+
Args:
|
|
464
|
+
space: The primal space from which to form the dual.
|
|
465
|
+
"""
|
|
466
|
+
self._underlying_space = space
|
|
362
467
|
|
|
363
|
-
|
|
364
|
-
|
|
468
|
+
@property
|
|
469
|
+
def underlying_space(self) -> HilbertSpace:
|
|
470
|
+
"""The primal `HilbertSpace` of which this is the dual."""
|
|
471
|
+
return self._underlying_space
|
|
365
472
|
|
|
366
|
-
|
|
367
|
-
|
|
473
|
+
@property
|
|
474
|
+
def dim(self) -> int:
|
|
475
|
+
"""The dimension of the dual space."""
|
|
476
|
+
return self._underlying_space.dim
|
|
368
477
|
|
|
369
|
-
|
|
370
|
-
|
|
478
|
+
@property
|
|
479
|
+
def dual(self) -> HilbertSpace:
|
|
480
|
+
"""The dual of the dual space, which is the original primal space."""
|
|
481
|
+
return self._underlying_space
|
|
371
482
|
|
|
372
|
-
def
|
|
373
|
-
|
|
483
|
+
def to_dual(self, x: LinearForm) -> Any:
|
|
484
|
+
"""Maps a dual vector back to its representative in the primal space."""
|
|
485
|
+
return self._underlying_space.from_dual(x)
|
|
374
486
|
|
|
375
|
-
def
|
|
376
|
-
|
|
487
|
+
def from_dual(self, xp: Vector) -> LinearForm:
|
|
488
|
+
"""Maps a primal vector to its corresponding dual `LinearForm`."""
|
|
489
|
+
return self._underlying_space.to_dual(xp)
|
|
490
|
+
|
|
491
|
+
def to_components(self, x: LinearForm) -> np.ndarray:
|
|
492
|
+
"""Maps a `LinearForm` to its NumPy component array."""
|
|
493
|
+
return x.components
|
|
494
|
+
|
|
495
|
+
def from_components(self, c: np.ndarray) -> LinearForm:
|
|
496
|
+
"""Creates a `LinearForm` from a NumPy component array."""
|
|
497
|
+
from .linear_forms import LinearForm
|
|
498
|
+
|
|
499
|
+
return LinearForm(self._underlying_space, components=c)
|
|
500
|
+
|
|
501
|
+
@final
|
|
502
|
+
def duality_product(self, xp: LinearForm, x: Vector) -> float:
|
|
503
|
+
"""
|
|
504
|
+
Computes the duality product <x, xp>.
|
|
505
|
+
|
|
506
|
+
In this context, `x` is from the primal space and `xp` is the dual
|
|
507
|
+
vector (a `LinearForm`). This is unconventional but maintains the
|
|
508
|
+
method signature; it evaluates `x(xp)`.
|
|
509
|
+
"""
|
|
510
|
+
return x(xp)
|
|
377
511
|
|
|
378
|
-
|
|
379
|
-
|
|
512
|
+
|
|
513
|
+
class HilbertModule(HilbertSpace, ABC):
|
|
514
|
+
"""
|
|
515
|
+
An ABC for a `HilbertSpace` where vector multiplication is defined.
|
|
516
|
+
|
|
517
|
+
This acts as a "mixin" interface, adding the `vector_multiply` requirement
|
|
518
|
+
to the `HilbertSpace` contract.
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
@abstractmethod
|
|
522
|
+
def vector_multiply(self, x1: Vector, x2: Vector) -> Vector:
|
|
523
|
+
"""
|
|
524
|
+
Computes the product of two vectors.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
x1: The first vector.
|
|
528
|
+
x2: The second vector.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
The product of the two vectors.
|
|
532
|
+
"""
|
|
380
533
|
|
|
381
534
|
|
|
382
535
|
class EuclideanSpace(HilbertSpace):
|
|
383
536
|
"""
|
|
384
537
|
An n-dimensional Euclidean space, R^n.
|
|
385
538
|
|
|
386
|
-
This is a concrete
|
|
387
|
-
|
|
539
|
+
This is a concrete `HilbertSpace` where vectors are represented directly by
|
|
540
|
+
NumPy arrays, and the inner product is the standard dot product.
|
|
388
541
|
"""
|
|
389
542
|
|
|
390
|
-
def __init__(self, dim: int)
|
|
543
|
+
def __init__(self, dim: int):
|
|
391
544
|
"""
|
|
392
545
|
Args:
|
|
393
|
-
dim
|
|
546
|
+
dim: The dimension of the space.
|
|
394
547
|
"""
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
548
|
+
if dim < 1:
|
|
549
|
+
raise ValueError("Dimension must be a positive integer.")
|
|
550
|
+
self._dim = dim
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def dim(self) -> int:
|
|
554
|
+
"""The dimension of the space."""
|
|
555
|
+
return self._dim
|
|
403
556
|
|
|
404
|
-
def
|
|
405
|
-
|
|
557
|
+
def to_components(self, x: np.ndarray) -> np.ndarray:
|
|
558
|
+
"""Returns the vector itself, as it is already a component array."""
|
|
559
|
+
return x
|
|
406
560
|
|
|
407
|
-
def
|
|
408
|
-
|
|
561
|
+
def from_components(self, c: np.ndarray) -> np.ndarray:
|
|
562
|
+
"""Returns the component array itself, as it is the vector."""
|
|
563
|
+
return c
|
|
409
564
|
|
|
410
|
-
def
|
|
411
|
-
|
|
412
|
-
|
|
565
|
+
def to_dual(self, x: np.ndarray) -> "LinearForm":
|
|
566
|
+
"""Maps a vector `x` to a `LinearForm` with the same components."""
|
|
567
|
+
from .linear_forms import LinearForm
|
|
413
568
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
569
|
+
return LinearForm(self, components=x)
|
|
570
|
+
|
|
571
|
+
def from_dual(self, xp: "LinearForm") -> np.ndarray:
|
|
572
|
+
"""Maps a `LinearForm` back to a vector via its components."""
|
|
573
|
+
return self.dual.to_components(xp)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class MassWeightedHilbertSpace(HilbertSpace):
|
|
577
|
+
"""
|
|
578
|
+
A Hilbert space with an inner product weighted by a mass operator.
|
|
579
|
+
|
|
580
|
+
This class wraps an existing `HilbertSpace` (let's call it X) and defines a new
|
|
581
|
+
inner product for a space (Y) as: `(u, v)_Y = (M @ u, v)_X`, where `M` is a
|
|
582
|
+
self-adjoint, positive-definite mass operator defined on X.
|
|
583
|
+
|
|
584
|
+
This is a common construction in numerical methods like the Finite Element
|
|
585
|
+
Method, where the basis functions are not orthonormal.
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
def __init__(
|
|
589
|
+
self,
|
|
590
|
+
underlying_space: HilbertSpace,
|
|
591
|
+
mass_operator: LinearOperator,
|
|
592
|
+
inverse_mass_operator: LinearOperator,
|
|
593
|
+
):
|
|
594
|
+
"""
|
|
595
|
+
Args:
|
|
596
|
+
underlying_space: The original space (X) on which the inner
|
|
597
|
+
product is defined.
|
|
598
|
+
mass_operator: The self-adjoint, positive-definite mass
|
|
599
|
+
operator (M).
|
|
600
|
+
inverse_mass_operator: The inverse of the mass operator.
|
|
601
|
+
"""
|
|
602
|
+
self._underlying_space = underlying_space
|
|
603
|
+
self._mass_operator = mass_operator
|
|
604
|
+
self._inverse_mass_operator = inverse_mass_operator
|
|
605
|
+
|
|
606
|
+
@property
|
|
607
|
+
def dim(self) -> int:
|
|
608
|
+
"""The dimension of the space."""
|
|
609
|
+
return self._underlying_space.dim
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def underlying_space(self) -> HilbertSpace:
|
|
613
|
+
"""The underlying Hilbert space (X) without mass weighting."""
|
|
614
|
+
return self._underlying_space
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def mass_operator(self) -> LinearOperator:
|
|
618
|
+
"""The mass operator (M) defining the weighted inner product."""
|
|
619
|
+
return self._mass_operator
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def inverse_mass_operator(self) -> LinearOperator:
|
|
623
|
+
"""The inverse of the mass operator."""
|
|
624
|
+
return self._inverse_mass_operator
|
|
625
|
+
|
|
626
|
+
def to_components(self, x: Vector) -> np.ndarray:
|
|
627
|
+
"""Delegates component mapping to the underlying space."""
|
|
628
|
+
return self.underlying_space.to_components(x)
|
|
629
|
+
|
|
630
|
+
def from_components(self, c: np.ndarray) -> Vector:
|
|
631
|
+
"""Delegates vector creation to the underlying space."""
|
|
632
|
+
return self.underlying_space.from_components(c)
|
|
633
|
+
|
|
634
|
+
def to_dual(self, x: Vector) -> "LinearForm":
|
|
635
|
+
"""
|
|
636
|
+
Computes the dual mapping `R_Y(x) = R_X(M x)`.
|
|
637
|
+
"""
|
|
638
|
+
from .linear_forms import LinearForm
|
|
639
|
+
|
|
640
|
+
y = self._mass_operator(x)
|
|
641
|
+
yp = self.underlying_space.to_dual(y)
|
|
642
|
+
return LinearForm(self, components=yp.components)
|
|
643
|
+
|
|
644
|
+
def from_dual(self, xp: "LinearForm") -> Vector:
|
|
645
|
+
"""
|
|
646
|
+
Computes the inverse dual mapping `R_Y^{-1}(xp) = M^{-1} R_X^{-1}(xp)`.
|
|
647
|
+
"""
|
|
648
|
+
# Note: This implementation relies on the from_dual operator of the
|
|
649
|
+
# underlying space not checking the domain of its argument. This is
|
|
650
|
+
# acceptable and avoids an unnecessary copy.
|
|
651
|
+
x = self.underlying_space.from_dual(xp)
|
|
652
|
+
return self._inverse_mass_operator(x)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class MassWeightedHilbertModule(MassWeightedHilbertSpace, HilbertModule):
|
|
656
|
+
"""
|
|
657
|
+
A mass-weighted Hilbert space that also supports vector multiplication.
|
|
658
|
+
|
|
659
|
+
This class inherits the mass-weighted inner product structure and mixes in
|
|
660
|
+
the `HilbertModule` interface, delegating the multiplication operation to
|
|
661
|
+
the underlying space.
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
def __init__(
|
|
665
|
+
self,
|
|
666
|
+
underlying_space: HilbertModule,
|
|
667
|
+
mass_operator: LinearOperator,
|
|
668
|
+
inverse_mass_operator: LinearOperator,
|
|
669
|
+
):
|
|
670
|
+
"""
|
|
671
|
+
Args:
|
|
672
|
+
underlying_space: The original space (X) on which the inner
|
|
673
|
+
product is defined.
|
|
674
|
+
mass_operator: The self-adjoint, positive-definite mass
|
|
675
|
+
operator (M).
|
|
676
|
+
inverse_mass_operator: The inverse of the mass operator.
|
|
677
|
+
"""
|
|
678
|
+
if not isinstance(underlying_space, HilbertModule):
|
|
679
|
+
raise TypeError("Underlying space must be a HilbertModule.")
|
|
680
|
+
|
|
681
|
+
MassWeightedHilbertSpace.__init__(
|
|
682
|
+
self, underlying_space, mass_operator, inverse_mass_operator
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
def vector_multiply(self, x1: Vector, x2: Vector) -> Vector:
|
|
686
|
+
"""
|
|
687
|
+
Computes vector multiplication by delegating to the underlying space.
|
|
688
|
+
|
|
689
|
+
Note: This assumes the underlying space provided during initialization
|
|
690
|
+
is itself an instance of `HilbertModule`.
|
|
691
|
+
"""
|
|
692
|
+
return self.underlying_space.vector_multiply(x1, x2)
|