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