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
@@ -0,0 +1,129 @@
1
+ import numpy as np
2
+ from scipy.special import erf
3
+ from cuqi.utilities import force_ndarray
4
+ from cuqi.distribution import Distribution
5
+ from cuqi.distribution import Normal
6
+
7
+ class TruncatedNormal(Distribution):
8
+ """
9
+ Truncated Normal probability distribution.
10
+
11
+ Generates instance of cuqi.distribution.TruncatedNormal.
12
+ It allows the user to specify upper and lower bounds on random variables
13
+ represented by a Normal distribution. This distribution is suitable for a
14
+ small dimension setup (e.g. `dim`=3 or 4). Using TruncatedNormal
15
+ Distribution with a larger dimension can lead to a high rejection rate when
16
+ used within MCMC samplers.
17
+
18
+ The variables of this distribution are iid.
19
+
20
+
21
+ Parameters
22
+ ------------
23
+ mean : float or array_like of floats
24
+ mean of distribution
25
+ std : float or array_like of floats
26
+ standard deviation
27
+ low : float or array_like of floats
28
+ lower bound of the distribution
29
+ high : float or array_like of floats
30
+ upper bound of the distribution
31
+
32
+ Example
33
+ -----------
34
+ .. code-block:: python
35
+
36
+ #Generate Normal with mean 0, standard deviation 1 and bounds [-2,2]
37
+ p = cuqi.distribution.TruncatedNormal(mean=0, std=1, low=-2, high=2)
38
+ samples = p.sample(5000)
39
+ """
40
+ def __init__(self, mean=None, std=None, low=-np.inf, high=np.inf, is_symmetric=False, **kwargs):
41
+ # Init from abstract distribution class
42
+ super().__init__(is_symmetric=is_symmetric, **kwargs)
43
+
44
+ # Init specific to this distribution
45
+ self.mean = mean
46
+ self.std = std
47
+ self.low = low
48
+ self.high = high
49
+
50
+ # Init underlying normal distribution
51
+ self._normal = Normal(self.mean, self.std, is_symmetric=True, **kwargs)
52
+
53
+ @property
54
+ def mean(self):
55
+ """ Mean of the distribution """
56
+ return self._mean
57
+
58
+ @mean.setter
59
+ def mean(self, value):
60
+ self._mean = force_ndarray(value, flatten=True)
61
+ if hasattr(self, '_normal'):
62
+ self._normal.mean = self._mean
63
+
64
+ @property
65
+ def std(self):
66
+ """ Std of the distribution """
67
+ return self._std
68
+
69
+ @std.setter
70
+ def std(self, value):
71
+ self._std = force_ndarray(value, flatten=True)
72
+ if hasattr(self, '_normal'):
73
+ self._normal.std = self._std
74
+
75
+ @property
76
+ def low(self):
77
+ """ Lower bound of the distribution """
78
+ return self._low
79
+
80
+ @low.setter
81
+ def low(self, value):
82
+ self._low = force_ndarray(value, flatten=True)
83
+
84
+ @property
85
+ def high(self):
86
+ """ Higher bound of the distribution """
87
+ return self._high
88
+
89
+ @high.setter
90
+ def high(self, value):
91
+ self._high = force_ndarray(value, flatten=True)
92
+
93
+ def logpdf(self, x):
94
+ """
95
+ Computes the unnormalized logpdf at the given values of x.
96
+ """
97
+ # the unnormalized logpdf
98
+ # check if x falls in the range between np.array a and b
99
+ if np.any(x < self.low) or np.any(x > self.high):
100
+ return -np.inf
101
+ else:
102
+ return self._normal.logpdf(x)
103
+
104
+ def _gradient(self, x, *args, **kwargs):
105
+ """
106
+ Computes the gradient of the unnormalized logpdf at the given values of x.
107
+ """
108
+ # check if x falls in the range between np.array a and b
109
+ if np.any(x < self.low) or np.any(x > self.high):
110
+ return np.nan*np.ones_like(x)
111
+ else:
112
+ return self._normal.gradient(x, *args, **kwargs)
113
+
114
+ def _sample(self, N=1, rng=None):
115
+ """
116
+ Generates random samples from the distribution.
117
+ """
118
+ max_iter = 1e9 # maximum number of trials to avoid infinite loop
119
+ samples = []
120
+ for i in range(int(max_iter)):
121
+ if len(samples) == N:
122
+ break
123
+ sample = self._normal.sample(1,rng)
124
+ if np.all(sample >= self.low) and np.all(sample <= self.high):
125
+ samples.append(sample)
126
+ # raise a error if the number of iterations exceeds max_iter
127
+ if i == max_iter-1:
128
+ raise RuntimeError("Failed to generate {} samples within {} iterations".format(N, max_iter))
129
+ return np.array(samples).T.reshape(-1,N)
@@ -1,5 +1,6 @@
1
1
  import numpy as np
