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
cuqi/sampler/_cwmh.py CHANGED
@@ -1,41 +1,49 @@
1
1
  import numpy as np
2
2
  import cuqi
3
3
  from cuqi.sampler import ProposalBasedSampler
4
-
4
+ from cuqi.array import CUQIarray
5
+ from numbers import Number
5
6
 
6
7
  class CWMH(ProposalBasedSampler):
7
8
  """Component-wise Metropolis Hastings sampler.
8
9
 
9
- Allows sampling of a target distribution by a component-wise random-walk sampling of a proposal distribution along with an accept/reject step.
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.
10
12
 
11
13
  Parameters
12
14
  ----------
13
15
 
14
16
  target : `cuqi.distribution.Distribution` or lambda function
15
- The target distribution to sample. Custom logpdfs are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
17
+ The target distribution to sample. Custom logpdfs are supported by using
18
+ a :class:`cuqi.distribution.UserDefinedDistribution`.
16
19
 
17
20
  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*.
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*.
19
24
 
20
- scale : float
21
- Scale parameter used to define correlation between previous and proposed sample in random-walk. *Optional*.
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.
22
30
 
23
- x0 : ndarray
31
+ initial_point : ndarray
24
32
  Initial parameters. *Optional*
25
33
 
26
- dim : int
27
- Dimension of parameter space. Required if target and proposal are callable functions. *Optional*.
34
+ callback : callable, optional
35
+ A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
36
+ 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)`.
28
37
 
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.
38
+ kwargs : dict
39
+ Additional keyword arguments to be passed to the base class
40
+ :class:`ProposalBasedSampler`.
34
41
 
35
42
  Example
36
43
  -------
37
44
  .. code-block:: python
38
-
45
+ import numpy as np
46
+ import cuqi
39
47
  # Parameters
40
48
  dim = 5 # Dimension of distribution
41
49
  mu = np.arange(dim) # Mean of Gaussian
@@ -44,153 +52,139 @@ class CWMH(ProposalBasedSampler):
44
52
  # Logpdf function
45
53
  logpdf_func = lambda x: -1/(std**2)*np.sum((x-mu)**2)
46
54
 
47
- # Define distribution from logpdf as UserDefinedDistribution (sample and gradients also supported as inputs to UserDefinedDistribution)
48
- target = cuqi.distribution.UserDefinedDistribution(dim=dim, logpdf_func=logpdf_func)
55
+ # Define distribution from logpdf as UserDefinedDistribution (sample
56
+ # and gradients also supported as inputs to UserDefinedDistribution)
57
+ target = cuqi.distribution.UserDefinedDistribution(
58
+ dim=dim, logpdf_func=logpdf_func)
49
59
 
50
60
  # Set up sampler
51
61
  sampler = cuqi.sampler.CWMH(target, scale=1)
52
62
 
53
63
  # Sample
54
- samples = sampler.sample(2000)
64
+ samples = sampler.sample(2000).get_samples()
55
65
 
56
66
  """
57
- def __init__(self, target, proposal=None, scale=1, x0=None, dim = None, **kwargs):
58
- super().__init__(target, proposal=proposal, scale=scale, x0=x0, dim=dim, **kwargs)
59
-
60
- @ProposalBasedSampler.proposal.setter
61
- def proposal(self, value):
62
- fail_msg = "Proposal should be either None, cuqi.distribution.Distribution conditioned only on 'location' and 'scale', lambda function, or cuqi.distribution.Normal conditioned only on 'mean' and 'std'"
63
-
64
- if value is None:
65
- self._proposal = cuqi.distribution.Normal(mean = lambda location:location,std = lambda scale:scale, geometry=self.dim)
66
-
67
- elif isinstance(value, cuqi.distribution.Distribution) and sorted(value.get_conditioning_variables())==['location','scale']:
68
- self._proposal = value
69
-
70
- elif isinstance(value, cuqi.distribution.Normal) and sorted(value.get_conditioning_variables())==['mean','std']:
71
- self._proposal = value(mean = lambda location:location, std = lambda scale:scale)
72
-
73
- elif not isinstance(value, cuqi.distribution.Distribution) and callable(value):
74
- self._proposal = value
75
-
76
- else:
77
- raise ValueError(fail_msg)
78
-
79
-
80
- def _sample(self, N, Nb):
81
- Ns = N+Nb # number of simulations
82
-
83
- # allocation
84
- samples = np.empty((self.dim, Ns))
85
- target_eval = np.empty(Ns)
86
- acc = np.zeros((self.dim, Ns), dtype=int)
87
-
88
- # initial state
89
- samples[:, 0] = self.x0
90
- target_eval[0] = self.target.logd(self.x0)
91
- acc[:, 0] = np.ones(self.dim)
92
-
93
- # run MCMC
94
- for s in range(Ns-1):
95
- # run component by component
96
- samples[:, s+1], target_eval[s+1], acc[:, s+1] = self.single_update(samples[:, s], target_eval[s])
97
67
 
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)
68
+ _STATE_KEYS = ProposalBasedSampler._STATE_KEYS.union(['_scale_temp'])
100
69
 
