pygeoinf 1.0.9__py3-none-any.whl → 1.1.1__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/hilbert_space.py CHANGED
@@ -1,140 +1,238 @@
1
1
  """
2
- Module defining the core HilbertSpace and EuclideanSpace classes.
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
- from typing import TypeVar, Callable, List, Optional, Any, TYPE_CHECKING
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 .forms import LinearForm
14
-
42
+ from .linear_forms import LinearForm
15
43
 
16
44
  # Define a generic type for vectors in a Hilbert space
17
- T_vec = TypeVar("T_vec")
45
+ Vector = TypeVar("Vector")
18
46
 
19
47
 
20
- class HilbertSpace:
48
+ class HilbertSpace(ABC):
21
49
  """
22
- A class for real Hilbert spaces.
50
+ An abstract base class for real Hilbert spaces.
23
51
 
24
- This class provides a mathematical abstraction for vector spaces equipped
25
- with an inner product. It separates the abstract vector operations from
26
- their concrete representation (e.g., as NumPy arrays).
27
-
28
- To define an instance, a user must provide the space's dimension and
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
- def __init__(
34
- self,
35
- dim: int,
36
- to_components: Callable[[T_vec], np.ndarray],
37
- from_components: Callable[[np.ndarray], T_vec],
38
- inner_product: Callable[[T_vec, T_vec], float],
39
- to_dual: Callable[[T_vec], Any],
40
- from_dual: Callable[[Any], T_vec],
41
- /,
42
- *,
43
- add: Optional[Callable[[T_vec, T_vec], T_vec]] = None,
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
- Initializes the HilbertSpace.
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
- dim (int): The dimension of the space.
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
- @property
98
- def dim(self) -> int:
99
- """The dimension of the space."""
100
- return self._dim
79
+ Returns:
80
+ The corresponding vector in the dual space.
81
+ """
101
82
 
102
- @property
103
- def has_vector_multiply(self) -> bool:
104
- """True if multiplication of elements is defined."""
105
- return self._vector_multiply is not None
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 the Hilbert space.
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 the Hilbert space to real numbers.
114
- """
115
- if self._base is None:
116
- return HilbertSpace(
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) -> T_vec:
130
- """Returns the zero vector for the space."""
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) -> "LinearOperator":
233
+ def coordinate_inclusion(self) -> LinearOperator:
135
234
  """
136
- Returns the operator mapping coordinate vectors in R^n to vectors
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: T_vec) -> np.ndarray:
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) -> "LinearOperator":
259
+ def coordinate_projection(self) -> LinearOperator:
161
260
  """
162
- Returns the operator mapping vectors in this Hilbert space to their
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) -> T_vec:
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) -> "LinearOperator":
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) -> "LinearOperator":
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
- def inner_product(self, x1: T_vec, x2: T_vec) -> float:
204
- """Computes the inner product of two vectors."""
205
- return self.__inner_product(x1, x2)
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
- def squared_norm(self, x: T_vec) -> float:
208
- """Computes the squared norm of a vector."""
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
- def norm(self, x: T_vec) -> float:
212
- """Computes the norm of a vector."""
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
- def gram_schmidt(self, vectors: List[T_vec]) -> List[T_vec]:
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[T_vec] = []
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
- def to_dual(self, x: T_vec) -> Any:
235
- """Maps a vector to its canonical dual vector (a linear functional)."""
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
- Checks if an object is an element of the space.
376
+ Returns the i-th standard basis vector.
248
377
 
249
- Note: The current implementation checks type against the zero vector.
250
- This may not be robust for all vector representations.
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
- def add(self, x: T_vec, y: T_vec) -> T_vec:
255
- """Adds two vectors."""
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
- def vector_multiply(self, x1: T_vec, x2: T_vec) -> T_vec:
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
- def random(self) -> T_vec:
391
+ @final
392
+ def sample_expectation(self, vectors: List[Vector]) -> Vector:
307
393
  """
308
- Returns a random vector from the space.
394
+ Computes the sample mean of a list of vectors.
309
395
 
310
- The vector's components are drawn from a standard Gaussian distribution.
311
- """
312
- return self.from_components(np.random.randn(self.dim))
396
+ Args:
397
+ vectors: A list of vectors from the space.
398
+
399
+ Returns:
400
+ The sample mean (average) vector.
313
401
 
314
- def sample_expectation(self, vectors: List[T_vec]) -> T_vec:
315
- """Computes the sample mean of a list of vectors."""
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
- def identity_operator(self) -> "LinearOperator":
325
- """Returns the identity operator on the space."""
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
- def zero_operator(
337
- self, codomain: Optional[HilbertSpace] = None
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
- If no codomain is provided, it maps to itself.
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
- def _dual_from_components(self, cp: np.ndarray) -> "LinearForm":
359
- from .forms import LinearForm
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
- return LinearForm(self, components=cp)
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
- def __add(self, x: T_vec, y: T_vec) -> T_vec:
364
- return x + y
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
- def __subtract(self, x: T_vec, y: T_vec) -> T_vec:
367
- return x - y
473
+ @property
474
+ def dim(self) -> int:
475
+ """The dimension of the dual space."""
476
+ return self._underlying_space.dim
368
477
 
369
- def __multiply(self, a: float, x: T_vec) -> T_vec:
370
- return a * x.copy()
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 __ax(self, a: float, x: T_vec) -> None:
373
- x *= a
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 __axpy(self, a: float, x: T_vec, y: T_vec) -> None:
376
- y += a * x
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
- def __copy(self, x: T_vec) -> T_vec:
379
- return x.copy()
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 implementation of HilbertSpace where vectors are
387
- represented directly by NumPy arrays.
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) -> None:
543
+ def __init__(self, dim: int):
391
544
  """
392
545
  Args:
393
- dim (int): Dimension of the space.
546
+ dim: The dimension of the space.
394
547
  """
395
- super().__init__(
396
- dim,
397
- lambda x: x,
398
- lambda x: x,
399
- self.__inner_product,
400
- self.__to_dual,
401
- self.__from_dual,
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 __inner_product(self, x1: np.ndarray, x2: np.ndarray) -> float:
405
- return np.dot(x1, x2)
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 __to_dual(self, x: np.ndarray) -> "LinearForm":
408
- return self.dual.from_components(x)
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 __from_dual(self, xp: "LinearForm") -> np.ndarray:
411
- cp = self.dual.to_components(xp)
412
- return self.from_components(cp)
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
- def __eq__(self, other: object) -> bool:
415
- """Checks for equality with another EuclideanSpace."""
416
- return isinstance(other, EuclideanSpace) and self.dim == other.dim
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)