pygeoinf 1.3.5__py3-none-any.whl → 1.3.6__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.
@@ -9,10 +9,8 @@ Key Classes
9
9
  -----------
10
10
  - `LinearBayesianInversion`: Computes the posterior Gaussian measure `p(u|d)`
11
11
  for the model `u` given observed data `d`.
12
- - `LinearBayesianInference`: Extends the framework to compute the posterior
13
- distribution for a derived property of the model.
14
12
  - `ConstrainedLinearBayesianInversion`: Solves the inverse problem subject to
15
- a hard affine constraint `u in A`, interpreting it as conditioning the prior.
13
+ an affine constraint `u in A`.
16
14
  """
17
15
 
18
16
  from __future__ import annotations
@@ -41,6 +39,11 @@ class LinearBayesianInversion(LinearInversion):
41
39
  model_prior_measure: GaussianMeasure,
42
40
  /,
43
41
  ) -> None:
42
+ """
43
+ Args:
44
+ forward_problem: The forward problem linking the model to the data.
45
+ model_prior_measure: The prior Gaussian measure on the model space.
46
+ """
44
47
  super().__init__(forward_problem)
45
48
  self._model_prior_measure: GaussianMeasure = model_prior_measure
46
49
 
@@ -52,16 +55,7 @@ class LinearBayesianInversion(LinearInversion):
52
55
  @property
53
56
  def normal_operator(self) -> LinearOperator:
54
57
  """
55
- Returns the Bayesian Norm operator:
56
-
57
- N = A Q A* + R
58
-
59
- with A the forward operator (with A* its adjoint), Q the model
60
- prior covariance, and R the data error covariance. For error-free
61
- problems this operator is reduced to:
62
-
63
- N = A Q A*
64
-
58
+ Returns the Bayesian Normal operator: N = A Q A* + R.
65
59
  """
66
60
  forward_operator = self.forward_problem.forward_operator
67
61
  model_prior_covariance = self.model_prior_measure.covariance
@@ -80,23 +74,10 @@ class LinearBayesianInversion(LinearInversion):
80
74
  /,
81
75
  *,
82
76
  preconditioner: Optional[LinearOperator] = None,
83
- ):
77
+ ) -> LinearOperator:
84
78
  """
85
- Returns the Kalman gain operator for the problem:
86
-
87
- K = Q A* Ni
88
-
89
- where Q is the model prior covariance, A the forward operator
90
- (with adjoint A*), and Ni is the inverse of the normal operator.
91
-
92
- Args:
93
- solver: A linear solver for inverting the normal operator.
94
- preconditioner: An optional preconditioner for.
95
-
96
- Returns:
97
- A LinearOperator for the Kalman gain.
79
+ Returns the Kalman gain operator K = Q A* N^-1.
98
80
  """
99
-
100
81
  forward_operator = self.forward_problem.forward_operator
101
82
  model_prior_covariance = self.model_prior_measure.covariance
102
83
  normal_operator = self.normal_operator
@@ -121,37 +102,45 @@ class LinearBayesianInversion(LinearInversion):
121
102
  preconditioner: Optional[LinearOperator] = None,
122
103
  ) -> GaussianMeasure:
123
104
  """
124
- Returns the posterior Gaussian measure for the model conditions on the data.
105
+ Returns the posterior Gaussian measure p(u|d).
125
106
 
126
107
  Args:
127
108
  data: The observed data vector.
128
- solver: A linear solver for inverting the normal operator C_d.
129
- preconditioner: An optional preconditioner for C_d.
109
+ solver: A linear solver for inverting the normal operator.
110
+ preconditioner: An optional preconditioner.
130
111
  """
131
112
  data_space = self.data_space
132
113
  model_space = self.model_space
133
114
  forward_operator = self.forward_problem.forward_operator
134
115
  model_prior_covariance = self.model_prior_measure.covariance
135
116
 
117
+ # 1. Compute Kalman Gain
136
118
  kalman_gain = self.kalman_operator(solver, preconditioner=preconditioner)
137
119
 
138
- # u_bar_post = u_bar + K (v - A u_bar - v_bar)
120
+ # 2. Compute Posterior Mean
121
+ # Shift data: d - A(mu_u)
139
122
  shifted_data = data_space.subtract(
140
123
  data, forward_operator(self.model_prior_measure.expectation)
141
124
  )
125
+
126
+ # Shift for noise mean: d - A(mu_u) - mu_e
142
127
  if self.forward_problem.data_error_measure_set:
143
- shifted_data = data_space.subtract(
144
- shifted_data, self.forward_problem.data_error_measure.expectation
145
- )
128
+ error_expectation = self.forward_problem.data_error_measure.expectation
129
+ shifted_data = data_space.subtract(shifted_data, error_expectation)
130
+ else:
131
+ error_expectation = data_space.zero
132
+
146
133
  mean_update = kalman_gain(shifted_data)
147
134
  expectation = model_space.add(self.model_prior_measure.expectation, mean_update)
148
135
 
149
- # Q_post = Q - K A Q
136
+ # 3. Compute Posterior Covariance (Implicitly)
137
+ # C_post = C_u - K A C_u
150
138
  covariance = model_prior_covariance - (
151
139
  kalman_gain @ forward_operator @ model_prior_covariance
152
140
  )
153
141
 
154
- # Add in a sampling method if that is possible.
142
+ # 4. Set up Posterior Sampling
143
+ # Logic: Can sample if prior is samplable AND (noise is absent OR samplable)
155
144
  can_sample_prior = self.model_prior_measure.sample_set
156
145
  can_sample_noise = (
157
146
  not self.forward_problem.data_error_measure_set
@@ -160,19 +149,21 @@ class LinearBayesianInversion(LinearInversion):
160
149
 
161
150
  if can_sample_prior and can_sample_noise:
162
151
 
163
- if self.forward_problem.data_error_measure_set:
164
- error_expectation = self.forward_problem.data_error_measure.expectation
165
-
166
152
  def sample():
153
+ # a. Sample Prior
167
154
  model_sample = self.model_prior_measure.sample()
155
+
156
+ # b. Calculate Residual
168
157
  prediction = forward_operator(model_sample)
169
158
  data_residual = data_space.subtract(data, prediction)
170
159
 
160
+ # c. Perturb Residual
171
161
  if self.forward_problem.data_error_measure_set:
172
162
  noise_raw = self.forward_problem.data_error_measure.sample()
173
163
  epsilon = data_space.subtract(noise_raw, error_expectation)
174
164
  data_space.axpy(1.0, epsilon, data_residual)
175
165
 
166
+ # d. Update
176
167
  correction = kalman_gain(data_residual)
177
168
  return model_space.add(model_sample, correction)
178
169
 
@@ -185,11 +176,13 @@ class LinearBayesianInversion(LinearInversion):
185
176
 
186
177
  class ConstrainedLinearBayesianInversion(LinearInversion):
187
178
  """
