pygeoinf 1.0.8__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pygeoinf/__init__.py CHANGED
@@ -8,7 +8,11 @@ from .random_matrix import (
8
8
 
9
9
  from .hilbert_space import (
10
10
  HilbertSpace,
11
+ DualHilbertSpace,
11
12
  EuclideanSpace,
13
+ HilbertModule,
14
+ MassWeightedHilbertSpace,
15
+ MassWeightedHilbertModule,
12
16
  )
13
17
 
14
18
 
@@ -18,7 +22,7 @@ from .operators import (
18
22
  DiagonalLinearOperator,
19
23
  )
20
24
 
21
- from .forms import (
25
+ from .linear_forms import (
22
26
  LinearForm,
23
27
  )
24
28
 
pygeoinf/direct_sum.py CHANGED
@@ -1,5 +1,24 @@
1
1
  """
2
- Module for direct sums of Hilbert spaces and related block operators.
2
+ Implements direct sums of Hilbert spaces and corresponding block operators.
3
+
4
+ This module provides tools for constructing larger, composite Hilbert spaces and
5
+ operators from smaller ones. This is essential for problems involving multiple
6
+ coupled fields or joint inversions where a single model is constrained by
7
+ data from different experiments.
8
+
9
+ Key Classes
10
+ -----------
11
+ - `HilbertSpaceDirectSum`: A `HilbertSpace` formed by the direct sum of a
12
+ list of other spaces. Vectors in this space are lists of vectors from the
13
+ component subspaces.
14
+ - `BlockLinearOperator`: A `LinearOperator` acting between direct sum spaces,
15
+ represented as a 2D grid (matrix) of sub-operators.
16
+ - `ColumnLinearOperator`: A specialized block operator mapping from a single
17
+ space to a direct sum space.
18
+ - `RowLinearOperator`: A specialized block operator mapping from a direct sum
19
+ space to a single space.
20
+ - `BlockDiagonalLinearOperator`: An efficient representation for block
21
+ operators with zero off-diagonal blocks.
3
22
  """
4
23
 
5
24
  from __future__ import annotations
@@ -9,15 +28,16 @@ import numpy as np
9
28
 
10
29
  from .hilbert_space import HilbertSpace
11
30
  from .operators import LinearOperator
12
- from .forms import LinearForm
31
+ from .linear_forms import LinearForm
13
32
 
14
33
 
15
34
  class HilbertSpaceDirectSum(HilbertSpace):
16
35
  """
17
36
  A Hilbert space formed from the direct sum of a list of other spaces.
18
37
 
19
- Vectors in this space are represented as lists of vectors, where each
20
- element of the list is a vector from the corresponding subspace.
38
+ A vector in this space is represented as a list of vectors, where the i-th
39
+ element of the list is a vector from the i-th component subspace. The
40
+ inner product is the sum of the inner products of the components.
21
41
  """
22
42
 
23
43
  def __init__(self, spaces: List[HilbertSpace]) -> None:
@@ -29,22 +49,58 @@ class HilbertSpaceDirectSum(HilbertSpace):
29
49
  in the direct sum.
30
50
  """
31
51
  self._spaces: List[HilbertSpace] = spaces
32
- dim = sum([space.dim for space in spaces])
33
- super().__init__(
34
- dim,
35
- self.__to_components,
36
- self.__from_components,
37
- self.__inner_product,
38
- self.__to_dual,
39
- self.__from_dual,
40
- add=self.__add,
41
- subtract=self.__subtract,
42
- multiply=self.__multiply,
43
- ax=self.__ax,
44
- axpy=self.__axpy,
45
- copy=self.__copy,
52
+ self._dim = sum([space.dim for space in spaces])
53
+
54
+ @property
55
+ def dim(self) -> int:
56
+ """Returns the dimension of the direct sum space."""
57
+ return self._dim
58
+
59
+ def to_components(self, xs: List[Any]) -> np.ndarray:
60
+ cs = [space.to_components(x) for space, x in zip(self._spaces, xs)]
61
+ return np.concatenate(cs, 0)
62
+
63
+ def from_components(self, c: np.ndarray) -> List[Any]:
64
+ xs = []
65
+ i = 0
66
+ for space in self._spaces:
67
+ j = i + space.dim
68
+ x = space.from_components(c[i:j])
69
+ xs.append(x)
70
+ i = j
71
+ return xs
72
+
73
+ def to_dual(self, xs: List[Any]) -> LinearForm:
74
+ if len(xs) != self.number_of_subspaces:
75
+ raise ValueError("Input list has incorrect number of vectors.")
76
+ return self.canonical_dual_isomorphism(
77
+ [space.to_dual(x) for space, x in zip(self._spaces, xs)]
46
78
  )
47
79
 
80
+ def from_dual(self, xp: LinearForm) -> List[Any]:
81
+ xps = self.canonical_dual_inverse_isomorphism(xp)
82
+ return [space.from_dual(xip) for space, xip in zip(self._spaces, xps)]
83
+
84
+ def add(self, xs: List[Any], ys: List[Any]) -> List[Any]:
85
+ return [space.add(x, y) for space, x, y in zip(self._spaces, xs, ys)]
86
+
87
+ def subtract(self, xs: List[Any], ys: List[Any]) -> List[Any]:
88
+ return [space.subtract(x, y) for space, x, y in zip(self._spaces, xs, ys)]
89
+
90
+ def multiply(self, a: float, xs: List[Any]) -> List[Any]:
91
+ return [space.multiply(a, x) for space, x in zip(self._spaces, xs)]
92
+
93
+ def ax(self, a: float, xs: List[Any]) -> None:
94
+ for space, x in zip(self._spaces, xs):
95
+ space.ax(a, x)
96
+
97
+ def axpy(self, a: float, xs: List[Any], ys: List[Any]) -> None:
98
+ for space, x, y in zip(self._spaces, xs, ys):
99
+ space.axpy(a, x, y)
100
+
101
+ def copy(self, xs: List[Any]) -> List[Any]:
102
+ return [space.copy(x) for space, x in zip(self._spaces, xs)]
103
+
48
104
  def __eq__(self, other: object) -> bool:
49
105
  """
50
106
  Checks for mathematical equality with another direct sum space.
@@ -55,7 +111,6 @@ class HilbertSpaceDirectSum(HilbertSpace):
55
111
  if not isinstance(other, HilbertSpaceDirectSum):
56
112
  return NotImplemented
57
113
 
58
- # This relies on the subspaces having their own __eq__ methods.
59
114
  return self.subspaces == other.subspaces
60
115
 
61
116
  @property
@@ -119,12 +174,10 @@ class HilbertSpaceDirectSum(HilbertSpace):
119
174
  """
120
175
  if len(xps) != self.number_of_subspaces:
121
176
  raise ValueError("Incorrect number of dual vectors provided.")
122
- return LinearForm(
123
- self,
124
- mapping=lambda x: sum(
125
- xp(self.subspace_projection(i)(x)) for i, xp in enumerate(xps)
126
- ),
127
- )
177
+
178
+ cps = [space.dual.to_components(xp) for space, xp in zip(self._spaces, xps)]
179
+ cp = np.concatenate(cps, 0)
180
+ return LinearForm(self, components=cp)
128
181
 
129
182
  def canonical_dual_inverse_isomorphism(self, xp: LinearForm) -> List[LinearForm]:
130
183
  """
@@ -136,61 +189,16 @@ class HilbertSpaceDirectSum(HilbertSpace):
136
189
  Args:
137
190
  xp (LinearForm): A dual vector on the direct sum space.
138
191
  """
139
- return [
140
- LinearForm(space, mapping=lambda x, j=i: xp(self.subspace_inclusion(j)(x)))
141
- for i, space in enumerate(self.subspaces)
142
- ]
143
-
144
- # ... (Private methods remain the same) ...
145
- def __to_components(self, xs: List[Any]) -> np.ndarray:
146
- cs = [space.to_components(x) for space, x in zip(self._spaces, xs)]
147
- return np.concatenate(cs, 0)
148
192
 
149
- def __from_components(self, c: np.ndarray) -> List[Any]:
150
- xs = []
193
+ cp = self.dual.to_components(xp)
194
+ xps = []
151
195
  i = 0
152
196
  for space in self._spaces:
153
197
  j = i + space.dim
154
- x = space.from_components(c[i:j])
155
- xs.append(x)
198
+ xp = space.dual.from_components(cp[i:j])
199
+ xps.append(xp)
156
200
  i = j
157
- return xs
158
-
159
- def __inner_product(self, x1s: List[Any], x2s: List[Any]) -> float:
160
- return sum(
161
- space.inner_product(x1, x2) for space, x1, x2 in zip(self._spaces, x1s, x2s)
162
- )
163
-
164
- def __to_dual(self, xs: List[Any]) -> LinearForm:
165
- if len(xs) != self.number_of_subspaces:
166
- raise ValueError("Input list has incorrect number of vectors.")
167
- return self.canonical_dual_isomorphism(
168
- [space.to_dual(x) for space, x in zip(self._spaces, xs)]
169
- )
170
-
171
- def __from_dual(self, xp: LinearForm) -> List[Any]:
172
- xps = self.canonical_dual_inverse_isomorphism(xp)
173
- return [space.from_dual(xip) for space, xip in zip(self._spaces, xps)]
174
-
175
- def __add(self, xs: List[Any], ys: List[Any]) -> List[Any]:
176
- return [space.add(x, y) for space, x, y in zip(self._spaces, xs, ys)]
177
-
178
- def __subtract(self, xs: List[Any], ys: List[Any]) -> List[Any]:
179
- return [space.subtract(x, y) for space, x, y in zip(self._spaces, xs, ys)]
180
-
181
- def __multiply(self, a: float, xs: List[Any]) -> List[Any]:
182
- return [space.multiply(a, x) for space, x in zip(self._spaces, xs)]
183
-
184
- def __ax(self, a: float, xs: List[Any]) -> None:
185
- for space, x in zip(self._spaces, xs):
186
- space.ax(a, x)
187
-
188
- def __axpy(self, a: float, xs: List[Any], ys: List[Any]) -> None:
189
- for space, x, y in zip(self._spaces, xs, ys):
190
- space.axpy(a, x, y)
191
-
192
- def __copy(self, xs: List[Any]) -> List[Any]:
193
- return [space.copy(x) for space, x in zip(self._spaces, xs)]
201
+ return xps
194
202
 
195
203
  def _subspace_projection_mapping(self, i: int, xs: List[Any]) -> Any:
196
204
  return xs[i]
@@ -204,21 +212,29 @@ class BlockStructure(ABC):
204
212
  An abstract base class for operators with a block structure.
205
213
  """
206
214
 
207
- # ... (class content is the same) ...
208
215
  def __init__(self, row_dim: int, col_dim: int) -> None:
209
216
  self._row_dim: int = row_dim
210
217
  self._col_dim: int = col_dim
211
218
 
212
219
  @property
213
220
  def row_dim(self) -> int:
221
+ """
222
+ Returns the number of rows in the block structure.
223
+ """
214
224
  return self._row_dim
215
225
 
216
226
  @property
217
227
  def col_dim(self) -> int:
228
+ """
229
+ Returns the number of columns in the block structure.
230
+ """
218
231
  return self._col_dim
219
232
 
220
233
  @abstractmethod
221
234
  def block(self, i: int, j: int) -> "LinearOperator":
235
+ """
236
+ Returns the operator in the (i, j)-th sub-block.
237
+ """
222
238
  pass
223
239
 
224
240
  def _check_block_indices(self, i: int, j: int) -> None:
@@ -230,8 +246,12 @@ class BlockStructure(ABC):
230
246
 
231
247
  class BlockLinearOperator(LinearOperator, BlockStructure):
232
248
  """
233
- A linear operator between direct sums of Hilbert spaces, defined by a
234
- matrix of sub-operators (blocks).
249
+ A linear operator between direct sum spaces, defined by a matrix of sub-operators.
250
+
251
+ This operator acts like a matrix where each entry is itself a `LinearOperator`.
252
+ It maps a list of input vectors `[x_1, x_2, ...]` to a list of output
253
+ vectors `[y_1, y_2, ...]`. The constructor checks for dimensional
254
+ consistency between the blocks.
235
255
  """
236
256
 
237
257
  def __init__(self, blocks: List[List[LinearOperator]]) -> None:
@@ -245,7 +265,7 @@ class BlockLinearOperator(LinearOperator, BlockStructure):
245
265
  """
246
266
  if not blocks or not blocks[0]:
247
267
  raise ValueError("Block structure cannot be empty.")
248
- # ... (rest of the method is the same) ...
268
+
249
269
  domains = [operator.domain for operator in blocks[0]]
250
270
  codomains = []
251
271
  for row in blocks:
@@ -281,7 +301,7 @@ class BlockLinearOperator(LinearOperator, BlockStructure):
281
301
  return self._blocks[i][j]
282
302
 
283
303
  def __mapping(self, xs: List[Any]) -> List[Any]:
284
- # ... (method content is the same) ...
304
+
285
305
  ys = []
286
306
  for i in range(self.row_dim):
287
307
  codomain = self._codomains[i]
@@ -293,7 +313,7 @@ class BlockLinearOperator(LinearOperator, BlockStructure):
293
313
  return ys
294
314
 
295
315
  def __adjoint_mapping(self, ys: List[Any]) -> List[Any]:
296
- # ... (method content is the same) ...
316
+
297
317
  xs = []
298
318
  for j in range(self.col_dim):
299
319
  domain = self._domains[j]
@@ -307,8 +327,12 @@ class BlockLinearOperator(LinearOperator, BlockStructure):
307
327
 
308
328
  class ColumnLinearOperator(LinearOperator, BlockStructure):
309
329
  """
310
- An operator that maps a single Hilbert space to a direct sum of spaces.
311
- It is represented as a column of operators.
330
+ An operator that maps from a single space to a direct sum space.
331
+
332
+ It can be visualized as a column vector of operators, `[A_1, A_2, ...]^T`.
333
+ It takes a single input vector `x` and produces a list of output vectors
334
+ `[A_1(x), A_2(x), ...]`. This is often used to represent a joint forward
335
+ operator in an inverse problem.
312
336
  """
313
337
 
314
338
  def __init__(self, operators: List[LinearOperator]) -> None:
@@ -358,8 +382,12 @@ class ColumnLinearOperator(LinearOperator, BlockStructure):
358
382
 
359
383
  class RowLinearOperator(LinearOperator, BlockStructure):
360
384
  """
361
- An operator that maps a direct sum of spaces to a single Hilbert space,
362
- structured as a row of operator blocks.
385
+ An operator that maps from a direct sum space to a single space.
386
+
387
+ It can be visualized as a row vector of operators, `[A_1, A_2, ...]`.
388
+ It takes a list of input vectors `[x_1, x_2, ...]` and produces a single
389
+ output vector `y = A_1(x_1) + A_2(x_2) + ...`. The adjoint of a
390
+ `ColumnLinearOperator` is a `RowLinearOperator`.
363
391
  """
364
392
 
365
393
  def __init__(self, operators: List[LinearOperator]) -> None:
@@ -1,10 +1,20 @@
1
1
  """
2
- Module for defining forward problem classes.
3
-
4
- This module provides classes to represent inverse problem formulations, which
5
- relate unknown model parameters to observed data through a forward operator.
6
- It handles both deterministic (error-free) and statistical (with data errors)
7
- scenarios.
2
+ Defines the mathematical structure of a forward problem.
3
+
4
+ This module provides classes that encapsulate the core components of an
5
+ inverse problem. A forward problem describes the physical or mathematical
6
+ process that maps a set of unknown model parameters `u` to a set of observable
7
+ data `d`.
8
+
9
+ The module handles both the deterministic relationship `d = A(u)` and the more
10
+ realistic statistical model `d = A(u) + e`, where `e` represents random noise.
11
+
12
+ Key Classes
13
+ -----------
14
+ - `ForwardProblem`: A general class representing the link between a model
15
+ space and a data space via a forward operator, with an optional data error.
16
+ - `LinearForwardProblem`: A specialization for linear problems where the
17
+ forward operator is a `LinearOperator`.
8
18
  """
9
19
 
10
20
  from __future__ import annotations
@@ -18,7 +28,7 @@ from .direct_sum import ColumnLinearOperator
18
28
  # This block only runs for type checkers, not at runtime, to prevent
19
29
  # circular import errors while still allowing type hints.
20
30
  if TYPE_CHECKING:
21
- from .hilbert_space import HilbertSpace, T_vec
31
+ from .hilbert_space import HilbertSpace, Vector
22
32
  from .operators import LinearOperator
23
33
 
24
34
 
@@ -33,7 +43,7 @@ class ForwardProblem:
33
43
 
34
44
  def __init__(
35
45
  self,
36
- forward_operator: "LinearOperator",
46
+ forward_operator: LinearOperator,
37
47
  /,
38
48
  *,
39
49
  data_error_measure: Optional["GaussianMeasure"] = None,
@@ -47,7 +57,7 @@ class ForwardProblem:
47
57
  from which data errors are assumed to be drawn. If None, the
48
58
  data is considered to be error-free.
49
59
  """
50
- self._forward_operator: "LinearOperator" = forward_operator
60
+ self._forward_operator: LinearOperator = forward_operator
51
61
  self._data_error_measure: Optional["GaussianMeasure"] = data_error_measure
52
62
  if self.data_error_measure_set:
53
63
  if self.data_space != data_error_measure.domain:
@@ -56,7 +66,7 @@ class ForwardProblem:
56
66
  )
57
67
 
58
68
  @property
59
- def forward_operator(self) -> "LinearOperator":
69
+ def forward_operator(self) -> LinearOperator:
60
70
  """The forward operator, mapping from model to data space."""
61
71
  return self._forward_operator
62
72
 
@@ -88,9 +98,7 @@ class LinearForwardProblem(ForwardProblem):
88
98
  Represents a linear forward problem of the form `d = A(u) + e`.
89
99
 
90
100
  Here, `d` is the data, `A` is the linear forward operator, `u` is the model,
91
- and `e` is a random error drawn from a Gaussian distribution. This class
92
- provides methods for statistical analysis, such as generating synthetic data
93
- and performing chi-squared tests.
101
+ and `e` is a random error drawn from a Gaussian distribution.
94
102
  """
95
103
 
96
104
  @staticmethod
@@ -100,8 +108,9 @@ class LinearForwardProblem(ForwardProblem):
100
108
  """
101
109
  Forms a joint forward problem from a list of separate problems.
102
110
 
103
- This is useful when a single underlying model is observed through
104
- multiple, independent measurement systems.
111
+ This is a powerful tool for joint inversions, where a single underlying
112
+ model is observed through multiple, independent measurement systems
113
+ (e.g., different types of geophysical surveys).
105
114
 
106
115
  Args:
107
116
  forward_problems: A list of `LinearForwardProblem` instances that
@@ -110,10 +119,6 @@ class LinearForwardProblem(ForwardProblem):
110
119
  Returns:
111
120
  A single `LinearForwardProblem` where the data space is the direct
112
121
  sum of the individual data spaces.
113
-
114
- Raises:
115
- ValueError: If the list of problems is empty or if they do not all
116
- share the same model space.
117
122
  """
118
123
  if not forward_problems:
119
124
  raise ValueError("Cannot form a direct sum from an empty list.")
@@ -139,7 +144,7 @@ class LinearForwardProblem(ForwardProblem):
139
144
  joint_forward_operator, data_error_measure=data_error_measure
140
145
  )
141
146
 
142
- def data_measure(self, model: "T_vec") -> "GaussianMeasure":
147
+ def data_measure(self, model: "Vector") -> "GaussianMeasure":
143
148
  """
144
149
  Returns the Gaussian measure for the data, given a specific model.
145
150
 
@@ -160,7 +165,7 @@ class LinearForwardProblem(ForwardProblem):
160
165
  translation=self.forward_operator(model)
161
166
  )
