CUQIpy 1.2.0.post0.dev352__py3-none-any.whl → 1.2.0.post0.dev380__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of CUQIpy might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: CUQIpy
3
- Version: 1.2.0.post0.dev352
3
+ Version: 1.2.0.post0.dev380
4
4
  Summary: Computational Uncertainty Quantification for Inverse problems in Python
5
5
  Maintainer-email: "Nicolai A. B. Riis" <nabr@dtu.dk>, "Jakob S. Jørgensen" <jakj@dtu.dk>, "Amal M. Alghamdi" <amaal@dtu.dk>, Chao Zhang <chaz@dtu.dk>
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  cuqi/__init__.py,sha256=LsGilhl-hBLEn6Glt8S_l0OJzAA1sKit_rui8h-D-p0,488
2
2
  cuqi/_messages.py,sha256=fzEBrZT2kbmfecBBPm7spVu7yHdxGARQB4QzXhJbCJ0,415
3
- cuqi/_version.py,sha256=C3E_fdIvWSE-v7utMG2sMEohKeQFQh02EkThKcjI6Ic,510
3
+ cuqi/_version.py,sha256=4Rygq2rO32tD5TYtalGmZY_AqICH80eNKpSu2m7Zj5A,510
4
4
  cuqi/config.py,sha256=wcYvz19wkeKW2EKCGIKJiTpWt5kdaxyt4imyRkvtTRA,526
5
5
  cuqi/diagnostics.py,sha256=5OrbJeqpynqRXOe5MtOKKhe7EAVdOEpHIqHnlMW9G_c,3029
6
6
  cuqi/array/__init__.py,sha256=-EeiaiWGNsE3twRS4dD814BIlfxEsNkTCZUc5gjOXb0,30
@@ -24,7 +24,7 @@ cuqi/distribution/_gamma.py,sha256=VcvBJS51N-MxuX42r9L2j2QYRlzhdgAtQ6Wa5IFO_YE,3
24
24
  cuqi/distribution/_gaussian.py,sha256=3L1L_3W6i6YuPQ8vnFmju5QsvkLlg4VsgCnj11lYBUE,32977
25
25
  cuqi/distribution/_gmrf.py,sha256=OwId8qQWEtmC2fxVhL4iBHZnc8ZCrZzfV6yGXDE3k30,9522
26
26
  cuqi/distribution/_inverse_gamma.py,sha256=oPJuiYp3O1m547pmmIz9OWesky9YpwLTHT7-9MmcYss,3159
27
- cuqi/distribution/_joint_distribution.py,sha256=7TxDaZ7y362LfxEpD4I5Z0icdtsBmOBJGnpIkz_bYXA,15900
27
+ cuqi/distribution/_joint_distribution.py,sha256=vadRTOpQh1skAgnf-f2-2e6IMvoH2d8beriDvjh236g,16709
28
28
  cuqi/distribution/_laplace.py,sha256=5exLvlzJm2AgfvZ3KUSkjfwlGwwbsktBxP8z0iLMik8,1401
29
29
  cuqi/distribution/_lmrf.py,sha256=rdGoQ-fPe1oW6Z29P-l3woq0NX3_RxUQ2rzm1VzemNM,3290
30
30
  cuqi/distribution/_lognormal.py,sha256=8_hOFQ3iu88ujX8vxmfVEZ0fdmlhTY98PlG5PasPjEg,2612
@@ -35,8 +35,10 @@ cuqi/distribution/_smoothed_laplace.py,sha256=p-1Y23mYA9omwiHGkEuv3T2mwcPAAoNlCr
35
35
  cuqi/distribution/_truncated_normal.py,sha256=sZkLYgnkGOyS_3ZxY7iw6L62t-Jh6shzsweRsRepN2k,4240
36
36
  cuqi/distribution/_uniform.py,sha256=KA8yQ6ZS3nQGS4PYJ4hpDg6Eq8EQKQvPsIpYfR8fj2w,1967
37
37
  cuqi/experimental/__init__.py,sha256=iStrmEy4ZMnGpEyd6QNlC6RK83lrS9iRkxQS0u-s8cU,105
38
- cuqi/experimental/algebra/__init__.py,sha256=3d4Bfx1upcHhEubNn6-Sa3WFpuksPQJif4OptcNDe_s,31
39
- cuqi/experimental/algebra/_ast.py,sha256=SAlqqQkW_559fyiM66S3dWgfACR7jdSZkomJm1mKix0,8698
38
+ cuqi/experimental/algebra/__init__.py,sha256=btRAWG58ZfdtK0afXKOg60AX7d76KMBjlZa4AWBCCgU,81
39
+ cuqi/experimental/algebra/_ast.py,sha256=iJ_umDzTct4O9tZM-ep2NNkrdxR4_PTIEOrxZiwvlc0,9257
40
+ cuqi/experimental/algebra/_orderedset.py,sha256=8SxktP1333ByTldtqzU9xLQ5SAFU0V9B-i6U1prVBYk,2019
41
+ cuqi/experimental/algebra/_randomvariable.py,sha256=lwOTy9KApqXwJ57VBYUBMrCwpbA7cR91OlT0ZUtiIaE,15841
40
42
  cuqi/experimental/mcmc/__init__.py,sha256=zSqLZmxOqQ-F94C9-gPv7g89TX1XxlrlNm071Eb167I,4487
41
43
  cuqi/experimental/mcmc/_conjugate.py,sha256=VNPQkGity0mposcqxrx4UIeXm35EvJvZED4p2stffvA,9924