101
- # remove burn-in
102
- samples = samples[:, Nb:]
103
- target_eval = target_eval[Nb:]
104
- acccomp = acc[:, Nb:].mean(axis=1)
105
- print('\nAverage acceptance rate all components:', acccomp.mean(), '\n')
70
+ def __init__(self, target:cuqi.density.Density=None, proposal=None, scale=1,
71
+ initial_point=None, **kwargs):
72
+ super().__init__(target, proposal=proposal, scale=scale,
73
+ initial_point=initial_point, **kwargs)
106
74
 
107
- return samples, target_eval, acccomp
108
-
109
- def _sample_adapt(self, N, Nb):
110
- # this follows the vanishing adaptation Algorithm 4 in:
111
- # Andrieu and Thoms (2008) - A tutorial on adaptive MCMC
112
- Ns = N+Nb # number of simulations
113
-
114
- # allocation
115
- samples = np.empty((self.dim, Ns))
116
- target_eval = np.empty(Ns)
117
- acc = np.zeros((self.dim, Ns), dtype=int)
118
-
119
- # initial state
120
- samples[:, 0] = self.x0
121
- target_eval[0] = self.target.logd(self.x0)
122
- acc[:, 0] = np.ones(self.dim)
123
-
124
- # initial adaptation params
125
- Na = int(0.1*N) # iterations to adapt
126
- hat_acc = np.empty((self.dim, int(np.floor(Ns/Na)))) # average acceptance rate of the chains
127
- lambd = np.empty((self.dim, int(np.floor(Ns/Na)+1))) # scaling parameter \in (0,1)
128
- lambd[:, 0] = self.scale
129
- star_acc = 0.21/self.dim + 0.23 # target acceptance rate RW
130
- i, idx = 0, 0
131
-
132
- # run MCMC
133
- for s in range(Ns-1):
134
- # run component by component
135
- samples[:, s+1], target_eval[s+1], acc[:, s+1] = self.single_update(samples[:, s], target_eval[s])
136
-
137
- # adapt prop spread of each component using acc of past samples
138
- if ((s+1) % Na == 0):
139
- # evaluate average acceptance rate
140
- hat_acc[:, i] = np.mean(acc[:, idx:idx+Na], axis=1)
141
-
142
- # compute new scaling parameter
143
- zeta = 1/np.sqrt(i+1) # ensures that the variation of lambda(i) vanishes
144
- lambd[:, i+1] = np.exp(np.log(lambd[:, i]) + zeta*(hat_acc[:, i]-star_acc))
145
-
146
- # update parameters
147
- self.scale = np.minimum(lambd[:, i+1], np.ones(self.dim))
148
-
149
- # update counters
150
- i += 1
151
- idx += Na
152
-
153
- # display iterations
154
- self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
155
- self._call_callback(samples[:, s+1], s+1)
156
-
157
- # remove burn-in
158
- samples = samples[:, Nb:]
159
- target_eval = target_eval[Nb:]
160
- acccomp = acc[:, Nb:].mean(axis=1)
161
- print('\nAverage acceptance rate all components:', acccomp.mean(), '\n')
75
+ def _initialize(self):
76
+ if isinstance(self.scale, Number):
77
+ self.scale = np.ones(self.dim)*self.scale
78
+ self._acc = [np.ones((self.dim))] # Overwrite acc from ProposalBasedSampler with list of arrays
79
+
80
+ # Handling of temporary scale parameter due to possible bug in old CWMH
81
+ self._scale_temp = self.scale.copy()
82
+
83
+ @property
84
+ def scale(self):
85
+ """ Get the scale parameter. """
86
+ return self._scale
87
+
88
+ @scale.setter
89
+ def scale(self, value):
90
+ """ Set the scale parameter. """
91
+ if self._is_initialized and isinstance(value, Number):
92
+ value = np.ones(self.dim)*value
93
+ self._scale = value
94
+
95
+ def validate_target(self):
96
+ if not isinstance(self.target, cuqi.density.Density):
97
+ raise ValueError(
98
+ "Target should be an instance of "+\
99
+ f"{cuqi.density.Density.__class__.__name__}")
100
+ # Fail when there is no log density, which is currently assumed to be the case in case NaN is returned.
101
+ if np.isnan(self.target.logd(self._get_default_initial_point(self.dim))):
102
+ raise ValueError("Target does not have valid logd")
162
103
 
