CUQIpy 1.0.0.post0.dev91__py3-none-any.whl → 1.0.0.post0.dev145__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.0.0.post0.dev91
3
+ Version: 1.0.0.post0.dev145
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=wjkb-AlAnGV3_a5XYx4H4xHteXak-c9qRWEs8JAhr3k,509
3
+ cuqi/_version.py,sha256=Qc2vgfclx0EbaHAQZJpzQEPFGq1LO7Yj1wMffmN8Llo,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
@@ -32,12 +32,13 @@ cuqi/distribution/_normal.py,sha256=UeoTtGDT7YSf4ZNo2amlVF9K-YQpYbf8q76jcRJTVFw,
32
32
  cuqi/distribution/_posterior.py,sha256=zAfL0GECxekZ2lBt1W6_LN0U_xskMwK4VNce5xAF7ig,5018
33
33
  cuqi/distribution/_uniform.py,sha256=7xJmCZH_LPhuGkwEDGh-_CTtzcWKrXMOxtTJUFb7Ydo,1607
34
34
  cuqi/experimental/__init__.py,sha256=vhZvyMX6rl8Y0haqCzGLPz6PSUKyu75XMQbeDHqTTrw,83
35
- cuqi/experimental/mcmc/__init__.py,sha256=MV1lHnRJTBsJMYEywnEx4iTEEAvN1SLIXhpRU_eiX-8,258
36
- cuqi/experimental/mcmc/_cwmh.py,sha256=G-8YjMqPraZm1Pm3n6scFkpa65gdtI1WTQxlL21etEI,8066
37
- cuqi/experimental/mcmc/_langevin_algorithm.py,sha256=ckVHDXLaw8hsUaOAFAEs7bL2Ny7W1QBKSc4AAC-TCis,9986
38
- cuqi/experimental/mcmc/_mh.py,sha256=AslackZJ3hPUNQfy70Fh9WoRPtcsHGqulI0LQgGHBus,3477
39
- cuqi/experimental/mcmc/_pcn.py,sha256=ma3pFqFgOmE7woZ41B5CGccKEuaacJPTmKvSEQhvtzs,3981
40
- cuqi/experimental/mcmc/_sampler.py,sha256=hIW_CIyv1e2jAJiHAZ8kbguZp3u24W7OnQKqcFDuzgQ,11043
35
+ cuqi/experimental/mcmc/__init__.py,sha256=vVnohcm4EIUwbp1sr3LbB0BkXO8jyZsbiKMJmIgetYY,314
36
+ cuqi/experimental/mcmc/_cwmh.py,sha256=yRlTk5a1QYfH3JyCecfOOTeDf-4-tmJ3Tl2Bc3pyp1Y,7336
37
+ cuqi/experimental/mcmc/_langevin_algorithm.py,sha256=MX48u3GYgCckB6Q5h5kXr_qdIaLQH2toOG5u29OY7gk,8245
38
+ cuqi/experimental/mcmc/_mh.py,sha256=aIV1Ntq0EAq3QJ1_X-DbP7eDAL-d_Or7d3RUO-R48I4,3090
39
+ cuqi/experimental/mcmc/_pcn.py,sha256=3M8zhQGQa53Gz04AkC8wJM61_5rIjGVnhPefi8m4dbY,3531
40
+ cuqi/experimental/mcmc/_rto.py,sha256=jSPznr34XPfWM6LysWIiN4hE-vtyti3cHyvzy9ruykg,11349
41
+ cuqi/experimental/mcmc/_sampler.py,sha256=hbZwUHcEZFSSVd2tICcp9FdcK9UKB_-izdM7w4xwijs,14408
41
42
  cuqi/geometry/__init__.py,sha256=Tz1WGzZBY-QGH3c0GiyKm9XHN8MGGcnU6TUHLZkzB3o,842
42
43
  cuqi/geometry/_geometry.py,sha256=WYFC-4_VBTW73b2ldsnfGYKvdSiCE8plr89xTSmkadg,46804
43
44
  cuqi/implicitprior/__init__.py,sha256=ZRZ9fgxgEl5n0A9F7WCl1_jid-GUiC8ZLkyTmGQmFlY,100
@@ -74,8 +75,8 @@ cuqi/testproblem/_testproblem.py,sha256=x769LwwRdJdzIiZkcQUGb_5-vynNTNALXWKato7s
74
75
  cuqi/utilities/__init__.py,sha256=EfxHLdsyDNugbmbzs43nV_AeKcycM9sVBjG9WZydagA,351
75
76
  cuqi/utilities/_get_python_variable_name.py,sha256=QwlBVj2koJRA8s8pWd554p7-ElcI7HUwY32HknaR92E,1827
76
77
  cuqi/utilities/_utilities.py,sha256=At3DOXRdF3GwLkVcM2FXooGyjAGfPkIM0bRzhTfLmWk,8046