42
44
  cuqi/experimental/mcmc/_conjugate_approx.py,sha256=uEnY2ea9su5ivcNagyRAwpQP2gBY98sXU7N0y5hTADo,3653
@@ -61,13 +63,13 @@ cuqi/implicitprior/_restorator.py,sha256=Z350XUJEt7N59Qw-SIUaBljQNDJk4Zb0i_KRFrt
61
63
  cuqi/likelihood/__init__.py,sha256=QXif382iwZ5bT3ZUqmMs_n70JVbbjxbqMrlQYbMn4Zo,1776
62
64
  cuqi/likelihood/_likelihood.py,sha256=z3AXAbIrv_DjOYh4jy3iDHemuIFUUJu6wdvJ5e2dgW0,6913
63
65
  cuqi/model/__init__.py,sha256=jgY2-jyxEMC79vkyH9BpfowW7_DbMRjqedOtO5fykXQ,62
64
- cuqi/model/_model.py,sha256=Hddc2qlKH0tVCB7gTY_U8TPO8IcjkAe63-TwCAVXUWI,31423
66
+ cuqi/model/_model.py,sha256=LqeMwOSb1oIGpT7g1cmItP_2Q4dmgg8eNPNo0joPUyg,32905
65
67
  cuqi/operator/__init__.py,sha256=0pc9p-KPyl7KtPV0noB0ddI0CP2iYEHw5rbw49D8Njk,136
66
68
  cuqi/operator/_operator.py,sha256=yNwPTh7jR07AiKMbMQQ5_54EgirlKFsbq9JN1EODaQI,8856
67
69
  cuqi/pde/__init__.py,sha256=NyS_ZYruCvy-Yg24qKlwm3ZIX058kLNQX9bqs-xg4ZM,99
68
70
  cuqi/pde/_pde.py,sha256=WRkOYyIdT_T3aZepRh0aS9C5nBbUZUcHaA80iSRvgoo,12572
69
71
  cuqi/problem/__init__.py,sha256=JxJty4JqHTOqSG6NeTGiXRQ7OLxiRK9jvVq3lXLeIRw,38
70
- cuqi/problem/_problem.py,sha256=d5jS5ETyinv8Ez_B6I5-QOIl4Vqvf-e_6qy5RN2gnUE,38160
72
+ cuqi/problem/_problem.py,sha256=XyJ_MZbVFUiiywUM-mrra3tUD_QMRK_kXLj4xVqRgAc,38169
71
73
  cuqi/sampler/__init__.py,sha256=D-dYa0gFgIwQukP8_VKhPGmlGKXbvVo7YqaET4SdAeQ,382
72
74
  cuqi/sampler/_conjugate.py,sha256=ztmUR3V3qZk9zelKx48ULnmMs_zKTDUfohc256VOIe8,2753
73
75
  cuqi/sampler/_conjugate_approx.py,sha256=xX-X71EgxGnZooOY6CIBhuJTs3dhcKfoLnoFxX3CO2g,1938
@@ -82,15 +84,15 @@ cuqi/sampler/_rto.py,sha256=KIs0cDEoYK5I35RwO9fr5eKWeINLsmTLSVBnLdZmzzM,11921
82
84
  cuqi/sampler/_sampler.py,sha256=TkZ_WAS-5Q43oICa-Elc2gftsRTBd7PEDUMDZ9tTGmU,5712
83
85
  cuqi/samples/__init__.py,sha256=vCs6lVk-pi8RBqa6cIN5wyn6u-K9oEf1Na4k1ZMrYv8,44
84
86
  cuqi/samples/_samples.py,sha256=hUc8OnCF9CTCuDTrGHwwzv3wp8mG_6vsJAFvuQ-x0uA,35832
85
- cuqi/solver/__init__.py,sha256=3eoTTgBHe3M6ygrbgUVG3GlqaZVe5lGajNV9rolXZJ8,179
86
- cuqi/solver/_solver.py,sha256=GquU_rj-9yfPQnBVE_gXo4wdF84xw_pLks3bJarzR58,29491
87
+ cuqi/solver/__init__.py,sha256=KuNlGxjPphG9tV-46YrmbcSQNhi0HMyhDd_v6V5sRaQ,209
88
+ cuqi/solver/_solver.py,sha256=2mil7Gq7InHlQaOfnuTRDB5nw6dqJk9gR8DLFxpw_6g,29481
87
89
  cuqi/testproblem/__init__.py,sha256=DWTOcyuNHMbhEuuWlY5CkYkNDSAqhvsKmJXBLivyblU,202
88
90
  cuqi/testproblem/_testproblem.py,sha256=x769LwwRdJdzIiZkcQUGb_5-vynNTNALXWKato7sS0Q,52540
89
91
  cuqi/utilities/__init__.py,sha256=H7xpJe2UinjZftKvE2JuXtTi4DqtkR6uIezStAXwfGg,428
90
- cuqi/utilities/_get_python_variable_name.py,sha256=QwlBVj2koJRA8s8pWd554p7-ElcI7HUwY32HknaR92E,1827
92
+ cuqi/utilities/_get_python_variable_name.py,sha256=wxpCaj9f3ZtBNqlGmmuGiITgBaTsY-r94lUIlK6UAU4,2043
91
93
  cuqi/utilities/_utilities.py,sha256=Jc4knn80vLoA7kgw9FzXwKVFGaNBOXiA9kgvltZU3Ao,11777
