pygeoinf 1.2.0__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,12 +21,12 @@ Key Classes
21
21
  from __future__ import annotations
22
22
  from typing import Optional, Union
23
23
 
24
- from .operators import Operator
24
+ from .nonlinear_operators import NonLinearOperator
25
25
  from .inversion import Inversion
26
26
 
27
27
 
28
28
  from .forward_problem import LinearForwardProblem
29
- from .operators import LinearOperator
29
+ from .linear_operators import LinearOperator
30
30
  from .linear_solvers import LinearSolver, IterativeLinearSolver
31
31
  from .hilbert_space import Vector
32
32
 
@@ -131,7 +131,7 @@ class LinearLeastSquaresInversion(Inversion):
131
131
  @ inverse_data_covariance
132
132
  )(shifted_data)
133
133
 
134
- return Operator(self.data_space, self.model_space, mapping)
134
+ return NonLinearOperator(self.data_space, self.model_space, mapping)
135
135
 
136
136
  else:
137
137
  return inverse_normal_operator @ forward_operator.adjoint
@@ -262,7 +262,7 @@ class LinearMinimumNormInversion(Inversion):
262
262
 
263
263
  raise RuntimeError("Bracketing search failed to converge.")
264
264
 
265
- return Operator(self.data_space, self.model_space, mapping)
265
+ return NonLinearOperator(self.data_space, self.model_space, mapping)
266
266
 
267
267
  else:
268
268
  # For error-free data, compute the minimum-norm solution via A*(A*A)^-1
@@ -24,15 +24,10 @@ from typing import Callable, Optional, Dict, Any
24
24
 
25
25
  import numpy as np
26
26
  from scipy.sparse.linalg import LinearOperator as ScipyLinOp
27
- from scipy.linalg import (
28
- cho_factor,
29
- cho_solve,
30
- lu_factor,
31
- lu_solve,
32
- )
27
+ from scipy.linalg import cho_factor, cho_solve, lu_factor, lu_solve, eigh
33
28
  from scipy.sparse.linalg import gmres, bicgstab, cg, bicg
34
29
 
35
- from .operators import LinearOperator
30
+ from .linear_operators import LinearOperator
36
31
  from .hilbert_space import Vector
37
32
 
38
33
 
@@ -167,6 +162,78 @@ class CholeskySolver(DirectLinearSolver):
167
162
  )
168
163
 
169
164
 
165
+ class EigenSolver(DirectLinearSolver):
166
+ """
167
+ A direct linear solver based on the eigendecomposition of a symmetric operator.
168
+
169
+ This solver is robust for symmetric operators that may be singular or
170
+ numerically ill-conditioned. In such cases, it computes a pseudo-inverse by
171
+ regularizing the eigenvalues, treating those close to zero (relative to the largest
172
+ eigenvalue) as exactly zero.
173
+ """
174
+
175
+ def __init__(
176
+ self,
177
+ /,
178
+ *,
179
+ galerkin: bool = False,
180
+ parallel: bool = False,
181
+ n_jobs: int = -1,
182
+ rtol: float = 1e-12,
183
+ ) -> None:
184
+ """
185
+ Args:
186
+ galerkin (bool): If True, the Galerkin matrix representation is used.
187
+ parallel (bool): If True, parallel computation is used.
188
+ n_jobs (int): Number of parallel jobs.
189
+ rtol (float): Relative tolerance for treating eigenvalues as zero.
190
+ An eigenvalue `s` is treated as zero if
191
+ `abs(s) < rtol * max(abs(eigenvalues))`.
192
+ """
193
+ super().__init__(galerkin=galerkin, parallel=parallel, n_jobs=n_jobs)
194
+ self._rtol = rtol
195
+
196
+ def __call__(self, operator: LinearOperator) -> LinearOperator:
197
+ """
198
+ Computes the pseudo-inverse of a self-adjoint LinearOperator.
199
+ """
200
+ assert operator.is_automorphism
201
+
202
+ matrix = operator.matrix(
203
+ dense=True,
204
+ galerkin=self._galerkin,
205
+ parallel=self._parallel,
206
+ n_jobs=self._n_jobs,
207
+ )
208
+
209
+ eigenvalues, eigenvectors = eigh(matrix)
210
+
211
+ max_abs_eigenvalue = np.max(np.abs(eigenvalues))
212
+ if max_abs_eigenvalue > 0:
213
+ threshold = self._rtol * max_abs_eigenvalue
214
+ else:
215
+ threshold = 0
216
+
217
+ inv_eigenvalues = np.where(
218
+ np.abs(eigenvalues) > threshold,
219
+ np.reciprocal(eigenvalues),
220
+ 0.0,
221
+ )
222
+
223
+ def matvec(cy: np.ndarray) -> np.ndarray:
224
+ z = eigenvectors.T @ cy
225
+ w = inv_eigenvalues * z
226
+ return eigenvectors @ w
227
+
228
+ inverse_matrix = ScipyLinOp(
229
+ (operator.domain.dim, operator.codomain.dim), matvec=matvec, rmatvec=matvec
230
+ )
231
+
232
+ return LinearOperator.from_matrix(
233
+ operator.domain, operator.domain, inverse_matrix, galerkin=self._galerkin
234
+ )
235
+
236
+
170
237
  class IterativeLinearSolver(LinearSolver):
