CUQIpy 1.2.0.post0.dev247__py3-none-any.whl → 1.2.0.post0.dev294__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.dev247
3
+ Version: 1.2.0.post0.dev294
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=q8OTJF8RLbDiDlnniXXgLZ5v_ckTT6rNzRdJrbsIHKw,510
3
+ cuqi/_version.py,sha256=CRmDXEkBbTbvKfpJXpazyEWYTSKkfhxALDOf_iKJkE4,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
@@ -43,11 +43,11 @@ cuqi/experimental/mcmc/_direct.py,sha256=9pQS_2Qk2-ybt6m8WTfPoKetcxQ00WaTRN85-Z0
43
43
  cuqi/experimental/mcmc/_gibbs.py,sha256=evgxf2tLFLlKB3hN0qz9a9NcZQSES8wdacnn3uNWocQ,12005
44
44
  cuqi/experimental/mcmc/_hmc.py,sha256=8p4QxZBRpFLzwamH-DWHSdZE0aXX3FqonBzczz_XkDw,19340
45
45
  cuqi/experimental/mcmc/_langevin_algorithm.py,sha256=LtPdC1IAeF_gS3T93FDLFutXy7THw-JqZeywZExpefo,14527
46
- cuqi/experimental/mcmc/_laplace_approximation.py,sha256=rdiE3cMQFq6FLQcOQwPpuGIxrTAp3aoGPxMDSdeopV0,5688
46
+ cuqi/experimental/mcmc/_laplace_approximation.py,sha256=XcGIa2wl9nCSTtAFurejYYOKkDVAJ22q75xQKsyu2nI,5803
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=Ub5rDe_yfkzxqcnimEArXWVb3twuGUJmvxEQNPKQWfU,10061
50
- cuqi/experimental/mcmc/_sampler.py,sha256=xtoT70T8xe3Ye7yYdIFQD_kivjXlqUImyV3bMt406nk,20106
49
+ cuqi/experimental/mcmc/_rto.py,sha256=j3PD3ZfOuGifbBu51Z7GdMCM47HjH0luhpWFDPXNtxc,10477
50
+ cuqi/experimental/mcmc/_sampler.py,sha256=BZHnpB6s-YSddd46wQSds0vNF61RA58Nc9ZU05WngdU,20184
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
@@ -58,8 +58,8 @@ cuqi/implicitprior/_regularizedUnboundedUniform.py,sha256=H2fTOSqYTlDiLxQ7Ya6wnp
58
58
  cuqi/implicitprior/_restorator.py,sha256=ixnH8RGcLpqlaIUdR5Dwjx72sO9f3BeotNFRC7Z7qZo,9198
59
59
  cuqi/likelihood/__init__.py,sha256=QXif382iwZ5bT3ZUqmMs_n70JVbbjxbqMrlQYbMn4Zo,1776
60
60
  cuqi/likelihood/_likelihood.py,sha256=z3AXAbIrv_DjOYh4jy3iDHemuIFUUJu6wdvJ5e2dgW0,6913
61
- cuqi/model/__init__.py,sha256=IcN4aZCnyp9o-8TNIoZ8vew99QQgi0EmZvnsIuR6qYI,49
62
- cuqi/model/_model.py,sha256=2MtQaahSGOVm45tvxh_xbke9vo_Aq0tpyNvLg9TK9dA,27791
61
+ cuqi/model/__init__.py,sha256=jgY2-jyxEMC79vkyH9BpfowW7_DbMRjqedOtO5fykXQ,62
62
+ cuqi/model/_model.py,sha256=Hddc2qlKH0tVCB7gTY_U8TPO8IcjkAe63-TwCAVXUWI,31423
63
63
  cuqi/operator/__init__.py,sha256=0pc9p-KPyl7KtPV0noB0ddI0CP2iYEHw5rbw49D8Njk,136
64
64
  cuqi/operator/_operator.py,sha256=yNwPTh7jR07AiKMbMQQ5_54EgirlKFsbq9JN1EODaQI,8856
65
65
  cuqi/pde/__init__.py,sha256=NyS_ZYruCvy-Yg24qKlwm3ZIX058kLNQX9bqs-xg4ZM,99
@@ -73,10 +73,10 @@ cuqi/sampler/_cwmh.py,sha256=VlAVT1SXQU0yD5ZeR-_ckWvX-ifJrMweFFdFbxdfB_k,7775
73
73
  cuqi/sampler/_gibbs.py,sha256=N7qcePwMkRtxINN5JF0FaMIdDCXZGqsfKjfha_KHCck,8627
