CUQIpy 1.2.0.post0.dev90__py3-none-any.whl → 1.2.0.post0.dev245__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: CUQIpy
3
- Version: 1.2.0.post0.dev90
3
+ Version: 1.2.0.post0.dev245
4
4
  Summary: Computational Uncertainty Quantification for Inverse problems in Python
5
5
  Maintainer-email: "Nicolai A. B. Riis" <nabr@dtu.dk>, "Jakob S. Jørgensen" <jakj@dtu.dk>, "Amal M. Alghamdi" <amaal@dtu.dk>, Chao Zhang <chaz@dtu.dk>
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  cuqi/__init__.py,sha256=LsGilhl-hBLEn6Glt8S_l0OJzAA1sKit_rui8h-D-p0,488
2
2
  cuqi/_messages.py,sha256=fzEBrZT2kbmfecBBPm7spVu7yHdxGARQB4QzXhJbCJ0,415
3
- cuqi/_version.py,sha256=fadCQ-al0LVIaJsUncww5HgsVEcSF53R-lWs5uar-ow,509
3
+ cuqi/_version.py,sha256=Td4M9WCq7hYHaBteaGyYBJsI-t7L2iZcgoErbmahT4I,510
4
4
  cuqi/config.py,sha256=wcYvz19wkeKW2EKCGIKJiTpWt5kdaxyt4imyRkvtTRA,526
5
5
  cuqi/diagnostics.py,sha256=5OrbJeqpynqRXOe5MtOKKhe7EAVdOEpHIqHnlMW9G_c,3029
6
6
  cuqi/array/__init__.py,sha256=-EeiaiWGNsE3twRS4dD814BIlfxEsNkTCZUc5gjOXb0,30
@@ -35,26 +35,27 @@ cuqi/distribution/_smoothed_laplace.py,sha256=p-1Y23mYA9omwiHGkEuv3T2mwcPAAoNlCr
35
35
  cuqi/distribution/_truncated_normal.py,sha256=sZkLYgnkGOyS_3ZxY7iw6L62t-Jh6shzsweRsRepN2k,4240
36
36
  cuqi/distribution/_uniform.py,sha256=KA8yQ6ZS3nQGS4PYJ4hpDg6Eq8EQKQvPsIpYfR8fj2w,1967
37
37
  cuqi/experimental/__init__.py,sha256=vhZvyMX6rl8Y0haqCzGLPz6PSUKyu75XMQbeDHqTTrw,83
38
- cuqi/experimental/mcmc/__init__.py,sha256=1sn0U6Ep0x5zv2602og2DkV3Bs8hNFOiq7C3VcMimVw,4472
38
+ cuqi/experimental/mcmc/__init__.py,sha256=zSqLZmxOqQ-F94C9-gPv7g89TX1XxlrlNm071Eb167I,4487
39
39
  cuqi/experimental/mcmc/_conjugate.py,sha256=VNPQkGity0mposcqxrx4UIeXm35EvJvZED4p2stffvA,9924
40
40
  cuqi/experimental/mcmc/_conjugate_approx.py,sha256=uEnY2ea9su5ivcNagyRAwpQP2gBY98sXU7N0y5hTADo,3653
41
41
  cuqi/experimental/mcmc/_cwmh.py,sha256=50v3uZaWhlVnfrEB5-lB_7pn8QoUVBe-xWxKGKbmNHg,7234
42
42
  cuqi/experimental/mcmc/_direct.py,sha256=9pQS_2Qk2-ybt6m8WTfPoKetcxQ00WaTRN85-Z0FrBY,777
43
43
  cuqi/experimental/mcmc/_gibbs.py,sha256=evgxf2tLFLlKB3hN0qz9a9NcZQSES8wdacnn3uNWocQ,12005
44
44
  cuqi/experimental/mcmc/_hmc.py,sha256=8p4QxZBRpFLzwamH-DWHSdZE0aXX3FqonBzczz_XkDw,19340
45
- cuqi/experimental/mcmc/_langevin_algorithm.py,sha256=yNO7ABxmkixzcLG-lv57GOTyeTr7HwFs2DrrhuZW9OI,8398
45
+ cuqi/experimental/mcmc/_langevin_algorithm.py,sha256=LtPdC1IAeF_gS3T93FDLFutXy7THw-JqZeywZExpefo,14527
46
46
  cuqi/experimental/mcmc/_laplace_approximation.py,sha256=rdiE3cMQFq6FLQcOQwPpuGIxrTAp3aoGPxMDSdeopV0,5688
47
47
  cuqi/experimental/mcmc/_mh.py,sha256=MXo0ahXP4KGFkaY4HtvcBE-TMQzsMlTmLKzSvpz7drU,2941
48
48
  cuqi/experimental/mcmc/_pcn.py,sha256=wqJBZLuRFSwxihaI53tumAg6AWVuceLMOmXssTetd1A,3374
49
- cuqi/experimental/mcmc/_rto.py,sha256=OtzgiYCxDoTdXp7y4mkLa2upj74qadesoqHYpr11ZCg,10061
49
+ cuqi/experimental/mcmc/_rto.py,sha256=Ub5rDe_yfkzxqcnimEArXWVb3twuGUJmvxEQNPKQWfU,10061
50
50
  cuqi/experimental/mcmc/_sampler.py,sha256=xtoT70T8xe3Ye7yYdIFQD_kivjXlqUImyV3bMt406nk,20106
51
51
  cuqi/experimental/mcmc/_utilities.py,sha256=kUzHbhIS3HYZRbneNBK41IogUYX5dS_bJxqEGm7TQBI,525
52
52
  cuqi/geometry/__init__.py,sha256=Tz1WGzZBY-QGH3c0GiyKm9XHN8MGGcnU6TUHLZkzB3o,842
53
53
  cuqi/geometry/_geometry.py,sha256=SDRZdiN2CIuS591lXxqgFoPWPIpwY-MHk75116QvdYY,46901