2
2
  from cuqi.distribution import Distribution
3
+ from cuqi.geometry import Geometry
3
4
 
4
5
  class Uniform(Distribution):
5
6
 
@@ -21,6 +22,9 @@ class Uniform(Distribution):
21
22
  self.high = high
22
23
 
23
24
  def logpdf(self, x):
25
+ """
26
+ Evaluate the logarithm of the PDF at the given values of x.
27
+ """
24
28
  # First check whether x is outside bounds.
25
29
  # It is outside if any coordinate is outside the interval.
26
30
  if np.any(x < self.low) or np.any(x > self.high):
@@ -38,6 +42,15 @@ class Uniform(Distribution):
38
42
  return_val = np.log(1.0/v)
39
43
  return return_val
40
44
 
45
+ def gradient(self, x):
46
+ """
47
+ Computes the gradient of logpdf at the given values of x.
48
+ """
49
+ if np.any(x < self.low) or np.any(x > self.high):
50
+ return np.nan*np.ones_like(x)
51
+ else:
52
+ return np.zeros_like(x)
53
+
41
54
  def _sample(self,N=1, rng=None):
42
55
 
43
56
  if rng is not None:
@@ -45,4 +58,37 @@ class Uniform(Distribution):
45
58
  else:
46
59
  s = np.random.uniform(self.low, self.high, (N,self.dim)).T
47
60
 