74
74
  cuqi/sampler/_hmc.py,sha256=EUTefZir-wapoZ7OZFb5M5vayL8z6XksZRMY1BpbuXc,15027
75
75
  cuqi/sampler/_langevin_algorithm.py,sha256=o5EyvaR6QGAD7LKwXVRC3WwAP5IYJf5GoMVWl9DrfOA,7861
76
- cuqi/sampler/_laplace_approximation.py,sha256=u018Z5eqlcq_cIwD9yNOaA15dLQE_vUWaee5Xp8bcjg,6454
76
+ cuqi/sampler/_laplace_approximation.py,sha256=M0SnpICcf8No1zsPKopGEncVgLqBUI7VUDf9-YIkk_g,6565
77
77
  cuqi/sampler/_mh.py,sha256=V5tIdn-KdfWo4J_Nbf-AH6XwKWblWUyc4BeuSikUHsE,7062
78
78
  cuqi/sampler/_pcn.py,sha256=F0h9-nUFtkqn-o-1s8BCsmr8V7u6R7ycoCOeeV1uhj0,8601
79
- cuqi/sampler/_rto.py,sha256=eJe7_gN_1NpHHc_okKmFtLcOrvoe6cBoVLdf9ULuB_w,11518
79
+ cuqi/sampler/_rto.py,sha256=KIs0cDEoYK5I35RwO9fr5eKWeINLsmTLSVBnLdZmzzM,11921
80
80
  cuqi/sampler/_sampler.py,sha256=TkZ_WAS-5Q43oICa-Elc2gftsRTBd7PEDUMDZ9tTGmU,5712
81
81
  cuqi/samples/__init__.py,sha256=vCs6lVk-pi8RBqa6cIN5wyn6u-K9oEf1Na4k1ZMrYv8,44
82
82
  cuqi/samples/_samples.py,sha256=hUc8OnCF9CTCuDTrGHwwzv3wp8mG_6vsJAFvuQ-x0uA,35832
@@ -87,8 +87,8 @@ cuqi/testproblem/_testproblem.py,sha256=x769LwwRdJdzIiZkcQUGb_5-vynNTNALXWKato7s
87
87
  cuqi/utilities/__init__.py,sha256=H7xpJe2UinjZftKvE2JuXtTi4DqtkR6uIezStAXwfGg,428
88
88
  cuqi/utilities/_get_python_variable_name.py,sha256=QwlBVj2koJRA8s8pWd554p7-ElcI7HUwY32HknaR92E,1827
89
89
  cuqi/utilities/_utilities.py,sha256=Jc4knn80vLoA7kgw9FzXwKVFGaNBOXiA9kgvltZU3Ao,11777
90
- CUQIpy-1.2.0.post0.dev247.dist-info/LICENSE,sha256=kJWRPrtRoQoZGXyyvu50Uc91X6_0XRaVfT0YZssicys,10799
91
- CUQIpy-1.2.0.post0.dev247.dist-info/METADATA,sha256=5WxgqbTOMtOtFJbMPvreuobDjNJCuIpVbbUrNzZLNpk,18496
92
- CUQIpy-1.2.0.post0.dev247.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
93
- CUQIpy-1.2.0.post0.dev247.dist-info/top_level.txt,sha256=AgmgMc6TKfPPqbjV0kvAoCBN334i_Lwwojc7HE3ZwD0,5
94
- CUQIpy-1.2.0.post0.dev247.dist-info/RECORD,,
90
+ CUQIpy-1.2.0.post0.dev294.dist-info/LICENSE,sha256=kJWRPrtRoQoZGXyyvu50Uc91X6_0XRaVfT0YZssicys,10799
91
+ CUQIpy-1.2.0.post0.dev294.dist-info/METADATA,sha256=2egodevuhjO5J4cPodLUGRymsjvvR6zYFueC7bfwG18,18496
92
+ CUQIpy-1.2.0.post0.dev294.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
93
+ CUQIpy-1.2.0.post0.dev294.dist-info/top_level.txt,sha256=AgmgMc6TKfPPqbjV0kvAoCBN334i_Lwwojc7HE3ZwD0,5
94
+ CUQIpy-1.2.0.post0.dev294.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-11T15:43:05+0300",
11
+ "date": "2024-11-11T14:11:48+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "f4befbfe86448fa686d551b3fc0a507ad2ef6035",
15
- "version": "1.2.0.post0.dev247"
14
+ "full-revisionid": "231aec2928a511ab48f0c9628b93646ef51c3c8f",
15
+ "version": "1.2.0.post0.dev294"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -74,8 +74,8 @@ class UGLA(Sampler):
74
74
  return self.target.model
