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
Make submersion and tangent_submersion abstract in LevelSet #1681
Conversation
…e children accordingly
Looks promising !!!
maybe with something like @abs.abstract
def define_embedding_space(self):
return
@property
def embedding_space(self):
if not hasattr(self, '_embedding_space'):
self._embedding_space = self.define_embedding_space()
return self._embedding_space or alternatively: self._embedding_space = self.define_embedding_space() in
Let us discuss ! |
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.
This is great! I also like the fact that having them as abstract method enforces a consistent API of these functions.
Re: "should value somehow also be forced to be defined or passing as argument to parent works fine? (I think it should be OK as argument)" I think that value could be hardcoded to 0. because the submersion can always be redefined (by subtracting Re: API |
Another inconsistency in |
Another comment: tangent_submersion should me a method but not be abstract but it does not have to be overwritten. If it is not provided, then we use gs.autodiff.jacobian(submersion). <--- that piece of code would go in the LevelSet method. |
Another question: we want to put But what prevents users to still do "sphere.embedding_space = new_space" again? |
Maybe my question is stupid but: we agree we have intrinsic => |
I think it is what is written in |
Because |
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
@ninamiolane @ymontmarin I think this may be ready. A new review from your side may be worthwhile, as several things have changed. I think it implements "our vision". |
Codecov Report
@@ Coverage Diff @@
## master #1681 +/- ##
===========================================
- Coverage 91.95% 80.56% -11.38%
===========================================
Files 129 117 -12
Lines 13327 12604 -723
===========================================
- Hits 12253 10153 -2100
- Misses 1074 2451 +1377
Flags with carried forward coverage won't be shown. Click here to find out more.
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
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.
Very nice, thank you! I've just put minor comments. Maybe double check the docstrings of the submersions so that they match the fact that the value is now 0 (I've put one comment to that effect in SE(n).
I'm still not a huge fan of the _define_embedding_space, because:
- this would be the only method across all manifolds that is "talking to the user" (with the keyword "define") and this feels wrong. All the other methods are purely impersonal and geometry-facing.
- if it's main role is to prevent the user from changing the embedding_space attribute, then this goes against our vision of "not preventing anything, but letting the user use the manifolds at their own risk". This would also be prevented by the new gold rule which is "never modify the attributes of a manifold, unless it's its numerics objects (ExpSolver)".
But I forget the details of our debate about this : happy to go with it if I'm forgetting important aspect of this choice.
@ninamiolane, regarding I can only see an alternative to this, but without the strength we have above (i.e. a less knowledgeable contributor may not know he/she has to define
The alternative of having a property (or function) simply called Do you have any alternatives in mind? Would we prefer the alternative I mentioned to the current implementation? Edit: What I would probably like do to make the current implementation more coherent with the second point you make is to not have |
I would indeed prefer the alternative of the attribute. To tell the less knowledgeable contributor he/she has to define embedding_space, we could have a clear error message: the first time self.embedding_space is needed, if it is not defined then the error tells the user to define it. I know we talked a lot about this already. Maybe we could poll everybody at the next meeting to get a vote? |
From what i remember from the discussion the starting point was that in python methods implementation are defined when child class are written, meaning it is decided before any instantiation. Whereas argument of As we discuss, it is more natural to have immersion (and its derivative) as a method, since it is a function anyway, and it is known when child class are written. Mathematically, the immersion is the tuple But if we step back, what we really want is that class Child(LevelSet):
def immersion(self, x):
XXX
e = Euclidean(n)
a = Child(dim=m, embedding_space=e) which is pretty weird to me, because immersion implementation is written inside the object, and the space it goes to is written outside. But I agree with @ninamiolane that a method So for a better use experience, it could be a property ! But then the instantiation would occur every time the attribute is asked. A python magic that can work is this: class LevelSet(ABC):
def __init__(self, dim, default_coords_type="intrinsic", **kwargs):
super().__init__(dim=dim, default_coords_type=default_coords_type, **kwargs)
def __getattribute__(self, name):
if name == 'embedding_space':
if not hasattr(self, '_embedding_space'):
self._embedding_space = super().__getattribute__('embedding_space')
return self._embedding_space
return super().__getattribute__(name)
@property
@abstractmethod
def embedding_space(self):
pass
@abstractmethod
def immersion(self):
pass In this way, We will have class Hypersphere(LevelSet):
def __init__(self, dim):
super().__init__(dim=dim, default_coords_type='extrasinc', **kwargs)
@property
def embedding_space(self):
return Euclidean(self.dim + 1)
def immersion(self, x):
return gs.norm(x) - 1 |
I think @ymontmarin's I fully disagree with the idea of instantiating it every time: it will negatively impact performance and will not allow to change any numerical parameter of algorithms associated with a given embedding space. I don't like so much the tricks of checking if it The semantics of I agree with the poll @ninamiolane, but after our discussions, I kind of think the solution current implemented in the PR is the strongest technically speaking. In order to move on, do you agree I merge this solution for now (after addressing @ninamiolane's comments) and then we update it based on our poll? (The implementation on this PR is by far much cleaner than the currently available in master.) |
@LPereira95 I also add some minor comments in the code. About the main discussion:
I think the circular issue (and other comments) need to be addressed before merging. I only give suggestion for the love <3 of geomstats; let me know for curiosity, what people want :) An idea for circular loop issue while following @LPereira95 implementation choice: class LevelSet(ABC):
def __init__(self, dim, shape=None, default_coords_type="extrinsic", **kwargs):
self.embedding_space = self._create_embedding_space(dim, shape, default_coords_type, **kwargs)
if shape is None:
shape = self.embedding_space.shape
super().__init__(dim=dim, shape=shape, default_coords_type=default_coords_type, **kwargs)
@abstractmethod
def _create_embedding_space(self, dim, shape, default_coords_type, **kwargs):
pass
@abstractmethod
def immersion(self):
pass Such that implementation of class Hypersphere(LevelSet):
def _create_embedding_space(self, dim, **kwargs):
return Euclidean(dim + 1)
def immersion(self, x):
return gs.norm(x) - 1 |
Thanks a lot @ymontmarin, we deeply appreciate your comments and suggestions. All this discussion is to try to come up with a solution that pleases everyone and is technically robust. Thanks for the clarification on the constant time operation for key lookup (in fact I should have thought better before my comment before). For the third point, I mean that since we definitely have to instantiate Your point on circularity is in fact interesting, but I would argue there's not really a circularity. What's happening is that we normally need the arguments passed in child init to instantiate the embedding space. I would say it is a good practice to store init args as attributes (e.g. Both |
Ok I get the implicit reasoning for the instanciation, I agree with you ! But in our case the argument are setted as attribute of the object during the call The natural solution (which is what happen in sklearn etc...) would be to have Maybe I am wrong but for me this code will fail: class LevelSet(Manifold):
def __init__(self, dim, shape=None, default_coords_type="extrinsic", **kwargs):
self.embedding_space = self._create_embedding_space()
if shape is None:
shape = self.embedding_space.shape
super().__init__(dim=dim, shape=shape, default_coords_type=default_coords_type, **kwargs)
@abstractmethod
def _create_embedding_space(self):
pass
@abstractmethod
def immersion(self, x):
pass
class Hypersphere(LevelSet):
def _create_embedding_space(self):
return Euclidean(self.dim + 1)
def immersion(self, x):
return gs.norm(x) - 1 And maybe I did not totally understand your response |
@LPereira95 Nevermind, I dit not see you were resetting |
class LevelSet(Manifold):
def __init__(self, dim, shape=None, default_coords_type="extrinsic", **kwargs):
self.embedding_space = self._create_embedding_space()
if shape is None:
shape = self.embedding_space.shape
super().__init__(dim=dim, shape=shape, default_coords_type=default_coords_type, **kwargs)
@abstractmethod
def _create_embedding_space(self):
pass
@abstractmethod
def immersion(self, x):
pass
class Hypersphere(LevelSet):
def __init__(self, dim):
self.dim = dim
super().__init__(dim)
def _create_embedding_space(self):
return Euclidean(self.dim + 1)
def immersion(self, x):
return gs.norm(x) - 1 will indeed work and looks pretty clean to me :) Sorry for the misunderstanding ! |
@ninamiolane I think I've addressed everything here. I'll just ensure tests are passing and then I'll merge. |
Hello @LPereira95 |
Hi @ymontmarin! It looks like that comments are pending or I really missed them out... |
@LPereira95 What does the label "pending" mean ? I am not so used to review on github |
Your review has not been submitted @ymontmarin. Only you have access to it. There should be a button on top with "Submit review". |
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.
First time using the review tool !
geomstats/geometry/base.py
Outdated
tuple(-n + 1 + i for i in range(n - 1)) | ||
if gs.ndim(point) > len(self.shape) | ||
else tuple(-n + i for i in range(n)) | ||
) |
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.
Isn't axis = tuple(range(n - len(self.shape), n))
working ? I think you want the all
to apply to the axis involved in the shape of point which are the latest. It may be shorter and even work with general batching
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.
Here @LPereira95
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.
Thanks @ymontmarin!
This solution fails for:
space = Hyperboloid(3)
space.belongs(space.random_point())
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 think this may work in all cases:
axis = tuple(range(n - len(self.shape), n)) if (n - len(self.shape)) >= 0 else ()
What do you think?
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.
Actually I'm not so sure this version increases readability (it may even make it harder to understand). Besides, we can easily achieve general batching by replacing 1 by gs.ndim(point) - len(self.shape)
in the first range
...
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.
a simplified and more readable version may be:
n_batch = gs.ndim(point) - len(self.shape)
axis = tuple(range(-n + n_batch, 0))
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.
After thinking about it, none of the two solutions are very universal.
point
is of shape [*batch_shape, *self.shape]
immersed_point
is of shape [*batch_shape, *sub_shape]
If the subermesion only give a real sub_value_shape = () otherwise we want the all
on the sub_shape axis.
So maybe a sorter, more readable and universal way that work is:
belongs = self.embedding_space.belongs(point, atol)
if not gs.any(belongs):
return belongs
submersed_point = self.submersion(point)
constraint = gs.isclose(submersed_point, 0.0, atol=atol)
sub_ndim = gs.ndim(submersed_point) - gs.ndim(point) + len(self.shape)
if sub_ndim:
constraint = gs.all(constraint, axis=tuple(range(-sub_ndim, 0)))
return gs.logical_and(belongs, constraint)
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.
@LPereira95 My message was written before i saw your two messages ! I think it is almost the same :) (apart from the tuple construction here being inside the if)
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'm glad we came up with similar solutions independently. These ones I think are pretty universal. I'll merge now.
geomstats/geometry/base.py
Outdated
if gs.ndim(base_point) > len(self.shape) | ||
or gs.ndim(vector) > len(self.shape) | ||
else tuple(-n + i for i in range(n)) | ||
) |
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.
Same as above
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.
Here @LPereira95
@LPereira95 Ok !!! Thank you :) sorry I miss that, I left out the only two points (qhape stuff) that was not adress with the discussion and so on |
Test failures are unrelated... merging. |
Partially answers #1680 for
LevelSet
.Missing:
Need to discuss:
point
andvector
,point
, respectively?@ninamiolane @ymontmarin