pygeoinf 1.2.6__py3-none-any.whl → 1.2.8__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 -1
- pygeoinf/gaussian_measure.py +25 -36
- pygeoinf/linear_operators.py +957 -133
- pygeoinf/linear_solvers.py +39 -7
- pygeoinf/random_matrix.py +114 -0
- {pygeoinf-1.2.6.dist-info → pygeoinf-1.2.8.dist-info}/METADATA +8 -15
- {pygeoinf-1.2.6.dist-info → pygeoinf-1.2.8.dist-info}/RECORD +9 -9
- {pygeoinf-1.2.6.dist-info → pygeoinf-1.2.8.dist-info}/LICENSE +0 -0
- {pygeoinf-1.2.6.dist-info → pygeoinf-1.2.8.dist-info}/WHEEL +0 -0
pygeoinf/linear_solvers.py
CHANGED
|
@@ -239,6 +239,18 @@ class IterativeLinearSolver(LinearSolver):
|
|
|
239
239
|
An abstract base class for iterative linear solvers.
|
|
240
240
|
"""
|
|
241
241
|
|
|
242
|
+
def __init__(self, /, *, preconditioning_method: LinearSolver = None) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Args:
|
|
245
|
+
preconditioning_method: A LinearSolver from which to generate a preconditioner
|
|
246
|
+
once the operator is known.
|
|
247
|
+
|
|
248
|
+
Notes:
|
|
249
|
+
If a preconditioner is provided to either the call or solve_linear_system
|
|
250
|
+
methods, then it takes precedence over the preconditioning method.
|
|
251
|
+
"""
|
|
252
|
+
self._preconditioning_method = preconditioning_method
|
|
253
|
+
|
|
242
254
|
@abstractmethod
|
|
243
255
|
def solve_linear_system(
|
|
244
256
|
self,
|
|
@@ -263,16 +275,13 @@ class IterativeLinearSolver(LinearSolver):
|
|
|
263
275
|
def solve_adjoint_linear_system(
|
|
264
276
|
self,
|
|
265
277
|
operator: LinearOperator,
|
|
266
|
-
|
|
278
|
+
adjoint_preconditioner: Optional[LinearOperator],
|
|
267
279
|
x: Vector,
|
|
268
280
|
y0: Optional[Vector],
|
|
269
281
|
) -> Vector:
|
|
270
282
|
"""
|
|
271
283
|
Solves the adjoint linear system A*y = x for y.
|
|
272
284
|
"""
|
|
273
|
-
adjoint_preconditioner = (
|
|
274
|
-
None if preconditioner is None else preconditioner.adjoint
|
|
275
|
-
)
|
|
276
285
|
return self.solve_linear_system(operator.adjoint, adjoint_preconditioner, x, y0)
|
|
277
286
|
|
|
278
287
|
def __call__(
|
|
@@ -295,12 +304,27 @@ class IterativeLinearSolver(LinearSolver):
|
|
|
295
304
|
original operator.
|
|
296
305
|
"""
|
|
297
306
|
assert operator.is_automorphism
|
|
307
|
+
|
|
308
|
+
if preconditioner is None:
|
|
309
|
+
if self._preconditioning_method is None:
|
|
310
|
+
_preconditioner = None
|
|
311
|
+
_adjoint_preconditions = None
|
|
312
|
+
else:
|
|
313
|
+
_preconditioner = self._preconditioning_method(operator)
|
|
314
|
+
else:
|
|
315
|
+
_preconditioner = preconditioner
|
|
316
|
+
|
|
317
|
+
if _preconditioner is None:
|
|
318
|
+
_adjoint_preconditioner = None
|
|
319
|
+
else:
|
|
320
|
+
_adjoint_preconditioner = _preconditioner.adjoint
|
|
321
|
+
|
|
298
322
|
return LinearOperator(
|
|
299
323
|
operator.codomain,
|
|
300
324
|
operator.domain,
|
|
301
|
-
lambda y: self.solve_linear_system(operator,
|
|
325
|
+
lambda y: self.solve_linear_system(operator, _preconditioner, y, None),
|
|
302
326
|
adjoint_mapping=lambda x: self.solve_adjoint_linear_system(
|
|
303
|
-
operator,
|
|
327
|
+
operator, _adjoint_preconditioner, x, None
|
|
304
328
|
),
|
|
305
329
|
)
|
|
306
330
|
|
|
@@ -327,6 +351,7 @@ class ScipyIterativeSolver(IterativeLinearSolver):
|
|
|
327
351
|
method: str,
|
|
328
352
|
/,
|
|
329
353
|
*,
|
|
354
|
+
preconditioning_method: LinearSolver = None,
|
|
330
355
|
galerkin: bool = False,
|
|
331
356
|
**kwargs,
|
|
332
357
|
) -> None:
|
|
@@ -337,6 +362,9 @@ class ScipyIterativeSolver(IterativeLinearSolver):
|
|
|
337
362
|
**kwargs: Keyword arguments to be passed directly to the SciPy solver
|
|
338
363
|
(e.g., rtol, atol, maxiter, restart).
|
|
339
364
|
"""
|
|
365
|
+
|
|
366
|
+
super().__init__(preconditioning_method=preconditioning_method)
|
|
367
|
+
|
|
340
368
|
if method not in self._SOLVER_MAP:
|
|
341
369
|
raise ValueError(
|
|
342
370
|
f"Unknown solver method '{method}'. Available methods: {list(self._SOLVER_MAP.keys())}"
|
|
@@ -410,6 +438,7 @@ class CGSolver(IterativeLinearSolver):
|
|
|
410
438
|
self,
|
|
411
439
|
/,
|
|
412
440
|
*,
|
|
441
|
+
preconditioning_method: LinearSolver = None,
|
|
413
442
|
rtol: float = 1.0e-5,
|
|
414
443
|
atol: float = 0.0,
|
|
415
444
|
maxiter: Optional[int] = None,
|
|
@@ -423,6 +452,9 @@ class CGSolver(IterativeLinearSolver):
|
|
|
423
452
|
callback (callable, optional): User-supplied function to call
|
|
424
453
|
after each iteration with the current solution vector.
|
|
425
454
|
"""
|
|
455
|
+
|
|
456
|
+
super().__init__(preconditioning_method=preconditioning_method)
|
|
457
|
+
|
|
426
458
|
if not rtol > 0:
|
|
427
459
|
raise ValueError("rtol must be positive")
|
|
428
460
|
self._rtol: float = rtol
|
|
@@ -463,7 +495,7 @@ class CGSolver(IterativeLinearSolver):
|
|
|
463
495
|
|
|
464
496
|
num = domain.inner_product(r, z)
|
|
465
497
|
|
|
466
|
-
for
|
|
498
|
+
for _ in range(maxiter):
|
|
467
499
|
# Check for convergence
|
|
468
500
|
if domain.squared_norm(r) <= tol_sq:
|
|
469
501
|
break
|
pygeoinf/random_matrix.py
CHANGED
|
@@ -385,3 +385,117 @@ def random_cholesky(
|
|
|
385
385
|
cholesky_factor = temp_factor * sqrt_s_inv
|
|
386
386
|
|
|
387
387
|
return cholesky_factor
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def random_diagonal(
|
|
391
|
+
matrix: MatrixLike,
|
|
392
|
+
size_estimate: int,
|
|
393
|
+
/,
|
|
394
|
+
*,
|
|
395
|
+
method: str = "variable",
|
|
396
|
+
use_rademacher: bool = False,
|
|
397
|
+
max_samples: int = None,
|
|
398
|
+
rtol: float = 1e-2,
|
|
399
|
+
block_size: int = 10,
|
|
400
|
+
parallel: bool = False,
|
|
401
|
+
n_jobs: int = -1,
|
|
402
|
+
) -> np.ndarray:
|
|
403
|
+
"""
|
|
404
|
+
Computes an approximate diagonal of a square matrix using Hutchinson's method.
|
|
405
|
+
|
|
406
|
+
This algorithm uses a progressive, iterative approach to estimate the diagonal.
|
|
407
|
+
It starts with an initial number of samples and adds new blocks of random
|
|
408
|
+
vectors until the estimate of the diagonal converges to a specified tolerance.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
matrix: The (m, n) matrix or LinearOperator to analyze.
|
|
412
|
+
size_estimate: For 'fixed' method, the exact target rank. For 'variable'
|
|
413
|
+
method, this is the initial rank to sample.
|
|
414
|
+
method ({'variable', 'fixed'}): The algorithm to use.
|
|
415
|
+
- 'variable': (Default) Progressively samples to find the rank needed
|
|
416
|
+
to meet tolerance `rtol`, stopping at `max_rank`.
|
|
417
|
+
- 'fixed': Returns a basis with exactly `size_estimate` columns.
|
|
418
|
+
use_rademacher: If true, draw components from [-1,1]. Default method draws
|
|
419
|
+
normally distributed components.
|
|
420
|
+
max_samples: For 'variable' method, a hard limit on the number of samples.
|
|
421
|
+
Ignored if method='fixed'. Defaults to dimension of matrix.
|
|
422
|
+
rtol: Relative tolerance for the 'variable' method. Ignored if
|
|
423
|
+
method='fixed'.
|
|
424
|
+
block_size: Number of new vectors to sample per iteration in 'variable'
|
|
425
|
+
method. Ignored if method='fixed'.
|
|
426
|
+
parallel: Whether to use parallel matrix multiplication.
|
|
427
|
+
n_jobs: Number of jobs for parallelism.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
A 1D numpy array of size n containing the approximate diagonal of the matrix.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
m, n = matrix.shape
|
|
434
|
+
if m != n:
|
|
435
|
+
raise ValueError("Input matrix must be square to estimate a diagonal.")
|
|
436
|
+
|
|
437
|
+
if max_samples is None:
|
|
438
|
+
max_samples = n
|
|
439
|
+
|
|
440
|
+
num_samples = min(size_estimate, max_samples)
|
|
441
|
+
if use_rademacher:
|
|
442
|
+
z = np.random.choice([-1.0, 1.0], size=(n, num_samples))
|
|
443
|
+
else:
|
|
444
|
+
z = np.random.randn(n, num_samples)
|
|
445
|
+
|
|
446
|
+
if parallel:
|
|
447
|
+
az = parallel_mat_mat(matrix, z, n_jobs)
|
|
448
|
+
else:
|
|
449
|
+
az = matrix @ z
|
|
450
|
+
|
|
451
|
+
diag_sum = np.sum(z * az, axis=1)
|
|
452
|
+
diag_estimate = diag_sum / num_samples
|
|
453
|
+
|
|
454
|
+
if method == "fixed":
|
|
455
|
+
return diag_estimate
|
|
456
|
+
|
|
457
|
+
if num_samples >= max_samples:
|
|
458
|
+
return diag_estimate
|
|
459
|
+
|
|
460
|
+
converged = False
|
|
461
|
+
while num_samples < max_samples:
|
|
462
|
+
old_diag_estimate = diag_estimate.copy()
|
|
463
|
+
|
|
464
|
+
# Generate a NEW block of random vectors
|
|
465
|
+
samples_to_add = min(block_size, max_samples - num_samples)
|
|
466
|
+
if use_rademacher:
|
|
467
|
+
z_new = np.random.choice([-1.0, 1.0], size=(n, samples_to_add))
|
|
468
|
+
else:
|
|
469
|
+
z_new = np.random.randn(n, samples_to_add)
|
|
470
|
+
|
|
471
|
+
if parallel:
|
|
472
|
+
az_new = parallel_mat_mat(matrix, z_new, n_jobs)
|
|
473
|
+
else:
|
|
474
|
+
az_new = matrix @ z_new
|
|
475
|
+
|
|
476
|
+
new_diag_sum = np.sum(z_new * az_new, axis=1)
|
|
477
|
+
|
|
478
|
+
# Update the running average
|
|
479
|
+
total_samples = num_samples + samples_to_add
|
|
480
|
+
diag_estimate = (diag_sum + new_diag_sum) / total_samples
|
|
481
|
+
|
|
482
|
+
# Check for convergence
|
|
483
|
+
norm_new_diag = np.linalg.norm(diag_estimate)
|
|
484
|
+
if norm_new_diag > 0:
|
|
485
|
+
error = np.linalg.norm(diag_estimate - old_diag_estimate) / norm_new_diag
|
|
486
|
+
if error < rtol:
|
|
487
|
+
converged = True
|
|
488
|
+
break
|
|
489
|
+
|
|
490
|
+
# Update sums and counts for next iteration
|
|
491
|
+
diag_sum += new_diag_sum
|
|
492
|
+
num_samples = total_samples
|
|
493
|
+
|
|
494
|
+
if not converged and num_samples >= max_samples:
|
|
495
|
+
warnings.warn(
|
|
496
|
+
f"Tolerance {rtol} not met before reaching max_samples={max_samples}. "
|
|
497
|
+
"Result may be inaccurate. Consider increasing `max_samples` or `rtol`.",
|
|
498
|
+
UserWarning,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
return diag_estimate
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pygeoinf
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.8
|
|
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
|
|
@@ -64,22 +64,15 @@ git clone https://github.com/da380/pygeoinf.git
|
|
|
64
64
|
cd pygeoinf
|
|
65
65
|
poetry install
|
|
66
66
|
```
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
You can install all optional dependencies for development—including tools for running the test suite,
|
|
68
|
+
building the documentation, and running the Jupyter tutorials—by using the ```--with``` flag and specifying the ```dev``` group.
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
#
|
|
72
|
-
poetry install --with
|
|
73
|
-
|
|
74
|
-
# To install dependencies for building the documentation
|
|
75
|
-
poetry install --with docs
|
|
71
|
+
# Install all development dependencies (for tests, docs, and tutorials)
|
|
72
|
+
poetry install --with dev
|
|
73
|
+
```
|
|
76
74
|
|
|
77
|
-
# To install dependencies for running the Jupyter tutorials
|
|
78
|
-
poetry install --with tutorials
|
|
79
75
|
|
|
80
|
-
# You can also combine them
|
|
81
|
-
poetry install --with tests,docs,tutorials
|
|
82
|
-
```
|
|
83
76
|
|
|
84
77
|
## Documentation
|
|
85
78
|
|
|
@@ -245,9 +238,9 @@ The output of the above script will look similar to the following figure:
|
|
|
245
238
|
|
|
246
239
|
## Future Plans
|
|
247
240
|
|
|
248
|
-
`pygeoinf` is under active development.
|
|
241
|
+
`pygeoinf` is under active development. Current work is focused on expanding the library's capabilities to address a broader range of geophysical problems. Key areas for development include:
|
|
249
242
|
|
|
250
|
-
* **Generalised Backus-Gilbert Methods**: Implementation of a generalised Backus-Gilbert framework for linear inference problems
|
|
243
|
+
* **Generalised Backus-Gilbert Methods**: Implementation of a generalised Backus-Gilbert framework for linear inference problems. The focus will be on constructing direct estimates of specific properties of interest (i.e., linear functionals of the model) from data, without needing to first solve for the full model itself.
|
|
251
244
|
|
|
252
245
|
* **Non-linear Optimisation**: Extension of the current optimisation framework to handle non-linear inverse problems. This will involve creating a general interface where users can provide their own non-linear forward mapping, a misfit functional, and methods for computing gradients (and optionally Hessians) for use in gradient-based optimisation algorithms.
|
|
253
246
|
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
pygeoinf/__init__.py,sha256=
|
|
1
|
+
pygeoinf/__init__.py,sha256=pV9UXNrRXAIagU4eTmHf2Qi0CtVgOp0sqLnltgBFoKc,1650
|
|
2
2
|
pygeoinf/backus_gilbert.py,sha256=vpIWvryUIy6pHWhT9A4bVB3A9MuTll1MyN9U8zmVI5c,3783
|
|
3
3
|
pygeoinf/checks/hilbert_space.py,sha256=Kr7PcOGrNIISezty0FBj5uXavIHC91yjCp2FVGNlHeE,7931
|
|
4
4
|
pygeoinf/checks/linear_operators.py,sha256=RkmtAW6e5Zr6EuhX6GAt_pI0IWu2WZ-CrfjSBN_7dsU,4664
|
|
5
5
|
pygeoinf/checks/nonlinear_operators.py,sha256=Rn9LTftyw5eGU3akx6xYNUJVGvX9J6gyTEXVFgLfYqs,5601
|
|
6
6
|
pygeoinf/direct_sum.py,sha256=1RHPJI_PoEvSRH9AmjX-v88IkSW4uT2rPSt5pmZQEZY,19377
|
|
7
7
|
pygeoinf/forward_problem.py,sha256=NnqWp7iMfkhHa9d-jBHzYHClaAfhKmO5D058AcJLLYg,10724
|
|
8
|
-
pygeoinf/gaussian_measure.py,sha256=
|
|
8
|
+
pygeoinf/gaussian_measure.py,sha256=zYpvQ29jBpWE2tDTF1rXZQiSICDuoxizVP4ZJr1k6Fk,23665
|
|
9
9
|
pygeoinf/hilbert_space.py,sha256=0NCCG-OOHysdXYEFUs1wtJhGgOnuKvjZCZg8NJZO-DA,25331
|
|
10
10
|
pygeoinf/inversion.py,sha256=RV0hG2bGnciWdja0oOPKPxnFhYzufqdj-mKYNr4JJ_o,6447
|
|
11
11
|
pygeoinf/linear_bayesian.py,sha256=L1cJkeHtba4fPXZ8CmiLRBtuG2fmzG228M_iEar-iP8,9643
|
|
12
12
|
pygeoinf/linear_forms.py,sha256=sgynBvlQ35CaH12PKU2vWPHh9ikrmQbD5IASCUQtlbw,9197
|
|
13
|
-
pygeoinf/linear_operators.py,sha256=
|
|
13
|
+
pygeoinf/linear_operators.py,sha256=HToie5nllV0PQFa-QzETtIPKveWnt2dkWiaWTJa6fTw,64577
|
|
14
14
|
pygeoinf/linear_optimisation.py,sha256=UbSr6AOPpR2sRYoN1Pvv24-Zu7_XlJk1zE1IhQu83hg,12428
|
|
15
|
-
pygeoinf/linear_solvers.py,sha256=
|
|
15
|
+
pygeoinf/linear_solvers.py,sha256=v-7yjKsa67Ts5EcyJzCdpj-aF0qBrA-akq0kLe59DS4,16843
|
|
16
16
|
pygeoinf/nonlinear_forms.py,sha256=eQudA-HfedbURvRmzVvU8HfNCxHTuWUpdDoWe_KlA4Y,7067
|
|
17
17
|
pygeoinf/nonlinear_operators.py,sha256=X4_UMV1Rn4MqorjfN4P_UTckzCb4Gy1XceR3Ix8G4F8,7170
|
|
18
18
|
pygeoinf/nonlinear_optimisation.py,sha256=skK1ikn9GrVYherD64Qt9WrEYHA2NAJ48msOu_J8Oig,7431
|
|
19
19
|
pygeoinf/parallel.py,sha256=VVFvNHszy4wSa9LuErIsch4NAkLaZezhdN9YpRROBJo,2267
|
|
20
|
-
pygeoinf/random_matrix.py,sha256=
|
|
20
|
+
pygeoinf/random_matrix.py,sha256=71l6eAXQ2pRMleaz1lXud6O1F78ugKyp3vHcRBXhdwM,17661
|
|
21
21
|
pygeoinf/symmetric_space/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
pygeoinf/symmetric_space/circle.py,sha256=7Bz9BfSkbDnoz5-HFwTsAQE4a09jUapBePwoCK0xYWw,18007
|
|
23
23
|
pygeoinf/symmetric_space/sphere.py,sha256=5elw48I8A2i5EuRyYnm4craVp-ZB2_bBy9QQ15GytxE,23144
|
|
24
24
|
pygeoinf/symmetric_space/symmetric_space.py,sha256=Q3KtfCtHO0_8LjsdKtH-5WVhRQurt5Bdk4yx1D2F5YY,17977
|
|
25
|
-
pygeoinf-1.2.
|
|
26
|
-
pygeoinf-1.2.
|
|
27
|
-
pygeoinf-1.2.
|
|
28
|
-
pygeoinf-1.2.
|
|
25
|
+
pygeoinf-1.2.8.dist-info/LICENSE,sha256=GrTQnKJemVi69FSbHprq60KN0OJGsOSR-joQoTq-oD8,1501
|
|
26
|
+
pygeoinf-1.2.8.dist-info/METADATA,sha256=BLWdHc9FE8C4X7L0y9xHAdQfY3_pkzljiNPIPZAqfH4,15169
|
|
27
|
+
pygeoinf-1.2.8.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
28
|
+
pygeoinf-1.2.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|