CUQIpy 1.1.1.post0.dev36__py3-none-any.whl → 1.4.1.post0.dev124__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of CUQIpy might be problematic. Click here for more details.
- cuqi/__init__.py +2 -0
- cuqi/_version.py +3 -3
- cuqi/algebra/__init__.py +2 -0
- cuqi/algebra/_abstract_syntax_tree.py +358 -0
- cuqi/algebra/_ordered_set.py +82 -0
- cuqi/algebra/_random_variable.py +457 -0
- cuqi/array/_array.py +4 -13
- cuqi/config.py +7 -0
- cuqi/density/_density.py +9 -1
- cuqi/distribution/__init__.py +3 -2
- cuqi/distribution/_beta.py +7 -11
- cuqi/distribution/_cauchy.py +2 -2
- cuqi/distribution/_custom.py +0 -6
- cuqi/distribution/_distribution.py +31 -45
- cuqi/distribution/_gamma.py +7 -3
- cuqi/distribution/_gaussian.py +2 -12
- cuqi/distribution/_inverse_gamma.py +4 -10
- cuqi/distribution/_joint_distribution.py +112 -15
- cuqi/distribution/_lognormal.py +0 -7
- cuqi/distribution/{_modifiedhalfnormal.py → _modified_half_normal.py} +23 -23
- cuqi/distribution/_normal.py +34 -7
- cuqi/distribution/_posterior.py +9 -0
- cuqi/distribution/_truncated_normal.py +129 -0
- cuqi/distribution/_uniform.py +47 -1
- cuqi/experimental/__init__.py +2 -2
- cuqi/experimental/_recommender.py +216 -0
- cuqi/geometry/__init__.py +2 -0
- cuqi/geometry/_geometry.py +15 -1
- cuqi/geometry/_product_geometry.py +181 -0
- cuqi/implicitprior/__init__.py +5 -3
- cuqi/implicitprior/_regularized_gaussian.py +483 -0
- cuqi/implicitprior/{_regularizedGMRF.py → _regularized_gmrf.py} +4 -2
- cuqi/implicitprior/{_regularizedUnboundedUniform.py → _regularized_unbounded_uniform.py} +3 -2
- cuqi/implicitprior/_restorator.py +269 -0
- cuqi/legacy/__init__.py +2 -0
- cuqi/{experimental/mcmc → legacy/sampler}/__init__.py +7 -11
- cuqi/legacy/sampler/_conjugate.py +55 -0
- cuqi/legacy/sampler/_conjugate_approx.py +52 -0
- cuqi/legacy/sampler/_cwmh.py +196 -0
- cuqi/legacy/sampler/_gibbs.py +231 -0
- cuqi/legacy/sampler/_hmc.py +335 -0
- cuqi/{experimental/mcmc → legacy/sampler}/_langevin_algorithm.py +82 -111
- cuqi/legacy/sampler/_laplace_approximation.py +184 -0
- cuqi/legacy/sampler/_mh.py +190 -0
- cuqi/legacy/sampler/_pcn.py +244 -0
- cuqi/{experimental/mcmc → legacy/sampler}/_rto.py +132 -90
- cuqi/legacy/sampler/_sampler.py +182 -0
- cuqi/likelihood/_likelihood.py +9 -1
- cuqi/model/__init__.py +1 -1
- cuqi/model/_model.py +1361 -359
- cuqi/pde/__init__.py +4 -0
- cuqi/pde/_observation_map.py +36 -0
- cuqi/pde/_pde.py +134 -33
- cuqi/problem/_problem.py +93 -87
- cuqi/sampler/__init__.py +120 -8
- cuqi/sampler/_conjugate.py +376 -35
- cuqi/sampler/_conjugate_approx.py +40 -16
- cuqi/sampler/_cwmh.py +132 -138
- cuqi/{experimental/mcmc → sampler}/_direct.py +1 -1
- cuqi/sampler/_gibbs.py +288 -130
- cuqi/sampler/_hmc.py +328 -201
- cuqi/sampler/_langevin_algorithm.py +284 -100
- cuqi/sampler/_laplace_approximation.py +87 -117
- cuqi/sampler/_mh.py +47 -157
- cuqi/sampler/_pcn.py +65 -213
- cuqi/sampler/_rto.py +211 -142
- cuqi/sampler/_sampler.py +553 -136
- cuqi/samples/__init__.py +1 -1
- cuqi/samples/_samples.py +24 -18
- cuqi/solver/__init__.py +6 -4
- cuqi/solver/_solver.py +230 -26
- cuqi/testproblem/_testproblem.py +2 -3
- cuqi/utilities/__init__.py +6 -1
- cuqi/utilities/_get_python_variable_name.py +2 -2
- cuqi/utilities/_utilities.py +182 -2
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/METADATA +10 -6
- cuqipy-1.4.1.post0.dev124.dist-info/RECORD +101 -0
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/WHEEL +1 -1
- CUQIpy-1.1.1.post0.dev36.dist-info/RECORD +0 -92
- cuqi/experimental/mcmc/_conjugate.py +0 -197
- cuqi/experimental/mcmc/_conjugate_approx.py +0 -81
- cuqi/experimental/mcmc/_cwmh.py +0 -191
- cuqi/experimental/mcmc/_gibbs.py +0 -268
- cuqi/experimental/mcmc/_hmc.py +0 -470
- cuqi/experimental/mcmc/_laplace_approximation.py +0 -156
- cuqi/experimental/mcmc/_mh.py +0 -78
- cuqi/experimental/mcmc/_pcn.py +0 -89
- cuqi/experimental/mcmc/_sampler.py +0 -561
- cuqi/experimental/mcmc/_utilities.py +0 -17
- cuqi/implicitprior/_regularizedGaussian.py +0 -323
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info/licenses}/LICENSE +0 -0
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
cuqi/distribution/_uniform.py
CHANGED
|
@@ -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")
|
cuqi/experimental/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
""" Experimental module for testing new features and ideas.
|
|
2
|
-
from . import
|
|
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]
|
cuqi/geometry/_geometry.py
CHANGED
|
@@ -225,7 +225,18 @@ class Geometry(ABC):
|
|
|
225
225
|
return self._all_values_equal(obj)
|
|
226
226
|
|
|
227
227
|
def __repr__(self) -> str:
|
|
228
|
-
|
|
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
|
cuqi/implicitprior/__init__.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
3
|
-
from .
|
|
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
|
+
|