pygeoinf 1.3.8__py3-none-any.whl → 1.4.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/subspaces.py CHANGED
@@ -1,18 +1,22 @@
1
1
  """
2
- Defines classes for representing affine and linear subspaces.
2
+ Defines classes for representing affine and linear subspaces, including
3
+ hyperplanes and half-spaces.
3
4
 
4
5
  The primary abstraction is the `AffineSubspace`, which represents a subset of
5
6
  a Hilbert space defined by a translation and a closed linear tangent space.
7
+ This module integrates with the `subset` module, allowing subspaces to be
8
+ treated as standard geometric sets.
6
9
  """
7
10
 
8
11
  from __future__ import annotations
9
12
  from typing import List, Optional, Any, Callable, TYPE_CHECKING
10
- import numpy as np
11
13
  import warnings
14
+ import numpy as np
12
15
 
13
16
  from .linear_operators import LinearOperator
14
17
  from .hilbert_space import HilbertSpace, Vector, EuclideanSpace
15
18
  from .linear_solvers import LinearSolver, CholeskySolver, IterativeLinearSolver
19
+ from .subsets import Subset, EmptySet
16
20
 
17
21
  if TYPE_CHECKING:
18
22
  from .gaussian_measure import GaussianMeasure
@@ -30,12 +34,26 @@ class OrthogonalProjector(LinearOperator):
30
34
  mapping: Callable[[Any], Any],
31
35
  complement_projector: Optional[LinearOperator] = None,
32
36
  ) -> None:
37
+ """
38
+ Initializes the orthogonal projector.
39
+
40
+ Args:
41
+ domain: The Hilbert space on which the projector acts.
42
+ mapping: The function implementing the projection P(x).
43
+ complement_projector: An optional LinearOperator representing (I - P).
44
+ If provided, it avoids re-computing the complement when requested.
45
+ """
33
46
  super().__init__(domain, domain, mapping, adjoint_mapping=mapping)
34
47
  self._complement_projector = complement_projector
35
48
 
36
49
  @property
37
50
  def complement(self) -> LinearOperator:
38
- """Returns the projector onto the orthogonal complement (I - P)."""
51
+ """
52
+ Returns the projector onto the orthogonal complement (I - P).
53
+
54
+ If a complement projector was not provided at initialization, one is
55
+ constructed automatically as the difference between the identity and self.
56
+ """
39
57
  if self._complement_projector is None:
40
58
  identity = self.domain.identity_operator()
41
59
  self._complement_projector = identity - self
@@ -48,7 +66,19 @@ class OrthogonalProjector(LinearOperator):
48
66
  basis_vectors: List[Vector],
49
67
  orthonormalize: bool = True,
50
68
  ) -> OrthogonalProjector:
51
- """Constructs a projector P onto the span of the provided basis vectors."""
69
+ """
70
+ Constructs a projector P onto the span of the provided basis vectors.
71
+
72
+ Args:
73
+ domain: The Hilbert space.
74
+ basis_vectors: A list of vectors spanning the subspace.
75
+ orthonormalize: If True, performs Gram-Schmidt orthonormalization
76
+ on the basis vectors before constructing the projector.
77
+ If False, assumes the basis is already orthonormal.
78
+
79
+ Returns:
80
+ An OrthogonalProjector instance.
81
+ """
52
82
  if not basis_vectors:
53
83
  return domain.zero_operator(domain)
54
84
 
@@ -62,9 +92,14 @@ class OrthogonalProjector(LinearOperator):
62
92
  return cls(domain, tensor_op)
63
93
 
64
94
 
65
- class AffineSubspace:
95
+ class AffineSubspace(Subset):
66
96
  """
67
97
  Represents an affine subspace A = x0 + V.
98
+
99
+ This class serves two primary roles:
100
+ 1. A geometric subset that can project points and check membership.
101
+ 2. A constraint definition for Bayesian inversion (conditioning a Gaussian
102
+ measure on the subspace).
68
103
  """
69
104
 
70
105
  def __init__(
@@ -78,7 +113,19 @@ class AffineSubspace:
78
113
  ) -> None:
79
114
  """
80
115
  Initializes the AffineSubspace.
116
+
117
+ Args:
118
+ projector: The orthogonal projector P onto the tangent space V.
119
+ translation: A vector x0 in the subspace. Defaults to the origin.
120
+ constraint_operator: The operator B defining the subspace implicitly
121
+ as {u | B(u) = w}. Used for Bayesian conditioning.
122
+ constraint_value: The RHS vector w for the implicit definition.
123
+ solver: A LinearSolver used to invert the constraint operator during
124
+ conditioning. If None, defaults to a CholeskySolver if an
125
+ explicit constraint operator is provided.
126
+ preconditioner: An optional preconditioner for iterative solvers.
81
127
  """