92
- CUQIpy-1.2.0.post0.dev352.dist-info/LICENSE,sha256=kJWRPrtRoQoZGXyyvu50Uc91X6_0XRaVfT0YZssicys,10799
93
- CUQIpy-1.2.0.post0.dev352.dist-info/METADATA,sha256=O3no2L4vDQEK-vO2bF_jWmHLydUYcXR6FN8deJHucmk,18529
94
- CUQIpy-1.2.0.post0.dev352.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
95
- CUQIpy-1.2.0.post0.dev352.dist-info/top_level.txt,sha256=AgmgMc6TKfPPqbjV0kvAoCBN334i_Lwwojc7HE3ZwD0,5
96
- CUQIpy-1.2.0.post0.dev352.dist-info/RECORD,,
94
+ CUQIpy-1.2.0.post0.dev380.dist-info/LICENSE,sha256=kJWRPrtRoQoZGXyyvu50Uc91X6_0XRaVfT0YZssicys,10799
95
+ CUQIpy-1.2.0.post0.dev380.dist-info/METADATA,sha256=pio8eeEPi2sbNpoalD1ZWlftxpKJMt3vin-Y0LRsJLo,18529
96
+ CUQIpy-1.2.0.post0.dev380.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
97
+ CUQIpy-1.2.0.post0.dev380.dist-info/top_level.txt,sha256=AgmgMc6TKfPPqbjV0kvAoCBN334i_Lwwojc7HE3ZwD0,5
98
+ CUQIpy-1.2.0.post0.dev380.dist-info/RECORD,,
cuqi/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-11-28T12:43:32+0100",
11
+ "date": "2024-12-20T09:13:49+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "6f800787a63fef7d6ba2c0b88b96dbc209afdbad",
15
- "version": "1.2.0.post0.dev352"
14
+ "full-revisionid": "7405af566d2a50ba59fc861865d41c08f8dcf160",
15
+ "version": "1.2.0.post0.dev380"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -5,6 +5,7 @@ from cuqi.density import Density, EvaluatedDensity
5
5
  from cuqi.distribution import Distribution, Posterior
6
6
  from cuqi.likelihood import Likelihood
7
7
  from cuqi.geometry import Geometry, _DefaultGeometry1D
8
+ import cuqi
8
9
  import numpy as np # for splitting array. Can avoid.
9
10
 
10
11
  class JointDistribution:
@@ -13,9 +14,11 @@ class JointDistribution:
13
14
 
14
15
  Parameters
15
16
  ----------
16
- densities : Density
17
+ densities : RandomVariable or Density
17
18
  The densities to include in the joint distribution.
18
- Each density is passed as comma-separated arguments.
19
+ Each density is passed as comma-separated arguments,
20
+ and can be either a :class:'Density' such as :class:'Distribution'
21
+ or :class:`RandomVariable`.
19
22
 
20
23
  Notes
21
24
  -----
@@ -59,7 +62,16 @@ class JointDistribution:
59
62
  posterior = joint(y=y_obs)
60
63
 
