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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pygeoinf/inversion.py CHANGED
@@ -13,22 +13,25 @@ inversion techniques, such as the existence of a data error measure.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- from .forward_problem import LinearForwardProblem
16
+
17
17
  from .hilbert_space import HilbertSpace
18
+ from .nonlinear_operators import NonLinearOperator
19
+ from .linear_operators import LinearOperator
20
+ from .forward_problem import LinearForwardProblem, ForwardProblem
18
21
 
19
22
 
20
23
  class Inversion:
21
24
  """
22
- An abstract base class for inversion methods.
25
+ A base class for inversion methods.
23
26
 
24
- This class provides a common structure for different inversion algorithms
27
+ This class provides a common structure for different inversion and inference algorithms
25
28
  (e.g., Bayesian, Least Squares). Its main purpose is to hold a reference
26
29
  to the forward problem being solved and provide convenient access to its
27
30
  properties. Subclasses should inherit from this class to implement a
28
- specific inversion technique.
31
+ specific inversion algorithm.
29
32
  """
30
33
 
31
- def __init__(self, forward_problem: "LinearForwardProblem", /) -> None:
34
+ def __init__(self, forward_problem: ForwardProblem, /) -> None:
32
35
  """
33
36
  Initializes the Inversion class.
34
37
 
@@ -36,20 +39,20 @@ class Inversion:
36
39
  forward_problem: An instance of a forward problem that defines the
37
40
  relationship between model parameters and data.
38
41
  """
39
- self._forward_problem: "LinearForwardProblem" = forward_problem
42
+ self._forward_problem: ForwardProblem = forward_problem
40
43
 
41
44
  @property
42
- def forward_problem(self) -> "LinearForwardProblem":
45
+ def forward_problem(self) -> ForwardProblem:
43
46
  """The forward problem associated with this inversion."""
44
47
  return self._forward_problem
45
48
 
46
49
  @property
47
- def model_space(self) -> "HilbertSpace":
50
+ def model_space(self) -> HilbertSpace:
48
51
  """The model space (domain) of the forward problem."""
49
52
  return self.forward_problem.model_space
50
53
 
51
54
  @property
52
- def data_space(self) -> "HilbertSpace":
55
+ def data_space(self) -> HilbertSpace:
53
56
  """The data space (codomain) of the forward problem."""
54
57
  return self.forward_problem.data_space
55
58
 
@@ -83,3 +86,92 @@ class Inversion:
83
86
  raise AttributeError(
84
87
  "An inverse data covariance (precision) operator is required for this inversion method."
85
88
  )
89
+
90
+
91
+ class LinearInversion(Inversion):
92
+ """
93
+ An abstract base class for linear inversion algorithms.
94
+ """
95
+
96
+ def __init__(self, forward_problem: LinearForwardProblem, /) -> None:
97
+ """
98
+ Initializes the LinearInversion class.
99
+
100
+ Args:
101
+ forward_problem: An instance of a linear forward problem.
102
+ """
103
+ if not isinstance(forward_problem, LinearForwardProblem):
104
+ raise ValueError("Forward problem must be a LinearForwardProblem.")
105
+ super().__init__(forward_problem)
106
+
107
+
108
+ class Inference(Inversion):
109
+ """
110
+ A base class for inference algorithms. These methods inherit common functionality from
111
+ the inversion base class, but need not themselves derive from a specific inversion scheme.
112
+
113
+ Within an inference problem, the aim is to estimate some property of the unknown model,
114
+ and hence a property operator mapping from the model to a property space must be
115
+ specified.
116
+ """
117
+
118
+ def __init__(
119
+ self, forward_problem: ForwardProblem, property_operator: NonLinearOperator
120
+ ) -> None:
121
+ """
122
+ Initializes the Inference class.
123
+
124
+ Args:
125
+ forward_problem: An instance of a forward problem that defines the
126
+ relationship between model parameters and data.
127
+ property_operator: A mapping takes elements of the model space to
128
+ property vector of interest.
129
+ """
130
+
131
+ super().__init__(forward_problem)
132
+
133
+ if property_operator.domain != self.model_space:
134
+ raise ValueError("Property operator incompatible with model space")
135
+
136
+ self._property_operator = property_operator
137
+
138
+ @property
139
+ def property_operator(self) -> NonLinearOperator:
140
+ """
141
+ Returns the property operator.
142
+ """
143
+ return self._property_operator
144
+
145
+ @property
146
+ def property_space(self) -> HilbertSpace:
147
+ """
148
+ Returns the property space.
149
+ """
150
+ return self.property_operator.codomain
151
+
152
+
153
+ class LinearInference(Inference):
154
+ """
155
+ A base class for linear inference algorithms.
156
+ """
157
+
158
+ def __init__(
159
+ self, forward_problem: LinearForwardProblem, property_operator: LinearOperator
160
+ ) -> None:
161
+ """
162
+ Initializes the LinearInference class.
163
+
164
+ Args:
165
+ forward_problem: An instance of a linear forward problem that defines the
166
+ relationship between model parameters and data.
167
+ property_operator: A linear mapping takes elements of the model space to
168
+ property vector of interest.
169
+ """
170
+
171
+ if not isinstance(forward_problem, LinearForwardProblem):
172
+ raise ValueError("Forward problem must be linear")
173
+
174
+ if not isinstance(property_operator, LinearOperator):
175
+ raise ValueError("Property mapping must be linear")
176
+
177
+ super().__init__(forward_problem, property_operator)
@@ -23,17 +23,17 @@ Key Classes
23
23
  from __future__ import annotations