188
- Solves a linear inverse problem using Bayesian methods subject to an
189
- affine subspace constraint `u in A`.
179
+ Solves a linear inverse problem subject to an affine subspace constraint.
190
180
 
191
- This interprets the constraint as conditioning the prior on the subspace.
192
- The subspace must be defined by a linear equation B(u) = w.
181
+ This class enforces the constraint `u in A` using either:
182
+ 1. Bayesian Conditioning (Default): p(u | d, u in A).
183
+ If A is defined geometrically (no explicit equation), an implicit
184
+ operator (I-P) is used, which requires a robust solver in the subspace.
185
+ 2. Geometric Projection: Projects the unconstrained posterior onto A.
193
186
  """
194
187
 
195
188
  def __init__(
@@ -205,8 +198,8 @@ class ConstrainedLinearBayesianInversion(LinearInversion):
205
198
  Args:
206
199
  forward_problem: The forward problem.
207
200
  model_prior_measure: The unconstrained prior Gaussian measure.
208
- constraint: The affine subspace A = {u | Bu = w}.
209
- geometric: If True, uses orthogonal projection to enforce the constraint.
201
+ constraint: The affine subspace A.
202
+ geometric: If True, uses orthogonal projection (Euclidean metric).
210
203
  If False (default), uses Bayesian conditioning.
211
204
  """
212
205
  super().__init__(forward_problem)
@@ -214,88 +207,37 @@ class ConstrainedLinearBayesianInversion(LinearInversion):
214
207
  self._constraint = constraint
215
208
  self._geometric = geometric
216
209
 
217
- if not constraint.has_constraint_equation:
218
- raise ValueError(
219
- "For Bayesian inversion, the subspace must be defined by a linear "
220
- "equation (constraint operator). Use AffineSubspace.from_linear_equation."
221
- )
222
-
223
- def conditioned_prior_measure(
224
- self,
225
- solver: LinearSolver,
226
- preconditioner: Optional[LinearOperator] = None,
227
- ) -> GaussianMeasure:
210
+ def conditioned_prior_measure(self) -> GaussianMeasure:
228
211
  """
229
- Computes the prior measure conditioned on the constraint B(u) = w.
230
-
231
- Args:
232
- solver: Linear solver used to invert the normal operator, BQB*.
233
- preconditioner: Optional preconditioner for the constraint solver.
212
+ Computes the prior measure conditioned on the constraint.
234
213
  """
235
-
236
- constraint_op = self._constraint.constraint_operator
237
- constraint_val = self._constraint.constraint_value
238
-
239
- if self._geometric:
240
- # --- Geometric Approach (Affine Mapping) ---
241
- # Map: u -> P u + v
242
- # P = I - B* (B B*)^-1 B
243
- # v = B* (B B*)^-1 w
244
-
245
- gram_operator = constraint_op @ constraint_op.adjoint
246
-
247
- if isinstance(solver, IterativeLinearSolver):
248
- inv_gram_operator = solver(gram_operator, preconditioner=preconditioner)
249
- else:
250
- inv_gram_operator = solver(gram_operator)
251
-
252
- pseudo_inverse = constraint_op.adjoint @ inv_gram_operator
253
- identity = self._unconstrained_prior.domain.identity_operator()
254
- projector = identity - pseudo_inverse @ constraint_op
255
- translation = pseudo_inverse(constraint_val)
256
-
257
- return self._unconstrained_prior.affine_mapping(
258
- operator=projector, translation=translation
259
- )
260
-
261
- else:
262
- # --- Bayesian Approach (Statistical Conditioning) ---
263
- # Treat the constraint as a noiseless observation: w = B(u)
264
-
265
- constraint_problem = LinearForwardProblem(constraint_op)
266
- constraint_inversion = LinearBayesianInversion(
267
- constraint_problem, self._unconstrained_prior
268
- )
269
-
270
- return constraint_inversion.model_posterior_measure(
271
- constraint_val, solver, preconditioner=preconditioner
272
- )
214
+ return self._constraint.condition_gaussian_measure(
215
+ self._unconstrained_prior, geometric=self._geometric
216
+ )
273
217
 
