pygeoinf 1.3.8__py3-none-any.whl → 1.4.0__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 CHANGED
@@ -104,8 +104,27 @@ from .nonlinear_optimisation import (
104
104
 
105
105
  from .subspaces import OrthogonalProjector, AffineSubspace, LinearSubspace
106
106
 
107
+ from .subsets import (
108
+ Subset,
109
+ EmptySet,
110
+ UniversalSet,
111
+ Complement,
112
+ Intersection,
113
+ Union,
114
+ SublevelSet,
115
+ LevelSet,
116
+ ConvexSubset,
117
+ Ellipsoid,
118
+ NormalisedEllipsoid,
119
+ EllipsoidSurface,
120
+ Ball,
121
+ Sphere,
122
+ )
123
+
107
124
  from .plot import plot_1d_distributions, plot_corner_distributions
108
125
 
126
+ from .utils import configure_threading
127
+
109
128
  __all__ = [
110
129
  # random_matrix
111
130
  "fixed_rank_random_range",
@@ -184,7 +203,24 @@ __all__ = [
184
203
  "OrthogonalProjector",
185
204
  "AffineSubspace",
186
205
  "LinearSubspace",
206
+ # Subsets
207
+ "Subset",
208
+ "EmptySet",
209
+ "UniversalSet",
210
+ "Complement",
211
+ "Intersection",
212
+ "Union",
213
+ "SublevelSet",
214
+ "LevelSet",
215
+ "ConvexSubset",
216
+ "Ellipsoid",
217
+ "NormalisedEllipsoid",
218
+ "EllipsoidSurface",
219
+ "Ball",
220
+ "Sphere",
187
221
  # plot
188
222
  "plot_1d_distributions",
189
223
  "plot_corner_distributions",
224
+ # utils
225
+ "configure_threading",
190
226
  ]
@@ -27,7 +27,7 @@ import numpy as np
27
27
  from scipy.linalg import eigh
28
28
  from scipy.sparse import diags
29
29
  from scipy.stats import multivariate_normal
30
-
30
+ from joblib import Parallel, delayed
31
31
 
32
32
  from .hilbert_space import EuclideanSpace, HilbertModule, Vector
33
33
 
@@ -44,7 +44,6 @@ from .direct_sum import (
44
44
  # This block is only processed by type checkers, not at runtime.
45
45
  if TYPE_CHECKING:
46
46
  from .hilbert_space import HilbertSpace
47
- from .typing import Vector
48
47
 
49
48
 
50
49
  class GaussianMeasure:
@@ -402,24 +401,52 @@ class GaussianMeasure:
402
401
  raise NotImplementedError("A sample method is not set for this measure.")
403
402
  return self._sample()
404
403
 
405
- def samples(self, n: int) -> List[Vector]:
406
- """Returns a list of n random samples from the measure."""
404
+ def samples(
405
+ self, n: int, /, *, parallel: bool = False, n_jobs: int = -1
406
+ ) -> List[Vector]:
407
+ """
408
+ Returns a list of n random samples from the measure.
409
+
410
+ Args:
411
+ n: Number of samples to draw.
412
+ parallel: If True, draws samples in parallel.
413
+ n_jobs: Number of CPU cores to use. -1 means all available.
414
+ """
407
415
  if n < 1:
408
416
  raise ValueError("Number of samples must be a positive integer.")
409
- return [self.sample() for _ in range(n)]
410
417
 
411
- def sample_expectation(self, n: int) -> Vector:
412
- """Estimates the expectation by drawing n samples."""
418
+ if not parallel:
419
+ return [self.sample() for _ in range(n)]
420
+
421
+ return Parallel(n_jobs=n_jobs)(delayed(self.sample)() for _ in range(n))
422
+
423
+ def sample_expectation(
424
+ self, n: int, /, *, parallel: bool = False, n_jobs: int = -1
425
+ ) -> Vector:
426
+ """
427
+ Estimates the expectation by drawing n samples.
428
+
429
+ Args:
430
+ n: Number of samples to draw.
431
+ parallel: If True, draws samples in parallel.
432
+ n_jobs: Number of CPU cores to use. -1 means all available.
433
+ """
413
434
  if n < 1:
414
435
  raise ValueError("Number of samples must be a positive integer.")
415
- return self.domain.sample_expectation(self.samples(n))
436
+ return self.domain.sample_expectation(
437
+ self.samples(n, parallel=parallel, n_jobs=n_jobs)
438
+ )
416
439
 
417
- def sample_pointwise_variance(self, n: int) -> Vector:
440
+ def sample_pointwise_variance(
441
+ self, n: int, /, *, parallel: bool = False, n_jobs: int = -1
442
+ ) -> Vector:
418
443
  """
419
444
  Estimates the pointwise variance by drawing n samples.
420
445
 
421
- This method is only available if the domain supports vector
422
- multiplication.
446
+ Args:
447
+ n: Number of samples to draw.
448
+ parallel: If True, draws samples in parallel.
449
+ n_jobs: Number of CPU cores to use. -1 means all available.
423
450
  """
424
451
  if not isinstance(self.domain, HilbertModule):
425
452
  raise NotImplementedError(
@@ -428,7 +455,10 @@ class GaussianMeasure:
428
455
  if n < 1:
429
456
  raise ValueError("Number of samples must be a positive integer.")
430
457
 
431
- samples = self.samples(n)
458
+ # Draw samples
459
+ samples = self.samples(n, parallel=parallel, n_jobs=n_jobs)
460
+
461
+ # Compute variance using vector arithmetic
432
462
  expectation = self.expectation
433
463
  variance = self.domain.zero
434
464
 
@@ -439,6 +469,42 @@ class GaussianMeasure:
439
469
 
440
470
  return variance
441
471
 
472
+ def sample_pointwise_std(
473
+ self, n: int, /, *, parallel: bool = False, n_jobs: int = -1
474
+ ) -> Vector:
475
+ """
476
+ Estimates the pointwise standard deviation by drawing n samples.
477
+
478
+ Args:
479
+ n: Number of samples to draw.
480
+ parallel: If True, draws samples in parallel.
481
+ n_jobs: Number of CPU cores to use. -1 means all available.
482
+ """
483
+ variance = self.sample_pointwise_variance(n, parallel=parallel, n_jobs=n_jobs)
484
+ return self.domain.vector_sqrt(variance)
485
+
486
+ def with_dense_covariance(self, parallel: bool = False, n_jobs: int = -1):
487
+ """
488
+ Forms a new Gaussian measure equivalent to the existing one, but
489
+ with its covariance matrix stored in dense form. The dense matrix
490
+ calculation can optionally be parallelised.
491
+
492
+ Args:
493
+ parallel: If True, computes the covariance in parallel.
494
+ n_jobs: Number of CPU cores to use. -1 means all available.
495
+
496
+ Returns:
497
+ The new Gaussian measure.
498
+ """
499
+
500
+ covariance_matrix = self.covariance.matrix(
501
+ dense=True, galerkin=True, parallel=parallel, n_jobs=n_jobs
502
+ )
503
+
504
+ return GaussianMeasure.from_covariance_matrix(
505
+ self.domain, covariance_matrix, expectation=self.expectation
506
+ )
507
+
442
508
  def affine_mapping(
443
509
  self, /, *, operator: LinearOperator = None, translation: Vector = None
444
510
  ) -> GaussianMeasure:
@@ -516,7 +582,7 @@ class GaussianMeasure:
516
582
 
517
583
  # Pass the parallelization arguments directly to the matrix creation method
518
584
  cov_matrix = self.covariance.matrix(
519
- dense=True, parallel=parallel, n_jobs=n_jobs
585
+ dense=True, galerkin=True, parallel=parallel, n_jobs=n_jobs
520
586
  )
521
587
 
522
588
  try:
pygeoinf/hilbert_space.py CHANGED
@@ -547,6 +547,12 @@ class HilbertModule(HilbertSpace, ABC):
547
547
  The product of the two vectors.
548
548
  """
549
549
 
550
+ @abstractmethod
551
+ def vector_sqrt(self, x: Vector) -> Vector:
552
+ """
553
+ Returns the square root of a vector.
554
+ """
555
+
550
556
 
551
557
  class EuclideanSpace(HilbertSpace):
552
558
  """
@@ -829,3 +835,12 @@ class MassWeightedHilbertModule(MassWeightedHilbertSpace, HilbertModule):
829
835
  is itself an instance of `HilbertModule`.
830
836
  """
831
837
  return self.underlying_space.vector_multiply(x1, x2)
838
+
839
+ def vector_sqrt(self, x: Vector) -> Vector:
840
+ """
841
+ Computes vector multiplication by delegating to the underlying space.
842
+
843
+ Note: This assumes the underlying space provided during initialization
844
+ is itself an instance of `HilbertModule`.
845
+ """
846
+ return self.underlying_space.vector_sqrt(x)
@@ -102,10 +102,13 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
102
102
  self.__adjoint_mapping: Callable[[Any], Any]
103
103
  self.__dual_mapping: Callable[[Any], Any]
104
104
 
105
+ self.__using_default_dual_and_adjoint = False
106
+
105
107
  if dual_mapping is None:
106
108
  if adjoint_mapping is None:
107
109
  self.__dual_mapping = self._dual_mapping_default
108
110
  self.__adjoint_mapping = self._adjoint_mapping_from_dual
111
+ self.__using_default_dual_and_adjoint = True
109
112
  else:
110
113
  self.__adjoint_mapping = adjoint_mapping
111
114
  self.__dual_mapping = self._dual_mapping_from_adjoint
@@ -919,6 +922,35 @@ class LinearOperator(NonLinearOperator, LinearOperatorAxiomChecks):
919
922
  self, galerkin: bool, parallel: bool, n_jobs: int
920
923
  ) -> np.ndarray:
921
924
 
925
+ # Optimization: If the codomain is smaller than the domain, it is cheaper
926
+ # to compute the matrix of the adjoint/dual (which has fewer columns)
927
+ # and transpose the result.
928
+
929
+ # Note: This recursion naturally terminates because the adjoint/dual
930
+ # swaps the domain and codomain. In the recursive call,
931
+ # (codomain.dim < domain.dim) will be False, forcing the standard path.
932
+
933
+ # If the operator has its dual and adjoint actions done using the
934
+ # default implementation, this optimisation is skipped.
935
+ if (
936
+ self.codomain.dim < self.domain.dim
937
+ and not self.__using_default_dual_and_adjoint
938
+ ):
939
+ if galerkin:
940
+ # For Galerkin representations: Matrix(L) = Matrix(L*).T
941
+ return self.adjoint.matrix(
942
+ dense=True, galerkin=True, parallel=parallel, n_jobs=n_jobs
943
+ ).T
944
+ else:
945
+ # For Standard representations: Matrix(L) = Matrix(L').T
946
+ return self.dual.matrix(
947
+ dense=True, galerkin=False, parallel=parallel, n_jobs=n_jobs
948
+ ).T
949
+
950
+ # --- Standard Column-wise Construction ---
951
+ # This block executes if optimization is not applicable (or in the
952
+ # recursive base case).
953
+
922
954
  scipy_op_wrapper = self.matrix(galerkin=galerkin)
923
955
 
924
956
  if not parallel:
pygeoinf/plot.py CHANGED
@@ -220,6 +220,8 @@ def plot_corner_distributions(
220
220
  show_plot: bool = True,
221
221
  include_sigma_contours: bool = True,
222
222
  colormap: str = "Blues",
223
+ parallel: bool = False,
224
+ n_jobs: int = -1,
223
225
  ):
224
226
  """
225
227
  Create a corner plot for multi-dimensional posterior distributions.
@@ -233,6 +235,8 @@ def plot_corner_distributions(
233
235
  show_plot: Whether to display the plot
234
236
  include_sigma_contours: Whether to include 1-sigma contour lines
235
237
  colormap: Colormap for 2D plots
238
+ parallel: Compute dense covariance matrix in parallel, default False.
239
+ n_jobs: Number of cores to use in parallel calculations, default -1.
236
240
 
237
241
  Returns:
238
242
  fig, axes: Figure and axes array
@@ -243,7 +247,9 @@ def plot_corner_distributions(
243
247
  posterior_measure, "covariance"
244
248
  ):
245
249
  mean_posterior = posterior_measure.expectation
246
- cov_posterior = posterior_measure.covariance.matrix(dense=True, parallel=True)
250
+ cov_posterior = posterior_measure.covariance.matrix(
251
+ dense=True, parallel=parallel, n_jobs=n_jobs
252
+ )
247
253
  else:
248
254
  raise ValueError(
249
255
  "posterior_measure must have 'expectation' and 'covariance' attributes"
@@ -36,7 +36,7 @@ class JacobiPreconditioningMethod(LinearSolver):
36
36
  method: str = "variable",
37
37
  rtol: float = 1e-2,
38
38
  block_size: int = 10,
39
- parallel: bool = True,
39
+ parallel: bool = False,
40
40
  n_jobs: int = -1,
41
41
  ) -> None:
42
42
  # Damping is removed: the operator passed to __call__ is already damped