24
24
  from typing import Optional
25
25
 
26
- from .inversion import Inversion
26
+ from .inversion import LinearInversion
27
27
  from .gaussian_measure import GaussianMeasure
28
28
 
29
29
 
30
30
  from .forward_problem import LinearForwardProblem
31
- from .operators import LinearOperator
31
+ from .linear_operators import LinearOperator
32
32
  from .linear_solvers import LinearSolver, IterativeLinearSolver
33
33
  from .hilbert_space import HilbertSpace, Vector
34
34
 
35
35
 
36
- class LinearBayesianInversion(Inversion):
36
+ class LinearBayesianInversion(LinearInversion):
37
37
  """
38
38
  Solves a linear inverse problem using Bayesian methods.
39
39
 
pygeoinf/linear_forms.py CHANGED
@@ -1,33 +1,36 @@
1
1
  """
2
- Provides the `LinearForm` class to represent linear functionals.
2
+ Provides the `LinearForm` class for representing linear functionals.
3
3
 
4
4
  A linear form is a linear mapping from a vector in a Hilbert space to a
5
- scalar (a real number). This class provides a concrete representation for
6
- elements of the dual space of a `HilbertSpace`.
7
-
8
- A `LinearForm` can be thought of as a dual vector and is a fundamental component
9
- for defining inner products and adjoint operators within the library.
5
+ scalar. This class provides a concrete, component-based representation for
6
+ elements of the dual space of a `HilbertSpace`. It inherits from `NonLinearForm`,
7
+ specializing it for the linear case.
10
8
  """
11
9
 
12
10
  from __future__ import annotations
13
11
  from typing import Callable, Optional, Any, TYPE_CHECKING
14
12
 
13
+ from joblib import Parallel, delayed
14
+
15
15
  import numpy as np
16
16
 
17
+ from .nonlinear_forms import NonLinearForm
18
+
17
19
  # This block only runs for type checkers, not at runtime
18
20
  if TYPE_CHECKING:
19
- from .hilbert_space import HilbertSpace, EuclideanSpace
20
- from .operators import LinearOperator
21
+ from .hilbert_space import HilbertSpace, EuclideanSpace, Vector
22
+ from .linear_operators import LinearOperator
21
23
 
22
24
 
23
- class LinearForm:
25
+ class LinearForm(NonLinearForm):
24
26
  """
25
- Represents a linear form, a functional that maps vectors to scalars.
27
+ Represents a linear form as an efficient, component-based functional.
26
28
 
