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/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
@@ -1,12 +1,14 @@
1
1
  from ._solver import (
2
- L_BFGS_B,
3
- minimize,
4
- maximize,
5
- LS,
2
+ ScipyLBFGSB,
3
+ ScipyMinimizer,
4
+ ScipyMaximizer,
5
+ ScipyLSQ,
6
+ ScipyLinearLSQ,
6
7
  CGLS,
7
8
  LM,
8
9
  PDHG,
9
10
  FISTA,
11
+ ADMM,
10
12
  ProjectNonnegative,
11
13
  ProjectBox,
12
14
  ProximalL1
cuqi/solver/_solver.py CHANGED
@@ -15,7 +15,7 @@ except ImportError:
15
15
  has_cholmod = False
16
16
 
17
17
 
18
- class L_BFGS_B(object):
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, then the solver approximates the gradient.
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 minimize(object):
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 = None, method = None, **kwargs):
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 maximize(minimize):
160
- """Simply calls ::class:: cuqi.solver.minimize with -func."""
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 LS(object):
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
- minimize F(x) = 0.5 * sum(rho(f_i(x)**2), i = 0, ..., m-1)
179
- subject to lb <= x <= ub
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 None, then the solver approximates the Jacobian.
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=None, method='trf', loss='linear', tol=1e-6, maxit=1e4):
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, x0, proximal = ProximalL1, stepsize = stepsize, maxit = 100, abstol=1e-12, adaptive = True)
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, x0, proximal, maxit=100, stepsize=1e0, abstol=1e-14, adaptive = True):
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.
@@ -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
@@ -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: {}. 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.".format(var))
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