75
75
 
76
76
  @property
77
- def data(self):
78
- return self.target.data
77
+ def _data(self):
78
+ return self.target.data - self.target.model._shift
79
79
 
80
80
  def _precompute(self):
81
81
 
@@ -89,7 +89,7 @@ class UGLA(Sampler):
89
89
  return W.sqrt() @ D
90
90
  self.Lk_fun = Lk_fun
91
91
 
92
- self._m = len(self.data)
92
+ self._m = len(self._data)
93
93
  self._L1 = self.likelihood.distribution.sqrtprec
94
94
 
95
95
  # If prior location is scalar, repeat it to match dimensions
@@ -101,17 +101,17 @@ class UGLA(Sampler):
101
101
  # Initial Laplace approx
102
102
  self._L2 = Lk_fun(self.initial_point)
103
103
  self._L2mu = self._L2@self._priorloc
104
- self._b_tild = np.hstack([self._L1@self.data, self._L2mu])
104
+ self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
105
105
 
106
106
  # Least squares form
107
107
  def M(x, flag):
108
108
  if flag == 1:
109
- out1 = self._L1 @ self.model.forward(x)
109
+ out1 = self._L1 @ self.model._forward_func_no_shift(x) # Use forward function which excludes shift
110
110
  out2 = np.sqrt(1/self.prior.scale)*(self._L2 @ x)
111
111
  out = np.hstack([out1, out2])
112
112
  elif flag == 2:
113
113
  idx = int(self._m)
114
- out1 = self.model.adjoint(self._L1.T@x[:idx])
114
+ out1 = self.model._adjoint_func_no_shift(self._L1.T@x[:idx])
115
115
  out2 = np.sqrt(1/self.prior.scale)*(self._L2.T @ x[idx:])
116
116
  out = out1 + out2
117
117
  return out
@@ -121,7 +121,7 @@ class UGLA(Sampler):
121
121
  # Update Laplace approximation
122
122
  self._L2 = self.Lk_fun(self.current_point)
123
123
  self._L2mu = self._L2@self._priorloc
124
- self._b_tild = np.hstack([self._L1@self.data, self._L2mu])
124
+ self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
125
125
 
126
126
  # Sample from approximate posterior
127
127
  e = np.random.randn(len(self._b_tild))
@@ -139,9 +139,9 @@ class UGLA(Sampler):
139
139
  if not isinstance(self.target, cuqi.distribution.Posterior):
140
140
  raise ValueError(f"To initialize an object of type {self.__class__}, 'target' need to be of type 'cuqi.distribution.Posterior'.")
141
141
 
142
- # Check Linear model
143
- if not isinstance(self.likelihood.model, cuqi.model.LinearModel):
144
- raise TypeError("Model needs to be linear")
142
+ # Check Affine model
143
+ if not isinstance(self.likelihood.model, cuqi.model.AffineModel):
144
+ raise TypeError("Model needs to be affine or linear")
145
145
 
146
146
  # Check Gaussian likelihood
147
147
  if not hasattr(self.likelihood.distribution, "sqrtprec"):