77
- CUQIpy-1.0.0.post0.dev91.dist-info/LICENSE,sha256=kJWRPrtRoQoZGXyyvu50Uc91X6_0XRaVfT0YZssicys,10799
78
- CUQIpy-1.0.0.post0.dev91.dist-info/METADATA,sha256=KG8D1oXbdnsjU-xCHTLbwmzUCXkndGBNZ9-xGP6XguI,18392
79
- CUQIpy-1.0.0.post0.dev91.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
80
- CUQIpy-1.0.0.post0.dev91.dist-info/top_level.txt,sha256=AgmgMc6TKfPPqbjV0kvAoCBN334i_Lwwojc7HE3ZwD0,5
81
- CUQIpy-1.0.0.post0.dev91.dist-info/RECORD,,
78
+ CUQIpy-1.0.0.post0.dev145.dist-info/LICENSE,sha256=kJWRPrtRoQoZGXyyvu50Uc91X6_0XRaVfT0YZssicys,10799
79
+ CUQIpy-1.0.0.post0.dev145.dist-info/METADATA,sha256=REboBBeNZ3O4SrH9hNrNAYgvQPU9XP49ezl4bHyCigQ,18393
80
+ CUQIpy-1.0.0.post0.dev145.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
81
+ CUQIpy-1.0.0.post0.dev145.dist-info/top_level.txt,sha256=AgmgMc6TKfPPqbjV0kvAoCBN334i_Lwwojc7HE3ZwD0,5
82
+ CUQIpy-1.0.0.post0.dev145.dist-info/RECORD,,
cuqi/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-04-04T16:40:53+0200",
11
+ "date": "2024-04-15T12:29:37+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "91a1edb3f1ae9412a27489f3faa9a4a9c8ff8a4a",
15
- "version": "1.0.0.post0.dev91"
14
+ "full-revisionid": "744dccfaf90c7e50a11b7c5a4a59d5438c37525d",
15
+ "version": "1.0.0.post0.dev145"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -4,4 +4,5 @@ from ._sampler import SamplerNew, ProposalBasedSamplerNew
4
4
  from ._langevin_algorithm import ULANew, MALANew
5
5
  from ._mh import MHNew
6
6
  from ._pcn import pCNNew
7
+ from ._rto import LinearRTONew, RegularizedLinearRTONew
7
8
  from ._cwmh import CWMHNew
@@ -144,7 +144,7 @@ class CWMHNew(ProposalBasedSamplerNew):
144
144
 
145
145
  # Propose a sample x_all_components from the proposal distribution
146
146
  # for all the components
147
- target_eval_t = self.current_target
147
+ target_eval_t = self.current_target_logd
148
148
  if isinstance(self.proposal,cuqi.distribution.Distribution):
149
149
  x_all_components = self.proposal(
150
150
  location= self.current_point, scale=self.scale).sample()
@@ -175,7 +175,7 @@ class CWMHNew(ProposalBasedSamplerNew):
175
175
 
176
176
  x_star = x_t.copy()
177
177
 
178
- self.current_target = target_eval_t
178
+ self.current_target_logd = target_eval_t
179
179
  self.current_point = x_t
180
180
 
181
181
  return acc
@@ -199,23 +199,3 @@ class CWMHNew(ProposalBasedSamplerNew):
199
199
  # Update the scale parameter
200
200
  self.scale = np.minimum(scale_temp, np.ones(self.dim))
201
201
  self._scale_temp = scale_temp
202
-
203
- def get_state(self):
204
- current_point = self.current_point
205
- if isinstance(current_point, CUQIarray):
206
- current_point = current_point.to_numpy()
207
-
208
- return {'sampler_type': 'CWMH',
209
- 'current_point': current_point,
210
- 'current_target': self.current_target,
211
- 'scale': self.scale}
212
-
213
- def set_state(self, state):
214
- current_point = state['current_point']
215
- if not isinstance(current_point, CUQIarray):
216
- current_point = CUQIarray(current_point,
217
- geometry=self.target.geometry)
218
-
219
- self.current_point = current_point
220
- self.current_target = state['current_target']
221
- self.scale = state['scale']
@@ -60,14 +60,17 @@ class ULANew(SamplerNew): # Refactor to Proposal-based sampler?
60
60
  A Deblur example can be found in demos/demo27_ULA.py
61
61
  # TODO: update demo once sampler merged
62
62
  """
63
+
64
+ _STATE_KEYS = SamplerNew._STATE_KEYS.union({'current_target_logd', 'scale', 'current_target_grad'})
65
+
63
66
  def __init__(self, target, scale=1.0, **kwargs):
64
67
 
65
68
  super().__init__(target, **kwargs)
66
69
 
67
70
  self.scale = scale
68
71
  self.current_point = self.initial_point
69
- self.current_target_eval = self.target.logd(self.current_point)
70
- self.current_target_grad_eval = self.target.gradient(self.current_point)
72
+ self.current_target_logd = self.target.logd(self.current_point)
73
+ self.current_target_grad = self.target.gradient(self.current_point)
71
74
  self._acc = [1] # TODO. Check if we need this
72
75
 
73
76
  def validate_target(self):
@@ -99,15 +102,15 @@ class ULANew(SamplerNew): # Refactor to Proposal-based sampler?
99
102
  1 (accepted)
100
103
  """