27
- A `LinearForm` is an element of a dual `HilbertSpace`. It is defined by its
28
- action on vectors from its `domain` space. Internally, this action is
29
- represented by a component vector, which when dotted with the component
30
- vector of a primal space element, produces the scalar result.
29
+ A `LinearForm` is an element of a dual `HilbertSpace` and is defined by its
30
+ action on vectors from its `domain`. Internally, this action is represented
31
+ by a component vector. This class provides optimized arithmetic operations
32
+ and correctly defines the gradient (a constant vector) and the Hessian
33
+ (the zero operator) for any linear functional.
31
34
  """
32
35
 
33
36
  def __init__(
@@ -35,29 +38,49 @@ class LinearForm:
35
38
  domain: HilbertSpace,
36
39
  /,
37
40
  *,
38
- mapping: Optional[Callable[[Any], float]] = None,
39
41
  components: Optional[np.ndarray] = None,
42
+ mapping: Optional[Callable[[Vector], float]] = None,
43
+ parallel: bool = False,
44
+ n_jobs: int = -1,
40
45
  ) -> None:
41
46
  """
42
- Initializes the LinearForm.
47
+ Initializes the LinearForm from a mapping or component vector.
48
+
49
+ A form must be defined by exactly one of two methods:
50
+ 1. **components**: The explicit component vector representing the form.
51
+ 2. **mapping**: A function `f(x)` that defines the form's action.
52
+ The components will be automatically computed from this mapping.
43
53
 
44
- A form can be defined either by its functional mapping or directly
45
- by its component vector. If a mapping is provided without components,
46
- the components will be computed by evaluating the mapping on the
47
- basis vectors of the domain.
48
54
 
49
55
  Args:
50
56
  domain: The Hilbert space on which the form is defined.
51
- mapping: A function `f(x)` defining the action of the form.
52
57
  components: The component representation of the form.
58
+ mapping: The functional mapping `f(x)`. Used if `components` is None.
59
+ parallel: Whether to use parallel computing components from the mapping.
60
+ n_jobs: The number of jobs to use for parallel computing.
61
+
62
+ Raises:
63
+ AssertionError: If neither or both `mapping` and `components`
64
+ are specified.
65
+
66
+ Notes:
67
+ Parallel options only relevant if the form is defined by a mapping.
68
+
69
+ If both `components` and `mapping` are specified, `components`
70
+ will take precedence.
53
71
  """
54
72
 
55
- self._domain: HilbertSpace = domain
73
+ super().__init__(
74
+ domain,
75
+ self._mapping_impl,
76
+ gradient=self._gradient_impl,
77
+ hessian=self._hessian_impl,
78
+ )
56
79
 
57
80
  if components is None:
58
81
  if mapping is None:
59
82
  raise AssertionError("Neither mapping nor components specified.")
60
- self._compute_components(mapping)
83
+ self._compute_components(mapping, parallel, n_jobs)
61
84
  else:
62
85
  self._components: np.ndarray = components
63
86
 
@@ -93,7 +116,7 @@ class LinearForm:
93
116
  is the scalar result of the form's action.
94
117
  """
95
118
  from .hilbert_space import EuclideanSpace
96
- from .operators import LinearOperator
119
+ from .linear_operators import LinearOperator
97
120
 