@@ -11,7 +11,7 @@ class LinearRTO(Sampler):
11
11
  """
12
12
  Linear RTO (Randomize-Then-Optimize) sampler.
13
13
 
14
- Samples posterior related to the inverse problem with Gaussian likelihood and prior, and where the forward model is Linear.
14
+ Samples posterior related to the inverse problem with Gaussian likelihood and prior, and where the forward model is linear or more generally affine.
15
15
 
16
16
  Parameters
17
17
  ------------
@@ -22,7 +22,7 @@ class LinearRTO(Sampler):
22
22
 
23
23
  Here:
24
24
  data: is a m-dimensional numpy array containing the measured data.
25
- model: is a m by n dimensional matrix or LinearModel representing the forward model.
25
+ model: is a m by n dimensional matrix, AffineModel or LinearModel representing the forward model.
26
26
  L_sqrtprec: is the squareroot of the precision matrix of the Gaussian likelihood.
27
27
  P_mean: is the prior mean.
28
28
  P_sqrtprec: is the squareroot of the precision matrix of the Gaussian mean.
@@ -71,12 +71,15 @@ class LinearRTO(Sampler):
71
71
 
72
72
  @property
73
73
  def model(self):
74
- return self.target.model
75
-
74
+ return self.target.model
75
+
76
76
  @property
77
- def data(self):
78
- return self.target.data
79
-
77
+ def models(self):
78
+ if isinstance(self.target, cuqi.distribution.Posterior):
79
+ return [self.target.model]
80
+ elif isinstance(self.target, cuqi.distribution.MultipleLikelihoodPosterior):
81
+ return self.target.models
82
+
80
83
  def _precompute(self):
81
84
  L1 = [likelihood.distribution.sqrtprec for likelihood in self.likelihoods]
82
85
  L2 = self.prior.sqrtprec
@@ -84,8 +87,7 @@ class LinearRTO(Sampler):
84
87
 
85
88
  # pre-computations
86
89
  self.n = self.prior.dim
87
- self.b_tild = np.hstack([L@likelihood.data for (L, likelihood) in zip(L1, self.likelihoods)]+ [L2mu])
88
-
90
+ self.b_tild = np.hstack([L@(likelihood.data - model._shift) for (L, likelihood, model) in zip(L1, self.likelihoods, self.models)]+ [L2mu]) # With shift from AffineModel
89
91
  callability = [callable(likelihood.model) for likelihood in self.likelihoods]
90
92
  notcallability = [not c for c in callability]
91
93
  if all(notcallability):
@@ -94,7 +96,7 @@ class LinearRTO(Sampler):
94
96
  # in this case, model is a function doing forward and backward operations
95
97
  def M(x, flag):
96
98
  if flag == 1:
97
- out1 = [L @ likelihood.model.forward(x) for (L, likelihood) in zip(L1, self.likelihoods)]
99
+ out1 = [L @ likelihood.model._forward_func_no_shift(x) for (L, likelihood) in zip(L1, self.likelihoods)] # Use forward function which excludes shift
98
100
  out2 = L2 @ x
99
101
  out = np.hstack(out1 + [out2])
100
102
  elif flag == 2:
@@ -103,7 +105,7 @@ class LinearRTO(Sampler):
103
105
  out1 = np.zeros(self.n)
104
106
  for likelihood in self.likelihoods:
105
107
  idx_end += len(likelihood.data)
106
- out1 += likelihood.model.adjoint(likelihood.distribution.sqrtprec.T@x[idx_start:idx_end])
108
+ out1 += likelihood.model._adjoint_func_no_shift(likelihood.distribution.sqrtprec.T@x[idx_start:idx_end])
107
109
  idx_start = idx_end
108
110
  out2 = L2.T @ x[idx_end:]
109
111
  out = out1 + out2
@@ -129,16 +131,16 @@ class LinearRTO(Sampler):
129
131
 
130
132
  # Check Linear model and Gaussian likelihood(s)
131
133
  if isinstance(self.target, cuqi.distribution.Posterior):
132
- if not isinstance(self.model, cuqi.model.LinearModel):
133
- raise TypeError("Model needs to be linear")
134
+ if not isinstance(self.model, cuqi.model.AffineModel):
135
+ raise TypeError("Model needs to be linear or more generally affine")
134
136
 
135
137
  if not hasattr(self.likelihood.distribution, "sqrtprec"):
136
138
  raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")
137
139
 
138
140
  elif isinstance(self.target, cuqi.distribution.MultipleLikelihoodPosterior): # Elif used for further alternatives, e.g., stacked posterior
139
141
  for likelihood in self.likelihoods:
140
- if not isinstance(likelihood.model, cuqi.model.LinearModel):
141
- raise TypeError("Model needs to be linear")
142
+ if not isinstance(likelihood.model, cuqi.model.AffineModel):
143
+ raise TypeError("Model needs to be linear or more generally affine")
142
144
 
143
145
  if not hasattr(likelihood.distribution, "sqrtprec"):
144
146
  raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")
@@ -396,16 +396,19 @@ class Sampler(ABC):
396
396
  """ Return a string representation of the sampler. """
397
397
  if self.target is None:
398
398
  return f"Sampler: {self.__class__.__name__} \n Target: None"
399
- self._ensure_initialized()
400
- state = self.get_state()
401
- msg = f" Sampler: \n\t {self.__class__.__name__} \n Target: \n \t {self.target} \n Current state: \n"
402
- # Sort keys alphabetically
403
- keys = sorted(state['state'].keys())
404
- # Put _ in the end
405
- keys = [key for key in keys if key[0] != '_'] + [key for key in keys if key[0] == '_']
406
- for key in keys:
407
- value = state['state'][key]
408
- msg += f"\t {key}: {value} \n"
399
+ else:
400
+ msg = f"Sampler: {self.__class__.__name__} \n Target: \n \t {self.target} "
401
+
402
+ if self._is_initialized:
403
+ state = self.get_state()
404
+ msg += f"\n Current state: \n"
405
+ # Sort keys alphabetically
406
+ keys = sorted(state['state'].keys())
407
+ # Put _ in the end
408
+ keys = [key for key in keys if key[0] != '_'] + [key for key in keys if key[0] == '_']
409
+ for key in keys:
410
+ value = state['state'][key]
411
+ msg += f"\t {key}: {value} \n"
409
412
  return msg
410
413
 
411
414
  class ProposalBasedSampler(Sampler, ABC):
cuqi/model/__init__.py CHANGED
@@ -1 +1 @@
1
- from ._model import Model, LinearModel, PDEModel
1
+ from ._model import Model, LinearModel, PDEModel, AffineModel
cuqi/model/_model.py CHANGED
@@ -469,8 +469,126 @@ class Model(object):
469
469
 
470
470
  def __repr__(self) -> str:
471
471
  return "CUQI {}: {} -> {}.\n Forward parameters: {}.".format(self.__class__.__name__,self.domain_geometry,self.range_geometry,cuqi.utilities.get_non_default_args(self))
472
-
473
- class LinearModel(Model):
472
+
473
+
474
+ class AffineModel(Model):
475
+ """ Model class representing an affine model, i.e. a linear operator with a fixed shift. For linear models, represented by a linear operator only, see :class:`~cuqi.model.LinearModel`.
476
+
477
+ The affine model is defined as:
478
+
479
+ .. math::
480
+
481
+ x \\mapsto Ax + shift
482
+
483
+ where :math:`A` is the linear operator and :math:`shift` is the shift.
484
+
485
+ Parameters
486
+ ----------
487
+
488
+ linear_operator : 2d ndarray, callable function or cuqi.model.LinearModel
489
+ The linear operator. If ndarray is given, the operator is assumed to be a matrix.
490
+
491
+ shift : scalar or array_like
492
+ The shift to be added to the forward operator.
493
+
494
+ linear_operator_adjoint : callable function, optional
495
+ The adjoint of the linear operator. Also used for computing gradients.
496
+
497
+ range_geometry : cuqi.geometry.Geometry
498
+ The geometry representing the range.
499
+
500
+ domain_geometry : cuqi.geometry.Geometry
501
+ The geometry representing the domain.
502
+
503
+ """
504
+
505
+ def __init__(self, linear_operator, shift, linear_operator_adjoint=None, range_geometry=None, domain_geometry=None):
506
+
507
+ # If input represents a matrix, extract needed properties from it
508
+ if hasattr(linear_operator, '__matmul__') and hasattr(linear_operator, 'T'):
509
+ if linear_operator_adjoint is not None:
510
+ raise ValueError("Adjoint of linear operator should not be provided when linear operator is a matrix. If you want to provide an adjoint, use a callable function for the linear operator.")
511
+
512
+ matrix = linear_operator
513
+
514
+ linear_operator = lambda x: matrix@x
515
+ linear_operator_adjoint = lambda y: matrix.T@y
516
+
517
+ if range_geometry is None:
518
+ if hasattr(matrix, 'shape'):
519
+ range_geometry = _DefaultGeometry1D(grid=matrix.shape[0])
520
+ elif isinstance(matrix, LinearModel):
521
+ range_geometry = matrix.range_geometry
522
+
523
+ if domain_geometry is None:
524
+ if hasattr(matrix, 'shape'):
525
+ domain_geometry = _DefaultGeometry1D(grid=matrix.shape[1])
526
+ elif isinstance(matrix, LinearModel):
527
+ domain_geometry = matrix.domain_geometry
528
+ else:
529
+ matrix = None
530
+
531
+ # Ensure that the operators are a callable functions (either provided or created from matrix)
532
+ if not callable(linear_operator):
533
+ raise TypeError("Linear operator must be defined as a matrix or a callable function of some kind")
534
+ if linear_operator_adjoint is not None and not callable(linear_operator_adjoint):
535
+ raise TypeError("Linear operator adjoint must be defined as a callable function of some kind")
536
+
537
+ # Check size of shift and match against range_geometry
538
+ if not np.isscalar(shift):
539
+ if len(shift) != range_geometry.par_dim:
540
+ raise ValueError("The shift should have the same dimension as the range geometry.")
541
+
542
+ # Initialize Model class
543
+ super().__init__(linear_operator, range_geometry, domain_geometry)
544
+
545
+ # Store matrix privately
546
+ self._matrix = matrix
547
+
548
+ # Store shift as private attribute
549
+ self._shift = shift
550
+
551
+ # Store linear operator privately
552
+ self._linear_operator = linear_operator
553
+
554
+ # Store adjoint function
555
+ self._linear_operator_adjoint = linear_operator_adjoint
556
+
557
+ # Define gradient
558
+ self._gradient_func = lambda direction, wrt: linear_operator_adjoint(direction)
559
+
560
+ # Update forward function to include shift (overwriting the one from Model class)
561
+ self._forward_func = lambda *args, **kwargs: linear_operator(*args, **kwargs) + shift
562
+
563
+ # Use arguments from user's callable linear operator (overwriting those found by Model class)
564
+ self._non_default_args = cuqi.utilities.get_non_default_args(linear_operator)
565
+
566
+ @property
567
+ def shift(self):
568
+ """ The shift of the affine model. """
569
+ return self._shift
570
+
571
+ @shift.setter
572
+ def shift(self, value):
573
+ """ Update the shift of the affine model. Updates both the shift value and the underlying forward function. """
574
+ self._shift = value
575
+ self._forward_func = lambda *args, **kwargs: self._linear_operator(*args, **kwargs) + value
576
+
577
+ def _forward_func_no_shift(self, x, is_par=True):
578
+ """ Helper function for computing the forward operator without the shift. """
579
+ return self._apply_func(self._linear_operator,
580
+ self.range_geometry,
581
+ self.domain_geometry,
582
+ x, is_par)
583
+
584
+ def _adjoint_func_no_shift(self, y, is_par=True):
585
+ """ Helper function for computing the adjoint operator without the shift. """
586
+ return self._apply_func(self._linear_operator_adjoint,
587
+ self.domain_geometry,
588
+ self.range_geometry,
589
+ y, is_par)
590
+
591
+ class LinearModel(AffineModel):
474
592
  """Model based on a Linear forward operator.
