pygeoinf 1.2.0__py3-none-any.whl → 1.2.2__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 CHANGED
@@ -1,6 +1,7 @@
1
1
  from .random_matrix import (
2
2
  fixed_rank_random_range,
3
3
  variable_rank_random_range,
4
+ random_range,
4
5
  random_svd,
5
6
  random_eig,
6
7
  random_cholesky,
@@ -16,16 +17,22 @@ from .hilbert_space import (
16
17
  )
17
18
 
18
19
 
19
- from .operators import (
20
- Operator,
21
- LinearOperator,
22
- DiagonalLinearOperator,
20
+ from .nonlinear_forms import (
21
+ NonLinearForm,
23
22
  )
24
23
 
24
+
25
25
  from .linear_forms import (
26
26
  LinearForm,
27
27
  )
28
28
 
29
+ from .nonlinear_operators import NonLinearOperator
30
+
31
+ from .linear_operators import (
32
+ LinearOperator,
33
+ DiagonalLinearOperator,
34
+ )
35
+
29
36
 
30
37
  from .gaussian_measure import (
31
38
  GaussianMeasure,
@@ -45,7 +52,9 @@ from .linear_solvers import (
45
52
  DirectLinearSolver,
46
53
  LUSolver,
47
54
  CholeskySolver,
55
+ EigenSolver,
48
56
  IterativeLinearSolver,
57
+ ScipyIterativeSolver,
49
58
  CGMatrixSolver,
50
59
  BICGMatrixSolver,
51
60
  BICGStabMatrixSolver,
@@ -61,3 +70,7 @@ from .linear_optimisation import (
61
70
  )
62
71
 
63
72
  from .linear_bayesian import LinearBayesianInversion, LinearBayesianInference
73
+
74
+ from .nonlinear_optimisation import (
75
+ ScipyUnconstrainedOptimiser,
76
+ )
@@ -0,0 +1,120 @@
1
+ """
2
+ Module for Backus-Gilbert like methods for solving inference problems. To be done...
3
+ """
4
+
5
+ from .hilbert_space import HilbertSpace, Vector
6
+ from .linear_operators import LinearOperator
7
+ from .nonlinear_forms import NonLinearForm
8
+
9
+
10
+ class HyperEllipsoid:
11
+ """
12
+ A class for hyper-ellipsoids in a Hilbert Space. Such sets occur within
13
+ the context of Backus-Gilbert methods, both in terms of prior constraints
14
+ and posterior bounds on the property space.
15
+
16
+ The hyper-ellipsoid is defined through the inequality
17
+
18
+ (A(x-x_0), x-x_0)_{X} <= r**2,
19
+
20
+ where A is a self-adjoint linear operator on the space, X, x is an arbitrary vector, x_0 is the
21
+ centre, and r the radius.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ space: HilbertSpace,
27
+ radius: float,
28
+ /,
29
+ *,
30
+ centre: Vector = None,
31
+ operator: LinearOperator = None,
32
+ ) -> None:
33
+ """
34
+ Args:
35
+ space (HilbertSpace): The Hilbert space in which the hyper-ellipsoid is defined.
36
+ radius (float): The radius of the hyper-ellipsoid.
37
+ centre (Vector); The centre of the hyper-ellipsoid. The default is None which corresponds to
38
+ the zero-vector.
39
+ operator (LinearOperator): A self-adjoint operator on the space defining the hyper-ellipsoid.
40
+ The default is None which corresponds to the identity operator.
41
+ """
42
+
43
+ if not isinstance(space, HilbertSpace):
44
+ raise ValueError("Input space must be a HilbertSpace")
45
+ self._space = space
46
+
47
+ if not radius > 0:
48
+ raise ValueError("Input radius must be positive.")
49
+ self._radius = radius
50
+
51
+ if operator is None:
52
+ self._operator = space.identity_operator()
53
+ else:
54
+ if not (operator.domain == space and operator.is_automorphism):
55
+ raise ValueError("Operator is not of the appropriate form.")
56
+ self._operator = operator
57
+
58
+ if centre is None:
59
+ self._centre = space.zero
60
+ else:
61
+ if not space.is_element(centre):
62
+ raise ValueError("The input centre does not lie in the space.")
63
+ self._centre = centre
64
+
65
+ @property
66
+ def space(self) -> HilbertSpace:
67
+ """
68
+ Returns the HilbertSpace the hyper-ellipsoid is defined on.
69
+ """
70
+ return self._space
71
+
72
+ @property
73
+ def radius(self) -> float:
74
+ """
75
+ Returns the radius of the hyper-ellipsoid.
76
+ """
77
+ return self._radius
78
+
79
+ @property
80
+ def operator(self) -> LinearOperator:
81
+ """
82
+ Returns the operator for the hyper-ellipsoid.
83
+ """
84
+ return self._operator
85
+
86
+ @property
87
+ def centre(self) -> Vector:
88
+ """
89
+ Returns the centre of the hyper-ellipsoid.
90
+ """
91
+ return self._centre
92
+
93
+ @property
94
+ def quadratic_form(self) -> NonLinearForm:
95
+ """
96
+ Returns the mapping x -> (A(x-x_0), x-x_0)_{X} as a NonLinearForm.
97
+ """
98
+
99
+ space = self.space
100
+ x0 = self.centre
101
+ A = self.operator
102
+
103
+ def mapping(x: Vector) -> float:
104
+ d = space.subtract(x, x0)
105
+ return space.inner_product(A(d), d)
106
+
107
+ def gradient(x: Vector) -> Vector:
108
+ d = space.subtract(x, x0)
109
+ return space.multiply(2, A(d))
110
+
111
+ def hessian(_: Vector) -> LinearOperator:
112
+ return A
113
+
114
+ return NonLinearForm(space, mapping, gradient=gradient, hessian=hessian)
115
+
116
+ def is_point(self, x: Vector) -> bool:
117
+ """
118
+ True if x lies in the hyper-ellipsoid.
119
+ """
120
+ return self.quadratic_form(x) <= self.radius**2
pygeoinf/direct_sum.py CHANGED
@@ -29,7 +29,7 @@ from scipy.linalg import block_diag
29
29
  from joblib import Parallel, delayed
30
30
 
31
31
  from .hilbert_space import HilbertSpace
32
- from .operators import LinearOperator
32
+ from .linear_operators import LinearOperator
33
33
  from .linear_forms import LinearForm
34
34
  from .parallel import parallel_compute_dense_matrix_from_scipy_op
35
35
 
@@ -116,6 +116,16 @@ class HilbertSpaceDirectSum(HilbertSpace):
116
116
 
117
117
  return self.subspaces == other.subspaces
118
118
 
119
+ def is_element(self, xs: Any) -> bool:
120
+ """
121
+ Checks if a list of vectors is a valid element of the direct sum space.
122
+ """
123
+ if not isinstance(xs, list):
124
+ return False
125
+ if len(xs) != self.number_of_subspaces:
126
+ return False
127
+ return all(space.is_element(x) for space, x in zip(self._spaces, xs))
128
+
119
129
  @property
120
130
  def subspaces(self) -> List[HilbertSpace]:
121
131
  """Returns the list of subspaces that form the direct sum."""
@@ -307,35 +317,18 @@ class BlockLinearOperator(LinearOperator, BlockStructure):
307
317
  self, galerkin: bool, parallel: bool, n_jobs: int
308
318
  ) -> np.ndarray:
309
319
  """Overloaded method to efficiently compute the dense matrix for a block operator."""
310
- if not parallel:
311
- block_matrices = [
312
- [
313
- self.block(i, j).matrix(
314
- dense=True, galerkin=galerkin, parallel=False
315
- )
316
- for j in range(self.col_dim)
317
- ]
318
- for i in range(self.row_dim)
319
- ]
320
- return np.block(block_matrices)
321
320
 
322
- else:
323
- block_scipy_ops = [
324
- self.block(i, j).matrix(galerkin=galerkin)
325
- for i in range(self.row_dim)
321
+ block_matrices = [
322
+ [
323
+ self.block(i, j).matrix(
324
+ dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs
325
+ )
326
326
  for j in range(self.col_dim)
327
327
  ]
328
+ for i in range(self.row_dim)
329
+ ]
328
330
 
329
- computed_blocks = Parallel(n_jobs=n_jobs)(
330
- delayed(parallel_compute_dense_matrix_from_scipy_op)(op, n_jobs=1)
331
- for op in block_scipy_ops
332
- )
333
-
334
- block_matrices = [
335
- computed_blocks[i * self.col_dim : (i + 1) * self.col_dim]
336
- for i in range(self.row_dim)
337
- ]
338
- return np.block(block_matrices)
331
+ return np.block(block_matrices)
339
332
 
340
333
  def __mapping(self, xs: List[Any]) -> List[Any]:
341
334
 
@@ -402,6 +395,7 @@ class ColumnLinearOperator(LinearOperator, BlockStructure):
402
395
  x = domain.zero
403
396
  for op, y in zip(self._operators, ys):
404
397
  domain.axpy(1.0, op.adjoint(y), x)
398
+ print(op.adjoint(y).data)
405
399
  return x
406
400
 
407
401
  LinearOperator.__init__(
@@ -420,23 +414,11 @@ class ColumnLinearOperator(LinearOperator, BlockStructure):
420
414
  self, galerkin: bool, parallel: bool, n_jobs: int
421
415
  ) -> np.ndarray:
422
416
  """Overloaded method to efficiently compute the dense matrix for a column operator."""
423
- if not parallel:
424
- block_matrices = [
425
- op.matrix(dense=True, galerkin=galerkin, parallel=False)
426
- for op in self._operators
427
- ]
428
- return np.vstack(block_matrices)
429
- else:
430
- block_scipy_ops = [
431
- op.matrix(galerkin=galerkin, parallel=False) for op in self._operators
432
- ]
433
-
434
- computed_blocks = Parallel(n_jobs=n_jobs)(
435
- delayed(parallel_compute_dense_matrix_from_scipy_op)(op, n_jobs=1)
436
- for op in block_scipy_ops
437
- )
438
-
439
- return np.vstack(computed_blocks)
417
+ block_matrices = [
418
+ op.matrix(dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs)
419
+ for op in self._operators
420
+ ]
421
+ return np.vstack(block_matrices)
440
422
 
441
423
 
442
424
  class RowLinearOperator(LinearOperator, BlockStructure):
@@ -496,23 +478,11 @@ class RowLinearOperator(LinearOperator, BlockStructure):
496
478
  self, galerkin: bool, parallel: bool, n_jobs: int
497
479
  ) -> np.ndarray:
498
480
  """Overloaded method to efficiently compute the dense matrix for a row operator."""
499
- if not parallel:
500
- block_matrices = [
501
- op.matrix(dense=True, galerkin=galerkin, parallel=False)
502
- for op in self._operators
503
- ]
504
- return np.hstack(block_matrices)
505
- else:
506
- block_scipy_ops = [
507
- op.matrix(galerkin=galerkin, parallel=False) for op in self._operators
508
- ]
509
-
510
- computed_blocks = Parallel(n_jobs=n_jobs)(
511
- delayed(parallel_compute_dense_matrix_from_scipy_op)(op, n_jobs=1)
512
- for op in block_scipy_ops
513
- )
514
-
515
- return np.hstack(computed_blocks)
481
+ block_matrices = [
482
+ op.matrix(dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs)
483
+ for op in self._operators
484
+ ]
485
+ return np.hstack(block_matrices)
516
486
 
517
487
 
518
488
  class BlockDiagonalLinearOperator(LinearOperator, BlockStructure):
@@ -559,20 +529,8 @@ class BlockDiagonalLinearOperator(LinearOperator, BlockStructure):
559
529
  self, galerkin: bool, parallel: bool, n_jobs: int
560
530
  ) -> np.ndarray:
561
531
  """Overloaded method to efficiently compute the dense matrix for a block-diagonal operator."""
562
- if not parallel:
563
- block_matrices = [
564
- op.matrix(dense=True, galerkin=galerkin, parallel=False)
565
- for op in self._operators
566
- ]
567
- return block_diag(*block_matrices)
568
- else:
569
- block_scipy_ops = [
570
- op.matrix(galerkin=galerkin, parallel=False) for op in self._operators
571
- ]
572
-
573
- computed_blocks = Parallel(n_jobs=n_jobs)(
574
- delayed(parallel_compute_dense_matrix_from_scipy_op)(op, n_jobs=1)
575
- for op in block_scipy_ops
576
- )
577
-
578
- return block_diag(*computed_blocks)
532
+ block_matrices = [
533
+ op.matrix(dense=True, galerkin=galerkin, parallel=parallel, n_jobs=n_jobs)
534
+ for op in self._operators
535
+ ]
536
+ return block_diag(*block_matrices)
@@ -24,12 +24,14 @@ from scipy.stats import chi2
24
24
 
25
25
  from .gaussian_measure import GaussianMeasure
26
26
  from .direct_sum import ColumnLinearOperator
27
+ from .linear_operators import LinearOperator
28
+
27
29
 
28
30
  # This block only runs for type checkers, not at runtime, to prevent
29
31
  # circular import errors while still allowing type hints.
30
32
  if TYPE_CHECKING:
31
33
  from .hilbert_space import HilbertSpace, Vector
32
- from .operators import LinearOperator
34
+ from .nonlinear_operators import NonLinearOperator
33
35
 
34
36
 
35
37
  class ForwardProblem:
@@ -43,10 +45,10 @@ class ForwardProblem:
43
45
 
44
46
  def __init__(
45
47
  self,
46
- forward_operator: LinearOperator,
48
+ forward_operator: NonLinearOperator,
47
49
  /,
48
50
  *,
49
- data_error_measure: Optional["GaussianMeasure"] = None,
51
+ data_error_measure: Optional[GaussianMeasure] = None,
50
52
  ) -> None:
51
53
  """Initializes the ForwardProblem.
52
54
 
@@ -57,8 +59,8 @@ class ForwardProblem:
57
59
  from which data errors are assumed to be drawn. If None, the
58
60
  data is considered to be error-free.
59
61
  """
60
- self._forward_operator: LinearOperator = forward_operator
61
- self._data_error_measure: Optional["GaussianMeasure"] = data_error_measure
62
+ self._forward_operator: NonLinearOperator = forward_operator
63
+ self._data_error_measure: Optional[GaussianMeasure] = data_error_measure
62
64
  if self.data_error_measure_set:
63
65
  if self.data_space != data_error_measure.domain:
64
66
  raise ValueError(
@@ -76,7 +78,7 @@ class ForwardProblem:
76
78
  return self._data_error_measure is not None
77
79
 
78
80
  @property
79
- def data_error_measure(self) -> "GaussianMeasure":
81
+ def data_error_measure(self) -> GaussianMeasure:
80
82
  """The measure from which data errors are drawn."""
81
83
  if not self.data_error_measure_set:
82
84
  raise AttributeError("Data error measure has not been set.")
@@ -101,10 +103,31 @@ class LinearForwardProblem(ForwardProblem):
101
103
  and `e` is a random error drawn from a Gaussian distribution.
102
104
  """
103
105
 
106
+ def __init__(
107
+ self,
108
+ forward_operator: LinearOperator,
109
+ /,
110
+ *,
111
+ data_error_measure: Optional[GaussianMeasure] = None,
112
+ ) -> None:
113
+ """
114
+ Args:
115
+ forward_operator: The operator that maps from the model space to the
116
+ data space.
117
+ data_error_measure: A Gaussian measure representing the distribution
118
+ from which data errors are assumed to be drawn. If None, the
119
+ data is considered to be error-free.
120
+ """
121
+
122
+ if not isinstance(forward_operator, LinearOperator):
123
+ raise ValueError("Forward operator must be a linear operator.")
124
+
125
+ super().__init__(forward_operator, data_error_measure=data_error_measure)
126
+
104
127
  @staticmethod
105
128
  def from_direct_sum(
106
- forward_problems: List["LinearForwardProblem"],
107
- ) -> "LinearForwardProblem":
129
+ forward_problems: List[LinearForwardProblem],
130
+ ) -> LinearForwardProblem:
108
131
  """
109
132
  Forms a joint forward problem from a list of separate problems.
110
133
 
@@ -144,7 +167,7 @@ class LinearForwardProblem(ForwardProblem):
144
167
  joint_forward_operator, data_error_measure=data_error_measure
145
168
  )
146
169
 
147
- def data_measure(self, model: "Vector") -> "GaussianMeasure":
170
+ def data_measure(self, model: Vector) -> GaussianMeasure:
148
171
  """
149
172
  Returns the Gaussian measure for the data, given a specific model.
150
173
 
@@ -165,7 +188,7 @@ class LinearForwardProblem(ForwardProblem):
165
188
  translation=self.forward_operator(model)
166
189
  )
167
190
 
168
- def synthetic_data(self, model: "Vector") -> "Vector":
191
+ def synthetic_data(self, model: Vector) -> Vector:
169
192
  """
170
193
  Generates a synthetic data vector for a given model.
171
194
 
@@ -180,9 +203,7 @@ class LinearForwardProblem(ForwardProblem):
180
203
  """
181
204
  return self.data_measure(model).sample()
182
205
 
183
- def synthetic_model_and_data(
184
- self, prior: "GaussianMeasure"
185
- ) -> Tuple["Vector", "Vector"]:
206
+ def synthetic_model_and_data(self, prior: GaussianMeasure) -> Tuple[Vector, Vector]:
186
207
  """
187
208
  Generates a random model and corresponding synthetic data.
188
209
 
@@ -216,14 +237,16 @@ class LinearForwardProblem(ForwardProblem):
216
237
  """
217
238
  return chi2.ppf(significance_level, self.data_space.dim)
218
239
 
219
- def chi_squared(self, model: "Vector", data: "Vector") -> float:
240
+ def chi_squared(self, model: Vector, data: Vector) -> float:
220
241
  """
221
242
  Calculates the chi-squared statistic for a given model and data.
222
243
 
223
244
  This measures the misfit between the predicted and observed data.
245
+
224
246
  - If a data error measure with an inverse covariance `C_e^-1` is defined,
225
247
  this is the weighted misfit: `(d - A(u))^T * C_e^-1 * (d - A(u))`.
226
248
  - Otherwise, it is the squared L2 norm of the data residual: `||d - A(u)||^2`.
249
+
227
250
  Args:
228
251
  model: A vector from the model space.
229
252
  data: An observed data vector from the data space.
@@ -231,6 +254,7 @@ class LinearForwardProblem(ForwardProblem):
231
254
  Returns:
232
255
  The chi-squared statistic.
233
256
  """
257
+
234
258
  residual = self.data_space.subtract(data, self.forward_operator(model))
235
259
 
236
260
  if self.data_error_measure_set:
@@ -247,7 +271,7 @@ class LinearForwardProblem(ForwardProblem):
247
271
  return self.data_space.squared_norm(residual)
248
272
 
249
273
  def chi_squared_test(
250
- self, significance_level: float, model: "Vector", data: "Vector"
274
+ self, significance_level: float, model: Vector, data: Vector
251
275
  ) -> bool:
252
276
  """
253
277
  Performs a chi-squared test for goodness of fit.
@@ -30,7 +30,7 @@ from scipy.stats import multivariate_normal
30
30
 
31
31
  from .hilbert_space import EuclideanSpace, HilbertModule
32
32
 
33
- from .operators import (
33
+ from .linear_operators import (
34
34
  LinearOperator,
35
35
  DiagonalLinearOperator,
36
36
  )
@@ -491,12 +491,16 @@ class GaussianMeasure:
491
491
 
492
492
  def low_rank_approximation(
493
493
  self,
494
- rank: int,
494
+ size_estimate: int,
495
495
  /,
496
496
  *,
497
- power: int = 0,
498
- method: str = "fixed",
499
- rtol: float = 1e-2,
497
+ method: str = "variable",
498
+ max_rank: int = None,
499
+ power: int = 2,
500
+ rtol: float = 1e-4,
501
+ block_size: int = 10,
502
+ parallel: bool = False,
503
+ n_jobs: int = -1,
500
504
  ) -> GaussianMeasure:
501
505
  """
502
506
  Constructs a low-rank approximation of the measure.
@@ -505,16 +509,37 @@ class GaussianMeasure:
505
509
  can be much more efficient for sampling and storage.
506
510
 
507
511
  Args:
508
- rank (int): The target rank for the approximation.
509
- power (int, optional): Power iterations for the randomized algorithm.
510
- method (str, optional): 'fixed' or 'variable' rank method.
511
- rtol (float, optional): Relative tolerance for variable rank method.
512
+ size_estimate: For 'fixed' method, the exact target rank. For 'variable'
513
+ method, this is the initial rank to sample.
514
+ method ({'variable', 'fixed'}): The algorithm to use.
515
+ - 'variable': (Default) Progressively samples to find the rank needed
516
+ to meet tolerance `rtol`, stopping at `max_rank`.
517
+ - 'fixed': Returns a basis with exactly `size_estimate` columns.
518
+ max_rank: For 'variable' method, a hard limit on the rank. Ignored if
519
+ method='fixed'. Defaults to min(m, n).
520
+ power: Number of power iterations to improve accuracy.
521
+ rtol: Relative tolerance for the 'variable' method. Ignored if
522
+ method='fixed'.
523
+ block_size: Number of new vectors to sample per iteration in 'variable'
524
+ method. Ignored if method='fixed'.
525
+ parallel: Whether to use parallel matrix multiplication.
526
+ n_jobs: Number of jobs for parallelism.
512
527
 
513
528
  Returns:
514
529
  GaussianMeasure: The new, low-rank Gaussian measure.
530
+
531
+ Notes:
532
+ Parallel implemention only currently possible with fixed-rank decompositions.
515
533
  """
516
534
  covariance_factor = self.covariance.random_cholesky(
517
- rank, power=power, method=method, rtol=rtol
535
+ size_estimate,
536
+ method=method,
537
+ max_rank=max_rank,
538
+ power=power,
539
+ rtol=rtol,
540
+ block_size=block_size,
541
+ parallel=parallel,
542
+ n_jobs=n_jobs,
518
543
  )
519
544
 
520
545
  return GaussianMeasure(
pygeoinf/hilbert_space.py CHANGED
@@ -38,7 +38,7 @@ import numpy as np
38
38
 
39
39
  # This block only runs for type checkers, not at runtime
40
40
  if TYPE_CHECKING:
41
- from .operators import LinearOperator
41
+ from .linear_operators import LinearOperator
42
42
  from .linear_forms import LinearForm
43
43
 
44
44
  # Define a generic type for vectors in a Hilbert space
@@ -223,15 +223,13 @@ class HilbertSpace(ABC):
223
223
  # Final (Non-Overridable) Methods #
224
224
  # ------------------------------------------------------------------- #
225
225
 
226
-
227
-
228
226
  @final
229
227
  @property
230
228
  def coordinate_inclusion(self) -> LinearOperator:
231
229
  """
232
230
  The linear operator mapping R^n component vectors into this space.
233
231
  """
234
- from .operators import LinearOperator
232
+ from .linear_operators import LinearOperator
235
233
 
236
234
  domain = EuclideanSpace(self.dim)
237
235
 
@@ -257,7 +255,7 @@ class HilbertSpace(ABC):
257
255
  """
258
256
  The linear operator projecting vectors from this space to R^n.
259
257
  """
260
- from .operators import LinearOperator
258
+ from .linear_operators import LinearOperator
261
259
 
262
260
  codomain = EuclideanSpace(self.dim)
263
261
 
@@ -281,7 +279,7 @@ class HilbertSpace(ABC):
281
279
  @property
282
280
  def riesz(self) -> LinearOperator:
283
281
  """The Riesz map (dual to primal) as a `LinearOperator`."""
284
- from .operators import LinearOperator
282
+ from .linear_operators import LinearOperator
285
283
 
286
284
  return LinearOperator.self_dual(self.dual, self.from_dual)
287
285
 
@@ -289,7 +287,7 @@ class HilbertSpace(ABC):
289
287
  @property
290
288
  def inverse_riesz(self) -> LinearOperator:
291
289
  """The inverse Riesz map (primal to dual) as a `LinearOperator`."""
292
- from .operators import LinearOperator
290
+ from .linear_operators import LinearOperator
293
291
 
294
292
  return LinearOperator.self_dual(self, self.to_dual)
295
293
 
@@ -412,7 +410,7 @@ class HilbertSpace(ABC):
412
410
  @final
413
411
  def identity_operator(self) -> LinearOperator:
414
412
  """Returns the identity operator `I` on the space."""
415
- from .operators import LinearOperator
413
+ from .linear_operators import LinearOperator
416
414
 
417
415
  return LinearOperator(
418
416
  self,
@@ -433,7 +431,7 @@ class HilbertSpace(ABC):
433
431
  Returns:
434
432
  The zero linear operator.
435
433
  """
436
- from .operators import LinearOperator
434
+ from .linear_operators import LinearOperator
437
435
 
438
436
  codomain = self if codomain is None else codomain
439
437
  return LinearOperator(
@@ -506,6 +504,14 @@ class DualHilbertSpace(HilbertSpace):
506
504
  return NotImplemented
507
505
  return self.underlying_space == other.underlying_space
508
506
 
507
+ def is_element(self, x: Any) -> bool:
508
+ """
509
+ Checks if an object is a valid element of the dual space.
510
+ """
511
+ from .linear_forms import LinearForm
512
+
513
+ return isinstance(x, LinearForm) and x.domain == self.underlying_space
514
+
509
515
  @final
510
516
  def duality_product(self, xp: LinearForm, x: Vector) -> float:
511
517
  """
@@ -585,6 +591,12 @@ class EuclideanSpace(HilbertSpace):
585
591
  return NotImplemented
586
592
  return self.dim == other.dim
587
593
 
594
+ def is_element(self, x: Any) -> bool:
595
+ """
596
+ Checks if an object is a valid element of the space.
597
+ """
598
+ return isinstance(x, np.ndarray) and len(x) == self.dim
599
+
588
600
 
589
601
  class MassWeightedHilbertSpace(HilbertSpace):
590
602
  """
@@ -680,6 +692,40 @@ class MassWeightedHilbertSpace(HilbertSpace):
680
692
  and (self.inverse_mass_operator == other.inverse_mass_operator)
681
693
  )
682
694
 
695
+ def is_element(self, x: Any) -> bool:
696
+ """
697
+ Checks if an object is a valid element of the space.
698
+ """
699
+ return self.underlying_space.is_element(x)
700
+
701
+ def add(self, x: Vector, y: Vector) -> Vector:
702
+ """Computes the sum of two vectors. Defaults to `x + y`."""
703
+ return self.underlying_space.add(x, y)
704
+
705
+ def subtract(self, x: Vector, y: Vector) -> Vector:
706
+ """Computes the difference of two vectors. Defaults to `x - y`."""
707
+ return self.underlying_space.subtract(x, y)
708
+
709
+ def multiply(self, a: float, x: Vector) -> Vector:
710
+ """Computes scalar multiplication. Defaults to `a * x`."""
711
+ return self.underlying_space.multiply(a, x)
712
+
713
+ def negative(self, x: Vector) -> Vector:
714
+ """Computes the additive inverse of a vector. Defaults to `-1 * x`."""
715
+ return self.underlying_space.negative(x)
716
+
717
+ def ax(self, a: float, x: Vector) -> None:
718
+ """Performs in-place scaling `x := a*x`. Defaults to `x *= a`."""
719
+ self.underlying_space.ax(a, x)
720
+
721
+ def axpy(self, a: float, x: Vector, y: Vector) -> None:
722
+ """Performs in-place operation `y := y + a*x`. Defaults to `y += a*x`."""
723
+ self.underlying_space.axpy(a, x, y)
724
+
725
+ def copy(self, x: Vector) -> Vector:
726
+ """Returns a deep copy of a vector. Defaults to `x.copy()`."""
727
+ return self.underlying_space.copy(x)
728
+
683
729
 
684
730
  class MassWeightedHilbertModule(MassWeightedHilbertSpace, HilbertModule):
685
731
  """