48
- return s
61
+ return s
62
+
63
+ class UnboundedUniform(Distribution):
64
+ """
65
+ Unbounded uniform distribution. This is a special case of the
66
+ Uniform distribution, where the lower and upper bounds are set to
67
+ -inf and inf, respectively. This distribution is not normalizable,
68
+ and therefore cannot be sampled from. It is mainly used for
69
+ initializing non-informative priors.
70
+ Parameters
71
+ ----------
72
+ geometry : int or Geometry
73
+ The geometry of the distribution. If an integer is given, it is
74
+ interpreted as the dimension of the distribution. If a
75
+ Geometry object is given, its par_dim attribute is used.
76
+ """
77
+ def __init__(self, geometry, is_symmetric=True, **kwargs):
78
+ super().__init__(geometry=geometry, is_symmetric=is_symmetric, **kwargs)
79
+
80
+ def logpdf(self, x):
81
+ """
82
+ Evaluate the logarithm of the unnormalized PDF at the given values of x.
83
+ """
84
+ # Always return 1.0 (the unnormalized log PDF)
85
+ return 1.0
86
+
87
+ def gradient(self, x):
88
+ """
89
+ Computes the gradient of logpdf at the given values of x.
90
+ """
91
+ return np.zeros_like(x)
92
+
93
+ def _sample(self, N=1, rng=None):
94
+ raise NotImplementedError("Cannot sample from UnboundedUniform distribution")
@@ -1,2 +1,2 @@
1
- """ Experimental module for testing new features and ideas. """
2
- from . import mcmc
1
+ """ Experimental module for testing new features and ideas. """
2
+ from ._recommender import SamplerRecommender
@@ -0,0 +1,216 @@
1
+ import cuqi
2
+ import inspect
3
+ import numpy as np
4
+
5
+ # This import makes suggest_sampler easier to read
6
+ import cuqi.sampler as samplers
7
+
8
+
9
+ class SamplerRecommender(object):
10
+ """
11
+ This class can be used to automatically choose a sampler.
12
+
13
+ Parameters
14
+ ----------
15
+ target: Density or JointDistribution
16
+ Distribution to get sampler recommendations for.
17
+
18
+ exceptions: list[cuqi.sampler.Sampler], *optional*
19
+ Samplers not to be recommended.
20
+
21
+ Example
22
+ -------
23
+ .. code-block:: python
24
+ import numpy as np
25
+ from cuqi.distribution import Gamma, Gaussian, JointDistribution
26
+ from cuqi.experimental import SamplerRecommender
27
+
28
+ x = Gamma(1, 1)
29
+ y = Gaussian(np.zeros(2), cov=lambda x: 1 / x)
30
+ target = JointDistribution(y, x)(y=1)
31
+
32
+ recommender = SamplerRecommender(target)
33
+ valid_samplers = recommender.valid_samplers()
34
+ recommended_sampler = recommender.recommend()
35
+ print("Valid samplers:", valid_samplers)
36
+ print("Recommended sampler:\n", recommended_sampler)
37
+
38
+ """
39
+
40
+ def __init__(self, target:cuqi.density.Density, exceptions = []):
41
+ self._target = target
42
+ self._exceptions = exceptions
43
+ self._create_ordering()
44
+
45
+ @property
46
+ def target(self) -> cuqi.density.Density:
47
+ """ Return the target Distribution. """
48
+ return self._target
49
+
50
+ @target.setter
51
+ def target(self, value:cuqi.density.Density):
52
+ """ Set the target Distribution. Runs validation of the target. """
53
+ if value is None:
54
+ raise ValueError("Target needs to be of type cuqi.density.Density.")
55
+ self._target = value
56
+
57
+ def _create_ordering(self):
58
+ """
59
+ Every element in the ordering consists of a tuple:
60
+ (
61
+ Sampler: Class
62
+ boolean: additional conditions on the target
63
+ parameters: additional parameters to be passed to the sampler once initialized
64
+ )
65
+ """
66
+ number_of_components = np.sum(self._target.dim)
67
+
68
+ self._ordering = [
69
+ # Direct and Conjugate samplers
70
+ (samplers.Direct, True, {}),
71
+ (samplers.Conjugate, True, {}),
72
+ (samplers.ConjugateApprox, True, {}),
73
+ # Specialized samplers
74
+ (samplers.LinearRTO, True, {}),
75
+ (samplers.RegularizedLinearRTO, True, {}),
76
+ (samplers.UGLA, True, {}),
77
+ # Gradient.based samplers (Hamiltonian and Langevin)
78
+ (samplers.NUTS, True, {}),
79
+ (samplers.MALA, True, {}),
80
+ (samplers.ULA, True, {}),
81
+ # Gibbs and Componentwise samplers
82
+ (samplers.HybridGibbs, True, {"sampling_strategy" : self.recommend_HybridGibbs_sampling_strategy(as_string = False)}),
83
+ (samplers.CWMH, number_of_components <= 100, {"scale" : 0.05*np.ones(number_of_components),
84
+ "initial_point" : 0.5*np.ones(number_of_components)}),
85
+ # Proposal based samplers
86
+ (samplers.PCN, True, {"scale" : 0.02}),
87
+ (samplers.MH, number_of_components <= 1000, {}),
88
+ ]
89
+
90
+ @property
91
+ def ordering(self):
92
+ """ Returns the ordered list of recommendation rules used by the recommender. """
93
+ return self._ordering
94
+
95
+ def valid_samplers(self, as_string = True):
96
+ """
97
+ Finds all possible samplers that can be used for sampling from the target distribution.
98
+
99
+ Parameters
100
+ ----------
101
+
102
+ as_string : boolean
103
+ Whether to return the name of the sampler as a string instead of instantiating a sampler. *Optional*
104
+
105
+ """
106
+
107
+ all_samplers = [(name, cls) for name, cls in inspect.getmembers(cuqi.sampler, inspect.isclass) if issubclass(cls, cuqi.sampler.Sampler)]
108
+ valid_samplers = []
109
+
110
+ for name, sampler in all_samplers:
111
+ try:
112
+ sampler(self.target)
113
+ valid_samplers += [name if as_string else sampler]
114
+ except:
115
+ pass
116
+
117
+ # Need a separate case for HybridGibbs
118
+ if self.valid_HybridGibbs_sampling_strategy() is not None:
119
+ valid_samplers += [cuqi.sampler.HybridGibbs.__name__ if as_string else cuqi.sampler.HybridGibbs]
120
+
121
+ return valid_samplers
122
+
123
+ def valid_HybridGibbs_sampling_strategy(self, as_string = True):
124
+ """
125
+ Find all possible sampling strategies to be used with the HybridGibbs sampler.
126
+ Returns None if no sampler could be suggested for at least one conditional distribution.
127
+
128
+ Parameters
129
+ ----------
130
+
131
+ as_string : boolean
132
+ Whether to return the name of the samplers in the sampling strategy as a string instead of instantiating samplers. *Optional*
133
+
134
+
135
+ """
136
+
137
+ if not isinstance(self.target, cuqi.distribution.JointDistribution):
138
+ return None
139
+
140
+ par_names = self.target.get_parameter_names()
141
+
142
+ valid_samplers = dict()
143
+ for par_name in par_names:
144
+ conditional_params = {par_name_: np.ones(self.target.dim[i]) for i, par_name_ in enumerate(par_names) if par_name_ != par_name}
145
+ conditional = self.target(**conditional_params)
146
+
147
+ recommender = SamplerRecommender(conditional)
148
+ samplers = recommender.valid_samplers(as_string)
149
+ if len(samplers) == 0:
150
+ return None
151
+
152
+ valid_samplers[par_name] = samplers
153
+
154
+ return valid_samplers
155
+
156
+ def recommend(self, as_string = False):
157
+ """
158
+ Suggests a possible sampler that can be used for sampling from the target distribution.
159
+ Return None if no sampler could be suggested.
160
+
161
+ Parameters
162
+ ----------
163
+
164
+ as_string : boolean
165
+ Whether to return the name of the sampler as a string instead of instantiating a sampler. *Optional*
166
+
167
+ """
168
+
169
+ valid_samplers = self.valid_samplers(as_string = False)
170
+
171
+ for suggestion, flag, values in self._ordering:
172
+ if flag and (suggestion in valid_samplers) and (suggestion not in self._exceptions):
173
+ # Sampler found
174
+ if as_string:
175
+ return suggestion.__name__
176
+ else:
177
+ return suggestion(self.target, **values)
178
+
179
+ # No sampler can be suggested
180
+ raise ValueError("Cannot suggest any sampler. Either the provided distribution is incorrectly defined or there are too many exceptions provided.")
181
+
182
+ def recommend_HybridGibbs_sampling_strategy(self, as_string = False):
183
+ """
184
+ Suggests a possible sampling strategy to be used with the HybridGibbs sampler.
185
+ Returns None if no sampler could be suggested for at least one conditional distribution.
186
+
187
+ Parameters
188
+ ----------
189
+
190
+ target : `cuqi.distribution.JointDistribution`
191
+ The target distribution get a sampling strategy for.
192
+
193
+ as_string : boolean
194
+ Whether to return the name of the samplers in the sampling strategy as a string instead of instantiating samplers. *Optional*
195
+
196
+ """
197
+
198
+ if not isinstance(self.target, cuqi.distribution.JointDistribution):
199
+ return None
200
+
201
+ par_names = self.target.get_parameter_names()
202
+
203
+ suggested_samplers = dict()
204
+ for par_name in par_names:
205
+ conditional_params = {par_name_: np.ones(self.target.dim[i]) for i, par_name_ in enumerate(par_names) if par_name_ != par_name}
206
+ conditional = self.target(**conditional_params)
207
+
208
+ recommender = SamplerRecommender(conditional, exceptions = self._exceptions.copy())
209
+ sampler = recommender.recommend(as_string = as_string)
210
+
211
+ if sampler is None:
212
+ return None
213
+
214
+ suggested_samplers[par_name] = sampler
215
+
216
+ return suggested_samplers
cuqi/geometry/__init__.py CHANGED
@@ -16,6 +16,8 @@ from ._geometry import (
16
16
  StepExpansion
17
17
  )