54
- cuqi/implicitprior/__init__.py,sha256=CaDQGYtmeFzN37vf3QUmKhcN9-H5lO66ZbK035k4qUw,246
54
+ cuqi/implicitprior/__init__.py,sha256=6z3lvw-tWDyjZSpB3pYzvijSMK9Zlf1IYqOVTtMD2h4,309
55
55
  cuqi/implicitprior/_regularizedGMRF.py,sha256=IR9tKzNMoz-b0RKu6ahVgMx_lDNB3jZHVWFMQm6QqZk,6259
56
56
  cuqi/implicitprior/_regularizedGaussian.py,sha256=cQtrgzyJU2pwoK4ORGl1erKLE9VY5NqwZTiqiViDswA,12371
57
57
  cuqi/implicitprior/_regularizedUnboundedUniform.py,sha256=H2fTOSqYTlDiLxQ7Ya6wnpCUIkpO4qKrkTOsOPnBBeU,3483
58
+ cuqi/implicitprior/_restorator.py,sha256=ixnH8RGcLpqlaIUdR5Dwjx72sO9f3BeotNFRC7Z7qZo,9198
58
59
  cuqi/likelihood/__init__.py,sha256=QXif382iwZ5bT3ZUqmMs_n70JVbbjxbqMrlQYbMn4Zo,1776
59
60
  cuqi/likelihood/_likelihood.py,sha256=z3AXAbIrv_DjOYh4jy3iDHemuIFUUJu6wdvJ5e2dgW0,6913
60
61
  cuqi/model/__init__.py,sha256=IcN4aZCnyp9o-8TNIoZ8vew99QQgi0EmZvnsIuR6qYI,49
@@ -75,19 +76,19 @@ cuqi/sampler/_langevin_algorithm.py,sha256=o5EyvaR6QGAD7LKwXVRC3WwAP5IYJf5GoMVWl
75
76
  cuqi/sampler/_laplace_approximation.py,sha256=u018Z5eqlcq_cIwD9yNOaA15dLQE_vUWaee5Xp8bcjg,6454
76
77
  cuqi/sampler/_mh.py,sha256=V5tIdn-KdfWo4J_Nbf-AH6XwKWblWUyc4BeuSikUHsE,7062
77
78
  cuqi/sampler/_pcn.py,sha256=F0h9-nUFtkqn-o-1s8BCsmr8V7u6R7ycoCOeeV1uhj0,8601
78
- cuqi/sampler/_rto.py,sha256=-AtMiYq4fh7pF9zVqfYjYtQbIIEGayrWyRGTj8KecfE,11518
79
+ cuqi/sampler/_rto.py,sha256=eJe7_gN_1NpHHc_okKmFtLcOrvoe6cBoVLdf9ULuB_w,11518
79
80
  cuqi/sampler/_sampler.py,sha256=TkZ_WAS-5Q43oICa-Elc2gftsRTBd7PEDUMDZ9tTGmU,5712
80
81
  cuqi/samples/__init__.py,sha256=vCs6lVk-pi8RBqa6cIN5wyn6u-K9oEf1Na4k1ZMrYv8,44
81
82
  cuqi/samples/_samples.py,sha256=hUc8OnCF9CTCuDTrGHwwzv3wp8mG_6vsJAFvuQ-x0uA,35832
82
- cuqi/solver/__init__.py,sha256=DGl8IdUnochRXHNDEy_13o_VT0vLFY6FjMmmSH6YUkY,169
83
- cuqi/solver/_solver.py,sha256=eRmpBkHv_RXFdZTWhYqebH-toNbQcPgEgklNd5zOyOw,22803
83
+ cuqi/solver/__init__.py,sha256=3eoTTgBHe3M6ygrbgUVG3GlqaZVe5lGajNV9rolXZJ8,179
84
+ cuqi/solver/_solver.py,sha256=4LdfxLaU-fUHltZw7Sq-Xohyxd_6RvKy03xxtIMW6Zs,29488
84
85
  cuqi/testproblem/__init__.py,sha256=DWTOcyuNHMbhEuuWlY5CkYkNDSAqhvsKmJXBLivyblU,202
85
86
  cuqi/testproblem/_testproblem.py,sha256=x769LwwRdJdzIiZkcQUGb_5-vynNTNALXWKato7sS0Q,52540
86
87
  cuqi/utilities/__init__.py,sha256=H7xpJe2UinjZftKvE2JuXtTi4DqtkR6uIezStAXwfGg,428
87
88
  cuqi/utilities/_get_python_variable_name.py,sha256=QwlBVj2koJRA8s8pWd554p7-ElcI7HUwY32HknaR92E,1827
88
89
  cuqi/utilities/_utilities.py,sha256=Jc4knn80vLoA7kgw9FzXwKVFGaNBOXiA9kgvltZU3Ao,11777
89
- CUQIpy-1.2.0.post0.dev90.dist-info/LICENSE,sha256=kJWRPrtRoQoZGXyyvu50Uc91X6_0XRaVfT0YZssicys,10799
90
- CUQIpy-1.2.0.post0.dev90.dist-info/METADATA,sha256=KBSZdCAb8ZYWIzYvHOZ4iqrog8QGiBynjOw0gbo_sis,18495
91
- CUQIpy-1.2.0.post0.dev90.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
92
- CUQIpy-1.2.0.post0.dev90.dist-info/top_level.txt,sha256=AgmgMc6TKfPPqbjV0kvAoCBN334i_Lwwojc7HE3ZwD0,5
93
- CUQIpy-1.2.0.post0.dev90.dist-info/RECORD,,
90
+ CUQIpy-1.2.0.post0.dev245.dist-info/LICENSE,sha256=kJWRPrtRoQoZGXyyvu50Uc91X6_0XRaVfT0YZssicys,10799
91
+ CUQIpy-1.2.0.post0.dev245.dist-info/METADATA,sha256=ibU2b50SIsnhPjnUdOJpuwftMbE4nU_jRugFzTbhOj4,18496
92
+ CUQIpy-1.2.0.post0.dev245.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
93
+ CUQIpy-1.2.0.post0.dev245.dist-info/top_level.txt,sha256=AgmgMc6TKfPPqbjV0kvAoCBN334i_Lwwojc7HE3ZwD0,5
94
+ CUQIpy-1.2.0.post0.dev245.dist-info/RECORD,,
cuqi/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-11-03T22:18:33+0100",
11
+ "date": "2024-11-08T12:37:05+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "8f8b00804a857370d46fd7bdf26cb9542a6b8f34",
15
- "version": "1.2.0.post0.dev90"
14
+ "full-revisionid": "113dd1dc30ade5f182e79d003153bcce9aee1894",
15
+ "version": "1.2.0.post0.dev245"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -109,7 +109,7 @@ Main changes for users
109
109
 