101
104
  self.current_point = x_star
102
- self.current_target_eval = target_eval_star
103
- self.current_target_grad_eval = target_grad_star
105
+ self.current_target_logd = target_eval_star
106
+ self.current_target_grad = target_grad_star
104
107
  acc = 1
105
108
  return acc
106
109
 
107
110
  def step(self):
108
111
  # propose state
109
112
  xi = cuqi.distribution.Normal(mean=np.zeros(self.dim), std=np.sqrt(self.scale)).sample()
110
- x_star = self.current_point + 0.5*self.scale*self.current_target_grad_eval + xi
113
+ x_star = self.current_point + 0.5*self.scale*self.current_target_grad + xi
111
114
 
112
115
  # evaluate target
113
116
  target_eval_star, target_grad_star = self.target.logd(x_star), self.target.gradient(x_star)
@@ -120,26 +123,6 @@ class ULANew(SamplerNew): # Refactor to Proposal-based sampler?
120
123
  def tune(self, skip_len, update_count):
121
124
  pass
122
125
 
123
- def get_state(self):
124
- if isinstance(self.current_point, CUQIarray):
125
- self.current_point = self.current_point.to_numpy()
126
- if isinstance(self.current_target_eval, CUQIarray):
127
- self.current_target_eval = self.current_target_eval.to_numpy()
128
- if isinstance(self.current_target_grad_eval, CUQIarray):
129
- self.current_target_grad_eval = self.current_target_grad_eval.to_numpy()
130
- return {'sampler_type': 'ULA', 'current_point': self.current_point, \
131
- 'current_target_eval': self.current_target_eval, \
132
- 'current_target_grad_eval': self.current_target_grad_eval, \
133
- 'scale': self.scale}
134
-
135
- def set_state(self, state):
136
- temp = CUQIarray(state['current_point'] , geometry=self.target.geometry)
137
- self.current_point = temp
138
- temp = CUQIarray(state['current_target_eval'] , geometry=self.target.geometry)
139
- self.current_target_eval = temp
140
- temp = CUQIarray(state['current_target_grad_eval'] , geometry=self.target.geometry)
141
- self.current_target_grad_eval = temp
142
- self.scale = state['scale']
143
126
 
144
127
  class MALANew(ULANew): # Refactor to Proposal-based sampler?
145
128
  """ Metropolis-adjusted Langevin algorithm (MALA) (Roberts and Tweedie, 1996)
@@ -219,9 +202,9 @@ class MALANew(ULANew): # Refactor to Proposal-based sampler?
219
202
  scaler
220
203
  1 if accepted, 0 otherwise
221
204
  """
222
- log_target_ratio = target_eval_star - self.current_target_eval
205
+ log_target_ratio = target_eval_star - self.current_target_logd
223
206
  log_prop_ratio = self._log_proposal(self.current_point, x_star, target_grad_star) \
224
- - self._log_proposal(x_star, self.current_point, self.current_target_grad_eval)
207
+ - self._log_proposal(x_star, self.current_point, self.current_target_grad)
225
208
  log_alpha = min(0, log_target_ratio + log_prop_ratio)
226
209
 
227
210
  # accept/reject with Metropolis
@@ -229,8 +212,8 @@ class MALANew(ULANew): # Refactor to Proposal-based sampler?
229
212
  log_u = np.log(np.random.rand())
230
213
  if (log_u <= log_alpha) and (np.isnan(target_eval_star) == False):
231
214
  self.current_point = x_star
232
- self.current_target_eval = target_eval_star
233
- self.current_target_grad_eval = target_grad_star
215
+ self.current_target_logd = target_eval_star
216
+ self.current_target_grad = target_grad_star
234
217
  acc = 1
235
218
  return acc
236
219
 
@@ -241,15 +224,3 @@ class MALANew(ULANew): # Refactor to Proposal-based sampler?
241
224
  mu = theta_k + ((self.scale)/2)*g_logpi_k
242
225
  misfit = theta_star - mu
243
226
  return -0.5*((1/(self.scale))*(misfit.T @ misfit))
244
-
245
- def get_state(self):
246
- if isinstance(self.current_point, CUQIarray):
247
- self.current_point = self.current_point.to_numpy()
248
- if isinstance(self.current_target_eval, CUQIarray):
249
- self.current_target_eval = self.current_target_eval.to_numpy()
250
- if isinstance(self.current_target_grad_eval, CUQIarray):
251
- self.current_target_grad_eval = self.current_target_grad_eval.to_numpy()
252
- return {'sampler_type': 'MALA', 'current_point': self.current_point, \
253
- 'current_target_eval': self.current_target_eval, \
254
- 'current_target_grad_eval': self.current_target_grad_eval, \
255
- 'scale': self.scale}
@@ -23,6 +23,8 @@ class MHNew(ProposalBasedSamplerNew):
23
23
 
