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,464 +0,0 @@
1
- import numpy as np
2
- import numpy as np
3
- from cuqi.experimental.mcmc import Sampler
4
- from cuqi.array import CUQIarray
5
- from numbers import Number
6
-
7
- class NUTS(Sampler):
8
- """No-U-Turn Sampler (Hoffman and Gelman, 2014).
9
-
10
- Samples a distribution given its logpdf and gradient using a Hamiltonian
11
- Monte Carlo (HMC) algorithm with automatic parameter tuning.
12
-
13
- For more details see: See Hoffman, M. D., & Gelman, A. (2014). The no-U-turn
14
- sampler: Adaptively setting path lengths in Hamiltonian Monte Carlo. Journal
15
- of Machine Learning Research, 15, 1593-1623.
16
-
17
- Parameters
18
- ----------
19
- target : `cuqi.distribution.Distribution`
20
- The target distribution to sample. Must have logpdf and gradient method.
21
- Custom logpdfs and gradients are supported by using a
22
- :class:`cuqi.distribution.UserDefinedDistribution`.
23
-
24
- initial_point : ndarray
25
- Initial parameters. *Optional*. If not provided, the initial point is
26
- an array of ones.
27
-
28
- max_depth : int
29
- Maximum depth of the tree >=0 and the default is 15.
30
-
31
- step_size : None or float
32
- If step_size is provided (as positive float), it will be used as initial
33
- step size. If None, the step size will be estimated by the sampler.
34
-
35
- opt_acc_rate : float
36
- The optimal acceptance rate to reach if using adaptive step size.
37
- Suggested values are 0.6 (default) or 0.8 (as in stan). In principle,
38
- opt_acc_rate should be in (0, 1), however, choosing a value that is very
39
- close to 1 or 0 might lead to poor performance of the sampler.
40
-
41
- callback : callable, *Optional*
42
- If set this function will be called after every sample.
43
- The signature of the callback function is
44
- `callback(sample, sample_index)`,
45
- where `sample` is the current sample and `sample_index` is the index of
46
- the sample.
47
- An example is shown in demos/demo31_callback.py.
48
-
49
- Example
50
- -------
51
- .. code-block:: python
52
-
53
- # Import cuqi
54
- import cuqi
55
-
56
- # Define a target distribution
57
- tp = cuqi.testproblem.WangCubic()
58
- target = tp.posterior
59
-
60
- # Set up sampler
61
- sampler = cuqi.experimental.mcmc.NUTS(target)
62
-
63
- # Sample
64
- sampler.warmup(5000)
65
- sampler.sample(10000)
66
-
67
- # Get samples
68
- samples = sampler.get_samples()
69
-
70
- # Plot samples
71
- samples.plot_pair()
72
-
73
- After running the NUTS sampler, run diagnostics can be accessed via the
74
- following attributes:
75
-
76
- .. code-block:: python
77
-
78
- # Number of tree nodes created each NUTS iteration
79
- sampler.num_tree_node_list
80
-
81
- # Step size used in each NUTS iteration
82
- sampler.epsilon_list
83
-
84
- # Suggested step size during adaptation (the value of this step size is
85
- # only used after adaptation).
86
- sampler.epsilon_bar_list
87
-
88
- """
89
-
90
- _STATE_KEYS = Sampler._STATE_KEYS.union({'_epsilon', '_epsilon_bar',
91
- '_H_bar',
92
- 'current_target_logd',
93
- 'current_target_grad',
94
- 'max_depth'})
95
-
96
- _HISTORY_KEYS = Sampler._HISTORY_KEYS.union({'num_tree_node_list',
97
- 'epsilon_list',
98
- 'epsilon_bar_list'})
99
-
100
- def __init__(self, target=None, initial_point=None, max_depth=None,
101
- step_size=None, opt_acc_rate=0.6, **kwargs):
102
- super().__init__(target, initial_point=initial_point, **kwargs)
103
-
104
- # Assign parameters as attributes
105
- self.max_depth = max_depth
106
- self.step_size = step_size
107
- self.opt_acc_rate = opt_acc_rate
108
-
109
-
110
- def _initialize(self):
111
-
112
- self._current_alpha_ratio = np.nan # Current alpha ratio will be set to some
113
- # value (other than np.nan) before
114
- # being used
115
-
116
- self.current_target_logd, self.current_target_grad = self._nuts_target(self.current_point)
117
-
118
- # Parameters dual averaging
119
- # Initialize epsilon and epsilon_bar
120
- # epsilon is the step size used in the current iteration
121
- # after warm up and one sampling step, epsilon is updated
122
- # to epsilon_bar for the remaining sampling steps.
123
- if self.step_size is None:
124
- self._epsilon = self._FindGoodEpsilon()
125
- else:
126
- self._epsilon = self.step_size
127
- self._epsilon_bar = "unset"
128
-
129
- # Parameter mu, does not change during the run
130
- self._mu = np.log(10*self._epsilon)
131
-
132
- self._H_bar = 0
133
-
134
- # NUTS run diagnostic:
135
- # number of tree nodes created each NUTS iteration
136
- self._num_tree_node = 0
137
-
138
- # Create lists to store NUTS run diagnostics
139
- self._create_run_diagnostic_attributes()
140
-
141
- #=========================================================================
142
- #============================== Properties ===============================
143
- #=========================================================================
144
- @property
145
- def max_depth(self):
146
- return self._max_depth
147
-
148
- @max_depth.setter
149
- def max_depth(self, value):
150
- if value is None:
151
- value = 15 # default value
152
- if not isinstance(value, int):
153
- raise TypeError('max_depth must be an integer.')
154
- if value < 0:
155
- raise ValueError('max_depth must be >= 0.')
156
- self._max_depth = value
157
-
158
- @property
159
- def step_size(self):
160
- return self._step_size
161
-
162
- @step_size.setter
163
- def step_size(self, value):
164
- if value is None:
165
- pass # NUTS will adapt the step size
166
-
167
- # step_size must be a positive float, raise error otherwise
168
- elif isinstance(value, bool)\
169
- or not isinstance(value, Number)\
170
- or value <= 0:
171
- raise TypeError('step_size must be a positive float or None.')
172
- self._step_size = value
173
-
174
- @property
175
- def opt_acc_rate(self):
176
- return self._opt_acc_rate
177
-
178
- @opt_acc_rate.setter
179
- def opt_acc_rate(self, value):
180
- if not isinstance(value, Number) or value <= 0 or value >= 1:
181
- raise ValueError('opt_acc_rate must be a float in (0, 1).')
182
- self._opt_acc_rate = value
183
-
184
- #=========================================================================
185
- #================== Implement methods required by Sampler =============
186
- #=========================================================================
187
- def validate_target(self):
188
- # Check if the target has logd and gradient methods
189
- try:
190
- current_target_logd, current_target_grad =\
191
- self._nuts_target(np.ones(self.dim))
192
- except:
193
- raise ValueError('Target must have logd and gradient methods.')
194
-
195
- def reinitialize(self):
196
- # Call the parent reset method
197
- super().reinitialize()
198
- # Reset NUTS run diagnostic attributes
199
- self._reset_run_diagnostic_attributes()
200
-
201
- def step(self):
202
- if isinstance(self._epsilon_bar, str) and self._epsilon_bar == "unset":
203
- self._epsilon_bar = self._epsilon
204
-
205
- # Convert current_point, logd, and grad to numpy arrays
206
- # if they are CUQIarray objects
207
- if isinstance(self.current_point, CUQIarray):
208
- self.current_point = self.current_point.to_numpy()
209
- if isinstance(self.current_target_logd, CUQIarray):
210
- self.current_target_logd = self.current_target_logd.to_numpy()
211
- if isinstance(self.current_target_grad, CUQIarray):
212
- self.current_target_grad = self.current_target_grad.to_numpy()
213
-
214
- # reset number of tree nodes for each iteration
215
- self._num_tree_node = 0
216
-
217
- # copy current point, logd, and grad in local variables
218
- point_k = self.current_point # initial position (parameters)
219
- logd_k = self.current_target_logd
220
- grad_k = self.current_target_grad # initial gradient
221
-
222
- # compute r_k and Hamiltonian
223
- r_k = self._Kfun(1, 'sample') # resample momentum vector
224
- Ham = logd_k - self._Kfun(r_k, 'eval') # Hamiltonian
225
-
226
- # slice variable
227
- log_u = Ham - np.random.exponential(1, size=1)
228
-
229
- # initialization
230
- j, s, n = 0, 1, 1
231
- point_minus, point_plus = point_k.copy(), point_k.copy()
232
- grad_minus, grad_plus = grad_k.copy(), grad_k.copy()
233
- r_minus, r_plus = r_k.copy(), r_k.copy()
234
-
235
- # run NUTS
236
- acc = 0
237
- while (s == 1) and (j <= self.max_depth):
238
- # sample a direction
239
- v = int(2*(np.random.rand() < 0.5)-1)
240
-
241
- # build tree: doubling procedure
242
- if (v == -1):
243
- point_minus, r_minus, grad_minus, _, _, _, \
244
- point_prime, logd_prime, grad_prime,\
245
- n_prime, s_prime, alpha, n_alpha = \
246
- self._BuildTree(point_minus, r_minus, grad_minus,
247
- Ham, log_u, v, j, self._epsilon)
248
- else:
249
- _, _, _, point_plus, r_plus, grad_plus, \
250
- point_prime, logd_prime, grad_prime,\
251
- n_prime, s_prime, alpha, n_alpha = \
252
- self._BuildTree(point_plus, r_plus, grad_plus,
253
- Ham, log_u, v, j, self._epsilon)
254
-
255
- # Metropolis step
256
- alpha2 = min(1, (n_prime/n)) #min(0, np.log(n_p) - np.log(n))
257
- if (s_prime == 1) and \
258
- (np.random.rand() <= alpha2) and \
259
- (not np.isnan(logd_prime)) and \
260
- (not np.isinf(logd_prime)):
261
- self.current_point = point_prime.copy()
262
- # copy if array, else assign if scalar
263
- self.current_target_logd = (
264
- logd_prime.copy()
265
- if isinstance(logd_prime, np.ndarray)
266
- else logd_prime
267
- )
268
- self.current_target_grad = grad_prime.copy()
269
- acc = 1
270
-
271
-
272
- # update number of particles, tree level, and stopping criterion
273
- n += n_prime
274
- dpoints = point_plus - point_minus
275
- s = s_prime *\
276
- int((dpoints @ r_minus.T) >= 0) * int((dpoints @ r_plus.T) >= 0)
277
- j += 1
278
- self._current_alpha_ratio = alpha/n_alpha
279
-
280
- # update run diagnostic attributes
281
- self._update_run_diagnostic_attributes(
282
- self._num_tree_node, self._epsilon, self._epsilon_bar)
283
-
284
- self._epsilon = self._epsilon_bar
285
- if np.isnan(self.current_target_logd):
286
- raise NameError('NaN potential func')
287
-
288
- return acc
289
-
290
- def tune(self, skip_len, update_count):
291
- """ adapt epsilon during burn-in using dual averaging"""
292
- if isinstance(self._epsilon_bar, str) and self._epsilon_bar == "unset":
293
- self._epsilon_bar = 1
294
-
295
- k = update_count+1
296
-
297
- # Fixed parameters that do not change during the run
298
- gamma, t_0, kappa = 0.05, 10, 0.75 # kappa in (0.5, 1]
299
-
300
- eta1 = 1/(k + t_0)
301
- self._H_bar = (1-eta1)*self._H_bar +\
302
- eta1*(self.opt_acc_rate - (self._current_alpha_ratio))
303
- self._epsilon = np.exp(self._mu - (np.sqrt(k)/gamma)*self._H_bar)
304
- eta = k**(-kappa)
305
- self._epsilon_bar =\
306
- np.exp(eta*np.log(self._epsilon) +(1-eta)*np.log(self._epsilon_bar))
307
-
308
- #=========================================================================
309
- def _nuts_target(self, x): # returns logposterior tuple evaluation-gradient
310
- return self.target.logd(x), self.target.gradient(x)
311
-
312
- #=========================================================================
313
- # auxiliary standard Gaussian PDF: kinetic energy function
314
- # d_log_2pi = d*np.log(2*np.pi)
315
- def _Kfun(self, r, flag):
316
- if flag == 'eval': # evaluate
317
- return 0.5*(r.T @ r) #+ d_log_2pi
318
- if flag == 'sample': # sample
319
- return np.random.standard_normal(size=self.dim)
320
-
321
- #=========================================================================
322
- def _FindGoodEpsilon(self, epsilon=1):
323
- point_k = self.current_point
324
- self.current_target_logd, self.current_target_grad = self._nuts_target(
325
- point_k)
326
- logd = self.current_target_logd
327
- grad = self.current_target_grad
328
-
329
- r = self._Kfun(1, 'sample') # resample a momentum
330
- Ham = logd - self._Kfun(r, 'eval') # initial Hamiltonian
331
- _, r_prime, logd_prime, grad_prime = self._Leapfrog(
332
- point_k, r, grad, epsilon)
333
-
334
- # trick to make sure the step is not huge, leading to infinite values of
335
- # the likelihood
336
- k = 1
337
- while np.isinf(logd_prime) or np.isinf(grad_prime).any():
338
- k *= 0.5
339
- _, r_prime, logd_prime, grad_prime = self._Leapfrog(
340
- point_k, r, grad, epsilon*k)
341
- epsilon = 0.5*k*epsilon
342
-
343
- # doubles/halves the value of epsilon until the accprob of the Langevin
344
- # proposal crosses 0.5
345
- Ham_prime = logd_prime - self._Kfun(r_prime, 'eval')
346
- log_ratio = Ham_prime - Ham
347
- a = 1 if log_ratio > np.log(0.5) else -1
348
- while (a*log_ratio > -a*np.log(2)):
349
- epsilon = (2**a)*epsilon
350
- _, r_prime, logd_prime, _ = self._Leapfrog(
351
- point_k, r, grad, epsilon)
352
- Ham_prime = logd_prime - self._Kfun(r_prime, 'eval')
353
- log_ratio = Ham_prime - Ham
354
- return epsilon
355
-
356
- #=========================================================================
357
- def _Leapfrog(self, point_old, r_old, grad_old, epsilon):
358
- # symplectic integrator: trajectories preserve phase space volumen
359
- r_new = r_old + 0.5*epsilon*grad_old # half-step
360
- point_new = point_old + epsilon*r_new # full-step
361
- logd_new, grad_new = self._nuts_target(point_new) # new gradient
362
- r_new += 0.5*epsilon*grad_new # half-step
363
- return point_new, r_new, logd_new, grad_new
364
-
365
- #=========================================================================
366
- def _BuildTree(
367
- self, point_k, r, grad, Ham, log_u, v, j, epsilon, Delta_max=1000):
368
- # Increment the number of tree nodes counter
369
- self._num_tree_node += 1
370
-
371
- if (j == 0): # base case
372
- # single leapfrog step in the direction v
373
- point_prime, r_prime, logd_prime, grad_prime = self._Leapfrog(
374
- point_k, r, grad, v*epsilon)
375
- Ham_prime = logd_prime - self._Kfun(r_prime, 'eval') # Hamiltonian
376
- # eval
377
- n_prime = int(log_u <= Ham_prime) # if particle is in the slice
378
- s_prime = int(log_u < Delta_max + Ham_prime) # check U-turn
379
- #
380
- diff_Ham = Ham_prime - Ham
381
-
382
- # Compute the acceptance probability
383
- # alpha_prime = min(1, np.exp(diff_Ham))
384
- # written in a stable way to avoid overflow when computing
385
- # exp(diff_Ham) for large values of diff_Ham
386
- alpha_prime = 1 if diff_Ham > 0 else np.exp(diff_Ham)
387
- n_alpha_prime = 1
388
- #
389
- point_minus, point_plus = point_prime, point_prime
390
- r_minus, r_plus = r_prime, r_prime
391
- grad_minus, grad_plus = grad_prime, grad_prime
392
- else:
393
- # recursion: build the left/right subtrees
394
- point_minus, r_minus, grad_minus, point_plus, r_plus, grad_plus, \
395
- point_prime, logd_prime, grad_prime,\
396
- n_prime, s_prime, alpha_prime, n_alpha_prime = \
397
- self._BuildTree(point_k, r, grad,
398
- Ham, log_u, v, j-1, epsilon)
399
- if (s_prime == 1): # do only if the stopping criteria does not
400
- # verify at the first subtree
401
- if (v == -1):
402
- point_minus, r_minus, grad_minus, _, _, _, \
403
- point_2prime, logd_2prime, grad_2prime,\
404
- n_2prime, s_2prime, alpha_2prime, n_alpha_2prime = \
405
- self._BuildTree(point_minus, r_minus, grad_minus,
406
- Ham, log_u, v, j-1, epsilon)
407
- else:
408
- _, _, _, point_plus, r_plus, grad_plus, \
409
- point_2prime, logd_2prime, grad_2prime,\
410
- n_2prime, s_2prime, alpha_2prime, n_alpha_2prime = \
411
- self._BuildTree(point_plus, r_plus, grad_plus,
412
- Ham, log_u, v, j-1, epsilon)
413
-
414
- # Metropolis step
415
- alpha2 = n_2prime / max(1, (n_prime + n_2prime))
416
- if (np.random.rand() <= alpha2):
417
- point_prime = point_2prime.copy()
418
- # copy if array, else assign if scalar
419
- logd_prime = (
420
- logd_2prime.copy()
421
- if isinstance(logd_2prime, np.ndarray)
422
- else logd_2prime
423
- )
424
- grad_prime = grad_2prime.copy()
425
-
426
- # update number of particles and stopping criterion
427
- alpha_prime += alpha_2prime
428
- n_alpha_prime += n_alpha_2prime
429
- dpoints = point_plus - point_minus
430
- s_prime = s_2prime *\
431
- int((dpoints@r_minus.T)>=0) * int((dpoints@r_plus.T)>=0)
432
- n_prime += n_2prime
433
-
434
- return point_minus, r_minus, grad_minus, point_plus, r_plus, grad_plus,\
435
- point_prime, logd_prime, grad_prime,\
436
- n_prime, s_prime, alpha_prime, n_alpha_prime
437
-
438
- #=========================================================================
439
- #======================== Diagnostic methods =============================
440
- #=========================================================================
441
-
442
- def _create_run_diagnostic_attributes(self):
443
- """A method to create attributes to store NUTS run diagnostic."""
444
- self._reset_run_diagnostic_attributes()
445
-
446
- def _reset_run_diagnostic_attributes(self):
447
- """A method to reset attributes to store NUTS run diagnostic."""
448
- # List to store number of tree nodes created each NUTS iteration
449
- self.num_tree_node_list = []
450
- # List of step size used in each NUTS iteration
451
- self.epsilon_list = []
452
- # List of burn-in step size suggestion during adaptation
453
- # only used when adaptation is done
454
- # remains fixed after adaptation (after burn-in)
455
- self.epsilon_bar_list = []
456
-
457
- def _update_run_diagnostic_attributes(self, n_tree, eps, eps_bar):
458
- """A method to update attributes to store NUTS run diagnostic."""
459
- # Store the number of tree nodes created in iteration k
460
- self.num_tree_node_list.append(n_tree)
461
- # Store the step size used in iteration k
462
- self.epsilon_list.append(eps)
463
- # Store the step size suggestion during adaptation in iteration k
464
- self.epsilon_bar_list.append(eps_bar)