110
110
 
111
111
  from ._sampler import Sampler, ProposalBasedSampler
112
- from ._langevin_algorithm import ULA, MALA
112
+ from ._langevin_algorithm import ULA, MALA, MYULA, PnPULA
113
113
  from ._mh import MH
114
114
  from ._pcn import PCN
115
115
  from ._rto import LinearRTO, RegularizedLinearRTO
@@ -1,14 +1,18 @@
1
1
  import numpy as np
2
2
  import cuqi
3
3
  from cuqi.experimental.mcmc import Sampler
4
+ from cuqi.implicitprior import RestorationPrior, MoreauYoshidaPrior
4
5
  from cuqi.array import CUQIarray
6
+ from copy import deepcopy
5
7
 
6
8
  class ULA(Sampler): # Refactor to Proposal-based sampler?
7
9
  """Unadjusted Langevin algorithm (ULA) (Roberts and Tweedie, 1996)
8
10
 
9
- Samples a distribution given its logpdf and gradient (up to a constant) based on
10
- Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt, where L_t is
11
- the Langevin diffusion and W_t is the `dim`-dimensional standard Brownian motion.
11
+ It approximately samples a distribution given its logpdf gradient based on
12
+ the Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt, where
13
+ W_t is the `dim`-dimensional standard Brownian motion.
14
+ ULA results from the Euler-Maruyama discretization of this Langevin stochastic
15
+ differential equation (SDE).
12
16
 
13
17
  For more details see: Roberts, G. O., & Tweedie, R. L. (1996). Exponential convergence
14
18
  of Langevin distributions and their discrete approximations. Bernoulli, 341-363.
@@ -23,9 +27,10 @@ class ULA(Sampler): # Refactor to Proposal-based sampler?
23
27
  initial_point : ndarray
24
28
  Initial parameters. *Optional*
25
29
 
26
- scale : int
27
- The Langevin diffusion discretization time step (In practice, a scale of 1/dim**2 is
28
- recommended but not guaranteed to be the optimal choice).
30
+ scale : float
31
+ The Langevin diffusion discretization time step (In practice, scale must
32
+ be smaller than 1/L, where L is the Lipschitz of the gradient of the log
33
+ target density, logd).
29
34
 
30
35
  callback : callable, *Optional*
31
36
  If set this function will be called after every sample.
@@ -61,26 +66,30 @@ class ULA(Sampler): # Refactor to Proposal-based sampler?
61
66
  # TODO: update demo once sampler merged
62
67
  """
63
68
 
64
- _STATE_KEYS = Sampler._STATE_KEYS.union({'current_target_logd', 'scale', 'current_target_grad'})
69
+ _STATE_KEYS = Sampler._STATE_KEYS.union({'scale', 'current_target_grad'})
65
70
 
66
71
  def __init__(self, target=None, scale=1.0, **kwargs):
67
72
 
68
73
  super().__init__(target, **kwargs)
69
-
70
74
  self.initial_scale = scale
71
75
 
72
76
  def _initialize(self):
73
77
  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)
78
+ self.current_target_grad = self._eval_target_grad(self.current_point)
76
79
 
77
80
  def validate_target(self):
78
81
  try:
79
- self.target.gradient(np.ones(self.dim))
82
+ self._eval_target_grad(np.ones(self.dim))
80
83
  pass
81
84
  except (NotImplementedError, AttributeError):
82
85
  raise ValueError("The target needs to have a gradient method")
83
86
 
87
+ def _eval_target_logd(self, x):
88
+ return None
89
+
90
+ def _eval_target_grad(self, x):
91
+ return self.target.gradient(x)
92
+
84
93
  def _accept_or_reject(self, x_star, target_eval_star, target_grad_star):
85
94
  """
86
95
  Accepts the proposed state and updates the sampler's state accordingly, i.e.,
@@ -102,14 +111,11 @@ class ULA(Sampler): # Refactor to Proposal-based sampler?
102
111
  scalar
103
112
  1 (accepted)
104
113
  """
105
- acc = 0
106
- if (not np.isnan(target_eval_star)) and \
107
- (not np.isinf(target_eval_star)):
108
- self.current_point = x_star
109
- self.current_target_logd = target_eval_star
110
- self.current_target_grad = target_grad_star
111
- acc = 1
112
-
114
+
115
+ self.current_point = x_star
116
+ self.current_target_grad = target_grad_star
117
+ acc = 1
118
+
113
119
  return acc
114
120
 
115
121
  def step(self):
@@ -118,7 +124,8 @@ class ULA(Sampler): # Refactor to Proposal-based sampler?
118
124
  x_star = self.current_point + 0.5*self.scale*self.current_target_grad + xi
119
125
 
120
126
  # evaluate target
121
- target_eval_star, target_grad_star = self.target.logd(x_star), self.target.gradient(x_star)
127
+ target_eval_star = self._eval_target_logd(x_star)
128
+ target_grad_star = self._eval_target_grad(x_star)
122
129
 
123
130
  # accept or reject proposal
124
131
  acc = self._accept_or_reject(x_star, target_eval_star, target_grad_star)
