CUQIpy 1.3.0.post0.dev395__tar.gz → 1.4.0.post0.dev13__tar.gz

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.

Potentially problematic release.


This version of CUQIpy might be problematic. Click here for more details.

Files changed (126) hide show
  1. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/CUQIpy.egg-info/PKG-INFO +1 -1
  2. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/PKG-INFO +1 -1
  3. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/_version.py +3 -3
  4. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/density/_density.py +9 -1
  5. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_joint_distribution.py +96 -11
  6. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_sampler.py +12 -4
  7. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_joint_distribution.py +173 -1
  8. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/CUQIpy.egg-info/SOURCES.txt +0 -0
  9. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/CUQIpy.egg-info/dependency_links.txt +0 -0
  10. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/CUQIpy.egg-info/requires.txt +0 -0
  11. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/CUQIpy.egg-info/top_level.txt +0 -0
  12. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/LICENSE +0 -0
  13. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/README.md +0 -0
  14. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/__init__.py +0 -0
  15. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/_messages.py +0 -0
  16. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/array/__init__.py +0 -0
  17. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/array/_array.py +0 -0
  18. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/config.py +0 -0
  19. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/data/__init__.py +0 -0
  20. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/data/_data.py +0 -0
  21. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/data/astronaut.npz +0 -0
  22. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/data/camera.npz +0 -0
  23. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/data/cat.npz +0 -0
  24. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/data/cookie.png +0 -0
  25. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/data/satellite.mat +0 -0
  26. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/density/__init__.py +0 -0
  27. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/diagnostics.py +0 -0
  28. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/__init__.py +0 -0
  29. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_beta.py +0 -0
  30. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_cauchy.py +0 -0
  31. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_cmrf.py +0 -0
  32. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_custom.py +0 -0
  33. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_distribution.py +0 -0
  34. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_gamma.py +0 -0
  35. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_gaussian.py +0 -0
  36. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_gmrf.py +0 -0
  37. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_inverse_gamma.py +0 -0
  38. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_laplace.py +0 -0
  39. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_lmrf.py +0 -0
  40. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_lognormal.py +0 -0
  41. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_modifiedhalfnormal.py +0 -0
  42. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_normal.py +0 -0
  43. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_posterior.py +0 -0
  44. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_smoothed_laplace.py +0 -0
  45. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_truncated_normal.py +0 -0
  46. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/distribution/_uniform.py +0 -0
  47. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/__init__.py +0 -0
  48. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/_recommender.py +0 -0
  49. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/algebra/__init__.py +0 -0
  50. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/algebra/_ast.py +0 -0
  51. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/algebra/_orderedset.py +0 -0
  52. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/algebra/_randomvariable.py +0 -0
  53. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/geometry/__init__.py +0 -0
  54. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/geometry/_productgeometry.py +0 -0
  55. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/__init__.py +0 -0
  56. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_conjugate.py +0 -0
  57. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_conjugate_approx.py +0 -0
  58. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_cwmh.py +0 -0
  59. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_direct.py +0 -0
  60. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_gibbs.py +0 -0
  61. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_hmc.py +0 -0
  62. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_langevin_algorithm.py +0 -0
  63. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_laplace_approximation.py +0 -0
  64. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_mh.py +0 -0
  65. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_pcn.py +0 -0
  66. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/experimental/mcmc/_rto.py +0 -0
  67. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/geometry/__init__.py +0 -0
  68. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/geometry/_geometry.py +0 -0
  69. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/implicitprior/__init__.py +0 -0
  70. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/implicitprior/_regularizedGMRF.py +0 -0
  71. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/implicitprior/_regularizedGaussian.py +0 -0
  72. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/implicitprior/_regularizedUnboundedUniform.py +0 -0
  73. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/implicitprior/_restorator.py +0 -0
  74. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/likelihood/__init__.py +0 -0
  75. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/likelihood/_likelihood.py +0 -0
  76. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/model/__init__.py +0 -0
  77. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/model/_model.py +0 -0
  78. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/operator/__init__.py +0 -0
  79. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/operator/_operator.py +0 -0
  80. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/pde/__init__.py +0 -0
  81. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/pde/_pde.py +0 -0
  82. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/problem/__init__.py +0 -0
  83. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/problem/_problem.py +0 -0
  84. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/__init__.py +0 -0
  85. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_conjugate.py +0 -0
  86. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_conjugate_approx.py +0 -0
  87. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_cwmh.py +0 -0
  88. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_gibbs.py +0 -0
  89. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_hmc.py +0 -0
  90. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_langevin_algorithm.py +0 -0
  91. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_laplace_approximation.py +0 -0
  92. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_mh.py +0 -0
  93. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_pcn.py +0 -0
  94. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_rto.py +0 -0
  95. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/sampler/_sampler.py +0 -0
  96. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/samples/__init__.py +0 -0
  97. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/samples/_samples.py +0 -0
  98. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/solver/__init__.py +0 -0
  99. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/solver/_solver.py +0 -0
  100. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/testproblem/__init__.py +0 -0
  101. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/testproblem/_testproblem.py +0 -0
  102. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/utilities/__init__.py +0 -0
  103. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/utilities/_get_python_variable_name.py +0 -0
  104. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/cuqi/utilities/_utilities.py +0 -0
  105. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/pyproject.toml +0 -0
  106. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/requirements.txt +0 -0
  107. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/setup.cfg +0 -0
  108. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/setup.py +0 -0
  109. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_MRFs.py +0 -0
  110. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_abstract_distribution_density.py +0 -0
  111. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_bayesian_inversion.py +0 -0
  112. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_density.py +0 -0
  113. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_distribution.py +0 -0
  114. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_distributions_shape.py +0 -0
  115. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_geometry.py +0 -0
  116. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_implicit_priors.py +0 -0
  117. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_likelihood.py +0 -0
  118. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_model.py +0 -0
  119. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_pde.py +0 -0
  120. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_posterior.py +0 -0
  121. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_problem.py +0 -0
  122. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_sampler.py +0 -0
  123. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_samples.py +0 -0
  124. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_solver.py +0 -0
  125. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_testproblem.py +0 -0
  126. {cuqipy-1.3.0.post0.dev395 → cuqipy-1.4.0.post0.dev13}/tests/test_utilities.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CUQIpy