61
64
  """
62
- def __init__(self, *densities: Density):
65
+ def __init__(self, *densities: [Density, cuqi.experimental.algebra.RandomVariable]):
66
+ """ Create a joint distribution from the given densities. """
67
+
68
+ # Check if all RandomVariables are simple (not-transformed)
69
+ for density in densities:
70
+ if isinstance(density, cuqi.experimental.algebra.RandomVariable) and density.is_transformed:
71
+ raise ValueError(f"To be used in {self.__class__.__name__}, all RandomVariables must be untransformed.")
72
+
73
+ # Convert potential random variables to their underlying distribution
74
+ densities = [density.distribution if isinstance(density, cuqi.experimental.algebra.RandomVariable) else density for density in densities]
63
75
 
64
76
  # Ensure all densities have unique names
65
77
  names = [density.name for density in densities]
@@ -1 +1,2 @@
1
- from ._ast import VariableNode
1
+ from ._ast import VariableNode, Node
2
+ from ._randomvariable import RandomVariable
@@ -56,6 +56,20 @@ class Node(ABC):
56
56
  """String representation of the node. Used for printing the AST."""
57
57
  pass
58
58
 
59
+ def get_variables(self, variables=None):
60
+ """Returns a set with the names of all variables in the sub-tree originated at this node."""
61
+ if variables is None:
62
+ variables = set()
63
+ if isinstance(self, VariableNode):
64
+ variables.add(self.name)
65
+ if hasattr(self, "child"):
66
+ self.child.get_variables(variables)
67
+ if hasattr(self, "left"):
68
+ self.left.get_variables(variables)
69
+ if hasattr(self, "right"):
70
+ self.right.get_variables(variables)
71
+ return variables
72
+
59
73
  def __add__(self, other):
60
74
  return AddNode(self, convert_to_node(other))
61
75
 
@@ -0,0 +1,59 @@
1
+ class _OrderedSet:
2
+ """A set (i.e. unique elements) that keeps its elements in the order they were added.
3
+
4
+ This is a minimal implementation of an ordered set, using a dictionary for storage.
5
+ """
6
+
7
+ def __init__(self, iterable=None):
8
+ """Initialize the OrderedSet.
9
+
10
+ If an iterable is provided, add all its elements to the set.
11
+ """
12
+ self.dict = dict.fromkeys(iterable if iterable else [])
13
+
14
+ def add(self, item):
15
+ """Add an item to the set.
16
+
17
+ If the item is already in the set, it does nothing.
18
+ Otherwise, the item is stored as a key in the dictionary, with None as its value.
19
+ """
20
+ self.dict[item] = None
21
+
22
+ def __contains__(self, item):
23
+ """Check if an item is in the set.
24
+
25
+ This is equivalent to checking if the item is a key in the dictionary.
26
+ """
27
+ return item in self.dict
28
+
29
+ def __iter__(self):
30
+ """Return an iterator over the set.
31
+
32
+ This iterates over the keys in the dictionary.
33
+ """
34
+ return iter(self.dict)
35
+
36
+ def __len__(self):
37
+ """Return the number of items in the set."""
38
+ return len(self.dict)
39
+
40
+ def extend(self, other):
41
+ """Extend the set with the items in another set.
42
+
43
+ Raises a TypeError if the other object is not an _OrderedSet.
44
+ """
45
+ if not isinstance(other, _OrderedSet):
46
+ raise TypeError("unsupported operand type(s) for extend: '_OrderedSet' and '{}'".format(type(other).__name__))
47
+ for item in other:
48
+ self.add(item)
49
+
50
+ def __or__(self, other):
51
+ """Return a new set that is the union of this set and another set.
52
+
53
+ Raises a TypeError if the other object is not an _OrderedSet.
54
+ """
55
+ if not isinstance(other, _OrderedSet):
56
+ raise TypeError("unsupported operand type(s) for |: '_OrderedSet' and '{}'".format(type(other).__name__))
57
+ new_set = _OrderedSet(self.dict.keys())
58
+ new_set.extend(other)
59
+ return new_set
@@ -0,0 +1,360 @@
1
+ from __future__ import annotations
2
+ from typing import List, Any, Union
3
+ from ._ast import VariableNode, Node
4
+ from ._orderedset import _OrderedSet
5
+ import operator
6
+ import cuqi
7
+ from cuqi.distribution import Distribution
8
+ from copy import copy
9
+
10
+
11
+ class RandomVariable:
12
+ """ Random variable defined by a distribution with the option to apply algebraic operations on it.
13
+
14
+ Random variables allow for the definition of Bayesian Problems in a natural way. In the context
15
+ of code, the random variable can be viewed as a lazily evaluated variable/array. It records
16
+ operations applied to it and acts as a function that, when called, evaluates the operations
17
+ and returns the result.
18
+
19
+ In CUQIpy, random variables can be in two forms: (1) a 'primal' random variable that is directly
20
+ defined by a distribution, e.g. x ~ N(0, 1), or (2) a 'transformed' random variable that is defined by
21
+ applying algebraic operations on one or more random variables, e.g. y = x + 1.
22
+
23
+ This distinction is purely for the purpose of the implementation in CUQIpy, as mathematically both
24
+ x ~ N(0, 1) and y = x + 1 ~ N(1, 1) are random variables. The distinction is useful for the
25
+ code implementation. In the future some operations like the above may allow primal random variables
26
+ that are transformed if the distribution can be analytically described.
27
+
28
+ Parameters
29
+ ----------
30
+ distributions : Distribution or list of Distributions
31
+ The distribution from which the random variable originates. If multiple distributions are
32
+ provided, the random variable is defined by the passed abstract syntax `tree` representing the
33
+ algebraic operations applied to one or more random variables.
34
+
35
+ tree : Node, optional
36
+ The tree, represented by the syntax tree nodes, that contain the algebraic operations applied to the random variable.
37
+ Specifically, the root of the tree should be provided.
38
+
39
+ name : str, optional
40
+ Name of the random variable. If not provided, the name is extracted from either the distribution provided
41
+ or from the variable name in the code. The name provided must match the parameter name of the distribution.
42
+
43
+ Example
44
+ -------
45
+
46
+ Basic usage:
47
+
48
+ .. code-block:: python
49
+
50
+ from cuqi.distribution import Gaussian
51
+
52
+ x = RandomVariable(Gaussian(0, 1))
53
+
54
+ Defining Bayesian problem using random variables:
55
+
56
+ .. code-block:: python
57
+
58
+ from cuqi.testproblem import Deconvolution1D
59
+ from cuqi.distribution import Gaussian, Gamma, GMRF
60
+ from cuqi.experimental.algebra import RandomVariable
61
+ from cuqi.problem import BayesianProblem
62
+
63
+ import numpy as np
64
+ A, y_obs, info = Deconvolution1D().get_components()
65
+
66
+ # Bayesian problem
67
+ d = RandomVariable(Gamma(1, 1e-4))
68
+ s = RandomVariable(Gamma(1, 1e-4))
69
+ x = RandomVariable(GMRF(np.zeros(A.domain_dim), d))
70
+ y = RandomVariable(Gaussian(A @ x, 1/s))
71
+
72
+ BP = BayesianProblem(y, x, s, d)
73
+ BP.set_data(y=y_obs)
74
+ BP.UQ()
75
+
76
+ Defining random variable from multiple distributions:
77
+
78
+ .. code-block:: python
79
+
80
+ from cuqi.distribution import Gaussian, Gamma
81
+ from cuqi.experimental.algebra import RandomVariable, VariableNode
82
+
83
+ # Define the variables
84
+ x = VariableNode('x')
85
+ y = VariableNode('y')
86
+
87
+ # Define the distributions (names must match variables)
88
+ dist_x = Gaussian(0, 1, name='x')
89
+ dist_y = Gamma(1, 1e-4, name='y')
90
+
91
+ # Define the tree (this is the algebra that defines the random variable along with the distributions)
92
+ tree = x + y
93
+
94
+ # Define random variable from 2 distributions with relation x+y
95
+ rv = RandomVariable([dist_x, dist_y], tree)
96
+
97
+ """
98
+
99
+
100
+ def __init__(self, distributions: Union['Distribution', List['Distribution']], tree: 'Node' = None, name: str = None):
101
+ """ Create random variable from distribution """
102
+
103
+ if isinstance(distributions, Distribution):
104
+ distributions = [distributions]
105
+
106
+ if not isinstance(distributions, list) and not isinstance(distributions, _OrderedSet):
107
+ raise ValueError("Expected a distribution or a list of distributions")
108
+
109
+ # Convert single distribution(s) to internal datastructure _OrderedSet.
110
+ # We use ordered set to ensure that the order of the distributions is preserved.
111
+ # which in turn ensures that the parameter names are always in the same order.
112
+ if not isinstance(distributions, _OrderedSet):
113
+ distributions = _OrderedSet(distributions)
114
+
115
+ # If tree is provided, check it is consistent with the given distributions
116
+ if tree:
117
+ tree_var_names = tree.get_variables()
118
+ dist_par_names = {dist._name for dist in distributions}
119
+
120
+ if len(tree_var_names) != len(distributions):
121
+ raise ValueError(
122
+ f"There are {len(tree_var_names)} variables in the tree, but {len(distributions)} distributions are provided. "
123
+ "This may be due to passing multiple distributions with the same parameter name. "
124
+ f"The tree variables are {tree_var_names} and the distribution parameter names are {dist_par_names}."
125
+ )
126
+
127
+ if not all(var_name in dist_par_names for var_name in tree_var_names):
128
+ raise ValueError(
129
+ f"Variable names in the tree {tree_var_names} do not match the parameter names in the distributions {dist_par_names}. "
130
+ "Ensure the name is inferred from the variable or explicitly provide it using name='var_name' in the distribution."
131
+ )
132
+
133
+ # Match random variable name with distribution parameter name (for single distribution)
134
+ if len(distributions) == 1 and tree is None:
135
+ dist = next(iter(distributions))
136
+ dist_par_name = dist._name
137
+ if dist_par_name is not None:
138
+ if name is not None and dist_par_name != name:
139
+ raise ValueError(f"Parameter name '{dist_par_name}' of the distribution does not match the input name '{name}' for the random variable.")
140
+ name = dist_par_name
141
+
142
+ self._distributions = distributions
143
+ """ The distribution from which the random variable originates. """
144
+
145
+ self._tree = tree
146
+ """ The tree representation of the random variable. """
147
+
148
+ self._original_variable = None
149
+ """ Stores the original variable if this is a conditioned copy"""
150
+
151
+ self._name = name
152
+ """ Name of the random variable. """
153
+
154
+
155
+ def __call__(self, *args, **kwargs) -> Any:
156
+ """ Evaluate random variable at a given parameter value. For example, for random variable `X`, `X(1)` gives `1` and `(X+1)(1)` gives `2` """
157
+
158
+ if args and kwargs:
159
+ raise ValueError("Cannot pass both positional and keyword arguments to RandomVariable")
160
+
161
+ if args:
162
+ kwargs = self._parse_args_add_to_kwargs(args, kwargs)
163
+
164
+ # Check if kwargs match parameter names using a all compare
165
+ if not all([name in kwargs for name in self.parameter_names]) or not all([name in self.parameter_names for name in kwargs]):
166
+ raise ValueError(f"Expected arguments {self.parameter_names}, got arguments {kwargs}")
167
+
168
+ return self.tree(**kwargs)
169
+
170
+ @property
171
+ def tree(self):
172
+ if self._tree is None:
173
+ if len(self._distributions) > 1:
174
+ 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__))
175
+ self._tree = VariableNode(self.name)
176
+ return self._tree
177
+
178
+ @property
179
+ def name(self):
180
+ """ Name of the random variable. If not provided, the name is extracted from the variable name in the code. """
181
+ if self._is_copy: # Extract the original variable name if this is a copy
182
+ return self._original_variable.name
183
+ if self._name is None: # If None extract the name from the stack
184
+ self._name = cuqi.utilities._get_python_variable_name(self)
185
+ if self._name is not None:
186
+ self._inject_name_into_distribution(self._name)
187
+ return self._name
188
+
189
+ @name.setter
190
+ def name(self, name):
191
+ if self._is_copy:
192
+ 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.")
193
+ self._name = name
194
+
195
+ @property
196
+ def distribution(self) -> cuqi.distribution.Distribution:
197
+ """ Distribution from which the random variable originates. """
198
+ if len(self._distributions) > 1:
199
+ raise ValueError("Cannot get distribution from random variable defined by multiple distributions")
200
+ self._inject_name_into_distribution()
201
+ return next(iter(self._distributions))
202
+
203
+ @property
204
+ def distributions(self) -> set:
205
+ """ Distributions from which the random variable originates. """
206
+ self._inject_name_into_distribution()
207
+ return self._distributions
208
+
209
+ @property
210
+ def parameter_names(self) -> str:
211
+ """ Name of the parameter that the random variable can be evaluated at. """
212
+ self._inject_name_into_distribution()
213
+ return [distribution.name for distribution in self.distributions] # Consider renaming .name to .par_name for distributions
214
+
215
+ @property
216
+ def dim(self):
217
+ if self.is_transformed:
218
+ raise NotImplementedError("Dimension not implemented for transformed random variables")
219
+ return self.distribution.dim
220
+
221
+ @property
222
+ def geometry(self):
223
+ if self.is_transformed:
224
+ raise NotImplementedError("Geometry not implemented for transformed random variables")
225
+ return self.distribution.geometry
226
+
227
+ @geometry.setter
228
+ def geometry(self, geometry):
229
+ if self.is_transformed:
230
+ raise NotImplementedError("Geometry not implemented for transformed random variables")
231
+ self.distribution.geometry = geometry
232
+
233
+ @property
234
+ def expression(self):
235
+ """ Expression (formula) of the random variable. """
236
+ return str(self.tree)
237
+
238
+ @property
239
+ def is_transformed(self):
240
+ """ Returns True if the random variable is transformed. """
241
+ return not isinstance(self.tree, VariableNode)
242
+
243
+ @property
244
+ def _non_default_args(self) -> List[str]:
245
+ """List of non-default arguments to distribution. This is used to return the correct
246
+ arguments when evaluating the random variable.
247
+ """
248
+ return self.parameter_names
249
+
250
+ def _inject_name_into_distribution(self, name=None):
251
+ if len(self._distributions) == 1:
252
+ dist = next(iter(self._distributions))
253
+ if dist._name is None:
254
+ if name is None:
255
+ name = self.name
256
+ dist._name = name
257
+
258
+ def _parse_args_add_to_kwargs(self, args, kwargs) -> dict:
259
+ """ Parse args and add to kwargs if any. Arguments follow self.parameter_names order. """
260
+ if len(args) != len(self.parameter_names):
261
+ raise ValueError(f"Expected {len(self.parameter_names)} arguments, got {len(args)}. Parameters are: {self.parameter_names}")
262
+
263
+ # Add args to kwargs
264
+ for arg, name in zip(args, self.parameter_names):
265
+ kwargs[name] = arg
266
+
267
+ return kwargs
268
+
269
+ def __repr__(self):
270
+ # Create strings for parameter name ~ distribution pairs
271
+ parameter_strings = [f"{name} ~ {distribution}" for name, distribution in zip(self.parameter_names, self.distributions)]
272
+ # Join strings with newlines
273
+ parameter_strings = "\n".join(parameter_strings)
274
+ # Add initial newline and indentations
275
+ parameter_strings = "\n".join(["\t"+line for line in parameter_strings.split("\n")])
276
+ # Print parameter strings with newlines
277
+ if self.is_transformed:
278
+ title = f"Transformed Random Variable"
279
+ else:
280
+ title = f""
281
+ if self.is_transformed:
282
+ body = (
283
+ f"\n"
284
+ f"Expression: {self.tree}\n"
285
+ f"Components: \n{parameter_strings}"
286
+ )
287
+ else:
288
+ body = parameter_strings.replace("\t","")
289
+ return title+body
290
+
291
+ @property
292
+ def _is_copy(self):
293
+ """ Returns True if this is a copy of another random variable, e.g. by conditioning. """
294
+ return hasattr(self, '_original_variable') and self._original_variable is not None
295
+
296
+ def _make_copy(self):
297
+ """ Returns a shallow copy of the density keeping a pointer to the original. """
298
+ new_variable = copy(self)
299
+ new_variable._distributions = copy(self.distributions)
300
+ new_variable._tree = copy(self._tree)
301
+ new_variable._original_variable = self
302
+ return new_variable
303
+
304
+ def _apply_operation(self, operation, other=None) -> 'RandomVariable':
305
+ """
306
+ Apply a specified operation to this RandomVariable.
307
+ """
308
+ if isinstance(other, cuqi.distribution.Distribution):
309
+ raise ValueError("Cannot apply operation to distribution. Use .rv to create random variable first.")
310
+ if other is None: # unary operation case
311
+ return RandomVariable(self.distributions, operation(self.tree))
312
+ elif isinstance(other, RandomVariable): # binary operation case with another random variable that has distributions
313
+ return RandomVariable(self.distributions | other.distributions, operation(self.tree, other.tree))
314
+ return RandomVariable(self.distributions, operation(self.tree, other)) # binary operation case with any other object (constant)
315
+
316
+ def __add__(self, other) -> 'RandomVariable':
317
+ return self._apply_operation(operator.add, other)
318
+
319
+ def __radd__(self, other) -> 'RandomVariable':
320
+ return self.__add__(other)
321
+
322
+ def __sub__(self, other) -> 'RandomVariable':
323
+ return self._apply_operation(operator.sub, other)
324
+
325
+ def __rsub__(self, other) -> 'RandomVariable':
326
+ return self._apply_operation(lambda x, y: operator.sub(y, x), other)
327
+
328
+ def __mul__(self, other) -> 'RandomVariable':
329
+ return self._apply_operation(operator.mul, other)
330
+
331
+ def __rmul__(self, other) -> 'RandomVariable':
332
+ return self.__mul__(other)
333
+
334
+ def __truediv__(self, other) -> 'RandomVariable':
335
+ return self._apply_operation(operator.truediv, other)
336
+
337
+ def __rtruediv__(self, other) -> 'RandomVariable':
338
+ return self._apply_operation(lambda x, y: operator.truediv(y, x), other)
339
+
340
+ def __matmul__(self, other) -> 'RandomVariable':
341
+ if isinstance(other, cuqi.model.Model) and not isinstance(other, cuqi.model.LinearModel):
342
+ raise TypeError("Cannot apply matmul to non-linear models")
343
+ return self._apply_operation(operator.matmul, other)
344
+
345
+ def __rmatmul__(self, other) -> 'RandomVariable':
346
+ if isinstance(other, cuqi.model.Model) and not isinstance(other, cuqi.model.LinearModel):
347
+ raise TypeError("Cannot apply matmul to non-linear models")
348
+ return self._apply_operation(lambda x, y: operator.matmul(y, x), other)
349
+
350
+ def __neg__(self) -> 'RandomVariable':
351
+ return self._apply_operation(operator.neg)
352
+
353
+ def __abs__(self) -> 'RandomVariable':
354
+ return self._apply_operation(abs)
355
+
356
+ def __pow__(self, other) -> 'RandomVariable':
357
+ return self._apply_operation(operator.pow, other)
358
+
359
+ def __getitem__(self, other) -> 'RandomVariable':
360
+ return self._apply_operation(operator.getitem, other)
cuqi/model/_model.py CHANGED
@@ -351,6 +351,17 @@ class Model(object):
351
351
  new_model._non_default_args = [x.name] # Defaults to x if distribution had no name
352
352
  return new_model
353
353
 
354
+ # If input is a random variable, we handle it separately
355
+ if isinstance(x, cuqi.experimental.algebra.RandomVariable):
356
+ return self._handle_random_variable(x)
357
+
358
+ # If input is a Node from internal abstract syntax tree, we let the Node handle the operation
359
+ # We use NotImplemented to indicate that the operation is not supported from the Model class
360
+ # in case of operations such as "@" that can be interpreted as both __matmul__ and __rmatmul__
361
+ # the operation may be delegated to the Node class.
362
+ if isinstance(x, cuqi.experimental.algebra.Node):
363
+ return NotImplemented
364
+
354
365
  # Else we apply the forward operator
355
366
  return self._apply_func(self._forward_func,
356
367
  self.range_geometry,
@@ -463,6 +474,20 @@ class Model(object):
463
474
  not type(self.domain_geometry) in _get_identity_geometries():
464
475
  raise NotImplementedError("Gradient not implemented for model {} with domain geometry {}".format(self,self.domain_geometry))
465
476
 
477
+ def _handle_random_variable(self, x):
478
+ """ Private function that handles the case of the input being a random variable. """
479
+ # If random variable is not a leaf-type node (e.g. internal node) we return NotImplemented
480
+ if not isinstance(x.tree, cuqi.experimental.algebra.VariableNode):
481
+ return NotImplemented
482
+
483
+ # In leaf-type node case we simply change the parameter name of model to match the random variable name
484
+ dist = x.distribution
485
+ if dist.dim != self.domain_dim:
486
+ raise ValueError("Attempting to match parameter name of Model with given random variable, but random variable dimension does not match model domain dimension.")
487
+
488
+ new_model = copy(self)
489
+ new_model._non_default_args = [dist.name]
490
+ return new_model
466
491
 
467
492
  def __len__(self):
468
493
  return self.range_dim
cuqi/problem/_problem.py CHANGED
@@ -771,11 +771,11 @@ class BayesianProblem(object):
771
771
  if self._check_posterior(self, CMRF, must_have_gradient=True): # Use L-BFGS-B for CMRF prior as it has better performance for this multi-modal posterior
772
772
  if disp: print(f"Using scipy.optimize.L_BFGS_B on negative log of {density.__class__.__name__}")
773
773
  if disp: print("x0: ones vector")
774
- solver = cuqi.solver.L_BFGS_B(func, x0, gradfunc=gradfunc)
774
+ solver = cuqi.solver.ScipyLBFGSB(func, x0, gradfunc=gradfunc)
775
775
  else:
776
776
  if disp: print(f"Using scipy.optimize.minimize on negative log of {density.__class__.__name__}")
777
777
  if disp: print("x0: ones vector")
778
- solver = cuqi.solver.minimize(func, x0, gradfunc=gradfunc)
778
+ solver = cuqi.solver.ScipyMinimizer(func, x0, gradfunc=gradfunc)
779
779
 
780
780
  x_MAP, solver_info = solver.solve()
781
781
 
cuqi/solver/__init__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from ._solver import (
2
- L_BFGS_B,
3
- minimize,
4
- maximize,
5
- LS,
2
+ ScipyLBFGSB,
3
+ ScipyMinimizer,
4
+ ScipyMaximizer,
5
+ ScipyLeastSquares,
6
6
  CGLS,
7
7
  LM,
8
8
  PDHG,
cuqi/solver/_solver.py CHANGED
@@ -15,7 +15,7 @@ except ImportError:
15
15
  has_cholmod = False
16
16
 
17
17
 
18
- class L_BFGS_B(object):
18
+ class ScipyLBFGSB(object):
19
19
  """Wrapper for :meth:`scipy.optimize.fmin_l_bfgs_b`.