@@ -133,9 +140,11 @@ class MALA(ULA): # Refactor to Proposal-based sampler?
133
140
  """ Metropolis-adjusted Langevin algorithm (MALA) (Roberts and Tweedie, 1996)
134
141
 
135
142
  Samples a distribution given its logd and gradient (up to a constant) based on
136
- Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt, where L_t is
137
- the Langevin diffusion and W_t is the `dim`-dimensional standard Brownian motion.
138
- The sample is then accepted or rejected according to Metropolis–Hastings algorithm.
143
+ Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt,
144
+ W_t is the `dim`-dimensional standard Brownian motion.
145
+ A sample is firstly proposed by ULA and is then accepted or rejected according
146
+ to a Metropolis–Hastings step.
147
+ This accept-reject step allows us to remove the asymptotic bias of ULA.
139
148
 
140
149
  For more details see: Roberts, G. O., & Tweedie, R. L. (1996). Exponential convergence
141
150
  of Langevin distributions and their discrete approximations. Bernoulli, 341-363.
@@ -150,8 +159,10 @@ class MALA(ULA): # Refactor to Proposal-based sampler?
150
159
  initial_point : ndarray
151
160
  Initial parameters. *Optional*
152
161
 
153
- scale : int
154
- The Langevin diffusion discretization time step.
162
+ scale : float
163
+ The Langevin diffusion discretization time step (In practice, scale must
164
+ be smaller than 1/L, where L is the Lipschitz of the gradient of the log
165
+ target density, logd).
155
166
 
156
167
  callback : callable, *Optional*
157
168
  If set this function will be called after every sample.
@@ -187,9 +198,20 @@ class MALA(ULA): # Refactor to Proposal-based sampler?
187
198
  # TODO: update demo once sampler merged
188
199
  """
189
200
 
201
+ _STATE_KEYS = ULA._STATE_KEYS.union({'current_target_logd'})
202
+
203
+ def _initialize(self):
204
+ super()._initialize()
205
+ self.current_target_logd = self.target.logd(self.current_point)
206
+
207
+ def _eval_target_logd(self, x):
208
+ return self.target.logd(x)
209
+
190
210
  def _accept_or_reject(self, x_star, target_eval_star, target_grad_star):