163
- return samples, target_eval, acccomp
104
+ def validate_proposal(self):
105
+ if not isinstance(self.proposal, cuqi.distribution.Distribution):
106
+ raise ValueError("Proposal must be a cuqi.distribution.Distribution object")
107
+ if not self.proposal.is_symmetric:
108
+ raise ValueError("Proposal must be symmetric")
109
+
110
+ @property
111
+ def proposal(self):
112
+ if self._proposal is None:
113
+ self._proposal = cuqi.distribution.Normal(
114
+ mean=lambda location: location,
115
+ std=lambda scale: scale,
116
+ geometry=self.dim,
117
+ )
118
+ return self._proposal
119
+
120
+ @proposal.setter
121
+ def proposal(self, value):
122
+ self._proposal = value
123
+
124
+ def step(self):
125
+ # Initialize x_t which is used to store the current CWMH sample
126
+ x_t = self.current_point.copy()
164
127
 
165
- def single_update(self, x_t, target_eval_t):
128
+ # Initialize x_star which is used to store the proposed sample by
129
+ # updating the current sample component-by-component
130
+ x_star = self.current_point.copy()
131
+
132
+ # Propose a sample x_all_components from the proposal distribution
133
+ # for all the components
134
+ target_eval_t = self.current_target_logd
166
135
  if isinstance(self.proposal,cuqi.distribution.Distribution):
167
- x_i_star = self.proposal(location= x_t, scale = self.scale).sample()
136
+ x_all_components = self.proposal(
137
+ location= self.current_point, scale=self.scale).sample()
168
138
  else:
169
- x_i_star = self.proposal(x_t, self.scale)
170
- x_star = x_t.copy()
139
+ x_all_components = self.proposal(self.current_point, self.scale)
140
+
141
+ # Initialize acceptance rate
171
142
  acc = np.zeros(self.dim)
172
143
 
144
+ # Loop over all the components of the sample and accept/reject
145
+ # each component update.
173
146
  for j in range(self.dim):
174
- # propose state
175
- x_star[j] = x_i_star[j]
147
+ # propose state x_star by updating the j-th component
148
+ x_star[j] = x_all_components[j]
176
149
 
177
150
  # evaluate target
178
151
  target_eval_star = self.target.logd(x_star)
179
152
 
180
- # ratio and acceptance probability
181
- ratio = target_eval_star - target_eval_t # proposal is symmetric
182
- alpha = min(0, ratio)
153
+ # compute Metropolis acceptance ratio
154
+ alpha = min(0, target_eval_star - target_eval_t)
183
155
 
184
156
  # accept/reject
185
157
  u_theta = np.log(np.random.rand())
186
- if (u_theta <= alpha):
187
- x_t[j] = x_i_star[j]
158
+ if (u_theta <= alpha) and \
159
+ (not np.isnan(target_eval_star)) and \
160
+ (not np.isinf(target_eval_star)):
161
+ x_t[j] = x_all_components[j]
188
162
  target_eval_t = target_eval_star
189
163
  acc[j] = 1
190
- else:
191
- pass
192
- # x_t[j] = x_t[j]
193
- # target_eval_t = target_eval_t
164
+
194
165
  x_star = x_t.copy()
195
- #
196
- return x_t, target_eval_t, acc
166
+
167
+ self.current_target_logd = target_eval_t
168
+ self.current_point = x_t
169
+
170
+ return acc
171
+
172
+ def tune(self, skip_len, update_count):
173
+ # Store update_count in variable i for readability
174
+ i = update_count
175
+
176
+ # Optimal acceptance rate for CWMH
177
+ star_acc = 0.21/self.dim + 0.23
178
+
179
+ # Mean of acceptance rate over the last skip_len samples
180
+ hat_acc = np.mean(self._acc[i*skip_len:(i+1)*skip_len], axis=0)
181
+
182
+ # Compute new intermediate scaling parameter scale_temp
183
+ # Factor zeta ensures that the variation of the scale update vanishes
184
+ zeta = 1/np.sqrt(update_count+1)
185
+ scale_temp = np.exp(
186
+ np.log(self._scale_temp) + zeta*(hat_acc-star_acc))
187
+
188
+ # Update the scale parameter
189
+ self.scale = np.minimum(scale_temp, np.ones(self.dim))
190
+ self._scale_temp = scale_temp
@@ -1,4 +1,4 @@
1
- from cuqi.experimental.mcmc import Sampler
1
+ from cuqi.sampler import Sampler
2
2
 
3
3
  class Direct(Sampler):
4
4
  """ Direct sampler