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,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
 
@@ -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 ._modifiedhalfnormal import ModifiedHalfNormal
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
@@ -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
- f(x) = x^(alpha-1) * (1-x)^(beta-1) * Gamma(alpha+beta) / (Gamma(alpha)*Gamma(beta))
12
+ .. math::
13
13
 
14
- where Gamma is the Gamma function.
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.Inf
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))
@@ -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.Inf
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.Inf
85
+ return -np.inf
86
86
 
87
87
  return np.sum(sps.cauchy.cdf(x, loc=self.location, scale=self.scale))
88
88
 
@@ -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