3
- Version: 1.3.0.post0.dev395
3
+ Version: 1.4.0.post0.dev13
4
4
  Summary: Computational Uncertainty Quantification for Inverse problems in Python
5
5
  Maintainer-email: "Nicolai A. B. Riis" <nabr@dtu.dk>, "Jakob S. Jørgensen" <jakj@dtu.dk>, "Amal M. Alghamdi" <amaal@dtu.dk>, Chao Zhang <chaz@dtu.dk>
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CUQIpy
3
- Version: 1.3.0.post0.dev395
3
+ Version: 1.4.0.post0.dev13
4
4
  Summary: Computational Uncertainty Quantification for Inverse problems in Python
5
5
  Maintainer-email: "Nicolai A. B. Riis" <nabr@dtu.dk>, "Jakob S. Jørgensen" <jakj@dtu.dk>, "Amal M. Alghamdi" <amaal@dtu.dk>, Chao Zhang <chaz@dtu.dk>
6
6
  License: Apache License
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-09-19T16:37:46+0300",
11
+ "date": "2025-10-09T01:04:51+0300",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "2cf72ec9af9af17dad4bb3870ee20d303376de24",
15
- "version": "1.3.0.post0.dev395"
14
+ "full-revisionid": "3b38e05b811faaaa6eb273ee4a1d03438734ddd6",
15
+ "version": "1.4.0.post0.dev13"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -143,7 +143,15 @@ class Density(ABC):
143
143
  def enable_FD(self, epsilon=1e-8):