18
18
 
19
+ from ._product_geometry import _ProductGeometry
20
+
19
21
 
20
22
  # TODO: We will remove the use of identity geometries in the future
21
23
  _identity_geometries = [_DefaultGeometry1D, _DefaultGeometry2D, Continuous1D, Continuous2D, Discrete, Image2D]
@@ -225,7 +225,18 @@ class Geometry(ABC):
225
225
  return self._all_values_equal(obj)
226
226
 
227
227
  def __repr__(self) -> str:
228
- return "{}{}".format(self.__class__.__name__,self.par_shape)
228
+ if self.par_shape == self.fun_shape:
229
+ return "{}[{}]".format(self.__class__.__name__,
230
+ self.par_shape if len(self.par_shape) != 1 else self.par_shape[0])
231
+ return "{}[{}: {}]".format(
232
+ self.__class__.__name__,
233
+ self.par_shape if len(self.par_shape) != 1 else self.par_shape[0],
234
+ (
235
+ self.fun_shape
236
+ if (self.fun_shape is None or len(self.fun_shape) != 1)
237
+ else self.fun_shape[0]
238
+ ),
239
+ )
229
240
 
230
241
  def _all_values_equal(self, obj):
231
242
  """Returns true of all values of the object and self are equal"""
@@ -522,6 +533,9 @@ class Image2D(Geometry):
522
533
  Plotting is handled via matplotlib.pyplot.imshow.