128
+ super().__init__(projector.domain)
82
129
  self._projector = projector
83
130
 
84
131
  if translation is None:
@@ -100,20 +147,24 @@ class AffineSubspace:
100
147
 
101
148
  self._preconditioner = preconditioner
102
149
 
103
- @property
104
- def domain(self) -> HilbertSpace:
105
- return self._projector.domain
106
-
107
150
  @property
108
151
  def translation(self) -> Vector:
152
+ """Returns the translation vector x0."""
109
153
  return self._translation
110
154
 
111
155
  @property
112
156
  def projector(self) -> OrthogonalProjector:
157
+ """Returns the orthogonal projector P onto the tangent space."""
113
158
  return self._projector
114
159
 
160
+ @property
161
+ def solver(self) -> Optional[LinearSolver]:
162
+ """Returns the linear solver associated with this subspace."""
163
+ return self._solver
164
+
115
165
  @property
116
166
  def tangent_space(self) -> LinearSubspace:
167
+ """Returns the LinearSubspace V parallel to this affine subspace."""
117
168
  return LinearSubspace(self._projector)
118
169
 
119
170
  @property
@@ -124,8 +175,10 @@ class AffineSubspace:
124
175
  @property
125
176
  def constraint_operator(self) -> LinearOperator:
126
177
  """
127
- Returns B for {u | B(u)=w}.
128
- Falls back to (I - P) if no explicit operator exists.
178
+ Returns the operator B defining the subspace as {u | B(u)=w}.
179
+
180
+ If no explicit operator was provided (geometric construction), this
181
+ falls back to the complement projector (I - P).
129
182
  """
130
183
  if self._constraint_operator is None:
131
184
  return self._projector.complement
@@ -134,8 +187,9 @@ class AffineSubspace:
134
187
  @property
135
188
  def constraint_value(self) -> Vector:
136
189
  """
137
- Returns w for {u | B(u)=w}.
138
- Falls back to (I - P)x0 if no explicit operator exists.
190
+ Returns the value w defining the subspace as {u | B(u)=w}.
191
+
192
+ If no explicit operator was provided, this falls back to (I - P)x0.
139
193
  """
140
194
  if self._constraint_value is None:
141
195
  complement = self._projector.complement
@@ -143,25 +197,66 @@ class AffineSubspace:
143
197
  return self._constraint_value
144
198
 
145
199
  def project(self, x: Vector) -> Vector:
146
- """Orthogonally projects x onto the affine subspace."""
200
+ """
201
+ Orthogonally projects a vector x onto the affine subspace.
202
+
203
+ Formula: P_A(x) = P(x - x0) + x0
204
+ """
147
205
  diff = self.domain.subtract(x, self.translation)
148
206
  proj_diff = self.projector(diff)
149
207
  return self.domain.add(self.translation, proj_diff)
150
208
 
151
- def is_element(self, x: Vector, rtol: float = 1e-6) -> bool:
152
- """Returns True if x lies in the subspace."""
209
+ def is_element(self, x: Vector, /, *, rtol: float = 1e-6) -> bool:
210
+ """
211
+ Returns True if the vector x lies within the subspace.
212
+
213
+ Checks if the projection residual ||x - P_A(x)|| is small relative
214
+ to the norm of x (or 1.0).
215
+
216
+ Args:
217
+ x: The vector to check.
218
+ rtol: Relative tolerance for the residual check.
219
+ """
153
220
  proj = self.project(x)
154
221
  diff = self.domain.subtract(x, proj)
155
222
  norm_diff = self.domain.norm(diff)
223
+
224
+ # Scale tolerance by norm of x to handle units/scaling, consistent with Sphere/Ball
156
225
  norm_x = self.domain.norm(x)
157
- scale = norm_x if norm_x > 1e-12 else 1.0
226
+ scale = max(1.0, norm_x)
158
227
  return norm_diff <= rtol * scale
159
228
 
229
+ @property
230
+ def boundary(self) -> Subset:
231
+ """
232
+ Returns the boundary of the affine subspace.
233
+
234
+ Geometrically, an affine subspace (like a line or plane) is a closed
235
+ manifold without a boundary. Returns EmptySet.
236
+ """
237
+ return EmptySet(self.domain)
238
+
160
239
  def condition_gaussian_measure(
161
240
  self, prior: GaussianMeasure, geometric: bool = False
162
241
  ) -> GaussianMeasure:
163
242
  """
164
243
  Conditions a Gaussian measure on this subspace.
244
+
245
+ Args:
246
+ prior: The prior Gaussian measure.
247
+ geometric: If True, performs a geometric projection of the measure
248
+ (equivalent to conditioning on "measurement = truth" with infinite
249
+ precision, effectively squashing the distribution onto the
250
+ subspace).
251
+ If False (default), performs standard Bayesian conditioning
252
+ using the constraint equation B(u) = w.
253
+
254
+ Returns:
255
+ The posterior (conditioned) GaussianMeasure.
256
+
257
+ Raises:
258
+ ValueError: If geometric=False and the subspace was constructed
259
+ without a solver capable of handling the constraint operator.
165
260
  """
166
261
  if geometric:
167
262
  # Geometric Projection: u -> P(u - x0) + x0
@@ -183,7 +278,7 @@ class AffineSubspace:
183
278
  "AffineSubspace constructor."
184
279
  )
185
280
 
186
- # Local imports
281
+ # Local imports to avoid circular dependency
187
282
  from .forward_problem import LinearForwardProblem
188
283
  from .linear_bayesian import LinearBayesianInversion
189
284
 
@@ -205,7 +300,16 @@ class AffineSubspace:
205
300
  solver: Optional[LinearSolver] = None,
206
301
  preconditioner: Optional[LinearOperator] = None,
207
302
  ) -> AffineSubspace:
208
- """Constructs subspace from B(u)=w."""
303
+ """
304
+ Constructs a subspace defined by the linear equation B(u) = w.
305
+
306
+ Args:
307
+ operator: The linear operator B.
308
+ value: The RHS vector w.
309
+ solver: Solver used to invert the Gram matrix (B B*) during
310
+ construction and later conditioning. Defaults to CholeskySolver.
311
+ preconditioner: Optional preconditioner for iterative solvers.
312
+ """
209
313
  domain = operator.domain
210
314
  G = operator @ operator.adjoint
211
315
 
@@ -284,8 +388,16 @@ class AffineSubspace:
284
388
  orthonormalize: bool = True,
285
389
  ) -> AffineSubspace:
286
390
  """
287
- Constructs subspace from complement basis.
288
- Constraint is explicit: <u, e_i> = <x0, e_i>.
391
+ Constructs a subspace defined by orthogonality to a set of complement basis vectors.
392
+
393
+ The subspace is defined as {u | <u - x0, v_i> = 0} for all v_i in basis.
394
+ This provides an explicit constraint operator B where B(u)_i = <u, v_i>.
395
+
396
+ Args:
397
+ domain: The Hilbert space.
398
+ basis_vectors: Basis vectors for the orthogonal complement.
399
+ translation: A point x0 in the subspace.
400
+ orthonormalize: If True, orthonormalizes the complement basis.
289
401
  """
290
402
  if orthonormalize:
291
403
  e_vectors = domain.gram_schmidt(basis_vectors)