144
144
  """ Enable finite difference approximation for logd gradient. Note
145
145
  that if enabled, the FD approximation will be used even if the
146
- _gradient method is implemented. """
146
+ _gradient method is implemented.
147
+
148
+ Parameters
149
+ ----------
150
+ epsilon : float
151
+
152
+ Spacing (step size) to use for finite difference approximation for logd
153
+ gradient for each variable. Default is 1e-8.
154
+ """
147
155
  self._FD_enabled = True
148
156
  self._FD_epsilon = epsilon
149
157
 
@@ -84,6 +84,8 @@ class JointDistribution:
84
84
  cond_vars = self._get_conditioning_variables()
85
85
  if len(cond_vars) > 0:
86
86
  raise ValueError(f"Every density parameter must have a distribution (prior). Missing prior for {cond_vars}.")
87
+ # Initialize finite difference gradient approximation settings
88
+ self.disable_FD()
87
89
 
88
90
  # --------- Public properties ---------
89
91
  @property
@@ -96,6 +98,38 @@ class JointDistribution:
96
98
  """ Returns the geometries of the joint distribution. """
97
99
  return [dist.geometry for dist in self._distributions]
98
100
 
101
+ @property
102
+ def FD_enabled(self):
103
+ """ Returns a dictionary of keys and booleans indicating for each
104
+ parameter name (key) if finite difference approximation of the logd
105
+ gradient is enabled. """
106
+ par_names = self.get_parameter_names()
107
+ FD_enabled = {
108
+ par_name: self.FD_epsilon[par_name] is not None for par_name in par_names
109
+ }
110
+ return FD_enabled
111
+
112
+ @property
113
+ def FD_epsilon(self):
114
+ """ Returns a dictionary indicating for each parameter name the
115
+ spacing for the finite difference approximation of the logd gradient."""
116
+ return self._FD_epsilon
117
+
118
+ @FD_epsilon.setter
119
+ def FD_epsilon(self, value):
120
+ """ Set the spacing for the finite difference approximation of the
121
+ logd gradient as a dictionary. The keys are the parameter names.
122
+ The value for each key is either None (no FD approximation) or a float
123
+ representing the FD step size.
124
+ """
125
+ par_names = self.get_parameter_names()
126
+ if value is None:
127
+ self._FD_epsilon = {par_name: None for par_name in par_names}
128
+ else:
129
+ if set(value.keys()) != set(par_names):
130
+ raise ValueError("Keys of FD_epsilon must match the parameter names of the distribution "+f" {par_names}")
131
+ self._FD_epsilon = value
132
+
99
133
  # --------- Public methods ---------
100
134
  def logd(self, *args, **kwargs):
101
135
  """ Evaluate the un-normalized log density function. """
@@ -136,6 +170,33 @@ class JointDistribution:
136
170
  # Can reduce to Posterior, Likelihood or Distribution.
137
171
  return new_joint._reduce_to_single_density()
138
172
 
173
+ def enable_FD(self, epsilon=None):
174
+ """ Enable finite difference approximation for logd gradient. Note
175
+ that if enabled, the FD approximation will be used even if the
176
+ _gradient method is implemented. By default, all parameters
177
+ will have FD enabled with a step size of 1e-8.
178
+
179
+ Parameters
180
+ ----------
181
+ epsilon : dict, *optional*
182
+
183
+ Dictionary indicating the spacing (step size) to use for finite
184
+ difference approximation for logd gradient for each variable.
185
+
186
+ Keys are variable names.
187
+ Values are either a float to enable FD with the given value as the FD
188
+ step size, or None to disable FD for that variable. Default is 1e-8 for
189
+ all variables.
190
+ """
191
+ if epsilon is None:
192
+ epsilon = {par_name: 1e-8 for par_name in self.get_parameter_names()}
193
+ self.FD_epsilon = epsilon
194
+
195
+ def disable_FD(self):
196
+ """ Disable finite difference approximation for logd gradient. """
197
+ par_names = self.get_parameter_names()
198
+ self.FD_epsilon = {par_name: None for par_name in par_names}
199
+
139
200
  def get_parameter_names(self) -> List[str]:
140
201
  """ Returns the parameter names of the joint distribution. """
141
202
  return [dist.name for dist in self._distributions]
@@ -202,34 +263,58 @@ class JointDistribution:
202
263
  # Count number of distributions and likelihoods
203
264
  n_dist = len(self._distributions)
204
265
  n_likelihood = len(self._likelihoods)
266
+ reduced_FD_epsilon = {par_name:self.FD_epsilon[par_name] for par_name in self.get_parameter_names()}
267
+ self.enable_FD(epsilon=reduced_FD_epsilon)
205
268
 
206
269
  # Cant reduce if there are multiple distributions or likelihoods
207
270
  if n_dist > 1:
208
271
  return self
209
272
 
273
+ # If only evaluated densities left return joint to ensure logd method is available
274
+ if n_dist == 0 and n_likelihood == 0:
275
+ return self
276
+
277
+ # Extract the parameter name of the distribution
278
+ if n_dist == 1:
279
+ par_name = self._distributions[0].name
280
+ elif n_likelihood == 1:
281
+ par_name = self._likelihoods[0].name
282
+ else:
283
+ par_name = None
284
+
210
285
  # If exactly one distribution and multiple likelihoods reduce
211
286
  if n_dist == 1 and n_likelihood > 1:
212
- return MultipleLikelihoodPosterior(*self._densities)
213
-
287
+ reduced_distribution = MultipleLikelihoodPosterior(*self._densities)
288
+ reduced_FD_epsilon = {par_name:self.FD_epsilon[par_name]}
289
+
214
290
  # If exactly one distribution and one likelihood its a Posterior
215
291
  if n_dist == 1 and n_likelihood == 1:
216
292
  # Ensure parameter names match, otherwise return the joint distribution
217
293
  if set(self._likelihoods[0].get_parameter_names()) != set(self._distributions[0].get_parameter_names()):
218
294
  return self
219
- return self._add_constants_to_density(Posterior(self._likelihoods[0], self._distributions[0]))
295
+ reduced_distribution = Posterior(self._likelihoods[0], self._distributions[0])
296
+ reduced_distribution = self._add_constants_to_density(reduced_distribution)
297
+ reduced_FD_epsilon = self.FD_epsilon[par_name]
220
298
 
221
299
  # If exactly one distribution and no likelihoods its a Distribution
222
300
  if n_dist == 1 and n_likelihood == 0:
223
- return self._add_constants_to_density(self._distributions[0])
224
-
301
+ # Intentionally skip enabling FD here. If the user wants FD, they
302
+ # can enable it for this particular distribution before forming
303
+ # the joint distribution.
304
+ return self._add_constants_to_density(self._distributions[0])
305
+
225
306
  # If no distributions and exactly one likelihood its a Likelihood
226
307
  if n_likelihood == 1 and n_dist == 0:
227
- return self._likelihoods[0]
308
+ # This case seems to not happen in practice, but we include it for
309
+ # completeness.
310
+ reduced_distribution = self._likelihoods[0]
311
+ reduced_FD_epsilon = self.FD_epsilon[par_name]
312
+
313
+ if self.FD_enabled[par_name]:
314
+ reduced_distribution.enable_FD(epsilon=reduced_FD_epsilon)
315
+
316
+ return reduced_distribution
228
317
 
229
- # If only evaluated densities left return joint to ensure logd method is available
230
- if n_dist == 0 and n_likelihood == 0:
231
- return self
232
-
233
318
  def _add_constants_to_density(self, density: Density):
234
319
  """ Add the constants (evaluated densities) to a single density. Used when reducing to single density. """
235
320
 
@@ -274,7 +359,7 @@ class JointDistribution:
274
359
  if len(cond_vars) > 0:
275
360
  msg += f"|{cond_vars}"
276
361
  msg += ")"
277
-
362
+
278
363
  msg += "\n"
279
364
  msg += " Densities: \n"
280
365
 
@@ -203,13 +203,16 @@ class Sampler(ABC):
203
203
 
204
204
  self.set_state(state)
205
205
 
206
- def sample(self, Ns, batch_size=0, sample_path='./CUQI_samples/') -> 'Sampler':
206
+ def sample(self, Ns, Nt=1, batch_size=0, sample_path='./CUQI_samples/') -> 'Sampler':
207
207
  """ Sample Ns samples from the target density.
