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.
- cuqi/__init__.py +2 -0
- cuqi/_version.py +3 -3
- cuqi/algebra/__init__.py +2 -0
- cuqi/algebra/_abstract_syntax_tree.py +358 -0
- cuqi/algebra/_ordered_set.py +82 -0
- cuqi/algebra/_random_variable.py +457 -0
- cuqi/array/_array.py +4 -13
- cuqi/config.py +7 -0
- cuqi/density/_density.py +9 -1
- cuqi/distribution/__init__.py +3 -2
- cuqi/distribution/_beta.py +7 -11
- cuqi/distribution/_cauchy.py +2 -2
- cuqi/distribution/_custom.py +0 -6
- cuqi/distribution/_distribution.py +31 -45
- cuqi/distribution/_gamma.py +7 -3
- cuqi/distribution/_gaussian.py +2 -12
- cuqi/distribution/_inverse_gamma.py +4 -10
- cuqi/distribution/_joint_distribution.py +112 -15
- cuqi/distribution/_lognormal.py +0 -7
- cuqi/distribution/{_modifiedhalfnormal.py → _modified_half_normal.py} +23 -23
- cuqi/distribution/_normal.py +34 -7
- cuqi/distribution/_posterior.py +9 -0
- cuqi/distribution/_truncated_normal.py +129 -0
- cuqi/distribution/_uniform.py +47 -1
- cuqi/experimental/__init__.py +2 -2
- cuqi/experimental/_recommender.py +216 -0
- cuqi/geometry/__init__.py +2 -0
- cuqi/geometry/_geometry.py +15 -1
- cuqi/geometry/_product_geometry.py +181 -0
- cuqi/implicitprior/__init__.py +5 -3
- cuqi/implicitprior/_regularized_gaussian.py +483 -0
- cuqi/implicitprior/{_regularizedGMRF.py → _regularized_gmrf.py} +4 -2
- cuqi/implicitprior/{_regularizedUnboundedUniform.py → _regularized_unbounded_uniform.py} +3 -2
- cuqi/implicitprior/_restorator.py +269 -0
- cuqi/legacy/__init__.py +2 -0
- cuqi/{experimental/mcmc → legacy/sampler}/__init__.py +7 -11
- cuqi/legacy/sampler/_conjugate.py +55 -0
- cuqi/legacy/sampler/_conjugate_approx.py +52 -0
- cuqi/legacy/sampler/_cwmh.py +196 -0
- cuqi/legacy/sampler/_gibbs.py +231 -0
- cuqi/legacy/sampler/_hmc.py +335 -0
- cuqi/{experimental/mcmc → legacy/sampler}/_langevin_algorithm.py +82 -111
- cuqi/legacy/sampler/_laplace_approximation.py +184 -0
- cuqi/legacy/sampler/_mh.py +190 -0
- cuqi/legacy/sampler/_pcn.py +244 -0
- cuqi/{experimental/mcmc → legacy/sampler}/_rto.py +132 -90
- cuqi/legacy/sampler/_sampler.py +182 -0
- cuqi/likelihood/_likelihood.py +9 -1
- cuqi/model/__init__.py +1 -1
- cuqi/model/_model.py +1361 -359
- cuqi/pde/__init__.py +4 -0
- cuqi/pde/_observation_map.py +36 -0
- cuqi/pde/_pde.py +134 -33
- cuqi/problem/_problem.py +93 -87
- cuqi/sampler/__init__.py +120 -8
- cuqi/sampler/_conjugate.py +376 -35
- cuqi/sampler/_conjugate_approx.py +40 -16
- cuqi/sampler/_cwmh.py +132 -138
- cuqi/{experimental/mcmc → sampler}/_direct.py +1 -1
- cuqi/sampler/_gibbs.py +288 -130
- cuqi/sampler/_hmc.py +328 -201
- cuqi/sampler/_langevin_algorithm.py +284 -100
- cuqi/sampler/_laplace_approximation.py +87 -117
- cuqi/sampler/_mh.py +47 -157
- cuqi/sampler/_pcn.py +65 -213
- cuqi/sampler/_rto.py +211 -142
- cuqi/sampler/_sampler.py +553 -136
- cuqi/samples/__init__.py +1 -1
- cuqi/samples/_samples.py +24 -18
- cuqi/solver/__init__.py +6 -4
- cuqi/solver/_solver.py +230 -26
- cuqi/testproblem/_testproblem.py +2 -3
- cuqi/utilities/__init__.py +6 -1
- cuqi/utilities/_get_python_variable_name.py +2 -2
- cuqi/utilities/_utilities.py +182 -2
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/METADATA +10 -6
- cuqipy-1.4.1.post0.dev124.dist-info/RECORD +101 -0
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/WHEEL +1 -1
- CUQIpy-1.1.1.post0.dev36.dist-info/RECORD +0 -92
- cuqi/experimental/mcmc/_conjugate.py +0 -197
- cuqi/experimental/mcmc/_conjugate_approx.py +0 -81
- cuqi/experimental/mcmc/_cwmh.py +0 -191
- cuqi/experimental/mcmc/_gibbs.py +0 -268
- cuqi/experimental/mcmc/_hmc.py +0 -470
- cuqi/experimental/mcmc/_laplace_approximation.py +0 -156
- cuqi/experimental/mcmc/_mh.py +0 -78
- cuqi/experimental/mcmc/_pcn.py +0 -89
- cuqi/experimental/mcmc/_sampler.py +0 -561
- cuqi/experimental/mcmc/_utilities.py +0 -17
- cuqi/implicitprior/_regularizedGaussian.py +0 -323
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info/licenses}/LICENSE +0 -0
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/top_level.txt +0 -0
cuqi/samples/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from ._samples import Samples
|
|
1
|
+
from ._samples import Samples, JointSamples
|
cuqi/samples/_samples.py
CHANGED
|
@@ -36,24 +36,6 @@ class Samples(object):
|
|
|
36
36
|
geometry : cuqi.geometry.Geometry, default None
|
|
37
37
|
Contains the geometry related of the samples
|
|
38
38
|
|
|
39
|
-
Attributes
|
|
40
|
-
----------
|
|
41
|
-
shape : tuple
|
|
42
|
-
Returns the shape of samples.
|
|
43
|
-
|
|
44
|
-
Ns : int
|
|
45
|
-
Returns the number of samples
|
|
46
|
-
|
|
47
|
-
Methods
|
|
48
|
-
----------
|
|
49
|
-
:meth:`plot`: Plots one or more samples.
|
|
50
|
-
:meth:`plot_ci`: Plots a credibility interval for the samples.
|
|
51
|
-
:meth:`plot_mean`: Plots the mean of the samples.
|
|
52
|
-
:meth:`plot_std`: Plots the std of the samples.
|
|
53
|
-
:meth:`plot_chain`: Plots all samples of one or more variables (MCMC chain).
|
|
54
|
-
:meth:`hist_chain`: Plots histogram of all samples of a single variable (MCMC chain).
|
|
55
|
-
:meth:`burnthin`: Removes burn-in and thins samples.
|
|
56
|
-
:meth:`diagnostics`: Conducts diagnostics on the chain.
|
|
57
39
|
"""
|
|
58
40
|
def __init__(self, samples, geometry=None, is_par=True, is_vec=True):
|
|
59
41
|
self.geometry = geometry
|
|
@@ -83,6 +65,7 @@ class Samples(object):
|
|
|
83
65
|
|
|
84
66
|
@property
|
|
85
67
|
def shape(self):
|
|
68
|
+
"""Returns the shape of samples."""
|
|
86
69
|
return self.samples.shape
|
|
87
70
|
|
|
88
71
|
@property
|
|
@@ -408,6 +391,7 @@ class Samples(object):
|
|
|
408
391
|
return ax
|
|
409
392
|
|
|
410
393
|
def plot(self,sample_indices=None,*args,**kwargs):
|
|
394
|
+
""" Plots one or more samples. """
|
|
411
395
|
Ns = self.Ns
|
|
412
396
|
Np = 5 # Number of samples to plot if Ns > 5
|
|
413
397
|
|
|
@@ -447,6 +431,7 @@ class Samples(object):
|
|
|
447
431
|
return lines
|
|
448
432
|
|
|
449
433
|
def hist_chain(self,variable_indices,*args,**kwargs):
|
|
434
|
+
""" Plots samples histogram of variables with indices specified in variable_indices. """
|
|
450
435
|
|
|
451
436
|
self._raise_error_if_not_vec(self.hist_chain.__name__)
|
|
452
437
|
|
|
@@ -580,6 +565,7 @@ class Samples(object):
|
|
|
580
565
|
|
|
581
566
|
|
|
582
567
|
def diagnostics(self):
|
|
568
|
+
""" Conducts diagnostics on the chain (Geweke test). """
|
|
583
569
|
# Geweke test
|
|
584
570
|
Geweke(self.samples.T)
|
|
585
571
|
|
|
@@ -881,3 +867,23 @@ class Samples(object):
|
|
|
881
867
|
"Geometry:\n {}\n\n".format(self.geometry) + \
|
|
882
868
|
"Shape:\n {}\n\n".format(self.shape) + \
|
|
883
869
|
"Samples:\n {}\n\n".format(self.samples)
|
|
870
|
+
|
|
871
|
+
class JointSamples(dict):
|
|
872
|
+
""" An object used to store samples from :class:`cuqi.distribution.JointDistribution`.
|
|
873
|
+
|
|
874
|
+
This object is a simple overload of the dictionary class to allow easy access to certain methods
|
|
875
|
+
of Samples objects without having to iterate over each key in the dictionary.
|
|
876
|
+
|
|
877
|
+
"""
|
|
878
|
+
|
|
879
|
+
def burnthin(self, Nb, Nt=1):
|
|
880
|
+
""" Remove burn-in and thin samples for all samples in the dictionary. Returns a copy of the samples stored in the dictionary. """
|
|
881
|
+
return JointSamples({key: samples.burnthin(Nb, Nt) for key, samples in self.items()})
|
|
882
|
+
|
|
883
|
+
def __repr__(self) -> str:
|
|
884
|
+
return "CUQIpy JointSamples Dict:\n" + \
|
|
885
|
+
"-------------------------\n\n" + \
|
|
886
|
+
"Keys:\n {}\n\n".format(list(self.keys())) + \
|
|
887
|
+
"Ns (number of samples):\n {}\n\n".format({key: samples.Ns for key, samples in self.items()}) + \
|
|
888
|
+
"Geometry:\n {}\n\n".format({key: samples.geometry for key, samples in self.items()}) + \
|
|
889
|
+
"Shape:\n {}\n\n".format({key: samples.shape for key, samples in self.items()})
|
cuqi/solver/__init__.py
CHANGED
cuqi/solver/_solver.py
CHANGED
|
@@ -15,7 +15,7 @@ except ImportError:
|
|
|
15
15
|
has_cholmod = False
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
18
|
+
class ScipyLBFGSB(object):
|
|
19
19
|
"""Wrapper for :meth:`scipy.optimize.fmin_l_bfgs_b`.
|
|
20
20
|
|
|
21
21
|
Minimize a function func using the L-BFGS-B algorithm.
|
|
@@ -30,14 +30,10 @@ class L_BFGS_B(object):
|
|
|
30
30
|
Initial guess.
|
|
31
31
|
gradfunc : callable f(x,*args), optional
|
|
32
32
|
The gradient of func.
|
|
33
|
-
If None,
|
|
33
|
+
If None, the solver approximates the gradient with a finite difference scheme.
|
|
34
34
|
kwargs : keyword arguments passed to scipy's L-BFGS-B algorithm. See documentation for scipy.optimize.minimize
|
|
35
|
-
|
|
36
|
-
Methods
|
|
37
|
-
----------
|
|
38
|
-
:meth:`solve`: Runs the solver and returns the solution and info about the optimization.
|
|
39
35
|
"""
|
|
40
|
-
def __init__(self,func,x0, gradfunc = None, **kwargs):
|
|
36
|
+
def __init__(self, func, x0, gradfunc = None, **kwargs):
|
|
41
37
|
self.func= func
|
|
42
38
|
self.x0 = x0
|
|
43
39
|
self.gradfunc = gradfunc
|
|
@@ -83,7 +79,7 @@ class L_BFGS_B(object):
|
|
|
83
79
|
"nfev": solution[2]['funcalls']}
|
|
84
80
|
return solution[0], info
|
|
85
81
|
|
|
86
|
-
class
|
|
82
|
+
class ScipyMinimizer(object):
|
|
87
83
|
"""Wrapper for :meth:`scipy.optimize.minimize`.
|
|
88
84
|
|
|
89
85
|
Minimize a function func using scipy's optimize.minimize module.
|
|
@@ -115,12 +111,8 @@ class minimize(object):
|
|
|
115
111
|
‘trust-krylov’
|
|
116
112
|
If not given, chosen to be one of BFGS, L-BFGS-B, SLSQP, depending if the problem has constraints or bounds.
|
|
117
113
|
kwargs : keyword arguments passed to scipy's minimizer. See documentation for scipy.optimize.minimize
|
|
118
|
-
|
|
119
|
-
Methods
|
|
120
|
-
----------
|
|
121
|
-
:meth:`solve`: Runs the solver and returns the solution and info about the optimization.
|
|
122
114
|
"""
|
|
123
|
-
def __init__(self,func,x0, gradfunc =
|
|
115
|
+
def __init__(self, func, x0, gradfunc = '2-point', method = None, **kwargs):
|
|
124
116
|
self.func= func
|
|
125
117
|
self.x0 = x0
|
|
126
118
|
self.method = method
|
|
@@ -147,18 +139,20 @@ class minimize(object):
|
|
|
147
139
|
info = {"success": solution['success'],
|
|
148
140
|
"message": solution['message'],
|
|
149
141
|
"func": solution['fun'],
|
|
150
|
-
"grad": solution['jac'],
|
|
151
142
|
"nit": solution['nit'],
|
|
152
143
|
"nfev": solution['nfev']}
|
|
144
|
+
# if gradfunc is callable, record the gradient in the info dict
|
|
145
|
+
if 'jac' in solution.keys():
|
|
146
|
+
info['grad'] = solution['jac']
|
|
153
147
|
if isinstance(self.x0,CUQIarray):
|
|
154
148
|
sol = CUQIarray(solution['x'],geometry=self.x0.geometry)
|
|
155
149
|
else:
|
|
156
150
|
sol = solution['x']
|
|
157
151
|
return sol, info
|
|
158
152
|
|
|
159
|
-
class
|
|
160
|
-
"""Simply calls ::class:: cuqi.solver.
|
|
161
|
-
def __init__(self,func,x0, gradfunc = None, method = None, **kwargs):
|
|
153
|
+
class ScipyMaximizer(ScipyMinimizer):
|
|
154
|
+
"""Simply calls ::class:: cuqi.solver.ScipyMinimizer with -func."""
|
|
155
|
+
def __init__(self, func, x0, gradfunc = None, method = None, **kwargs):
|
|
162
156
|
def nfunc(*args,**kwargs):
|
|
163
157
|
return -func(*args,**kwargs)
|
|
164
158
|
if gradfunc is not None:
|
|
@@ -170,13 +164,16 @@ class maximize(minimize):
|
|
|
170
164
|
|
|
171
165
|
|
|
172
166
|
|
|
173
|
-
class
|
|
167
|
+
class ScipyLSQ(object):
|
|
174
168
|
"""Wrapper for :meth:`scipy.optimize.least_squares`.
|
|
175
169
|
|
|
176
170
|
Solve nonlinear least-squares problems with bounds:
|
|
171
|
+
|
|
172
|
+
.. math::
|
|
177
173
|
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
\min F(x) = 0.5 * \sum(\\rho(f_i(x)^2), i = 0, ..., m-1)
|
|
175
|
+
|
|
176
|
+
subject to :math:`lb <= x <= ub`.
|
|
180
177
|
|
|
181
178
|
Parameters
|
|
182
179
|
----------
|
|
@@ -186,7 +183,7 @@ class LS(object):
|
|
|
186
183
|
Initial guess.
|
|
187
184
|
Jac : callable f(x,*args), optional
|
|
188
185
|
The Jacobian of func.
|
|
189
|
-
If
|
|
186
|
+
If not specified, the solver approximates the Jacobian with a finite difference scheme.
|
|
190
187
|
loss: callable rho(x,*args)
|
|
191
188
|
Determines the loss function
|
|
192
189
|
'linear' : rho(z) = z. Gives a standard least-squares problem.
|
|
@@ -199,8 +196,11 @@ class LS(object):
|
|
|
199
196
|
'trf', Trust Region Reflective algorithm: for large sparse problems with bounds.
|
|
200
197
|
'dogbox', dogleg algorithm with rectangular trust regions, for small problems with bounds.
|
|
201
198
|
'lm', Levenberg-Marquardt algorithm as implemented in MINPACK. Doesn't handle bounds and sparse Jacobians.
|
|
199
|
+
tol : The numerical tolerance for convergence checks.
|
|
200
|
+
maxit : The maximum number of iterations.
|
|
201
|
+
kwargs : Additional keyword arguments passed to scipy's least_squares. Empty by default. See documentation for scipy.optimize.least_squares
|
|
202
202
|
"""
|
|
203
|
-
def __init__(self, func, x0, jacfun=
|
|
203
|
+
def __init__(self, func, x0, jacfun='2-point', method='trf', loss='linear', tol=1e-6, maxit=1e4, **kwargs):
|
|
204
204
|
self.func = func
|
|
205
205
|
self.x0 = x0
|
|
206
206
|
self.jacfun = jacfun
|
|
@@ -208,6 +208,7 @@ class LS(object):
|
|
|
208
208
|
self.loss = loss
|
|
209
209
|
self.tol = tol
|
|
210
210
|
self.maxit = int(maxit)
|
|
211
|
+
self.kwargs = kwargs
|
|
211
212
|
|
|
212
213
|
def solve(self):
|
|
213
214
|
"""Runs optimization algorithm and returns solution and info.
|
|
@@ -218,7 +219,7 @@ class LS(object):
|
|
|
218
219
|
Solution found (array_like) and optimization information (dictionary).
|
|
219
220
|
"""
|
|
220
221
|
solution = least_squares(self.func, self.x0, jac=self.jacfun, \
|
|
221
|
-
method=self.method, loss=self.loss, xtol=self.tol, max_nfev=self.maxit)
|
|
222
|
+
method=self.method, loss=self.loss, xtol=self.tol, max_nfev=self.maxit, **self.kwargs)
|
|
222
223
|
info = {"success": solution['success'],
|
|
223
224
|
"message": solution['message'],
|
|
224
225
|
"func": solution['fun'],
|
|
@@ -230,6 +231,44 @@ class LS(object):
|
|
|
230
231
|
sol = solution['x']
|
|
231
232
|
return sol, info
|
|
232
233
|
|
|
234
|
+
class ScipyLinearLSQ(object):
|
|
235
|
+
"""Wrapper for :meth:`scipy.optimize.lsq_linear`.
|
|
236
|
+
|
|
237
|
+
Solve linear least-squares problems with bounds:
|
|
238
|
+
|
|
239
|
+
.. math::
|
|
240
|
+
|
|
241
|
+
\min \|A x - b\|_2^2
|
|
242
|
+
|
|
243
|
+
subject to :math:`lb <= x <= ub`.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
A : ndarray, LinearOperator
|
|
248
|
+
Design matrix (system matrix).
|
|
249
|
+
b : ndarray
|
|
250
|
+
The right-hand side of the linear system.
|
|
251
|
+
bounds : 2-tuple of array_like or scipy.optimize Bounds
|
|
252
|
+
Bounds for variables.
|
|
253
|
+
kwargs : Other keyword arguments passed to Scipy's `lsq_linear`. See documentation of `scipy.optimize.lsq_linear` for details.
|
|
254
|
+
"""
|
|
255
|
+
def __init__(self, A, b, bounds=(-np.inf, np.inf), **kwargs):
|
|
256
|
+
self.A = A
|
|
257
|
+
self.b = b
|
|
258
|
+
self.bounds = bounds
|
|
259
|
+
self.kwargs = kwargs
|
|
260
|
+
|
|
261
|
+
def solve(self):
|
|
262
|
+
"""Runs optimization algorithm and returns solution and optimization information.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
----------
|
|
266
|
+
solution : Tuple
|
|
267
|
+
Solution found (array_like) and optimization information (dictionary).
|
|
268
|
+
"""
|
|
269
|
+
res = opt.lsq_linear(self.A, self.b, bounds=self.bounds, **self.kwargs)
|
|
270
|
+
x = res.pop('x')
|
|
271
|
+
return x, res
|
|
233
272
|
|
|
234
273
|
|
|
235
274
|
class CGLS(object):
|
|
@@ -581,8 +620,8 @@ class FISTA(object):
|
|
|
581
620
|
----------
|
|
582
621
|
A : ndarray or callable f(x,*args).
|
|
583
622
|
b : ndarray.
|
|
584
|
-
x0 : ndarray. Initial guess.
|
|
585
623
|
proximal : callable f(x, gamma) for proximal mapping.
|
|
624
|
+
x0 : ndarray. Initial guess.
|
|
586
625
|
maxit : The maximum number of iterations.
|
|
587
626
|
stepsize : The stepsize of the gradient step.
|
|
588
627
|
abstol : The numerical tolerance for convergence checks.
|
|
@@ -603,11 +642,11 @@ class FISTA(object):
|
|
|
603
642
|
b = rng.standard_normal(m)
|
|
604
643
|
stepsize = 0.99/(sp.linalg.interpolative.estimate_spectral_norm(A)**2)
|
|
605
644
|
x0 = np.zeros(n)
|
|
606
|
-
fista = FISTA(A, b,
|
|
645
|
+
fista = FISTA(A, b, proximal = ProximalL1, x0, stepsize = stepsize, maxit = 100, abstol=1e-12, adaptive = True)
|
|
607
646
|
sol, _ = fista.solve()
|
|
608
647
|
|
|
609
648
|
"""
|
|
610
|
-
def __init__(self, A, b,
|
|
649
|
+
def __init__(self, A, b, proximal, x0, maxit=100, stepsize=1e0, abstol=1e-14, adaptive = True):
|
|
611
650
|
|
|
612
651
|
self.A = A
|
|
613
652
|
self.b = b
|
|
@@ -647,8 +686,157 @@ class FISTA(object):
|
|
|
647
686
|
x_new = x_new + ((k-1)/(k+2))*(x_new - x_old)
|
|
648
687
|
|
|
649
688
|
x = x_new.copy()
|
|
689
|
+
|
|
690
|
+
class ADMM(object):
|
|
691
|
+
"""Alternating Direction Method of Multipliers for solving regularized linear least squares problems of the form:
|
|
692
|
+
Minimize ||Ax-b||^2 + sum_i f_i(L_i x),
|
|
693
|
+
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
|
|
694
|
+
|
|
695
|
+
Reference:
|
|
696
|
+
[1] Boyd et al. "Distributed optimization and statistical learning via the alternating direction method of multipliers."Foundations and Trends® in Machine learning, 2011.
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
Parameters
|
|
700
|
+
----------
|
|
701
|
+
A : ndarray or callable
|
|
702
|
+
Represents a matrix or a function that performs matrix-vector multiplications.
|
|
703
|
+
When A is a callable, it accepts arguments (x, flag) where:
|
|
704
|
+
- flag=1 indicates multiplication of A with vector x, that is A @ x.
|
|
705
|
+
- flag=2 indicates multiplication of the transpose of A with vector x, that is A.T @ x.
|
|
706
|
+
b : ndarray.
|
|
707
|
+
penalty_terms : List of tuples (callable proximal operator of f_i, linear operator L_i)
|
|
708
|
+
Each callable proximal operator of 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.
|
|
709
|
+
x0 : ndarray. Initial guess.
|
|
710
|
+
penalty_parameter : Trade-off between linear least squares and regularization term in the solver iterates. Denoted as "rho" in [1].
|
|
711
|
+
maxit : The maximum number of iterations.
|
|
712
|
+
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
|
|
713
|
+
|
|
714
|
+
Example
|
|
715
|
+
-----------
|
|
716
|
+
.. code-block:: python
|
|
650
717
|
|
|
718
|
+
from cuqi.solver import ADMM, ProximalL1, ProjectNonnegative
|
|
719
|
+
import numpy as np
|
|
720
|
+
|
|
721
|
+
rng = np.random.default_rng()
|
|
722
|
+
|
|
723
|
+
m, n, k = 10, 5, 4
|
|
724
|
+
A = rng.standard_normal((m, n))
|
|
725
|
+
b = rng.standard_normal(m)
|
|
726
|
+
L = rng.standard_normal((k, n))
|
|
727
|
+
|
|
728
|
+
x0 = np.zeros(n)
|
|
729
|
+
admm = ADMM(A, b, x0, penalty_terms = [(ProximalL1, L), (lambda z, _ : ProjectNonnegative(z), np.eye(n))], tradeoff = 10)
|
|
730
|
+
sol, _ = admm.solve()
|
|
731
|
+
|
|
732
|
+
"""
|
|
733
|
+
|
|
734
|
+
def __init__(self, A, b, penalty_terms, x0, penalty_parameter = 10, maxit = 100, inner_max_it = 10, adaptive = True):
|
|
735
|
+
|
|
736
|
+
self.A = A
|
|
737
|
+
self.b = b
|
|
738
|
+
self.x_cur = x0
|
|
739
|
+
|
|
740
|
+
dual_len = [penalty[1].shape[0] for penalty in penalty_terms]
|
|
741
|
+
self.z_cur = [np.zeros(l) for l in dual_len]
|
|
742
|
+
self.u_cur = [np.zeros(l) for l in dual_len]
|
|
743
|
+
self.n = penalty_terms[0][1].shape[1]
|
|
744
|
+
|
|
745
|
+
self.rho = penalty_parameter
|
|
746
|
+
self.maxit = maxit
|
|
747
|
+
self.inner_max_it = inner_max_it
|
|
748
|
+
self.adaptive = adaptive
|
|
749
|
+
|
|
750
|
+
self.penalty_terms = penalty_terms
|
|
751
|
+
|
|
752
|
+
self.p = len(self.penalty_terms)
|
|
753
|
+
self._big_matrix = None
|
|
754
|
+
self._big_vector = None
|
|
755
|
+
|
|
756
|
+
def solve(self):
|
|
757
|
+
"""
|
|
758
|
+
Solves the regularized linear least squares problem using ADMM in scaled form. Based on [1], Subsection 3.1.1
|
|
759
|
+
"""
|
|
760
|
+
z_new = self.p*[0]
|
|
761
|
+
u_new = self.p*[0]
|
|
762
|
+
|
|
763
|
+
# Iterating
|
|
764
|
+
for i in range(self.maxit):
|
|
765
|
+
self._iteration_pre_processing()
|
|
766
|
+
|
|
767
|
+
# Main update (Least Squares)
|
|
768
|
+
solver = CGLS(self._big_matrix, self._big_vector, self.x_cur, self.inner_max_it)
|
|
769
|
+
x_new, _ = solver.solve()
|
|
770
|
+
|
|
771
|
+
# Regularization update
|
|
772
|
+
for j, penalty in enumerate(self.penalty_terms):
|
|
773
|
+
z_new[j] = penalty[0](penalty[1]@x_new + self.u_cur[j], 1.0/self.rho)
|
|
774
|
+
|
|
775
|
+
res_primal = 0.0
|
|
776
|
+
# Dual update
|
|
777
|
+
for j, penalty in enumerate(self.penalty_terms):
|
|
778
|
+
r_partial = penalty[1]@x_new - z_new[j]
|
|
779
|
+
res_primal += LA.norm(r_partial)**2
|
|
780
|
+
|
|
781
|
+
u_new[j] = self.u_cur[j] + r_partial
|
|
782
|
+
|
|
783
|
+
res_dual = 0.0
|
|
784
|
+
for j, penalty in enumerate(self.penalty_terms):
|
|
785
|
+
res_dual += LA.norm(penalty[1].T@(z_new[j] - self.z_cur[j]))**2
|
|
786
|
+
|
|
787
|
+
# Adaptive approach based on [1], Subsection 3.4.1
|
|
788
|
+
if self.adaptive:
|
|
789
|
+
if res_dual > 1e2*res_primal:
|
|
790
|
+
self.rho *= 0.5 # More regularization
|
|
791
|
+
elif res_primal > 1e2*res_dual:
|
|
792
|
+
self.rho *= 2.0 # More data fidelity
|
|
793
|
+
|
|
794
|
+
self.x_cur, self.z_cur, self.u_cur = x_new, z_new.copy(), u_new
|
|
795
|
+
|
|
796
|
+
return self.x_cur, i
|
|
651
797
|
|
|
798
|
+
def _iteration_pre_processing(self):
|
|
799
|
+
""" Preprocessing
|
|
800
|
+
Every iteration of ADMM requires solving a linear least squares system of the form
|
|
801
|
+
minimize 1/(rho) \|Ax-b\|_2^2 + sum_{i=1}^{p} \|penalty[1]x - (y - u)\|_2^2
|
|
802
|
+
To solve this, all linear least squares terms are combined into a single big term
|
|
803
|
+
with matrix big_matrix and data big_vector.
|
|
804
|
+
|
|
805
|
+
The matrix only needs to be updated when rho changes, i.e., when the adaptive option is used.
|
|
806
|
+
The data vector needs to be updated every iteration.
|
|
807
|
+
"""
|
|
808
|
+
|
|
809
|
+
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)])
|
|
810
|
+
|
|
811
|
+
# Check whether matrix needs to be updated
|
|
812
|
+
if self._big_matrix is not None and not self.adaptive:
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
# Update big_matrix
|
|
816
|
+
if callable(self.A):
|
|
817
|
+
def matrix_eval(x, flag):
|
|
818
|
+
if flag == 1:
|
|
819
|
+
out1 = np.sqrt(1/self.rho)*self.A(x, 1)
|
|
820
|
+
out2 = [penalty[1]@x for penalty in self.penalty_terms]
|
|
821
|
+
out = np.hstack([out1] + out2)
|
|
822
|
+
elif flag == 2:
|
|
823
|
+
idx_start = len(x)
|
|
824
|
+
idx_end = len(x)
|
|
825
|
+
out1 = np.zeros(self.n)
|
|
826
|
+
for _, t in reversed(self.penalty_terms):
|
|
827
|
+
idx_start -= t.shape[0]
|
|
828
|
+
out1 += t.T@x[idx_start:idx_end]
|
|
829
|
+
idx_end = idx_start
|
|
830
|
+
out2 = np.sqrt(1/self.rho)*self.A(x[:idx_end], 2)
|
|
831
|
+
out = out1 + out2
|
|
832
|
+
return out
|
|
833
|
+
self._big_matrix = matrix_eval
|
|
834
|
+
else:
|
|
835
|
+
self._big_matrix = np.vstack([np.sqrt(1/self.rho)*self.A] + [penalty[1] for penalty in self.penalty_terms])
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
|
|
652
840
|
def ProjectNonnegative(x):
|
|
653
841
|
"""(Euclidean) projection onto the nonnegative orthant.
|
|
654
842
|
|
|
@@ -675,6 +863,22 @@ def ProjectBox(x, lower = None, upper = None):
|
|
|
675
863
|
|
|
676
864
|
return np.minimum(np.maximum(x, lower), upper)
|
|
677
865
|
|
|
866
|
+
def ProjectHalfspace(x, a, b):
|
|
867
|
+
"""(Euclidean) projection onto the halfspace defined {z|<a,z> <= b}.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
x : array_like.
|
|
872
|
+
a : array_like.
|
|
873
|
+
b : array_like.
|
|
874
|
+
"""
|
|
875
|
+
|
|
876
|
+
ax_b = np.inner(a,x) - b
|
|
877
|
+
if ax_b <= 0:
|
|
878
|
+
return x
|
|
879
|
+
else:
|
|
880
|
+
return x - (ax_b/np.inner(a,a))*a
|
|
881
|
+
|
|
678
882
|
def ProximalL1(x, gamma):
|
|
679
883
|
"""(Euclidean) proximal operator of the \|x\|_1 norm.
|
|
680
884
|
Also known as the shrinkage or soft thresholding operator.
|
cuqi/testproblem/_testproblem.py
CHANGED
|
@@ -863,10 +863,9 @@ class Heat1D(BayesianProblem):
|
|
|
863
863
|
# Bayesian model
|
|
864
864
|
x = cuqi.distribution.Gaussian(np.zeros(model.domain_dim), 1)
|
|
865
865
|
y = cuqi.distribution.Gaussian(model(x), sigma2)
|
|
866
|
-
|
|
867
|
-
# Initialize Deconvolution as BayesianProblem problem
|
|
868
|
-
super().__init__(y, x, y=data)
|
|
869
866
|
|
|
867
|
+
# Initialize Heat1D as BayesianProblem problem
|
|
868
|
+
super().__init__(y, x, y=data)
|
|
870
869
|
# Store exact values
|
|
871
870
|
self.exactSolution = x_exact
|
|
872
871
|
self.exactData = y_exact
|
cuqi/utilities/__init__.py
CHANGED
|
@@ -12,7 +12,12 @@ from ._utilities import (
|
|
|
12
12
|
approx_derivative,
|
|
13
13
|
check_if_conditional_from_attr,
|
|
14
14
|
plot_1D_density,
|
|
15
|
-
plot_2D_density
|
|
15
|
+
plot_2D_density,
|
|
16
|
+
count_nonzero,
|
|
17
|
+
count_within_bounds,
|
|
18
|
+
count_constant_components_1D,
|
|
19
|
+
count_constant_components_2D,
|
|
20
|
+
piecewise_linear_1D_DoF
|
|
16
21
|
)
|
|
17
22
|
|
|
18
23
|
from ._get_python_variable_name import _get_python_variable_name
|
|
@@ -9,7 +9,7 @@ import cuqi
|
|
|
9
9
|
def _get_python_variable_name(var):
|
|
10
10
|
""" Retrieve the Python variable name of an object. Takes the first variable name appearing on the stack that is not in the ignore list. """
|
|
11
11
|
|
|
12
|
-
ignored_var_names = ["self", "cls", "obj", "var", "_"]
|
|
12
|
+
ignored_var_names = ["self", "cls", "obj", "var", "_", "result", "args", "kwargs", "par_name", "name", "distribution", "dist"]
|
|
13
13
|
|
|
14
14
|
# First get the stack size and loop (in reverse) through the stack
|
|
15
15
|
# It can be a bit slow to loop through stack size so we limit the levels
|
|
@@ -29,7 +29,7 @@ def _get_python_variable_name(var):
|
|
|
29
29
|
if len(var_names) > 0:
|
|
30
30
|
return var_names[0]
|
|
31
31
|
|
|
32
|
-
warnings.warn("Could not automatically find variable name for object
|
|
32
|
+
warnings.warn("Could not automatically find variable name for object. Did you assign (=) the object to a python variable? Alternatively, use keyword `name` when defining distribution to specify a name. If code runs slowly and variable name is not needed set config.MAX_STACK_SEARCH_DEPTH to 0. These names are reserved {} and should not be used as object name.".format(ignored_var_names))
|
|
33
33
|
|
|
34
34
|
return None
|
|
35
35
|
|