475
593
 
476
594
  Parameters
@@ -534,45 +652,11 @@ class LinearModel(Model):
534
652
  Note that you would need to specify the range and domain geometries in this
535
653
  case as they cannot be inferred from the forward and adjoint functions.
536
654
  """
537
- # Linear forward model with forward and adjoint (transpose).
538
655
 
539
- def __init__(self,forward,adjoint=None,range_geometry=None,domain_geometry=None):
540
- #Assume forward is matrix if not callable (TODO: add more checks)
541
- if not callable(forward):
542
- forward_func = lambda x: self._matrix@x
543
- adjoint_func = lambda y: self._matrix.T@y
544
- matrix = forward
545
- else:
546
- forward_func = forward
547
- adjoint_func = adjoint
548
- matrix = None
549
-
550
- #Check if input is callable
551
- if callable(adjoint_func) is not True:
552
- raise TypeError("Adjoint needs to be callable function of some kind")
553
-
554
- # Use matrix to derive range_geometry and domain_geometry
555
- if matrix is not None:
556
- if range_geometry is None:
557
- range_geometry = _DefaultGeometry1D(grid=matrix.shape[0])
558
- if domain_geometry is None:
559
- domain_geometry = _DefaultGeometry1D(grid=matrix.shape[1])
560
-
561
- #Initialize Model class
562
- super().__init__(forward_func,range_geometry,domain_geometry)
563
-
564
- #Add adjoint
565
- self._adjoint_func = adjoint_func
566
-
567
- #Store matrix privately
568
- self._matrix = matrix
569
-
570
- #Add gradient
571
- self._gradient_func = lambda direction, wrt: self._adjoint_func(direction)
656
+ def __init__(self, forward, adjoint=None, range_geometry=None, domain_geometry=None):
572
657
 
573
- # if matrix is not None:
574
- # assert(self.range_dim == matrix.shape[0]), "The parameter 'forward' dimensions are inconsistent with the parameter 'range_geometry'"
575
- # assert(self.domain_dim == matrix.shape[1]), "The parameter 'forward' dimensions are inconsistent with parameter 'domain_geometry'"
658
+ #Initialize as AffineModel with shift=0
659
+ super().__init__(forward, 0, adjoint, range_geometry, domain_geometry)
576
660
 
577
661
  def adjoint(self, y, is_par=True):
578
662
  """ Adjoint of the model.
