CUQIpy 1.3.0__py3-none-any.whl → 1.4.0.post0.dev61__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.
Files changed (72) hide show
  1. cuqi/__init__.py +1 -0
  2. cuqi/_version.py +3 -3
  3. cuqi/density/_density.py +9 -1
  4. cuqi/distribution/__init__.py +1 -1
  5. cuqi/distribution/_beta.py +1 -1
  6. cuqi/distribution/_cauchy.py +2 -2
  7. cuqi/distribution/_distribution.py +24 -15
  8. cuqi/distribution/_joint_distribution.py +97 -12
  9. cuqi/distribution/_posterior.py +9 -0
  10. cuqi/distribution/_truncated_normal.py +3 -3
  11. cuqi/distribution/_uniform.py +36 -2
  12. cuqi/experimental/__init__.py +1 -1
  13. cuqi/experimental/_recommender.py +216 -0
  14. cuqi/experimental/geometry/_productgeometry.py +3 -3
  15. cuqi/geometry/_geometry.py +12 -1
  16. cuqi/implicitprior/__init__.py +1 -1
  17. cuqi/implicitprior/_regularizedGaussian.py +40 -4
  18. cuqi/implicitprior/_restorator.py +35 -1
  19. cuqi/legacy/__init__.py +2 -0
  20. cuqi/legacy/sampler/__init__.py +11 -0
  21. cuqi/legacy/sampler/_conjugate.py +55 -0
  22. cuqi/legacy/sampler/_conjugate_approx.py +52 -0
  23. cuqi/legacy/sampler/_cwmh.py +196 -0
  24. cuqi/legacy/sampler/_gibbs.py +231 -0
  25. cuqi/legacy/sampler/_hmc.py +335 -0
  26. cuqi/legacy/sampler/_langevin_algorithm.py +198 -0
  27. cuqi/legacy/sampler/_laplace_approximation.py +184 -0
  28. cuqi/legacy/sampler/_mh.py +190 -0
  29. cuqi/legacy/sampler/_pcn.py +244 -0
  30. cuqi/{experimental/mcmc → legacy/sampler}/_rto.py +134 -152
  31. cuqi/legacy/sampler/_sampler.py +182 -0
  32. cuqi/likelihood/_likelihood.py +1 -1
  33. cuqi/model/_model.py +1248 -357
  34. cuqi/pde/__init__.py +4 -0
  35. cuqi/pde/_observation_map.py +36 -0
  36. cuqi/pde/_pde.py +133 -32
  37. cuqi/problem/_problem.py +88 -82
  38. cuqi/sampler/__init__.py +120 -8
  39. cuqi/sampler/_conjugate.py +376 -35
  40. cuqi/sampler/_conjugate_approx.py +40 -16
  41. cuqi/sampler/_cwmh.py +132 -138
  42. cuqi/{experimental/mcmc → sampler}/_direct.py +1 -1
  43. cuqi/sampler/_gibbs.py +269 -130
  44. cuqi/sampler/_hmc.py +328 -201
  45. cuqi/sampler/_langevin_algorithm.py +282 -98
  46. cuqi/sampler/_laplace_approximation.py +87 -117
  47. cuqi/sampler/_mh.py +47 -157
  48. cuqi/sampler/_pcn.py +56 -211
  49. cuqi/sampler/_rto.py +206 -140
  50. cuqi/sampler/_sampler.py +540 -135
  51. cuqi/solver/_solver.py +6 -2
  52. cuqi/testproblem/_testproblem.py +2 -3
  53. cuqi/utilities/__init__.py +3 -1
  54. cuqi/utilities/_utilities.py +94 -12
  55. {CUQIpy-1.3.0.dist-info → cuqipy-1.4.0.post0.dev61.dist-info}/METADATA +6 -4
  56. cuqipy-1.4.0.post0.dev61.dist-info/RECORD +102 -0
  57. {CUQIpy-1.3.0.dist-info → cuqipy-1.4.0.post0.dev61.dist-info}/WHEEL +1 -1
  58. CUQIpy-1.3.0.dist-info/RECORD +0 -100
  59. cuqi/experimental/mcmc/__init__.py +0 -123
  60. cuqi/experimental/mcmc/_conjugate.py +0 -345
  61. cuqi/experimental/mcmc/_conjugate_approx.py +0 -76
  62. cuqi/experimental/mcmc/_cwmh.py +0 -193
  63. cuqi/experimental/mcmc/_gibbs.py +0 -318
  64. cuqi/experimental/mcmc/_hmc.py +0 -464
  65. cuqi/experimental/mcmc/_langevin_algorithm.py +0 -392
  66. cuqi/experimental/mcmc/_laplace_approximation.py +0 -156
  67. cuqi/experimental/mcmc/_mh.py +0 -80
  68. cuqi/experimental/mcmc/_pcn.py +0 -89
  69. cuqi/experimental/mcmc/_sampler.py +0 -566
  70. cuqi/experimental/mcmc/_utilities.py +0 -17
  71. {CUQIpy-1.3.0.dist-info → cuqipy-1.4.0.post0.dev61.dist-info/licenses}/LICENSE +0 -0
  72. {CUQIpy-1.3.0.dist-info → cuqipy-1.4.0.post0.dev61.dist-info}/top_level.txt +0 -0