274
218
  def model_posterior_measure(
275
219
  self,
276
220
  data: Vector,
277
221
  solver: LinearSolver,
278
- constraint_solver: LinearSolver,
222
+ /,
279
223
  *,
280
224
  preconditioner: Optional[LinearOperator] = None,
281
- constraint_preconditioner: Optional[LinearOperator] = None,
282
225
  ) -> GaussianMeasure:
283
226
  """
284
- Returns the posterior Gaussian measure for the model given the constraint and the data.
227
+ Returns the posterior Gaussian measure p(u | d, u in A).
285
228
 
286
229
  Args:
287
230
  data: Observed data vector.
288
231
  solver: Solver for the data update (inverts A C_cond A* + Ce).
289
- constraint_solver: Solver for the prior conditioning (inverts B C_prior B*).
290
- preconditioner: Preconditioner for the data update (acts on Data Space).
291
- constraint_preconditioner: Preconditioner for the constraint update (acts on Property Space).
232
+ preconditioner: Preconditioner for the data update.
233
+
234
+ Note: The solver for the constraint update is managed internally by
235
+ the AffineSubspace object passed at initialization.
292
236
  """
293
- # 1. Condition Prior (Uses constraint_solver and constraint_preconditioner)
294
- cond_prior = self.conditioned_prior_measure(
295
- constraint_solver, preconditioner=constraint_preconditioner
296
- )
237
+ # 1. Condition Prior
238
+ cond_prior = self.conditioned_prior_measure()
297
239
 
298
- # 2. Solve Bayesian Inverse Problem (Uses solver and preconditioner)
240
+ # 2. Solve Bayesian Inverse Problem with the new prior
299
241
  bayes_inv = LinearBayesianInversion(self.forward_problem, cond_prior)
300
242
 