98
121
  return LinearOperator(
99
122
  self.domain,
@@ -108,10 +131,6 @@ class LinearForm:
108
131
  """
109
132
  return LinearForm(self.domain, components=self.components.copy())
110
133
 
111
- def __call__(self, x: Any) -> float:
112
- """Applies the linear form to a vector."""
113
- return np.dot(self._components, self.domain.to_components(x))
114
-
115
134
  def __neg__(self) -> LinearForm:
116
135
  """Returns the additive inverse of the form."""
117
136
  return LinearForm(self.domain, components=-self._components)
@@ -128,13 +147,47 @@ class LinearForm:
128
147
  """Returns the division of the form by a scalar."""
129
148
  return self * (1.0 / a)
130
149
 
131
- def __add__(self, other: LinearForm) -> LinearForm:
132
- """Returns the sum of this form and another."""
133
- return LinearForm(self.domain, components=self.components + other.components)
150
+ def __add__(self, other: NonLinearForm | LinearForm) -> NonLinearForm | LinearForm:
151
+ """
152
+ Returns the sum of this form and another.
153
+
154
+ If `other` is also a `LinearForm`, this performs an optimized,
155
+ component-wise addition. Otherwise, it delegates to the general
156
+ implementation in the `NonLinearForm` base class.
157
+
158
+ Args:
159
+ other: The form to add to this one.
160
+
161
+ Returns:
162
+ A `LinearForm` if adding two `LinearForm`s, otherwise a `NonLinearForm`.
163
+ """
164
+ if isinstance(other, LinearForm):
165
+ return LinearForm(
166
+ self.domain, components=self.components + other.components
167
+ )
168
+ else:
169
+ return super().__add__(other)
170
+
171
+ def __sub__(self, other: NonLinearForm | LinearForm) -> NonLinearForm | LinearForm:
172
+ """
173
+ Returns the difference of this form and another.
174
+
175
+ If `other` is also a `LinearForm`, this performs an optimized,
176
+ component-wise subtraction. Otherwise, it delegates to the general
177
+ implementation in the `NonLinearForm` base class.
178
+
179
+ Args:
180
+ other: The form to subtract from this one.
134
181
 
135
- def __sub__(self, other: LinearForm) -> LinearForm:
136
- """Returns the difference between this form and another."""
137
- return LinearForm(self.domain, components=self.components - other.components)
182
+ Returns:
183
+ A `LinearForm` if subtracting two `LinearForm`s, otherwise a `NonLinearForm`.
184
+ """
185
+ if isinstance(other, LinearForm):
186
+ return LinearForm(
187
+ self.domain, components=self.components - other.components
188
+ )
189
+ else:
190
+ return super().__sub__(other)
138
191
 
139
192
  def __imul__(self, a: float) -> "LinearForm":
140
193
  """
@@ -156,14 +209,55 @@ class LinearForm:
156
209
  """Returns the string representation of the form's components."""
157
210
  return self.components.__str__()
158
211
 
159
- def _compute_components(self, mapping: Callable[[Any], float]):
212
+ def _compute_components(
213
+ self,
214
+ mapping: Callable[[Any], float],
215
+ parallel: bool,
216
+ n_jobs: Optional[int],
217
+ ):
218
+ """Computes the component vector of the form, with an optional parallel backend."""
219
+ if not parallel:
220
+ self._components = np.zeros(self.domain.dim)
221
+ cx = np.zeros(self.domain.dim)
222
+ for i in range(self.domain.dim):
223
+ cx[i] = 1.0
224
+ x = self.domain.from_components(cx)
225
+ self._components[i] = mapping(x)
226
+ cx[i] = 0.0
227
+ else:
228
+
229
+ def compute_one_component(i: int) -> float:
230
+ """
231
+ Computes a single component for a given basis vector index.
232
+ This function is sent to each parallel worker.
233
+ """
234
+
235
+ # cx = np.zeros(self.domain.dim)
236
+ # cx[i] = 1.0
237
+ # x = self.domain.from_components(cx)
238
+ x = self.domain.basis_vector(i)
239
+ return mapping(x)
240
+
241
+ # Run the helper function in parallel for each dimension
242
+ results = Parallel(n_jobs=n_jobs)(
243
+ delayed(compute_one_component)(i) for i in range(self.domain.dim)
244
+ )
245
+ self._components = np.array(results)
246
+
247
+ def _mapping_impl(self, x: Vector) -> float:
248
+ """
249
+ Maps a vector to its scalar value.
250
+ """
251
+ return np.dot(self.components, self.domain.to_components(x))
252
+
253
+ def _gradient_impl(self, _: Vector) -> Vector:
254
+ """
255
+ Computes the gradient of the form at a point.
256
+ """
257
+ return self.domain.from_dual(self)
258
+
259
+ def _hessian_impl(self, _: Vector) -> LinearOperator:
160
260
  """
161
- Computes the component vector of the form.
261
+ Computes the Hessian of the form at a point.
162
262
  """
163
- self._components = np.zeros(self.domain.dim)
164
- cx = np.zeros(self.domain.dim)
165
- for i in range(self.domain.dim):
166
- cx[i] = 1
167
- x = self.domain.from_components(cx)
168
- self._components[i] = mapping(x)
169
- cx[i] = 0
263
+ return self.domain.zero_operator()