162
167
 
163
- def synthetic_data(self, model: "T_vec") -> "T_vec":
168
+ def synthetic_data(self, model: "Vector") -> "Vector":
164
169
  """
165
170
  Generates a synthetic data vector for a given model.
166
171
 
@@ -177,7 +182,7 @@ class LinearForwardProblem(ForwardProblem):
177
182
 
178
183
  def synthetic_model_and_data(
179
184
  self, prior: "GaussianMeasure"
180
- ) -> Tuple["T_vec", "T_vec"]:
185
+ ) -> Tuple["Vector", "Vector"]:
181
186
  """
182
187
  Generates a random model and corresponding synthetic data.
183
188
 
@@ -211,24 +216,20 @@ class LinearForwardProblem(ForwardProblem):
211
216
  """
212
217
  return chi2.ppf(significance_level, self.data_space.dim)
213
218
 
214
- def chi_squared(self, model: "T_vec", data: "T_vec") -> float:
219
+ def chi_squared(self, model: "Vector", data: "Vector") -> float:
215
220
  """
216
221
  Calculates the chi-squared statistic for a given model and data.
217
222
 
218
- If a data error measure with an inverse covariance is defined, this is
219
- the weighted misfit: `(d - A(u))^T * C_e^-1 * (d - A(u))`. Otherwise,
220
- it is the squared norm of the data residual: `||d - A(u)||^2`.
221
-
223
+ This measures the misfit between the predicted and observed data.
224
+ - If a data error measure with an inverse covariance `C_e^-1` is defined,
225
+ this is the weighted misfit: `(d - A(u))^T * C_e^-1 * (d - A(u))`.
226
+ - Otherwise, it is the squared L2 norm of the data residual: `||d - A(u)||^2`.
222
227
  Args:
223
228
  model: A vector from the model space.
224
229
  data: An observed data vector from the data space.
225
230
 
226
231
  Returns:
227
232
  The chi-squared statistic.
228
-
229
- Raises:
230
- AttributeError: If a data error measure is set but its inverse
231
- covariance (precision operator) is not available.
232
233
  """
233
234
  residual = self.data_space.subtract(data, self.forward_operator(model))
234
235
 
@@ -246,7 +247,7 @@ class LinearForwardProblem(ForwardProblem):
246
247
  return self.data_space.squared_norm(residual)
247
248
 
248
249
  def chi_squared_test(
249
- self, significance_level: float, model: "T_vec", data: "T_vec"
250
+ self, significance_level: float, model: "Vector", data: "Vector"
250
251
  ) -> bool:
251
252
  """
252
253
  Performs a chi-squared test for goodness of fit.