@@ -590,16 +674,21 @@ class LinearModel(Model):
590
674
  ndarray or cuqi.array.CUQIarray
591
675
  The adjoint model output. Always returned as parameters.
592
676
  """
593
- return self._apply_func(self._adjoint_func,
677
+ if self._linear_operator_adjoint is None:
678
+ raise ValueError("No adjoint operator was provided for this model.")
679
+ return self._apply_func(self._linear_operator_adjoint,
594
680
  self.domain_geometry,
595
681
  self.range_geometry,
596
682
  y, is_par)
597
683
 
598
-
684
+ def __matmul__(self, x):
685
+ return self.forward(x)
686
+
599
687
  def get_matrix(self):
600
688
  """
601
689
  Returns an ndarray with the matrix representing the forward operator.
602
690
  """
691
+
603
692
  if self._matrix is not None: #Matrix exists so return it
604
693
  return self._matrix
605
694
  else:
@@ -617,15 +706,12 @@ class LinearModel(Model):
617
706
  #Store matrix for future use
618
707
  self._matrix = mat
619
708
 
620
- return self._matrix
621
-
622
- def __matmul__(self, x):
623
- return self.forward(x)
709
+ return self._matrix
624
710
 
625
711
  @property
626
712
  def T(self):
627
713
  """Transpose of linear model. Returns a new linear model acting as the transpose."""
628
- transpose = LinearModel(self.adjoint,self.forward,self.domain_geometry,self.range_geometry)
714
+ transpose = LinearModel(self.adjoint, self.forward, self.domain_geometry, self.range_geometry)
629
715
  if self._matrix is not None:
630
716
  transpose._matrix = self._matrix.T
631
717
  return transpose
@@ -60,9 +60,9 @@ class UGLA(Sampler):
60
60
  if not isinstance(self.target, cuqi.distribution.Posterior):
61
61
  raise ValueError(f"To initialize an object of type {self.__class__}, 'target' need to be of type 'cuqi.distribution.Posterior'.")
62
62
 
63
- # Check Linear model
64
- if not isinstance(self.target.likelihood.model, cuqi.model.LinearModel):
65
- raise TypeError("Model needs to be linear")
63
+ # Check Affine model
64
+ if not isinstance(self.target.likelihood.model, cuqi.model.AffineModel):
65
+ raise TypeError("Model needs to be affine or linear")
66
66
 
67
67
  # Check Gaussian likelihood
68
68
  if not hasattr(self.target.likelihood.distribution, "sqrtprec"):
@@ -126,7 +126,7 @@ class UGLA(Sampler):
126
126
 
127
127
  # Pre-computations
128
128
  self._model = self.target.likelihood.model
129
- self._data = self.target.likelihood.data
129
+ self._data = self.target.likelihood.data - self.target.model._shift
130
130
  self._m = len(self._data)
131
131
  self._L1 = self.target.likelihood.distribution.sqrtprec
132
132
 
@@ -146,12 +146,12 @@ class UGLA(Sampler):
146
146
  # Least squares form
147
147
  def M(x, flag):
148
148
  if flag == 1:
149
- out1 = self._L1 @ self._model.forward(x)
149
+ out1 = self._L1 @ self._model._forward_func_no_shift(x) # Use forward function which excludes shift
150
150
  out2 = np.sqrt(1/self.target.prior.scale)*(self._L2 @ x)
151
151
  out = np.hstack([out1, out2])
152
152
  elif flag == 2:
153
153
  idx = int(self._m)
154
- out1 = self._model.adjoint(self._L1.T@x[:idx])
154
+ out1 = self._model._adjoint_func_no_shift(self._L1.T@x[:idx])
155
155
  out2 = np.sqrt(1/self.target.prior.scale)*(self._L2.T @ x[idx:])
156
156
  out = out1 + out2
157
157
  return out
cuqi/sampler/_rto.py CHANGED
@@ -11,7 +11,7 @@ class LinearRTO(Sampler):
11
11
  """
12
12
  Linear RTO (Randomize-Then-Optimize) sampler.
13
13
 
14
- Samples posterior related to the inverse problem with Gaussian likelihood and prior, and where the forward model is Linear.
14
+ Samples posterior related to the inverse problem with Gaussian likelihood and prior, and where the forward model is linear or more generally affine.
15
15
 
16
16
  Parameters
17
17
  ------------
@@ -22,7 +22,7 @@ class LinearRTO(Sampler):
22
22
 
23
23
  Here:
24
24
  data: is a m-dimensional numpy array containing the measured data.
25
- model: is a m by n dimensional matrix or LinearModel representing the forward model.
25
+ model: is a m by n dimensional matrix, AffineModel or LinearModel representing the forward model.
26
26
  L_sqrtprec: is the squareroot of the precision matrix of the Gaussian likelihood.
27
27
  P_mean: is the prior mean.
28
28
  P_sqrtprec: is the squareroot of the precision matrix of the Gaussian mean.
@@ -59,8 +59,8 @@ class LinearRTO(Sampler):
59
59
  model = cuqi.model.LinearModel(model)
60
60
 
61
61
  # Check model input
62
- if not isinstance(model, cuqi.model.LinearModel):
63
- raise TypeError("Model needs to be cuqi.model.LinearModel or matrix")
62
+ if not isinstance(model, cuqi.model.AffineModel):
63
+ raise TypeError("Model needs to be cuqi.model.AffineModel or matrix")
64
64
 