523
534
  Colormap is defaulted to grayscale.
524
535
 
536
+ A How-To guide on the use of Image2D as the domain/range geometry of a CUQI
537
+ :class:`Model` is available `here <https://cuqi-dtu.github.io/CUQIpy/user/_auto_howtos/Image2D.html>`_.
538
+
525
539
  Parameters
526
540
  -----------
527
541
  im_shape : tuple
@@ -0,0 +1,181 @@
1
+ from cuqi.geometry import Geometry
2
+ import numpy as np
3
+
4
+ class _ProductGeometry(Geometry):
5
+ """ A class for representing a product geometry. A product geometry
6
+ represents the product space of multiple geometries of type :class:`Geometry`.
7
+ See the example below for a product geometry of two geometries.
8
+
9
+ Parameters
10
+ ----------
11
+ \*geometries : cuqi.geometry.Geometry
12
+ The geometries to be combined into a product geometry. Each geometry
13
+ is passed as a comma-separated argument.
14
+
15
+ Example
16
+ -------
17
+ .. code-block:: python
18
+ import numpy as np
19
+ from cuqi.geometry import Continuous1D, Discrete
20
+ from cuqi.geometry import _ProductGeometry
21
+ geometry1 = Continuous1D(np.linspace(0, 1, 100))
22
+ geometry2 = Discrete(["sound_speed"])
23
+ product_geometry = _ProductGeometry(geometry1, geometry2)
24
+ """
25
+
26
+ def __init__(self, *geometries):
27
+ self.geometries = geometries
28
+
29
+ @property
30
+ def geometries(self):
31
+ """List of geometries that are combined to form the product geometry."""
32
+ return self._geometries
33
+
34
+ @geometries.setter
35
+ def geometries(self, geometries):
36
+ # Check if all geometries are of type Geometry.
37
+ for g in geometries:
38
+ if not isinstance(g, Geometry):
39
+ raise TypeError(
40
+ "All geometries must be of type Geometry. "
41
+ "Received: {}".format(type(g))
42
+ )
43
+ self._geometries = geometries
44
+
45
+ @property
46
+ def fun_shape(self):
47
+ """Shape of the function representation. Returns a tuple, where
48
+ each element of the tuple is the shape of the function
49
+ representation of each geometry."""
50
+ return tuple([g.fun_shape for g in self.geometries])
51
+
52
+ @property
53
+ def fun_dim(self):
54
+ """Dimension of the function representation which is the sum of
55
+ the function representation dimensions of each geometry."""
56
+ return sum([g.fun_dim for g in self.geometries])
57
+
58
+ @property
59
+ def par_shape(self):
60
+ """Shape of the parameter representation. Returns a tuple, where
61
+ each element of the tuple is the shape of the parameter
62
+ representation of each geometry."""
63
+ return tuple([g.par_shape for g in self.geometries])
64
+
65
+ @property
66
+ def par_dim(self):
67
+ """Dimension of the parameter representation which is the sum of
68
+ the parameter representation dimensions of each geometry."""
69
+ return sum(self.par_dim_list)
70
+
71
+ @property
72
+ def par_dim_list(self):
73
+ """List of the parameter representation dimensions of each
74
+ geometry. This property is useful for indexing a stacked parameter
75
+ vector."""
76
+ return [g.par_dim for g in self.geometries]
77
+
78
+ @property
79
+ def stacked_par_split_indices(self):
80
+ """Indices at which the stacked parameter vector should be split
81
+ to obtain the parameter vectors for each geometry. For example, if
82
+ the stacked parameter vector is [1, 2, 3, 4, 5, 6] and the parameter
83
+ vectors for each geometry are [1, 2], [3, 4], and [5, 6], then the
84
+ split indices are [2, 4]"""
85
+ return np.cumsum(self.par_dim_list[:-1])
86
+
87
+ @property
88
+ def number_of_geometries(self):
89
+ """Number of geometries in the product geometry."""
90
+ return len(self.geometries)
91
+
92
+ def _split_par(self, par):
93
+ """Splits a stacked parameter vector into parameter vectors for each
94
+ geometry."""
95
+ return tuple(np.split(par, self.stacked_par_split_indices))
96
+
97
+ def _plot(self, values, **kwargs):
98
+ """Plotting function for the product geometry."""
99
+ raise NotImplementedError(
100
+ f"Plotting not implemented for {self.__class__.__name__}.")
101
+
102
+ def par2fun(self, *pars):
103
+ """Converts parameter vector(s) into function values for each
104
+ geometry. The parameter vector can be stacked (all parameters are
105
+ in one vector) or unstacked (one parameter vector corresponds to
106
+ each geometry). In all cases, the order of the parameter vectors
107
+ should follow the order of the geometries in the product, i.e., the
108
+ first parameter vector corresponds to the first geometry and so on."""
109
+
110
+ # If one argument is passed, then it is assumed that the parameter
111
+ # vector is stacked and split it.
112
+ # No effect if the parameter vector is already split and corresponds
113
+ # to one geometry.
114
+ if len(pars) == 1:
115
+ pars = self._split_par(pars[0])
116
+
117
+ # Convert parameter vectors to function values for each geometry.
118
+ funvals = []
119
+ for i, g in enumerate(self.geometries):
120
+ funval_i = g.par2fun(pars[i])
121
+ funvals.append(funval_i)
122
+ return tuple(funvals)
123
+
124
+ def fun2par(self, *funvals, stacked=False):
125
+ """Converts (multiple) function values into the corresponding
126
+ parameter vectors. If the flag stacked is set to True, then the
127
+ parameter vectors are stacked into one vector. Otherwise, the
128
+ parameter vectors are returned as a tuple. The order of function
129
+ values should follow the order of the geometries in the product,
130
+ i.e., the first function value corresponds to the first geometry
131
+ and so on."""
132
+
133
+ pars = []
134
+ for i, g in enumerate(self.geometries):
135
+ par_i = g.fun2par(funvals[i])
136
+ pars.append(par_i)
137
+
138
+ # stack parameters:
139
+ if stacked:
140
+ # if single sample
141
+ if len(pars[0].shape) == 1:
142
+ stacked_val = np.hstack(pars)
143
+ elif len(pars[0].shape) == 2:
144
+ stacked_val = np.vstack(pars)
145
+ else:
146
+ raise ValueError(
147
+ "Cannot stack parameter vectors with more than 2 dimensions."
148
+ )
149
+
150
+ return stacked_val if stacked else tuple(pars)
151
+
152
+ def vec2fun(self, *funvecs):
153
+ """Maps function vector representation, if available, to function
154
+ values. The order of the function vectors should follow the order of
155
+ the geometries in the product, i.e., the first function vector
156
+ corresponds to the first geometry and so on."""
157
+ funvals = []
158
+ for i, g in enumerate(self.geometries):
159
+ funvals.append(g.vec2fun(funvecs[i]))
160
+
161
+ return tuple(funvals)
162
+
163
+ def fun2vec(self, *funvals):
164
+ """Maps function values to a vector representation of the function
165
+ values, if available. The order of the function values should follow
166
+ the order of the geometries in the product, i.e., the first function
167
+ value corresponds to the first geometry and so on."""
168
+ funvecs = []
169
+ for i, g in enumerate(self.geometries):
170
+ funvecs.append(g.fun2vec(funvals[i]))
171
+
172
+ return tuple(funvecs)
173
+
174
+
175
+ def __repr__(self, pad="") -> str:
176
+ """Representation of the product geometry."""
177
+ string = "{}(".format(self.__class__.__name__) + "\n"
178
+ for g in self.geometries:
179
+ string += pad + " {}\n".format(g.__repr__())
180
+ string += pad + ")"
181
+ return string
@@ -1,3 +1,5 @@
1
- from ._regularizedGaussian import RegularizedGaussian, ConstrainedGaussian, NonnegativeGaussian
2
- from ._regularizedGMRF import RegularizedGMRF, ConstrainedGMRF, NonnegativeGMRF
3
- from ._regularizedUnboundedUniform import RegularizedUnboundedUniform
1
+ from ._regularized_gaussian import RegularizedGaussian, ConstrainedGaussian, NonnegativeGaussian
2
+ from ._regularized_gmrf import RegularizedGMRF, ConstrainedGMRF, NonnegativeGMRF
3
+ from ._regularized_unbounded_uniform import RegularizedUnboundedUniform
4
+ from ._restorator import RestorationPrior, MoreauYoshidaPrior, TweediePrior
5
+