CUQIpy 1.1.1.post0.dev36__py3-none-any.whl → 1.4.1.post0.dev124__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.

Potentially problematic release.


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

Files changed (92) hide show
  1. cuqi/__init__.py +2 -0
  2. cuqi/_version.py +3 -3
  3. cuqi/algebra/__init__.py +2 -0
  4. cuqi/algebra/_abstract_syntax_tree.py +358 -0
  5. cuqi/algebra/_ordered_set.py +82 -0
  6. cuqi/algebra/_random_variable.py +457 -0
  7. cuqi/array/_array.py +4 -13
  8. cuqi/config.py +7 -0
  9. cuqi/density/_density.py +9 -1
  10. cuqi/distribution/__init__.py +3 -2
  11. cuqi/distribution/_beta.py +7 -11
  12. cuqi/distribution/_cauchy.py +2 -2
  13. cuqi/distribution/_custom.py +0 -6
  14. cuqi/distribution/_distribution.py +31 -45
  15. cuqi/distribution/_gamma.py +7 -3
  16. cuqi/distribution/_gaussian.py +2 -12
  17. cuqi/distribution/_inverse_gamma.py +4 -10
  18. cuqi/distribution/_joint_distribution.py +112 -15
  19. cuqi/distribution/_lognormal.py +0 -7
  20. cuqi/distribution/{_modifiedhalfnormal.py → _modified_half_normal.py} +23 -23
  21. cuqi/distribution/_normal.py +34 -7
  22. cuqi/distribution/_posterior.py +9 -0
  23. cuqi/distribution/_truncated_normal.py +129 -0
  24. cuqi/distribution/_uniform.py +47 -1
  25. cuqi/experimental/__init__.py +2 -2
  26. cuqi/experimental/_recommender.py +216 -0
  27. cuqi/geometry/__init__.py +2 -0
  28. cuqi/geometry/_geometry.py +15 -1
  29. cuqi/geometry/_product_geometry.py +181 -0
  30. cuqi/implicitprior/__init__.py +5 -3
  31. cuqi/implicitprior/_regularized_gaussian.py +483 -0
  32. cuqi/implicitprior/{_regularizedGMRF.py → _regularized_gmrf.py} +4 -2
  33. cuqi/implicitprior/{_regularizedUnboundedUniform.py → _regularized_unbounded_uniform.py} +3 -2
  34. cuqi/implicitprior/_restorator.py +269 -0
  35. cuqi/legacy/__init__.py +2 -0
  36. cuqi/{experimental/mcmc → legacy/sampler}/__init__.py +7 -11
  37. cuqi/legacy/sampler/_conjugate.py +55 -0
  38. cuqi/legacy/sampler/_conjugate_approx.py +52 -0
  39. cuqi/legacy/sampler/_cwmh.py +196 -0
  40. cuqi/legacy/sampler/_gibbs.py +231 -0
  41. cuqi/legacy/sampler/_hmc.py +335 -0
  42. cuqi/{experimental/mcmc → legacy/sampler}/_langevin_algorithm.py +82 -111
  43. cuqi/legacy/sampler/_laplace_approximation.py +184 -0
  44. cuqi/legacy/sampler/_mh.py +190 -0
  45. cuqi/legacy/sampler/_pcn.py +244 -0
  46. cuqi/{experimental/mcmc → legacy/sampler}/_rto.py +132 -90
  47. cuqi/legacy/sampler/_sampler.py +182 -0
  48. cuqi/likelihood/_likelihood.py +9 -1
  49. cuqi/model/__init__.py +1 -1
  50. cuqi/model/_model.py +1361 -359
  51. cuqi/pde/__init__.py +4 -0
  52. cuqi/pde/_observation_map.py +36 -0
  53. cuqi/pde/_pde.py +134 -33
  54. cuqi/problem/_problem.py +93 -87
  55. cuqi/sampler/__init__.py +120 -8
  56. cuqi/sampler/_conjugate.py +376 -35
  57. cuqi/sampler/_conjugate_approx.py +40 -16
  58. cuqi/sampler/_cwmh.py +132 -138
  59. cuqi/{experimental/mcmc → sampler}/_direct.py +1 -1
  60. cuqi/sampler/_gibbs.py +288 -130
  61. cuqi/sampler/_hmc.py +328 -201
  62. cuqi/sampler/_langevin_algorithm.py +284 -100
  63. cuqi/sampler/_laplace_approximation.py +87 -117
  64. cuqi/sampler/_mh.py +47 -157
  65. cuqi/sampler/_pcn.py +65 -213
  66. cuqi/sampler/_rto.py +211 -142
  67. cuqi/sampler/_sampler.py +553 -136
  68. cuqi/samples/__init__.py +1 -1
  69. cuqi/samples/_samples.py +24 -18
  70. cuqi/solver/__init__.py +6 -4
  71. cuqi/solver/_solver.py +230 -26
  72. cuqi/testproblem/_testproblem.py +2 -3
  73. cuqi/utilities/__init__.py +6 -1
  74. cuqi/utilities/_get_python_variable_name.py +2 -2
  75. cuqi/utilities/_utilities.py +182 -2
  76. {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/METADATA +10 -6
  77. cuqipy-1.4.1.post0.dev124.dist-info/RECORD +101 -0
  78. {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/WHEEL +1 -1
  79. CUQIpy-1.1.1.post0.dev36.dist-info/RECORD +0 -92
  80. cuqi/experimental/mcmc/_conjugate.py +0 -197
  81. cuqi/experimental/mcmc/_conjugate_approx.py +0 -81
  82. cuqi/experimental/mcmc/_cwmh.py +0 -191
  83. cuqi/experimental/mcmc/_gibbs.py +0 -268
  84. cuqi/experimental/mcmc/_hmc.py +0 -470
  85. cuqi/experimental/mcmc/_laplace_approximation.py +0 -156
  86. cuqi/experimental/mcmc/_mh.py +0 -78
  87. cuqi/experimental/mcmc/_pcn.py +0 -89
  88. cuqi/experimental/mcmc/_sampler.py +0 -561
  89. cuqi/experimental/mcmc/_utilities.py +0 -17
  90. cuqi/implicitprior/_regularizedGaussian.py +0 -323
  91. {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info/licenses}/LICENSE +0 -0
  92. {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,8 @@
1
1
  import numpy as np
2
2
  import cuqi
3
- from cuqi.experimental.mcmc import Sampler
4
- from cuqi.array import CUQIarray
3
+ from cuqi.legacy.sampler import Sampler
5
4
 
6
- class ULA(Sampler): # Refactor to Proposal-based sampler?
5
+ class ULA(Sampler):
7
6
  """Unadjusted Langevin algorithm (ULA) (Roberts and Tweedie, 1996)
8
7
 
9
8
  Samples a distribution given its logpdf and gradient (up to a constant) based on
@@ -20,13 +19,17 @@ class ULA(Sampler): # Refactor to Proposal-based sampler?
20
19
  The target distribution to sample. Must have logd and gradient method. Custom logpdfs
21
20
  and gradients are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
22
21
 
23
- initial_point : ndarray
22
+ x0 : ndarray
24
23
  Initial parameters. *Optional*
25
24
 
26
25
  scale : int
27
26
  The Langevin diffusion discretization time step (In practice, a scale of 1/dim**2 is
28
27
  recommended but not guaranteed to be the optimal choice).
29
28
 
29
+ dim : int
30
+ Dimension of parameter space. Required if target logpdf and gradient are callable
31
+ functions. *Optional*.
32
+
30
33
  callback : callable, *Optional*
31
34
  If set this function will be called after every sample.
32
35
  The signature of the callback function is `callback(sample, sample_index)`,
@@ -52,80 +55,61 @@ class ULA(Sampler): # Refactor to Proposal-based sampler?
52
55
  gradient_func=gradient_func)
53
56
 
54
57
  # Set up sampler
55
- sampler = cuqi.experimental.mcmc.ULA(target, scale=1/dim**2)
58
+ sampler = cuqi.legacy.sampler.ULA(target, scale=1/dim**2)
56
59
 
57
60
  # Sample
58
- sampler.sample(2000)
61
+ samples = sampler.sample(2000)
59
62
 
60
- A Deblur example can be found in demos/demo27_ULA.py
61
- # TODO: update demo once sampler merged
63
+ A Deblur example can be found in demos/demo27_ula.py
62
64
  """
65
+ def __init__(self, target, scale, x0=None, dim=None, rng=None, **kwargs):
66
+ super().__init__(target, x0=x0, dim=dim, **kwargs)
67
+ self.scale = scale
68
+ self.rng = rng
69
+
70
+ def _sample_adapt(self, N, Nb):
71
+ return self._sample(N, Nb)
72
+
73
+ def _sample(self, N, Nb):
74
+ # allocation
75
+ Ns = Nb+N
76
+ samples = np.empty((self.dim, Ns))
77
+ target_eval = np.empty(Ns)
78
+ g_target_eval = np.empty((self.dim, Ns))
79
+ acc = np.zeros(Ns)
80
+
81
+ # initial state
82
+ samples[:, 0] = self.x0
83
+ target_eval[0], g_target_eval[:,0] = self.target.logd(self.x0), self.target.gradient(self.x0)
84
+ acc[0] = 1
85
+
86
+ # ULA
87
+ for s in range(Ns-1):
88
+ samples[:, s+1], target_eval[s+1], g_target_eval[:,s+1], acc[s+1] = \
89
+ self.single_update(samples[:, s], target_eval[s], g_target_eval[:,s])
90
+ self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
91
+ self._call_callback(samples[:, s+1], s+1)
92
+
93
+ # apply burn-in
94
+ samples = samples[:, Nb:]
95
+ target_eval = target_eval[Nb:]
96
+ acc = acc[Nb:]
97
+ return samples, target_eval, np.mean(acc)
63
98
 
64
- _STATE_KEYS = Sampler._STATE_KEYS.union({'current_target_logd', 'scale', 'current_target_grad'})
65
-
66
- def __init__(self, target=None, scale=1.0, **kwargs):
67
-
68
- super().__init__(target, **kwargs)
69
-
70
- self.initial_scale = scale
71
-
72
- def _initialize(self):
73
- self.scale = self.initial_scale
74
- self.current_target_logd = self.target.logd(self.current_point)
75
- self.current_target_grad = self.target.gradient(self.current_point)
76
-
77
- def validate_target(self):
78
- try:
79
- self.target.gradient(np.ones(self.dim))
80
- pass
81
- except (NotImplementedError, AttributeError):
82
- raise ValueError("The target needs to have a gradient method")
83
-
84
- def _accept_or_reject(self, x_star, target_eval_star, target_grad_star):
85
- """
86
- Accepts the proposed state and updates the sampler's state accordingly, i.e.,
87
- current_point, current_target_eval, and current_target_grad_eval.
88
-
89
- Parameters
90
- ----------
91
- x_star :
92
- The proposed state
93
-
94
- target_eval_star:
95
- The log likelihood evaluated at x_star
96
-
97
- target_grad_star:
98
- The gradient of log likelihood evaluated at x_star
99
-
100
- Returns
101
- -------
102
- scalar
103
- 1 (accepted)
104
- """
105
- self.current_point = x_star
106
- self.current_target_logd = target_eval_star
107
- self.current_target_grad = target_grad_star
108
- acc = 1
109
- return acc
110
-
111
- def step(self):
112
- # propose state
113
- xi = cuqi.distribution.Normal(mean=np.zeros(self.dim), std=np.sqrt(self.scale)).sample()
114
- x_star = self.current_point + 0.5*self.scale*self.current_target_grad + xi
115
-
116
- # evaluate target
117
- target_eval_star, target_grad_star = self.target.logd(x_star), self.target.gradient(x_star)
118
-
119
- # accept or reject proposal
120
- acc = self._accept_or_reject(x_star, target_eval_star, target_grad_star)
99
+ def single_update(self, x_t, target_eval_t, g_target_eval_t):
100
+ # approximate Langevin diffusion
101
+ xi = cuqi.distribution.Normal(mean=np.zeros(self.dim), std=np.sqrt(self.scale)).sample(rng=self.rng)
102
+ x_star = x_t + 0.5*self.scale*g_target_eval_t + xi
103
+ logpi_eval_star, g_logpi_star = self.target.logd(x_star), self.target.gradient(x_star)
121
104
 
122
- return acc
105
+ # msg
106
+ if np.isnan(logpi_eval_star):
107
+ raise NameError('NaN potential func. Consider using smaller scale parameter')
123
108
 
124
- def tune(self, skip_len, update_count):
125
- pass
109
+ return x_star, logpi_eval_star, g_logpi_star, 1 # sample always accepted without Metropolis correction
126
110
 
127
111
 
128
- class MALA(ULA): # Refactor to Proposal-based sampler?
112
+ class MALA(ULA):
129
113
  """ Metropolis-adjusted Langevin algorithm (MALA) (Roberts and Tweedie, 1996)
130
114
 
131
115
  Samples a distribution given its logd and gradient (up to a constant) based on
@@ -143,12 +127,16 @@ class MALA(ULA): # Refactor to Proposal-based sampler?
143
127
  The target distribution to sample. Must have logpdf and gradient method. Custom logpdfs
144
128
  and gradients are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
145
129
 
146
- initial_point : ndarray
130
+ x0 : ndarray
147
131
  Initial parameters. *Optional*
148
132
 
149
133
  scale : int
150
134
  The Langevin diffusion discretization time step.
151
135
 
136
+ dim : int
137
+ Dimension of parameter space. Required if target logpdf and gradient are callable
138
+ functions. *Optional*.
139
+
152
140
  callback : callable, *Optional*
153
141
  If set this function will be called after every sample.
154
142
  The signature of the callback function is `callback(sample, sample_index)`,
@@ -174,54 +162,37 @@ class MALA(ULA): # Refactor to Proposal-based sampler?
174
162
  gradient_func=gradient_func)
175
163
 
176
164
  # Set up sampler
177
- sampler = cuqi.experimental.mcmc.MALA(target, scale=1/5**2)
165
+ sampler = cuqi.legacy.sampler.MALA(target, scale=1/5**2)
178
166
 
179
167
  # Sample
180
- sampler.sample(2000)
168
+ samples = sampler.sample(2000)
181
169
 
182
- A Deblur example can be found in demos/demo28_MALA.py
183
- # TODO: update demo once sampler merged
170
+ A Deblur example can be found in demos/demo28_mala.py
184
171
  """
185
-
186
- def _accept_or_reject(self, x_star, target_eval_star, target_grad_star):
187
- """
188
- Accepts the proposed state according to a Metropolis step and updates the sampler's state accordingly, i.e., current_point, current_target_eval, and current_target_grad_eval.
189
-
190
- Parameters
191
- ----------
192
- x_star :
193
- The proposed state
194
-
195
- target_eval_star:
196
- The log likelihood evaluated at x_star
197
-
198
- target_grad_star:
199
- The gradient of log likelihood evaluated at x_star
200
-
201
- Returns
202
- -------
203
- scaler
204
- 1 if accepted, 0 otherwise
205
- """
206
- log_target_ratio = target_eval_star - self.current_target_logd
207
- log_prop_ratio = self._log_proposal(self.current_point, x_star, target_grad_star) \
208
- - self._log_proposal(x_star, self.current_point, self.current_target_grad)
172
+ def __init__(self, target, scale, x0=None, dim=None, rng=None, **kwargs):
173
+ super().__init__(target, scale, x0=x0, dim=dim, rng=rng, **kwargs)
174
+
175
+ def single_update(self, x_t, target_eval_t, g_target_eval_t):
176
+ # approximate Langevin diffusion
177
+ xi = cuqi.distribution.Normal(mean=np.zeros(self.dim), std=np.sqrt(self.scale)).sample(rng=self.rng)
178
+ x_star = x_t + (self.scale/2)*g_target_eval_t + xi
179
+ logpi_eval_star, g_logpi_star = self.target.logd(x_star), self.target.gradient(x_star)
180
+
181
+ # Metropolis step
182
+ log_target_ratio = logpi_eval_star - target_eval_t
183
+ log_prop_ratio = self.log_proposal(x_t, x_star, g_logpi_star) \
184
+ - self.log_proposal(x_star, x_t, g_target_eval_t)
209
185
  log_alpha = min(0, log_target_ratio + log_prop_ratio)
210
186
 
211
- # accept/reject with Metropolis
212
- acc = 0
213
- log_u = np.log(np.random.rand())
214
- if (log_u <= log_alpha) and (np.isnan(target_eval_star) == False):
215
- self.current_point = x_star
216
- self.current_target_logd = target_eval_star
217
- self.current_target_grad = target_grad_star
218
- acc = 1
219
- return acc
187
+ # accept/reject
188
+ log_u = np.log(cuqi.distribution.Uniform(low=0, high=1).sample(rng=self.rng))
189
+ if (log_u <= log_alpha) and (np.isnan(logpi_eval_star) == False):
190
+ return x_star, logpi_eval_star, g_logpi_star, 1
191
+ else:
192
+ return x_t.copy(), target_eval_t, g_target_eval_t.copy(), 0
220
193
 
221
- def tune(self, skip_len, update_count):
222
- pass
223
-
224
- def _log_proposal(self, theta_star, theta_k, g_logpi_k):
194
+ def log_proposal(self, theta_star, theta_k, g_logpi_k):
225
195
  mu = theta_k + ((self.scale)/2)*g_logpi_k
226
196
  misfit = theta_star - mu
227
197
  return -0.5*((1/(self.scale))*(misfit.T @ misfit))
198
+
@@ -0,0 +1,184 @@
1
+ import scipy as sp
2
+ import numpy as np
3
+ import cuqi
4
+ from cuqi.distribution import Normal
5
+ from cuqi.solver import CGLS
6
+ from cuqi.legacy.sampler import Sampler
7
+
8
+
9
+ class UGLA(Sampler):
10
+ """ Unadjusted (Gaussian) Laplace Approximation sampler
11
+
12
+ Samples an approximate posterior where the prior is approximated
13
+ by a Gaussian distribution. The likelihood must be Gaussian.
14
+
15
+ Currently only works for LMRF priors.
16
+
17
+ The inner solver is Conjugate Gradient Least Squares (CGLS) solver.
18
+
19
+ For more details see: Uribe, Felipe, et al. "A hybrid Gibbs sampler for edge-preserving
20
+ tomographic reconstruction with uncertain view angles." arXiv preprint arXiv:2104.06919 (2021).
21
+
22
+ Parameters
23
+ ----------
24
+ target : `cuqi.distribution.Posterior`
25
+ The target posterior distribution to sample.
26
+
27
+ x0 : ndarray
28
+ Initial parameters. *Optional*
29
+
30
+ maxit : int
31
+ Maximum number of inner iterations for solver when generating one sample.
32
+
33
+ tol : float
34
+ Tolerance for inner solver. Will stop before maxit if the inner solvers convergence check reaches tol.
35
+
36
+ beta : float
37
+ Smoothing parameter for the Gaussian approximation of the Laplace distribution. Larger beta is easier to sample but is a worse approximation.
38
+
39
+ rng : np.random.RandomState
40
+ Random number generator used for sampling. *Optional*
41
+
42
+ callback : callable, *Optional*
43
+ If set this function will be called after every sample.
44
+ The signature of the callback function is `callback(sample, sample_index)`,
45
+ where `sample` is the current sample and `sample_index` is the index of the sample.
46
+ An example is shown in demos/demo31_callback.py.
47
+
48
+ Returns
49
+ -------
50
+ cuqi.samples.Samples
51
+ Samples from the posterior distribution.
52
+
53
+ """
54
+
55
+ def __init__(self, target, x0=None, maxit=50, tol=1e-4, beta=1e-5, rng=None, **kwargs):
56
+
57
+ super().__init__(target, x0=x0, **kwargs)
58
+
59
+ # Check target type
60
+ if not isinstance(self.target, cuqi.distribution.Posterior):
61
+ raise ValueError(f"To initialize an object of type {self.__class__}, 'target' need to be of type 'cuqi.distribution.Posterior'.")
62
+
63
+ # Check Affine model
64
+ if not isinstance(self.target.likelihood.model, cuqi.model.AffineModel):
65
+ raise TypeError("Model needs to be affine or linear")
66
+
67
+ # Check Gaussian likelihood
68
+ if not hasattr(self.target.likelihood.distribution, "sqrtprec"):
69
+ raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")
70
+
71
+ # Check that prior is LMRF
72
+ if not isinstance(self.target.prior, cuqi.distribution.LMRF):
73
+ raise ValueError('Unadjusted Gaussian Laplace approximation (UGLA) requires LMRF prior')
74
+
75
+ # Modify initial guess since Sampler sets it to ones.
76
+ if x0 is not None:
77
+ self.x0 = x0
78
+ else:
79
+ self.x0 = np.zeros(self.target.prior.dim)
80
+
81
+ # Store internal parameters
82
+ self.maxit = maxit
83
+ self.tol = tol
84
+ self.beta = beta
85
+ self.rng = rng
86
+
87
+ def _sample_adapt(self, Ns, Nb):
88
+ return self._sample(Ns, Nb)
89
+
90
+ def _sample(self, Ns, Nb):
91
+ """ Sample from the approximate posterior.
92
+
93
+ Parameters
94
+ ----------
95
+ Ns : int
96
+ Number of samples to draw.
97
+
98
+ Nb : int
99
+ Number of burn-in samples to discard.
100
+
101
+ Returns
102
+ -------
103
+ samples : ndarray
104
+ Samples from the approximate posterior.
105
+
106
+ target_eval : ndarray
107
+ Log-likelihood of each sample.
108
+
109
+ acc : ndarray
110
+ Acceptance rate of each sample.
111
+
112
+ """
113
+
114
+ # Extract diff_op from target prior
115
+ D = self.target.prior._diff_op
116
+ n = D.shape[0]
117
+
118
+ # Gaussian approximation of LMRF prior as function of x_k
119
+ def Lk_fun(x_k):
120
+ dd = 1/np.sqrt((D @ x_k)**2 + self.beta*np.ones(n))
121
+ W = sp.sparse.diags(dd)
122
+ return W.sqrt() @ D
123
+
124
+ # Now prepare "LinearRTO" type sampler. TODO: Use LinearRTO for this instead
125
+ self._shift = 0
126
+
127
+ # Pre-computations
128
+ self._model = self.target.likelihood.model
129
+ self._data = self.target.likelihood.data - self.target.model._shift
130
+ self._m = len(self._data)
131
+ self._L1 = self.target.likelihood.distribution.sqrtprec
132
+
133
+ # If prior location is scalar, repeat it to match dimensions
134
+ if len(self.target.prior.location) == 1:
135
+ self._priorloc = np.repeat(self.target.prior.location, self.dim)
136
+ else:
137
+ self._priorloc = self.target.prior.location
138
+
139
+ # Initial Laplace approx
140
+ self._L2 = Lk_fun(self.x0)
141
+ self._L2mu = self._L2@self._priorloc
142
+ self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
143
+
144
+ #self.n = len(self.x0)
145
+
146
+ # Least squares form
147
+ def M(x, flag):
148
+ if flag == 1:
149
+ out1 = self._L1 @ self._model._forward_func_no_shift(x) # Use forward function which excludes shift
150
+ out2 = np.sqrt(1/self.target.prior.scale)*(self._L2 @ x)
151
+ out = np.hstack([out1, out2])
152
+ elif flag == 2:
153
+ idx = int(self._m)
154
+ out1 = self._model._adjoint_func_no_shift(self._L1.T@x[:idx])
155
+ out2 = np.sqrt(1/self.target.prior.scale)*(self._L2.T @ x[idx:])
156
+ out = out1 + out2
157
+ return out
158
+
159
+ # Initialize samples
160
+ N = Ns+Nb # number of simulations
161
+ samples = np.empty((self.target.dim, N))
162
+
163
+ # initial state
164
+ samples[:, 0] = self.x0
165
+ for s in range(N-1):
166
+
167
+ # Update Laplace approximation
168
+ self._L2 = Lk_fun(samples[:, s])
169
+ self._L2mu = self._L2@self._priorloc
170
+ self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
171
+
172
+ # Sample from approximate posterior
173
+ e = Normal(mean=np.zeros(len(self._b_tild)), std=1).sample(rng=self.rng)
174
+ y = self._b_tild + e # Perturb data
175
+ sim = CGLS(M, y, samples[:, s], self.maxit, self.tol, self._shift)
176
+ samples[:, s+1], _ = sim.solve()
177
+
178
+ self._print_progress(s+2,N) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
179
+ self._call_callback(samples[:, s+1], s+1)
180
+
181
+ # remove burn-in
182
+ samples = samples[:, Nb:]
183
+
184
+ return samples, None, None
@@ -0,0 +1,190 @@
1
+ import numpy as np
2
+ import cuqi
3
+ from cuqi.legacy.sampler import ProposalBasedSampler
4
+
5
+
6
+ class MH(ProposalBasedSampler):
7
+ """Metropolis Hastings sampler.
8
+
9
+ Allows sampling of a target distribution by random-walk sampling of a proposal distribution along with an accept/reject step.
10
+
11
+ Parameters
12
+ ----------
13
+
14
+ target : `cuqi.distribution.Distribution` or lambda function
15
+ The target distribution to sample. Custom logpdfs are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
16
+
17
+ proposal : `cuqi.distribution.Distribution` or callable method
18
+ The proposal to sample from. If a callable method it should provide a single independent sample from proposal distribution. Defaults to a Gaussian proposal. *Optional*.
19
+
20
+ scale : float
21
+ Scale parameter used to define correlation between previous and proposed sample in random-walk. *Optional*.
22
+
23
+ x0 : ndarray
24
+ Initial parameters. *Optional*
25
+
26
+ dim : int
27
+ Dimension of parameter space. Required if target and proposal are callable functions. *Optional*.
28
+
29
+ callback : callable, *Optional*
30
+ If set this function will be called after every sample.
31
+ The signature of the callback function is `callback(sample, sample_index)`,
32
+ where `sample` is the current sample and `sample_index` is the index of the sample.
33
+ An example is shown in demos/demo31_callback.py.
34
+
35
+ Example
36
+ -------
37
+ .. code-block:: python
38
+
39
+ # Parameters
40
+ dim = 5 # Dimension of distribution
41
+ mu = np.arange(dim) # Mean of Gaussian
42
+ std = 1 # standard deviation of Gaussian
43
+
44
+ # Logpdf function
45
+ logpdf_func = lambda x: -1/(std**2)*np.sum((x-mu)**2)
46
+
47
+ # Define distribution from logpdf as UserDefinedDistribution (sample and gradients also supported)
48
+ target = cuqi.distribution.UserDefinedDistribution(dim=dim, logpdf_func=logpdf_func)
49
+
50
+ # Set up sampler
51
+ sampler = cuqi.legacy.sampler.MH(target, scale=1)
52
+
53
+ # Sample
54
+ samples = sampler.sample(2000)
55
+
56
+ """
57
+ #target, proposal=None, scale=1, x0=None, dim=None
58
+ # super().__init__(target, proposal=proposal, scale=scale, x0=x0, dim=dim)
59
+ def __init__(self, target, proposal=None, scale=None, x0=None, dim=None, **kwargs):
60
+ """ Metropolis-Hastings (MH) sampler. Default (if proposal is None) is random walk MH with proposal that is Gaussian with identity covariance"""
61
+ super().__init__(target, proposal=proposal, scale=scale, x0=x0, dim=dim, **kwargs)
62
+
63
+
64
+ @ProposalBasedSampler.proposal.setter
65
+ def proposal(self, value):
66
+ fail_msg = "Proposal should be either None, symmetric cuqi.distribution.Distribution or a lambda function."
67
+
68
+ if value is None:
69
+ self._proposal = cuqi.distribution.Gaussian(np.zeros(self.dim), 1)
70
+ elif not isinstance(value, cuqi.distribution.Distribution) and callable(value):
71
+ raise NotImplementedError(fail_msg)
72
+ elif isinstance(value, cuqi.distribution.Distribution) and value.is_symmetric:
73
+ self._proposal = value
74
+ else:
75
+ raise ValueError(fail_msg)
76
+ self._proposal.geometry = self.target.geometry
77
+
78
+ def _sample(self, N, Nb):
79
+ if self.scale is None:
80
+ raise ValueError("Scale must be set to sample without adaptation. Consider using sample_adapt instead.")
81
+
82
+ Ns = N+Nb # number of simulations
83
+
84
+ # allocation
85
+ samples = np.empty((self.dim, Ns))
86
+ target_eval = np.empty(Ns)
87
+ acc = np.zeros(Ns, dtype=int)
88
+
89
+ # initial state
90
+ samples[:, 0] = self.x0
91
+ target_eval[0] = self.target.logd(self.x0)
92
+ acc[0] = 1
93
+
94
+ # run MCMC
95
+ for s in range(Ns-1):
96
+ # run component by component
97
+ samples[:, s+1], target_eval[s+1], acc[s+1] = self.single_update(samples[:, s], target_eval[s])
98
+ self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
99
+ self._call_callback(samples[:, s+1], s+1)
100
+
101
+ # remove burn-in
102
+ samples = samples[:, Nb:]
103
+ target_eval = target_eval[Nb:]
104
+ accave = acc[Nb:].mean()
105
+ print('\nAverage acceptance rate:', accave, '\n')
106
+ #
107
+ return samples, target_eval, accave
108
+
109
+ def _sample_adapt(self, N, Nb):
110
+ # Set intial scale if not set
111
+ if self.scale is None:
112
+ self.scale = 0.1
113
+
114
+ Ns = N+Nb # number of simulations
115
+
116
+ # allocation
117
+ samples = np.empty((self.dim, Ns))
118
+ target_eval = np.empty(Ns)
119
+ acc = np.zeros(Ns)
120
+
121
+ # initial state
122
+ samples[:, 0] = self.x0
123
+ target_eval[0] = self.target.logd(self.x0)
124
+ acc[0] = 1
125
+
126
+ # initial adaptation params
127
+ Na = int(0.1*N) # iterations to adapt
128
+ hat_acc = np.empty(int(np.floor(Ns/Na))) # average acceptance rate of the chains
129
+ lambd = self.scale
130
+ star_acc = 0.234 # target acceptance rate RW
131
+ i, idx = 0, 0
132
+
133
+ # run MCMC
134
+ for s in range(Ns-1):
135
+ # run component by component
136
+ samples[:, s+1], target_eval[s+1], acc[s+1] = self.single_update(samples[:, s], target_eval[s])
137
+
138
+ # adapt prop spread using acc of past samples
139
+ if ((s+1) % Na == 0):
140
+ # evaluate average acceptance rate
141
+ hat_acc[i] = np.mean(acc[idx:idx+Na])
142
+
143
+ # d. compute new scaling parameter
144
+ zeta = 1/np.sqrt(i+1) # ensures that the variation of lambda(i) vanishes
145
+ lambd = np.exp(np.log(lambd) + zeta*(hat_acc[i]-star_acc))
146
+
147
+ # update parameters
148
+ self.scale = min(lambd, 1)
149
+
150
+ # update counters
151
+ i += 1
152
+ idx += Na
153
+
154
+ # display iterations
155
+ self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
156
+
157
+
158
+ # remove burn-in
159
+ samples = samples[:, Nb:]
160
+ target_eval = target_eval[Nb:]
161
+ accave = acc[Nb:].mean()
162
+ print('\nAverage acceptance rate:', accave, 'MCMC scale:', self.scale, '\n')
163
+
164
+ return samples, target_eval, accave
165
+
166
+
167
+ def single_update(self, x_t, target_eval_t):
168
+ # propose state
169
+ xi = self.proposal.sample(1) # sample from the proposal
170
+ x_star = x_t + self.scale*xi.flatten() # MH proposal
171
+
172
+ # evaluate target
173
+ target_eval_star = self.target.logd(x_star)
174
+
175
+ # ratio and acceptance probability
176
+ ratio = target_eval_star - target_eval_t # proposal is symmetric
177
+ alpha = min(0, ratio)
178
+
179
+ # accept/reject
180
+ u_theta = np.log(np.random.rand())
181
+ if (u_theta <= alpha):
182
+ x_next = x_star
183
+ target_eval_next = target_eval_star
184
+ acc = 1
185
+ else:
186
+ x_next = x_t
187
+ target_eval_next = target_eval_t
188
+ acc = 0
189
+
190
+ return x_next, target_eval_next, acc