65
65
  # Likelihood
66
66
  L = cuqi.distribution.Gaussian(model, sqrtprec=L_sqrtprec).to_likelihood(data)
@@ -95,7 +95,7 @@ class LinearRTO(Sampler):
95
95
 
96
96
  # pre-computations
97
97
  self.n = len(self.x0)
98
- self.b_tild = np.hstack([L@likelihood.data for (L, likelihood) in zip(L1, self.likelihoods)]+ [L2mu])
98
+ self.b_tild = np.hstack([L@(likelihood.data - model._shift) for (L, likelihood, model) in zip(L1, self.likelihoods, self.models)]+ [L2mu])
99
99
 
100
100
  callability = [callable(likelihood.model) for likelihood in self.likelihoods]
101
101
  notcallability = [not c for c in callability]
@@ -105,7 +105,7 @@ class LinearRTO(Sampler):
105
105
  # in this case, model is a function doing forward and backward operations
106
106
  def M(x, flag):
107
107
  if flag == 1:
108
- out1 = [L @ likelihood.model.forward(x) for (L, likelihood) in zip(L1, self.likelihoods)]
108
+ out1 = [L @ likelihood.model._forward_func_no_shift(x) for (L, likelihood) in zip(L1, self.likelihoods)] # Use forward function which excludes shift
109
109
  out2 = L2 @ x
110
110
  out = np.hstack(out1 + [out2])
111
111
  elif flag == 2:
@@ -114,7 +114,7 @@ class LinearRTO(Sampler):
114
114
  out1 = np.zeros(self.n)
115
115
  for likelihood in self.likelihoods:
116
116
  idx_end += len(likelihood.data)
117
- out1 += likelihood.model.adjoint(likelihood.distribution.sqrtprec.T@x[idx_start:idx_end])
117
+ out1 += likelihood.model._adjoint_func_no_shift(likelihood.distribution.sqrtprec.T@x[idx_start:idx_end]) # Use adjoint function which excludes shift
118
118
  idx_start = idx_end
119
119
  out2 = L2.T @ x[idx_end:]
120
120
  out = out1 + out2
@@ -143,8 +143,11 @@ class LinearRTO(Sampler):
143
143
  return self.target.model
144
144
 
145
145
  @property
146
- def data(self):
147
- return self.target.data
146
+ def models(self):
147
+ if isinstance(self.target, cuqi.distribution.Posterior):
148
+ return [self.target.model]
149
+ elif isinstance(self.target, cuqi.distribution.MultipleLikelihoodPosterior):
150
+ return self.target.models
148
151
 
149
152
  def _sample(self, N, Nb):
150
153
  Ns = N+Nb # number of simulations
@@ -175,8 +178,8 @@ class LinearRTO(Sampler):
175
178
 
176
179
  # Check Linear model and Gaussian likelihood(s)
177
180
  if isinstance(self.target, cuqi.distribution.Posterior):
178
- if not isinstance(self.model, cuqi.model.LinearModel):
179
- raise TypeError("Model needs to be linear")
181
+ if not isinstance(self.model, cuqi.model.AffineModel):
182
+ raise TypeError("Model needs to be linear or affine")
180
183
 
181
184
  if not hasattr(self.likelihood.distribution, "sqrtprec"):
182
185
  raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")