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,457 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List, Any, Union
|
|
3
|
+
from ._abstract_syntax_tree import VariableNode, Node
|
|
4
|
+
from ._ordered_set import _OrderedSet
|
|
5
|
+
import operator
|
|
6
|
+
import cuqi
|
|
7
|
+
from cuqi.distribution import Distribution
|
|
8
|
+
from copy import copy, deepcopy
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RandomVariable:
|
|
13
|
+
""" Random variable defined by a distribution with the option to apply algebraic operations on it.
|
|
14
|
+
|
|
15
|
+
Random variables allow for the definition of Bayesian Problems in a natural way. In the context
|
|
16
|
+
of code, the random variable can be viewed as a lazily evaluated variable/array. It records
|
|
17
|
+
operations applied to it and acts as a function that, when called, evaluates the operations
|
|
18
|
+
and returns the result.
|
|
19
|
+
|
|
20
|
+
In CUQIpy, random variables can be in two forms: (1) a 'primal' random variable that is directly
|
|
21
|
+
defined by a distribution, e.g. x ~ N(0, 1), or (2) a 'transformed' random variable that is defined by
|
|
22
|
+
applying algebraic operations on one or more random variables, e.g. y = x + 1.
|
|
23
|
+
|
|
24
|
+
This distinction is purely for the purpose of the implementation in CUQIpy, as mathematically both
|
|
25
|
+
x ~ N(0, 1) and y = x + 1 ~ N(1, 1) are random variables. The distinction is useful for the
|
|
26
|
+
code implementation. In the future some operations like the above may allow primal random variables
|
|
27
|
+
that are transformed if the distribution can be analytically described.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
distributions : Distribution or list of Distributions
|
|
32
|
+
The distribution from which the random variable originates. If multiple distributions are
|
|
33
|
+
provided, the random variable is defined by the passed abstract syntax `tree` representing the
|
|
34
|
+
algebraic operations applied to one or more random variables.
|
|
35
|
+
|
|
36
|
+
tree : Node, optional
|
|
37
|
+
The tree, represented by the syntax tree nodes, that contain the algebraic operations applied to the random variable.
|
|
38
|
+
Specifically, the root of the tree should be provided.
|
|
39
|
+
|
|
40
|
+
name : str, optional
|
|
41
|
+
Name of the random variable. If not provided, the name is extracted from either the distribution provided
|
|
42
|
+
or from the variable name in the code. The name provided must match the parameter name of the distribution.
|
|
43
|
+
|
|
44
|
+
Example
|
|
45
|
+
-------
|
|
46
|
+
|
|
47
|
+
Basic usage:
|
|
48
|
+
|
|
49
|
+
.. code-block:: python
|
|
50
|
+
|
|
51
|
+
from cuqi.distribution import Gaussian
|
|
52
|
+
|
|
53
|
+
x = RandomVariable(Gaussian(0, 1))
|
|
54
|
+
|
|
55
|
+
Defining Bayesian problem using random variables:
|
|
56
|
+
|
|
57
|
+
.. code-block:: python
|
|
58
|
+
|
|
59
|
+
from cuqi.testproblem import Deconvolution1D
|
|
60
|
+
from cuqi.distribution import Gaussian, Gamma, GMRF
|
|
61
|
+
from cuqi.algebra import RandomVariable
|
|
62
|
+
from cuqi.problem import BayesianProblem
|
|
63
|
+
|
|
64
|
+
import numpy as np
|
|
65
|
+
A, y_obs, info = Deconvolution1D().get_components()
|
|
66
|
+
|
|
67
|
+
# Bayesian problem
|
|
68
|
+
d = RandomVariable(Gamma(1, 1e-4))
|
|
69
|
+
s = RandomVariable(Gamma(1, 1e-4))
|
|
70
|
+
x = RandomVariable(GMRF(np.zeros(A.domain_dim), d))
|
|
71
|
+
y = RandomVariable(Gaussian(A @ x, 1/s))
|
|
72
|
+
|
|
73
|
+
BP = BayesianProblem(y, x, s, d)
|
|
74
|
+
BP.set_data(y=y_obs)
|
|
75
|
+
BP.UQ()
|
|
76
|
+
|
|
77
|
+
Defining random variable from multiple distributions:
|
|
78
|
+
|
|
79
|
+
.. code-block:: python
|
|
80
|
+
|
|
81
|
+
from cuqi.distribution import Gaussian, Gamma
|
|
82
|
+
from cuqi.algebra import RandomVariable, VariableNode
|
|
83
|
+
|
|
84
|
+
# Define the variables
|
|
85
|
+
x = VariableNode('x')
|
|
86
|
+
y = VariableNode('y')
|
|
87
|
+
|
|
88
|
+
# Define the distributions (names must match variables)
|
|
89
|
+
dist_x = Gaussian(0, 1, name='x')
|
|
90
|
+
dist_y = Gamma(1, 1e-4, name='y')
|
|
91
|
+
|
|
92
|
+
# Define the tree (this is the algebra that defines the random variable along with the distributions)
|
|
93
|
+
tree = x + y
|
|
94
|
+
|
|
95
|
+
# Define random variable from 2 distributions with relation x+y
|
|
96
|
+
rv = RandomVariable([dist_x, dist_y], tree)
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def __init__(self, distributions: Union['Distribution', List['Distribution']], tree: 'Node' = None, name: str = None):
|
|
102
|
+
""" Create random variable from distribution """
|
|
103
|
+
|
|
104
|
+
if isinstance(distributions, Distribution):
|
|
105
|
+
distributions = [distributions]
|
|
106
|
+
|
|
107
|
+
if not isinstance(distributions, list) and not isinstance(distributions, _OrderedSet):
|
|
108
|
+
raise ValueError("Expected a distribution or a list of distributions")
|
|
109
|
+
|
|
110
|
+
# Convert single distribution(s) to internal datastructure _OrderedSet.
|
|
111
|
+
# We use ordered set to ensure that the order of the distributions is preserved.
|
|
112
|
+
# which in turn ensures that the parameter names are always in the same order.
|
|
113
|
+
if not isinstance(distributions, _OrderedSet):
|
|
114
|
+
distributions = _OrderedSet(distributions)
|
|
115
|
+
|
|
116
|
+
# If tree is provided, check it is consistent with the given distributions
|
|
117
|
+
if tree:
|
|
118
|
+
tree_var_names = tree.get_variables()
|
|
119
|
+
dist_par_names = {dist._name for dist in distributions}
|
|
120
|
+
|
|
121
|
+
if len(tree_var_names) != len(distributions):
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"There are {len(tree_var_names)} variables in the tree, but {len(distributions)} distributions are provided. "
|
|
124
|
+
"This may be due to passing multiple distributions with the same parameter name. "
|
|
125
|
+
f"The tree variables are {tree_var_names} and the distribution parameter names are {dist_par_names}."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not all(var_name in dist_par_names for var_name in tree_var_names):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Variable names in the tree {tree_var_names} do not match the parameter names in the distributions {dist_par_names}. "
|
|
131
|
+
"Ensure the name is inferred from the variable or explicitly provide it using name='var_name' in the distribution."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Match random variable name with distribution parameter name (for single distribution)
|
|
135
|
+
if len(distributions) == 1 and tree is None:
|
|
136
|
+
dist = next(iter(distributions))
|
|
137
|
+
dist_par_name = dist._name
|
|
138
|
+
if dist_par_name is not None:
|
|
139
|
+
if name is not None and dist_par_name != name:
|
|
140
|
+
raise ValueError(f"Parameter name '{dist_par_name}' of the distribution does not match the input name '{name}' for the random variable.")
|
|
141
|
+
name = dist_par_name
|
|
142
|
+
|
|
143
|
+
self._distributions = distributions
|
|
144
|
+
""" The distribution from which the random variable originates. """
|
|
145
|
+
|
|
146
|
+
self._tree = tree
|
|
147
|
+
""" The tree representation of the random variable. """
|
|
148
|
+
|
|
149
|
+
self._original_variable = None
|
|
150
|
+
""" Stores the original variable if this is a conditioned copy"""
|
|
151
|
+
|
|
152
|
+
self._name = name
|
|
153
|
+
""" Name of the random variable. """
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def __call__(self, *args, **kwargs) -> Any:
|
|
157
|
+
""" Evaluate random variable at a given parameter value. For example, for random variable `X`, `X(1)` gives `1` and `(X+1)(1)` gives `2` """
|
|
158
|
+
|
|
159
|
+
if args and kwargs:
|
|
160
|
+
raise ValueError("Cannot pass both positional and keyword arguments to RandomVariable")
|
|
161
|
+
|
|
162
|
+
if args:
|
|
163
|
+
kwargs = self._parse_args_add_to_kwargs(args, kwargs)
|
|
164
|
+
|
|
165
|
+
# Check if kwargs match parameter names using a all compare
|
|
166
|
+
if not all([name in kwargs for name in self.parameter_names]) or not all([name in self.parameter_names for name in kwargs]):
|
|
167
|
+
raise ValueError(f"Expected arguments {self.parameter_names}, got arguments {kwargs}")
|
|
168
|
+
|
|
169
|
+
return self.tree(**kwargs)
|
|
170
|
+
|
|
171
|
+
def sample(self, N=1):
|
|
172
|
+
""" Sample from the random variable.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
N : int, optional
|
|
177
|
+
Number of samples to draw. Default is 1.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
if self.is_cond:
|
|
181
|
+
raise NotImplementedError(
|
|
182
|
+
"Unable to directly sample from a random variable that has distributions with "
|
|
183
|
+
"conditioning variables. This is not implemented."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if N == 1: return self(**{dist.name: dist.sample() for dist in self.distributions})
|
|
187
|
+
|
|
188
|
+
samples = np.array([
|
|
189
|
+
self(**{dist.name: dist.sample() for dist in self.distributions})
|
|
190
|
+
for _ in range(N)
|
|
191
|
+
]).reshape(-1, N) # Ensure correct shape (dim, N)
|
|
192
|
+
|
|
193
|
+
return cuqi.samples.Samples(samples)
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def tree(self):
|
|
197
|
+
if self._tree is None:
|
|
198
|
+
if len(self._distributions) > 1:
|
|
199
|
+
raise ValueError("Tree for multiple distributions can not be created automatically and need to be passed as an argument to the {} initializer.".format(type(self).__name__))
|
|
200
|
+
self._tree = VariableNode(self.name)
|
|
201
|
+
return self._tree
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def name(self):
|
|
205
|
+
""" Name of the random variable. If not provided, the name is extracted from the variable name in the code. """
|
|
206
|
+
if self._is_copy: # Extract the original variable name if this is a copy
|
|
207
|
+
return self._original_variable.name
|
|
208
|
+
if self._name is None: # If None extract the name from the stack
|
|
209
|
+
self._name = cuqi.utilities._get_python_variable_name(self)
|
|
210
|
+
if self._name is not None:
|
|
211
|
+
self._inject_name_into_distribution(self._name)
|
|
212
|
+
return self._name
|
|
213
|
+
|
|
214
|
+
@name.setter
|
|
215
|
+
def name(self, name):
|
|
216
|
+
if self._is_copy:
|
|
217
|
+
raise ValueError("This random variable is derived from the conditional random variable named "+self._original_variable.name+". The name of the derived random variable cannot be set, but follows the name of the original random variable.")
|
|
218
|
+
self._name = name
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def distribution(self) -> cuqi.distribution.Distribution:
|
|
222
|
+
""" Distribution from which the random variable originates. """
|
|
223
|
+
if len(self._distributions) > 1:
|
|
224
|
+
raise ValueError("Cannot get distribution from random variable defined by multiple distributions")
|
|
225
|
+
self._inject_name_into_distribution()
|
|
226
|
+
return next(iter(self._distributions))
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def distributions(self) -> set:
|
|
230
|
+
""" Distributions from which the random variable originates. """
|
|
231
|
+
self._inject_name_into_distribution()
|
|
232
|
+
return self._distributions
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def parameter_names(self) -> str:
|
|
236
|
+
""" Name of the parameter that the random variable can be evaluated at. """
|
|
237
|
+
self._inject_name_into_distribution()
|
|
238
|
+
return [distribution._name for distribution in self.distributions] # Consider renaming .name to .par_name for distributions
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def dim(self):
|
|
242
|
+
if self.is_transformed:
|
|
243
|
+
raise NotImplementedError("Dimension not implemented for transformed random variables")
|
|
244
|
+
return self.distribution.dim
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def geometry(self):
|
|
248
|
+
if self.is_transformed:
|
|
249
|
+
raise NotImplementedError("Geometry not implemented for transformed random variables")
|
|
250
|
+
return self.distribution.geometry
|
|
251
|
+
|
|
252
|
+
@geometry.setter
|
|
253
|
+
def geometry(self, geometry):
|
|
254
|
+
if self.is_transformed:
|
|
255
|
+
raise NotImplementedError("Geometry not implemented for transformed random variables")
|
|
256
|
+
self.distribution.geometry = geometry
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def expression(self):
|
|
260
|
+
""" Expression (formula) of the random variable. """
|
|
261
|
+
return str(self.tree)
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def is_transformed(self):
|
|
265
|
+
""" Returns True if the random variable is transformed. """
|
|
266
|
+
return not isinstance(self.tree, VariableNode)
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def is_cond(self):
|
|
270
|
+
""" Returns True if the random variable is a conditional random variable. """
|
|
271
|
+
return any(dist.is_cond for dist in self.distributions)
|
|
272
|
+
|
|
273
|
+
def condition(self, *args, **kwargs):
|
|
274
|
+
"""Condition the random variable on a given value. Only one of either positional or keyword arguments can be passed.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
*args : Any
|
|
279
|
+
Positional arguments to condition the random variable on. The order of the arguments must match the order of the parameter names.
|
|
280
|
+
|
|
281
|
+
**kwargs : Any
|
|
282
|
+
Keyword arguments to condition the random variable on. The keys must match the parameter names.
|
|
283
|
+
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
# Before conditioning, capture repr to ensure all variable names are injected
|
|
287
|
+
self.__repr__()
|
|
288
|
+
|
|
289
|
+
if args and kwargs:
|
|
290
|
+
raise ValueError("Cannot pass both positional and keyword arguments to RandomVariable")
|
|
291
|
+
|
|
292
|
+
if args:
|
|
293
|
+
kwargs = self._parse_args_add_to_kwargs(args, kwargs)
|
|
294
|
+
|
|
295
|
+
# Create a deep copy of the random variable to ensure the original tree is not modified
|
|
296
|
+
new_variable = self._make_copy(deep=True)
|
|
297
|
+
|
|
298
|
+
for kwargs_name in list(kwargs.keys()):
|
|
299
|
+
value = kwargs.pop(kwargs_name)
|
|
300
|
+
|
|
301
|
+
# Condition the tree turning the variable into a constant
|
|
302
|
+
if kwargs_name in self.parameter_names:
|
|
303
|
+
new_variable._tree = new_variable.tree.condition(**{kwargs_name: value})
|
|
304
|
+
|
|
305
|
+
# Condition the random variable on both the distribution parameter name and distribution conditioning variables
|
|
306
|
+
for dist in self.distributions:
|
|
307
|
+
if kwargs_name == dist.name:
|
|
308
|
+
new_variable._remove_distribution(dist.name)
|
|
309
|
+
elif kwargs_name in dist.get_conditioning_variables():
|
|
310
|
+
new_variable._replace_distribution(dist.name, dist(**{kwargs_name: value}))
|
|
311
|
+
|
|
312
|
+
# Check if any kwargs are left unprocessed
|
|
313
|
+
if kwargs:
|
|
314
|
+
raise ValueError(f"Conditioning variables {list(kwargs.keys())} not found in the random variable {self}")
|
|
315
|
+
|
|
316
|
+
return new_variable
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def _non_default_args(self) -> List[str]:
|
|
320
|
+
"""List of non-default arguments to distribution. This is used to return the correct
|
|
321
|
+
arguments when evaluating the random variable.
|
|
322
|
+
"""
|
|
323
|
+
return self.parameter_names
|
|
324
|
+
|
|
325
|
+
def _replace_distribution(self, name, new_distribution):
|
|
326
|
+
""" Replace distribution with a given name with a new distribution in the same position of the ordered set. """
|
|
327
|
+
for dist in self.distributions:
|
|
328
|
+
if dist._name == name:
|
|
329
|
+
self._distributions.replace(dist, new_distribution)
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
def _remove_distribution(self, name):
|
|
333
|
+
""" Remove distribution with a given name from the set of distributions. """
|
|
334
|
+
for dist in self.distributions:
|
|
335
|
+
if dist._name == name:
|
|
336
|
+
self._distributions.remove(dist)
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
def _inject_name_into_distribution(self, name=None):
|
|
340
|
+
if len(self._distributions) == 1:
|
|
341
|
+
dist = next(iter(self._distributions))
|
|
342
|
+
|
|
343
|
+
if dist._is_copy:
|
|
344
|
+
dist = dist._original_density
|
|
345
|
+
|
|
346
|
+
if dist._name is None:
|
|
347
|
+
if name is None:
|
|
348
|
+
name = self.name
|
|
349
|
+
dist.name = name # Inject using setter
|
|
350
|
+
|
|
351
|
+
def _parse_args_add_to_kwargs(self, args, kwargs) -> dict:
|
|
352
|
+
""" Parse args and add to kwargs if any. Arguments follow self.parameter_names order. """
|
|
353
|
+
if len(args) != len(self.parameter_names):
|
|
354
|
+
raise ValueError(f"Expected {len(self.parameter_names)} arguments, got {len(args)}. Parameters are: {self.parameter_names}")
|
|
355
|
+
|
|
356
|
+
# Add args to kwargs
|
|
357
|
+
for arg, name in zip(args, self.parameter_names):
|
|
358
|
+
kwargs[name] = arg
|
|
359
|
+
|
|
360
|
+
return kwargs
|
|
361
|
+
|
|
362
|
+
def __repr__(self):
|
|
363
|
+
# Create strings for parameter name ~ distribution pairs
|
|
364
|
+
parameter_strings = [f"{name} ~ {distribution}" for name, distribution in zip(self.parameter_names, self.distributions)]
|
|
365
|
+
# Join strings with newlines
|
|
366
|
+
parameter_strings = "\n".join(parameter_strings)
|
|
367
|
+
# Add initial newline and indentations
|
|
368
|
+
parameter_strings = "\n".join(["\t"+line for line in parameter_strings.split("\n")])
|
|
369
|
+
# Print parameter strings with newlines
|
|
370
|
+
if self.is_transformed:
|
|
371
|
+
title = f"Transformed Random Variable"
|
|
372
|
+
else:
|
|
373
|
+
title = f""
|
|
374
|
+
if self.is_transformed:
|
|
375
|
+
body = (
|
|
376
|
+
f"\n"
|
|
377
|
+
f"Expression: {self.tree}\n"
|
|
378
|
+
f"Components: \n{parameter_strings}"
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
body = parameter_strings.replace("\t","")
|
|
382
|
+
return title+body
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def _is_copy(self):
|
|
386
|
+
""" Returns True if this is a copy of another random variable, e.g. by conditioning. """
|
|
387
|
+
return hasattr(self, '_original_variable') and self._original_variable is not None
|
|
388
|
+
|
|
389
|
+
def _make_copy(self, deep=False) -> 'RandomVariable':
|
|
390
|
+
""" Returns a copy of the density keeping a pointer to the original. """
|
|
391
|
+
if deep:
|
|
392
|
+
new_variable = deepcopy(self)
|
|
393
|
+
new_variable._original_variable = self
|
|
394
|
+
return new_variable
|
|
395
|
+
new_variable = copy(self)
|
|
396
|
+
new_variable._distributions = copy(self.distributions)
|
|
397
|
+
new_variable._tree = copy(self._tree)
|
|
398
|
+
new_variable._original_variable = self
|
|
399
|
+
return new_variable
|
|
400
|
+
|
|
401
|
+
def _apply_operation(self, operation, other=None) -> 'RandomVariable':
|
|
402
|
+
"""
|
|
403
|
+
Apply a specified operation to this RandomVariable.
|
|
404
|
+
"""
|
|
405
|
+
if isinstance(other, cuqi.distribution.Distribution):
|
|
406
|
+
raise ValueError("Cannot apply operation to distribution. Use .rv to create random variable first.")
|
|
407
|
+
if other is None: # unary operation case
|
|
408
|
+
return RandomVariable(self.distributions, operation(self.tree))
|
|
409
|
+
elif isinstance(other, RandomVariable): # binary operation case with another random variable that has distributions
|
|
410
|
+
return RandomVariable(self.distributions | other.distributions, operation(self.tree, other.tree))
|
|
411
|
+
return RandomVariable(self.distributions, operation(self.tree, other)) # binary operation case with any other object (constant)
|
|
412
|
+
|
|
413
|
+
def __add__(self, other) -> 'RandomVariable':
|
|
414
|
+
return self._apply_operation(operator.add, other)
|
|
415
|
+
|
|
416
|
+
def __radd__(self, other) -> 'RandomVariable':
|
|
417
|
+
return self.__add__(other)
|
|
418
|
+
|
|
419
|
+
def __sub__(self, other) -> 'RandomVariable':
|
|
420
|
+
return self._apply_operation(operator.sub, other)
|
|
421
|
+
|
|
422
|
+
def __rsub__(self, other) -> 'RandomVariable':
|
|
423
|
+
return self._apply_operation(lambda x, y: operator.sub(y, x), other)
|
|
424
|
+
|
|
425
|
+
def __mul__(self, other) -> 'RandomVariable':
|
|
426
|
+
return self._apply_operation(operator.mul, other)
|
|
427
|
+
|
|
428
|
+
def __rmul__(self, other) -> 'RandomVariable':
|
|
429
|
+
return self.__mul__(other)
|
|
430
|
+
|
|
431
|
+
def __truediv__(self, other) -> 'RandomVariable':
|
|
432
|
+
return self._apply_operation(operator.truediv, other)
|
|
433
|
+
|
|
434
|
+
def __rtruediv__(self, other) -> 'RandomVariable':
|
|
435
|
+
return self._apply_operation(lambda x, y: operator.truediv(y, x), other)
|
|
436
|
+
|
|
437
|
+
def __matmul__(self, other) -> 'RandomVariable':
|
|
438
|
+
if isinstance(other, cuqi.model.Model) and not isinstance(other, cuqi.model.LinearModel):
|
|
439
|
+
raise TypeError("Cannot apply matmul to non-linear models")
|
|
440
|
+
return self._apply_operation(operator.matmul, other)
|
|
441
|
+
|
|
442
|
+
def __rmatmul__(self, other) -> 'RandomVariable':
|
|
443
|
+
if isinstance(other, cuqi.model.Model) and not isinstance(other, cuqi.model.LinearModel):
|
|
444
|
+
raise TypeError("Cannot apply matmul to non-linear models")
|
|
445
|
+
return self._apply_operation(lambda x, y: operator.matmul(y, x), other)
|
|
446
|
+
|
|
447
|
+
def __neg__(self) -> 'RandomVariable':
|
|
448
|
+
return self._apply_operation(operator.neg)
|
|
449
|
+
|
|
450
|
+
def __abs__(self) -> 'RandomVariable':
|
|
451
|
+
return self._apply_operation(abs)
|
|
452
|
+
|
|
453
|
+
def __pow__(self, other) -> 'RandomVariable':
|
|
454
|
+
return self._apply_operation(operator.pow, other)
|
|
455
|
+
|
|
456
|
+
def __getitem__(self, other) -> 'RandomVariable':
|
|
457
|
+
return self._apply_operation(operator.getitem, other)
|
cuqi/array/_array.py
CHANGED
|
@@ -15,19 +15,7 @@ class CUQIarray(np.ndarray):
|
|
|
15
15
|
Boolean flag whether input_array is to be interpreted as parameter (True) or function values (False).
|
|
16
16
|
|
|
17
17
|
geometry : cuqi.geometry.Geometry, default None
|
|
18
|
-
Contains the geometry related of the data
|
|
19
|
-
|
|
20
|
-
Attributes
|
|
21
|
-
----------
|
|
22
|
-
funvals : CUQIarray
|
|
23
|
-
Returns itself as function values.
|
|
24
|
-
|
|
25
|
-
parameters : CUQIarray
|
|
26
|
-
Returns itself as parameters.
|
|
27
|
-
|
|
28
|
-
Methods
|
|
29
|
-
----------
|
|
30
|
-
:meth:`plot`: Plots the data as function or parameters.
|
|
18
|
+
Contains the geometry related of the data.
|
|
31
19
|
"""
|
|
32
20
|
|
|
33
21
|
def __repr__(self) -> str:
|
|
@@ -62,6 +50,7 @@ class CUQIarray(np.ndarray):
|
|
|
62
50
|
|
|
63
51
|
@property
|
|
64
52
|
def funvals(self):
|
|
53
|
+
""" Returns itself as function values. """
|
|
65
54
|
if self.is_par is True:
|
|
66
55
|
vals = self.geometry.par2fun(self)
|
|
67
56
|
else:
|
|
@@ -82,6 +71,7 @@ class CUQIarray(np.ndarray):
|
|
|
82
71
|
|
|
83
72
|
@property
|
|
84
73
|
def parameters(self):
|
|
74
|
+
""" Returns itself as parameters. """
|
|
85
75
|
if self.is_par is False:
|
|
86
76
|
if self.dtype == np.dtype('O'):
|
|
87
77
|
# If the current state if the CUQIarray is function values, and
|
|
@@ -109,6 +99,7 @@ class CUQIarray(np.ndarray):
|
|
|
109
99
|
f"Cannot convert {self.__class__.__name__} to numpy array")
|
|
110
100
|
|
|
111
101
|
def plot(self, plot_par=False, **kwargs):
|
|
102
|
+
""" Plot the data as function or parameters. """
|
|
112
103
|
if plot_par:
|
|
113
104
|
kwargs["is_par"]=True
|
|
114
105
|
return self.geometry.plot(self.parameters, plot_par=plot_par, **kwargs)
|
cuqi/config.py
CHANGED
|
@@ -12,3 +12,10 @@ MAX_STACK_SEARCH_DEPTH = 1000
|
|
|
12
12
|
|
|
13
13
|
MIN_DIM_SPARSE = 75
|
|
14
14
|
""" Minimum dimension to start storing Nd-arrays as sparse for N>2. The minimum dimension is defined as MIN_DIM_SPARSE^N. """
|
|
15
|
+
|
|
16
|
+
PROGRESS_BAR_DYNAMIC_UPDATE = True
|
|
17
|
+
""" If True, progress bars are updated frequently (dynamic updates).
|
|
18
|
+
If False, progress bars are minimal/static (only shown at start and end),
|
|
19
|
+
which is useful, for example, when building documentation or the Jupyter book
|
|
20
|
+
(CUQI-Book).
|
|
21
|
+
"""
|
cuqi/density/_density.py
CHANGED
|
@@ -143,7 +143,15 @@ class Density(ABC):
|
|
|
143
143
|
def enable_FD(self, epsilon=1e-8):
|
|
144
144
|
""" Enable finite difference approximation for logd gradient. Note
|
|
145
145
|
that if enabled, the FD approximation will be used even if the
|
|
146
|
-
_gradient method is implemented.
|
|
146
|
+
_gradient method is implemented.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
epsilon : float
|
|
151
|
+
|
|
152
|
+
Spacing (step size) to use for finite difference approximation for logd
|
|
153
|
+
gradient for each variable. Default is 1e-8.
|
|
154
|
+
"""
|
|
147
155
|
self._FD_enabled = True
|
|
148
156
|
self._FD_epsilon = epsilon
|
|
149
157
|
|
cuqi/distribution/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ from ._beta import Beta
|
|
|
3
3
|
from ._cauchy import Cauchy
|
|
4
4
|
from ._cmrf import CMRF
|
|
5
5
|
from ._gamma import Gamma
|
|
6
|
-
from .
|
|
6
|
+
from ._modified_half_normal import ModifiedHalfNormal
|
|
7
7
|
from ._gaussian import Gaussian, JointGaussianSqrtPrec
|
|
8
8
|
from ._gmrf import GMRF
|
|
9
9
|
from ._inverse_gamma import InverseGamma
|
|
@@ -12,7 +12,8 @@ from ._laplace import Laplace
|
|
|
12
12
|
from ._smoothed_laplace import SmoothedLaplace
|
|
13
13
|
from ._lognormal import Lognormal
|
|
14
14
|
from ._normal import Normal
|
|
15
|
+
from ._truncated_normal import TruncatedNormal
|
|
15
16
|
from ._posterior import Posterior
|
|
16
|
-
from ._uniform import Uniform
|
|
17
|
+
from ._uniform import Uniform, UnboundedUniform
|
|
17
18
|
from ._custom import UserDefinedDistribution, DistributionGallery
|
|
18
19
|
from ._joint_distribution import JointDistribution, _StackedJointDistribution, MultipleLikelihoodPosterior
|
cuqi/distribution/_beta.py
CHANGED
|
@@ -9,23 +9,19 @@ class Beta(Distribution):
|
|
|
9
9
|
"""
|
|
10
10
|
Multivariate beta distribution of independent random variables x_i. Each is distributed according to the PDF function
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
.. math::
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
f(x) = x^{(\\alpha-1)}(1-x)^{(\\beta-1)}\Gamma(\\alpha+\\beta) / (\Gamma(\\alpha)\Gamma(\\beta))
|
|
15
|
+
|
|
16
|
+
where :math:`\Gamma` is the Gamma function.
|
|
15
17
|
|
|
16
18
|
Parameters
|
|
17
19
|
------------
|
|
18
20
|
alpha: float or array_like
|
|
21
|
+
The shape parameter :math:`\\alpha` of the beta distribution.
|
|
19
22
|
|
|
20
23
|
beta: float or array_like
|
|
21
|
-
|
|
22
|
-
Methods
|
|
23
|
-
-----------
|
|
24
|
-
sample: generate one or more random samples
|
|
25
|
-
pdf: evaluate probability density function
|
|
26
|
-
logpdf: evaluate log probability density function
|
|
27
|
-
cdf: evaluate cumulative probability function
|
|
28
|
-
gradient: evaluate the gradient of the logpdf
|
|
24
|
+
The shape parameter :math:`\\beta` of the beta distribution.
|
|
29
25
|
|
|
30
26
|
Example
|
|
31
27
|
-------
|
|
@@ -52,7 +48,7 @@ class Beta(Distribution):
|
|
|
52
48
|
|
|
53
49
|
# Check bounds
|
|
54
50
|
if np.any(x<=0) or np.any(x>=1) or np.any(self.alpha<=0) or np.any(self.beta<=0):
|
|
55
|
-
return -np.
|
|
51
|
+
return -np.inf
|
|
56
52
|
|
|
57
53
|
# Compute logpdf
|
|
58
54
|
return np.sum(sps.beta.logpdf(x, a=self.alpha, b=self.beta))
|
cuqi/distribution/_cauchy.py
CHANGED
|
@@ -75,14 +75,14 @@ class Cauchy(Distribution):
|
|
|
75
75
|
def logpdf(self, x):
|
|
76
76
|
|
|
77
77
|
if self._is_out_of_bounds(x):
|
|
78
|
-
return -np.
|
|
78
|
+
return -np.inf
|
|
79
79
|
|
|
80
80
|
return np.sum(-np.log(np.pi*self.scale*(1+((x-self.location)/self.scale)**2)))
|
|
81
81
|
|
|
82
82
|
def cdf(self, x):
|
|
83
83
|
|
|
84
84
|
if self._is_out_of_bounds(x):
|
|
85
|
-
return -np.
|
|
85
|
+
return -np.inf
|
|
86
86
|
|
|
87
87
|
return np.sum(sps.cauchy.cdf(x, loc=self.location, scale=self.scale))
|
|
88
88
|
|
cuqi/distribution/_custom.py
CHANGED
|
@@ -12,12 +12,6 @@ class UserDefinedDistribution(Distribution):
|
|
|
12
12
|
gradient_func: Function evaluating the gradient of the logpdf. Callable.
|
|
13
13
|
sample_func: Function drawing samples from distribution. Callable.
|
|
14
14
|
|
|
15
|
-
Methods
|
|
16
|
-
-----------
|
|
17
|
-
sample: generate one or more random samples
|
|
18
|
-
logpdf: evaluate log probability density function
|
|
19
|
-
gradient: evaluate gradient of logpdf
|
|
20
|
-
|
|
21
15
|
Example
|
|
22
16
|
-----------
|
|
23
17
|
.. code-block:: python
|