pygeoinf 1.2.3__py3-none-any.whl → 1.2.5__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 +6 -0
- pygeoinf/backus_gilbert.py +5 -0
- pygeoinf/checks/hilbert_space.py +183 -0
- pygeoinf/checks/linear_operators.py +124 -0
- pygeoinf/checks/nonlinear_operators.py +154 -0
- pygeoinf/forward_problem.py +22 -13
- pygeoinf/hilbert_space.py +3 -1
- pygeoinf/inversion.py +8 -0
- pygeoinf/linear_operators.py +3 -1
- pygeoinf/linear_optimisation.py +48 -9
- pygeoinf/linear_solvers.py +1 -1
- pygeoinf/nonlinear_operators.py +4 -1
- pygeoinf/nonlinear_optimisation.py +8 -1
- pygeoinf/symmetric_space/sphere.py +31 -1
- {pygeoinf-1.2.3.dist-info → pygeoinf-1.2.5.dist-info}/METADATA +3 -3
- pygeoinf-1.2.5.dist-info/RECORD +28 -0
- pygeoinf/symmetric_space/__pycache__/__init__.cpython-311.pyc +0 -0
- pygeoinf/symmetric_space/__pycache__/circle.cpython-311.pyc +0 -0
- pygeoinf/symmetric_space/__pycache__/sphere.cpython-311.pyc +0 -0
- pygeoinf/symmetric_space/__pycache__/symmetric_space.cpython-311.pyc +0 -0
- pygeoinf-1.2.3.dist-info/RECORD +0 -29
- {pygeoinf-1.2.3.dist-info → pygeoinf-1.2.5.dist-info}/LICENSE +0 -0
- {pygeoinf-1.2.3.dist-info → pygeoinf-1.2.5.dist-info}/WHEEL +0 -0
pygeoinf/__init__.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified imports for the package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
from .random_matrix import (
|
|
2
6
|
fixed_rank_random_range,
|
|
3
7
|
variable_rank_random_range,
|
|
@@ -71,6 +75,8 @@ from .linear_optimisation import (
|
|
|
71
75
|
|
|
72
76
|
from .linear_bayesian import LinearBayesianInversion, LinearBayesianInference
|
|
73
77
|
|
|
78
|
+
from .backus_gilbert import HyperEllipsoid
|
|
79
|
+
|
|
74
80
|
from .nonlinear_optimisation import (
|
|
75
81
|
ScipyUnconstrainedOptimiser,
|
|
76
82
|
)
|
pygeoinf/backus_gilbert.py
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
Module for Backus-Gilbert like methods for solving inference problems. To be done...
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
from .hilbert_space import HilbertSpace, Vector
|
|
6
8
|
from .linear_operators import LinearOperator
|
|
7
9
|
from .nonlinear_forms import NonLinearForm
|
|
8
10
|
|
|
11
|
+
from .forward_problem import LinearForwardProblem
|
|
12
|
+
from .inversion import LinearInference
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
class HyperEllipsoid:
|
|
11
16
|
"""
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HilbertSpaceAxiomChecks:
|
|
5
|
+
"""
|
|
6
|
+
A mixin class providing a self-checking mechanism for Hilbert space axioms.
|
|
7
|
+
|
|
8
|
+
When inherited by a HilbertSpace subclass, it provides the `.check()` method
|
|
9
|
+
to run a suite of randomized tests, ensuring the implementation is valid.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def _check_vector_space_axioms(self, x, y, a):
|
|
13
|
+
"""Checks axioms related to vector addition and scalar multiplication."""
|
|
14
|
+
# (x + y) - y == x
|
|
15
|
+
sum_vec = self.add(x, y)
|
|
16
|
+
res_vec = self.subtract(sum_vec, y)
|
|
17
|
+
if not np.allclose(self.to_components(x), self.to_components(res_vec)):
|
|
18
|
+
raise AssertionError("Axiom failed: (x + y) - y != x")
|
|
19
|
+
|
|
20
|
+
# a*(x+y) == a*x + a*y
|
|
21
|
+
lhs = self.multiply(a, self.add(x, y))
|
|
22
|
+
rhs = self.add(self.multiply(a, x), self.multiply(a, y))
|
|
23
|
+
if not np.allclose(self.to_components(lhs), self.to_components(rhs)):
|
|
24
|
+
raise AssertionError("Axiom failed: a*(x+y) != a*x + a*y")
|
|
25
|
+
|
|
26
|
+
# x + 0 = x
|
|
27
|
+
zero_vec = self.zero
|
|
28
|
+
res_vec = self.add(x, zero_vec)
|
|
29
|
+
if not np.allclose(self.to_components(x), self.to_components(res_vec)):
|
|
30
|
+
raise AssertionError("Axiom failed: x + 0 != x")
|
|
31
|
+
|
|
32
|
+
def _check_inner_product_axioms(self, x, y, z, a, b):
|
|
33
|
+
"""Checks axioms related to the inner product and norm."""
|
|
34
|
+
# Linearity: <ax+by, z> = a<x,z> + b<y,z>
|
|
35
|
+
lhs = self.inner_product(self.add(self.multiply(a, x), self.multiply(b, y)), z)
|
|
36
|
+
rhs = a * self.inner_product(x, z) + b * self.inner_product(y, z)
|
|
37
|
+
if not np.isclose(lhs, rhs):
|
|
38
|
+
raise AssertionError("Axiom failed: Inner product linearity")
|
|
39
|
+
|
|
40
|
+
# Symmetry: <x, y> == <y, x>
|
|
41
|
+
if not np.isclose(self.inner_product(x, y), self.inner_product(y, x)):
|
|
42
|
+
raise AssertionError("Axiom failed: Inner product symmetry")
|
|
43
|
+
|
|
44
|
+
# Triangle Inequality: ||x + y|| <= ||x|| + ||y||
|
|
45
|
+
norm_sum = self.norm(self.add(x, y))
|
|
46
|
+
if not norm_sum <= self.norm(x) + self.norm(y):
|
|
47
|
+
raise AssertionError("Axiom failed: Triangle inequality")
|
|
48
|
+
|
|
49
|
+
def _check_mapping_identities(self, x):
|
|
50
|
+
"""Checks that component and dual mappings are self-consistent."""
|
|
51
|
+
# from_components(to_components(x)) == x
|
|
52
|
+
components = self.to_components(x)
|
|
53
|
+
reconstructed_x = self.from_components(components)
|
|
54
|
+
if not np.allclose(components, self.to_components(reconstructed_x)):
|
|
55
|
+
raise AssertionError("Axiom failed: Component mapping round-trip")
|
|
56
|
+
|
|
57
|
+
# from_dual(to_dual(x)) == x
|
|
58
|
+
x_dual = self.to_dual(x)
|
|
59
|
+
reconstructed_x = self.from_dual(x_dual)
|
|
60
|
+
if not np.allclose(self.to_components(x), self.to_components(reconstructed_x)):
|
|
61
|
+
raise AssertionError("Axiom failed: Dual mapping round-trip")
|
|
62
|
+
|
|
63
|
+
def _check_inplace_operations(self, x, y, a):
|
|
64
|
+
"""Checks the in-place operations `ax` and `axpy`."""
|
|
65
|
+
# Test ax: y := a*x
|
|
66
|
+
x_copy = self.copy(x)
|
|
67
|
+
expected_ax = self.multiply(a, x)
|
|
68
|
+
self.ax(a, x_copy)
|
|
69
|
+
if not np.allclose(self.to_components(expected_ax), self.to_components(x_copy)):
|
|
70
|
+
raise AssertionError("Axiom failed: In-place operation ax")
|
|
71
|
+
|
|
72
|
+
# Test axpy: y := a*x + y
|
|
73
|
+
y_copy = self.copy(y)
|
|
74
|
+
expected_axpy = self.add(self.multiply(a, x), y)
|
|
75
|
+
self.axpy(a, x, y_copy)
|
|
76
|
+
if not np.allclose(
|
|
77
|
+
self.to_components(expected_axpy), self.to_components(y_copy)
|
|
78
|
+
):
|
|
79
|
+
raise AssertionError("Axiom failed: In-place operation axpy")
|
|
80
|
+
|
|
81
|
+
def _check_copy(self, x):
|
|
82
|
+
"""Checks that the copy method creates a deep, independent copy."""
|
|
83
|
+
x_copy = self.copy(x)
|
|
84
|
+
|
|
85
|
+
# The copy should have the same value but be a different object
|
|
86
|
+
if x is x_copy:
|
|
87
|
+
raise AssertionError("Axiom failed: copy() returned the same object.")
|
|
88
|
+
if not np.allclose(self.to_components(x), self.to_components(x_copy)):
|
|
89
|
+
raise AssertionError("Axiom failed: copy() did not preserve values.")
|
|
90
|
+
|
|
91
|
+
# Modify the copy and ensure the original is unchanged
|
|
92
|
+
self.ax(2.0, x_copy)
|
|
93
|
+
if np.allclose(self.to_components(x), self.to_components(x_copy)):
|
|
94
|
+
raise AssertionError("Axiom failed: copy() is not a deep copy.")
|
|
95
|
+
|
|
96
|
+
def _check_gram_schmidt(self):
|
|
97
|
+
"""Checks the Gram-Schmidt orthonormalization process."""
|
|
98
|
+
# Create a list of linearly independent vectors
|
|
99
|
+
vectors = [self.random() for _ in range(min(self.dim, 5))]
|
|
100
|
+
if not vectors:
|
|
101
|
+
return # Skip if dimension is 0
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
orthonormal_vectors = self.gram_schmidt(vectors)
|
|
105
|
+
except ValueError as e:
|
|
106
|
+
# This can happen if the random vectors are not linearly independent
|
|
107
|
+
print(f"Skipping Gram-Schmidt check due to non-independent vectors: {e}")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Check for orthonormality
|
|
111
|
+
for i, v1 in enumerate(orthonormal_vectors):
|
|
112
|
+
for j, v2 in enumerate(orthonormal_vectors):
|
|
113
|
+
inner_product = self.inner_product(v1, v2)
|
|
114
|
+
if i == j:
|
|
115
|
+
if not np.isclose(inner_product, 1.0):
|
|
116
|
+
raise AssertionError(
|
|
117
|
+
"Axiom failed: Gram-Schmidt vector norm is not 1."
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
if not np.isclose(inner_product, 0.0):
|
|
121
|
+
raise AssertionError(
|
|
122
|
+
"Axiom failed: Gram-Schmidt vectors are not orthogonal."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _check_basis_and_expectation(self):
|
|
126
|
+
"""Checks the basis_vector and sample_expectation methods."""
|
|
127
|
+
if self.dim == 0:
|
|
128
|
+
return # Skip for zero-dimensional spaces
|
|
129
|
+
|
|
130
|
+
# Check basis vectors
|
|
131
|
+
for i in range(self.dim):
|
|
132
|
+
basis_vector = self.basis_vector(i)
|
|
133
|
+
components = self.to_components(basis_vector)
|
|
134
|
+
expected_components = np.zeros(self.dim)
|
|
135
|
+
expected_components[i] = 1.0
|
|
136
|
+
if not np.allclose(components, expected_components):
|
|
137
|
+
raise AssertionError(
|
|
138
|
+
"Axiom failed: basis_vector has incorrect components."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Check sample expectation
|
|
142
|
+
vectors = [self.random() for _ in range(5)]
|
|
143
|
+
mean_vec = self.sample_expectation(vectors)
|
|
144
|
+
|
|
145
|
+
mean_comps = np.mean([self.to_components(v) for v in vectors], axis=0)
|
|
146
|
+
if not np.allclose(self.to_components(mean_vec), mean_comps):
|
|
147
|
+
raise AssertionError("Axiom failed: sample_expectation is incorrect.")
|
|
148
|
+
|
|
149
|
+
def check(self, n_checks: int = 10) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Runs a suite of randomized checks to verify the Hilbert space axioms.
|
|
152
|
+
|
|
153
|
+
This method performs `n_checks` iterations, generating new random
|
|
154
|
+
vectors and scalars for each one. It provides an "interactive" way
|
|
155
|
+
to validate any concrete HilbertSpace implementation.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
n_checks: The number of randomized trials to run.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
AssertionError: If any of the underlying axiom checks fail.
|
|
162
|
+
"""
|
|
163
|
+
print(
|
|
164
|
+
f"\nRunning {n_checks} randomized axiom checks for {self.__class__.__name__}... (and some one-off checks)"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# These checks only need to be run once
|
|
168
|
+
self._check_gram_schmidt()
|
|
169
|
+
self._check_basis_and_expectation()
|
|
170
|
+
|
|
171
|
+
for _ in range(n_checks):
|
|
172
|
+
# Generate fresh random data for each trial
|
|
173
|
+
x, y, z = self.random(), self.random(), self.random()
|
|
174
|
+
a, b = np.random.randn(), np.random.randn()
|
|
175
|
+
|
|
176
|
+
# Run all checks
|
|
177
|
+
self._check_vector_space_axioms(x, y, a)
|
|
178
|
+
self._check_inner_product_axioms(x, y, z, a, b)
|
|
179
|
+
self._check_mapping_identities(x)
|
|
180
|
+
self._check_inplace_operations(x, y, a)
|
|
181
|
+
self._check_copy(x)
|
|
182
|
+
|
|
183
|
+
print(f"✅ All {n_checks} Hilbert space axiom checks passed successfully.")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides a self-checking mechanism for LinearOperator implementations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
# Import the base checks from the sibling module
|
|
10
|
+
from .nonlinear_operators import NonLinearOperatorAxiomChecks
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..hilbert_space import Vector
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LinearOperatorAxiomChecks(NonLinearOperatorAxiomChecks):
|
|
18
|
+
"""
|
|
19
|
+
A mixin for checking the properties of a LinearOperator.
|
|
20
|
+
|
|
21
|
+
Inherits the derivative check from NonLinearOperatorAxiomChecks and adds
|
|
22
|
+
checks for linearity and the adjoint identity.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def _check_linearity(self, x: Vector, y: Vector, a: float, b: float):
|
|
26
|
+
"""Verifies the linearity property: L(ax + by) = a*L(x) + b*L(y)"""
|
|
27
|
+
ax_plus_by = self.domain.add(
|
|
28
|
+
self.domain.multiply(a, x), self.domain.multiply(b, y)
|
|
29
|
+
)
|
|
30
|
+
lhs = self(ax_plus_by)
|
|
31
|
+
|
|
32
|
+
aLx = self.codomain.multiply(a, self(x))
|
|
33
|
+
bLy = self.codomain.multiply(b, self(y))
|
|
34
|
+
rhs = self.codomain.add(aLx, bLy)
|
|
35
|
+
|
|
36
|
+
# Compare the results in the codomain
|
|
37
|
+
diff_norm = self.codomain.norm(self.codomain.subtract(lhs, rhs))
|
|
38
|
+
rhs_norm = self.codomain.norm(rhs)
|
|
39
|
+
relative_error = diff_norm / (rhs_norm + 1e-12)
|
|
40
|
+
|
|
41
|
+
if relative_error > 1e-9:
|
|
42
|
+
raise AssertionError(
|
|
43
|
+
f"Linearity check failed: L(ax+by) != aL(x)+bL(y). Relative error: {relative_error:.2e}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _check_adjoint_definition(self, x: Vector, y: Vector):
|
|
47
|
+
"""Verifies the adjoint identity: <L(x), y> = <x, L*(y)>"""
|
|
48
|
+
lhs = self.codomain.inner_product(self(x), y)
|
|
49
|
+
rhs = self.domain.inner_product(x, self.adjoint(y))
|
|
50
|
+
|
|
51
|
+
if not np.isclose(lhs, rhs):
|
|
52
|
+
raise AssertionError(
|
|
53
|
+
f"Adjoint definition failed: <L(x),y> = {lhs:.4e}, but <x,L*(y)> = {rhs:.4e}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _check_algebraic_identities(self, op1, op2, x, y, a):
|
|
57
|
+
"""
|
|
58
|
+
Verifies the algebraic properties of the adjoint and dual operators.
|
|
59
|
+
Requires a second compatible operator (op2).
|
|
60
|
+
"""
|
|
61
|
+
# --- Adjoint Identities ---
|
|
62
|
+
# (A+B)* = A* + B*
|
|
63
|
+
op_sum_adj = (op1 + op2).adjoint
|
|
64
|
+
adj_sum = op1.adjoint + op2.adjoint
|
|
65
|
+
diff = op1.domain.subtract(op_sum_adj(y), adj_sum(y))
|
|
66
|
+
if op1.domain.norm(diff) > 1e-9:
|
|
67
|
+
raise AssertionError("Axiom failed: (A+B)* != A* + B*")
|
|
68
|
+
|
|
69
|
+
# (a*A)* = a*A*
|
|
70
|
+
op_scaled_adj = (a * op1).adjoint
|
|
71
|
+
adj_scaled = a * op1.adjoint
|
|
72
|
+
diff = op1.domain.subtract(op_scaled_adj(y), adj_scaled(y))
|
|
73
|
+
if op1.domain.norm(diff) > 1e-9:
|
|
74
|
+
raise AssertionError("Axiom failed: (a*A)* != a*A*")
|
|
75
|
+
|
|
76
|
+
# (A*)* = A
|
|
77
|
+
op_adj_adj = op1.adjoint.adjoint
|
|
78
|
+
diff = op1.codomain.subtract(op_adj_adj(x), op1(x))
|
|
79
|
+
if op1.codomain.norm(diff) > 1e-9:
|
|
80
|
+
raise AssertionError("Axiom failed: (A*)* != A")
|
|
81
|
+
|
|
82
|
+
# (A@B)* = B*@A*
|
|
83
|
+
if op1.domain == op2.codomain:
|
|
84
|
+
op_comp_adj = (op1 @ op2).adjoint
|
|
85
|
+
adj_comp = op2.adjoint @ op1.adjoint
|
|
86
|
+
diff = op2.domain.subtract(op_comp_adj(y), adj_comp(y))
|
|
87
|
+
if op2.domain.norm(diff) > 1e-9:
|
|
88
|
+
raise AssertionError("Axiom failed: (A@B)* != B*@A*")
|
|
89
|
+
|
|
90
|
+
# --- Dual Identities ---
|
|
91
|
+
# (A+B)' = A' + B'
|
|
92
|
+
op_sum_dual = (op1 + op2).dual
|
|
93
|
+
dual_sum = op1.dual + op2.dual
|
|
94
|
+
y_dual = op1.codomain.to_dual(y)
|
|
95
|
+
# The result of applying a dual operator is a LinearForm, which supports subtraction
|
|
96
|
+
diff_dual = op_sum_dual(y_dual) - dual_sum(y_dual)
|
|
97
|
+
if op1.domain.dual.norm(diff_dual) > 1e-9:
|
|
98
|
+
raise AssertionError("Axiom failed: (A+B)' != A' + B'")
|
|
99
|
+
|
|
100
|
+
def check(self, n_checks: int = 5, op2=None) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Runs all checks for the LinearOperator, including non-linear checks
|
|
103
|
+
and algebraic identities.
|
|
104
|
+
"""
|
|
105
|
+
# First, run the parent (non-linear) checks from the base class
|
|
106
|
+
super().check(n_checks, op2=op2)
|
|
107
|
+
|
|
108
|
+
# Now, run the linear-specific checks
|
|
109
|
+
print(
|
|
110
|
+
f"Running {n_checks} additional randomized checks for linearity and adjoints..."
|
|
111
|
+
)
|
|
112
|
+
for _ in range(n_checks):
|
|
113
|
+
x1 = self.domain.random()
|
|
114
|
+
x2 = self.domain.random()
|
|
115
|
+
y = self.codomain.random()
|
|
116
|
+
a, b = np.random.randn(), np.random.randn()
|
|
117
|
+
|
|
118
|
+
self._check_linearity(x1, x2, a, b)
|
|
119
|
+
self._check_adjoint_definition(x1, y)
|
|
120
|
+
|
|
121
|
+
if op2:
|
|
122
|
+
self._check_algebraic_identities(self, op2, x1, y, a)
|
|
123
|
+
|
|
124
|
+
print(f"✅ All {n_checks} linear operator checks passed successfully.")
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides a self-checking mechanism for NonLinearOperator implementations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..linear_operators import LinearOperator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NonLinearOperatorAxiomChecks:
|
|
14
|
+
"""A mixin for checking the properties of a NonLinearOperator."""
|
|
15
|
+
|
|
16
|
+
def _check_derivative_finite_difference(self, x, v, h=1e-7):
|
|
17
|
+
"""
|
|
18
|
+
Verifies the derivative using the finite difference formula:
|
|
19
|
+
D[F](x) @ v ≈ (F(x + h*v) - F(x)) / h
|
|
20
|
+
"""
|
|
21
|
+
from ..linear_operators import LinearOperator
|
|
22
|
+
|
|
23
|
+
derivative_op = self.derivative(x)
|
|
24
|
+
|
|
25
|
+
# 1. Check that the derivative is a valid LinearOperator
|
|
26
|
+
if not isinstance(derivative_op, LinearOperator):
|
|
27
|
+
raise AssertionError("The derivative must be a valid LinearOperator.")
|
|
28
|
+
if not (
|
|
29
|
+
derivative_op.domain == self.domain
|
|
30
|
+
and derivative_op.codomain == self.codomain
|
|
31
|
+
):
|
|
32
|
+
raise AssertionError("The derivative has a mismatched domain or codomain.")
|
|
33
|
+
|
|
34
|
+
# 2. Calculate the analytical derivative's action on a random vector v
|
|
35
|
+
analytic_result = derivative_op(v)
|
|
36
|
+
|
|
37
|
+
# 3. Calculate the numerical approximation using the finite difference formula
|
|
38
|
+
x_plus_hv = self.domain.add(x, self.domain.multiply(h, v))
|
|
39
|
+
fx_plus_hv = self(x_plus_hv)
|
|
40
|
+
fx = self(x)
|
|
41
|
+
finite_diff_result = self.codomain.multiply(
|
|
42
|
+
1 / h, self.codomain.subtract(fx_plus_hv, fx)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# 4. Compare the analytical and numerical results
|
|
46
|
+
diff_norm = self.codomain.norm(
|
|
47
|
+
self.codomain.subtract(analytic_result, finite_diff_result)
|
|
48
|
+
)
|
|
49
|
+
analytic_norm = self.codomain.norm(analytic_result)
|
|
50
|
+
relative_error = diff_norm / (analytic_norm + 1e-12)
|
|
51
|
+
|
|
52
|
+
if relative_error > 1e-4:
|
|
53
|
+
raise AssertionError(
|
|
54
|
+
f"Finite difference check failed. Relative error: {relative_error:.2e}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _check_add_derivative(self, op1, op2, x, v):
|
|
58
|
+
"""Verifies the sum rule for derivatives: (F+G)' = F' + G'"""
|
|
59
|
+
if not (op1.has_derivative and op2.has_derivative):
|
|
60
|
+
return # Skip if derivatives aren't defined
|
|
61
|
+
|
|
62
|
+
# Derivative of the sum of operators
|
|
63
|
+
sum_op = op1 + op2
|
|
64
|
+
derivative_of_sum = sum_op.derivative(x)
|
|
65
|
+
|
|
66
|
+
# Sum of the individual derivatives
|
|
67
|
+
sum_of_derivatives = op1.derivative(x) + op2.derivative(x)
|
|
68
|
+
|
|
69
|
+
# Compare their action on a random vector
|
|
70
|
+
res1 = derivative_of_sum(v)
|
|
71
|
+
res2 = sum_of_derivatives(v)
|
|
72
|
+
|
|
73
|
+
diff_norm = self.codomain.norm(self.codomain.subtract(res1, res2))
|
|
74
|
+
if diff_norm > 1e-9:
|
|
75
|
+
raise AssertionError("Axiom failed: Derivative of sum is incorrect.")
|
|
76
|
+
|
|
77
|
+
def _check_scalar_mul_derivative(self, op, x, v, a):
|
|
78
|
+
"""Verifies the scalar multiple rule: (a*F)' = a*F'"""
|
|
79
|
+
if not op.has_derivative:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Derivative of the scaled operator
|
|
83
|
+
scaled_op = a * op
|
|
84
|
+
derivative_of_scaled = scaled_op.derivative(x)
|
|
85
|
+
|
|
86
|
+
# Scaled original derivative
|
|
87
|
+
scaled_derivative = a * op.derivative(x)
|
|
88
|
+
|
|
89
|
+
# Compare their action
|
|
90
|
+
res1 = derivative_of_scaled(v)
|
|
91
|
+
res2 = scaled_derivative(v)
|
|
92
|
+
|
|
93
|
+
diff_norm = self.codomain.norm(self.codomain.subtract(res1, res2))
|
|
94
|
+
if diff_norm > 1e-9:
|
|
95
|
+
raise AssertionError(
|
|
96
|
+
"Axiom failed: Derivative of scalar multiple is incorrect."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _check_matmul_derivative(self, op1, op2, x, v):
|
|
100
|
+
"""Verifies the chain rule for derivatives: (F o G)'(x) = F'(G(x)) @ G'(x)"""
|
|
101
|
+
if not (op1.has_derivative and op2.has_derivative):
|
|
102
|
+
return
|
|
103
|
+
if op1.domain != op2.codomain:
|
|
104
|
+
return # Skip if not composable
|
|
105
|
+
|
|
106
|
+
# Derivative of the composed operator
|
|
107
|
+
composed_op = op1 @ op2
|
|
108
|
+
derivative_of_composed = composed_op.derivative(x)
|
|
109
|
+
|
|
110
|
+
# Apply the chain rule manually
|
|
111
|
+
gx = op2(x)
|
|
112
|
+
chain_rule_derivative = op1.derivative(gx) @ op2.derivative(x)
|
|
113
|
+
|
|
114
|
+
# Compare their action
|
|
115
|
+
res1 = derivative_of_composed(v)
|
|
116
|
+
res2 = chain_rule_derivative(v)
|
|
117
|
+
|
|
118
|
+
diff_norm = op1.codomain.norm(op1.codomain.subtract(res1, res2))
|
|
119
|
+
if diff_norm > 1e-9:
|
|
120
|
+
raise AssertionError(
|
|
121
|
+
"Axiom failed: Chain rule for derivatives is incorrect."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def check(self, n_checks: int = 5, op2=None) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Runs randomized checks to validate the operator's derivative and
|
|
127
|
+
its algebraic properties.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
n_checks: The number of randomized trials to perform.
|
|
131
|
+
op2: An optional second operator for testing algebraic rules.
|
|
132
|
+
"""
|
|
133
|
+
print(
|
|
134
|
+
f"\nRunning {n_checks} randomized checks for {self.__class__.__name__}..."
|
|
135
|
+
)
|
|
136
|
+
for _ in range(n_checks):
|
|
137
|
+
x = self.domain.random()
|
|
138
|
+
v = self.domain.random()
|
|
139
|
+
a = np.random.randn()
|
|
140
|
+
|
|
141
|
+
# Ensure the direction vector 'v' is not a zero vector
|
|
142
|
+
if self.domain.norm(v) < 1e-12:
|
|
143
|
+
v = self.domain.random()
|
|
144
|
+
|
|
145
|
+
# Original check
|
|
146
|
+
self._check_derivative_finite_difference(x, v)
|
|
147
|
+
|
|
148
|
+
# New algebraic checks
|
|
149
|
+
self._check_scalar_mul_derivative(self, x, v, a)
|
|
150
|
+
if op2:
|
|
151
|
+
self._check_add_derivative(self, op2, x, v)
|
|
152
|
+
self._check_matmul_derivative(self, op2, x, v)
|
|
153
|
+
|
|
154
|
+
print(f"✅ All {n_checks} non-linear operator checks passed successfully.")
|
pygeoinf/forward_problem.py
CHANGED
|
@@ -237,6 +237,27 @@ class LinearForwardProblem(ForwardProblem):
|
|
|
237
237
|
"""
|
|
238
238
|
return chi2.ppf(significance_level, self.data_space.dim)
|
|
239
239
|
|
|
240
|
+
def chi_squared_from_residual(self, residual: Vector) -> float:
|
|
241
|
+
"""
|
|
242
|
+
Calculates the chi-squared statistic from a residual vector.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
residual: The residual vector.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The chi-squared statistic.
|
|
249
|
+
"""
|
|
250
|
+
if self.data_error_measure_set:
|
|
251
|
+
residual = self.data_space.subtract(
|
|
252
|
+
residual, self.data_error_measure.expectation
|
|
253
|
+
)
|
|
254
|
+
inverse_data_covariance = self.data_error_measure.inverse_covariance
|
|
255
|
+
return self.data_space.inner_product(
|
|
256
|
+
inverse_data_covariance(residual), residual
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
return self.data_space.squared_norm(residual)
|
|
260
|
+
|
|
240
261
|
def chi_squared(self, model: Vector, data: Vector) -> float:
|
|
241
262
|
"""
|
|
242
263
|
Calculates the chi-squared statistic for a given model and data.
|
|
@@ -256,19 +277,7 @@ class LinearForwardProblem(ForwardProblem):
|
|
|
256
277
|
"""
|
|
257
278
|
|
|
258
279
|
residual = self.data_space.subtract(data, self.forward_operator(model))
|
|
259
|
-
|
|
260
|
-
if self.data_error_measure_set:
|
|
261
|
-
# Center the residual with respect to the error measure's mean
|
|
262
|
-
residual = self.data_space.subtract(
|
|
263
|
-
residual, self.data_error_measure.expectation
|
|
264
|
-
)
|
|
265
|
-
inverse_data_covariance = self.data_error_measure.inverse_covariance
|
|
266
|
-
return self.data_space.inner_product(
|
|
267
|
-
inverse_data_covariance(residual), residual
|
|
268
|
-
)
|
|
269
|
-
else:
|
|
270
|
-
# Fallback to the squared L2 norm of the residual
|
|
271
|
-
return self.data_space.squared_norm(residual)
|
|
280
|
+
return self.chi_squared_from_residual(residual)
|
|
272
281
|
|
|
273
282
|
def chi_squared_test(
|
|
274
283
|
self, significance_level: float, model: Vector, data: Vector
|
pygeoinf/hilbert_space.py
CHANGED
|
@@ -36,6 +36,8 @@ from typing import (
|
|
|
36
36
|
|
|
37
37
|
import numpy as np
|
|
38
38
|
|
|
39
|
+
from .checks.hilbert_space import HilbertSpaceAxiomChecks
|
|
40
|
+
|
|
39
41
|
# This block only runs for type checkers, not at runtime
|
|
40
42
|
if TYPE_CHECKING:
|
|
41
43
|
from .linear_operators import LinearOperator
|
|
@@ -45,7 +47,7 @@ if TYPE_CHECKING:
|
|
|
45
47
|
Vector = TypeVar("Vector")
|
|
46
48
|
|
|
47
49
|
|
|
48
|
-
class HilbertSpace(ABC):
|
|
50
|
+
class HilbertSpace(ABC, HilbertSpaceAxiomChecks):
|
|
49
51
|
"""
|
|
50
52
|
An abstract base class for real Hilbert spaces.
|
|
51
53
|
|
pygeoinf/inversion.py
CHANGED
|
@@ -126,6 +126,10 @@ class Inference(Inversion):
|
|
|
126
126
|
relationship between model parameters and data.
|
|
127
127
|
property_operator: A mapping takes elements of the model space to
|
|
128
128
|
property vector of interest.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValueError: If the domain of the property operator is
|
|
132
|
+
not equal to the model space.
|
|
129
133
|
"""
|
|
130
134
|
|
|
131
135
|
super().__init__(forward_problem)
|
|
@@ -166,6 +170,10 @@ class LinearInference(Inference):
|
|
|
166
170
|
relationship between model parameters and data.
|
|
167
171
|
property_operator: A linear mapping takes elements of the model space to
|
|
168
172
|
property vector of interest.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: If the domain of the property operator is
|
|
176
|
+
not equal to the model space.
|
|
169
177
|
"""
|
|
170
178
|
|
|
171
179
|
if not isinstance(forward_problem, LinearForwardProblem):
|
pygeoinf/linear_operators.py
CHANGED
|
@@ -36,13 +36,15 @@ from .random_matrix import (
|
|
|
36
36
|
|
|
37
37
|
from .parallel import parallel_compute_dense_matrix_from_scipy_op
|
|
38
38
|
|
|
39
|
+
from .checks.linear_operators import LinearOperatorAxiomChecks
|
|
40
|
+
|
|
39
41
|
# This block only runs for type checkers, not at runtime
|
|
40
42
|
if TYPE_CHECKING:
|
|
41
43
|
from .hilbert_space import HilbertSpace, EuclideanSpace
|
|
42
44
|
from .linear_forms import LinearForm
|
|
43
45
|
|
|
44
46
|
|
|
45
|
-
class LinearOperator(NonLinearOperator):
|
|
47
|
+
class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
|
|
46
48
|
"""A linear operator between two Hilbert spaces.
|
|
47
49
|
|
|
48
50
|
This class represents a linear map `L(x) = Ax` and provides rich
|
pygeoinf/linear_optimisation.py
CHANGED
|
@@ -21,6 +21,7 @@ Key Classes
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
from typing import Optional, Union
|
|
23
23
|
|
|
24
|
+
|
|
24
25
|
from .nonlinear_operators import NonLinearOperator
|
|
25
26
|
from .inversion import LinearInversion
|
|
26
27
|
|
|
@@ -82,6 +83,27 @@ class LinearLeastSquaresInversion(LinearInversion):
|
|
|
82
83
|
else:
|
|
83
84
|
return forward_operator.adjoint @ forward_operator + damping * identity
|
|
84
85
|
|
|
86
|
+
def normal_rhs(self, data: Vector) -> Vector:
|
|
87
|
+
"""
|
|
88
|
+
Returns the right hand side of the normal equations for given data.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
forward_operator = self.forward_problem.forward_operator
|
|
92
|
+
|
|
93
|
+
if self.forward_problem.data_error_measure_set:
|
|
94
|
+
inverse_data_covariance = (
|
|
95
|
+
self.forward_problem.data_error_measure.inverse_covariance
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
shifted_data = self.forward_problem.data_space.subtract(
|
|
99
|
+
data, self.forward_problem.data_error_measure.expectation
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return (forward_operator.adjoint @ inverse_data_covariance)(shifted_data)
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
return forward_operator.adjoint(data)
|
|
106
|
+
|
|
85
107
|
def least_squares_operator(
|
|
86
108
|
self,
|
|
87
109
|
damping: float,
|
|
@@ -105,6 +127,7 @@ class LinearLeastSquaresInversion(LinearInversion):
|
|
|
105
127
|
Returns:
|
|
106
128
|
An operator that maps from the data space to the model space.
|
|
107
129
|
"""
|
|
130
|
+
|
|
108
131
|
forward_operator = self.forward_problem.forward_operator
|
|
109
132
|
normal_operator = self.normal_operator(damping)
|
|
110
133
|
|
|
@@ -198,20 +221,33 @@ class LinearMinimumNormInversion(LinearInversion):
|
|
|
198
221
|
def get_model_for_damping(
|
|
199
222
|
damping: float, data: Vector, model0: Optional[Vector] = None
|
|
200
223
|
) -> tuple[Vector, float]:
|
|
201
|
-
"""
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
224
|
+
"""
|
|
225
|
+
Computes the LS model and its chi-squared for a given damping.
|
|
226
|
+
|
|
227
|
+
When an iterative solver is used, an initial guess can be provided.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
normal_operator = lsq_inversion.normal_operator(damping)
|
|
231
|
+
normal_rhs = lsq_inversion.normal_rhs(data)
|
|
232
|
+
|
|
233
|
+
if isinstance(solver, IterativeLinearSolver):
|
|
234
|
+
model = solver.solve_linear_system(
|
|
235
|
+
normal_operator, preconditioner, normal_rhs, model0
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
inverse_normal_operator = solver(normal_operator)
|
|
239
|
+
model = inverse_normal_operator(normal_rhs)
|
|
240
|
+
|
|
206
241
|
chi_squared = self.forward_problem.chi_squared(model, data)
|
|
207
242
|
return model, chi_squared
|
|
208
243
|
|
|
209
244
|
def mapping(data: Vector) -> Vector:
|
|
210
245
|
"""The non-linear mapping from data to the minimum-norm model."""
|
|
211
|
-
|
|
212
|
-
|
|
246
|
+
|
|
247
|
+
# Check to see if the zero model fits the data.
|
|
248
|
+
chi_squared = self.forward_problem.chi_squared_from_residual(data)
|
|
213
249
|
if chi_squared <= critical_value:
|
|
214
|
-
return
|
|
250
|
+
return self.model_space.zero
|
|
215
251
|
|
|
216
252
|
# Find upper and lower bounds for the optimal damping parameter
|
|
217
253
|
damping = 1.0
|
|
@@ -246,9 +282,10 @@ class LinearMinimumNormInversion(LinearInversion):
|
|
|
246
282
|
)
|
|
247
283
|
|
|
248
284
|
# Bracket search for the optimal damping
|
|
285
|
+
model0 = None
|
|
249
286
|
for _ in range(maxiter):
|
|
250
287
|
damping = 0.5 * (damping_lower + damping_upper)
|
|
251
|
-
model, chi_squared = get_model_for_damping(damping, data)
|
|
288
|
+
model, chi_squared = get_model_for_damping(damping, data, model0)
|
|
252
289
|
|
|
253
290
|
if chi_squared < critical_value:
|
|
254
291
|
damping_lower = damping
|
|
@@ -260,6 +297,8 @@ class LinearMinimumNormInversion(LinearInversion):
|
|
|
260
297
|
):
|
|
261
298
|
return model
|
|
262
299
|
|
|
300
|
+
model0 = model
|
|
301
|
+
|
|
263
302
|
raise RuntimeError("Bracketing search failed to converge.")
|
|
264
303
|
|
|
265
304
|
return NonLinearOperator(self.data_space, self.model_space, mapping)
|
pygeoinf/linear_solvers.py
CHANGED
pygeoinf/nonlinear_operators.py
CHANGED
|
@@ -11,13 +11,16 @@ from __future__ import annotations
|
|
|
11
11
|
from typing import Callable, Optional, Any, TYPE_CHECKING
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
from .checks.nonlinear_operators import NonLinearOperatorAxiomChecks
|
|
15
|
+
|
|
16
|
+
|
|
14
17
|
# This block only runs for type checkers, not at runtime
|
|
15
18
|
if TYPE_CHECKING:
|
|
16
19
|
from .hilbert_space import HilbertSpace, EuclideanSpace, Vector
|
|
17
20
|
from .linear_operators import LinearOperator
|
|
18
21
|
|
|
19
22
|
|
|
20
|
-
class NonLinearOperator:
|
|
23
|
+
class NonLinearOperator(NonLinearOperatorAxiomChecks):
|
|
21
24
|
"""
|
|
22
25
|
Represents a general non-linear operator that maps vectors to vectors.
|
|
23
26
|
|
|
@@ -17,6 +17,13 @@ from .nonlinear_forms import NonLinearForm
|
|
|
17
17
|
class ScipyUnconstrainedOptimiser:
|
|
18
18
|
"""
|
|
19
19
|
A wrapper for scipy.optimize.minimize that adapts a NonLinearForm.
|
|
20
|
+
|
|
21
|
+
Note on derivative-free methods:
|
|
22
|
+
Internal testing has shown that the 'Nelder-Mead' solver can be unreliable
|
|
23
|
+
for some problems, failing to converge to the correct minimum while still
|
|
24
|
+
reporting success. The 'Powell' method appears to be more robust. Users
|
|
25
|
+
should exercise caution and verify results when using derivative-free
|
|
26
|
+
methods.
|
|
20
27
|
"""
|
|
21
28
|
|
|
22
29
|
_HESSIAN_METHODS = {
|
|
@@ -168,7 +175,7 @@ def line_search(
|
|
|
168
175
|
# terms of the components of the derivative (i.e., an element
|
|
169
176
|
# of the dual space) and not the gradient, this meaning that
|
|
170
177
|
# the standard Euclidean pairing with the components on the
|
|
171
|
-
#
|
|
178
|
+
# descent direction will yield the correct slope.
|
|
172
179
|
def myfprime(c: np.ndarray) -> np.ndarray:
|
|
173
180
|
x = domain.from_components(c)
|
|
174
181
|
g = form.derivative(x)
|
|
@@ -225,6 +225,9 @@ class SphereHelper:
|
|
|
225
225
|
map_extent: Optional[List[float]] = None,
|
|
226
226
|
gridlines: bool = True,
|
|
227
227
|
symmetric: bool = False,
|
|
228
|
+
contour_lines: bool = False,
|
|
229
|
+
contour_lines_kwargs: Optional[dict] = None,
|
|
230
|
+
num_levels: int = 10,
|
|
228
231
|
**kwargs,
|
|
229
232
|
) -> Tuple[Figure, "GeoAxes", Any]:
|
|
230
233
|
"""
|
|
@@ -241,6 +244,11 @@ class SphereHelper:
|
|
|
241
244
|
map_extent: A list `[lon_min, lon_max, lat_min, lat_max]` to set map bounds.
|
|
242
245
|
gridlines: If True, draws latitude/longitude gridlines.
|
|
243
246
|
symmetric: If True, centers the color scale symmetrically around zero.
|
|
247
|
+
contour_lines: If True, overlays contour lines on the plot.
|
|
248
|
+
contour_lines_kwargs: A dictionary of keyword arguments for styling the
|
|
249
|
+
contour lines (e.g., {'colors': 'k', 'linewidths': 0.5})
|
|
250
|
+
num_levels: The number of levels to generate automatically if `levels`
|
|
251
|
+
is not provided directly.
|
|
244
252
|
**kwargs: Additional keyword arguments forwarded to the plotting function
|
|
245
253
|
(`ax.contourf` or `ax.pcolormesh`).
|
|
246
254
|
|
|
@@ -269,9 +277,17 @@ class SphereHelper:
|
|
|
269
277
|
kwargs.setdefault("vmin", -data_max)
|
|
270
278
|
kwargs.setdefault("vmax", data_max)
|
|
271
279
|
|
|
272
|
-
|
|
280
|
+
if "levels" in kwargs:
|
|
281
|
+
levels = kwargs.pop("levels")
|
|
282
|
+
else:
|
|
283
|
+
vmin = kwargs.get("vmin", np.nanmin(u.data))
|
|
284
|
+
vmax = kwargs.get("vmax", np.nanmax(u.data))
|
|
285
|
+
levels = np.linspace(vmin, vmax, num_levels)
|
|
286
|
+
|
|
273
287
|
im: Any
|
|
274
288
|
if contour:
|
|
289
|
+
kwargs.pop("vmin", None)
|
|
290
|
+
kwargs.pop("vmax", None)
|
|
275
291
|
im = ax.contourf(
|
|
276
292
|
lons,
|
|
277
293
|
lats,
|
|
@@ -285,6 +301,20 @@ class SphereHelper:
|
|
|
285
301
|
lons, lats, u.data, transform=ccrs.PlateCarree(), **kwargs
|
|
286
302
|
)
|
|
287
303
|
|
|
304
|
+
if contour_lines:
|
|
305
|
+
cl_kwargs = contour_lines_kwargs if contour_lines_kwargs is not None else {}
|
|
306
|
+
cl_kwargs.setdefault("colors", "k")
|
|
307
|
+
cl_kwargs.setdefault("linewidths", 0.5)
|
|
308
|
+
|
|
309
|
+
ax.contour(
|
|
310
|
+
lons,
|
|
311
|
+
lats,
|
|
312
|
+
u.data,
|
|
313
|
+
transform=ccrs.PlateCarree(),
|
|
314
|
+
levels=levels,
|
|
315
|
+
**cl_kwargs,
|
|
316
|
+
)
|
|
317
|
+
|
|
288
318
|
if gridlines:
|
|
289
319
|
lat_interval = kwargs.pop("lat_interval", 30)
|
|
290
320
|
lon_interval = kwargs.pop("lon_interval", 30)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pygeoinf
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: A package for solving geophysical inference and inverse problems
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Author: David Al-Attar and Dan Heathcote
|
|
@@ -16,7 +16,7 @@ Requires-Dist: matplotlib (>=3.0.0)
|
|
|
16
16
|
Requires-Dist: numpy (>=1.26.0)
|
|
17
17
|
Requires-Dist: pyqt6 (>=6.0.0)
|
|
18
18
|
Requires-Dist: pyshtools (>=4.0.0)
|
|
19
|
-
Requires-Dist: scipy (>=1.
|
|
19
|
+
Requires-Dist: scipy (>=1.16.1)
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
|
|
22
22
|
# pygeoinf: A Python Library for Geophysical Inference
|
|
@@ -233,7 +233,7 @@ plt.show()
|
|
|
233
233
|
|
|
234
234
|
The output of the above script will look similar to the following figure:
|
|
235
235
|
|
|
236
|
-

|
|
236
|
+

|
|
237
237
|
|
|
238
238
|
## Dependencies
|
|
239
239
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
pygeoinf/__init__.py,sha256=r5dumZhCLtuk9I-YXFDKnXB8t1gzjmgmjazkbqrZQpQ,1505
|
|
2
|
+
pygeoinf/backus_gilbert.py,sha256=vpIWvryUIy6pHWhT9A4bVB3A9MuTll1MyN9U8zmVI5c,3783
|
|
3
|
+
pygeoinf/checks/hilbert_space.py,sha256=Kr7PcOGrNIISezty0FBj5uXavIHC91yjCp2FVGNlHeE,7931
|
|
4
|
+
pygeoinf/checks/linear_operators.py,sha256=RkmtAW6e5Zr6EuhX6GAt_pI0IWu2WZ-CrfjSBN_7dsU,4664
|
|
5
|
+
pygeoinf/checks/nonlinear_operators.py,sha256=Rn9LTftyw5eGU3akx6xYNUJVGvX9J6gyTEXVFgLfYqs,5601
|
|
6
|
+
pygeoinf/direct_sum.py,sha256=1RHPJI_PoEvSRH9AmjX-v88IkSW4uT2rPSt5pmZQEZY,19377
|
|
7
|
+
pygeoinf/forward_problem.py,sha256=NnqWp7iMfkhHa9d-jBHzYHClaAfhKmO5D058AcJLLYg,10724
|
|
8
|
+
pygeoinf/gaussian_measure.py,sha256=EOUyBYT-K9u2ZD_uwPXDv17BJHk-L0RM55jfIR-DmXY,24020
|
|
9
|
+
pygeoinf/hilbert_space.py,sha256=0NCCG-OOHysdXYEFUs1wtJhGgOnuKvjZCZg8NJZO-DA,25331
|
|
10
|
+
pygeoinf/inversion.py,sha256=RV0hG2bGnciWdja0oOPKPxnFhYzufqdj-mKYNr4JJ_o,6447
|
|
11
|
+
pygeoinf/linear_bayesian.py,sha256=L1cJkeHtba4fPXZ8CmiLRBtuG2fmzG228M_iEar-iP8,9643
|
|
12
|
+
pygeoinf/linear_forms.py,sha256=sgynBvlQ35CaH12PKU2vWPHh9ikrmQbD5IASCUQtlbw,9197
|
|
13
|
+
pygeoinf/linear_operators.py,sha256=p03t2Azvdd4gakJws-myYDYDthyEskylsg6wODKbzJk,36424
|
|
14
|
+
pygeoinf/linear_optimisation.py,sha256=UbSr6AOPpR2sRYoN1Pvv24-Zu7_XlJk1zE1IhQu83hg,12428
|
|
15
|
+
pygeoinf/linear_solvers.py,sha256=mPhPiWKW82WHul_tfc0Xf-Y0GRtZQcPxfwEsWXh9G6M,15706
|
|
16
|
+
pygeoinf/nonlinear_forms.py,sha256=eQudA-HfedbURvRmzVvU8HfNCxHTuWUpdDoWe_KlA4Y,7067
|
|
17
|
+
pygeoinf/nonlinear_operators.py,sha256=X4_UMV1Rn4MqorjfN4P_UTckzCb4Gy1XceR3Ix8G4F8,7170
|
|
18
|
+
pygeoinf/nonlinear_optimisation.py,sha256=skK1ikn9GrVYherD64Qt9WrEYHA2NAJ48msOu_J8Oig,7431
|
|
19
|
+
pygeoinf/parallel.py,sha256=VVFvNHszy4wSa9LuErIsch4NAkLaZezhdN9YpRROBJo,2267
|
|
20
|
+
pygeoinf/random_matrix.py,sha256=afEUFuoVbkFobhC9Jy9SuGb4Yib-fn3pQyiWUqXrA-8,13629
|
|
21
|
+
pygeoinf/symmetric_space/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
pygeoinf/symmetric_space/circle.py,sha256=7Bz9BfSkbDnoz5-HFwTsAQE4a09jUapBePwoCK0xYWw,18007
|
|
23
|
+
pygeoinf/symmetric_space/sphere.py,sha256=5elw48I8A2i5EuRyYnm4craVp-ZB2_bBy9QQ15GytxE,23144
|
|
24
|
+
pygeoinf/symmetric_space/symmetric_space.py,sha256=Q3KtfCtHO0_8LjsdKtH-5WVhRQurt5Bdk4yx1D2F5YY,17977
|
|
25
|
+
pygeoinf-1.2.5.dist-info/LICENSE,sha256=GrTQnKJemVi69FSbHprq60KN0OJGsOSR-joQoTq-oD8,1501
|
|
26
|
+
pygeoinf-1.2.5.dist-info/METADATA,sha256=MzCodk83iQ3UmX4sM3UbQP5F0wWEsCxp3WwimgbZolU,15376
|
|
27
|
+
pygeoinf-1.2.5.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
28
|
+
pygeoinf-1.2.5.dist-info/RECORD,,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
pygeoinf-1.2.3.dist-info/RECORD
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
pygeoinf/__init__.py,sha256=LG49pW8RUK-NxO0KSEHNVG_tBClUK5GFLtjvNLDRnjE,1419
|
|
2
|
-
pygeoinf/backus_gilbert.py,sha256=bXJ0JKh49elNKLm5cGJj_RBh0oXcH3hpR7U-QUFHj8M,3657
|
|
3
|
-
pygeoinf/direct_sum.py,sha256=1RHPJI_PoEvSRH9AmjX-v88IkSW4uT2rPSt5pmZQEZY,19377
|
|
4
|
-
pygeoinf/forward_problem.py,sha256=iQsTQ4CV4XAqWd48EzhA82NMySGJSQ0_PaEtfG40agw,10529
|
|
5
|
-
pygeoinf/gaussian_measure.py,sha256=EOUyBYT-K9u2ZD_uwPXDv17BJHk-L0RM55jfIR-DmXY,24020
|
|
6
|
-
pygeoinf/hilbert_space.py,sha256=StS2AoTnOFTrh3XRyZ6K9lhQDqJijDaJGMC8RRagoTQ,25247
|
|
7
|
-
pygeoinf/inversion.py,sha256=3FiujTK4PDBPjS0aYdo02nHQjsVFL4GDqv4gvg2YilA,6189
|
|
8
|
-
pygeoinf/linear_bayesian.py,sha256=L1cJkeHtba4fPXZ8CmiLRBtuG2fmzG228M_iEar-iP8,9643
|
|
9
|
-
pygeoinf/linear_forms.py,sha256=sgynBvlQ35CaH12PKU2vWPHh9ikrmQbD5IASCUQtlbw,9197
|
|
10
|
-
pygeoinf/linear_operators.py,sha256=ha6QHKHVBd_MLMNmk8zAoqm_yDM2dClb8C6p13jo7Ik,36333
|
|
11
|
-
pygeoinf/linear_optimisation.py,sha256=sO155SkGg5H1RR-jmULru7R4vlCPjUce--6Z52l3Pks,11147
|
|
12
|
-
pygeoinf/linear_solvers.py,sha256=fPcr4f2mhSK34cHdRXk9LsonQJ_gLhXQYwCYA4O6Jv4,15706
|
|
13
|
-
pygeoinf/nonlinear_forms.py,sha256=eQudA-HfedbURvRmzVvU8HfNCxHTuWUpdDoWe_KlA4Y,7067
|
|
14
|
-
pygeoinf/nonlinear_operators.py,sha256=1FvimPwMxt0h1qOvTTjGabm-2ctDO4bT71LLro-7t68,7069
|
|
15
|
-
pygeoinf/nonlinear_optimisation.py,sha256=xcIJX6Uw6HuJ3OySGXm3cDQ-BVgIVi3jjtOpIHNq8ks,7074
|
|
16
|
-
pygeoinf/parallel.py,sha256=VVFvNHszy4wSa9LuErIsch4NAkLaZezhdN9YpRROBJo,2267
|
|
17
|
-
pygeoinf/random_matrix.py,sha256=afEUFuoVbkFobhC9Jy9SuGb4Yib-fn3pQyiWUqXrA-8,13629
|
|
18
|
-
pygeoinf/symmetric_space/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
pygeoinf/symmetric_space/__pycache__/__init__.cpython-311.pyc,sha256=mNBD912BewlTTEf1AutjuLwFyZzl76XX9PRCC1sH9D8,174
|
|
20
|
-
pygeoinf/symmetric_space/__pycache__/circle.cpython-311.pyc,sha256=Gi7nJQmhng8bXAjOa-TmJLES--96jyJLh_H_VImyV_Y,27071
|
|
21
|
-
pygeoinf/symmetric_space/__pycache__/sphere.cpython-311.pyc,sha256=sjTM71d-B70lMNjU-dinEQFY-QmVGFFdaiuVzemka5Y,32232
|
|
22
|
-
pygeoinf/symmetric_space/__pycache__/symmetric_space.cpython-311.pyc,sha256=msPMApzg25Z4Dg9smh9983sf7e3om26fQE4rlgGVGck,25610
|
|
23
|
-
pygeoinf/symmetric_space/circle.py,sha256=7Bz9BfSkbDnoz5-HFwTsAQE4a09jUapBePwoCK0xYWw,18007
|
|
24
|
-
pygeoinf/symmetric_space/sphere.py,sha256=poasBQEXV5WNSA9LBuCY2lsxv79aV90jKP13FSoQUmU,21950
|
|
25
|
-
pygeoinf/symmetric_space/symmetric_space.py,sha256=Q3KtfCtHO0_8LjsdKtH-5WVhRQurt5Bdk4yx1D2F5YY,17977
|
|
26
|
-
pygeoinf-1.2.3.dist-info/LICENSE,sha256=GrTQnKJemVi69FSbHprq60KN0OJGsOSR-joQoTq-oD8,1501
|
|
27
|
-
pygeoinf-1.2.3.dist-info/METADATA,sha256=zo3zrrAmpM1Swy2TycZrbRb3sfWmOsnyuvj1x8aelmw,15363
|
|
28
|
-
pygeoinf-1.2.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
29
|
-
pygeoinf-1.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|