20
20
 
21
21
  Minimize a function func using the L-BFGS-B algorithm.
@@ -30,14 +30,10 @@ class L_BFGS_B(object):
30
30
  Initial guess.
31
31
  gradfunc : callable f(x,*args), optional
32
32
  The gradient of func.
33
- If None, then the solver approximates the gradient.
33
+ If None, the solver approximates the gradient with a finite difference scheme.
34
34
  kwargs : keyword arguments passed to scipy's L-BFGS-B algorithm. See documentation for scipy.optimize.minimize
35
-
36
- Methods
37
- ----------
38
- :meth:`solve`: Runs the solver and returns the solution and info about the optimization.
39
35
  """
40
- def __init__(self,func,x0, gradfunc = None, **kwargs):
36
+ def __init__(self, func, x0, gradfunc = None, **kwargs):
41
37
  self.func= func
42
38
  self.x0 = x0
43
39
  self.gradfunc = gradfunc
@@ -83,7 +79,7 @@ class L_BFGS_B(object):
83
79
  "nfev": solution[2]['funcalls']}
84
80
  return solution[0], info
85
81
 
86
- class minimize(object):
82
+ class ScipyMinimizer(object):
87
83
  """Wrapper for :meth:`scipy.optimize.minimize`.
88
84
 
89
85
  Minimize a function func using scipy's optimize.minimize module.