171
238
  """
172
239
  An abstract base class for iterative linear solvers.
@@ -0,0 +1,225 @@
1
+ """
2
+ Provides the `NonLinearForm` base class to represent non-linear functionals.
3
+
4
+ A non-linear form, or functional, is a mapping from a vector in a Hilbert
5
+ space to a scalar. This class provides a foundational structure for these
6
+ functionals, equipping them with algebraic operations and an interface for
7
+ derivatives like gradients and Hessians.
8
+ """
9
+
10
+ from __future__ import annotations
11
+ from typing import Callable, Optional, Any, TYPE_CHECKING
12
+
13
+
14
+ # This block only runs for type checkers, not at runtime
15
+ if TYPE_CHECKING:
16
+ from .hilbert_space import HilbertSpace, Vector
17
+ from .linear_forms import LinearForm
18
+ from .linear_operators import LinearOperator
19
+
20
+
21
+ class NonLinearForm:
22
+ """
23
+ Represents a general non-linear functional that maps vectors to scalars.
24
+
25
+ This class serves as the foundation for all forms. It defines the basic
26
+ callable interface `form(x)` and overloads arithmetic operators (`+`, `-`, `*`)
27
+ to create new forms. It also provides an optional framework for specifying
28
+ a form's gradient and Hessian.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ domain: HilbertSpace,
34
+ mapping: Callable[[Vector], float],
35
+ /,
36
+ *,
37
+ gradient: Optional[Callable[[Vector], Vector]] = None,
38
+ hessian: Optional[Callable[[Vector], LinearOperator]] = None,
39
+ ) -> None:
40
+ """
41
+ Initializes the NonLinearForm.
42
+
43
+ Args:
44
+ domain: The Hilbert space on which the form is defined.
45
+ mapping: The function `f(x)` that defines the action of the form.
46
+ gradient: An optional function that computes the gradient of the form.
47
+ hessian: An optional function that computes the Hessian of the form.
48
+ """
49
+
50
+ self._domain: HilbertSpace = domain
51
+ self._mapping = mapping
52
+ self._gradient = gradient
53
+ self._hessian = hessian
54
+
55
+ @property
56
+ def domain(self) -> HilbertSpace:
57
+ """The Hilbert space on which the form is defined."""
58
+ return self._domain
59
+
60
+ @property
61
+ def has_gradient(self) -> bool:
62
+ """True if the form has a gradient."""
63
+ return self._gradient is not None
64
+
65
+ @property
66
+ def has_hessian(self) -> bool:
67
+ """True if the form has a Hessian."""
68
+ return self._hessian is not None
69
+
70
+ def __call__(self, x: Any) -> float:
71
+ """Applies the linear form to a vector."""
72
+ return self._mapping(x)
73
+
74
+ def gradient(self, x: Any) -> Vector:
75
+ """
76
+ Computes the gradient of the form at a given point.
77
+
78
+ Args:
79
+ x: The vector at which to evaluate the gradient.
80
+
81
+ Returns:
82
+ The gradient of the form as a vector in the domain space.
83
+
84
+ Raises:
85
+ NotImplementedError: If a gradient function was not provided
86
+ during initialization.
87
+ """
88
+ if self._gradient is None:
89
+ raise NotImplementedError("Gradient not implemented for this form.")
90
+ return self._gradient(x)
91
+
92
+ def derivative(self, x: Vector) -> LinearForm:
93
+ """
94
+ Computes the derivative of the form at a given point.
95
+
96
+ Args:
97
+ x: The vector at which to evaluate the derivative.
98
+
99
+ Returns:
100
+ The derivative of the form as a `LinearForm`.
101
+
102
+ Raises:
103
+ NotImplementedError: If a gradient function was not provided
104
+ during initialization.
105
+ """
106
+ return self.domain.to_dual(self.gradient(x))
107
+
108
+ def hessian(self, x: Any) -> LinearOperator:
109
+ """
110
+ Computes the Hessian of the form at a given point.
111
+
112
+ Args:
113
+ x: The vector at which to evaluate the Hessian.
114
+
115
+ Returns:
116
+ The Hessian of the form as a LinearOperator mapping the domain to itself.
117
+
118
+ Raises:
119
+ NotImplementedError: If a Hessian function was not provided
120
+ during initialization.
121
+ """
122
+ if self._hessian is None:
123
+ raise NotImplementedError("Hessian not implemented for this form.")
124
+ return self._hessian(x)
125
+
126
+ def __neg__(self) -> NonLinearForm:
127
+ """Returns the additive inverse of the form."""
128
+
129
+ if self._gradient is None:
130
+ gradient = None
131
+ else:
132
+
133
+ def gradient(x: Vector) -> Vector:
134
+ return self.domain.negative(self.gradient(x))
135
+
136
+ if self._hessian is None:
137
+ hessian = None
138
+ else:
139
+
140
+ def hessian(x: Vector) -> LinearOperator:
141
+ return -self.hessian(x)
142
+
143
+ return NonLinearForm(
144
+ self.domain, lambda x: -self(x), gradient=gradient, hessian=hessian
145
+ )
146
+
147
+ def __mul__(self, a: float) -> NonLinearForm:
148
+ """Returns the product of the form and a scalar."""
149
+
150
+ if self._gradient is None:
151
+ gradient = None
152
+ else:
153
+
154
+ def gradient(x: Vector) -> Vector:
155
+ return self.domain.multiply(a, self.gradient(x))
156
+
157
+ if self._hessian is None:
158
+ hessian = None
159
+ else:
160
+
161
+ def hessian(x: Vector) -> LinearOperator:
162
+ return a * self.hessian(x)
163
+
164
+ return NonLinearForm(
165
+ self.domain,
166
+ lambda x: a * self(x),
167
+ gradient=gradient,
168
+ hessian=hessian,
169
+ )
170
+
171
+ def __rmul__(self, a: float) -> NonLinearForm:
172
+ """Returns the product of the form and a scalar."""
173
+ return self * a
174
+
175
+ def __truediv__(self, a: float) -> NonLinearForm:
176
+ """Returns the division of the form by a scalar."""
177
+ return self * (1.0 / a)
178
+
179
+ def __add__(self, other: NonLinearForm) -> NonLinearForm:
180
+ """Returns the sum of this form and another."""
181
+
182
+ if self._gradient is None or other._gradient is None:
183
+ gradient = None
184
+ else:
185
+
186
+ def gradient(x: Vector) -> Vector:
187
+ return self.domain.add(self.gradient(x), other.gradient(x))
188
+
189
+ if self._hessian is None or other._hessian is None:
190
+ hessian = None
191
+ else:
192
+
193
+ def hessian(x: Vector) -> LinearOperator:
194
+ return self.hessian(x) + other.hessian(x)
195
+
196
+ return NonLinearForm(
197
+ self.domain,
198
+ lambda x: self(x) + other(x),
199
+ gradient=gradient,
200
+ hessian=hessian,
201
+ )
202
+
203
+ def __sub__(self, other: NonLinearForm) -> NonLinearForm:
204
+ """Returns the difference between this form and another."""
205
+
206
+ if self._gradient is None or other._gradient is None:
207
+ gradient = None
208
+ else:
209
+
210
+ def gradient(x: Vector) -> Vector:
211
+ return self.domain.subtract(self.gradient(x), other.gradient(x))
212
+
213
+ if self._hessian is None or other._hessian is None:
214
+ hessian = None
215
+ else:
216
+
217
+ def hessian(x: Vector) -> LinearOperator:
218
+ return self.hessian(x) - other.hessian(x)
219
+
220
+ return NonLinearForm(
221
+ self.domain,
222
+ lambda x: self(x) - other(x),
223
+ gradient=gradient,
224
+ hessian=hessian,
225
+ )
@@ -0,0 +1,209 @@
1
+ """
2
+ Provides the `NonLinearOperator` base class for mappings between Hilbert spaces.
3
+
4
+ A non-linear operator is a general mapping `F(x)` from a vector `x` in a
5
+ domain Hilbert space to a vector `y` in a codomain Hilbert space. This class
6
+ provides a foundational structure for these mappings, equipping them with
7
+ algebraic operations and an interface for the Frécher derivative.
8
+ """
9
+
10
+ from __future__ import annotations
11
+ from typing import Callable, Optional, Any, TYPE_CHECKING
12
+
13
+
14
+ # This block only runs for type checkers, not at runtime
15
+ if TYPE_CHECKING:
16
+ from .hilbert_space import HilbertSpace, EuclideanSpace, Vector
17
+ from .linear_operators import LinearOperator
18
+
19
+
20
+ class NonLinearOperator:
21
+ """
22
+ Represents a general non-linear operator that maps vectors to vectors.
23
+
24
+ This class provides a functional representation for an operator `F(x)`,
25
+ and includes an interface for its Fréchet derivative, F'(x), which is the
26
+ linear operator that best approximates F at a given point x. It serves
27
+ as the base class for the more specialized `LinearOperator`.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ domain: HilbertSpace,
33
+ codomain: HilbertSpace,
34
+ mapping: Callable[[Vector], Any],
35
+ /,
36
+ *,
37
+ derivative: Callable[[Vector], LinearOperator] = None,
38
+ ) -> None:
39
+ """Initializes the NonLinearOperator.
40
+
41
+ Args:
42
+ domain: The Hilbert space from which the operator maps.
43
+ codomain: The Hilbert space to which the operator maps.
44
+ mapping: The function `F(x)` that defines the mapping.
45
+ derivative: An optional function that takes a vector `x` and
46
+ returns the Fréchet derivative (a `LinearOperator`) at
47
+ that point.
48
+ """
49
+ self._domain: HilbertSpace = domain
50
+ self._codomain: HilbertSpace = codomain
51
+ self._mapping: Callable[[Any], Any] = mapping
52
+ self._derivative: Callable[[Any], LinearOperator] = derivative
53
+
54
+ @property
55
+ def domain(self) -> HilbertSpace:
56
+ """The domain of the operator."""
57
+ return self._domain
58
+
59
+ @property
60
+ def codomain(self) -> HilbertSpace:
61
+ """The codomain of the operator."""
62
+ return self._codomain
63
+
64
+ @property
65
+ def is_automorphism(self) -> bool:
66
+ """True if the operator maps a space into itself."""
67
+ return self.domain == self.codomain
68
+
69
+ @property
70
+ def is_square(self) -> bool:
71
+ """True if the operator's domain and codomain have the same dimension."""
72
+ return self.domain.dim == self.codomain.dim
73
+
74
+ def __call__(self, x: Any) -> Any:
75
+ """Applies the operator's mapping to a vector."""
76
+ return self._mapping(x)
77
+
78
+ def derivative(self, x: Vector) -> LinearOperator:
79
+ """Computes the Fréchet derivative of the operator at a given point.
80
+
81
+ The Fréchet derivative is the linear operator that best approximates
82
+ the non-linear operator in the neighborhood of the point `x`.
83
+
84
+ Args:
85
+ x: The point at which to compute the derivative.
86
+
87
+ Returns:
88
+ The derivative as a `LinearOperator`.
89
+
90
+ Raises:
91
+ NotImplementedError: If a derivative function was not provided.
92
+ """
93
+ if self._derivative is None:
94
+ raise NotImplementedError("Derivative not implemented")
95
+ return self._derivative(x)
96
+
97
+ def __neg__(self) -> NonLinearOperator:
98
+ domain = self.domain
99
+ codomain = self.codomain
100
+
101
+ def mapping(x: Any) -> Any:
102
+ return codomain.negative(self(x))
103
+
104
+ if self._derivative is not None:
105
+
106
+ def derivative(x: Vector) -> LinearOperator:
107
+ return -self.derivative(x)
108
+
109
+ else:
110
+ derivative = None
111
+
112
+ return NonLinearOperator(domain, codomain, mapping, derivative=derivative)
113
+
114
+ def __mul__(self, a: float) -> NonLinearOperator:
115
+ domain = self.domain
116
+ codomain = self.codomain
117
+
118
+ def mapping(x: Any) -> Any:
119
+ return codomain.multiply(a, self(x))
120
+
121
+ if self._derivative is not None:
122
+
123
+ def derivative(x: Vector) -> LinearOperator:
124
+ return a * self.derivative(x)
125
+
126
+ else:
127
+ derivative = None
128
+
129
+ return NonLinearOperator(domain, codomain, mapping, derivative=derivative)
130
+
131
+ def __rmul__(self, a: float) -> NonLinearOperator:
132
+ return self * a
133
+
134
+ def __truediv__(self, a: float) -> NonLinearOperator:
135
+ return self * (1.0 / a)
136
+
137
+ def __add__(self, other: NonLinearOperator) -> NonLinearOperator:
138
+
139
+ if not isinstance(other, NonLinearOperator):
140
+ raise TypeError("Operand must be a NonLinearOperator")
141
+
142
+ domain = self.domain
143
+ codomain = self.codomain
144
+
145
+ def mapping(x: Any) -> Any:
146
+ return codomain.add(self(x), other(x))
147
+
148
+ if self._derivative is not None and other._derivative is not None:
149
+
150
+ def derivative(x: Vector) -> LinearOperator:
151
+ return self.derivative(x) + other.derivative(x)
152
+
153
+ else:
154
+ derivative = None
155
+
156
+ return NonLinearOperator(domain, codomain, mapping, derivative=derivative)
157
+
158
+ def __sub__(self, other: NonLinearOperator) -> NonLinearOperator:
159
+
160
+ if not isinstance(other, NonLinearOperator):
161
+ raise TypeError("Operand must be a NonLinearOperator")
162
+
163
+ domain = self.domain
164
+ codomain = self.codomain
165
+
166
+ def mapping(x: Any) -> Any:
167
+ return codomain.subtract(self(x), other(x))
168
+
169
+ if self._derivative is not None and other._derivative is not None:
170
+
171
+ def derivative(x: Vector) -> LinearOperator:
172
+ return self.derivative(x) - other.derivative(x)
173
+
174
+ else:
175
+ derivative = None
176
+
177
+ return NonLinearOperator(domain, codomain, mapping, derivative=derivative)
178
+
179
+ def __matmul__(self, other: NonLinearOperator) -> NonLinearOperator:
180
+ """Composes this operator with another: `(self @ other)(x) = self(other(x))`.
181
+
182
+ The derivative of the composed operator is computed using the chain rule:
183
+ `(F o G)'(x) = F'(G(x)) @ G'(x)`.
184
+
185
+ Args:
186
+ other: The operator to apply before this one.
187
+
188
+ Returns:
189
+ A new `NonLinearOperator` representing the composition.
190
+ """
191
+
192
+ if not isinstance(other, NonLinearOperator):
193
+ raise TypeError("Operand must be a NonLinearOperator")
194
+
195
+ domain = other.domain
196
+ codomain = self.codomain
197
+
198
+ def mapping(x: Any) -> Any:
199
+ return self(other(x))
200
+
201
+ if self._derivative is not None and other._derivative is not None:
202
+
203
+ def derivative(x: Vector) -> LinearOperator:
204
+ return self.derivative(other(x)) @ other.derivative(x)
205
+
206
+ else:
207
+ derivative = None
208
+
209
+ return NonLinearOperator(domain, codomain, mapping, derivative=derivative)