@@ -1,345 +0,0 @@
1
- import numpy as np
2
- from abc import ABC, abstractmethod
3
- import math
4
- from cuqi.experimental.mcmc import Sampler
5
- from cuqi.distribution import Posterior, Gaussian, Gamma, GMRF, ModifiedHalfNormal
6
- from cuqi.implicitprior import RegularizedGaussian, RegularizedGMRF, RegularizedUnboundedUniform
7
- from cuqi.utilities import get_non_default_args, count_nonzero, count_constant_components_1D, count_constant_components_2D
8
- from cuqi.geometry import Continuous1D, Continuous2D, Image2D
9
-
10
- class Conjugate(Sampler):
11
- """ Conjugate sampler
12
-
13
- Sampler for sampling a posterior distribution which is a so-called "conjugate" distribution, i.e., where the likelihood and prior are conjugate to each other - denoted as a conjugate pair.
14
-
15
- Currently supported conjugate pairs are:
16
- - (Gaussian, Gamma) where Gamma is defined on the precision parameter of the Gaussian
17
- - (GMRF, Gamma) where Gamma is defined on the precision parameter of the GMRF
18
- - (RegularizedGaussian, Gamma) with preset constraints only and Gamma is defined on the precision parameter of the RegularizedGaussian
19
- - (RegularizedGMRF, Gamma) with preset constraints only and Gamma is defined on the precision parameter of the RegularizedGMRF
20
- - (RegularizedGaussian, ModifiedHalfNormal) with preset constraints and regularization only
21
- - (RegularizedGMRF, ModifiedHalfNormal) with preset constraints and regularization only
22
-
23
- Currently the Gamma and ModifiedHalfNormal distribution must be univariate.
24
-
25
- A conjugate pair defines implicitly a so-called conjugate distribution which can be sampled from directly.
26
-
27
- The conjugate parameter is the parameter that both the likelihood and prior PDF depend on.
28
-
29
- For more information on conjugacy and conjugate distributions see https://en.wikipedia.org/wiki/Conjugate_prior.
30
-
31
- For implicit regularized Gaussians and the corresponding conjugacy relations, see:
32
-
33
- Section 3.3 from [1] Everink, Jasper M., Yiqiu Dong, and Martin S. Andersen. "Bayesian inference with projected densities." SIAM/ASA Journal on Uncertainty Quantification 11.3 (2023): 1025-1043.
34
- Section 4 from [2] Everink, Jasper M., Yiqiu Dong, and Martin S. Andersen. "Sparse Bayesian inference with regularized Gaussian distributions." Inverse Problems 39.11 (2023): 115004.
35
-
36
- """
37
-
38
- def _initialize(self):
39
- pass
40
-
41
- @Sampler.target.setter # Overwrite the target setter to set the conjugate pair
42
- def target(self, value):
43
- """ Set the target density. Runs validation of the target. """
44
- self._target = value
45
- if self._target is not None:
46
- self._set_conjugatepair()
47
- self.validate_target()
48
-
49
- def validate_target(self):
50
- self._ensure_target_is_posterior()
51
- self._conjugatepair.validate_target()
52
-
53
- def step(self):
54
- self.current_point = self._conjugatepair.sample()
55
- return 1 # Returns acceptance rate of 1
56
-
57
- def tune(self, skip_len, update_count):
58
- pass # No tuning required for conjugate sampler
59
-
60
- def _ensure_target_is_posterior(self):
61
- """ Ensure that the target is a Posterior distribution. """
62
- if not isinstance(self.target, Posterior):
63
- raise TypeError("Conjugate sampler requires a target of type Posterior")
64
-
65
- def _set_conjugatepair(self):
66
- """ Set the conjugate pair based on the likelihood and prior. This requires target to be set. """
67
- self._ensure_target_is_posterior()
68
- if isinstance(self.target.likelihood.distribution, (Gaussian, GMRF)) and isinstance(self.target.prior, Gamma):
69
- self._conjugatepair = _GaussianGammaPair(self.target)
70
- elif isinstance(self.target.likelihood.distribution, RegularizedUnboundedUniform) and isinstance(self.target.prior, Gamma):
71
- # Check RegularizedUnboundedUniform before RegularizedGaussian and RegularizedGMRF due to the first inheriting from the second.
72
- self._conjugatepair = _RegularizedUnboundedUniformGammaPair(self.target)
73
- elif isinstance(self.target.likelihood.distribution, (RegularizedGaussian, RegularizedGMRF)) and isinstance(self.target.prior, Gamma):
74
- self._conjugatepair = _RegularizedGaussianGammaPair(self.target)
75
- elif isinstance(self.target.likelihood.distribution, (RegularizedGaussian, RegularizedGMRF)) and isinstance(self.target.prior, ModifiedHalfNormal):
76
- self._conjugatepair = _RegularizedGaussianModifiedHalfNormalPair(self.target)
77
- else:
78
- raise ValueError(f"Conjugacy is not defined for likelihood {type(self.target.likelihood.distribution)} and prior {type(self.target.prior)}, in CUQIpy")
79
-
80
- def conjugate_distribution(self):
81
- return self._conjugatepair.conjugate_distribution()
82
-
83
- def __repr__(self):
84
- msg = super().__repr__()
85
- if hasattr(self, "_conjugatepair"):
86
- msg += f"\n Conjugate pair:\n\t {type(self._conjugatepair).__name__.removeprefix('_')}"
87
- return msg
88
-
89
- class _ConjugatePair(ABC):
90
- """ Abstract base class for conjugate pairs (likelihood, prior) used in the Conjugate sampler. """
91
-
92
- def __init__(self, target):
93
- self.target = target
94
-
95
- @abstractmethod
96
- def validate_target(self):
97
- """ Validate the target distribution for the conjugate pair. """
98
- pass
99
-
100
- @abstractmethod
101
- def conjugate_distribution(self):
102
- """ Returns the posterior distribution in the form of a CUQIpy distribution """
103
- pass
104
-
105
- def sample(self):
106
- """ Sample from the conjugate distribution. """
107
- return self.conjugate_distribution().sample()
108
-
109
-
110
- class _GaussianGammaPair(_ConjugatePair):
111
- """ Implementation for the Gaussian-Gamma conjugate pair."""
112
-
113
- def validate_target(self):
114
- if self.target.prior.dim != 1:
115
- raise ValueError("Gaussian-Gamma conjugacy only works with univariate Gamma prior")
116
-
117
- key_value_pairs = _get_conjugate_parameter(self.target)
118
- if len(key_value_pairs) != 1:
119
- raise ValueError(f"Multiple references to conjugate parameter {self.target.prior.name} found in likelihood. Only one occurance is supported.")
120
- for key, value in key_value_pairs:
121
- if key == "cov":
122
- if not _check_conjugate_parameter_is_scalar_linear_reciprocal(value):
123
- raise ValueError("Gaussian-Gamma conjugate pair defined via covariance requires cov: lambda x : s/x for the conjugate parameter")
124
- elif key == "prec":
125
- if not _check_conjugate_parameter_is_scalar_linear(value):
126
- raise ValueError("Gaussian-Gamma conjugate pair defined via precision requires prec: lambda x : s*x for the conjugate parameter")
127
- else:
128
- raise ValueError(f"RegularizedGaussian-ModifiedHalfNormal conjugacy does not support the conjugate parameter {self.target.prior.name} in the {key} attribute. Only cov and prec")
129
-
130
- def conjugate_distribution(self):
131
- # Extract variables
132
- b = self.target.likelihood.data # mu
133
- m = len(b) # n
134
- Ax = self.target.likelihood.distribution.mean # x_i
135
- L = self.target.likelihood.distribution(np.array([1])).sqrtprec # L
136
- alpha = self.target.prior.shape # alpha
137
- beta = self.target.prior.rate # beta
138
-
139
- # Create Gamma distribution and sample
140
- return Gamma(shape=m/2 + alpha, rate=.5 * np.linalg.norm(L @ (Ax - b))**2 + beta)
141
-
142
-
143
- class _RegularizedGaussianGammaPair(_ConjugatePair):
144
- """Implementation for the Regularized Gaussian-Gamma conjugate pair using the conjugacy rules from [1], Section 3.3."""
145
-
146
- def validate_target(self):
147
- if self.target.prior.dim != 1:
148
- raise ValueError("RegularizedGaussian-Gamma conjugacy only works with univariate ModifiedHalfNormal prior")
149
-
150
- if self.target.likelihood.distribution.preset["constraint"] not in ["nonnegativity"]:
151
- raise ValueError("RegularizedGaussian-Gamma conjugacy only works with implicit regularized Gaussian likelihood with nonnegativity constraints")
152
-
153
- key_value_pairs = _get_conjugate_parameter(self.target)
154
- if len(key_value_pairs) != 1:
155
- raise ValueError(f"Multiple references to conjugate parameter {self.target.prior.name} found in likelihood. Only one occurance is supported.")
156
- for key, value in key_value_pairs:
157
- if key == "cov":
158
- if not _check_conjugate_parameter_is_scalar_linear_reciprocal(value):
159
- raise ValueError("Regularized Gaussian-Gamma conjugacy defined via covariance requires cov: lambda x : s/x for the conjugate parameter")
160
- elif key == "prec":
161
- if not _check_conjugate_parameter_is_scalar_linear(value):
162
- raise ValueError("Regularized Gaussian-Gamma conjugacy defined via precision requires prec: lambda x : s*x for the conjugate parameter")
163
- else:
164
- raise ValueError(f"RegularizedGaussian-ModifiedHalfNormal conjugacy does not support the conjugate parameter {self.target.prior.name} in the {key} attribute. Only cov and prec")
165
-
166
- def conjugate_distribution(self):
167
- # Extract variables
168
- b = self.target.likelihood.data # mu
169
- m = np.count_nonzero(b) # n
170
- Ax = self.target.likelihood.distribution.mean # x_i
171
- L = self.target.likelihood.distribution(np.array([1])).sqrtprec # L
172
- alpha = self.target.prior.shape # alpha
173
- beta = self.target.prior.rate # beta
174
-
175
- # Create Gamma distribution and sample
176
- return Gamma(shape=m/2 + alpha, rate=.5 * np.linalg.norm(L @ (Ax - b))**2 + beta)
177
-
178
-
179
- class _RegularizedUnboundedUniformGammaPair(_ConjugatePair):
180
- """Implementation for the RegularizedUnboundedUniform-ModifiedHalfNormal conjugate pair using the conjugacy rules from [2], Section 4."""
181
-
182
- def validate_target(self):
183
- if self.target.prior.dim != 1:
184
- raise ValueError("RegularizedUnboundedUniform-Gamma conjugacy only works with univariate Gamma prior")
185
-
186
- if self.target.likelihood.distribution.preset["regularization"] not in ["l1", "tv"]:
187
- raise ValueError("RegularizedUnboundedUniform-Gamma conjugacy only works with implicit regularized Gaussian likelihood with l1 or tv regularization")
188
-
189
- key_value_pairs = _get_conjugate_parameter(self.target)
190
- if len(key_value_pairs) != 1:
191
- raise ValueError(f"Multiple references to conjugate parameter {self.target.prior.name} found in likelihood. Only one occurance is supported.")
192
- for key, value in key_value_pairs:
193
- if key == "strength":
194
- if not _check_conjugate_parameter_is_scalar_linear(value):
195
- raise ValueError("RegularizedUnboundedUniform-Gamma conjugacy defined via strength requires strength: lambda x : s*x for the conjugate parameter")
196
- else:
197
- raise ValueError(f"RegularizedUnboundedUniform-Gamma conjugacy does not support the conjugate parameter {self.target.prior.name} in the {key} attribute. Only strength is supported")
198
-
199
- def conjugate_distribution(self):
200
- # Extract prior variables
201
- alpha = self.target.prior.shape
202
- beta = self.target.prior.rate
203
-
204
- # Compute likelihood quantities
205
- x = self.target.likelihood.data
206
- m = _compute_sparsity_level(self.target)
207
-
208
- reg_op = self.target.likelihood.distribution._regularization_oper
209
- reg_strength = self.target.likelihood.distribution(np.array([1])).strength
210
- fx = reg_strength*np.linalg.norm(reg_op@x, ord = 1)
211
-
212
- # Create Gamma distribution
213
- return Gamma(shape=m/2 + alpha, rate=fx + beta)
214
-
215
- class _RegularizedGaussianModifiedHalfNormalPair(_ConjugatePair):
216
- """Implementation for the Regularized Gaussian-ModifiedHalfNormal conjugate pair using the conjugacy rules from [2], Section 4."""
217
-
218
- def validate_target(self):
219
- if self.target.prior.dim != 1:
220
- raise ValueError("RegularizedGaussian-ModifiedHalfNormal conjugacy only works with univariate ModifiedHalfNormal prior")
221
-
222
- if self.target.likelihood.distribution.preset["regularization"] not in ["l1", "tv"]:
223
- raise ValueError("RegularizedGaussian-ModifiedHalfNormal conjugacy only works with implicit regularized Gaussian likelihood with l1 or tv regularization")
224
-
225
- key_value_pairs = _get_conjugate_parameter(self.target)
226
- if len(key_value_pairs) != 2:
227
- raise ValueError(f"Incorrect number of references to conjugate parameter {self.target.prior.name} found in likelihood. Found {len(key_value_pairs)} times, but needs to occur in prec or cov, and in strength")
228
- for key, value in key_value_pairs:
229
- if key == "strength":
230
- if not _check_conjugate_parameter_is_scalar_linear(value):
231
- raise ValueError("RegularizedGaussian-ModifiedHalfNormal conjugacy defined via strength requires strength: lambda x : s*x for the conjugate parameter")
232
- elif key == "prec":
233
- if not _check_conjugate_parameter_is_scalar_quadratic(value):
234
- raise ValueError("RegularizedGaussian-ModifiedHalfNormal conjugacy defined via precision requires prec: lambda x : s*x for the conjugate parameter")
235
- elif key == "cov":
236
- if not _check_conjugate_parameter_is_scalar_quadratic_reciprocal(value):
237
- raise ValueError("RegularizedGaussian-ModifiedHalfNormal conjugacy defined via covariance requires cov: lambda x : s/x for the conjugate parameter")
238
- else:
239
- raise ValueError(f"RegularizedGaussian-ModifiedHalfNormal conjugacy does not support the conjugate parameter {self.target.prior.name} in the {key} attribute. Only cov, prec and strength are supported")
240
-
241
-
242
- def conjugate_distribution(self):
243
- # Extract prior variables
244
- alpha = self.target.prior.alpha
245
- beta = self.target.prior.beta
246
- gamma = self.target.prior.gamma
247
-
248
- # Compute likelihood variables
249
- x = self.target.likelihood.data
250
- mu = self.target.likelihood.distribution.mean
251
- L = self.target.likelihood.distribution(np.array([1])).sqrtprec
252
-
253
- m = _compute_sparsity_level(self.target)
254
-
255
- reg_op = self.target.likelihood.distribution._regularization_oper
256
- reg_strength = self.target.likelihood.distribution(np.array([1])).strength
257
- fx = reg_strength*np.linalg.norm(reg_op@x, ord = 1)
258
-
259
- # Compute parameters of conjugate distribution
260
- conj_alpha = m + alpha
261
- conj_beta = 0.5*np.linalg.norm(L @ (mu - x))**2 + beta
262
- conj_gamma = -fx + gamma
263
-
264
- # Create conjugate distribution
265
- return ModifiedHalfNormal(conj_alpha, conj_beta, conj_gamma)
266
-
267
-
268
- def _compute_sparsity_level(target):
269
- """Computes the sparsity level in accordance with Section 4 from [2],"""
270
- x = target.likelihood.data
271
- if target.likelihood.distribution.preset["constraint"] == "nonnegativity":
272
- if target.likelihood.distribution.preset["regularization"] == "l1":
273
- m = count_nonzero(x)
274
- elif target.likelihood.distribution.preset["regularization"] == "tv" and isinstance(target.likelihood.distribution.geometry, Continuous1D):
275
- m = count_constant_components_1D(x, lower = 0.0)
276
- elif target.likelihood.distribution.preset["regularization"] == "tv" and isinstance(target.likelihood.distribution.geometry, (Continuous2D, Image2D)):
277
- m = count_constant_components_2D(target.likelihood.distribution.geometry.par2fun(x), lower = 0.0)
278
- else: # No constraints, only regularization
279
- if target.likelihood.distribution.preset["regularization"] == "l1":
280
- m = count_nonzero(x)
281
- elif target.likelihood.distribution.preset["regularization"] == "tv" and isinstance(target.likelihood.distribution.geometry, Continuous1D):
282
- m = count_constant_components_1D(x)
283
- elif target.likelihood.distribution.preset["regularization"] == "tv" and isinstance(target.likelihood.distribution.geometry, (Continuous2D, Image2D)):
284
- m = count_constant_components_2D(target.likelihood.distribution.geometry.par2fun(x))
285
- return m
286
-
287
-
288
- def _get_conjugate_parameter(target):
289
- """Extract the conjugate parameter name (e.g. d), and returns the mutable variable that is defined by the conjugate parameter, e.g. cov and its value e.g. lambda d:1/d"""
290
- par_name = target.prior.name
291
- mutable_likelihood_vars = target.likelihood.distribution.get_mutable_variables()
292
-
293
- found_parameter_pairs = []
294
-
295
- for var_key in mutable_likelihood_vars:
296
- attr = getattr(target.likelihood.distribution, var_key)
297
- if callable(attr) and par_name in get_non_default_args(attr):
298
- found_parameter_pairs.append((var_key, attr))
299
- if len(found_parameter_pairs) == 0:
300
- raise ValueError(f"Unable to find conjugate parameter {par_name} in likelihood function for conjugate sampler with target {target}")
301
- return found_parameter_pairs
302
-
303
- def _check_conjugate_parameter_is_scalar_identity(f):
304
- """Tests whether a function (scalar to scalar) is the identity (lambda x: x)."""
305
- test_values = [1.0, 10.0, 100.0]
306
- return all(np.allclose(f(x), x) for x in test_values)
307
-
308
- def _check_conjugate_parameter_is_scalar_reciprocal(f):
309
- """Tests whether a function (scalar to scalar) is the reciprocal (lambda x : 1.0/x)."""
310
- return all(math.isclose(f(x), 1.0 / x) for x in [1.0, 10.0, 100.0])
311
-
312
- def _check_conjugate_parameter_is_scalar_linear(f):
313
- """
314
- Tests whether a function (scalar to scalar) is linear (lambda x: s*x for some s).
315
- The tests checks whether the function is zero and some finite differences are constant.
316
- """
317
- test_values = [1.0, 10.0, 100.0]
318
- h = 1e-2
319
- finite_diffs = [(f(x + h*x)-f(x))/(h*x) for x in test_values]
320
- return np.isclose(f(0.0), 0.0) and all(np.allclose(c, finite_diffs[0]) for c in finite_diffs[1:])
321
-
322
- def _check_conjugate_parameter_is_scalar_linear_reciprocal(f):
323
- """
324
- Tests whether a function (scalar to scalar) is a constant times the inverse of the input (lambda x: s/x for some s).
325
- The tests checks whether the the reciprocal of the function has constant finite differences.
326
- """
327
- g = lambda x : 1.0/f(x)
328
- test_values = [1.0, 10.0, 100.0]
329
- h = 1e-2
330
- finite_diffs = [(g(x + h*x)-g(x))/(h*x) for x in test_values]
331
- return all(np.allclose(c, finite_diffs[0]) for c in finite_diffs[1:])
332
-
333
- def _check_conjugate_parameter_is_scalar_quadratic(f):
334
- """
335
- Tests whether a function (scalar to scalar) is linear (lambda x: s*x**2 for some s).
336
- The tests checks whether the function divided by the parameter is linear
337
- """
338
- return _check_conjugate_parameter_is_scalar_linear(lambda x: f(x)/x if x != 0.0 else f(0.0))
339
-
340
- def _check_conjugate_parameter_is_scalar_quadratic_reciprocal(f):
341
- """
342
- Tests whether a function (scalar to scalar) is linear (lambda x: s*x**-2 for some s).
343
- The tests checks whether the function divided by the parameter is the reciprical of a linear function.
344
- """
345
- return _check_conjugate_parameter_is_scalar_linear_reciprocal(lambda x: f(x)/x)
@@ -1,76 +0,0 @@
1
- import numpy as np
2
- from cuqi.experimental.mcmc import Conjugate
3
- from cuqi.experimental.mcmc._conjugate import _ConjugatePair, _get_conjugate_parameter, _check_conjugate_parameter_is_scalar_reciprocal
4
- from cuqi.distribution import LMRF, Gamma
5
- import scipy as sp
6
-
7
- class ConjugateApprox(Conjugate):
8
- """ Approximate Conjugate sampler
9
-
10
- Sampler for sampling a posterior distribution where the likelihood and prior can be approximated
11
- by a conjugate pair.
12
-
13
- Currently supported pairs are:
14
- - (LMRF, Gamma): Approximated by (Gaussian, Gamma) where Gamma is defined on the inverse of the scale parameter of the LMRF distribution.
15
-
16
- Gamma distribution must be univariate.
17
-
18
- LMRF likelihood must have zero mean.
19
-
20
- For more details on conjugacy see :class:`Conjugate`.
21
-
22
- """
23
-
24
- def _set_conjugatepair(self):
25
- """ Set the conjugate pair based on the likelihood and prior. This requires target to be set. """
26
- if isinstance(self.target.likelihood.distribution, LMRF) and isinstance(self.target.prior, Gamma):
27
- self._conjugatepair = _LMRFGammaPair(self.target)
28
- else:
29
- raise ValueError(f"Conjugacy is not defined for likelihood {type(self.target.likelihood.distribution)} and prior {type(self.target.prior)}, in CUQIpy")
30
-
31
-
32
- class _LMRFGammaPair(_ConjugatePair):
33
- """ Implementation of the conjugate pair (LMRF, Gamma) """
34
-
35
- def validate_target(self):
36
- if not self.target.prior.dim == 1:
37
- raise ValueError("Approximate conjugate sampler only works with univariate Gamma prior")
38
-
39
- if np.sum(self.target.likelihood.distribution.location) != 0:
40
- raise ValueError("Approximate conjugate sampler only works with zero mean LMRF likelihood")
41
-
42
- key_value_pairs = _get_conjugate_parameter(self.target)
43
- if len(key_value_pairs) != 1:
44
- raise ValueError(f"Multiple references to conjugate parameter {self.target.prior.name} found in likelihood. Only one occurance is supported.")
45
- for key, value in key_value_pairs:
46
- if key == "scale":
47
- if not _check_conjugate_parameter_is_scalar_reciprocal(value):
48
- raise ValueError("Approximate conjugate sampler only works with Gamma prior on the inverse of the scale parameter of the LMRF likelihood")
49
- else:
50
- raise ValueError(f"No approximate conjugacy defined for likelihood {type(self.target.likelihood.distribution)} and prior {type(self.target.prior)}, in CUQIpy")
51
-
52
- def conjugate_distribution(self):
53
- # Extract variables
54
- # Here we approximate the LMRF with a Gaussian
55
-
56
- # Extract diff_op from target likelihood
57
- D = self.target.likelihood.distribution._diff_op
58
- n = D.shape[0]
59
-
60
- # Gaussian approximation of LMRF prior as function of x_k
61
- # See Uribe et al. (2022) for details
62
- # Current has a zero mean assumption on likelihood! TODO
63
- beta=1e-5
64
- def Lk_fun(x_k):
65
- dd = 1/np.sqrt((D @ x_k)**2 + beta*np.ones(n))
66
- W = sp.sparse.diags(dd)
67
- return W.sqrt() @ D
68
-
69
- x = self.target.likelihood.data #x
70
- d = len(x) #d
71
- Lx = Lk_fun(x)@x #Lx
72
- alpha = self.target.prior.shape #alpha
73
- beta = self.target.prior.rate #beta
74
-
75
- # Create Gamma distribution and sample
76
- return Gamma(shape=d+alpha, rate=np.linalg.norm(Lx)**2+beta)
@@ -1,193 +0,0 @@
1
- import numpy as np
2
- import cuqi
3
- from cuqi.experimental.mcmc import ProposalBasedSampler
4
- from cuqi.array import CUQIarray
5
- from numbers import Number
6
-
7
- class CWMH(ProposalBasedSampler):
8
- """Component-wise Metropolis Hastings sampler.
9
-
10
- Allows sampling of a target distribution by a component-wise random-walk
11
- sampling of a proposal distribution along with an accept/reject step.
12
-
13
- Parameters
14
- ----------
15
-
16
- target : `cuqi.distribution.Distribution` or lambda function
17
- The target distribution to sample. Custom logpdfs are supported by using
18
- a :class:`cuqi.distribution.UserDefinedDistribution`.
19
-
20
- proposal : `cuqi.distribution.Distribution` or callable method
21
- The proposal to sample from. If a callable method it should provide a
22
- single independent sample from proposal distribution. Defaults to a
23
- Gaussian proposal. *Optional*.
24
-
25
- scale : float or ndarray
26
- Scale parameter used to define correlation between previous and proposed
27
- sample in random-walk. *Optional*. If float, the same scale is used for
28
- all dimensions. If ndarray, a (possibly) different scale is used for
29
- each dimension.
30
-
31
- initial_point : ndarray
32
- Initial parameters. *Optional*
33
-
34
- callback : callable, *Optional*
35
- If set this function will be called after every sample.
36
- The signature of the callback function is
37
- `callback(sample, sample_index)`, where `sample` is the current sample
38
- and `sample_index` is the index of the sample.
39
- An example is shown in demos/demo31_callback.py.
40
-
41
- kwargs : dict
42
- Additional keyword arguments to be passed to the base class
43
- :class:`ProposalBasedSampler`.
44
-
45
- Example
46
- -------
47
- .. code-block:: python
48
- import numpy as np
49
- import cuqi
50
- # Parameters
51
- dim = 5 # Dimension of distribution
52
- mu = np.arange(dim) # Mean of Gaussian
53
- std = 1 # standard deviation of Gaussian
54
-
55
- # Logpdf function
56
- logpdf_func = lambda x: -1/(std**2)*np.sum((x-mu)**2)
57
-
58
- # Define distribution from logpdf as UserDefinedDistribution (sample
59
- # and gradients also supported as inputs to UserDefinedDistribution)
60
- target = cuqi.distribution.UserDefinedDistribution(
61
- dim=dim, logpdf_func=logpdf_func)
62
-
63
- # Set up sampler
64
- sampler = cuqi.experimental.mcmc.CWMH(target, scale=1)
65
-
66
- # Sample
67
- samples = sampler.sample(2000).get_samples()
68
-
69
- """
70
-
71
- _STATE_KEYS = ProposalBasedSampler._STATE_KEYS.union(['_scale_temp'])
72
-
73
- def __init__(self, target:cuqi.density.Density=None, proposal=None, scale=1,
74
- initial_point=None, **kwargs):
75
- super().__init__(target, proposal=proposal, scale=scale,
76
- initial_point=initial_point, **kwargs)
77
-
78
- def _initialize(self):
79
- if isinstance(self.scale, Number):
80
- self.scale = np.ones(self.dim)*self.scale
81
- self._acc = [np.ones((self.dim))] # Overwrite acc from ProposalBasedSampler with list of arrays
82
-
83
- # Handling of temporary scale parameter due to possible bug in old CWMH
84
- self._scale_temp = self.scale.copy()
85
-
86
- @property
87
- def scale(self):
88
- """ Get the scale parameter. """
89
- return self._scale
90
-
91
- @scale.setter
92
- def scale(self, value):
93
- """ Set the scale parameter. """
94
- if self._is_initialized and isinstance(value, Number):
95
- value = np.ones(self.dim)*value
96
- self._scale = value
97
-
98
- def validate_target(self):
99
- if not isinstance(self.target, cuqi.density.Density):
100
- raise ValueError(
101
- "Target should be an instance of "+\
102
- f"{cuqi.density.Density.__class__.__name__}")
103
- # Fail when there is no log density, which is currently assumed to be the case in case NaN is returned.
104
- if np.isnan(self.target.logd(self._get_default_initial_point(self.dim))):
105
- raise ValueError("Target does not have valid logd")
106
-
107
- def validate_proposal(self):
108
- if not isinstance(self.proposal, cuqi.distribution.Distribution):
109
- raise ValueError("Proposal must be a cuqi.distribution.Distribution object")
110
- if not self.proposal.is_symmetric:
111
- raise ValueError("Proposal must be symmetric")
112
-
113
- @property
114
- def proposal(self):
115
- if self._proposal is None:
116
- self._proposal = cuqi.distribution.Normal(
117
- mean=lambda location: location,
118
- std=lambda scale: scale,
119
- geometry=self.dim,
120
- )
121
- return self._proposal
122
-
123
- @proposal.setter
124
- def proposal(self, value):
125
- self._proposal = value
126
-
127
- def step(self):
128
- # Initialize x_t which is used to store the current CWMH sample
129
- x_t = self.current_point.copy()
130
-
131
- # Initialize x_star which is used to store the proposed sample by
132
- # updating the current sample component-by-component
133
- x_star = self.current_point.copy()
134
-
135
- # Propose a sample x_all_components from the proposal distribution
136
- # for all the components
137
- target_eval_t = self.current_target_logd
138
- if isinstance(self.proposal,cuqi.distribution.Distribution):
139
- x_all_components = self.proposal(
140
- location= self.current_point, scale=self.scale).sample()
141
- else:
142
- x_all_components = self.proposal(self.current_point, self.scale)
143
-
144
- # Initialize acceptance rate
145
- acc = np.zeros(self.dim)
146
-
147
- # Loop over all the components of the sample and accept/reject
148
- # each component update.
149
- for j in range(self.dim):
150
- # propose state x_star by updating the j-th component
151
- x_star[j] = x_all_components[j]
152
-
153
- # evaluate target
154
- target_eval_star = self.target.logd(x_star)
155
-
156
- # compute Metropolis acceptance ratio
157
- alpha = min(0, target_eval_star - target_eval_t)
158
-
159
- # accept/reject
160
- u_theta = np.log(np.random.rand())
161
- if (u_theta <= alpha) and \
162
- (not np.isnan(target_eval_star)) and \
163
- (not np.isinf(target_eval_star)):
164
- x_t[j] = x_all_components[j]
165
- target_eval_t = target_eval_star
166
- acc[j] = 1
167
-
168
- x_star = x_t.copy()
169
-
170
- self.current_target_logd = target_eval_t
171
- self.current_point = x_t
172
-
173
- return acc
174
-
175
- def tune(self, skip_len, update_count):
176
- # Store update_count in variable i for readability
177
- i = update_count
178
-
179
- # Optimal acceptance rate for CWMH
180
- star_acc = 0.21/self.dim + 0.23
181
-
182
- # Mean of acceptance rate over the last skip_len samples
183
- hat_acc = np.mean(self._acc[i*skip_len:(i+1)*skip_len], axis=0)
184
-
185
- # Compute new intermediate scaling parameter scale_temp
186
- # Factor zeta ensures that the variation of the scale update vanishes
187
- zeta = 1/np.sqrt(update_count+1)
188
- scale_temp = np.exp(
189
- np.log(self._scale_temp) + zeta*(hat_acc-star_acc))
190
-
191
- # Update the scale parameter
192
- self.scale = np.minimum(scale_temp, np.ones(self.dim))
193
- self._scale_temp = scale_temp