191
211
  """
192
- 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.
212
+ Accepts the proposed state according to a Metropolis step and updates
213
+ the sampler's state accordingly, i.e., current_point, current_target_eval,
214
+ and current_target_grad_eval.
193
215
 
194
216
  Parameters
195
217
  ----------
@@ -231,3 +253,137 @@ class MALA(ULA): # Refactor to Proposal-based sampler?
231
253
  mu = theta_k + ((self.scale)/2)*g_logpi_k
232
254
  misfit = theta_star - mu
233
255
  return -0.5*((1/(self.scale))*(misfit.T @ misfit))
256
+
257
+
258
+ class MYULA(ULA):
259
+ """Moreau-Yoshida Unadjusted Langevin algorithm (MYUULA) (Durmus et al., 2018)
260
+
261
+ Samples a smoothed target distribution given its smoothed logpdf gradient.
262
+ It is based on the Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt,
263
+ where W_t is a `dim`-dimensional standard Brownian motion.
264
+ It targets a differentiable density (partially) smoothed by the Moreau-Yoshida
265
+ envelope. The smoothed target density can be made arbitrarily closed to the
266
+ true unsmoothed target density.
267
+
268
+ For more details see: Durmus, Alain, Eric Moulines, and Marcelo Pereyra.
269
+ "Efficient Bayesian
270
+ computation by proximal Markov chain Monte Carlo: when Langevin meets Moreau."
271
+ SIAM Journal on Imaging Sciences 11.1 (2018): 473-506.
272
+
273
+ Parameters
274
+ ----------
275
+
276
+ target : `cuqi.distribution.Distribution`
277
+ The target distribution to sample from. The target distribution results from
278
+ a differentiable likelihood and prior of type RestorationPrior.
279
+
280
+ initial_point : ndarray
281
+ Initial parameters. *Optional*
282
+
283
+ scale : float
284
+ The Langevin diffusion discretization time step (In practice, scale must
285
+ be smaller than 1/L, where L is the Lipschitz of the gradient of the log
286
+ target density, logd).
287
+
288
+ smoothing_strength : float
289
+ This parameter controls the smoothing strength of MYULA.
290
+
291
+ callback : callable, *Optional*
292
+ If set this function will be called after every sample.
293
+ The signature of the callback function is `callback(sample, sample_index)`,
294
+ where `sample` is the current sample and `sample_index` is the index of
295
+ the sample.
296
+ An example is shown in demos/demo31_callback.py.
297
+
298
+ A Deblur example can be found in demos/howtos/myula.py
299
+ # TODO: update demo once sampler merged
300
+ """
301
+ def __init__(self, target=None, scale=1.0, smoothing_strength=0.1, **kwargs):
302
+ self.smoothing_strength = smoothing_strength
303
+ super().__init__(target=target, scale=scale, **kwargs)
304
+
305
+ @Sampler.target.setter
306
+ def target(self, value):
307
+ """ Set the target density. Runs validation of the target. """
308
+ self._target = value
309
+
310
+ if self._target is not None:
311
+ # Create a smoothed target
312
+ self._smoothed_target = self._create_smoothed_target(value)
313
+
314
+ # Validate the target
315
+ self.validate_target()
316
+
317
+ def _create_smoothed_target(self, value):
318
+ """ Create a smoothed target using a Moreau-Yoshida envelope. """
319
+ copied_value = deepcopy(value)
320
+ if isinstance(copied_value.prior, RestorationPrior):
321
+ copied_value.prior = MoreauYoshidaPrior(
322
+ copied_value.prior,
323
+ self.smoothing_strength)
324
+ return copied_value
325
+
326
+ def validate_target(self):
327
+ # Call ULA target validation
328
+ super().validate_target()
329
+
330
+ # Additional validation for MYULA target
331
+ if isinstance(self.target.prior, MoreauYoshidaPrior):
332
+ raise ValueError(("The prior is already smoothed, apply"
333
+ " ULA when using a MoreauYoshidaPrior."))
334
+ if not hasattr(self.target.prior, "restore"):
335
+ raise NotImplementedError(
336
+ ("Using MYULA with a prior that does not have a restore method"
337
+ " is not supported.")
338
+ )
339
+
340
+ def _eval_target_grad(self, x):
341
+ return self._smoothed_target.gradient(x)
342
+
343
+ class PnPULA(MYULA):
344
+ """Plug-and-Play Unadjusted Langevin algorithm (PnP-ULA)
345
+ (Laumont et al., 2022)
346
+
347
+ Samples a smoothed target distribution given its smoothed logpdf gradient based on
348
+ Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt, where W_t is
349
+ a `dim`-dimensional standard Brownian motion.
350
+ It targets a differentiable density (partially) smoothed by a convolution
351
+ with Gaussian kernel with zero mean and smoothing_strength variance. The
352
+ smoothed target density can be made arbitrarily closed to the
353
+ true unsmoothed target density.
354
+
355
+ For more details see: Laumont, R., Bortoli, V. D., Almansa, A., Delon, J.,
356
+ Durmus, A., & Pereyra, M. (2022). Bayesian imaging using plug & play priors:
357
+ when Langevin meets Tweedie. SIAM Journal on Imaging Sciences, 15(2), 701-737.
358
+
359
+ Parameters
360
+ ----------
361
+
362
+ target : `cuqi.distribution.Distribution`
363
+ The target distribution to sample. The target distribution result from
364
+ a differentiable likelihood and prior of type RestorationPrior.
365
+
366
+ initial_point : ndarray
367
+ Initial parameters. *Optional*
368
+
369
+ scale : float
370
+ The Langevin diffusion discretization time step (In practice, a scale of
371
+ 1/L, where L is the Lipschitz of the gradient of the log target density
372
+ is recommended but not guaranteed to be the optimal choice).
373
+
374
+ smoothing_strength : float
375
+ This parameter controls the smoothing strength of PnP-ULA.
376
+
377
+
378
+ callback : callable, *Optional*
379
+ If set this function will be called after every sample.
380
+ The signature of the callback function is `callback(sample, sample_index)`,
381
+ where `sample` is the current sample and `sample_index` is the index of
382
+ the sample.
383
+ An example is shown in demos/demo31_callback.py.
384
+
385
+ # TODO: update demo once sampler merged
386
+ """
387
+ def __init__ (self, target=None, scale=1.0, smoothing_strength=0.1, **kwargs):
388
+ super().__init__(target=target, scale=scale,
389
+ smoothing_strength=smoothing_strength, **kwargs)
@@ -235,8 +235,8 @@ class RegularizedLinearRTO(LinearRTO):
235
235
 
236
236
  def step(self):
237
237
  y = self.b_tild + np.random.randn(len(self.b_tild))
238
- sim = FISTA(self.M, y, self.current_point, self.proximal,
239
- maxit = self.maxit, stepsize = self._stepsize, abstol = self.abstol, adaptive = self.adaptive)
238
+ sim = FISTA(self.M, y, self.proximal,
239
+ self.current_point, maxit = self.maxit, stepsize = self._stepsize, abstol = self.abstol, adaptive = self.adaptive)
240
240
  self.current_point, _ = sim.solve()
241
241
  acc = 1
242
242
  return acc
@@ -1,3 +1,5 @@
1
1
  from ._regularizedGaussian import RegularizedGaussian, ConstrainedGaussian, NonnegativeGaussian
2
2
  from ._regularizedGMRF import RegularizedGMRF, ConstrainedGMRF, NonnegativeGMRF
3
3
  from ._regularizedUnboundedUniform import RegularizedUnboundedUniform
4
+ from ._restorator import RestorationPrior, MoreauYoshidaPrior
5
+
@@ -0,0 +1,223 @@
1
+ from abc import ABC, abstractmethod
2
+ from cuqi.distribution import Distribution
3
+ import numpy as np
4
+
5
+ class RestorationPrior(Distribution):
6
+ """
7
+ This class defines an implicit distribution associated with a restoration operator
8
+ (eg denoiser). They are several works relating restorations operators with
9
+ priors, see
10
+ -Laumont et al. https://arxiv.org/abs/2103.04715
11
+ -Hu et al. https://openreview.net/pdf?id=x7d1qXEn1e
12
+ We cannot sample from this distribution, neither compute its logpdf except in
13
+ some cases. It allows us to apply algorithms such as MYULA and PnPULA.
14
+
15
+ Parameters
16
+ ----------
17
+ restorator : callable f(x, restoration_strength)
18
+ Function f that accepts input x to be restored and returns the
19
+ restored version of x and information about the restoration operation.
20
+
21
+ restorator_kwargs : dictionary
22
+ Dictionary containing information about the restorator.
23
+ It contains keyword argument parameters that will be passed to the
24
+ restorator f. An example could be algorithm parameters such as the number
25
+ of iterations or the stopping criteria.
26
+
27
+ potential : callable function, optional
28
+ The potential corresponds to the negative logpdf when it is accessible.
29
+ This function is a mapping from the parameter domain to the real set.
30
+ It can be provided if the user knows how to relate it to the restorator.
31
+ Ex: restorator is the proximal operator of the total variation (TV), then
32
+ potential is the TV function.
33
+ """
34
+ def __init__(self, restorator, restorator_kwargs
35
+ =None, potential=None, **kwargs):
36
+ if restorator_kwargs is None:
37
+ restorator_kwargs = {}
38
+ self.restorator = restorator
39
+ self.restorator_kwargs = restorator_kwargs
40
+ self.potential = potential
41
+ super().__init__(**kwargs)
42
+
43
+ def restore(self, x, restoration_strength):
44
+ """This function allows us to restore the input x and returns the
45
+ restored version of x.
46
+
47
+ Parameters
48
+ ----------
49
+ x : ndarray
50
+ parameter we want to restore.
51
+
52
+ restoration_strength: positive float
53
+ Strength of the restoration operation. In the case where the
54
+ restorator is a denoiser, this parameter might correspond to the
55
+ noise level.
56
+ """
57
+ solution, info = self.restorator(x, restoration_strength=restoration_strength,
58
+ **self.restorator_kwargs)
59
+ self.info = info
60
+ return solution
61
+
62
+ def logpdf(self, x):
63
+ """The logpdf function. It returns nan because we don't know the
64
+ logpdf of the implicit prior."""
65
+ if self.potential is None:
66
+ return np.nan
67
+ else:
68
+ return -self.potential(x)
69
+
70
+ def _sample(self, N, rng=None):
71
+ raise NotImplementedError("The sample method is not implemented for the"
72
+ + "RestorationPrior class.")
73
+
74
+ @property
75
+ def _mutable_vars(self):
76
+ """ Returns the mutable variables of the distribution. """
77
+ # Currently mutable variables are not supported for user-defined
78
+ # distributions.
79
+ return []
80
+
81
+ def get_conditioning_variables(self):
82
+ """ Returns the conditioning variables of the distribution. """
83
+ # Currently conditioning variables are not supported for user-defined
84
+ # distributions.
85
+ return []
86
+
87
+
88
+ class MoreauYoshidaPrior(Distribution):
89
+ """
90
+ This class defines (implicit) smoothed priors for which we can apply
91
+ gradient-based algorithms. The smoothing is performed using
92
+ the Moreau-Yoshida envelope of the target prior potential.
93
+
94
+ In the following we give a detailed explanation of the
95
+ Moreau-Yoshida smoothing.
96
+
97
+ We consider a density such that - \log\pi(x) = -g(x) with g convex, lsc,
98
+ proper but not differentiable. Consequently, we cannot apply any
99
+ algorithm requiring the gradient of g.
100
+ Idea:
101
+ We consider the Moreau envelope of g defined as
102
+
103
+ g_{smoothing_strength} (x) = inf_z 0.5*\| x-z \|_2^2/smoothing_strength + g(z).
104
+
105
+ g_{smoothing_strength} has some nice properties
106
+ - g_{smoothing_strength}(x)-->g(x) as smoothing_strength-->0 for all x
107
+ - \nabla g_{smoothing_strength} is 1/smoothing_strength-Lipschitz
108
+ - \nabla g_{smoothing_strength}(x) = (x - prox_g^{smoothing_strength}(x))/smoothing_strength for all x with
109
+
110
+ prox_g^{smoothing_strength}(x) = argmin_z 0.5*\| x-z \|_2^2/smoothing_strength + g(z) .
111
+
112
+ Consequently, we can apply any gradient-based algorithm with
113
+ g_{smoothing_strength} in lieu of g. These algorithms do not require the
114
+ full knowledge of g_{smoothing_strength} but only its gradient. The gradient
115
+ of g_{smoothing_strength} is fully determined by prox_g^{smoothing_strength}
116
+ and smoothing_strength.
117
+ It is important as, although there exists an explicit formula for
118
+ g_{smoothing_strength}, it is rarely used in practice, as it would require
119
+ us to solve an optimization problem each time we want to
120
+ estimate g_{smoothing_strength}. Furthermore, there exist cases where we dont't
121
+ the regularization g with which the mapping prox_g^{smoothing_strength} is
122
+ associated.
123
+
124
+ Remark (Proximal operators are denoisers):
125
+ We consider the denoising inverse problem x = u + n, with
126
+ n \sim \mathcal{N}(0, smoothing_strength I).
127
+ A mapping solving a denoising inverse problem is called denoiser. It takes
128
+ the noisy observation x as an input and returns a less noisy version of x
129
+ which is an estimate of u.
130
+ We assume a prior density \pi(u) \propto exp(- g(u)).
131
+ Then the MAP estimate is given by
132
+ x_MAP = \argmin_z 0.5 \| x - z \|_2^2/smoothing_strength + g(z) = prox_g^smoothing_strength(x)
133
+ Then proximal operators are denoisers.
134
+
135
+ Remark (Denoisers are not necessarily proximal operators): Data-driven
136
+ denoisers are not necessarily proximal operators
137
+ (see https://arxiv.org/pdf/2201.13256)
138
+
139
+ Parameters
140
+ ----------
141
+ prior : RestorationPrior
142
+ Prior of the RestorationPrior type. In order to stay within the MYULA
143
+ framework the restorator of RestorationPrior must be a proximal operator.
144
+
145
+ smoothing_strength : float
146
+ Smoothing strength of the Moreau-Yoshida envelope of the prior potential.
147
+ """
148
+
149
+ def __init__(self, prior:RestorationPrior, smoothing_strength=0.1,
150
+ **kwargs):
151
+ self.prior = prior
152
+ self.smoothing_strength = smoothing_strength
153
+
154
+ # if kwargs does not contain the geometry,
155
+ # we set it to the geometry of the prior, if it exists
156
+ if "geometry" in kwargs:
157
+ raise ValueError(
158
+ "The geometry parameter is not supported for the"
159
+ + "MoreauYoshidaPrior class. The geometry is"
160
+ + "automatically set to the geometry of the prior.")
161
+ try:
162
+ geometry = prior.geometry
163
+ except:
164
+ geometry = None
165
+
166
+ super().__init__(geometry=geometry, **kwargs)
167
+
168
+ @property
169
+ def geometry(self):
170
+ return self.prior.geometry
171
+
172
+ @geometry.setter
173
+ def geometry(self, value):
174
+ self.prior.geometry = value
175
+
176
+ @property
177
+ def smoothing_strength(self):
178
+ """ smoothing_strength of the distribution"""
179
+ return self._smoothing_strength
180
+
181
+ @smoothing_strength.setter
182
+ def smoothing_strength(self, value):
183
+ self._smoothing_strength = value
184
+
185
+ @property
186
+ def prior(self):
187
+ """Getter for the MoreauYoshida prior."""
188
+ return self._prior
189
+
190
+ @prior.setter
191
+ def prior(self, value):
192
+ self._prior = value
193
+
194
+ def gradient(self, x):
195
+ """This is the gradient of the regularizer ie gradient of the negative
196
+ logpdf of the implicit prior."""
197
+ return -(x - self.prior.restore(x, self.smoothing_strength))/self.smoothing_strength
198
+
199
+ def logpdf(self, x):
200
+ """The logpdf function. It returns nan because we don't know the
201
+ logpdf of the implicit prior."""
202
+ if self.prior.potential == None:
203
+ return np.nan
204
+ else:
205
+ return -(self.prior.potential(self.prior.restore(x, self.smoothing_strength))*self.smoothing_strength +
206
+ 0.5*((x-self.prior.restore(x, self.smoothing_strength))**2).sum())
207
+
208
+ def _sample(self, N, rng=None):
209
+ raise NotImplementedError("The sample method is not implemented for the"
210
+ + f"{self.__class__.__name__} class.")
211
+
212
+ @property
213
+ def _mutable_vars(self):
214
+ """ Returns the mutable variables of the distribution. """
215
+ # Currently mutable variables are not supported for user-defined
216
+ # distributions.
217
+ return []
218
+
219
+ def get_conditioning_variables(self):
220
+ """ Returns the conditioning variables of the distribution. """
221
+ # Currently conditioning variables are not supported for user-defined
222
+ # distributions.
223
+ return []
cuqi/sampler/_rto.py CHANGED
@@ -267,8 +267,8 @@ class RegularizedLinearRTO(LinearRTO):
267
267
  samples[:, 0] = self.x0
268
268
  for s in range(Ns-1):
269
269
  y = self.b_tild + np.random.randn(len(self.b_tild))
270
- sim = FISTA(self.M, y, samples[:, s], self.proximal,
271
- maxit = self.maxit, stepsize = _stepsize, abstol = self.abstol, adaptive = self.adaptive)
270
+ sim = FISTA(self.M, y, self.proximal,
271
+ samples[:, s], maxit = self.maxit, stepsize = _stepsize, abstol = self.abstol, adaptive = self.adaptive)
272
272
  samples[:, s+1], _ = sim.solve()
273
273
 
274
274
  self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
cuqi/solver/__init__.py CHANGED
@@ -7,6 +7,7 @@ from ._solver import (
7
7
  LM,
8
8
  PDHG,
9
9
  FISTA,
10
+ ADMM,
10
11
  ProjectNonnegative,
11
12
  ProjectBox,
12
13
  ProximalL1
cuqi/solver/_solver.py CHANGED
@@ -584,8 +584,8 @@ class FISTA(object):
584
584
  ----------
585
585
  A : ndarray or callable f(x,*args).
586
586
  b : ndarray.
587
- x0 : ndarray. Initial guess.
588
587
  proximal : callable f(x, gamma) for proximal mapping.
588
+ x0 : ndarray. Initial guess.
589
589
  maxit : The maximum number of iterations.
590
590
  stepsize : The stepsize of the gradient step.
591
591
  abstol : The numerical tolerance for convergence checks.
@@ -606,11 +606,11 @@ class FISTA(object):
606
606
  b = rng.standard_normal(m)
607
607
  stepsize = 0.99/(sp.linalg.interpolative.estimate_spectral_norm(A)**2)
608
608
  x0 = np.zeros(n)
609
- fista = FISTA(A, b, x0, proximal = ProximalL1, stepsize = stepsize, maxit = 100, abstol=1e-12, adaptive = True)
609
+ fista = FISTA(A, b, proximal = ProximalL1, x0, stepsize = stepsize, maxit = 100, abstol=1e-12, adaptive = True)
610
610
  sol, _ = fista.solve()
611
611
 
612
612
  """
613
- def __init__(self, A, b, x0, proximal, maxit=100, stepsize=1e0, abstol=1e-14, adaptive = True):
613
+ def __init__(self, A, b, proximal, x0, maxit=100, stepsize=1e0, abstol=1e-14, adaptive = True):
614
614
 
615
615
  self.A = A
616
616
  self.b = b
@@ -650,8 +650,157 @@ class FISTA(object):
650
650
  x_new = x_new + ((k-1)/(k+2))*(x_new - x_old)
651
651
 
652
652
  x = x_new.copy()
653
+
654
+ class ADMM(object):
655
+ """Alternating Direction Method of Multipliers for solving regularized linear least squares problems of the form:
656
+ Minimize ||Ax-b||^2 + sum_i f_i(L_i x),
657
+ where the sum ranges from 1 to an arbitrary n. See definition of the parameter `penalty_terms` below for more details about f_i and L_i
658
+
659
+ Reference:
660
+ [1] Boyd et al. "Distributed optimization and statistical learning via the alternating direction method of multipliers."Foundations and Trends® in Machine learning, 2011.
661
+
662
+
663
+ Parameters
664
+ ----------
665
+ A : ndarray or callable
666
+ Represents a matrix or a function that performs matrix-vector multiplications.
667
+ When A is a callable, it accepts arguments (x, flag) where:
668
+ - flag=1 indicates multiplication of A with vector x, that is A @ x.
669
+ - flag=2 indicates multiplication of the transpose of A with vector x, that is A.T @ x.
670
+ b : ndarray.
671
+ penalty_terms : List of tuples (callable proximal operator of f_i, linear operator L_i)
672
+ Each callable proximal operator f_i accepts two arguments (x, p) and should return the minimizer of p/2||x-z||^2 + f(x) over z for some f.
673
+ x0 : ndarray. Initial guess.
674
+ penalty_parameter : Trade-off between linear least squares and regularization term in the solver iterates. Denoted as "rho" in [1].
675
+ maxit : The maximum number of iterations.
676
+ adaptive : Whether to adaptively update the penalty_parameter each iteration such that the primal and dual residual norms are of the same order of magnitude. Based on [1], Subsection 3.4.1
677
+
678
+ Example
679
+ -----------
680
+ .. code-block:: python
653
681
 
682
+ from cuqi.solver import ADMM, ProximalL1, ProjectNonnegative
683
+ import numpy as np
684
+
685
+ rng = np.random.default_rng()
686
+
687
+ m, n, k = 10, 5, 4
688
+ A = rng.standard_normal((m, n))
689
+ b = rng.standard_normal(m)
690
+ L = rng.standard_normal((k, n))
691
+
692
+ x0 = np.zeros(n)
693
+ admm = ADMM(A, b, x0, penalty_terms = [(ProximalL1, L), (lambda z, _ : ProjectNonnegative(z), np.eye(n))], tradeoff = 10)
694
+ sol, _ = admm.solve()
695
+
696
+ """
697
+
698
+ def __init__(self, A, b, penalty_terms, x0, penalty_parameter = 10, maxit = 100, inner_max_it = 10, adaptive = True):
699
+
700
+ self.A = A
701
+ self.b = b
702
+ self.x_cur = x0
703
+
704
+ dual_len = [penalty[1].shape[0] for penalty in penalty_terms]
705
+ self.z_cur = [np.zeros(l) for l in dual_len]
706
+ self.u_cur = [np.zeros(l) for l in dual_len]
707
+ self.n = penalty_terms[0][1].shape[1]
708
+
709
+ self.rho = penalty_parameter
710
+ self.maxit = maxit
711
+ self.inner_max_it = inner_max_it
712
+ self.adaptive = adaptive
713
+
714
+ self.penalty_terms = penalty_terms
715
+
716
+ self.p = len(self.penalty_terms)
717
+ self._big_matrix = None
718
+ self._big_vector = None
719
+
720
+ def solve(self):
721
+ """
722
+ Solves the regularized linear least squares problem using ADMM in scaled form. Based on [1], Subsection 3.1.1
723
+ """
724
+ z_new = self.p*[0]
725
+ u_new = self.p*[0]
726
+
727
+ # Iterating
728
+ for i in range(self.maxit):
729
+ self._iteration_pre_processing()
730
+
731
+ # Main update (Least Squares)
732
+ solver = CGLS(self._big_matrix, self._big_vector, self.x_cur, self.inner_max_it)
733
+ x_new, _ = solver.solve()
734
+
735
+ # Regularization update
736
+ for j, penalty in enumerate(self.penalty_terms):
737
+ z_new[j] = penalty[0](penalty[1]@x_new + self.u_cur[j], 1.0/self.rho)
738
+
739
+ res_primal = 0.0
740
+ # Dual update
741
+ for j, penalty in enumerate(self.penalty_terms):
742
+ r_partial = penalty[1]@x_new - z_new[j]
743
+ res_primal += LA.norm(r_partial)**2
744
+
745
+ u_new[j] = self.u_cur[j] + r_partial
746
+
747
+ res_dual = 0.0
748
+ for j, penalty in enumerate(self.penalty_terms):
749
+ res_dual += LA.norm(penalty[1].T@(z_new[j] - self.z_cur[j]))**2
750
+
751
+ # Adaptive approach based on [1], Subsection 3.4.1
752
+ if self.adaptive:
753
+ if res_dual > 1e2*res_primal:
754
+ self.rho *= 0.5 # More regularization
755
+ elif res_primal > 1e2*res_dual:
756
+ self.rho *= 2.0 # More data fidelity
757
+
758
+ self.x_cur, self.z_cur, self.u_cur = x_new, z_new.copy(), u_new
759
+
760
+ return self.x_cur, i
654
761
 
762
+ def _iteration_pre_processing(self):
763
+ """ Preprocessing
764
+ Every iteration of ADMM requires solving a linear least squares system of the form
765
+ minimize 1/(rho) \|Ax-b\|_2^2 + sum_{i=1}^{p} \|penalty[1]x - (y - u)\|_2^2
766
+ To solve this, all linear least squares terms are combined into a single big term
767
+ with matrix big_matrix and data big_vector.
768
+
769
+ The matrix only needs to be updated when rho changes, i.e., when the adaptive option is used.
770
+ The data vector needs to be updated every iteration.
771
+ """
772
+
773
+ self._big_vector = np.hstack([np.sqrt(1/self.rho)*self.b] + [self.z_cur[i] - self.u_cur[i] for i in range(self.p)])
774
+
775
+ # Check whether matrix needs to be updated
776
+ if self._big_matrix is not None and not self.adaptive:
777
+ return
778
+
779
+ # Update big_matrix
780
+ if callable(self.A):
781
+ def matrix_eval(x, flag):
782
+ if flag == 1:
783
+ out1 = np.sqrt(1/self.rho)*self.A(x, 1)
784
+ out2 = [penalty[1]@x for penalty in self.penalty_terms]
785
+ out = np.hstack([out1] + out2)
786
+ elif flag == 2:
787
+ idx_start = len(x)
788
+ idx_end = len(x)
789
+ out1 = np.zeros(self.n)
790
+ for _, t in reversed(self.penalty_terms):
791
+ idx_start -= t.shape[0]
792
+ out1 += t.T@x[idx_start:idx_end]
793
+ idx_end = idx_start
794
+ out2 = np.sqrt(1/self.rho)*self.A(x[:idx_end], 2)
795
+ out = out1 + out2
796
+ return out
797
+ self._big_matrix = matrix_eval
798
+ else:
799
+ self._big_matrix = np.vstack([np.sqrt(1/self.rho)*self.A] + [penalty[1] for penalty in self.penalty_terms])
800
+
801
+
802
+
803
+
655
804
  def ProjectNonnegative(x):
656
805
  """(Euclidean) projection onto the nonnegative orthant.
657
806
 
@@ -678,6 +827,22 @@ def ProjectBox(x, lower = None, upper = None):
678
827
 
679
828
  return np.minimum(np.maximum(x, lower), upper)
680
829
 
830
+ def ProjectHalfspace(x, a, b):
831
+ """(Euclidean) projection onto the halfspace defined {z|<a,z> <= b}.
832
+
833
+ Parameters
834
+ ----------
835
+ x : array_like.
836
+ a : array_like.
837
+ b : array_like.
838
+ """
839
+
840
+ ax_b = np.inner(a,x) - b
841
+ if ax_b <= 0:
842
+ return x
843
+ else:
844
+ return x - (ax_b/np.inner(a,a))*a
845
+
681
846
  def ProximalL1(x, gamma):
682
847
  """(Euclidean) proximal operator of the \|x\|_1 norm.
683
848
  Also known as the shrinkage or soft thresholding operator.
@@ -687,4 +852,4 @@ def ProximalL1(x, gamma):
687
852
  x : array_like.
688
853
  gamma : scale parameter.
689
854
  """
690
- return np.multiply(np.sign(x), np.maximum(np.abs(x)-gamma, 0))
855
+ return np.multiply(np.sign(x), np.maximum(np.abs(x)-gamma, 0))