Skip to content
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

Curvature implementation #1606

Closed

Conversation

Jules-Deschamps
Copy link
Collaborator

Implementation of methods for metric and connection, so that riemannian metrics can return :

  • riemannian tensor
  • curvature
  • ricci tensor
  • scalar curvature
  • sectional curvature (was already implemented if curvature was)

Tests on shapes of curvature tensors symmetries of riemannian tensor on test files (geometry_test_cases and data_generation), and on negativity of sectional curvature of dirichlet distributions (on another script file, I did not know if I should add it in the test file).

@ninamiolane
Copy link
Collaborator

@Jules-Deschamps Looking good! You also need to modify:https://github.com/geomstats/geomstats/blob/master/tests/data/connection_data.py
to solve this error appearing in the tests:

testing_data object doesn't have 'ricci_tensor_shape_test_data' function for pairing with 'test_ricci_tensor_shape'

You can run pytest tests/test_geomstats/test_connection.py on your machine directly (so that you do not wait for the GitHub actions to finish).

Copy link
Collaborator

@ninamiolane ninamiolane left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM when test's infrastructure problem is solved.

@@ -453,6 +453,25 @@ def normal_basis(self, basis, base_point=None):

return gs.einsum("i, ikl->ikl", 1.0 / gs.sqrt(norms), basis)

def covariant_riemannian_tensor(self, base_point):
r"""Compute purely covariant version of Riemannian tensor at base_point.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, could you also add the formula for it, i.e. R_{ijkl} = g_{ll'}R_{ijk}^l' ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes will do! As for the testing thing I thought I had resolved this but turns out I haven't yes, thanks!

Copy link
Collaborator

@luisfpereira luisfpereira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Jules-Deschamps.

I'm just reviewing Connection.riemann_tensor now. Can you please take a look to my comments.

I'll come back to this PR soon.

R_ijk^l = riemann_tensor[i,j,k,l]
Riemannian tensor curvature.
"""
base_point = gs.to_ndarray(base_point, to_ndim=2)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work when base_point is a matrix?

Can we somehow avoid its use? Because we create a new array every time we do it. christoffels should work with a base point or multiple base points, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I was using it to know the number of points in base_point (to be able to permute the axes using gs.transpose), but if gs.move_axis (which I did not know existed) works I will get rid of these lines.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if base_point is a matrix (representing only a point in the manifold), you will get wrong information, no?

Your branch is too far behind, that's why you cannot find moveaxis. Please merge master after pulling upstream.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works apparently because there is a squeeze in christoffels. So for base_point of shape (1,dim), christoffels will return shape (dim, dim, dim) and jacobian_christoffels (dim, dim, dim, dim) because it is defined with a squeeze. So these lines are really just to get the number of points in base_point. I hope this is what you are talking about. This is a minimum reproducible example showing it works for several types of base_point (and namely a matrix representing only a point in the manifold).

import geomstats.backend as gs
from geomstats.information_geometry.dirichlet import DirichletDistributions

space = DirichletDistributions(3)

point_1 = space.random_point()
point_2 = gs.array([[1.0, 1.0, 1.0]])
point_3 = gs.array([[1.0, 2.0, 2.0], [1.0, 2.0, 1.0]])

print(space.metric.riemann_tensor(point_1))
print(space.metric.riemann_tensor(point_2))
print(space.metric.riemann_tensor(point_3))

Ok I will merge thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So turns out I am keeping base_point = gs.to_ndarray(base_point, to_ndim=2) in my last commit also because I want to compute the jacobian christoffels for each of the points in base_point, which is not possible otherwise (or so far I haven't found anything).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean here is related with #1607: riemann_tensor, the way it is coded now, will not work for manifolds whose default_point_type is matrix (in the same way christoffels doesn't).

I may be missing something in terms of concepts, but shouldn't everything also work for that case? (otherwise, as I told in #1607, we need to reconsider the class where to put this code).

base_point = gs.to_ndarray(base_point, to_ndim=2) can't work in the general case because you need to consider both point_types (matrix and vector). You need to do something like:

if self.default_point_type == 'vector':
  n_points = base_point.shape[0] if len(base_point.shape) > 1 else 1