301
243
  return bayes_inv.model_posterior_measure(
pygeoinf/subspaces.py CHANGED
@@ -3,29 +3,25 @@ Defines classes for representing affine and linear subspaces.
3
3
 
4
4
  The primary abstraction is the `AffineSubspace`, which represents a subset of
5
5
  a Hilbert space defined by a translation and a closed linear tangent space.
6
- `LinearSubspace` is a specialization where the translation is zero.
7
6
  """
8
7
 
9
8
  from __future__ import annotations
10
9
  from typing import List, Optional, Any, Callable, TYPE_CHECKING
11
10
  import numpy as np
11
+ import warnings
12
12
 
13
13
  from .linear_operators import LinearOperator
14
14
  from .hilbert_space import HilbertSpace, Vector, EuclideanSpace
15
15
  from .linear_solvers import LinearSolver, CholeskySolver, IterativeLinearSolver
16
16
 
17
17
  if TYPE_CHECKING:
18
- # Avoid circular imports for type checking
19
- pass
18
+ from .gaussian_measure import GaussianMeasure
20
19
 
21
20
 
22
21
  class OrthogonalProjector(LinearOperator):
23
22
  """
24
23
  Internal engine for subspace projections.
25
-
26
24
  Represents an orthogonal projection operator P = P* = P^2.
27
- While this class can be used directly, it is generally recommended to use
28
- `AffineSubspace` or `LinearSubspace` for high-level problem definitions.
29
25
  """
30
26
 
31
27
  def __init__(
@@ -52,9 +48,8 @@ class OrthogonalProjector(LinearOperator):
52
48
  basis_vectors: List[Vector],
53
49
  orthonormalize: bool = True,
54
50
  ) -> OrthogonalProjector:
55
- """Constructs P from a basis spanning the range."""
51
+ """Constructs a projector P onto the span of the provided basis vectors."""
56
52
  if not basis_vectors:
57
- # Return zero operator if basis is empty
58
53
  return domain.zero_operator(domain)
59
54
 
60
55
  if orthonormalize:
@@ -78,7 +73,12 @@ class AffineSubspace:
78
73
  translation: Optional[Vector] = None,
79
74
  constraint_operator: Optional[LinearOperator] = None,
80
75
  constraint_value: Optional[Vector] = None,
76
+ solver: Optional[LinearSolver] = None,
77
+ preconditioner: Optional[LinearOperator] = None,
81
78
  ) -> None:
79
+ """
80
+ Initializes the AffineSubspace.
81
+ """
82
82
  self._projector = projector
83
83
 
84
84
  if translation is None:
@@ -91,6 +91,15 @@ class AffineSubspace:
91
91
  self._constraint_operator = constraint_operator
92
92
  self._constraint_value = constraint_value
93
93
 
94
+ # Logic: If explicit equation exists, default to Cholesky.
95
+ # If implicit, leave None (requires robust solver from user).
96
+ if self._constraint_operator is not None and solver is None:
97
+ self._solver = CholeskySolver(galerkin=True)
98
+ else:
99
+ self._solver = solver
100
+
101
+ self._preconditioner = preconditioner
102
+
94
103
  @property
95
104
  def domain(self) -> HilbertSpace:
96
105
  return self._projector.domain
@@ -108,27 +117,39 @@ class AffineSubspace:
108
117
  return LinearSubspace(self._projector)
109
118
 
110
119
  @property
111
- def has_constraint_equation(self) -> bool:
120
+ def has_explicit_equation(self) -> bool:
121
+ """True if defined by B(u)=w, False if defined only by geometry."""
112
122
  return self._constraint_operator is not None
113
123
 
114
124
  @property
115
125
  def constraint_operator(self) -> LinearOperator:
126
+ """
127
+ Returns B for {u | B(u)=w}.
128
+ Falls back to (I - P) if no explicit operator exists.
129
+ """
116
130
  if self._constraint_operator is None:
117
- raise AttributeError("This subspace is not defined by a linear equation.")
131
+ return self._projector.complement
118
132
  return self._constraint_operator
119
133
 
120
134
  @property
121
135
  def constraint_value(self) -> Vector:
136
+ """
137
+ Returns w for {u | B(u)=w}.
138
+ Falls back to (I - P)x0 if no explicit operator exists.
139
+ """
122
140
  if self._constraint_value is None:
123
- raise AttributeError("This subspace is not defined by a linear equation.")
141
+ complement = self._projector.complement
142
+ return complement(self._translation)
124
143
  return self._constraint_value
125
144
 
126
145
  def project(self, x: Vector) -> Vector:
146
+ """Orthogonally projects x onto the affine subspace."""
127
147
  diff = self.domain.subtract(x, self.translation)
128
148
  proj_diff = self.projector(diff)
129
149
  return self.domain.add(self.translation, proj_diff)
130
150
 
131
151
  def is_element(self, x: Vector, rtol: float = 1e-6) -> bool:
152
+ """Returns True if x lies in the subspace."""
132
153
  proj = self.project(x)
133
154
  diff = self.domain.subtract(x, proj)
134
155
  norm_diff = self.domain.norm(diff)
@@ -136,6 +157,46 @@ class AffineSubspace:
136
157
  scale = norm_x if norm_x > 1e-12 else 1.0
137
158
  return norm_diff <= rtol * scale
138
159
 
160
+ def condition_gaussian_measure(
161
+ self, prior: GaussianMeasure, geometric: bool = False
162
+ ) -> GaussianMeasure:
163
+ """
164
+ Conditions a Gaussian measure on this subspace.
165
+ """
166
+ if geometric:
167
+ # Geometric Projection: u -> P(u - x0) + x0
168
+ # Affine Map: u -> P(u) + (I-P)x0
169
+ shift = self.domain.subtract(
170
+ self.translation, self.projector(self.translation)
171
+ )
172
+ return prior.affine_mapping(operator=self.projector, translation=shift)
173
+
174
+ else:
175
+ # Bayesian Conditioning: u | B(u)=w
176
+
177
+ # Check for singular implicit operator usage
178
+ if not self.has_explicit_equation and self._solver is None:
179
+ raise ValueError(
180
+ "This subspace defines the constraint implicitly as (I-P)u = (I-P)x0. "
181
+ "The operator (I-P) is singular. You must provide a solver "
182
+ "capable of handling singular systems (e.g. MinRes) to the "
183
+ "AffineSubspace constructor."
184
+ )
185
+
186
+ # Local imports
187
+ from .forward_problem import LinearForwardProblem
188
+ from .linear_bayesian import LinearBayesianInversion
189
+
190
+ solver = self._solver
191
+ preconditioner = self._preconditioner
192
+
193
+ constraint_problem = LinearForwardProblem(self.constraint_operator)
194
+ constraint_inversion = LinearBayesianInversion(constraint_problem, prior)
195
+
196
+ return constraint_inversion.model_posterior_measure(
197
+ self.constraint_value, solver, preconditioner=preconditioner
198
+ )
199
+
139
200
  @classmethod
140
201
  def from_linear_equation(
141
202
  cls,
@@ -144,7 +205,7 @@ class AffineSubspace:
144
205
  solver: Optional[LinearSolver] = None,
145
206
  preconditioner: Optional[LinearOperator] = None,
146
207
  ) -> AffineSubspace:
147
- """Constructs the subspace {u | B(u) = w}."""
208
+ """Constructs subspace from B(u)=w."""
148
209
  domain = operator.domain
149
210
  G = operator @ operator.adjoint
150
211
 
@@ -166,7 +227,12 @@ class AffineSubspace:
166
227
  projector = OrthogonalProjector(domain, mapping, complement_projector=P_perp_op)
167
228
 
168
229
  return cls(
169
- projector, translation, constraint_operator=operator, constraint_value=value
230
+ projector,
231
+ translation,
232
+ constraint_operator=operator,
233
+ constraint_value=value,
234
+ solver=solver,
235
+ preconditioner=preconditioner,
170
236
  )
171
237
 
172
238
  @classmethod
@@ -176,18 +242,38 @@ class AffineSubspace:
176
242
  basis_vectors: List[Vector],
177
243
  translation: Optional[Vector] = None,
178
244
  orthonormalize: bool = True,
245
+ solver: Optional[LinearSolver] = None,
246
+ preconditioner: Optional[LinearOperator] = None,
179
247
  ) -> AffineSubspace:
180
248
  """
181
- Constructs the subspace passing through 'translation' with the given
182
- tangent basis.
183
-
184
- Note: This does not define a constraint equation B(u)=w, so it cannot
185
- be used directly with ConstrainedLinearBayesianInversion.
249
+ Constructs an affine subspace from a translation and a basis for the tangent space.
250
+
251
+ This method defines the subspace geometrically. The constraint is implicit:
252
+ (I - P)u = (I - P)x0.
253
+
254
+ Args:
255
+ domain: The Hilbert space.
256
+ basis_vectors: Basis vectors for the tangent space V.
257
+ translation: A point x0 in the subspace.
258
+ orthonormalize: If True, orthonormalizes the basis.
259
+ solver: A linear solver capable of handling the singular operator (I-P).
260
+ Required if you intend to use this subspace for Bayesian conditioning.
261
+ preconditioner: Optional preconditioner for the solver.
186
262
  """
263
+ if solver is None:
264
+ warnings.warn(
265
+ "Constructing a subspace from a tangent basis without a solver. "
266
+ "This defines an implicit constraint with a singular operator. "
267
+ "Bayesian conditioning will fail; geometric projection remains available.",
268
+ UserWarning,
269
+ stacklevel=2,
270
+ )
271
+
187
272
  projector = OrthogonalProjector.from_basis(
188
273
  domain, basis_vectors, orthonormalize=orthonormalize
189
274
  )
190
- return cls(projector, translation)
275
+
276
+ return cls(projector, translation, solver=solver, preconditioner=preconditioner)
191
277
 
192
278
  @classmethod
193
279
  def from_complement_basis(
@@ -198,24 +284,18 @@ class AffineSubspace:
198
284
  orthonormalize: bool = True,
199
285
  ) -> AffineSubspace:
200
286
  """
201
- Constructs the subspace orthogonal to the given basis, passing through
202
- 'translation'.
203
-
204
- This automatically constructs the constraint operator B such that
205
- the subspace is {u | B(u) = B(translation)}.
287
+ Constructs subspace from complement basis.
288
+ Constraint is explicit: <u, e_i> = <x0, e_i>.
206
289
  """
207
- # 1. Orthonormalize basis for stability
208
290
  if orthonormalize:
209
291
  e_vectors = domain.gram_schmidt(basis_vectors)
210
292
  else:
211
293
  e_vectors = basis_vectors
212
294
 
213
- # 2. Construct Projector P_perp
214
295
  complement_projector = OrthogonalProjector.from_basis(
215
- domain, e_vectors, orthonormalize=False # Already done
296
+ domain, e_vectors, orthonormalize=False
216
297
  )
217
298
 
218
- # 3. Construct Projector P = I - P_perp
219
299
  def mapping(x: Any) -> Any:
220
300
  return domain.subtract(x, complement_projector(x))
221
301
 
@@ -223,16 +303,12 @@ class AffineSubspace:
223
303
  domain, mapping, complement_projector=complement_projector
224
304
  )
225
305
 
226
- # 4. Construct Constraint Operator B implicitly defined by the basis
227
- # B: E -> R^k, u -> [<e_1, u>, ..., <e_k, u>]
228
- # Since e_i are orthonormal, BB* = I, which is perfect for solvers.
229
306
  codomain = EuclideanSpace(len(e_vectors))
230
307
 
231
308
  def constraint_mapping(u: Vector) -> np.ndarray:
232
309
  return np.array([domain.inner_product(e, u) for e in e_vectors])
233
310
 
234
311
  def constraint_adjoint(c: np.ndarray) -> Vector:
235
- # sum c_i e_i
236
312
  res = domain.zero
237
313
  for i, e in enumerate(e_vectors):
238
314
  domain.axpy(c[i], e, res)
@@ -242,8 +318,6 @@ class AffineSubspace:
242
318
  domain, codomain, constraint_mapping, adjoint_mapping=constraint_adjoint
243
319
  )
244
320
 
245
- # 5. Determine Constraint Value w = B(translation)
246
- # If translation is None (zero), w is zero.
247
321
  if translation is None:
248
322
  _translation = domain.zero
249
323
  w = codomain.zero
@@ -251,12 +325,20 @@ class AffineSubspace:
251
325
  _translation = translation
252
326
  w = B(_translation)
253
327
 
254
- return cls(projector, _translation, constraint_operator=B, constraint_value=w)
328
+ solver = CholeskySolver(galerkin=True)
329
+
330
+ return cls(
331
+ projector,
332
+ _translation,
333
+ constraint_operator=B,
334
+ constraint_value=w,
335
+ solver=solver,
336
+ )
255
337
 
256
338
 
257
339
  class LinearSubspace(AffineSubspace):
258
340
  """
259
- Represents a linear subspace (an affine subspace passing through zero).
341
+ Represents a linear subspace (an affine subspace passing through the origin).
260
342
  """
261
343
 
262
344
  def __init__(self, projector: OrthogonalProjector) -> None:
@@ -272,14 +354,19 @@ class LinearSubspace(AffineSubspace):
272
354
 
273
355
  @classmethod
274
356
  def from_kernel(
275
- cls, operator: LinearOperator, solver: Optional[LinearSolver] = None
357
+ cls,
358
+ operator: LinearOperator,
359
+ solver: Optional[LinearSolver] = None,
360
+ preconditioner: Optional[LinearOperator] = None,
276
361
  ) -> LinearSubspace:
277
362
  affine = AffineSubspace.from_linear_equation(
278
- operator, operator.codomain.zero, solver
363
+ operator, operator.codomain.zero, solver, preconditioner
279
364
  )
280
365
  instance = cls(affine.projector)
281
366
  instance._constraint_operator = operator
282
367
  instance._constraint_value = operator.codomain.zero
368
+ instance._solver = affine._solver
369
+ instance._preconditioner = preconditioner
283
370
  return instance
284
371
 
285
372
  @classmethod
@@ -288,11 +375,16 @@ class LinearSubspace(AffineSubspace):
288
375
  domain: HilbertSpace,
289
376
  basis_vectors: List[Vector],
290
377
  orthonormalize: bool = True,
378
+ solver: Optional[LinearSolver] = None,
379
+ preconditioner: Optional[LinearOperator] = None,
291
380
  ) -> LinearSubspace:
292
381
  projector = OrthogonalProjector.from_basis(
293
382
  domain, basis_vectors, orthonormalize=orthonormalize
294
383
  )
295
- return cls(projector)
384
+ instance = cls(projector)
385
+ instance._solver = solver
386
+ instance._preconditioner = preconditioner
387
+ return instance
296
388
 
297
389
  @classmethod
298
390
  def from_complement_basis(
@@ -304,8 +396,8 @@ class LinearSubspace(AffineSubspace):
304
396
  affine = AffineSubspace.from_complement_basis(
305
397
  domain, basis_vectors, translation=None, orthonormalize=orthonormalize
306
398
  )
307
- # Copy constraint info from the affine instance
308
399
  instance = cls(affine.projector)
309
400
  instance._constraint_operator = affine.constraint_operator
310
401
  instance._constraint_value = affine.constraint_value
402
+ instance._solver = affine._solver
311
403
  return instance
@@ -11,16 +11,28 @@ class SHVectorConverter:
11
11
  """
12
12
  Handles conversion between pyshtools 3D coefficient arrays and 1D vectors.
13
13
 
14
- This class provides a bridge between the pyshtools data structure for
15
- spherical harmonic coefficients, a 3D array of shape (2, lmax+1, lmax+1),
16
- and the 1D vector format often used in linear algebra and inverse problems.
14
+ This class bridges the gap between the `pyshtools` 3D array format
15
+ (shape `[2, lmax+1, lmax+1]`) and the flat 1D vector format used in
16
+ linear algebra.
17
17
 
18
- The vector is ordered by degree l, and within each degree, by order m,
19
- from -l to +l.
18
+ **Vector Layout:**
19
+ The output vector is ordered first by degree $l$ (ascending from `lmin` to `lmax`),
20
+ and then by order $m$ (ascending from $-l$ to $+l$).
21
+
22
+ The sequence of coefficients is:
23
+
24
+ .. math::
25
+ [u_{l_{min}, -l_{min}}, \dots, u_{l_{min}, l_{min}}, \quad
26
+ u_{l_{min}+1, -(l_{min}+1)}, \dots, u_{l_{min}+1, l_{min}+1}, \quad \dots]
27
+
28
+ **Example (lmin=0):**
29
+
30
+ .. math::
31
+ [u_{0,0}, \quad u_{1,-1}, u_{1,0}, u_{1,1}, \quad u_{2,-2}, u_{2,-1}, u_{2,0}, u_{2,1}, u_{2,2}, \dots]
20
32
 
21
33
  Args:
22
34
  lmax (int): The maximum spherical harmonic degree to include.
23
- lmin (int): The minimum spherical harmonic degree to include. Defaults to 2.
35
+ lmin (int): The minimum spherical harmonic degree to include. Defaults to 0.
24
36
  """
25
37
 
26
38
  def __init__(self, lmax: int, lmin: int = 0):
@@ -532,30 +532,26 @@ class Lebesgue(SphereHelper, HilbertModule, AbstractInvariantLebesgueSpace):
532
532
  )
533
533
 
534
534
  def to_coefficient_operator(self, lmax: int, lmin: int = 0):
535
- """
536
- Returns a LinearOperator that maps an element of the space to
537
- a vector of its spherical harmonic coefficients within the
538
- specified range of degrees.
535
+ r"""
536
+ Returns a LinearOperator mapping a function to its spherical harmonic coefficients.
539
537
 
540
- The output coefficients are ordered in the following manner:
538
+ The operator maps an element of the Hilbert space to a vector in $\mathbb{R}^k$.
539
+ The coefficients in the output vector are ordered by degree $l$ (major)
540
+ and order $m$ (minor), from $-l$ to $+l$.
541
541
 
542
- u_{00}, u_{1-1}, u_{10}, u_{11}, u_{2-2}, u_{2-1}, u_{20}, u_{21}, u_{22}, ...
542
+ **Ordering:**
543
543
 
544
- in this case assuming lmin = 0.
544
+ .. math::
545
+ u = [u_{0,0}, \quad u_{1,-1}, u_{1,0}, u_{1,1}, \quad u_{2,-2}, \dots, u_{2,2}, \quad \dots]
545
546
 
546
- If lmax is larger than the field's lmax, the output will be padded by zeros.
547
+ (assuming `lmin=0`).
547
548
 
548
549
  Args:
549
550
  lmax: The maximum spherical harmonic degree to include in the output.
550
- lmin: The minimum spherical harmonic degree to include in the output.
551
- Defaults to 0.
551
+ lmin: The minimum spherical harmonic degree to include. Defaults to 0.
552
552
 
553
553
  Returns:
554
- A LinearOperator that maps an SHGrid to a NumPy vector of coefficients.
555
-
556
- Notes:
557
- This is a left inverse of the from_coefficient_operator so long a the
558
- values for lmin and lmax are equal.
554
+ A LinearOperator mapping `SHGrid` -> `numpy.ndarray`.
559
555
  """
560
556
 
561
557
  converter = SHVectorConverter(lmax, lmin)
@@ -577,27 +573,25 @@ class Lebesgue(SphereHelper, HilbertModule, AbstractInvariantLebesgueSpace):
577
573
  return LinearOperator(self, codomain, mapping, adjoint_mapping=adjoint_mapping)
578
574
 
579
575
  def from_coefficient_operator(self, lmax: int, lmin: int = 0):
580
- """
581
- Returns a LinearOperator that maps a vector of spherical harmonic coefficients
582
- to an element of the space.
576
+ r"""
577
+ Returns a LinearOperator mapping a vector of coefficients to a function.
583
578
 
584
- The input coefficients are ordered in the following manner:
579
+ The operator maps a vector in $\mathbb{R}^k$ to an element of the Hilbert space.
580
+ The input vector must follow the standard $l$-major, $m$-minor ordering.
585
581
 
586
- u_{00}, u_{1-1}, u_{10}, u_{11}, u_{2-2}, u_{2-1}, u_{20}, u_{21}, u_{22}, ...
582
+ **Ordering:**
587
583
 
588
- in this case assuming lmin = 0.
584
+ .. math::
585
+ v = [u_{0,0}, \quad u_{1,-1}, u_{1,0}, u_{1,1}, \quad u_{2,-2}, \dots, u_{2,2}, \quad \dots]
586
+
587
+ (assuming `lmin=0`).
589
588
 
590
589
  Args:
591
- lmax: The maximum spherical harmonic degree to include in the output.
592
- lmin: The minimum spherical harmonic degree to include in the output.
593
- Defaults to 0.
590
+ lmax: The maximum spherical harmonic degree expected in the input.
591
+ lmin: The minimum spherical harmonic degree expected. Defaults to 0.
594
592
 
595
593
  Returns:
596
- A LinearOperator that maps a NumPy vector of coefficients to an SHGrid.
597
-
598
- Notes:
599
- This is a right inverse of the to_coefficient_operator so long a the
600
- values for lmin and lmax are equal.
594
+ A LinearOperator mapping `numpy.ndarray` -> `SHGrid`.
601
595
  """
602
596
 
603
597
  converter = SHVectorConverter(lmax, lmin)
@@ -783,30 +777,26 @@ class Sobolev(SphereHelper, MassWeightedHilbertModule, AbstractInvariantSobolevS
783
777
  )
784
778
 
785
779
  def to_coefficient_operator(self, lmax: int, lmin: int = 0):
786
- """
787
- Returns a LinearOperator that maps an element of the space to
788
- a vector of its spherical harmonic coefficients within the
789
- specified range of degrees.
780
+ r"""
781
+ Returns a LinearOperator mapping a function to its spherical harmonic coefficients.
790
782
 
791
- The output coefficients are ordered in the following manner:
783
+ The operator maps an element of the Hilbert space to a vector in $\mathbb{R}^k$.
784
+ The coefficients in the output vector are ordered by degree $l$ (major)
785
+ and order $m$ (minor), from $-l$ to $+l$.
792
786
 
793
- u_{00}, u_{1-1}, u_{10}, u_{11}, u_{2-2}, u_{2-1}, u_{20}, u_{21}, u_{22}, ...
787
+ **Ordering:**
794
788
 
795
- in this case assuming lmin = 0.
789
+ .. math::
790
+ u = [u_{0,0}, \quad u_{1,-1}, u_{1,0}, u_{1,1}, \quad u_{2,-2}, \dots, u_{2,2}, \quad \dots]
796
791
 
797
- If lmax is larger than the field's lmax, the output will be padded by zeros.
792
+ (assuming `lmin=0`).
798
793
 
799
794
  Args:
800
795
  lmax: The maximum spherical harmonic degree to include in the output.
801
- lmin: The minimum spherical harmonic degree to include in the output.
802
- Defaults to 0.
796
+ lmin: The minimum spherical harmonic degree to include. Defaults to 0.
803
797
 
804
798
  Returns:
805
- A LinearOperator that maps an SHGrid to a NumPy vector of coefficients.
806
-
807
- Notes:
808
- This is a left inverse of the from_coefficient_operator so long a the
809
- values for lmin and lmax are equal.
799
+ A LinearOperator mapping `SHGrid` -> `numpy.ndarray`.
810
800
  """
811
801
 
812
802
  l2_operator = self.underlying_space.to_coefficient_operator(lmax, lmin)
@@ -816,27 +806,25 @@ class Sobolev(SphereHelper, MassWeightedHilbertModule, AbstractInvariantSobolevS
816
806
  )
817
807
 
818
808
  def from_coefficient_operator(self, lmax: int, lmin: int = 0):
819
- """
820
- Returns a LinearOperator that maps a vector of spherical harmonic coefficients
821
- to an element of the space.
809
+ r"""
810
+ Returns a LinearOperator mapping a vector of coefficients to a function.
822
811
 
823
- The input coefficients are ordered in the following manner:
812
+ The operator maps a vector in $\mathbb{R}^k$ to an element of the Hilbert space.
813
+ The input vector must follow the standard $l$-major, $m$-minor ordering.
824
814
 
825
- u_{00}, u_{1-1}, u_{10}, u_{11}, u_{2-2}, u_{2-1}, u_{20}, u_{21}, u_{22}, ...
815
+ **Ordering:**
826
816
 
827
- in this case assuming lmin = 0.
817
+ .. math::
818
+ v = [u_{0,0}, \quad u_{1,-1}, u_{1,0}, u_{1,1}, \quad u_{2,-2}, \dots, u_{2,2}, \quad \dots]
819
+
820
+ (assuming `lmin=0`).
828
821
 
829
822
  Args:
830
- lmax: The maximum spherical harmonic degree to include in the output.
831
- lmin: The minimum spherical harmonic degree to include in the output.
832
- Defaults to 0.
823
+ lmax: The maximum spherical harmonic degree expected in the input.
824
+ lmin: The minimum spherical harmonic degree expected. Defaults to 0.
833
825
 
834
826
  Returns:
835
- A LinearOperator that maps a NumPy vector of coefficients to an SHGrid.
836
-
837
- Notes:
838
- This is a right inverse of the to_coefficient_operator so long a the
839
- values for lmin and lmax are equal.
827
+ A LinearOperator mapping `numpy.ndarray` -> `SHGrid`.
840
828
  """
841
829
 
842
830
  l2_operator = self.underlying_space.from_coefficient_operator(lmax, lmin)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeoinf
3
- Version: 1.3.5
3
+ Version: 1.3.6
4
4
  Summary: A package for solving geophysical inference and inverse problems
5
5
  License: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -10,7 +10,7 @@ pygeoinf/forward_problem.py,sha256=NnqWp7iMfkhHa9d-jBHzYHClaAfhKmO5D058AcJLLYg,1
10
10
  pygeoinf/gaussian_measure.py,sha256=bBh64xHgmLFl27krn9hkf8qDQjop_39x69cyhJgUHN8,26219
11
11
  pygeoinf/hilbert_space.py,sha256=Yc0Jw0A8Jo12Zpgb_9dhcF7CD77S1IDMrSTDFHpTRB8,27340
12
12
  pygeoinf/inversion.py,sha256=RV0hG2bGnciWdja0oOPKPxnFhYzufqdj-mKYNr4JJ_o,6447
13
- pygeoinf/linear_bayesian.py,sha256=o6m0fYESba8rE-rjxfCqXaV6irPthB1aWf8vhM2v8XQ,11324
13
+ pygeoinf/linear_bayesian.py,sha256=qzWEVaNe9AwG5GBmGHgVHswEMFKBWvOOJDlS95ahyxc,8877
14
14
  pygeoinf/linear_forms.py,sha256=mgZeDRegNKo8kviE68KrxkHR4gG9bf1RgsJz1MtDMCk,9181
15
15
  pygeoinf/linear_operators.py,sha256=Bn-uzwUXi2kkWZ7wc9Uhj3vBHtocN17hnzc_r7DAzTk,64530
16
16
  pygeoinf/linear_optimisation.py,sha256=vF1T3HE9rPOnXy3PU82-46dlvGwdAvsqUNXOx0o-KD8,20431
@@ -21,13 +21,13 @@ pygeoinf/nonlinear_optimisation.py,sha256=skK1ikn9GrVYherD64Qt9WrEYHA2NAJ48msOu_
21
21
  pygeoinf/parallel.py,sha256=VVFvNHszy4wSa9LuErIsch4NAkLaZezhdN9YpRROBJo,2267
22
22
  pygeoinf/plot.py,sha256=Uw9PCdxymUiAkFF0BS0kUAZBRWL6sh89FJnSIxtp_2s,13664
23
23
  pygeoinf/random_matrix.py,sha256=71l6eAXQ2pRMleaz1lXud6O1F78ugKyp3vHcRBXhdwM,17661
24
- pygeoinf/subspaces.py,sha256=5Bo1F9if5oEmKJ0YPWMFaajJKec5aGIrYZKvetwWa3c,10454
24
+ pygeoinf/subspaces.py,sha256=FJobjDRr8JG1zz-TjBsncJ1M5phQYwbttlaGuJz9ycU,13779
25
25
  pygeoinf/symmetric_space/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  pygeoinf/symmetric_space/circle.py,sha256=GuwVmLdHGTMxMrZfyXIPP3pz_y971ntlD5pl42lKJZ0,18796
27
- pygeoinf/symmetric_space/sh_tools.py,sha256=_A_coPElu8DNYwA5udL92X87wiTyBClTbFtf6ch9nXM,3520
28
- pygeoinf/symmetric_space/sphere.py,sha256=wZlAVG3kKMuPmRURL4r2ZP3sQ8kWOAWRk1G_Lo_TZmo,28983
27
+ pygeoinf/symmetric_space/sh_tools.py,sha256=k3bm2M-7-nprfKUwj1meIX3f8rpvkUPFM2moZFjvvog,3883
28
+ pygeoinf/symmetric_space/sphere.py,sha256=wYaZ2wqkQAHw9pn4vP_6LR9HAXSpzCncCh24xmSSC5A,28481
29
29
  pygeoinf/symmetric_space/symmetric_space.py,sha256=pEIZZYWsdegrYCwUs3bo86JTz3d2LsXFWdRYFa0syFs,17963
30
- pygeoinf-1.3.5.dist-info/METADATA,sha256=m0YVVuJ1McMBzUG1QYdurV1hB91UGmIiSn-STSq9a0s,16482
31
- pygeoinf-1.3.5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
32
- pygeoinf-1.3.5.dist-info/licenses/LICENSE,sha256=GrTQnKJemVi69FSbHprq60KN0OJGsOSR-joQoTq-oD8,1501
33
- pygeoinf-1.3.5.dist-info/RECORD,,
30
+ pygeoinf-1.3.6.dist-info/METADATA,sha256=4HHENA4PIYGX3S-Vi1RnjeOIBLMWeyLpMa_z-V3fv-k,16482
31
+ pygeoinf-1.3.6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
32
+ pygeoinf-1.3.6.dist-info/licenses/LICENSE,sha256=GrTQnKJemVi69FSbHprq60KN0OJGsOSR-joQoTq-oD8,1501
33
+ pygeoinf-1.3.6.dist-info/RECORD,,