@@ -115,12 +111,8 @@ class minimize(object):
115
111
  ‘trust-krylov’
116
112
  If not given, chosen to be one of BFGS, L-BFGS-B, SLSQP, depending if the problem has constraints or bounds.
117
113
  kwargs : keyword arguments passed to scipy's minimizer. See documentation for scipy.optimize.minimize
118
-
119
- Methods
120
- ----------
121
- :meth:`solve`: Runs the solver and returns the solution and info about the optimization.
122
114
  """
123
- def __init__(self,func,x0, gradfunc = None, method = None, **kwargs):
115
+ def __init__(self, func, x0, gradfunc = '2-point', method = None, **kwargs):
124
116
  self.func= func
125
117
  self.x0 = x0
126
118
  self.method = method
@@ -147,18 +139,20 @@ class minimize(object):
147
139
  info = {"success": solution['success'],
148
140
  "message": solution['message'],
149
141
  "func": solution['fun'],
150
- "grad": solution['jac'],
151
142
  "nit": solution['nit'],
152
143
  "nfev": solution['nfev']}
144
+ # if gradfunc is callable, record the gradient in the info dict
145
+ if 'jac' in solution.keys():
146
+ info['grad'] = solution['jac']
153
147
  if isinstance(self.x0,CUQIarray):
154
148
  sol = CUQIarray(solution['x'],geometry=self.x0.geometry)
155
149
  else:
156
150
  sol = solution['x']
157
151
  return sol, info
158
152
 
159
- class maximize(minimize):
160
- """Simply calls ::class:: cuqi.solver.minimize with -func."""
161
- def __init__(self,func,x0, gradfunc = None, method = None, **kwargs):
153
+ class ScipyMaximizer(ScipyMinimizer):
154
+ """Simply calls ::class:: cuqi.solver.ScipyMinimizer with -func."""
155
+ def __init__(self, func, x0, gradfunc = None, method = None, **kwargs):
162
156
  def nfunc(*args,**kwargs):
163
157
  return -func(*args,**kwargs)
164
158
  if gradfunc is not None:
@@ -170,7 +164,7 @@ class maximize(minimize):
170
164
 
171
165
 
172
166
 
173
- class LS(object):
167
+ class ScipyLeastSquares(object):
174
168
  """Wrapper for :meth:`scipy.optimize.least_squares`.
175
169
 
176
170
  Solve nonlinear least-squares problems with bounds:
@@ -189,7 +183,7 @@ class LS(object):
189
183
  Initial guess.
190
184
  Jac : callable f(x,*args), optional
191
185
  The Jacobian of func.
192
- If None, then the solver approximates the Jacobian.
186
+ If not specified, the solver approximates the Jacobian with a finite difference scheme.
193
187
  loss: callable rho(x,*args)
194
188
  Determines the loss function
195
189
  'linear' : rho(z) = z. Gives a standard least-squares problem.
@@ -203,7 +197,7 @@ class LS(object):
203
197
  'dogbox', dogleg algorithm with rectangular trust regions, for small problems with bounds.
204
198
  'lm', Levenberg-Marquardt algorithm as implemented in MINPACK. Doesn't handle bounds and sparse Jacobians.
205
199
  """
206
- def __init__(self, func, x0, jacfun=None, method='trf', loss='linear', tol=1e-6, maxit=1e4):
200
+ def __init__(self, func, x0, jacfun='2-point', method='trf', loss='linear', tol=1e-6, maxit=1e4):
207
201
  self.func = func
208
202
  self.x0 = x0
209
203
  self.jacfun = jacfun
@@ -9,7 +9,7 @@ import cuqi
9
9
  def _get_python_variable_name(var):
10
10
  """ Retrieve the Python variable name of an object. Takes the first variable name appearing on the stack that is not in the ignore list. """
11
11
 
12
- ignored_var_names = ["self", "cls", "obj", "var", "_"]
12
+ ignored_var_names = ["self", "cls", "obj", "var", "_", "result", "args", "kwargs", "par_name", "name", "distribution", "dist"]
13
13
 
14
14
  # First get the stack size and loop (in reverse) through the stack
15
15
  # It can be a bit slow to loop through stack size so we limit the levels
@@ -29,7 +29,7 @@ def _get_python_variable_name(var):
29
29
  if len(var_names) > 0:
30
30
  return var_names[0]
31
31
 
32
- warnings.warn("Could not automatically find variable name for object: {}. Use keyword `name` when defining distribution to specify a name. If code runs slowly and variable name is not needed set config.MAX_STACK_SEARCH_DEPTH to 0.".format(var))
32
+ warnings.warn("Could not automatically find variable name for object. Did you assign (=) the object to a python variable? Alternatively, use keyword `name` when defining distribution to specify a name. If code runs slowly and variable name is not needed set config.MAX_STACK_SEARCH_DEPTH to 0. These names are reserved {} and should not be used as object name.".format(ignored_var_names))
33
33
 
34
34
  return None
35
35