-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sprint20 Add Random Variables (Using Abstract Syntax Tree) #243
base: main
Are you sure you want to change the base?
Conversation
3cbec6e
to
99974ac
Compare
Hi @amal-ghamdi and @jakobsj. Following the seminar I have now updated the tutorial. Feel free to review |
Minor issue if you do A = cuqi.model.LinearModel(lambda x: x, lambda y: y, 1, 1))
x = cuqi.distribution.Gaussian(0,1)
y = x+1
A@y # returns x+1 This is because EDIT: now fixed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick approval. Few tiny comments inline. Looks really good, exciting new functionality. Merge pending more detailed review and approval from @amal-ghamdi
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @nabriis for this rich feature. Really impressive work you have done here that I enjoyed reviewing and learning from!.
I have some comments here and there for your consideration.
6ab12d9
to
3d73813
Compare
Need to decide if |
Co-authored-by: amal-ghamdi <amal.m.alghamdi@gmail.com>
Co-authored-by: amal-ghamdi <amal.m.alghamdi@gmail.com>
Hello @jakobsj, @amal-ghamdi, @chaozg. I updated the code according to Jakobs comments. I also reverted the to_cuqi_format change, which greatly simplified the additional code to be reviewed (e.g. changes to specific distributions etc.). |
…ibution to RandomVariable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @nabriis, I've run through the tutorial again and really like the updates you have made. Most points are now addressed. Running through did result in a number of new questions/points, not sure if bugs, please see specific comments. I also resolved many of the conversations. I will take a look at the remaining ones. I haven't got more time today so I think it makes sense if you can address this round of comments, and any from others, then after that I can take a look again, including at other parts of the code.
I have gone through another time and resolved conversations that had been addressed. There are four old ones, I think all having to do with if_dist_force_rv. I think this functionality has changed back and forth to to_cuqi_array or similar, and I'd like to understand these changes and behavior better before resolving these. |
Co-authored-by: Jakob Sauer Jørgensen <jakobsj@users.noreply.github.com>
Thanks @jakobsj. I updated once again. In relation to the X = Gaussian(0, 1) both the mean and covariance will become ndarrays. The reason to generalize to # A Gaussian distribution with conditioning variable x (from cov) is created in line two:
x = Gaussian(0, 1).rv
y = Gaussian(0, x).rv The above is as expected. However, in the following case we get odd behavior that we would like to catch # A Gaussian distribution with a ??Gaussian distribution?? as cov is created in line two:
x = Gaussian(0, 1) # Forgot to put .rv
y = Gaussian(0, x).rv This behavior was also present in current CUQIpy. We could catch this mistake by parsing inputs and if they were a Distribution object, throw error with helpful message asking if the user forgot Originally I had a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @nabriis again for this carefully and creatively designed PR, powerful and impressive work!
I left some comments throughout, feel free to address or immediately resolve those as it makes sense to you.
I have few general thoughts for your consideration:
- I thought we can make
par_name
more specific and clear if we call itrv_name
, if that is applicable in all cases. - I would assume we need issues to update other demo/plugin repos to address this change.
Well done!
cuqi/distribution/_distribution.py
Outdated
# The following methods define algebraic operations on distributions | ||
# The return type is a RandomVariable instance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are these two comments outdated? if not, it seems a bit unclear
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Removed.
cuqi/likelihood/_likelihood.py
Outdated
@property | ||
def _par_name(self): | ||
""" Return name of likelihood """ | ||
return self.distribution._par_name | ||
|
||
@_par_name.setter | ||
def _par_name(self, value): | ||
self.distribution._par_name = value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just curious if the property par_name is sufficient here? May be I am missing why we need property _par_name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. This is now removed and it helped simplify some checks in RV class, which checked for the private property _par_name where it should not have done so.
self._name = _get_python_variable_name(self) | ||
return self._name | ||
if self._par_name is None: | ||
self._par_name = _get_python_variable_name(self) # TODO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have an issue #184 about refactoring UserDefinedLikelihood. I am not sure what the TODO here refer to but thought may be it can be added to that issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its related. Currently the UserDefinedLikelihood can infer python variable names where normally after this change it was supposed to only be RV class that does that. However, we have some use cases, where users define their UserDefinedLikelihood using e.g.
y = UserDefinedLikelihood(...)
Assuming it gets the name y. I think we can add this info into the issue you mentioned.
def get_conditioning_variables(self): | ||
""" Get conditioning variables. """ | ||
if self.is_transformed: | ||
raise NotImplementedError("Extracting conditioning variables is not implemented for transformed random variables") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you see this as high priority issue or not necessarily? if so, we can create an issue to address it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we should generate a tracking issue to track all of the features we have disabled and aim to create in the future for RV class. There are more than thus, but if we create the issue we can add bullets to it.
new_variable._distributions = copy(self.distributions) | ||
new_variable._tree = copy(self._tree) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are these two lines needed? Don’t they happen automatically after first line? not sure though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not, it is a shallow copy. So copying here ensures we get a new pointer to the distributions, meaning if we change internal parameters it only affects this new copied RV.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think of it as a 2-level deep copy.
# Providing data to the Bayesian problem creates a posterior as shown. | ||
# | ||
|
||
BP.set_data(y=y_obs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a comment here, the resulting distribution BP is JointDistribution. I think this is expected because our posterior is very specific and not general to handle this case. Do we need/have an issue for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't recall. But yes its not a "Posterior" class, but a JointDistribution representing a posterior over joint set of variables
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I experimented a bit with the rvs, and one scenario in inferring the distribution name came to mind (below). Depending on the flag, z2 name will be different. Is that something we want to capture in an issue? Also, is this is something we should prevent by not allowing creating more than one rv
from a distribution (kind of 1 to 1 association in terms of class relationship).
Z_s = cuqi.distribution.Gaussian(0, lambda s: s)
Z = Z_s(s=3)
z = Z.rv
flag = False
if flag:
print(z.name)
z2 = Z.rv
print(z2.name)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point here. I actually updated now so that .rv creates a copy of the distribution and does NOT introduce side effects to the original variables. Thus Z_s
and Z
in the above example does not get their parameter name updated. Instead its updated in z.dist.par_name
.
|
||
# %% | ||
###################################################################### | ||
# Direct evaluation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be we can clarify to the user here or in some other place in the tutorial that using () operator means different things when we talk about X and x, X(1) is logpdf and X.rv(1) is the evaluation as described here.
I believe I have now understood the points about forcing rv, etc, and I agree that in the spirit of being clear about when something is an RV and a distb respectively, we should not convert automatically between the two behind the scenes. Instead, as you suggest suitable error messages should be given when a user tries to mix. |
I have spent the morning thinking more about distributions, random variables and the "call" syntax. I am not sure if this is the best place to capture these, but for now I will put them here. Please note, that I honestly feel the random variable is an amazing addition, and I am only giving it so much scrutiny, because it introduces such a fundamental change in the usage of CUQIpy, that we want to make sure everything remains (or becomes!) consistent. This will be the first part, outlining some thoughts first on "call" syntax with only distributions, followed by using only random variables. I expect to follow up later (but out of time for now), with additional thoughts on potential cases of combining the two. Sorry if it reads slightly rambling at times, but I thought I'd sent it now, rather than let it sit until I have time to streamline it more. Only distributions Before introduction of RVs, we have distributions X = Gaussian(0,1) One can do X(4) in which the “call” syntax amounts to returning the scalar value of the pdf at the input value. We can have a “simple” conditional distribution but eg omitting an input Y = Gaussian(0) In which the “call” syntax amounts to conditioning on cov=2, and returning the resulting (unconditional) distribution. Similarly using lambda function can set up more involved conditional distributions such as Two issues here:
Z(t=4)(0) and doing this without keyword we see that Z(4) corresponds to Z(t=4): It surprises me that using “cov” is permitted, when we have explicitly made Y conditional on t, instead of cov. Is that what we want? Ideas to consider here: Replace both call syntax by more explicit syntax. And consider disallowing the usage of “cov” in conditioning when lambda function is used. And/or disallow positional inputs, allowing only keyword based for clarity. Only random variables: Introducing random variables (RVs): X = Gaussian(0, 1) we can now use “call” syntax (of the RV) to do For a transformed RV, eg. Conceptually, this appears very similar to conditioning for distributions, in the sense that we fix one and given that evaluate the other. Except the transformed variable here is doing algebraic functions, not feeding through a distribution. But we do want to use RVs to specify relations between Rvs/distributions, not just algebraic manipulations. So we want to do eg An additional aspect is that before conditioning we have |
Co-authored-by: amal-ghamdi <amal.m.alghamdi@gmail.com>
Co-authored-by: amal-ghamdi <amal.m.alghamdi@gmail.com>
Co-authored-by: amal-ghamdi <amal.m.alghamdi@gmail.com>
Co-authored-by: amal-ghamdi <amal.m.alghamdi@gmail.com>
Co-authored-by: amal-ghamdi <amal.m.alghamdi@gmail.com>
…hat is not the original one prior to conditioning
closes #202, #85, #29
Related to #250
Tracking of final changes to PR
17/11/2023
18/11/2023
condition
related properties of RVs for now.__call__
method of Distribution. Rename to e.g.fix
orgiven
24/11/2023