24
24
  """
25
25
 
26
+ _STATE_KEYS = ProposalBasedSamplerNew._STATE_KEYS.union({'scale', '_scale_temp'})
27
+
26
28
  def __init__(self, target, proposal=None, scale=1, **kwargs):
27
29
  super().__init__(target, proposal=proposal, scale=scale, **kwargs)
28
30
  # Due to a bug? in old MH, we must keep track of this extra variable to match behavior.
@@ -54,7 +56,7 @@ class MHNew(ProposalBasedSamplerNew):
54
56
  target_eval_star = self.target.logd(x_star)
55
57
 
56
58
  # ratio and acceptance probability
57
- ratio = target_eval_star - self.current_target # proposal is symmetric
59
+ ratio = target_eval_star - self.current_target_logd # proposal is symmetric
58
60
  alpha = min(0, ratio)
59
61
 
60
62
  # accept/reject
@@ -62,7 +64,7 @@ class MHNew(ProposalBasedSamplerNew):
62
64
  acc = 0
63
65
  if (u_theta <= alpha):
64
66
  self.current_point = x_star
65
- self.current_target = target_eval_star
67
+ self.current_target_logd = target_eval_star
66
68
  acc = 1
67
69
 
68
70
  return acc
@@ -79,13 +81,3 @@ class MHNew(ProposalBasedSamplerNew):
79
81
 
80
82
  # update parameters
81
83
  self.scale = min(self._scale_temp, 1)
82
-
83
- def get_state(self):
84
- return {'sampler_type': 'MH', 'current_point': self.current_point.to_numpy(), 'current_target': self.current_target.to_numpy(), 'scale': self.scale}
85
-
86
- def set_state(self, state):
87
- temp = CUQIarray(state['current_point'] , geometry=self.target.geometry)
88
- self.current_point = temp
89
- temp = CUQIarray(state['current_target'] , geometry=self.target.geometry)
90
- self.current_target = temp
91
- self.scale = state['scale']
@@ -5,13 +5,15 @@ from cuqi.array import CUQIarray
5
5
 
6
6
  class pCNNew(SamplerNew): # Refactor to Proposal-based sampler?
7
7
 
8
+ _STATE_KEYS = SamplerNew._STATE_KEYS.union({'scale', 'current_likelihood_logd'})
9
+
8
10
  def __init__(self, target, scale=1.0, **kwargs):
9
11
 
10
12
  super().__init__(target, **kwargs)
11
13
 
12
14
  self.scale = scale
13
15
  self.current_point = self.initial_point
14
- self.current_loglike_eval = self._loglikelihood(self.current_point)
16
+ self.current_likelihood_logd = self._loglikelihood(self.current_point)
15
17
 
16
18
  self._acc = [1] # TODO. Check if we need this
17
19
 
@@ -33,7 +35,7 @@ class pCNNew(SamplerNew): # Refactor to Proposal-based sampler?
33
35
  loglike_eval_star = self._loglikelihood(x_star)
34
36
 
35
37
  # ratio and acceptance probability
36
- ratio = loglike_eval_star - self.current_loglike_eval # proposal is symmetric
38
+ ratio = loglike_eval_star - self.current_likelihood_logd # proposal is symmetric
37
39
  alpha = min(0, ratio)
38
40
 
39
41
  # accept/reject
@@ -41,7 +43,7 @@ class pCNNew(SamplerNew): # Refactor to Proposal-based sampler?
41
43
  u_theta = np.log(np.random.rand())
42
44
  if (u_theta <= alpha):
43
45
  self.current_point = x_star
44
- self.current_loglike_eval = loglike_eval_star
46
+ self.current_likelihood_logd = loglike_eval_star
45
47
  acc = 1
46
48
 
47
49
  return acc
@@ -87,15 +89,3 @@ class pCNNew(SamplerNew): # Refactor to Proposal-based sampler?
87
89
 
88
90
  def tune(self, skip_len, update_count):
89
91
  pass
90
-
91
- def get_state(self):
92
- return {'sampler_type': 'PCN', 'current_point': self.current_point.to_numpy(), \
93
- 'current_loglike_eval': self.current_loglike_eval.to_numpy(), \
94
- 'scale': self.scale}
95
-
96
- def set_state(self, state):
97
- temp = CUQIarray(state['current_point'] , geometry=self.target.geometry)
98
- self.current_point = temp
99
- temp = CUQIarray(state['current_loglike_eval'] , geometry=self.target.geometry)
100
- self.current_loglike_eval = temp
101
- self.scale = state['scale']
@@ -0,0 +1,270 @@
1
+ import scipy as sp
2
+ from scipy.linalg.interpolative import estimate_spectral_norm
3
+ from scipy.sparse.linalg import LinearOperator as scipyLinearOperator
4
+ import numpy as np
5
+ import cuqi
6
+ from cuqi.solver import CGLS, FISTA
7
+ from cuqi.experimental.mcmc import SamplerNew
8
+ from cuqi.array import CUQIarray
9
+
10
+
11
+ class LinearRTONew(SamplerNew):
12
+ """
13
+ Linear RTO (Randomize-Then-Optimize) sampler.
14
+
15
+ Samples posterior related to the inverse problem with Gaussian likelihood and prior, and where the forward model is Linear.
16
+
17
+ Parameters
18
+ ------------
19
+ target : `cuqi.distribution.Posterior`, `cuqi.distribution.MultipleLikelihoodPosterior` or 5-dimensional tuple.
20
+ If target is of type cuqi.distribution.Posterior or cuqi.distribution.MultipleLikelihoodPosterior, it represents the posterior distribution.
21
+ If target is a 5-dimensional tuple, it assumes the following structure:
22
+ (data, model, L_sqrtprec, P_mean, P_sqrtrec)
23
+
24
+ Here:
25
+ data: is a m-dimensional numpy array containing the measured data.
26
+ model: is a m by n dimensional matrix or LinearModel representing the forward model.
27
+ L_sqrtprec: is the squareroot of the precision matrix of the Gaussian likelihood.
28
+ P_mean: is the prior mean.
29
+ P_sqrtprec: is the squareroot of the precision matrix of the Gaussian mean.
30
+
31
+ initial_point : `np.ndarray`
32
+ Initial point for the sampler. *Optional*.
33
+
34
+ maxit : int
35
+ Maximum number of iterations of the inner CGLS solver. *Optional*.
36
+
37
+ tol : float
38
+ Tolerance of the inner CGLS solver. *Optional*.
39
+
40
+ callback : callable, *Optional*
41
+ If set this function will be called after every sample.
42
+ The signature of the callback function is `callback(sample, sample_index)`,
43
+ where `sample` is the current sample and `sample_index` is the index of the sample.
44
+ An example is shown in demos/demo31_callback.py.
45
+
46
+ """
47
+ def __init__(self, target, initial_point=None, maxit=10, tol=1e-6, **kwargs):
48
+
49
+ super().__init__(target=target, initial_point=initial_point, **kwargs)
50
+
51
+ if initial_point is None: #TODO: Replace later with a getter
52
+ self.initial_point = np.zeros(self.dim)
53
+ self._samples = [self.initial_point]
54
+
55
+ self.current_point = self.initial_point
56
+ self._acc = [1] # TODO. Check if we need this
57
+
58
+ # Other parameters
59
+ self.maxit = maxit
60
+ self.tol = tol
61
+
62
+ @property
63
+ def prior(self):
64
+ return self.target.prior
65
+
66
+ @property
67
+ def likelihood(self):
68
+ return self.target.likelihood
69
+
70
+ @property
71
+ def likelihoods(self):
72
+ if isinstance(self.target, cuqi.distribution.Posterior):
73
+ return [self.target.likelihood]
74
+ elif isinstance(self.target, cuqi.distribution.MultipleLikelihoodPosterior):
75
+ return self.target.likelihoods
76
+
77
+ @property
78
+ def model(self):
79
+ return self.target.model
80
+
81
+ @property
82
+ def data(self):
83
+ return self.target.data
84
+
85
+ @SamplerNew.target.setter
86
+ def target(self, value):
87
+ """ Set the target density. Runs validation of the target. """
88
+ # Accept tuple of inputs and construct posterior
89
+ if isinstance(value, tuple) and len(value) == 5:
90
+ # Structure (data, model, L_sqrtprec, P_mean, P_sqrtprec)
91
+ data = value[0]
92
+ model = value[1]
93
+ L_sqrtprec = value[2]
94
+ P_mean = value[3]
95
+ P_sqrtprec = value[4]
96
+
97
+ # If numpy matrix convert to CUQI model
98
+ if isinstance(model, np.ndarray) and len(model.shape) == 2:
99
+ model = cuqi.model.LinearModel(model)
100
+
101
+ # Check model input
102
+ if not isinstance(model, cuqi.model.LinearModel):
103
+ raise TypeError("Model needs to be cuqi.model.LinearModel or matrix")
104
+
105
+ # Likelihood
106
+ L = cuqi.distribution.Gaussian(model, sqrtprec=L_sqrtprec).to_likelihood(data)
107
+
108
+ # Prior TODO: allow multiple priors stacked
109
+ #if isinstance(P_mean, list) and isinstance(P_sqrtprec, list):
110
+ # P = cuqi.distribution.JointGaussianSqrtPrec(P_mean, P_sqrtprec)
111
+ #else:
112
+ P = cuqi.distribution.Gaussian(P_mean, sqrtprec=P_sqrtprec)
113
+
114
+ # Construct posterior
115
+ value = cuqi.distribution.Posterior(L, P)
116
+ super(LinearRTONew, type(self)).target.fset(self, value)
117
+ self._precompute()
118
+
119
+ def _precompute(self):
120
+ L1 = [likelihood.distribution.sqrtprec for likelihood in self.likelihoods]
121
+ L2 = self.prior.sqrtprec
122
+ L2mu = self.prior.sqrtprecTimesMean
123
+
124
+ # pre-computations
125
+ self.n = self.prior.dim
126
+ self.b_tild = np.hstack([L@likelihood.data for (L, likelihood) in zip(L1, self.likelihoods)]+ [L2mu])
127
+
128
+ callability = [callable(likelihood.model) for likelihood in self.likelihoods]
129
+ notcallability = [not c for c in callability]
130
+ if all(notcallability):
131
+ self.M = sp.sparse.vstack([L@likelihood.model for (L, likelihood) in zip(L1, self.likelihoods)] + [L2])
132
+ elif all(callability):
133
+ # in this case, model is a function doing forward and backward operations
134
+ def M(x, flag):
135
+ if flag == 1:
136
+ out1 = [L @ likelihood.model.forward(x) for (L, likelihood) in zip(L1, self.likelihoods)]
137
+ out2 = L2 @ x
138
+ out = np.hstack(out1 + [out2])
139
+ elif flag == 2:
140
+ idx_start = 0
141
+ idx_end = 0
142
+ out1 = np.zeros(self.n)
143
+ for likelihood in self.likelihoods:
144
+ idx_end += len(likelihood.data)
145
+ out1 += likelihood.model.adjoint(likelihood.distribution.sqrtprec.T@x[idx_start:idx_end])
146
+ idx_start = idx_end
147
+ out2 = L2.T @ x[idx_end:]
148
+ out = out1 + out2
149
+ return out
150
+ self.M = M
151
+ else:
152
+ raise TypeError("All likelihoods need to be callable or none need to be callable.")
153
+
154
+ def step(self):
155
+ y = self.b_tild + np.random.randn(len(self.b_tild))
156
+ sim = CGLS(self.M, y, self.current_point, self.maxit, self.tol)
157
+ self.current_point, _ = sim.solve()
158
+ acc = 1
159
+ return acc
160
+
161
+ def tune(self, skip_len, update_count):
162
+ pass
163
+
164
+ def validate_target(self):
165
+ # Check target type
166
+ if not isinstance(self.target, (cuqi.distribution.Posterior, cuqi.distribution.MultipleLikelihoodPosterior)):
167
+ raise ValueError(f"To initialize an object of type {self.__class__}, 'target' need to be of type 'cuqi.distribution.Posterior' or 'cuqi.distribution.MultipleLikelihoodPosterior'.")
168
+
169
+ # Check Linear model and Gaussian likelihood(s)
170
+ if isinstance(self.target, cuqi.distribution.Posterior):
171
+ if not isinstance(self.model, cuqi.model.LinearModel):
172
+ raise TypeError("Model needs to be linear")
173
+
174
+ if not hasattr(self.likelihood.distribution, "sqrtprec"):
175
+ raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")
176
+
177
+ elif isinstance(self.target, cuqi.distribution.MultipleLikelihoodPosterior): # Elif used for further alternatives, e.g., stacked posterior
178
+ for likelihood in self.likelihoods:
179
+ if not isinstance(likelihood.model, cuqi.model.LinearModel):
180
+ raise TypeError("Model needs to be linear")
181
+
182
+ if not hasattr(likelihood.distribution, "sqrtprec"):
183
+ raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")
184
+
185
+ # Check Gaussian prior
186
+ if not hasattr(self.prior, "sqrtprec"):
187
+ raise TypeError("prior must contain a sqrtprec attribute")
188
+
189
+ if not hasattr(self.prior, "sqrtprecTimesMean"):
190
+ raise TypeError("Prior must contain a sqrtprecTimesMean attribute")
191
+
192
+ class RegularizedLinearRTONew(LinearRTONew):
193
+ """
194
+ Regularized Linear RTO (Randomize-Then-Optimize) sampler.
195
+
196
+ Samples posterior related to the inverse problem with Gaussian likelihood and implicit Gaussian prior, and where the forward model is Linear.
197
+
198
+ Parameters
199
+ ------------
200
+ target : `cuqi.distribution.Posterior`
201
+ See `cuqi.sampler.LinearRTO`
202
+
203
+ initial_point : `np.ndarray`
204
+ Initial point for the sampler. *Optional*.
205
+
206
+ maxit : int
207
+ Maximum number of iterations of the inner FISTA solver. *Optional*.
208
+
209
+ stepsize : string or float
210
+ If stepsize is a string and equals either "automatic", then the stepsize is automatically estimated based on the spectral norm.
211
+ If stepsize is a float, then this stepsize is used.
212
+
213
+ abstol : float
214
+ Absolute tolerance of the inner FISTA solver. *Optional*.
215
+
216
+ adaptive : bool
217
+ If True, FISTA is used as inner solver, otherwise ISTA is used. *Optional*.
218
+
219
+ callback : callable, *Optional*
220
+ If set this function will be called after every sample.
221
+ The signature of the callback function is `callback(sample, sample_index)`,
222
+ where `sample` is the current sample and `sample_index` is the index of the sample.
223
+ An example is shown in demos/demo31_callback.py.
224
+
225
+ """
226
+ def __init__(self, target, initial_point=None, maxit=100, stepsize="automatic", abstol=1e-10, adaptive=True, **kwargs):
227
+
228
+ super().__init__(target=target, initial_point=initial_point, **kwargs)
229
+
230
+ # Other parameters
231
+ self.stepsize = stepsize
232
+ self.abstol = abstol
233
+ self.adaptive = adaptive
234
+ self.proximal = target.prior.proximal
235
+ self._stepsize = self._choose_stepsize()
236
+ self.maxit = maxit
237
+
238
+ @LinearRTONew.target.setter
239
+ def target(self, value):
240
+ if not callable(value.prior.proximal):
241
+ raise TypeError("Projector needs to be callable")
242
+ return super(RegularizedLinearRTONew, type(self)).target.fset(self, value)
243
+
244
+ def _choose_stepsize(self):
245
+ if isinstance(self.stepsize, str):
246
+ if self.stepsize in ["automatic"]:
247
+ if not callable(self.M):
248
+ M_op = scipyLinearOperator(self.M.shape, matvec = lambda v: self.M@v, rmatvec = lambda w: self.M.T@w)
249
+ else:
250
+ M_op = scipyLinearOperator((len(self.b_tild), self.n), matvec = lambda v: self.M(v,1), rmatvec = lambda w: self.M(w,2))
251
+
252
+ _stepsize = 0.99/(estimate_spectral_norm(M_op)**2)
253
+ # print(f"Estimated stepsize for regularized Linear RTO: {_stepsize}")
254
+ else:
255
+ raise ValueError("Stepsize choice not supported")
256
+ else:
257
+ _stepsize = self.stepsize
258
+ return _stepsize
259
+
260
+ @property
261
+ def prior(self):
262
+ return self.target.prior.gaussian
263
+
264
+ def step(self):
265
+ y = self.b_tild + np.random.randn(len(self.b_tild))
266
+ sim = FISTA(self.M, y, self.current_point, self.proximal,
267
+ maxit = self.maxit, stepsize = self._stepsize, abstol = self.abstol, adaptive = self.adaptive)
268
+ self.current_point, _ = sim.solve()
269
+ acc = 1
270
+ return acc
@@ -21,6 +21,11 @@ class SamplerNew(ABC):
21
21
  Samples are stored in a list to allow for dynamic growth of the sample set. Returning samples is done by creating a new Samples object from the list of samples.
22
22
 
23
23
  """
24
+ _STATE_KEYS = {'current_point'}
25
+ """ Set of keys for the state dictionary. """
26
+
27
+ _HISTORY_KEYS = {'_samples', '_acc'}
28
+ """ Set of keys for the history dictionary. """
24
29
 
25
30
  def __init__(self, target: cuqi.density.Density, initial_point=None, callback=None):
26
31
  """ Initializer for abstract base class for all samplers.
@@ -45,8 +50,10 @@ class SamplerNew(ABC):
45
50
  # Choose initial point if not given
46
51
  if initial_point is None:
47
52
  initial_point = np.ones(self.dim)
53
+
54
+ self.initial_point = initial_point
48
55
 
49
- self._samples = [initial_point]
56
+ self._samples = [initial_point] # Remove. See #324.
50
57
 
51
58
  # ------------ Abstract methods to be implemented by subclasses ------------
52
59
 
@@ -65,24 +72,7 @@ class SamplerNew(ABC):
65
72
  """ Validate the target is compatible with the sampler. Called when the target is set. Should raise an error if the target is not compatible. """
66
73
  pass
67
74
 
68
- @abstractmethod
69
- def get_state(self):
70
- """ Return the state of the sampler. """
71
- pass
72
-
73
- @abstractmethod
74
- def set_state(self, state):
75
- """ Set the state of the sampler. """
76
- pass
77
-
78
-
79
75
  # ------------ Public attributes ------------
80
-
81
- @property
82
- def initial_point(self):
83
- """ Return the initial point of the sampler. This is always the first sample. """
84
- return self._samples[0]
85
-
86
76
  @property
87
77
  def dim(self):
88
78
  """ Dimension of the target density. """
@@ -104,6 +94,15 @@ class SamplerNew(ABC):
104
94
  self._target = value
105
95
  self.validate_target()
106
96
 
97
+ @property
98
+ def current_point(self):
99
+ """ The current point of the sampler. """
100
+ return self._current_point
101
+
102
+ @current_point.setter
103
+ def current_point(self, value):
104
+ """ Set the current point of the sampler. """
105
+ self._current_point = value
107
106
 
108
107
  # ------------ Public methods ------------
109
108
 
@@ -204,6 +203,94 @@ class SamplerNew(ABC):
204
203
  self._call_callback(self.current_point, len(self._samples)-1)
205
204
 
206
205
  return self
206
+
207
+ def get_state(self) -> dict:
208
+ """ Return the state of the sampler.
209
+
210
+ The state is used when checkpointing the sampler.
211
+
212
+ The state of the sampler is a dictionary with keys 'metadata' and 'state'.
213
+ The 'metadata' key contains information about the sampler type.
214
+ The 'state' key contains the state of the sampler.
215
+
216
+ For example, the state of a "MH" sampler could be:
217
+
218
+ state = {
219
+ 'metadata': {
220
+ 'sampler_type': 'MH'
221
+ },
222
+ 'state': {
223
+ 'current_point': np.array([...]),
224
+ 'current_target_logd': -123.45,
225
+ 'scale': 1.0,
226
+ ...
227
+ }
228
+ }
229
+ """
230
+ state = {
231
+ 'metadata': {
232
+ 'sampler_type': self.__class__.__name__
233
+ },
234
+ 'state': {
235
+ key: getattr(self, key) for key in self._STATE_KEYS
236
+ }
237
+ }
238
+ return state
239
+
240
+ def set_state(self, state: dict):
241
+ """ Set the state of the sampler.
242
+
243
+ The state is used when loading the sampler from a checkpoint.
244
+
245
+ The state of the sampler is a dictionary with keys 'metadata' and 'state'.
246
+
247
+ For example, the state of a "MH" sampler could be:
248
+
249
+ state = {
250
+ 'metadata': {
251
+ 'sampler_type': 'MH'
252
+ },
253
+ 'state': {
254
+ 'current_point': np.array([...]),
255
+ 'current_target_logd': -123.45,
256
+ 'scale': 1.0,
257
+ ...
258
+ }
259
+ }
260
+ """
261
+ if state['metadata']['sampler_type'] != self.__class__.__name__:
262
+ raise ValueError(f"Sampler type in state dictionary ({state['metadata']['sampler_type']}) does not match the type of the sampler ({self.__class__.__name__}).")
263
+
264
+ for key, value in state['state'].items():
265
+ if key in self._STATE_KEYS:
266
+ setattr(self, key, value)
267
+ else:
268
+ raise ValueError(f"Key {key} not recognized in state dictionary of sampler {self.__class__.__name__}.")
269
+
270
+ def get_history(self) -> dict:
271
+ """ Return the history of the sampler. """
272
+ history = {
273
+ 'metadata': {
274
+ 'sampler_type': self.__class__.__name__
275
+ },
276
+ 'history': {
277
+ key: getattr(self, key) for key in self._HISTORY_KEYS
278
+ }
279
+ }
280
+ return history
281
+
282
+ def set_history(self, history: dict):
283
+ """ Set the history of the sampler. """
284
+ if history['metadata']['sampler_type'] != self.__class__.__name__:
285
+ raise ValueError(f"Sampler type in history dictionary ({history['metadata']['sampler_type']}) does not match the type of the sampler ({self.__class__.__name__}).")
286
+
287
+ for key, value in history['history'].items():
288
+ if key in self._HISTORY_KEYS:
289
+ setattr(self, key, value)
290
+ else:
291
+ raise ValueError(f"Key {key} not recognized in history dictionary of sampler {self.__class__.__name__}.")
292
+
293
+ # ------------ Private methods ------------
207
294
 
208
295
  def _call_callback(self, sample, sample_index):
209
296
  """ Calls the callback function. Assumes input is sample and sample index"""
@@ -213,6 +300,9 @@ class SamplerNew(ABC):
213
300
 
214
301
  class ProposalBasedSamplerNew(SamplerNew, ABC):
215
302
  """ Abstract base class for samplers that use a proposal distribution. """
303
+
304
+ _STATE_KEYS = SamplerNew._STATE_KEYS.union({'current_target_logd', 'scale'})
305
+
216
306
  def __init__(self, target, proposal=None, scale=1, **kwargs):
217
307
  """ Initializer for proposal based samplers.
218
308
 
@@ -235,7 +325,7 @@ class ProposalBasedSamplerNew(SamplerNew, ABC):
235
325
  super().__init__(target, **kwargs)
236
326
 
237
327
  self.current_point = self.initial_point
238
- self.current_target = self.target.logd(self.current_point)
328
+ self.current_target_logd = self.target.logd(self.current_point)
239
329
  self.proposal = proposal
240
330
  self.scale = scale
241
331