@@ -342,13 +454,23 @@ class LinearSubspace(AffineSubspace):
342
454
  """
343
455
 
344
456
  def __init__(self, projector: OrthogonalProjector) -> None:
457
+ """
458
+ Initializes the LinearSubspace.
459
+
460
+ Args:
461
+ projector: The orthogonal projector P onto the subspace.
462
+ """
345
463
  super().__init__(projector, translation=None)
346
464
 
347
465
  @property
348
466
  def complement(self) -> LinearSubspace:
467
+ """
468
+ Returns the orthogonal complement of this subspace as a new LinearSubspace.
469
+ """
349
470
  op_perp = self.projector.complement
350
471
  if isinstance(op_perp, OrthogonalProjector):
351
472
  return LinearSubspace(op_perp)
473
+ # Wrap if the complement isn't strictly an OrthogonalProjector instance
352
474
  p_perp = OrthogonalProjector(self.domain, op_perp._mapping)
353
475
  return LinearSubspace(p_perp)
354
476
 
@@ -359,13 +481,22 @@ class LinearSubspace(AffineSubspace):
359
481
  solver: Optional[LinearSolver] = None,
360
482
  preconditioner: Optional[LinearOperator] = None,
361
483
  ) -> LinearSubspace:
484
+ """
485
+ Constructs the subspace corresponding to the kernel (null space) of an operator.
486
+ K = {u | A(u) = 0}.
487
+
488
+ Args:
489
+ operator: The operator A.
490
+ solver: Solver used for the Gram matrix (A A*).
491
+ preconditioner: Optional preconditioner.
492
+ """
362
493
  affine = AffineSubspace.from_linear_equation(
363
494
  operator, operator.codomain.zero, solver, preconditioner
364
495
  )
365
496
  instance = cls(affine.projector)
366
497
  instance._constraint_operator = operator
367
498
  instance._constraint_value = operator.codomain.zero
368
- instance._solver = affine._solver
499
+ instance._solver = affine.solver
369
500
  instance._preconditioner = preconditioner
370
501
  return instance
371
502
 
@@ -378,6 +509,16 @@ class LinearSubspace(AffineSubspace):
378
509
  solver: Optional[LinearSolver] = None,
379
510
  preconditioner: Optional[LinearOperator] = None,
380
511
  ) -> LinearSubspace:
512
+ """
513
+ Constructs a linear subspace from a set of basis vectors.
514
+
515
+ Args:
516
+ domain: The Hilbert space.
517
+ basis_vectors: List of vectors spanning the subspace.
518
+ orthonormalize: Whether to orthonormalize the basis.
519
+ solver: Optional solver for implicit constraints (see AffineSubspace.from_tangent_basis).
520
+ preconditioner: Optional preconditioner.
521
+ """
381
522
  projector = OrthogonalProjector.from_basis(
382
523
  domain, basis_vectors, orthonormalize=orthonormalize
383
524
  )
@@ -393,11 +534,20 @@ class LinearSubspace(AffineSubspace):
393
534
  basis_vectors: List[Vector],
394
535
  orthonormalize: bool = True,
395
536
  ) -> LinearSubspace:
537
+ """
538
+ Constructs a linear subspace defined by orthogonality to a complement basis.
539
+ S = {u | <u, v_i> = 0}.
540
+
541
+ Args:
542
+ domain: The Hilbert space.
543
+ basis_vectors: Basis vectors for the complement.
544
+ orthonormalize: Whether to orthonormalize the complement basis.
545
+ """
396
546
  affine = AffineSubspace.from_complement_basis(
397
547
  domain, basis_vectors, translation=None, orthonormalize=orthonormalize
398
548
  )
399
549
  instance = cls(affine.projector)
400
550
  instance._constraint_operator = affine.constraint_operator
401
551
  instance._constraint_value = affine.constraint_value
402
- instance._solver = affine._solver
552
+ instance._solver = affine.solver
403
553
  return instance
@@ -24,7 +24,7 @@ Key Classes
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
- from typing import Callable, Tuple, Optional, Any
27
+ from typing import Callable, Tuple, Optional, Any, List
28
28
  import matplotlib.pyplot as plt
29
29
  import numpy as np
30
30
  from scipy.fft import rfft, irfft
@@ -226,6 +226,40 @@ class CircleHelper:
226
226
  ax.fill_between(self.angles(), u - u_bound, u + u_bound, **kwargs)
227
227
  return fig, ax
228
228
 
229
+ def geodesic_quadrature(
230
+ self, p1: float, p2: float, n_points: int
231
+ ) -> Tuple[List[float], np.ndarray]:
232
+ """
233
+ Returns quadrature points and weights for the shortest arc between p1 and p2.
234
+
235
+ Args:
236
+ p1: Starting angle in radians.
237
+ p2: Ending angle in radians.
238
+ n_points: Number of quadrature points.
239
+
240
+ Returns:
241
+ points: A list of angles (floats) along the shortest arc.
242
+ weights: Integration weights scaled by the arc length.
243
+ """
244
+ # Calculate the shortest signed angular distance on the circle
245
+ # This ensures we take the "inner" arc rather than the long way around.
246
+ diff = (p2 - p1 + np.pi) % (2 * np.pi) - np.pi
247
+ arc_length = np.abs(diff) * self.radius
248
+
249
+ # Get standard Gauss-Legendre nodes (x) and weights (w) on [-1, 1]
250
+ x, w = np.polynomial.legendre.leggauss(n_points)
251
+
252
+ # Map nodes to the angular interval [p1, p1 + diff]
253
+ # t moves from 0 to 1 as x moves from -1 to 1
254
+ t = (x + 1) / 2.0
255
+ angles = p1 + t * diff
256
+
257
+ # Scale weights: (w * 0.5) maps [-1, 1] to [0, 1]
258
+ # Multiplying by total arc_length gives the proper integration weights.
259
+ scaled_weights = w * (arc_length / 2.0)
260
+
261
+ return angles.tolist(), scaled_weights
262
+
229
263
  def _coefficient_to_component(self, coeff: np.ndarray) -> np.ndarray:
230
264
  """Packs complex Fourier coefficients into a real component vector."""
231
265
  # For a real-valued input, the output of rfft (real FFT) has
@@ -346,6 +380,12 @@ class Lebesgue(CircleHelper, HilbertModule, AbstractInvariantLebesgueSpace):
346
380
  return False
347
381
  return True
348
382
 
383
+ def vector_sqrt(self, u: np.ndarray) -> np.ndarray:
384
+ """
385
+ Returns the pointwise square root of a function.
386
+ """
387
+ return np.sqrt(u)
388
+
349
389
  def invariant_automorphism_from_index_function(self, g: Callable[[int], float]):
350
390
  """
351
391
  Implements an invariant automorphism of the form f(Δ) using Fourier