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.
@@ -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
- preconditioner: Optional[LinearOperator],
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, preconditioner, y, None),
325
+ lambda y: self.solve_linear_system(operator, _preconditioner, y, None),
302
326
  adjoint_mapping=lambda x: self.solve_adjoint_linear_system(
303
- operator, preconditioner, x, None
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 i in range(maxiter):
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.6
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
- You can install the optional dependencies for running tests, building documentation, or running the tutorials by using the --with flag.
67
+ You can install all optional dependencies for development—including tools for running the test suite,
68
+ building the documentation, and running the Jupyter tutorialsby using the ```--with``` flag and specifying the ```dev``` group.
69
69
 
70
70
  ```bash
71
- # To install dependencies for running the test suite
72
- poetry install --with tests
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. Future work will focus on expanding the library's capabilities to address a broader range of geophysical problems. Key areas for development include:
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, building on the work of [Al-Attar(2021)](https://arxiv.org/abs/2104.12256). 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.
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=r5dumZhCLtuk9I-YXFDKnXB8t1gzjmgmjazkbqrZQpQ,1505
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=EOUyBYT-K9u2ZD_uwPXDv17BJHk-L0RM55jfIR-DmXY,24020
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=p03t2Azvdd4gakJws-myYDYDthyEskylsg6wODKbzJk,36424
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=mPhPiWKW82WHul_tfc0Xf-Y0GRtZQcPxfwEsWXh9G6M,15706
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=afEUFuoVbkFobhC9Jy9SuGb4Yib-fn3pQyiWUqXrA-8,13629
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.6.dist-info/LICENSE,sha256=GrTQnKJemVi69FSbHprq60KN0OJGsOSR-joQoTq-oD8,1501
26
- pygeoinf-1.2.6.dist-info/METADATA,sha256=HJkNlabCyoWvQ9Ogvwp2PPIUX1-jJKTyN1glD706FtQ,15376
27
- pygeoinf-1.2.6.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
28
- pygeoinf-1.2.6.dist-info/RECORD,,
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,,