208
208
 
209
209
  Parameters
210
210
  ----------
211
211
  Ns : int
212
212
  The number of samples to draw.
213
+
214
+ Nt : int, optional, default=1
215
+ The thinning interval. If Nt >= 1, every Nt'th sample is stored. The larger Nt, the fewer samples are stored.
213
216
 
214
217
  batch_size : int, optional
215
218
  The batch size for saving samples to disk. If 0, no batching is used. If positive, samples are saved to disk in batches of the specified size.
@@ -233,7 +236,8 @@ class Sampler(ABC):
233
236
 
234
237
  # Store samples
235
238
  self._acc.append(acc)
236
- self._samples.append(self.current_point)
239
+ if (Nt > 0) and ((idx + 1) % Nt == 0):
240
+ self._samples.append(self.current_point)
237
241
 
238
242
  # display acc rate at progress bar
239
243
  pbar.set_postfix_str(f"acc rate: {np.mean(self._acc[-1-idx:]):.2%}")
@@ -248,7 +252,7 @@ class Sampler(ABC):
248
252
  return self
249
253
 
250
254
 
251
- def warmup(self, Nb, tune_freq=0.1) -> 'Sampler':
255
+ def warmup(self, Nb, Nt=1, tune_freq=0.1) -> 'Sampler':
252
256
  """ Warmup the sampler by drawing Nb samples.
253
257
 
254
258
  Parameters
@@ -256,6 +260,9 @@ class Sampler(ABC):
256
260
  Nb : int
257
261
  The number of samples to draw during warmup.
258
262
 
263
+ Nt : int, optional, default=1
264
+ The thinning interval. If Nt >= 1, every Nt'th sample is stored. The larger Nt, the fewer samples are stored.
265
+
259
266
  tune_freq : float, optional
260
267
  The frequency of tuning. Tuning is performed every tune_freq*Nb samples.
261
268
 
@@ -278,7 +285,8 @@ class Sampler(ABC):
278
285
 
279
286
  # Store samples
280
287
  self._acc.append(acc)
281
- self._samples.append(self.current_point)
288
+ if (Nt > 0) and ((idx + 1) % Nt == 0):
289
+ self._samples.append(self.current_point)
282
290
 
283
291
  # display acc rate at progress bar
284
292
  pbar.set_postfix_str(f"acc rate: {np.mean(self._acc[-1-idx:]):.2%}")
@@ -484,4 +484,176 @@ def test_joint_distribution_with_multiple_inputs_model_has_correct_parameter_nam
484
484
 
485
485
  assert joint_dist(x_dist=x_val, y_dist=y_val, data_dist=np.array([2,2,3])).likelihood.get_parameter_names() == ['z_dist']
486
486
  assert joint_dist(x_dist=x_val, z_dist=z_val, data_dist=np.array([2,2,3])).likelihood.get_parameter_names() == ['y_dist']
487
- assert joint_dist(y_dist=y_val, z_dist=z_val, data_dist=np.array([2,2,3])).likelihood.get_parameter_names() == ['x_dist']
487
+ assert joint_dist(y_dist=y_val, z_dist=z_val, data_dist=np.array([2,2,3])).likelihood.get_parameter_names() == ['x_dist']
488
+
489
+
490
+ def test_FD_enabled_is_set_correctly():
491
+ """ Test that FD_enabled property is set correctly in JointDistribution """
492
+
493
+ # Create a joint distribution with two distributions
494
+ d1 = cuqi.distribution.Normal(0, 1, name="x")
495
+ d2 = cuqi.distribution.Gamma(lambda x: x**2, 1, name="y")
496
+ J = cuqi.distribution.JointDistribution(d1, d2)
497
+
498
+ # Initially FD should be disabled for both
499
+ assert J.FD_enabled == {"x": False, "y": False}
500
+
501
+ # Enable FD for x
502
+ J.enable_FD(epsilon={"x": 1e-6, "y": None})
503
+ assert J.FD_enabled == {"x": True, "y": False}
504
+ assert J.FD_epsilon == {"x": 1e-6, "y": None}
505
+
506
+ # Enable FD for y as well
507
+ J.enable_FD(epsilon={"x": 1e-6, "y": 1e-5})
508
+ assert J.FD_enabled == {"x": True, "y": True}
509
+ assert J.FD_epsilon == {"x": 1e-6, "y": 1e-5}
510
+
511
+ # Disable FD for x
512
+ J.enable_FD(epsilon={"x": None, "y": 1e-5})
513
+ assert J.FD_enabled == {"x": False, "y": True}
514
+ assert J.FD_epsilon == {"x": None, "y": 1e-5}
515
+
516
+ # Disable FD for all
517
+ J.disable_FD()
518
+ assert J.FD_enabled == {"x": False, "y": False}
519
+ assert J.FD_epsilon == {"x": None, "y": None}
520
+
521
+ # Enable FD and reduce to single density
522
+ J.enable_FD() # Enable FD for all
523
+ J_given_x = J(x=0)
524
+ J_given_y = J(y=1)
525
+
526
+ # Check types and FD_enabled status of J_given_x
527
+ assert isinstance(J_given_x, cuqi.distribution.Gamma)
528
+ assert not J_given_x.FD_enabled # intentionally disabled for single remaining
529
+ # distribution
530
+ assert J_given_x.FD_epsilon == None
531
+
532
+ # Check types and FD_enabled status of J_given_y
533
+ assert isinstance(J_given_y, cuqi.distribution.Posterior)
534
+ assert J_given_y.FD_enabled
535
+ assert J_given_y.FD_epsilon == 1e-8 # Default epsilon for remaining density
536
+
537
+ # Catch error if epsilon keys do not match parameter names
538
+ with pytest.raises(ValueError, match=r"Keys of FD_epsilon must match"):
539
+ J.enable_FD(epsilon={"x": 1e-6}) # Missing "y" key
540
+
541
+ def test_FD_enabled_is_set_correctly_for_stacked_joint_distribution():
542
+ """ Test that FD_enabled property is set correctly in JointDistribution """
543
+
544
+ # Create a joint distribution with two distributions
545
+ x = cuqi.distribution.Normal(0, 1, name="x")
546
+ y = cuqi.distribution.Uniform(1, 2, name="y")
547
+ J = cuqi.distribution._StackedJointDistribution(x, y)
548
+ J.enable_FD(epsilon={"x": 1e-6, "y": None})
549
+
550
+ assert J.FD_enabled == {"x": True, "y": False}
551
+ assert J.FD_epsilon == {"x": 1e-6, "y": None}
552
+
553
+ # Reduce to single density (substitute y)
554
+ J_given_y = J(y=1.5)
555
+ assert isinstance(J_given_y, cuqi.distribution.Normal)
556
+ assert J_given_y.FD_enabled == False # Intentionally disabled for
557
+ # single remaining
558
+ # distribution
559
+ assert J_given_y.FD_epsilon is None
560
+
561
+ # Reduce to single density (substitute x)
562
+ J_given_x = J(x=0)
563
+ assert isinstance(J_given_x, cuqi.distribution.Uniform)
564
+ assert J_given_x.FD_enabled == False
565
+ assert J_given_x.FD_epsilon is None
566
+
567
+
568
+
569
+ @pytest.mark.parametrize(
570
+ "densities,kwargs,fd_epsilon,expected_type,expected_fd_enabled",
571
+ [
572
+ # Case 0: Single Distribution, FD enabled
573
+ (
574
+ [cuqi.distribution.Normal(np.zeros(3), 1, name="x")],
575
+ {},
576
+ {"x": 1e-5},
577
+ cuqi.distribution.Normal,
578
+ False, # Intentionally disabled for single remaining distribution
579
+ ),
580
+ # Case 1: Single Distribution, FD disabled
581
+ (
582
+ [cuqi.distribution.Normal(np.zeros(3), 1, name="x")],
583
+ {},
584
+ {"x": None},
585
+ cuqi.distribution.Normal,
586
+ False,
587
+ ),
588
+ # Case 2: Distribution + Data distribution, substitute y
589
+ (
590
+ [
591
+ cuqi.distribution.Normal(np.zeros(3), 1, name="x"),
592
+ cuqi.distribution.Gaussian(lambda x: x**2, np.ones(3), name="y"),
593
+ ],
594
+ {"y": np.ones(3)},
595
+ {"x": 1e-6, "y": 1e-7},
596
+ cuqi.distribution.Posterior,
597
+ True,
598
+ ),
599
+ # Case 3: Distribution + data distribution, substitute x
600
+ (
601
+ [
602
+ cuqi.distribution.Normal(np.zeros(3), 1, name="x"),
603
+ cuqi.distribution.Gaussian(lambda x: x**2, np.ones(3), name="y"),
604
+ ],
605
+ {"x": np.ones(3)},
606
+ {"x": 1e-5, "y": 1e-6},
607
+ cuqi.distribution.Distribution,
608
+ False, # Intentionally disabled for single remaining distribution
609
+ ),
610
+ # Case 4: Multiple data distributions + prior (MultipleLikelihoodPosterior)
611
+ (
612
+ [
613
+ cuqi.distribution.Normal(np.zeros(3), 1, name="x"),
614
+ cuqi.distribution.Gaussian(lambda x: x, np.ones(3), name="y1"),
615
+ cuqi.distribution.Gaussian(lambda x: x + 1, np.ones(3), name="y2"),
616
+ ],
617
+ {"y1": np.ones(3), "y2": np.ones(3)},
618
+ {"x": 1e-5, "y1": 1e-6, "y2": 1e-7},
619
+ cuqi.distribution.MultipleLikelihoodPosterior,
620
+ {"x": True},
621
+ ),
622
+ # Case 5: Distribution, substitute x
623
+ (
624
+ [cuqi.distribution.Normal(np.zeros(3), 1, name="x")],
625
+ {"x": np.ones(3)},
626
+ {"x": 1e-8},
627
+ cuqi.distribution.JointDistribution,
628
+ {},
629
+ ),
630
+ ],
631
+ )
632
+ def test_fd_enabled_of_joint_distribution_after_substitution_is_correct(
633
+ densities, kwargs, fd_epsilon, expected_type, expected_fd_enabled
634
+ ):
635
+ """ Test that FD_enabled and FD_epsilon properties are set correctly in JointDistribution even after substitution."""
636
+ joint = cuqi.distribution.JointDistribution(*densities)
637
+ joint.enable_FD(epsilon=fd_epsilon)
638
+
639
+ # Assert FD_epsilon is set correctly
640
+ assert joint.FD_epsilon == fd_epsilon
641
+
642
+ # Substitute parameters (if any), which reduces the joint distribution
643
+ reduced = joint(**kwargs)
644
+
645
+ # Assert the type and FD_enabled status of the reduced distribution
646
+ assert isinstance(reduced, expected_type)
647
+ assert reduced.FD_enabled == expected_fd_enabled
648
+
649
+ # Assert FD_epsilon is set correctly in the reduced distribution
650
+ if expected_fd_enabled is not False:
651
+ fd_epsilon_reduced = {
652
+ k: v for k, v in fd_epsilon.items() if k not in kwargs.keys()
653
+ }
654
+ if len(fd_epsilon_reduced) == 1 and not isinstance(
655
+ reduced, cuqi.distribution.MultipleLikelihoodPosterior
656
+ ):
657
+ # Single value instead of dict in this case
658
+ fd_epsilon_reduced = list(fd_epsilon_reduced.values())[0]
659
+ assert reduced.FD_epsilon == fd_epsilon_reduced