else:
  n_points = base_point.shape[0] if len(base_points.shape > 2 else 1

or, even better:

from vectorization import get_n_points

n_points = get_n_points(base_point, self.default_point_type)

geomstats/geometry/connection.py Outdated Show resolved Hide resolved
Copy link
Collaborator

@alebrigant alebrigant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition 🎉 ! Just a few comments on the docstrings. Otherwise, it would be nice to have tests that check known values for the sectional curvature: for the hyperbolic space, sphere, and e.g. beta distributions.

return curvature

def riemann_tensor(self, base_point):
r"""Compute Riemannian tensor at base_point.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add in the docstring somewhere that the returned R_{ijk}^l verifies R(X_i, X_j)X_k = \sum_{l=1}^n R_{ijk}^l X_l where X_i is i-th basis vector of the tangent space.

geomstats/geometry/connection.py Show resolved Hide resolved

def scalar_curvature(self, base_point):
r"""Compute scalar curvature at base_point.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same: add formula of the scalar curvature in the docstring ?

@@ -453,6 +453,25 @@ def normal_basis(self, basis, base_point=None):

return gs.einsum("i, ikl->ikl", 1.0 / gs.sqrt(norms), basis)

def covariant_riemannian_tensor(self, base_point):
r"""Compute purely covariant version of Riemannian tensor at base_point.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be clarified by a formula. I suppose this is R_{ijks} = <R(X_i,X_j)X_k,X_s>=\sum_{l=1}^n R_{ijk}^lg_{ls} ? If so, it should be mentioned in the docstring, with X_i is the i-th basis vector of the tangent space.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I just checked because the tests were wrong but the convention used in the literature (or at least on wikipedia) is VERY weird (R_{ijkl} = <R(x_j, x_k)x_i, x_l>) so I think the previous successful tests were just a fluke. I will make a commit as soon as possible with your feedback!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must beware that the sign convention taken in Wikipedia for the Riemann curvature tensor (https://en.wikipedia.org/wiki/Riemann_curvature_tensor) is not the same as the one taken in geomstats (see this docstring) and this has effects on the rest of the definitions. So all definitions should be taken from the same reference book, and not Wikipedia.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @alebrigant !! @Jules-Deschamps , you can even put a mention in all the docstrings that we do not use the same convention as in wikipedia, and therefore there are differences. Otherwise this might confuse a lot of people.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at all the formulae, I'm not completely sure that they are all compatible, i.e. take the same conventions. The only way to avoid mistakes in my view, is to pick a reference, and to take all formulas from this reference. This reference should be added to all docstrings.

metric = self.metric_matrix(base_point)
covariant_tensor = gs.einsum("...ij, ...klmj->...iklm", metric, riemann_tensor)
return covariant_tensor

def sectional_curvature(self, tangent_vec_a, tangent_vec_b, base_point=None):
r"""Compute the sectional curvature.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wikipedia reference in the docstring should be removed because the convention is not the same one. Instead, one can put e.g. do Carmo, or some other book where the convention is the same.

@Jules-Deschamps
Copy link
Collaborator Author

I have added modifications following your feedbacks and have tested for dirichlet distributions (the sectional curvature is indeed everywhere negative), but for hyperbolic spaces it is more complicated because sectional and scalar curvatures require metric_matrix which has not yet been implemented. As for testing on the hypersphere, it is a bit complicated considering the extrinsic and intrinsic coordinates but I will try.

Also, I was wondering where I should add the tests and data generators so that I don't have to rewrite all the tests for every manifold? For example for dirichlet distributions, in dirichlet_data:

def ricci_tensor_shape_test_data(self):
        return self._ricci_tensor_shape_test_data(
            self.metric_args_list, self.space_list
        )
```, this looks like it could be replaced up above the architecture? Thanks

@ninamiolane
Copy link
Collaborator

Fantastic, thanks for addressing the comments. There are still some testing issues, which you can access by clicking on "details" in the github workflows above, or e.g. here:
https://github.com/geomstats/geomstats/runs/7400372430?check_suite_focus=true
for example:

    raise Exception(
E   Exception: testing_data object doesn't have 'covariant_riemann_tensor_bianchi_identity_test_data' function for pairing with 'test_covariant_riemann_tensor_bianchi_identity'

Yes, I believe we have to have this bit of redundant code for now:

def ricci_tensor_shape_test_data(self):
        return self._ricci_tensor_shape_test_data(
            self.metric_args_list, self.space_list
        )

but @LPereira95 was working on removing it so that you do not have to copy-paste it like this, in the future. @LPereira95 do you confirm?

@alebrigant
Copy link
Collaborator

To test sectional curvature more precisely than just the sign, you can check that for beta distributions, at any point gs.array([x, y]), it is given by

K_xy = gs.polygamma(2, x) * gs.polygamma(2, y) * gs.polygamma(2, x+y) * (
    gs.polygamma(1, x) / gs.polygamma(2, x) 
  + gs.polygamma(1, y) / gs.polygamma(2, y) 
  - gs.polygamma(1, x+y) / gs.polygamma(2, x+y)
) / detg ** 2

where

detg = gs.polygamma(1, x) * gs.polygamma(1, y) - gs.polygamma(1, x+y) * (gs.polygamma(1, x) + gs.polygamma(1, y))

See Appendix in https://www.sciencedirect.com/science/article/pii/S2405896321005851?via%3Dihub.

@Jules-Deschamps
Copy link
Collaborator Author

Thanks again!

I don't really understand how to address the issues pointed out by pytest because they are the same for every manifold (the data generation methods are not implemented without the '_' underscore whent they inherit from the tests so they don't know with what data to run the tests). This is not only for the bianchi identity but for all the new tests I have added in geometry_test_cases. So should I add the test methods without underscore for every manifold/geometry?

As for the testing of the sectional curvature on the Beta manifold, it works perfect! I have added the associated test.

@luisfpereira
Copy link
Collaborator

For the tests, I suggest for the moment that we add a skip to them (which we unskip as soon as I merge the tests PR). Nevertheless, notice that most of the metrics do not have metric_matrix implemented, which means most of new implementations will not work.

We still need to go over some parts of the new code (see above) before merging, as it may not be working for several cases.

Copy link
Collaborator

@alebrigant alebrigant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making the changes ! I still recommend further checks of the docstrings, to avoid all mistakes. See the detailed comments.

@@ -443,6 +443,9 @@ def curvature(self, tangent_vec_a, tangent_vec_b, tangent_vec_c, base_point):
the curvature is defined by :math:`R(X,Y)Z = \nabla_{[X,Y]}Z
- \nabla_X\nabla_Y Z + \nabla_Y\nabla_X Z`.

Convention used in the literature is:
R_{ijkl} = <R(x_j, x_k)x_i, x_l>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not appear here because we only have a connection and no Riemannian metric. However it is a good addition to RiemannianMetric.

-------
riemann_curvature : array-like, shape=[..., {dim, [n, m]}, {dim, [n, m]},
{dim, [n, m]}, {dim, [n, m]}]
R_{ijk}^l = riemann_tensor[...,i,j,k,l]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, the convention used should be specified: what is the definition of R_{ijk}^l ?

If x_i denotes local coordinates and X_i= \partial / \partial x_i the i-th basis vector of the tangent space,

is the convention do Carmo's, where R(X_i, X_j)X_k = R_{ijk}^l X_l, which means R_{ijk}^l = dx^l(R(X_i, X_j)X_k)

or is it Wikipedia's convention, where R_{ijk}^l = dx^l(R(X_j, X_k)X_i) ?

def sectional_curvature(self, tangent_vec_a, tangent_vec_b, base_point=None):
r"""Compute the sectional curvature.

For two orthonormal tangent vectors :math:`x,y` at a base point,
the sectional curvature is defined by :math:`<R(x, y)x, y> =
<R_x(y), y>`. For non-orthonormal vectors vectors, it is
<R_x(y), y>` (see do Carmo). For non-orthonormal vectors vectors, it is
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean <R(x, y)x, y>?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was from before me (sectional_curvature was already implemented but not curvature) but I think it is yes. I will unify the conventions if there are some incompatibilities.

@@ -453,6 +453,25 @@ def normal_basis(self, basis, base_point=None):

return gs.einsum("i, ikl->ikl", 1.0 / gs.sqrt(norms), basis)

def covariant_riemannian_tensor(self, base_point):
r"""Compute purely covariant version of Riemannian tensor at base_point.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at all the formulae, I'm not completely sure that they are all compatible, i.e. take the same conventions. The only way to avoid mistakes in my view, is to pick a reference, and to take all formulas from this reference. This reference should be added to all docstrings.

luisfpereira and others added 24 commits October 3, 2022 11:31
Remove upper version limit on torch
Fix expm due to changes in scipy.linalg.expm
Remove greetings GitHub workflow that often fails
Fix bug in the autodiff of pullback metric matrix
@ninamiolane
Copy link
Collaborator

@Jules-Deschamps @alebrigant I'm tackling this now

@review-notebook-app
Copy link

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@ninamiolane ninamiolane closed this Oct 6, 2022
ninamiolane added a commit that referenced this pull request Oct 12, 2022
Add curvature tensors - Follow-up from PR #1606
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants