From 951e23a37eb12eaa0e804c7d3d5b4e135c415691 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 6 Jul 2020 17:35:55 +0200 Subject: adding essential parts management in wasserstein distance --- src/python/gudhi/wasserstein/wasserstein.py | 146 ++++++++++++++++++++++++++-- 1 file changed, 138 insertions(+), 8 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index b37d30bb..283ecd9d 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -79,6 +79,9 @@ def _perstot(X, order, internal_p, enable_autodiff): transparent to automatic differentiation. :type enable_autodiff: bool :returns: float, the total persistence of the diagram (that is, its distance to the empty diagram). + + .. note:: + Can be +infty if the diagram has an essential part (points with infinite coordinates). ''' if enable_autodiff: import eagerpy as ep @@ -88,32 +91,136 @@ def _perstot(X, order, internal_p, enable_autodiff): return np.linalg.norm(_dist_to_diag(X, internal_p), ord=order) -def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enable_autodiff=False): +def _get_essential_parts(a): + ''' + :param a: (n x 2) numpy.array (point of a diagram) + :retuns: five lists of indices (between 0 and len(a)) accounting for the five types of points with infinite + coordinates that can occur in a diagram, namely: + type0 : (-inf, finite) + type1 : (finite, +inf) + type2 : (-inf, +inf) + type3 : (-inf, -inf) + type4 : (+inf, +inf) + .. note:: + For instance, a[_get_essential_parts(a)[0]] returns the points in a of coordinates (-inf, x) for some finite x. + ''' + if len(a): + ess_first_type = np.where(np.isfinite(a[:,1]) & (a[:,0] == -np.inf))[0] # coord (-inf, x) + ess_second_type = np.where(np.isfinite(a[:,0]) & (a[:,1] == np.inf))[0] # coord (x, +inf) + ess_third_type = np.where((a[:,0] == -np.inf) & (a[:,1] == np.inf))[0] # coord (-inf, +inf) + ess_fourth_type = np.where((a[:,0] == -np.inf) & (a[:,1] == -np.inf))[0] # coord (-inf, -inf) + ess_fifth_type = np.where((a[:,0] == np.inf) & (a[:,1] == np.inf))[0] # coord (+inf, +inf) + return ess_first_type, ess_second_type, ess_third_type, ess_fourth_type, ess_fifth_type + else: + return [], [], [], [], [] + + +def _cost_and_match_essential_parts(X, Y, idX, idY, order, axis): + ''' + :param X: (n x 2) numpy.array (dgm points) + :param Y: (n x 2) numpy.array (dgm points) + :param idX: indices to consider for this one dimensional OT problem (in X) + :param idY: indices to consider for this one dimensional OT problem (in Y) + :param order: exponent for Wasserstein distanc ecomputation + :param axis: must be 0 or 1, correspond to the coordinate which is finite. + :returns: cost (float) and match for points with *one* infinite coordinate. + + .. note:: + Assume idX, idY come when calling _handle_essential_parts, thus have same length. ''' - :param X: (n x 2) numpy.array encoding the (finite points of the) first diagram. Must not contain essential points - (i.e. with infinite coordinate). + u = X[idX, axis] + v = Y[idY, axis] + + cost = np.sum(np.abs(np.sort(u) - np.sort(v))**(order)) # OT cost in 1D + + sortidX = idX[np.argsort(u)] + sortidY = idY[np.argsort(v)] + # We return [i,j] sorted per value, and then [i, -1] (or [-1, j]) to account for essential points matched to the diagonal + match = list(zip(sortidX, sortidY)) + + return cost, match + + +def _handle_essential_parts(X, Y, order): + ''' + :param X: (n x 2) numpy array, first diagram. + :param Y: (n x 2) numpy array, second diagram. + :order: Wasserstein order for cost computation. + :returns: cost and matching due to essential parts. If cost is +inf, matching will be set to None. + ''' + c = 0 + m = [] + + ess_parts_X = _get_essential_parts(X) + ess_parts_Y = _get_essential_parts(Y) + + # Treats the case of infinite cost (cardinalities of essential parts differ). + for u, v in zip(ess_parts_X, ess_parts_Y): + if len(u) != len(v): + return np.inf, None + + # Now we know each essential part has the same number of points in both diagrams. + # Handle type 0 and type 1 essential parts (those with one finite coordinates) + c1, m1 = _cost_and_match_essential_parts(X, Y, ess_parts_X[0], ess_parts_Y[0], axis=1, order=order) + c2, m2 = _cost_and_match_essential_parts(X, Y, ess_parts_X[1], ess_parts_Y[1], axis=0, order=order) + + c += c1 + c2 + m += m1 + m2 + + # Handle type >= 2 (both coordinates are infinite, so we essentially just align points) + for u, v in zip(ess_parts_X[2:], ess_parts_Y[2:]): + m += list(zip(u, v)) # cost is 0 + + return c, np.array(m) + + +def _offdiag(X): + ''' + :param X: (n x 2) numpy array encoding a persistence diagram. + :returns: The off-diagonal part of a diagram `X` (points with finite coordinates). + ''' + return X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))] + + +def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enable_autodiff=False, + keep_essential_parts=True): + ''' + :param X: (n x 2) numpy.array encoding the first diagram. Can now contain essential parts (points with infinite + coordinates). :param Y: (m x 2) numpy.array encoding the second diagram. :param matching: if True, computes and returns the optimal matching between X and Y, encoded as a (n x 2) np.array [...[i,j]...], meaning the i-th point in X is matched to the j-th point in Y, with the convention (-1) represents the diagonal. + Note that if the cost is +inf (essential parts have different number of points, + then the optimal matching will be set to `None`. :param order: exponent for Wasserstein; Default value is 1. :param internal_p: Ground metric on the (upper-half) plane (i.e. norm L^p in R^2); - Default value is `np.inf`. + default value is `np.inf`. :param enable_autodiff: If X and Y are torch.tensor, tensorflow.Tensor or jax.numpy.ndarray, make the computation transparent to automatic differentiation. This requires the package EagerPy and is currently incompatible - with `matching=True`. + with `matching=True` and with `keep_essential_parts=True`. .. note:: This considers the function defined on the coordinates of the off-diagonal points of X and Y and lets the various frameworks compute its gradient. It never pulls new points from the diagonal. :type enable_autodiff: bool + :param keep_essential_parts: If False, only considers the off-diagonal points in the diagrams. + Otherwise, computes the distance between the essential parts separately. + :type keep_essential_parts: bool :returns: the Wasserstein distance of order q (1 <= q < infinity) between persistence diagrams with respect to the internal_p-norm as ground metric. If matching is set to True, also returns the optimal matching between X and Y. + If cost is +inf, any matching is optimal and thus it returns `None` instead. ''' + # Zeroth step: check compatibility of arguments + if keep_essential_parts and enable_autodiff: + import warnings + warnings.warn("enable_autodiff does not handle essential parts yet. These will be ignored in the following computations") + keep_essential_parts = False + + # First step: handle empty diagrams n = len(X) m = len(Y) - # handle empty diagrams if n == 0: if m == 0: if not matching: @@ -132,6 +239,25 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab else: return _perstot(X, order, internal_p, enable_autodiff), np.array([[i, -1] for i in range(n)]) + + # Second step: handle essential parts + if keep_essential_parts: + essential_cost, essential_matching = _handle_essential_parts(X, Y, order=order) + if (essential_cost == np.inf): + if matching: + return np.inf, None + else: + return np.inf # avoid computing off-diagonal transport cost if essential parts do not match (saves time) + + else: + essential_cost = 0 + essential_matching = None + + X, Y = _offdiag(X), _offdiag(Y) + n = len(X) + m = len(Y) + + # Now the standard pipeline for off-diagonal parts if enable_autodiff: import eagerpy as ep @@ -139,6 +265,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab Y_orig = ep.astensor(Y) X = X_orig.numpy() Y = Y_orig.numpy() + M = _build_dist_matrix(X, Y, order=order, internal_p=internal_p) a = np.ones(n+1) # weight vector of the input diagram. Uniform here. a[-1] = m @@ -154,7 +281,10 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab # Now we turn to -1 points encoding the diagonal match[:,0][match[:,0] >= n] = -1 match[:,1][match[:,1] >= m] = -1 - return ot_cost ** (1./order) , match + # Finally incorporate the essential part matching + if essential_matching is not None: + match = np.concatenate([match, essential_matching]) if essential_matching.size else match + return (ot_cost + essential_cost) ** (1./order) , match if enable_autodiff: P = ot.emd(a=a, b=b, M=M, numItermax=2000000) @@ -178,4 +308,4 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab # The default numItermax=100000 is not sufficient for some examples with 5000 points, what is a good value? ot_cost = ot.emd2(a, b, M, numItermax=2000000) - return ot_cost ** (1./order) + return (ot_cost + essential_cost) ** (1./order) -- cgit v1.2.3 From 91a9d77ed48847a8859e6bdd759390001910d411 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 6 Jul 2020 17:52:47 +0200 Subject: update doc (examples) with essential parts --- src/python/doc/wasserstein_distance_user.rst | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/wasserstein_distance_user.rst b/src/python/doc/wasserstein_distance_user.rst index 96ec7872..d747344b 100644 --- a/src/python/doc/wasserstein_distance_user.rst +++ b/src/python/doc/wasserstein_distance_user.rst @@ -44,7 +44,7 @@ Basic example ************* This example computes the 1-Wasserstein distance from 2 persistence diagrams with Euclidean ground metric. -Note that persistence diagrams must be submitted as (n x 2) numpy arrays and must not contain inf values. +Note that persistence diagrams must be submitted as (n x 2) numpy arrays. .. testcode:: @@ -67,14 +67,16 @@ We can also have access to the optimal matching by letting `matching=True`. It is encoded as a list of indices (i,j), meaning that the i-th point in X is mapped to the j-th point in Y. An index of -1 represents the diagonal. +It handles essential parts (points with infinite coordinates). However if the cardinalities of the essential parts differ, +any matching has a cost +inf and thus can be considered to be optimal. In such a case, the function returns `(np.inf, None)`. .. testcode:: import gudhi.wasserstein import numpy as np - dgm1 = np.array([[2.7, 3.7],[9.6, 14.],[34.2, 34.974]]) - dgm2 = np.array([[2.8, 4.45], [5, 6], [9.5, 14.1]]) + dgm1 = np.array([[2.7, 3.7],[9.6, 14.],[34.2, 34.974], [3, np.inf]]) + dgm2 = np.array([[2.8, 4.45], [5, 6], [9.5, 14.1], [4, np.inf]]) cost, matchings = gudhi.wasserstein.wasserstein_distance(dgm1, dgm2, matching=True, order=1, internal_p=2) message_cost = "Wasserstein distance value = %.2f" %cost @@ -90,16 +92,30 @@ An index of -1 represents the diagonal. for j in dgm2_to_diagonal: print("point %s in dgm2 is matched to the diagonal" %j) + dgm3 = np.array([[1, 2], [0, np.inf]]) + dgm4 = np.array([[1, 2], [0, np.inf], [1, np.inf]]) + cost, matchings = gudhi.wasserstein.wasserstein_distance(dgm3, dgm4, matching=True, order=1, internal_p=2) + print("\nSecond example:") + print("cost:", cost) + print("matchings:", matchings) + + The output is: .. testoutput:: - Wasserstein distance value = 2.15 + Wasserstein distance value = 3.15 point 0 in dgm1 is matched to point 0 in dgm2 point 1 in dgm1 is matched to point 2 in dgm2 + point 3 in dgm1 is matched to point 3 in dgm2 point 2 in dgm1 is matched to the diagonal point 1 in dgm2 is matched to the diagonal + Second example: + cost: inf + matchings: None + + Barycenters ----------- -- cgit v1.2.3 From fe3e6a3a47828841ba3cb4a0721e5d8c16ab126f Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 6 Jul 2020 18:27:52 +0200 Subject: update test including essential parts --- src/python/gudhi/wasserstein/wasserstein.py | 18 +++++-- src/python/test/test_wasserstein_distance.py | 72 +++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 12 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 283ecd9d..2a1dee7a 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -105,10 +105,10 @@ def _get_essential_parts(a): For instance, a[_get_essential_parts(a)[0]] returns the points in a of coordinates (-inf, x) for some finite x. ''' if len(a): - ess_first_type = np.where(np.isfinite(a[:,1]) & (a[:,0] == -np.inf))[0] # coord (-inf, x) + ess_first_type = np.where(np.isfinite(a[:,1]) & (a[:,0] == -np.inf))[0] # coord (-inf, x) ess_second_type = np.where(np.isfinite(a[:,0]) & (a[:,1] == np.inf))[0] # coord (x, +inf) - ess_third_type = np.where((a[:,0] == -np.inf) & (a[:,1] == np.inf))[0] # coord (-inf, +inf) - ess_fourth_type = np.where((a[:,0] == -np.inf) & (a[:,1] == -np.inf))[0] # coord (-inf, -inf) + ess_third_type = np.where((a[:,0] == -np.inf) & (a[:,1] == np.inf))[0] # coord (-inf, +inf) + ess_fourth_type = np.where((a[:,0] == -np.inf) & (a[:,1] == -np.inf))[0] # coord (-inf, -inf) ess_fifth_type = np.where((a[:,0] == np.inf) & (a[:,1] == np.inf))[0] # coord (+inf, +inf) return ess_first_type, ess_second_type, ess_third_type, ess_fourth_type, ess_fifth_type else: @@ -232,12 +232,20 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab if not matching: return _perstot(Y, order, internal_p, enable_autodiff) else: - return _perstot(Y, order, internal_p, enable_autodiff), np.array([[-1, j] for j in range(m)]) + cost = _perstot(Y, order, internal_p, enable_autodiff) + if cost == np.inf: # We had some essential part here. + return cost, None + else: + return cost, np.array([[-1, j] for j in range(m)]) elif m == 0: if not matching: return _perstot(X, order, internal_p, enable_autodiff) else: - return _perstot(X, order, internal_p, enable_autodiff), np.array([[i, -1] for i in range(n)]) + cost = _perstot(X, order, internal_p, enable_autodiff) + if cost == np.inf: + return cost, None + else: + return np.array([[i, -1] for i in range(n)]) # Second step: handle essential parts diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index 90d26809..24be228b 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -5,10 +5,11 @@ Copyright (C) 2019 Inria Modification(s): + - 2020/07 Théo Lacombe: Added tests about handling essential parts in diagrams. - YYYY/MM Author: Description of the modification """ -from gudhi.wasserstein.wasserstein import _proj_on_diag +from gudhi.wasserstein.wasserstein import _proj_on_diag, _offdiag, _handle_essential_parts from gudhi.wasserstein import wasserstein_distance as pot from gudhi.hera import wasserstein_distance as hera import numpy as np @@ -18,12 +19,62 @@ __author__ = "Theo Lacombe" __copyright__ = "Copyright (C) 2019 Inria" __license__ = "MIT" + def test_proj_on_diag(): dgm = np.array([[1., 1.], [1., 2.], [3., 5.]]) assert np.array_equal(_proj_on_diag(dgm), [[1., 1.], [1.5, 1.5], [4., 4.]]) empty = np.empty((0, 2)) assert np.array_equal(_proj_on_diag(empty), empty) + +def test_offdiag(): + diag = np.array([[0, 1], [3, 5], [2, np.inf], [3, np.inf], [-np.inf, 8], [-np.inf, 12], [-np.inf, -np.inf], + [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) + assert np.array_equal(_offdiag(diag), [[0, 1], [3, 5]]) + + +def test_handle_essential_parts(): + diag1 = np.array([[0, 1], [3, 5], + [2, np.inf], [3, np.inf], + [-np.inf, 8], [-np.inf, 12], + [-np.inf, -np.inf], + [np.inf, np.inf], + [-np.inf, np.inf], [-np.inf, np.inf]]) + + diag2 = np.array([[0, 2], [3, 5], + [2, np.inf], [4, np.inf], + [-np.inf, 8], [-np.inf, 11], + [-np.inf, -np.inf], + [np.inf, np.inf], + [-np.inf, np.inf], [-np.inf, np.inf]]) + + diag3 = np.array([[0, 2], [3, 5], + [2, np.inf], [4, np.inf], + [-np.inf, 8], [-np.inf, 11], + [-np.inf, -np.inf], [-np.inf, -np.inf], + [np.inf, np.inf], + [-np.inf, np.inf], [-np.inf, np.inf]]) + + c, m = _handle_essential_parts(diag1, diag2, matching=True, order=1) + assert c == pytest.approx(3, 0.0001) + assert np.array_equal(m, [[0,0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8], [9, 9]]) + c, m = _handle_essential_parts(diag1, diag3, matching=True, order=1) + assert c == np.inf + assert (m is None) + + +def test_get_essential_parts(): + diag = np.array([[0, 1], [3, 5], [2, np.inf], [3, np.inf], [-np.inf, 8], [-np.inf, 12], [-np.inf, -np.inf], + [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) + + res = _get_essential_parts(diag) + assert res[0] = [4, 5] + assert res[1] = [2, 3] + assert res[2] = [8, 9] + assert res[3] = [6] + assert res[4] = [7] + + def _basic_wasserstein(wasserstein_distance, delta, test_infinity=True, test_matching=True): diag1 = np.array([[2.7, 3.7], [9.6, 14.0], [34.2, 34.974]]) diag2 = np.array([[2.8, 4.45], [9.5, 14.1]]) @@ -64,7 +115,7 @@ def _basic_wasserstein(wasserstein_distance, delta, test_infinity=True, test_mat assert wasserstein_distance(diag4, diag5) == np.inf assert wasserstein_distance(diag5, diag6, order=1, internal_p=np.inf) == approx(4.) - + assert wasserstein_distance(diag5, emptydiag) == np.inf if test_matching: match = wasserstein_distance(emptydiag, emptydiag, matching=True, internal_p=1., order=2)[1] @@ -78,6 +129,13 @@ def _basic_wasserstein(wasserstein_distance, delta, test_infinity=True, test_mat match = wasserstein_distance(diag1, diag2, matching=True, internal_p=2., order=2.)[1] assert np.array_equal(match, [[0, 0], [1, 1], [2, -1]]) + if test_matching and test_infinity: + diag7 = np.array([[0, 3], [4, np.inf], [5, np.inf]]) + + match = wasserstein_distance(diag5, diag6, matching=True, internal_p=2., order=2.)[1] + assert np.array_equal(match, [[0, -1], [-1,0], [-1, 1], [1, 2]]) + match = wasserstein_distance(diag5, diag7, matching=True, internal_p=2., order=2.)[1] + assert (match is None) def hera_wrap(**extra): @@ -92,7 +150,7 @@ def pot_wrap(**extra): def test_wasserstein_distance_pot(): _basic_wasserstein(pot, 1e-15, test_infinity=False, test_matching=True) - _basic_wasserstein(pot_wrap(enable_autodiff=True), 1e-15, test_infinity=False, test_matching=False) + _basic_wasserstein(pot_wrap(enable_autodiff=True, keep_essential_parts=False), 1e-15, test_infinity=False, test_matching=False) def test_wasserstein_distance_hera(): _basic_wasserstein(hera_wrap(delta=1e-12), 1e-12, test_matching=False) @@ -105,19 +163,19 @@ def test_wasserstein_distance_grad(): diag2 = torch.tensor([[2.8, 4.45], [9.5, 14.1]], requires_grad=True) diag3 = torch.tensor([[2.8, 4.45], [9.5, 14.1]], requires_grad=True) assert diag1.grad is None and diag2.grad is None and diag3.grad is None - dist12 = pot(diag1, diag2, internal_p=2, order=2, enable_autodiff=True) - dist30 = pot(diag3, torch.tensor([]), internal_p=2, order=2, enable_autodiff=True) + dist12 = pot(diag1, diag2, internal_p=2, order=2, enable_autodiff=True, keep_essential_parts=False) + dist30 = pot(diag3, torch.tensor([]), internal_p=2, order=2, enable_autodiff=True, keep_essential_parts=False) dist12.backward() dist30.backward() assert not torch.isnan(diag1.grad).any() and not torch.isnan(diag2.grad).any() and not torch.isnan(diag3.grad).any() diag4 = torch.tensor([[0., 10.]], requires_grad=True) diag5 = torch.tensor([[1., 11.], [3., 4.]], requires_grad=True) - dist45 = pot(diag4, diag5, internal_p=1, order=1, enable_autodiff=True) + dist45 = pot(diag4, diag5, internal_p=1, order=1, enable_autodiff=True, keep_essential_parts=False) assert dist45 == 3. dist45.backward() assert np.array_equal(diag4.grad, [[-1., -1.]]) assert np.array_equal(diag5.grad, [[1., 1.], [-1., 1.]]) diag6 = torch.tensor([[5., 10.]], requires_grad=True) - pot(diag6, diag6, internal_p=2, order=2, enable_autodiff=True).backward() + pot(diag6, diag6, internal_p=2, order=2, enable_autodiff=True, keep_essential_parts=False).backward() # https://github.com/jonasrauber/eagerpy/issues/6 # assert np.array_equal(diag6.grad, [[0., 0.]]) -- cgit v1.2.3 From e0eba14109e02676825f8c24563872a5b49c6120 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Tue, 7 Jul 2020 11:52:35 +0200 Subject: correction typo in test wdist --- src/python/gudhi/wasserstein/wasserstein.py | 2 +- src/python/test/test_wasserstein_distance.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 2a1dee7a..009c1bf7 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -245,7 +245,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab if cost == np.inf: return cost, None else: - return np.array([[i, -1] for i in range(n)]) + return cost, np.array([[i, -1] for i in range(n)]) # Second step: handle essential parts diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index 24be228b..e50091e9 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -55,10 +55,10 @@ def test_handle_essential_parts(): [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) - c, m = _handle_essential_parts(diag1, diag2, matching=True, order=1) + c, m = _handle_essential_parts(diag1, diag2, order=1) assert c == pytest.approx(3, 0.0001) assert np.array_equal(m, [[0,0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8], [9, 9]]) - c, m = _handle_essential_parts(diag1, diag3, matching=True, order=1) + c, m = _handle_essential_parts(diag1, diag3, order=1) assert c == np.inf assert (m is None) @@ -68,11 +68,11 @@ def test_get_essential_parts(): [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) res = _get_essential_parts(diag) - assert res[0] = [4, 5] - assert res[1] = [2, 3] - assert res[2] = [8, 9] - assert res[3] = [6] - assert res[4] = [7] + assert res[0] == [4, 5] + assert res[1] == [2, 3] + assert res[2] == [8, 9] + assert res[3] == [6] + assert res[4] == [7] def _basic_wasserstein(wasserstein_distance, delta, test_infinity=True, test_matching=True): -- cgit v1.2.3 From 42a399c273fde7c76ec23d2993957fcbb492ee79 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Tue, 7 Jul 2020 12:37:51 +0200 Subject: correction mistake in tests --- src/python/gudhi/wasserstein/wasserstein.py | 4 ++-- src/python/test/test_wasserstein_distance.py | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 009c1bf7..981bbf08 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -214,7 +214,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab # Zeroth step: check compatibility of arguments if keep_essential_parts and enable_autodiff: import warnings - warnings.warn("enable_autodiff does not handle essential parts yet. These will be ignored in the following computations") + warnings.warn("enable_autodiff does not handle essential parts yet. keep_essential_parts set to False.") keep_essential_parts = False # First step: handle empty diagrams @@ -256,11 +256,11 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab return np.inf, None else: return np.inf # avoid computing off-diagonal transport cost if essential parts do not match (saves time) - else: essential_cost = 0 essential_matching = None + # Extract off-diaognal points of the diagrams. X, Y = _offdiag(X), _offdiag(Y) n = len(X) m = len(Y) diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index e50091e9..285b95c9 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -9,12 +9,13 @@ - YYYY/MM Author: Description of the modification """ -from gudhi.wasserstein.wasserstein import _proj_on_diag, _offdiag, _handle_essential_parts +from gudhi.wasserstein.wasserstein import _proj_on_diag, _offdiag, _handle_essential_parts, _get_essential_parts from gudhi.wasserstein import wasserstein_distance as pot from gudhi.hera import wasserstein_distance as hera import numpy as np import pytest + __author__ = "Theo Lacombe" __copyright__ = "Copyright (C) 2019 Inria" __license__ = "MIT" @@ -56,8 +57,10 @@ def test_handle_essential_parts(): [-np.inf, np.inf], [-np.inf, np.inf]]) c, m = _handle_essential_parts(diag1, diag2, order=1) - assert c == pytest.approx(3, 0.0001) - assert np.array_equal(m, [[0,0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8], [9, 9]]) + assert c == pytest.approx(2, 0.0001) # Note: here c is only the cost due to essential part (thus 2, not 3) + # Similarly, the matching only corresponds to essential parts. + assert np.array_equal(m, [[4, 4], [5, 5], [2, 2], [3, 3], [8, 8], [9, 9], [6, 6], [7, 7]]) + c, m = _handle_essential_parts(diag1, diag3, order=1) assert c == np.inf assert (m is None) @@ -68,11 +71,11 @@ def test_get_essential_parts(): [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) res = _get_essential_parts(diag) - assert res[0] == [4, 5] - assert res[1] == [2, 3] - assert res[2] == [8, 9] - assert res[3] == [6] - assert res[4] == [7] + assert np.array_equal(res[0], [4, 5]) + assert np.array_equal(res[1], [2, 3]) + assert np.array_equal(res[2], [8, 9]) + assert np.array_equal(res[3], [6] ) + assert np.array_equal(res[4], [7] ) def _basic_wasserstein(wasserstein_distance, delta, test_infinity=True, test_matching=True): -- cgit v1.2.3 From 107f8e6668509f5fd36e179f9a538b460d3941a9 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Tue, 7 Jul 2020 18:15:17 +0200 Subject: added enable autodiff management in _offdiag utils function --- src/python/gudhi/wasserstein/wasserstein.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 981bbf08..495142c4 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -70,6 +70,7 @@ def _perstot_autodiff(X, order, internal_p): ''' return _dist_to_diag(X, internal_p).norms.lp(order) + def _perstot(X, order, internal_p, enable_autodiff): ''' :param X: (n x 2) numpy.array (points of a given diagram). @@ -174,12 +175,18 @@ def _handle_essential_parts(X, Y, order): return c, np.array(m) -def _offdiag(X): +def _offdiag(X, enable_autodiff): ''' :param X: (n x 2) numpy array encoding a persistence diagram. :returns: The off-diagonal part of a diagram `X` (points with finite coordinates). ''' - return X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))] + if enable_autodiff: + import eagerpy as ep + + return ep.astensor(X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))]) + + else: + return X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))] def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enable_autodiff=False, @@ -261,7 +268,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab essential_matching = None # Extract off-diaognal points of the diagrams. - X, Y = _offdiag(X), _offdiag(Y) + X, Y = _offdiag(X, enable_autodiff), _offdiag(Y, enable_autodiff) n = len(X) m = len(Y) -- cgit v1.2.3 From e94892f972357283e70c7534f84662dfaa21cc3e Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 20 Jul 2020 11:41:13 +0200 Subject: update test enable_autodiff and _offdiag --- src/python/gudhi/wasserstein/wasserstein.py | 16 ++++++---------- src/python/test/test_wasserstein_distance.py | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 495142c4..142385b1 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -178,13 +178,13 @@ def _handle_essential_parts(X, Y, order): def _offdiag(X, enable_autodiff): ''' :param X: (n x 2) numpy array encoding a persistence diagram. + :param enable_autodiff: boolean, to handle the case where X is a eagerpy tensor. :returns: The off-diagonal part of a diagram `X` (points with finite coordinates). ''' if enable_autodiff: - import eagerpy as ep - - return ep.astensor(X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))]) - + # Assumes the diagrams only have finite coordinates. Thus, return X directly. + # TODO improve this to get rid of essential parts if there are any. + return X else: return X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))] @@ -218,11 +218,6 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab If matching is set to True, also returns the optimal matching between X and Y. If cost is +inf, any matching is optimal and thus it returns `None` instead. ''' - # Zeroth step: check compatibility of arguments - if keep_essential_parts and enable_autodiff: - import warnings - warnings.warn("enable_autodiff does not handle essential parts yet. keep_essential_parts set to False.") - keep_essential_parts = False # First step: handle empty diagrams n = len(X) @@ -267,7 +262,8 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab essential_cost = 0 essential_matching = None - # Extract off-diaognal points of the diagrams. + # Extract off-diaognal points of the diagrams. Note that if enable_autodiff is True, nothing is done here (X,Y are + # assumed to be tensors with only finite coordinates). X, Y = _offdiag(X, enable_autodiff), _offdiag(Y, enable_autodiff) n = len(X) m = len(Y) diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index 285b95c9..6701c7ba 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -31,7 +31,7 @@ def test_proj_on_diag(): def test_offdiag(): diag = np.array([[0, 1], [3, 5], [2, np.inf], [3, np.inf], [-np.inf, 8], [-np.inf, 12], [-np.inf, -np.inf], [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) - assert np.array_equal(_offdiag(diag), [[0, 1], [3, 5]]) + assert np.array_equal(_offdiag(diag, enable_autodiff=False), [[0, 1], [3, 5]]) def test_handle_essential_parts(): -- cgit v1.2.3 From 89bb3d11064de40f2b4fda958aa2e2e8cfa5b489 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 12 Apr 2021 10:45:32 +0200 Subject: change name _offdiag to _finite_part --- src/python/gudhi/wasserstein/wasserstein.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 572d4249..d64d433e 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -175,11 +175,11 @@ def _handle_essential_parts(X, Y, order): return c, np.array(m) -def _offdiag(X, enable_autodiff): +def _finite_part(X, enable_autodiff): ''' :param X: (n x 2) numpy array encoding a persistence diagram. :param enable_autodiff: boolean, to handle the case where X is a eagerpy tensor. - :returns: The off-diagonal part of a diagram `X` (points with finite coordinates). + :returns: The finite part of a diagram `X` (points with finite coordinates). ''' if enable_autodiff: # Assumes the diagrams only have finite coordinates. Thus, return X directly. @@ -262,13 +262,13 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab essential_cost = 0 essential_matching = None - # Extract off-diaognal points of the diagrams. Note that if enable_autodiff is True, nothing is done here (X,Y are + # Extract finite points of the diagrams. Note that if enable_autodiff is True, nothing is done here (X,Y are # assumed to be tensors with only finite coordinates). - X, Y = _offdiag(X, enable_autodiff), _offdiag(Y, enable_autodiff) + X, Y = _finite_part(X, enable_autodiff), _finite_part(Y, enable_autodiff) n = len(X) m = len(Y) - # Now the standard pipeline for off-diagonal parts + # Now the standard pipeline for finite parts if enable_autodiff: import eagerpy as ep -- cgit v1.2.3 From 01bd9eef85b0d93eb1629f1a0c5a28a359e4e7b9 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 12 Apr 2021 10:47:18 +0200 Subject: change name _offdiag to _finite_part in test file --- src/python/test/test_wasserstein_distance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index 6701c7ba..12bf71df 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -9,7 +9,7 @@ - YYYY/MM Author: Description of the modification """ -from gudhi.wasserstein.wasserstein import _proj_on_diag, _offdiag, _handle_essential_parts, _get_essential_parts +from gudhi.wasserstein.wasserstein import _proj_on_diag, _finite_part, _handle_essential_parts, _get_essential_parts from gudhi.wasserstein import wasserstein_distance as pot from gudhi.hera import wasserstein_distance as hera import numpy as np @@ -28,10 +28,10 @@ def test_proj_on_diag(): assert np.array_equal(_proj_on_diag(empty), empty) -def test_offdiag(): +def test_finite_part(): diag = np.array([[0, 1], [3, 5], [2, np.inf], [3, np.inf], [-np.inf, 8], [-np.inf, 12], [-np.inf, -np.inf], [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) - assert np.array_equal(_offdiag(diag, enable_autodiff=False), [[0, 1], [3, 5]]) + assert np.array_equal(_finite_part(diag, enable_autodiff=False), [[0, 1], [3, 5]]) def test_handle_essential_parts(): -- cgit v1.2.3 From 777522b82bde16b55f15c21471bad06038849fd1 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 12 Apr 2021 15:52:36 +0200 Subject: improved essential part and enable autodiff management --- src/python/gudhi/wasserstein/wasserstein.py | 75 ++++++++++++++++------------- 1 file changed, 41 insertions(+), 34 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index d64d433e..2911f826 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -95,7 +95,7 @@ def _perstot(X, order, internal_p, enable_autodiff): def _get_essential_parts(a): ''' :param a: (n x 2) numpy.array (point of a diagram) - :retuns: five lists of indices (between 0 and len(a)) accounting for the five types of points with infinite + :returns: five lists of indices (between 0 and len(a)) accounting for the five types of points with infinite coordinates that can occur in a diagram, namely: type0 : (-inf, finite) type1 : (finite, +inf) @@ -104,13 +104,20 @@ def _get_essential_parts(a): type4 : (+inf, +inf) .. note:: For instance, a[_get_essential_parts(a)[0]] returns the points in a of coordinates (-inf, x) for some finite x. + Note also that points with (+inf, -inf) are not handled (points (x,y) in dgm satisfy by assumption (y >= x)). ''' if len(a): - ess_first_type = np.where(np.isfinite(a[:,1]) & (a[:,0] == -np.inf))[0] # coord (-inf, x) - ess_second_type = np.where(np.isfinite(a[:,0]) & (a[:,1] == np.inf))[0] # coord (x, +inf) - ess_third_type = np.where((a[:,0] == -np.inf) & (a[:,1] == np.inf))[0] # coord (-inf, +inf) - ess_fourth_type = np.where((a[:,0] == -np.inf) & (a[:,1] == -np.inf))[0] # coord (-inf, -inf) - ess_fifth_type = np.where((a[:,0] == np.inf) & (a[:,1] == np.inf))[0] # coord (+inf, +inf) + first_coord_finite = np.isfinite(a[:,0]) + second_coord_finite = np.isfinite(a[:,1]) + first_coord_infinite_positive = (a[:,0] == np.inf) + second_coord_infinite_positive = (a[:,1] == np.inf) + first_coord_infinite_negative = (a[:,0] == -np.inf) + second_coord_infinite_negative = (a[:,1] == -np.inf) + ess_first_type = np.where(second_coord_finite & first_coord_infinite_negative)[0] # coord (-inf, x) + ess_second_type = np.where(first_coord_finite & second_coord_infinite_positive)[0] # coord (x, +inf) + ess_third_type = np.where(first_coord_infinite_negative & second_coord_infinite_positive)[0] # coord (-inf, +inf) + ess_fourth_type = np.where(first_coord_infinite_negative & second_coord_infinite_negative)[0] # coord (-inf, -inf) + ess_fifth_type = np.where(first_coord_infinite_positive & second_coord_infinite_positive)[0] # coord (+inf, +inf) return ess_first_type, ess_second_type, ess_third_type, ess_fourth_type, ess_fifth_type else: return [], [], [], [], [] @@ -136,7 +143,7 @@ def _cost_and_match_essential_parts(X, Y, idX, idY, order, axis): sortidX = idX[np.argsort(u)] sortidY = idY[np.argsort(v)] - # We return [i,j] sorted per value, and then [i, -1] (or [-1, j]) to account for essential points matched to the diagonal + # We return [i,j] sorted per value match = list(zip(sortidX, sortidY)) return cost, match @@ -149,9 +156,6 @@ def _handle_essential_parts(X, Y, order): :order: Wasserstein order for cost computation. :returns: cost and matching due to essential parts. If cost is +inf, matching will be set to None. ''' - c = 0 - m = [] - ess_parts_X = _get_essential_parts(X) ess_parts_Y = _get_essential_parts(Y) @@ -165,8 +169,8 @@ def _handle_essential_parts(X, Y, order): c1, m1 = _cost_and_match_essential_parts(X, Y, ess_parts_X[0], ess_parts_Y[0], axis=1, order=order) c2, m2 = _cost_and_match_essential_parts(X, Y, ess_parts_X[1], ess_parts_Y[1], axis=0, order=order) - c += c1 + c2 - m += m1 + m2 + c = c1 + c2 + m = m1 + m2 # Handle type >= 2 (both coordinates are infinite, so we essentially just align points) for u, v in zip(ess_parts_X[2:], ess_parts_Y[2:]): @@ -175,24 +179,18 @@ def _handle_essential_parts(X, Y, order): return c, np.array(m) -def _finite_part(X, enable_autodiff): +def _finite_part(X): ''' :param X: (n x 2) numpy array encoding a persistence diagram. - :param enable_autodiff: boolean, to handle the case where X is a eagerpy tensor. :returns: The finite part of a diagram `X` (points with finite coordinates). ''' - if enable_autodiff: - # Assumes the diagrams only have finite coordinates. Thus, return X directly. - # TODO improve this to get rid of essential parts if there are any. - return X - else: - return X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))] + return X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))] def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enable_autodiff=False, keep_essential_parts=True): ''' - :param X: (n x 2) numpy.array encoding the first diagram. Can now contain essential parts (points with infinite + :param X: (n x 2) numpy.array encoding the first diagram. Can contain essential parts (points with infinite coordinates). :param Y: (m x 2) numpy.array encoding the second diagram. :param matching: if True, computes and returns the optimal matching between X and Y, encoded as @@ -200,17 +198,17 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab the j-th point in Y, with the convention (-1) represents the diagonal. Note that if the cost is +inf (essential parts have different number of points, then the optimal matching will be set to `None`. - :param order: exponent for Wasserstein; Default value is 1. - :param internal_p: Ground metric on the (upper-half) plane (i.e. norm L^p in R^2); + :param order: exponent for Wasserstein. Default value is 1. + :param internal_p: Ground metric on the (upper-half) plane (i.e. norm L^p in R^2). Default value is `np.inf`. :param enable_autodiff: If X and Y are torch.tensor or tensorflow.Tensor, make the computation transparent to automatic differentiation. This requires the package EagerPy and is currently incompatible with `matching=True` and with `keep_essential_parts=True`. - .. note:: This considers the function defined on the coordinates of the off-diagonal points of X and Y + .. note:: This considers the function defined on the coordinates of the off-diagonal finite points of X and Y and lets the various frameworks compute its gradient. It never pulls new points from the diagonal. :type enable_autodiff: bool - :param keep_essential_parts: If False, only considers the off-diagonal points in the diagrams. + :param keep_essential_parts: If False, only considers the finite points in the diagrams. Otherwise, computes the distance between the essential parts separately. :type keep_essential_parts: bool :returns: the Wasserstein distance of order q (1 <= q < infinity) between persistence diagrams with @@ -235,7 +233,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab return _perstot(Y, order, internal_p, enable_autodiff) else: cost = _perstot(Y, order, internal_p, enable_autodiff) - if cost == np.inf: # We had some essential part here. + if cost == np.inf: # We had some essential part in Y. return cost, None else: return cost, np.array([[-1, j] for j in range(m)]) @@ -250,24 +248,28 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab return cost, np.array([[i, -1] for i in range(n)]) - # Second step: handle essential parts + # Check essential part and enable autodiff together + if enable_autodiff and keep_essential_parts: + import warnings # should it be done at the top of the file? + warnings.warn('''enable_autodiff=True and keep_essential_parts=True are incompatible together. + keep_essential_parts is set to False: only points with finite coordiantes are considered + in the following. + ''') + keep_essential_parts = False + + # Second step: handle essential parts if needed. if keep_essential_parts: essential_cost, essential_matching = _handle_essential_parts(X, Y, order=order) if (essential_cost == np.inf): if matching: return np.inf, None else: - return np.inf # avoid computing off-diagonal transport cost if essential parts do not match (saves time) + return np.inf # avoid computing transport cost between the finite parts if essential parts + # cardinalities do not match (saves time) else: essential_cost = 0 essential_matching = None - # Extract finite points of the diagrams. Note that if enable_autodiff is True, nothing is done here (X,Y are - # assumed to be tensors with only finite coordinates). - X, Y = _finite_part(X, enable_autodiff), _finite_part(Y, enable_autodiff) - n = len(X) - m = len(Y) - # Now the standard pipeline for finite parts if enable_autodiff: import eagerpy as ep @@ -277,6 +279,11 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab X = X_orig.numpy() Y = Y_orig.numpy() + # Extract finite points of the diagrams. + X, Y = _finite_part(X), _finite_part(Y) + n = len(X) + m = len(Y) + M = _build_dist_matrix(X, Y, order=order, internal_p=internal_p) a = np.ones(n+1) # weight vector of the input diagram. Uniform here. a[-1] = m -- cgit v1.2.3 From 2a11e3651c2d66df8371a9aa1d23dff69ffbc31c Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 12 Apr 2021 15:54:26 +0200 Subject: removed test_wasserstein_distance_grad to be consistent with master --- src/python/test/test_wasserstein_distance.py | 23 ----------------------- 1 file changed, 23 deletions(-) (limited to 'src/python') diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index 12bf71df..14d5c2ca 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -159,26 +159,3 @@ def test_wasserstein_distance_hera(): _basic_wasserstein(hera_wrap(delta=1e-12), 1e-12, test_matching=False) _basic_wasserstein(hera_wrap(delta=.1), .1, test_matching=False) -def test_wasserstein_distance_grad(): - import torch - - diag1 = torch.tensor([[2.7, 3.7], [9.6, 14.0], [34.2, 34.974]], requires_grad=True) - diag2 = torch.tensor([[2.8, 4.45], [9.5, 14.1]], requires_grad=True) - diag3 = torch.tensor([[2.8, 4.45], [9.5, 14.1]], requires_grad=True) - assert diag1.grad is None and diag2.grad is None and diag3.grad is None - dist12 = pot(diag1, diag2, internal_p=2, order=2, enable_autodiff=True, keep_essential_parts=False) - dist30 = pot(diag3, torch.tensor([]), internal_p=2, order=2, enable_autodiff=True, keep_essential_parts=False) - dist12.backward() - dist30.backward() - assert not torch.isnan(diag1.grad).any() and not torch.isnan(diag2.grad).any() and not torch.isnan(diag3.grad).any() - diag4 = torch.tensor([[0., 10.]], requires_grad=True) - diag5 = torch.tensor([[1., 11.], [3., 4.]], requires_grad=True) - dist45 = pot(diag4, diag5, internal_p=1, order=1, enable_autodiff=True, keep_essential_parts=False) - assert dist45 == 3. - dist45.backward() - assert np.array_equal(diag4.grad, [[-1., -1.]]) - assert np.array_equal(diag5.grad, [[1., 1.], [-1., 1.]]) - diag6 = torch.tensor([[5., 10.]], requires_grad=True) - pot(diag6, diag6, internal_p=2, order=2, enable_autodiff=True, keep_essential_parts=False).backward() - # https://github.com/jonasrauber/eagerpy/issues/6 - # assert np.array_equal(diag6.grad, [[0., 0.]]) -- cgit v1.2.3 From cdab3c9e32923f83d25d2cdf207f3cddbb3f94f6 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 12 Apr 2021 17:02:34 +0200 Subject: handle essential parts test --- src/python/gudhi/wasserstein/wasserstein.py | 1 + src/python/test/test_wasserstein_distance.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 2911f826..7cb9d5d9 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -113,6 +113,7 @@ def _get_essential_parts(a): second_coord_infinite_positive = (a[:,1] == np.inf) first_coord_infinite_negative = (a[:,0] == -np.inf) second_coord_infinite_negative = (a[:,1] == -np.inf) + ess_first_type = np.where(second_coord_finite & first_coord_infinite_negative)[0] # coord (-inf, x) ess_second_type = np.where(first_coord_finite & second_coord_infinite_positive)[0] # coord (x, +inf) ess_third_type = np.where(first_coord_infinite_negative & second_coord_infinite_positive)[0] # coord (-inf, +inf) diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index 14d5c2ca..df7acc91 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -31,7 +31,7 @@ def test_proj_on_diag(): def test_finite_part(): diag = np.array([[0, 1], [3, 5], [2, np.inf], [3, np.inf], [-np.inf, 8], [-np.inf, 12], [-np.inf, -np.inf], [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) - assert np.array_equal(_finite_part(diag, enable_autodiff=False), [[0, 1], [3, 5]]) + assert np.array_equal(_finite_part(diag), [[0, 1], [3, 5]]) def test_handle_essential_parts(): -- cgit v1.2.3 From bb0792ed7bfe9d718be3e8039e8fb89af6d160e5 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Mon, 12 Apr 2021 19:48:57 +0200 Subject: added warning when cost is infty and matching is None --- src/python/doc/wasserstein_distance_user.rst | 4 +-- src/python/gudhi/wasserstein/wasserstein.py | 44 ++++++++++++++++++---------- 2 files changed, 30 insertions(+), 18 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/wasserstein_distance_user.rst b/src/python/doc/wasserstein_distance_user.rst index b3d17495..091c9fd9 100644 --- a/src/python/doc/wasserstein_distance_user.rst +++ b/src/python/doc/wasserstein_distance_user.rst @@ -100,7 +100,7 @@ any matching has a cost +inf and thus can be considered to be optimal. In such a print("matchings:", matchings) -The output is: +The output is: .. testoutput:: @@ -197,4 +197,4 @@ Tutorial This `notebook `_ -presents the concept of barycenter, or Fréchet mean, of a family of persistence diagrams. \ No newline at end of file +presents the concept of barycenter, or Fréchet mean, of a family of persistence diagrams. diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 7cb9d5d9..8ccbe12e 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -9,6 +9,7 @@ import numpy as np import scipy.spatial.distance as sc +import warnings try: import ot @@ -188,6 +189,20 @@ def _finite_part(X): return X[np.where(np.isfinite(X[:,0]) & np.isfinite(X[:,1]))] +def _warn_infty(matching): + ''' + Handle essential parts with different cardinalities. Warn the user about cost being infinite and (if + `matching=True`) about the returned matching being `None`. + ''' + if matching: + warnings.warn('Cardinality of essential parts differs. Distance (cost) is +infty, and the returned matching is None.') + return np.inf, None + else: + warnings.warn('Cardinality of essential parts diffes. Distance (cost) is +infty.') + return np.inf + + + def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enable_autodiff=False, keep_essential_parts=True): ''' @@ -230,28 +245,27 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab else: return 0., np.array([]) else: - if not matching: - return _perstot(Y, order, internal_p, enable_autodiff) + cost = _perstot(Y, order, internal_p, enable_autodiff) + if cost == np.inf: + return _warn_infty(matching) else: - cost = _perstot(Y, order, internal_p, enable_autodiff) - if cost == np.inf: # We had some essential part in Y. - return cost, None + if not matching: + return cost else: return cost, np.array([[-1, j] for j in range(m)]) elif m == 0: - if not matching: - return _perstot(X, order, internal_p, enable_autodiff) + cost = _perstot(X, order, internal_p, enable_autodiff) + if cost == np.inf: + return _warn_infty(matching) else: - cost = _perstot(X, order, internal_p, enable_autodiff) - if cost == np.inf: - return cost, None + if not matching: + return cost else: return cost, np.array([[i, -1] for i in range(n)]) # Check essential part and enable autodiff together if enable_autodiff and keep_essential_parts: - import warnings # should it be done at the top of the file? warnings.warn('''enable_autodiff=True and keep_essential_parts=True are incompatible together. keep_essential_parts is set to False: only points with finite coordiantes are considered in the following. @@ -262,11 +276,9 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab if keep_essential_parts: essential_cost, essential_matching = _handle_essential_parts(X, Y, order=order) if (essential_cost == np.inf): - if matching: - return np.inf, None - else: - return np.inf # avoid computing transport cost between the finite parts if essential parts - # cardinalities do not match (saves time) + return _warn_infty(matching) # Tells the user that cost is infty and matching (if True) is None. + # avoid computing transport cost between the finite parts if essential parts + # cardinalities do not match (saves time) else: essential_cost = 0 essential_matching = None -- cgit v1.2.3 From 0e11a3a2232770b0526918d2b543848abd092254 Mon Sep 17 00:00:00 2001 From: Hind Date: Thu, 15 Apr 2021 09:46:27 +0200 Subject: Set Cython language level to remove warning --- src/python/gudhi/simplex_tree.pyx | 2 +- src/python/setup.py.in | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index d7991417..be08a3a1 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -11,7 +11,7 @@ from cython.operator import dereference, preincrement from libc.stdint cimport intptr_t import numpy from numpy import array as np_array -cimport simplex_tree +cimport gudhi.simplex_tree __author__ = "Vincent Rouvreau" __copyright__ = "Copyright (C) 2016 Inria" diff --git a/src/python/setup.py.in b/src/python/setup.py.in index 98d058fc..65f5446e 100644 --- a/src/python/setup.py.in +++ b/src/python/setup.py.in @@ -41,10 +41,9 @@ for module in cython_modules: libraries=libraries, library_dirs=library_dirs, include_dirs=include_dirs, - runtime_library_dirs=runtime_library_dirs, - cython_directives = {'language_level': str(sys.version_info[0])},)) + runtime_library_dirs=runtime_library_dirs,)) -ext_modules = cythonize(ext_modules) +ext_modules = cythonize(ext_modules, compiler_directives={'language_level': str(sys.version_info[0])}) for module in pybind11_modules: my_include_dirs = include_dirs + [pybind11.get_include(False), pybind11.get_include(True)] -- cgit v1.2.3 From e72aacb4c9a83f22da50f71c3528458d6e539ef9 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Thu, 15 Apr 2021 18:16:30 +0200 Subject: Fix contradictory color --- src/python/gudhi/clustering/tomato.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/clustering/tomato.py b/src/python/gudhi/clustering/tomato.py index fbba3cc8..d0e9995c 100644 --- a/src/python/gudhi/clustering/tomato.py +++ b/src/python/gudhi/clustering/tomato.py @@ -271,7 +271,7 @@ class Tomato: l = self.max_weight_per_cc_.min() r = self.max_weight_per_cc_.max() if self.diagram_.size > 0: - plt.plot(self.diagram_[:, 0], self.diagram_[:, 1], "ro") + plt.plot(self.diagram_[:, 0], self.diagram_[:, 1], "o", color="red") l = min(l, self.diagram_[:, 1].min()) r = max(r, self.diagram_[:, 0].max()) if l == r: @@ -283,7 +283,7 @@ class Tomato: l, r = -1.0, 1.0 plt.plot([l, r], [l, r]) plt.plot( - self.max_weight_per_cc_, numpy.full(self.max_weight_per_cc_.shape, 1.1 * l - 0.1 * r), "ro", color="green" + self.max_weight_per_cc_, numpy.full(self.max_weight_per_cc_.shape, 1.1 * l - 0.1 * r), "o", color="green" ) plt.show() -- cgit v1.2.3 From d2f12c8563d9f7aa1f8ead0da0034796a88704e9 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Thu, 15 Apr 2021 20:41:49 +0200 Subject: Compare lists with == numpy/core/_asarray.py:102: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray. --- src/python/test/test_tomato.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/test/test_tomato.py b/src/python/test/test_tomato.py index ecab03c4..c571f799 100755 --- a/src/python/test/test_tomato.py +++ b/src/python/test/test_tomato.py @@ -37,7 +37,7 @@ def test_tomato_1(): t = Tomato(metric="euclidean", graph_type="radius", r=4.7, k=4) t.fit(a) assert t.max_weight_per_cc_.size == 2 - assert np.array_equal(t.neighbors_, [[0, 1, 2], [0, 1, 2], [0, 1, 2], [3, 4, 5, 6], [3, 4, 5], [3, 4, 5], [3, 6]]) + assert t.neighbors_ == [[0, 1, 2], [0, 1, 2], [0, 1, 2], [3, 4, 5, 6], [3, 4, 5], [3, 4, 5], [3, 6]] t.plot_diagram() t = Tomato(graph_type="radius", r=4.7, k=4, symmetrize_graph=True) -- cgit v1.2.3 From 2ce8b315fe5e069d6444b79f6b01e76327fa5d1d Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Thu, 15 Apr 2021 21:25:03 +0200 Subject: Name the argument n_neighbors of NearestNeighbors It seems that it has had the same name for a while, so this shouldn't break anything. sklearn/utils/validation.py:70: FutureWarning: Pass n_neighbors=3 as keyword args. From version 1.0 (renaming of 0.25) passing these as positional arguments will result in an error --- src/python/gudhi/point_cloud/knn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/point_cloud/knn.py b/src/python/gudhi/point_cloud/knn.py index 994be3b6..829bf1bf 100644 --- a/src/python/gudhi/point_cloud/knn.py +++ b/src/python/gudhi/point_cloud/knn.py @@ -111,7 +111,7 @@ class KNearestNeighbors: nargs = { k: v for k, v in self.params.items() if k in {"p", "n_jobs", "metric_params", "algorithm", "leaf_size"} } - self.nn = NearestNeighbors(self.k, metric=self.metric, **nargs) + self.nn = NearestNeighbors(n_neighbors=self.k, metric=self.metric, **nargs) self.nn.fit(X) if self.params["implementation"] == "hnsw": -- cgit v1.2.3 From 0a00c46d770699bbe467ade1c619dc94c8fad7b7 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Sat, 17 Apr 2021 14:53:53 +0200 Subject: Sparse Rips: disable `mini`, optimize a bit redundant points --- .../include/gudhi/Sparse_rips_complex.h | 23 +++++++++++++++++----- src/python/test/test_rips_complex.py | 21 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/Rips_complex/include/gudhi/Sparse_rips_complex.h b/src/Rips_complex/include/gudhi/Sparse_rips_complex.h index a5501004..d7669dad 100644 --- a/src/Rips_complex/include/gudhi/Sparse_rips_complex.h +++ b/src/Rips_complex/include/gudhi/Sparse_rips_complex.h @@ -45,6 +45,7 @@ template class Sparse_rips_complex { private: // TODO(MG): use a different graph where we know we can safely insert in parallel. + // Use a graph that lets us skip some vertices, for `mini` or redundant points. typedef typename boost::adjacency_list, boost::property> @@ -58,7 +59,8 @@ class Sparse_rips_complex { * @param[in] points Range of points. * @param[in] distance Distance function that returns a `Filtration_value` from 2 given points. * @param[in] epsilon Approximation parameter. epsilon must be positive. - * @param[in] mini Minimal filtration value. Ignore anything below this scale. This is a less efficient version of `Gudhi::subsampling::sparsify_point_set()`. + * @param[in] mini Not implemented yet, and broken in previous versions. Minimal filtration value. + * Ignore anything below this scale. This is a less efficient version of `Gudhi::subsampling::sparsify_point_set()`. * @param[in] maxi Maximal filtration value. Ignore anything above this scale. * */ @@ -67,6 +69,7 @@ class Sparse_rips_complex { : epsilon_(epsilon) { GUDHI_CHECK(epsilon > 0, "epsilon must be positive"); auto dist_fun = [&](Vertex_handle i, Vertex_handle j) { return distance(points[i], points[j]); }; + // TODO: stop choose_n_farthest_points once it reaches mini? Then the graph vertices would not be [0, ..., n-1] which complicates things. subsampling::choose_n_farthest_points(dist_fun, boost::irange(0, boost::size(points)), -1, -1, std::back_inserter(sorted_points), std::back_inserter(params)); compute_sparse_graph(dist_fun, epsilon, mini, maxi); @@ -116,9 +119,9 @@ class Sparse_rips_complex { double cst = epsilon_ * (1 - epsilon_) / 2; auto block = [cst,&complex,&lambda](typename SimplicialComplexForRips::Simplex_handle sh){ auto filt = complex.filtration(sh); - auto mini = filt * cst; + auto min_f = filt * cst; for(auto v : complex.simplex_vertex_range(sh)){ - if(lambda[v] < mini) + if(lambda[v] < min_f) return true; // v died before this simplex could be born } return false; @@ -149,12 +152,22 @@ class Sparse_rips_complex { for (int i = 0; i < n; ++i) { auto&& pi = points[i]; auto li = params[i]; - if (li < mini) break; + // FIXME: see below about mini. It might be ok to uncomment just this one, but it requires a proof. + // if ((li < mini || li <= 0) && i != 0) break; + if (li <= 0 && i != 0) break; + // The parameter of the first point is not very meaningful, it is supposed to be infinite, + // but if the type does not support it... + // Points with multiplicity get connected to their first representative, no need to handle + // the redundant ones in the outer loop. for (int j = i + 1; j < n; ++j) { auto&& pj = points[j]; auto d = dist(pi, pj); auto lj = params[j]; - if (lj < mini) break; + // FIXME: It would make sense to ignore the points with low param completely, but the current graph type we are + // using implicitly inserts all the vertices 0 ... n-1, so this would create isolated vertices, which is bad. + // If we do end up ignoring those points, we should do it early, around choose_n_farthest_points. But be careful + // that the size of lambda should reflect the original number of points then. + // if (lj < mini) break; GUDHI_CHECK(lj <= li, "Bad furthest point sorting"); Filtration_value alpha; diff --git a/src/python/test/test_rips_complex.py b/src/python/test/test_rips_complex.py index b86e7498..cae21435 100755 --- a/src/python/test/test_rips_complex.py +++ b/src/python/test/test_rips_complex.py @@ -133,3 +133,24 @@ def test_filtered_rips_from_distance_matrix(): assert simplex_tree.num_simplices() == 8 assert simplex_tree.num_vertices() == 4 + + +def test_sparse_with_multiplicity(): + points = [ + [3, 4], + [0.1, 2], + [0.1, 2], + [0.1, 2], + [0.1, 2], + [0.1, 2], + [0.1, 2], + [0.1, 2], + [0.1, 2], + [0.1, 2], + [0.1, 2], + [3, 4.1], + ] + rips = RipsComplex(points=points, sparse=0.01) + simplex_tree = rips.create_simplex_tree(max_dimension=2) + assert simplex_tree.num_simplices() == 25 + diag = simplex_tree.persistence() -- cgit v1.2.3 From 71337179d95d1e330902b431907cb07698abcdc9 Mon Sep 17 00:00:00 2001 From: Marc Glisse Date: Sat, 17 Apr 2021 17:31:23 +0200 Subject: Safely drop some vertices in sparse Rips --- .../include/gudhi/Sparse_rips_complex.h | 48 ++++++++++------------ src/python/test/test_rips_complex.py | 2 +- 2 files changed, 23 insertions(+), 27 deletions(-) (limited to 'src/python') diff --git a/src/Rips_complex/include/gudhi/Sparse_rips_complex.h b/src/Rips_complex/include/gudhi/Sparse_rips_complex.h index 30afb1d0..28031e68 100644 --- a/src/Rips_complex/include/gudhi/Sparse_rips_complex.h +++ b/src/Rips_complex/include/gudhi/Sparse_rips_complex.h @@ -76,7 +76,7 @@ struct graph_traits> typedef directed_tag directed_category; typedef disallow_parallel_edge_tag edge_parallel_category; }; -// Etc, since we don't expose this graph to the world, we know we are not going to query property_traits. +// Etc, since we don't expose this graph to the world, we know we are not going to query property_traits for instance. } namespace Gudhi { @@ -113,8 +113,7 @@ class Sparse_rips_complex { * @param[in] points Range of points. * @param[in] distance Distance function that returns a `Filtration_value` from 2 given points. * @param[in] epsilon Approximation parameter. epsilon must be positive. - * @param[in] mini Not implemented yet, and broken in previous versions. Minimal filtration value. - * Ignore anything below this scale. This is a less efficient version of `Gudhi::subsampling::sparsify_point_set()`. + * @param[in] mini Minimal filtration value. Ignore anything below this scale. This is a less efficient version of `Gudhi::subsampling::sparsify_point_set()`. * @param[in] maxi Maximal filtration value. Ignore anything above this scale. * */ @@ -123,7 +122,7 @@ class Sparse_rips_complex { : epsilon_(epsilon) { GUDHI_CHECK(epsilon > 0, "epsilon must be positive"); auto dist_fun = [&](Vertex_handle i, Vertex_handle j) { return distance(points[i], points[j]); }; - // TODO: stop choose_n_farthest_points once it reaches mini? Then the graph vertices would not be [0, ..., n-1] which complicates things. + // TODO: stop choose_n_farthest_points once it reaches mini or 0? subsampling::choose_n_farthest_points(dist_fun, boost::irange(0, boost::size(points)), -1, -1, std::back_inserter(sorted_points), std::back_inserter(params)); compute_sparse_graph(dist_fun, epsilon, mini, maxi); @@ -165,10 +164,10 @@ class Sparse_rips_complex { complex.expansion(dim_max); return; } - const int n = boost::size(params); - std::vector lambda(n); + const Vertex_handle n = num_vertices(graph_); + std::vector lambda(max_v + 1); // lambda[original_order]=params[sorted_order] - for(int i=0;i void compute_sparse_graph(Distance& dist, double epsilon, Filtration_value mini, Filtration_value maxi) { const auto& points = sorted_points; // convenience alias - const int n = boost::size(points); + Vertex_handle n = boost::size(points); double cst = epsilon * (1 - epsilon) / 2; - graph_.~Graph(); - new (&graph_) Graph(); - for (int i = 0; i < n; ++i) { - add_vertex(i, graph_); + max_v = -1; // Useful for the size of the map lambda. + for (Vertex_handle i = 0; i < n; ++i) { + if ((params[i] < mini || params[i] <= 0) && i != 0) break; + // The parameter of the first point is not very meaningful, it is supposed to be infinite, + // but if the type does not support it... + // It would be better to do this reduction of the number of points earlier, around choose_n_farthest_points. + add_vertex(points[i], graph_); + max_v = std::max(max_v, points[i]); } + n = num_vertices(graph_); // TODO(MG): // - make it parallel // - only test near-enough neighbors - for (int i = 0; i < n; ++i) { + for (Vertex_handle i = 0; i < n; ++i) { auto&& pi = points[i]; auto li = params[i]; - // FIXME: see below about mini. It might be ok to uncomment just this one, but it requires a proof. - // if ((li < mini || li <= 0) && i != 0) break; - if (li <= 0 && i != 0) break; - // The parameter of the first point is not very meaningful, it is supposed to be infinite, - // but if the type does not support it... - // Points with multiplicity get connected to their first representative, no need to handle - // the redundant ones in the outer loop. - for (int j = i + 1; j < n; ++j) { + // If we inserted all the points, points with multiplicity would get connected to their first representative, + // no need to handle the redundant ones in the outer loop. + // if (li <= 0 && i != 0) break; + for (Vertex_handle j = i + 1; j < n; ++j) { auto&& pj = points[j]; auto d = dist(pi, pj); auto lj = params[j]; - // FIXME: It would make sense to ignore the points with low param completely, but the current graph type we are - // using implicitly inserts all the vertices 0 ... n-1, so this would create isolated vertices, which is bad. - // If we do end up ignoring those points, we should do it early, around choose_n_farthest_points. But be careful - // that the size of lambda should reflect the original number of points then. - // if (lj < mini) break; GUDHI_CHECK(lj <= li, "Bad furthest point sorting"); Filtration_value alpha; @@ -241,6 +236,7 @@ class Sparse_rips_complex { Graph graph_; double epsilon_; + Vertex_handle max_v; // Because of the arbitrary split between constructor and create_complex // sorted_points[sorted_order]=original_order std::vector sorted_points; diff --git a/src/python/test/test_rips_complex.py b/src/python/test/test_rips_complex.py index cae21435..a2f43a1b 100755 --- a/src/python/test/test_rips_complex.py +++ b/src/python/test/test_rips_complex.py @@ -152,5 +152,5 @@ def test_sparse_with_multiplicity(): ] rips = RipsComplex(points=points, sparse=0.01) simplex_tree = rips.create_simplex_tree(max_dimension=2) - assert simplex_tree.num_simplices() == 25 + assert simplex_tree.num_simplices() == 7 diag = simplex_tree.persistence() -- cgit v1.2.3 From 604b2cde0c7951c81d1c510f3038e2c65c19e6fe Mon Sep 17 00:00:00 2001 From: tlacombe Date: Tue, 20 Apr 2021 19:06:56 +0200 Subject: update doc and tests --- src/python/doc/wasserstein_distance_user.rst | 1 + src/python/test/test_wasserstein_distance.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/wasserstein_distance_user.rst b/src/python/doc/wasserstein_distance_user.rst index 091c9fd9..76eb1469 100644 --- a/src/python/doc/wasserstein_distance_user.rst +++ b/src/python/doc/wasserstein_distance_user.rst @@ -92,6 +92,7 @@ any matching has a cost +inf and thus can be considered to be optimal. In such a for j in dgm2_to_diagonal: print("point %s in dgm2 is matched to the diagonal" %j) + # An example where essential part cardinalities differ dgm3 = np.array([[1, 2], [0, np.inf]]) dgm4 = np.array([[1, 2], [0, np.inf], [1, np.inf]]) cost, matchings = gudhi.wasserstein.wasserstein_distance(dgm3, dgm4, matching=True, order=1, internal_p=2) diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index df7acc91..121ba065 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -67,16 +67,25 @@ def test_handle_essential_parts(): def test_get_essential_parts(): - diag = np.array([[0, 1], [3, 5], [2, np.inf], [3, np.inf], [-np.inf, 8], [-np.inf, 12], [-np.inf, -np.inf], + diag1 = np.array([[0, 1], [3, 5], [2, np.inf], [3, np.inf], [-np.inf, 8], [-np.inf, 12], [-np.inf, -np.inf], [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) - res = _get_essential_parts(diag) + diag2 = np.array([[0, 1], [3, 5], [2, np.inf], [3, np.inf]]) + + res = _get_essential_parts(diag1) + res2 = _get_essential_parts(diag2) assert np.array_equal(res[0], [4, 5]) assert np.array_equal(res[1], [2, 3]) assert np.array_equal(res[2], [8, 9]) assert np.array_equal(res[3], [6] ) assert np.array_equal(res[4], [7] ) + assert np.array_equal(res2[0], [] ) + assert np.array_equal(res2[1], [2, 3]) + assert np.array_equal(res2[2], [] ) + assert np.array_equal(res2[3], [] ) + assert np.array_equal(res2[4], [] ) + def _basic_wasserstein(wasserstein_distance, delta, test_infinity=True, test_matching=True): diag1 = np.array([[2.7, 3.7], [9.6, 14.0], [34.2, 34.974]]) @@ -152,7 +161,7 @@ def pot_wrap(**extra): return fun def test_wasserstein_distance_pot(): - _basic_wasserstein(pot, 1e-15, test_infinity=False, test_matching=True) + _basic_wasserstein(pot, 1e-15, test_infinity=False, test_matching=True) # pot with its standard args _basic_wasserstein(pot_wrap(enable_autodiff=True, keep_essential_parts=False), 1e-15, test_infinity=False, test_matching=False) def test_wasserstein_distance_hera(): -- cgit v1.2.3 From 0360f02ec1778daae53b50c50f223049fa294328 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Wed, 21 Apr 2021 10:26:05 +0200 Subject: typo corrected --- src/python/gudhi/wasserstein/wasserstein.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 8ccbe12e..a89c7efd 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -198,7 +198,7 @@ def _warn_infty(matching): warnings.warn('Cardinality of essential parts differs. Distance (cost) is +infty, and the returned matching is None.') return np.inf, None else: - warnings.warn('Cardinality of essential parts diffes. Distance (cost) is +infty.') + warnings.warn('Cardinality of essential parts differs. Distance (cost) is +infty.') return np.inf -- cgit v1.2.3 From 45917ecf17acacfede909994d7b3a78fc18355da Mon Sep 17 00:00:00 2001 From: Hind Date: Thu, 22 Apr 2021 17:08:17 +0200 Subject: Add random points generator on sphere in python, with an example --- src/python/CMakeLists.txt | 6 ++ .../alpha_complex_from_generated_points_example.py | 52 +++++++++++++++++ src/python/gudhi/random_point_generators.cc | 68 ++++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 src/python/example/alpha_complex_from_generated_points_example.py create mode 100644 src/python/gudhi/random_point_generators.cc (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 73303a24..8baf0f02 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -43,6 +43,7 @@ endfunction( add_gudhi_debug_info ) if(PYTHONINTERP_FOUND) if(PYBIND11_FOUND) add_gudhi_debug_info("Pybind11 version ${PYBIND11_VERSION}") + set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'random_point_generators', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'bottleneck', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'hera', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'clustering', ") @@ -151,6 +152,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/wasserstein', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/bottleneck', ") if (NOT CGAL_VERSION VERSION_LESS 4.11.0) + set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'random_point_generators', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'bottleneck', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'nerve_gic', ") endif () @@ -425,6 +427,10 @@ if(PYTHONINTERP_FOUND) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/example/alpha_complex_from_points_example.py") + add_test(NAME alpha_complex_from_generated_points_example_py_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" + ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/example/alpha_complex_from_generated_points_example.py") add_test(NAME alpha_complex_diagram_persistence_from_off_file_example_py_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" diff --git a/src/python/example/alpha_complex_from_generated_points_example.py b/src/python/example/alpha_complex_from_generated_points_example.py new file mode 100644 index 00000000..7a07ed42 --- /dev/null +++ b/src/python/example/alpha_complex_from_generated_points_example.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +from gudhi import random_point_generators +from gudhi import AlphaComplex, SimplexTree +from gudhi import plot_persistence_barcode, plot_persistence_diagram + +import matplotlib.pyplot as plt + + +""" This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + Author(s): Hind Montassif + + Copyright (C) 2021 Inria + + Modification(s): + - YYYY/MM Author: Description of the modification +""" + +__author__ = "Hind Montassif" +__copyright__ = "Copyright (C) 2021 Inria" +__license__ = "MIT" + +print("#####################################################################") +print("AlphaComplex creation from generated points") + + +# Generate a circle: 50 points; dim 2; radius 1 +points = random_point_generators.generate_points_on_sphere_d(50, 2, 1) + +# Plot the generated points (to uncomment if wished) +#plt.scatter(points[:,0], points[:,1]) +#plt.show() + +# Create an alpha complex +alpha_complex = AlphaComplex(points=points) +simplex_tree = alpha_complex.create_simplex_tree() + +result_str = 'Alpha complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ + repr(simplex_tree.num_simplices()) + ' simplices - ' + \ + repr(simplex_tree.num_vertices()) + ' vertices.' +print(result_str) + + +# Compute the persistence +diag = simplex_tree.persistence() + +# Plot the barcode and diagram (to uncomment if wished) +#plot_persistence_barcode(diag) +#plt.show() +#plot_persistence_diagram(diag) +#plt.show() diff --git a/src/python/gudhi/random_point_generators.cc b/src/python/gudhi/random_point_generators.cc new file mode 100644 index 00000000..39b09a6d --- /dev/null +++ b/src/python/gudhi/random_point_generators.cc @@ -0,0 +1,68 @@ +/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + * Author(s): Hind Montassif + * + * Copyright (C) 2021 Inria + * + * Modification(s): + * - YYYY/MM Author: Description of the modification + */ + +#include +#include + +#include + +#include + +namespace py = pybind11; + + +typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; + +template +py::array_t generate_points_on_sphere(py::object num_points, py::object dim, py::object radius) { + int npoints = num_points.cast(); + int d = dim.cast(); + double rad = radius.cast(); + + py::gil_scoped_release release; + + auto points_generated = Gudhi::generate_points_on_sphere_d(npoints, d, rad); + + py::gil_scoped_acquire acquire; + + py::array_t points({npoints, d}); + + py::buffer_info buf = points.request(); + + double *ptr = static_cast(buf.ptr); + + assert(npoints == buf.shape[0]); + assert(d == buf.shape[1]); + + + for (size_t i = 0; i < (size_t)npoints; i++) + for (size_t j = 0; j < (size_t)d; j++) + ptr[i*d+j] = points_generated.at(i).at(j); + + return points; +} + +PYBIND11_MODULE(random_point_generators, m) { + m.attr("__license__") = "LGPL v3"; + m.def("generate_points_on_sphere_d", &generate_points_on_sphere, + py::arg("num_points"), py::arg("dim"), py::arg("radius"), + R"pbdoc( + Generate points on a sphere + + :param num_points: The number of points to be generated. + :type num_points: integer + :param dim: The sphere dimension. + :type dim: integer + :param radius: The sphere radius. + :type radius: float + :rtype: numpy array of points + :returns: the generated points on a sphere. + )pbdoc"); +} -- cgit v1.2.3 From db7ce3487e526741c0408b00c2cffda0048b0026 Mon Sep 17 00:00:00 2001 From: Hind Date: Fri, 23 Apr 2021 11:27:59 +0200 Subject: Make adjustments according to the received reviews --- src/python/CMakeLists.txt | 2 +- src/python/gudhi/random_point_generators.cc | 45 +++++++++++++---------------- 2 files changed, 21 insertions(+), 26 deletions(-) (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 8baf0f02..87f10a1a 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -43,10 +43,10 @@ endfunction( add_gudhi_debug_info ) if(PYTHONINTERP_FOUND) if(PYBIND11_FOUND) add_gudhi_debug_info("Pybind11 version ${PYBIND11_VERSION}") - set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'random_point_generators', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'bottleneck', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'hera', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'clustering', ") + set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'random_point_generators', ") endif() if(CYTHON_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'off_reader', ") diff --git a/src/python/gudhi/random_point_generators.cc b/src/python/gudhi/random_point_generators.cc index 39b09a6d..4306ba98 100644 --- a/src/python/gudhi/random_point_generators.cc +++ b/src/python/gudhi/random_point_generators.cc @@ -21,30 +21,25 @@ namespace py = pybind11; typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; template -py::array_t generate_points_on_sphere(py::object num_points, py::object dim, py::object radius) { - int npoints = num_points.cast(); - int d = dim.cast(); - double rad = radius.cast(); - - py::gil_scoped_release release; - - auto points_generated = Gudhi::generate_points_on_sphere_d(npoints, d, rad); - - py::gil_scoped_acquire acquire; - - py::array_t points({npoints, d}); +py::array_t generate_points_on_sphere(size_t num_points, int dim, double radius) { + + py::array_t points({(int)num_points, dim}); py::buffer_info buf = points.request(); - double *ptr = static_cast(buf.ptr); - assert(npoints == buf.shape[0]); - assert(d == buf.shape[1]); + assert(num_points == buf.shape[0]); + assert(dim == buf.shape[1]); - - for (size_t i = 0; i < (size_t)npoints; i++) - for (size_t j = 0; j < (size_t)d; j++) - ptr[i*d+j] = points_generated.at(i).at(j); + std::vector points_generated; + { + py::gil_scoped_release release; + points_generated = Gudhi::generate_points_on_sphere_d(num_points, dim, radius); + + for (size_t i = 0; i < num_points; i++) + for (size_t j = 0; j < (size_t)dim; j++) + ptr[i*dim+j] = points_generated[i][j]; + } return points; } @@ -52,17 +47,17 @@ py::array_t generate_points_on_sphere(py::object num_points, py::object PYBIND11_MODULE(random_point_generators, m) { m.attr("__license__") = "LGPL v3"; m.def("generate_points_on_sphere_d", &generate_points_on_sphere, - py::arg("num_points"), py::arg("dim"), py::arg("radius"), + py::arg("num_points"), py::arg("dim"), py::arg("radius") = 1, R"pbdoc( - Generate points on a sphere + Generate random i.i.d. points uniformly on a (d-1)-sphere in Rd :param num_points: The number of points to be generated. - :type num_points: integer - :param dim: The sphere dimension. + :type num_points: unsigned integer + :param dim: The dimension. :type dim: integer - :param radius: The sphere radius. + :param radius: The radius. :type radius: float - :rtype: numpy array of points + :rtype: numpy array of float :returns: the generated points on a sphere. )pbdoc"); } -- cgit v1.2.3 From e59b1cfd338a80a769c0e2b6d677b9474b07beb3 Mon Sep 17 00:00:00 2001 From: Hind Date: Mon, 26 Apr 2021 11:47:36 +0200 Subject: Replace assert with GUDHI_CHECK Make the function non-template Change typing and casting --- src/python/gudhi/random_point_generators.cc | 34 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 18 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/random_point_generators.cc b/src/python/gudhi/random_point_generators.cc index 4306ba98..6eb40429 100644 --- a/src/python/gudhi/random_point_generators.cc +++ b/src/python/gudhi/random_point_generators.cc @@ -12,6 +12,7 @@ #include #include +#include #include @@ -20,36 +21,33 @@ namespace py = pybind11; typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; -template py::array_t generate_points_on_sphere(size_t num_points, int dim, double radius) { - - py::array_t points({(int)num_points, dim}); - + + py::array_t points({num_points, (size_t)dim}); + py::buffer_info buf = points.request(); double *ptr = static_cast(buf.ptr); - assert(num_points == buf.shape[0]); - assert(dim == buf.shape[1]); - - std::vector points_generated; - { - py::gil_scoped_release release; - points_generated = Gudhi::generate_points_on_sphere_d(num_points, dim, radius); - - for (size_t i = 0; i < num_points; i++) - for (size_t j = 0; j < (size_t)dim; j++) - ptr[i*dim+j] = points_generated[i][j]; - } + GUDHI_CHECK(num_points == buf.shape[0], "Py array first dimension not matching num_points on sphere"); + GUDHI_CHECK(dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); + + + py::gil_scoped_release release; + auto points_generated = Gudhi::generate_points_on_sphere_d(num_points, dim, radius); + + for (size_t i = 0; i < num_points; i++) + for (int j = 0; j < dim; j++) + ptr[i*dim+j] = points_generated[i][j]; return points; } PYBIND11_MODULE(random_point_generators, m) { m.attr("__license__") = "LGPL v3"; - m.def("generate_points_on_sphere_d", &generate_points_on_sphere, + m.def("generate_points_on_sphere_d", &generate_points_on_sphere, py::arg("num_points"), py::arg("dim"), py::arg("radius") = 1, R"pbdoc( - Generate random i.i.d. points uniformly on a (d-1)-sphere in Rd + Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d :param num_points: The number of points to be generated. :type num_points: unsigned integer -- cgit v1.2.3 From 154596a39b2b26c90e46ec851b8f05ea08fa47d4 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 27 Apr 2021 09:17:53 +0200 Subject: Remove make install target from python and rewrite documentation accordingly --- src/common/doc/installation.h | 4 ++++ src/python/CMakeLists.txt | 2 -- src/python/doc/installation.rst | 14 ++++---------- 3 files changed, 8 insertions(+), 12 deletions(-) (limited to 'src/python') diff --git a/src/common/doc/installation.h b/src/common/doc/installation.h index c2e63a24..ce393c38 100644 --- a/src/common/doc/installation.h +++ b/src/common/doc/installation.h @@ -30,6 +30,10 @@ make \endverbatim * This action may require to be in the sudoer or administrator of the machine in function of the operating system and * of CMAKE_INSTALL_PREFIX. * + * \note Python module will be compiled by the `make` command, but `make install` will not install it. Please refer to + * the Python + * module installation documentation. + * * \subsection testsuites Test suites * To test your build, run the following command in a terminal: * \verbatim make test \endverbatim diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 73303a24..a1440cbc 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -274,8 +274,6 @@ if(PYTHONINTERP_FOUND) add_custom_target(python ALL DEPENDS gudhi.so COMMENT "Do not forget to add ${CMAKE_CURRENT_BINARY_DIR}/ to your PYTHONPATH before using examples or tests") - install(CODE "execute_process(COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/setup.py install)") - set(GUDHI_PYTHON_PATH_ENV "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}:$ENV{PYTHONPATH}") # Documentation generation is available through sphinx - requires all modules # Make it first as sphinx test is by far the longest test which is nice when testing in parallel diff --git a/src/python/doc/installation.rst b/src/python/doc/installation.rst index 66efe45a..2881055f 100644 --- a/src/python/doc/installation.rst +++ b/src/python/doc/installation.rst @@ -99,20 +99,14 @@ Or install it definitely in your Python packages folder: .. code-block:: bash cd /path-to-gudhi/build/python - # May require sudo or administrator privileges - make install + python setup.py install # add --user to the command if you do not have the permission + # Or 'pip install .' .. note:: - :code:`make install` is only a - `CMake custom targets `_ - to shortcut :code:`python setup.py install` command. It does not take into account :code:`CMAKE_INSTALL_PREFIX`. - But one can use :code:`python setup.py install ...` specific options in the python directory: - -.. code-block:: bash - - python setup.py install --prefix /home/gudhi # Install in /home/gudhi directory + But one can use + `alternate location installation `_. Test suites =========== -- cgit v1.2.3 From 44085e15a0ce83d8139db7da276d656bd6381026 Mon Sep 17 00:00:00 2001 From: Théo Lacombe Date: Tue, 27 Apr 2021 09:55:13 +0200 Subject: Typo correction - Update src/python/gudhi/wasserstein/wasserstein.py Co-authored-by: Vincent Rouvreau <10407034+VincentRouvreau@users.noreply.github.com> --- src/python/gudhi/wasserstein/wasserstein.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 8ccbe12e..926dec33 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -131,7 +131,7 @@ def _cost_and_match_essential_parts(X, Y, idX, idY, order, axis): :param Y: (n x 2) numpy.array (dgm points) :param idX: indices to consider for this one dimensional OT problem (in X) :param idY: indices to consider for this one dimensional OT problem (in Y) - :param order: exponent for Wasserstein distanc ecomputation + :param order: exponent for Wasserstein distance computation :param axis: must be 0 or 1, correspond to the coordinate which is finite. :returns: cost (float) and match for points with *one* infinite coordinate. -- cgit v1.2.3 From 29ffea359c52e2813c8e6887bda51874c36a56a5 Mon Sep 17 00:00:00 2001 From: Théo Lacombe Date: Tue, 27 Apr 2021 09:56:53 +0200 Subject: Typo - Update src/python/gudhi/wasserstein/wasserstein.py Co-authored-by: Vincent Rouvreau <10407034+VincentRouvreau@users.noreply.github.com> --- src/python/gudhi/wasserstein/wasserstein.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 926dec33..90988512 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -267,7 +267,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab # Check essential part and enable autodiff together if enable_autodiff and keep_essential_parts: warnings.warn('''enable_autodiff=True and keep_essential_parts=True are incompatible together. - keep_essential_parts is set to False: only points with finite coordiantes are considered + keep_essential_parts is set to False: only points with finite coordinates are considered in the following. ''') keep_essential_parts = False -- cgit v1.2.3 From c1ab7c43d4797da93aa74ba823dd1a6b28fb2cfd Mon Sep 17 00:00:00 2001 From: tlacombe Date: Tue, 27 Apr 2021 12:16:22 +0200 Subject: now consider (inf,inf) as belonging to the diagonal ; more tests --- src/python/gudhi/wasserstein/wasserstein.py | 18 ++++++++++---- src/python/test/test_wasserstein_distance.py | 36 +++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 3abecfe6..5095e672 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -106,6 +106,8 @@ def _get_essential_parts(a): .. note:: For instance, a[_get_essential_parts(a)[0]] returns the points in a of coordinates (-inf, x) for some finite x. Note also that points with (+inf, -inf) are not handled (points (x,y) in dgm satisfy by assumption (y >= x)). + + Finally, we consider that points with coordinates (-inf,-inf) and (+inf, +inf) belong to the diagonal. ''' if len(a): first_coord_finite = np.isfinite(a[:,0]) @@ -118,6 +120,7 @@ def _get_essential_parts(a): ess_first_type = np.where(second_coord_finite & first_coord_infinite_negative)[0] # coord (-inf, x) ess_second_type = np.where(first_coord_finite & second_coord_infinite_positive)[0] # coord (x, +inf) ess_third_type = np.where(first_coord_infinite_negative & second_coord_infinite_positive)[0] # coord (-inf, +inf) + ess_fourth_type = np.where(first_coord_infinite_negative & second_coord_infinite_negative)[0] # coord (-inf, -inf) ess_fifth_type = np.where(first_coord_infinite_positive & second_coord_infinite_positive)[0] # coord (+inf, +inf) return ess_first_type, ess_second_type, ess_third_type, ess_fourth_type, ess_fifth_type @@ -162,7 +165,7 @@ def _handle_essential_parts(X, Y, order): ess_parts_Y = _get_essential_parts(Y) # Treats the case of infinite cost (cardinalities of essential parts differ). - for u, v in zip(ess_parts_X, ess_parts_Y): + for u, v in list(zip(ess_parts_X, ess_parts_Y))[:3]: # ignore types 4 and 5 as they belong to the diagonal if len(u) != len(v): return np.inf, None @@ -174,9 +177,14 @@ def _handle_essential_parts(X, Y, order): c = c1 + c2 m = m1 + m2 - # Handle type >= 2 (both coordinates are infinite, so we essentially just align points) - for u, v in zip(ess_parts_X[2:], ess_parts_Y[2:]): - m += list(zip(u, v)) # cost is 0 + # Handle type3 (coordinates (-inf,+inf), so we just align points) + m += list(zip(ess_parts_X[2], ess_parts_Y[2])) + + # Handle type 4 and 5, considered as belonging to the diagonal so matched to (-1) with cost 0. + for z in ess_parts_X[3:]: + m += [(u, -1) for u in z] # points in X are matched to -1 + for z in ess_parts_Y[3:]: + m += [(-1, v) for v in z] # -1 is match to points in Y return c, np.array(m) @@ -334,7 +342,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab return ep.concatenate(dists).norms.lp(order).raw # We can also concatenate the 3 vectors to compute just one norm. - # Comptuation of the otcost using the ot.emd2 library. + # Comptuation of the ot cost using the ot.emd2 library. # Note: it is the Wasserstein distance to the power q. # The default numItermax=100000 is not sufficient for some examples with 5000 points, what is a good value? ot_cost = ot.emd2(a, b, M, numItermax=2000000) diff --git a/src/python/test/test_wasserstein_distance.py b/src/python/test/test_wasserstein_distance.py index 121ba065..3a004d77 100755 --- a/src/python/test/test_wasserstein_distance.py +++ b/src/python/test/test_wasserstein_distance.py @@ -10,6 +10,7 @@ """ from gudhi.wasserstein.wasserstein import _proj_on_diag, _finite_part, _handle_essential_parts, _get_essential_parts +from gudhi.wasserstein.wasserstein import _warn_infty from gudhi.wasserstein import wasserstein_distance as pot from gudhi.hera import wasserstein_distance as hera import numpy as np @@ -50,16 +51,17 @@ def test_handle_essential_parts(): [-np.inf, np.inf], [-np.inf, np.inf]]) diag3 = np.array([[0, 2], [3, 5], - [2, np.inf], [4, np.inf], + [2, np.inf], [4, np.inf], [6, np.inf], [-np.inf, 8], [-np.inf, 11], - [-np.inf, -np.inf], [-np.inf, -np.inf], + [-np.inf, -np.inf], [np.inf, np.inf], [-np.inf, np.inf], [-np.inf, np.inf]]) c, m = _handle_essential_parts(diag1, diag2, order=1) assert c == pytest.approx(2, 0.0001) # Note: here c is only the cost due to essential part (thus 2, not 3) # Similarly, the matching only corresponds to essential parts. - assert np.array_equal(m, [[4, 4], [5, 5], [2, 2], [3, 3], [8, 8], [9, 9], [6, 6], [7, 7]]) + # Note that (-inf,-inf) and (+inf,+inf) coordinates are matched to the diagonal. + assert np.array_equal(m, [[4, 4], [5, 5], [2, 2], [3, 3], [8, 8], [9, 9], [6, -1], [7, -1], [-1, 6], [-1, 7]]) c, m = _handle_essential_parts(diag1, diag3, order=1) assert c == np.inf @@ -87,6 +89,13 @@ def test_get_essential_parts(): assert np.array_equal(res2[4], [] ) +def test_warn_infty(): + assert _warn_infty(matching=False)==np.inf + c, m = _warn_infty(matching=True) + assert (c == np.inf) + assert (m is None) + + def _basic_wasserstein(wasserstein_distance, delta, test_infinity=True, test_matching=True): diag1 = np.array([[2.7, 3.7], [9.6, 14.0], [34.2, 34.974]]) diag2 = np.array([[2.8, 4.45], [9.5, 14.1]]) @@ -143,11 +152,29 @@ def _basic_wasserstein(wasserstein_distance, delta, test_infinity=True, test_mat if test_matching and test_infinity: diag7 = np.array([[0, 3], [4, np.inf], [5, np.inf]]) + diag8 = np.array([[0,1], [0, np.inf], [-np.inf, -np.inf], [np.inf, np.inf]]) + diag9 = np.array([[-np.inf, -np.inf], [np.inf, np.inf]]) + diag10 = np.array([[0,1], [-np.inf, -np.inf], [np.inf, np.inf]]) match = wasserstein_distance(diag5, diag6, matching=True, internal_p=2., order=2.)[1] assert np.array_equal(match, [[0, -1], [-1,0], [-1, 1], [1, 2]]) match = wasserstein_distance(diag5, diag7, matching=True, internal_p=2., order=2.)[1] assert (match is None) + cost, match = wasserstein_distance(diag7, emptydiag, matching=True, internal_p=2., order=2.3) + assert (cost == np.inf) + assert (match is None) + cost, match = wasserstein_distance(emptydiag, diag7, matching=True, internal_p=2.42, order=2.) + assert (cost == np.inf) + assert (match is None) + cost, match = wasserstein_distance(diag8, diag9, matching=True, internal_p=2., order=2.) + assert (cost == np.inf) + assert (match is None) + cost, match = wasserstein_distance(diag9, diag10, matching=True, internal_p=1., order=1.) + assert (cost == 1) + assert (match == [[0, -1],[1, -1],[-1, 0], [-1, 1], [-1, 2]]) # type 4 and 5 are match to the diag anyway. + cost, match = wasserstein_distance(diag9, emptydiag, matching=True, internal_p=2., order=2.) + assert (cost == 0.) + assert (match == [[0, -1], [1, -1]]) def hera_wrap(**extra): @@ -155,15 +182,18 @@ def hera_wrap(**extra): return hera(*kargs,**kwargs,**extra) return fun + def pot_wrap(**extra): def fun(*kargs,**kwargs): return pot(*kargs,**kwargs,**extra) return fun + def test_wasserstein_distance_pot(): _basic_wasserstein(pot, 1e-15, test_infinity=False, test_matching=True) # pot with its standard args _basic_wasserstein(pot_wrap(enable_autodiff=True, keep_essential_parts=False), 1e-15, test_infinity=False, test_matching=False) + def test_wasserstein_distance_hera(): _basic_wasserstein(hera_wrap(delta=1e-12), 1e-12, test_matching=False) _basic_wasserstein(hera_wrap(delta=.1), .1, test_matching=False) -- cgit v1.2.3 From b5fc64b23f8c92377a86111f75178abcc171050d Mon Sep 17 00:00:00 2001 From: tlacombe Date: Tue, 27 Apr 2021 14:57:04 +0200 Subject: changed infty to inf in doc --- src/python/gudhi/wasserstein/wasserstein.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 5095e672..61505d03 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -83,7 +83,7 @@ def _perstot(X, order, internal_p, enable_autodiff): :returns: float, the total persistence of the diagram (that is, its distance to the empty diagram). .. note:: - Can be +infty if the diagram has an essential part (points with infinite coordinates). + Can be +inf if the diagram has an essential part (points with infinite coordinates). ''' if enable_autodiff: import eagerpy as ep @@ -203,10 +203,10 @@ def _warn_infty(matching): `matching=True`) about the returned matching being `None`. ''' if matching: - warnings.warn('Cardinality of essential parts differs. Distance (cost) is +infty, and the returned matching is None.') + warnings.warn('Cardinality of essential parts differs. Distance (cost) is +inf, and the returned matching is None.') return np.inf, None else: - warnings.warn('Cardinality of essential parts differs. Distance (cost) is +infty.') + warnings.warn('Cardinality of essential parts differs. Distance (cost) is +inf.') return np.inf -- cgit v1.2.3 From 7573e67c8c6c1bb3cd21fd8b9ffb8aa0168eb7f7 Mon Sep 17 00:00:00 2001 From: Hind Date: Tue, 27 Apr 2021 15:13:25 +0200 Subject: Remove the commented graphic part from the example (to be added to tutorial notebooks) --- .../alpha_complex_from_generated_points_example.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) (limited to 'src/python') diff --git a/src/python/example/alpha_complex_from_generated_points_example.py b/src/python/example/alpha_complex_from_generated_points_example.py index 7a07ed42..c2562d8a 100644 --- a/src/python/example/alpha_complex_from_generated_points_example.py +++ b/src/python/example/alpha_complex_from_generated_points_example.py @@ -1,10 +1,7 @@ #!/usr/bin/env python from gudhi import random_point_generators -from gudhi import AlphaComplex, SimplexTree -from gudhi import plot_persistence_barcode, plot_persistence_diagram - -import matplotlib.pyplot as plt +from gudhi import AlphaComplex """ This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. @@ -28,10 +25,6 @@ print("AlphaComplex creation from generated points") # Generate a circle: 50 points; dim 2; radius 1 points = random_point_generators.generate_points_on_sphere_d(50, 2, 1) -# Plot the generated points (to uncomment if wished) -#plt.scatter(points[:,0], points[:,1]) -#plt.show() - # Create an alpha complex alpha_complex = AlphaComplex(points=points) simplex_tree = alpha_complex.create_simplex_tree() @@ -41,12 +34,3 @@ result_str = 'Alpha complex is of dimension ' + repr(simplex_tree.dimension()) + repr(simplex_tree.num_vertices()) + ' vertices.' print(result_str) - -# Compute the persistence -diag = simplex_tree.persistence() - -# Plot the barcode and diagram (to uncomment if wished) -#plot_persistence_barcode(diag) -#plt.show() -#plot_persistence_diagram(diag) -#plt.show() -- cgit v1.2.3 From df9daf64aa7623ac188a5842a90162d65a54b07e Mon Sep 17 00:00:00 2001 From: Hind Date: Tue, 27 Apr 2021 18:01:00 +0200 Subject: Rename and reorganize point generators module --- src/python/CMakeLists.txt | 10 ++-- .../alpha_complex_from_generated_points_example.py | 36 ------------- ...plex_from_generated_points_on_sphere_example.py | 36 +++++++++++++ src/python/gudhi/datasets/generators/__init__.py | 0 src/python/gudhi/datasets/generators/sphere.cc | 61 ++++++++++++++++++++++ src/python/gudhi/random_point_generators.cc | 61 ---------------------- 6 files changed, 103 insertions(+), 101 deletions(-) delete mode 100644 src/python/example/alpha_complex_from_generated_points_example.py create mode 100644 src/python/example/alpha_complex_from_generated_points_on_sphere_example.py create mode 100644 src/python/gudhi/datasets/generators/__init__.py create mode 100644 src/python/gudhi/datasets/generators/sphere.cc delete mode 100644 src/python/gudhi/random_point_generators.cc (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 87f10a1a..bcdd0741 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -46,7 +46,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'bottleneck', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'hera', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'clustering', ") - set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'random_point_generators', ") + set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets/generators', ") endif() if(CYTHON_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'off_reader', ") @@ -152,7 +152,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/wasserstein', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/bottleneck', ") if (NOT CGAL_VERSION VERSION_LESS 4.11.0) - set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'random_point_generators', ") + set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'datasets/generators/sphere', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'bottleneck', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'nerve_gic', ") endif () @@ -264,6 +264,8 @@ if(PYTHONINTERP_FOUND) file(COPY "gudhi/weighted_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/dtm_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/hera/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/hera") + file(COPY "gudhi/datasets/generators/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/datasets/generators") + # Some files for pip package file(COPY "introduction.rst" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/") @@ -427,10 +429,10 @@ if(PYTHONINTERP_FOUND) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/example/alpha_complex_from_points_example.py") - add_test(NAME alpha_complex_from_generated_points_example_py_test + add_test(NAME alpha_complex_from_generated_points_on_sphere_example_py_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" - ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/example/alpha_complex_from_generated_points_example.py") + ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/example/alpha_complex_from_generated_points_on_sphere_example.py") add_test(NAME alpha_complex_diagram_persistence_from_off_file_example_py_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" diff --git a/src/python/example/alpha_complex_from_generated_points_example.py b/src/python/example/alpha_complex_from_generated_points_example.py deleted file mode 100644 index c2562d8a..00000000 --- a/src/python/example/alpha_complex_from_generated_points_example.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python - -from gudhi import random_point_generators -from gudhi import AlphaComplex - - -""" This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. - See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. - Author(s): Hind Montassif - - Copyright (C) 2021 Inria - - Modification(s): - - YYYY/MM Author: Description of the modification -""" - -__author__ = "Hind Montassif" -__copyright__ = "Copyright (C) 2021 Inria" -__license__ = "MIT" - -print("#####################################################################") -print("AlphaComplex creation from generated points") - - -# Generate a circle: 50 points; dim 2; radius 1 -points = random_point_generators.generate_points_on_sphere_d(50, 2, 1) - -# Create an alpha complex -alpha_complex = AlphaComplex(points=points) -simplex_tree = alpha_complex.create_simplex_tree() - -result_str = 'Alpha complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ - repr(simplex_tree.num_simplices()) + ' simplices - ' + \ - repr(simplex_tree.num_vertices()) + ' vertices.' -print(result_str) - diff --git a/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py b/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py new file mode 100644 index 00000000..2de9ec08 --- /dev/null +++ b/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +from gudhi.datasets.generators import sphere +from gudhi import AlphaComplex + + +""" This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + Author(s): Hind Montassif + + Copyright (C) 2021 Inria + + Modification(s): + - YYYY/MM Author: Description of the modification +""" + +__author__ = "Hind Montassif" +__copyright__ = "Copyright (C) 2021 Inria" +__license__ = "MIT" + +print("#####################################################################") +print("AlphaComplex creation from generated points on sphere") + + +# Generate a circle: 50 points; dim 2; radius 1 +points = sphere.generate_random_points(50, 2, 1) + +# Create an alpha complex +alpha_complex = AlphaComplex(points=points) +simplex_tree = alpha_complex.create_simplex_tree() + +result_str = 'Alpha complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ + repr(simplex_tree.num_simplices()) + ' simplices - ' + \ + repr(simplex_tree.num_vertices()) + ' vertices.' +print(result_str) + diff --git a/src/python/gudhi/datasets/generators/__init__.py b/src/python/gudhi/datasets/generators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/gudhi/datasets/generators/sphere.cc b/src/python/gudhi/datasets/generators/sphere.cc new file mode 100644 index 00000000..79392ef0 --- /dev/null +++ b/src/python/gudhi/datasets/generators/sphere.cc @@ -0,0 +1,61 @@ +/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + * Author(s): Hind Montassif + * + * Copyright (C) 2021 Inria + * + * Modification(s): + * - YYYY/MM Author: Description of the modification + */ + +#include +#include + +#include +#include + +#include + +namespace py = pybind11; + + +typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; + +py::array_t generate_points_on_sphere(size_t num_points, int dim, double radius) { + + py::array_t points({num_points, (size_t)dim}); + + py::buffer_info buf = points.request(); + double *ptr = static_cast(buf.ptr); + + GUDHI_CHECK(num_points == buf.shape[0], "Py array first dimension not matching num_points on sphere"); + GUDHI_CHECK(dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); + + + py::gil_scoped_release release; + auto points_generated = Gudhi::generate_points_on_sphere_d(num_points, dim, radius); + + for (size_t i = 0; i < num_points; i++) + for (int j = 0; j < dim; j++) + ptr[i*dim+j] = points_generated[i][j]; + + return points; +} + +PYBIND11_MODULE(sphere, m) { + m.attr("__license__") = "LGPL v3"; + m.def("generate_random_points", &generate_points_on_sphere, + py::arg("num_points"), py::arg("dim"), py::arg("radius") = 1, + R"pbdoc( + Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d + + :param num_points: The number of points to be generated. + :type num_points: unsigned integer + :param dim: The dimension. + :type dim: integer + :param radius: The radius. + :type radius: float + :rtype: numpy array of float + :returns: the generated points on a sphere. + )pbdoc"); +} diff --git a/src/python/gudhi/random_point_generators.cc b/src/python/gudhi/random_point_generators.cc deleted file mode 100644 index 6eb40429..00000000 --- a/src/python/gudhi/random_point_generators.cc +++ /dev/null @@ -1,61 +0,0 @@ -/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. - * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. - * Author(s): Hind Montassif - * - * Copyright (C) 2021 Inria - * - * Modification(s): - * - YYYY/MM Author: Description of the modification - */ - -#include -#include - -#include -#include - -#include - -namespace py = pybind11; - - -typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; - -py::array_t generate_points_on_sphere(size_t num_points, int dim, double radius) { - - py::array_t points({num_points, (size_t)dim}); - - py::buffer_info buf = points.request(); - double *ptr = static_cast(buf.ptr); - - GUDHI_CHECK(num_points == buf.shape[0], "Py array first dimension not matching num_points on sphere"); - GUDHI_CHECK(dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); - - - py::gil_scoped_release release; - auto points_generated = Gudhi::generate_points_on_sphere_d(num_points, dim, radius); - - for (size_t i = 0; i < num_points; i++) - for (int j = 0; j < dim; j++) - ptr[i*dim+j] = points_generated[i][j]; - - return points; -} - -PYBIND11_MODULE(random_point_generators, m) { - m.attr("__license__") = "LGPL v3"; - m.def("generate_points_on_sphere_d", &generate_points_on_sphere, - py::arg("num_points"), py::arg("dim"), py::arg("radius") = 1, - R"pbdoc( - Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d - - :param num_points: The number of points to be generated. - :type num_points: unsigned integer - :param dim: The dimension. - :type dim: integer - :param radius: The radius. - :type radius: float - :rtype: numpy array of float - :returns: the generated points on a sphere. - )pbdoc"); -} -- cgit v1.2.3 From e498bc8f838ab0cc433f9f67206088064b52b6fa Mon Sep 17 00:00:00 2001 From: tlacombe Date: Wed, 28 Apr 2021 10:54:55 +0200 Subject: enhancing the doc --- src/python/gudhi/wasserstein/wasserstein.py | 37 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 61505d03..5196b280 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -210,32 +210,37 @@ def _warn_infty(matching): return np.inf - def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enable_autodiff=False, keep_essential_parts=True): ''' - :param X: (n x 2) numpy.array encoding the first diagram. Can contain essential parts (points with infinite - coordinates). - :param Y: (m x 2) numpy.array encoding the second diagram. - :param matching: if True, computes and returns the optimal matching between X and Y, encoded as - a (n x 2) np.array [...[i,j]...], meaning the i-th point in X is matched to - the j-th point in Y, with the convention (-1) represents the diagonal. - Note that if the cost is +inf (essential parts have different number of points, - then the optimal matching will be set to `None`. - :param order: exponent for Wasserstein. Default value is 1. + Compute the Wasserstein distance between persistence diagram using Python Optimal Transport backend. + Diagrams can contain points with infinity coordinates (essential parts). + Points with (-inf,-inf) and (+inf,+inf) coordinates are considered as belonging to the diagonal. + If the distance between two diagrams is +inf (which happens if the cardinalities of essential + parts differ) and optimal matching is required, it will be set to ``None``. + + :param X: The first diagram. + :type X: n x 2 numpy.array + :param Y: The second diagram. + :type Y: m x 2 numpy.array + :param matching: if ``True``, computes and returns the optimal matching between X and Y, encoded as + a (n x 2) np.array [...[i,j]...], meaning the i-th point in X is matched to + the j-th point in Y, with the convention that (-1) represents the diagonal. + :param order: Wasserstein exponent W_q + :type order: float :param internal_p: Ground metric on the (upper-half) plane (i.e. norm L^p in R^2). - Default value is `np.inf`. - :param enable_autodiff: If X and Y are torch.tensor or tensorflow.Tensor, make the computation + :type internal_p: float + :param enable_autodiff: If X and Y are ``torch.tensor`` or ``tensorflow.Tensor``, make the computation transparent to automatic differentiation. This requires the package EagerPy and is currently incompatible - with `matching=True` and with `keep_essential_parts=True`. + with ``matching=True`` and with ``keep_essential_parts=True``. .. note:: This considers the function defined on the coordinates of the off-diagonal finite points of X and Y and lets the various frameworks compute its gradient. It never pulls new points from the diagonal. :type enable_autodiff: bool - :param keep_essential_parts: If False, only considers the finite points in the diagrams. - Otherwise, computes the distance between the essential parts separately. + :param keep_essential_parts: If ``False``, only considers the finite points in the diagrams. + Otherwise, include essential parts in cost and matching computation. :type keep_essential_parts: bool - :returns: the Wasserstein distance of order q (1 <= q < infinity) between persistence diagrams with + :returns: The Wasserstein distance of order q (1 <= q < infinity) between persistence diagrams with respect to the internal_p-norm as ground metric. If matching is set to True, also returns the optimal matching between X and Y. If cost is +inf, any matching is optimal and thus it returns `None` instead. -- cgit v1.2.3 From 9e59ca4f4497969ae6d159407e913c31dba7d6c5 Mon Sep 17 00:00:00 2001 From: tlacombe Date: Wed, 28 Apr 2021 10:56:45 +0200 Subject: enhancing the doc2 --- src/python/gudhi/wasserstein/wasserstein.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/wasserstein/wasserstein.py b/src/python/gudhi/wasserstein/wasserstein.py index 5196b280..dc18806e 100644 --- a/src/python/gudhi/wasserstein/wasserstein.py +++ b/src/python/gudhi/wasserstein/wasserstein.py @@ -226,7 +226,7 @@ def wasserstein_distance(X, Y, matching=False, order=1., internal_p=np.inf, enab :param matching: if ``True``, computes and returns the optimal matching between X and Y, encoded as a (n x 2) np.array [...[i,j]...], meaning the i-th point in X is matched to the j-th point in Y, with the convention that (-1) represents the diagonal. - :param order: Wasserstein exponent W_q + :param order: Wasserstein exponent q (1 <= q < infinity). :type order: float :param internal_p: Ground metric on the (upper-half) plane (i.e. norm L^p in R^2). :type internal_p: float -- cgit v1.2.3 From d68ddc94bd82c48a4433ae0b3b1b3f10c167ed0b Mon Sep 17 00:00:00 2001 From: Hind Date: Wed, 28 Apr 2021 14:05:03 +0200 Subject: Add points (dataset type) before the underlying model (sphere) as a module --- src/python/CMakeLists.txt | 6 +-- ...plex_from_generated_points_on_sphere_example.py | 2 +- src/python/gudhi/datasets/generators/__init__.py | 0 .../gudhi/datasets/generators/points/__init__.py | 0 .../gudhi/datasets/generators/points/sphere.cc | 61 ++++++++++++++++++++++ src/python/gudhi/datasets/generators/sphere.cc | 61 ---------------------- 6 files changed, 65 insertions(+), 65 deletions(-) delete mode 100644 src/python/gudhi/datasets/generators/__init__.py create mode 100644 src/python/gudhi/datasets/generators/points/__init__.py create mode 100644 src/python/gudhi/datasets/generators/points/sphere.cc delete mode 100644 src/python/gudhi/datasets/generators/sphere.cc (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index bcdd0741..ef9dc3ab 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -46,7 +46,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'bottleneck', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'hera', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'clustering', ") - set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets/generators', ") + set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets/generators/points', ") endif() if(CYTHON_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'off_reader', ") @@ -152,7 +152,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/wasserstein', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/bottleneck', ") if (NOT CGAL_VERSION VERSION_LESS 4.11.0) - set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'datasets/generators/sphere', ") + set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'datasets/generators/points/sphere', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'bottleneck', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'nerve_gic', ") endif () @@ -264,7 +264,7 @@ if(PYTHONINTERP_FOUND) file(COPY "gudhi/weighted_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/dtm_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/hera/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/hera") - file(COPY "gudhi/datasets/generators/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/datasets/generators") + file(COPY "gudhi/datasets/generators/points/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/datasets/generators/points") # Some files for pip package diff --git a/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py b/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py index 2de9ec08..2b023bbe 100644 --- a/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py +++ b/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from gudhi.datasets.generators import sphere +from gudhi.datasets.generators.points import sphere from gudhi import AlphaComplex diff --git a/src/python/gudhi/datasets/generators/__init__.py b/src/python/gudhi/datasets/generators/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/python/gudhi/datasets/generators/points/__init__.py b/src/python/gudhi/datasets/generators/points/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/gudhi/datasets/generators/points/sphere.cc b/src/python/gudhi/datasets/generators/points/sphere.cc new file mode 100644 index 00000000..79392ef0 --- /dev/null +++ b/src/python/gudhi/datasets/generators/points/sphere.cc @@ -0,0 +1,61 @@ +/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + * Author(s): Hind Montassif + * + * Copyright (C) 2021 Inria + * + * Modification(s): + * - YYYY/MM Author: Description of the modification + */ + +#include +#include + +#include +#include + +#include + +namespace py = pybind11; + + +typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; + +py::array_t generate_points_on_sphere(size_t num_points, int dim, double radius) { + + py::array_t points({num_points, (size_t)dim}); + + py::buffer_info buf = points.request(); + double *ptr = static_cast(buf.ptr); + + GUDHI_CHECK(num_points == buf.shape[0], "Py array first dimension not matching num_points on sphere"); + GUDHI_CHECK(dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); + + + py::gil_scoped_release release; + auto points_generated = Gudhi::generate_points_on_sphere_d(num_points, dim, radius); + + for (size_t i = 0; i < num_points; i++) + for (int j = 0; j < dim; j++) + ptr[i*dim+j] = points_generated[i][j]; + + return points; +} + +PYBIND11_MODULE(sphere, m) { + m.attr("__license__") = "LGPL v3"; + m.def("generate_random_points", &generate_points_on_sphere, + py::arg("num_points"), py::arg("dim"), py::arg("radius") = 1, + R"pbdoc( + Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d + + :param num_points: The number of points to be generated. + :type num_points: unsigned integer + :param dim: The dimension. + :type dim: integer + :param radius: The radius. + :type radius: float + :rtype: numpy array of float + :returns: the generated points on a sphere. + )pbdoc"); +} diff --git a/src/python/gudhi/datasets/generators/sphere.cc b/src/python/gudhi/datasets/generators/sphere.cc deleted file mode 100644 index 79392ef0..00000000 --- a/src/python/gudhi/datasets/generators/sphere.cc +++ /dev/null @@ -1,61 +0,0 @@ -/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. - * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. - * Author(s): Hind Montassif - * - * Copyright (C) 2021 Inria - * - * Modification(s): - * - YYYY/MM Author: Description of the modification - */ - -#include -#include - -#include -#include - -#include - -namespace py = pybind11; - - -typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; - -py::array_t generate_points_on_sphere(size_t num_points, int dim, double radius) { - - py::array_t points({num_points, (size_t)dim}); - - py::buffer_info buf = points.request(); - double *ptr = static_cast(buf.ptr); - - GUDHI_CHECK(num_points == buf.shape[0], "Py array first dimension not matching num_points on sphere"); - GUDHI_CHECK(dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); - - - py::gil_scoped_release release; - auto points_generated = Gudhi::generate_points_on_sphere_d(num_points, dim, radius); - - for (size_t i = 0; i < num_points; i++) - for (int j = 0; j < dim; j++) - ptr[i*dim+j] = points_generated[i][j]; - - return points; -} - -PYBIND11_MODULE(sphere, m) { - m.attr("__license__") = "LGPL v3"; - m.def("generate_random_points", &generate_points_on_sphere, - py::arg("num_points"), py::arg("dim"), py::arg("radius") = 1, - R"pbdoc( - Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d - - :param num_points: The number of points to be generated. - :type num_points: unsigned integer - :param dim: The dimension. - :type dim: integer - :param radius: The radius. - :type radius: float - :rtype: numpy array of float - :returns: the generated points on a sphere. - )pbdoc"); -} -- cgit v1.2.3 From 8cf6bbe7e2bd7c71cb44872aba772a1c4caf06a9 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 29 Apr 2021 11:23:43 +0200 Subject: numpy.take_along_axis used in knn requires numpy>=1.15.0 --- .github/build-requirements.txt | 5 ----- .github/test-requirements.txt | 15 --------------- src/python/doc/installation.rst | 2 +- src/python/setup.py.in | 4 ++-- 4 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 .github/build-requirements.txt delete mode 100644 .github/test-requirements.txt (limited to 'src/python') diff --git a/.github/build-requirements.txt b/.github/build-requirements.txt deleted file mode 100644 index 7de60d23..00000000 --- a/.github/build-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -setuptools -wheel -numpy -Cython -pybind11 \ No newline at end of file diff --git a/.github/test-requirements.txt b/.github/test-requirements.txt deleted file mode 100644 index d0803574..00000000 --- a/.github/test-requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -pytest -pytest-cov -sphinx -sphinxcontrib-bibtex==1.0.0 -sphinx-paramlinks -matplotlib -scipy -scikit-learn -POT -tensorflow -tensorflow-addons -torch<1.5 -pykeops -hnswlib -eagerpy diff --git a/src/python/doc/installation.rst b/src/python/doc/installation.rst index 2881055f..9c16b04e 100644 --- a/src/python/doc/installation.rst +++ b/src/python/doc/installation.rst @@ -41,7 +41,7 @@ there. The library uses c++14 and requires `Boost `_ :math:`\geq` 1.56.0, `CMake `_ :math:`\geq` 3.5 to generate makefiles, -`NumPy `_, `Cython `_ and +`NumPy `_ :math:`\geq` 1.15.0, `Cython `_ and `pybind11 `_ to compile the GUDHI Python module. It is a multi-platform library and compiles on Linux, Mac OSX and Visual diff --git a/src/python/setup.py.in b/src/python/setup.py.in index 65f5446e..759ec8d8 100644 --- a/src/python/setup.py.in +++ b/src/python/setup.py.in @@ -85,7 +85,7 @@ setup( long_description_content_type='text/x-rst', long_description=long_description, ext_modules = ext_modules, - install_requires = ['numpy >= 1.9',], - setup_requires = ['cython','numpy >= 1.9','pybind11',], + install_requires = ['numpy >= 1.15.0',], + setup_requires = ['cython','numpy >= 1.15.0','pybind11',], package_data={"": ["*.dll"], }, ) -- cgit v1.2.3 From 1ef113ff6f5db7288e4dc4c18c053b18d90dbf1a Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Fri, 30 Apr 2021 11:17:35 +0200 Subject: First version of points generation on torus --- src/python/CMakeLists.txt | 4 ++ .../gudhi/datasets/generators/points/__init__.py | 0 .../gudhi/datasets/generators/points/_torus.cc | 70 ++++++++++++++++++++++ .../gudhi/datasets/generators/points/torus.py | 52 ++++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 src/python/gudhi/datasets/generators/points/__init__.py create mode 100644 src/python/gudhi/datasets/generators/points/_torus.cc create mode 100644 src/python/gudhi/datasets/generators/points/torus.py (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index a1440cbc..1b9db2b5 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -46,6 +46,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'bottleneck', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'hera', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'clustering', ") + set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets/generators/points', ") endif() if(CYTHON_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'off_reader', ") @@ -151,6 +152,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/wasserstein', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/bottleneck', ") if (NOT CGAL_VERSION VERSION_LESS 4.11.0) + set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'datasets/generators/points/_torus', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'bottleneck', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'nerve_gic', ") endif () @@ -262,6 +264,8 @@ if(PYTHONINTERP_FOUND) file(COPY "gudhi/weighted_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/dtm_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/hera/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/hera") + file(COPY "gudhi/datasets/generators/points/" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/datasets/generators/points/" FILES_MATCHING PATTERN "*.py") + # Some files for pip package file(COPY "introduction.rst" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/") diff --git a/src/python/gudhi/datasets/generators/points/__init__.py b/src/python/gudhi/datasets/generators/points/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/gudhi/datasets/generators/points/_torus.cc b/src/python/gudhi/datasets/generators/points/_torus.cc new file mode 100644 index 00000000..21638bb8 --- /dev/null +++ b/src/python/gudhi/datasets/generators/points/_torus.cc @@ -0,0 +1,70 @@ +/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + * Author(s): Hind Montassif + * + * Copyright (C) 2021 Inria + * + * Modification(s): + * - YYYY/MM Author: Description of the modification + */ + +#include +#include + +#include +#include + +#include + +namespace py = pybind11; + + +typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; + + +py::array_t generate_points_on_torus(size_t num_points, int dim, bool uniform) { + + std::vector points_generated; + + { + py::gil_scoped_release release; + points_generated = Gudhi::generate_points_on_torus_d(num_points, dim, uniform); + } + + size_t npoints = points_generated.size(); + + py::print("points generated size: "); + py::print(points_generated.size()); + py::print(points_generated[0].size()); + + GUDHI_CHECK(2*dim == points_generated[0].size(), "Py array second dimension not matching the double ambient space dimension"); + + py::array_t points({npoints, (size_t)2*dim}); + + py::buffer_info buf = points.request(); + double *ptr = static_cast(buf.ptr); + + for (size_t i = 0; i < npoints; i++) + for (int j = 0; j < 2*dim; j++) + ptr[i*(2*dim)+j] = points_generated[i][j]; + + return points; +} + +PYBIND11_MODULE(_torus, m) { + m.attr("__license__") = "LGPL v3"; + m.def("generate_random_points", &generate_points_on_torus, + py::arg("num_points"), py::arg("dim"), py::arg("uniform") = false, + R"pbdoc( + Generate random i.i.d. points on a d-torus in R^2d + + :param num_points: The number of points to be generated. + :type num_points: unsigned integer + :param dim: The dimension. + :type dim: integer + :param uniform: A flag to define if the points generation is uniform (generated as a grid). + :type uniform: bool + :rtype: numpy array of float + :returns: the generated points on a torus. + )pbdoc"); +} diff --git a/src/python/gudhi/datasets/generators/points/torus.py b/src/python/gudhi/datasets/generators/points/torus.py new file mode 100644 index 00000000..2de696b2 --- /dev/null +++ b/src/python/gudhi/datasets/generators/points/torus.py @@ -0,0 +1,52 @@ +# This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. +# See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. +# Author(s): Hind Montassif +# +# Copyright (C) 2021 Inria +# +# Modification(s): +# - YYYY/MM Author: Description of the modification + +import numpy as np +import math + + +def generate_random_points(num_points, dim): + + # Generate random angles of size num_points*dim + alpha = 2*math.pi*np.random.rand(num_points*dim) + + # Based on angles, construct points of size num_points*dim on a circle and reshape the result in a num_points*2*dim array + array_points = np.asarray([[np.cos(a), np.sin(a)] for a in alpha]).ravel().reshape(num_points, 2*dim) + + return array_points + + +def generate_grid_points(num_points, dim): + + num_points_grid = (int(num_points**(1./dim)))**dim + + alpha = 2*math.pi*np.random.rand(num_points_grid*dim) + + array_points = np.asarray([[np.cos(a), np.sin(a)] for a in alpha]).ravel().reshape(num_points_grid, 2*dim) + + return array_points + +def generate_points(num_points, dim, sample='random'): + if sample == 'random': + print("Sample is random") + npoints = num_points + elif sample == 'grid': + print("Sample is grid") + npoints = (int(num_points**(1./dim)))**dim + else: + print("Sample type '{}' is not supported".format(sample)) + return + + # Generate random angles of size num_points*dim + alpha = 2*math.pi*np.random.rand(npoints*dim) + + # Based on angles, construct points of size num_points*dim on a circle and reshape the result in a num_points*2*dim array + array_points = np.asarray([[np.cos(a), np.sin(a)] for a in alpha]).ravel().reshape(npoints, 2*dim) + + return array_points -- cgit v1.2.3 From aeff21a712e488a2948d7c12f67f2f11b047ada8 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Tue, 4 May 2021 10:40:03 +0200 Subject: Set Simplex_tree_interface_full_featured constructor name (Simplex_tree) to the class name --- src/python/gudhi/simplex_tree.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 000323af..3b8ea4f9 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -44,7 +44,7 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": cdef cppclass Simplex_tree_interface_full_featured "Gudhi::Simplex_tree_interface": - Simplex_tree() nogil + Simplex_tree_interface_full_featured() nogil double simplex_filtration(vector[int] simplex) nogil void assign_simplex_filtration(vector[int] simplex, double filtration) nogil void initialize_filtration() nogil -- cgit v1.2.3 From 2b694f9beae0e5fa78ae5b8923e7f2905c58777f Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Wed, 5 May 2021 13:58:30 +0200 Subject: Add __init__ files at every level in modules Remove last modules level and add sample type argument Rename num_points to n_samples --- src/python/CMakeLists.txt | 7 +-- ...plex_from_generated_points_on_sphere_example.py | 7 +-- src/python/gudhi/datasets/__init__.py | 0 src/python/gudhi/datasets/generators/__init__.py | 0 src/python/gudhi/datasets/generators/points.cc | 68 ++++++++++++++++++++++ .../gudhi/datasets/generators/points/__init__.py | 0 .../gudhi/datasets/generators/points/sphere.cc | 61 ------------------- 7 files changed, 74 insertions(+), 69 deletions(-) create mode 100644 src/python/gudhi/datasets/__init__.py create mode 100644 src/python/gudhi/datasets/generators/__init__.py create mode 100644 src/python/gudhi/datasets/generators/points.cc delete mode 100644 src/python/gudhi/datasets/generators/points/__init__.py delete mode 100644 src/python/gudhi/datasets/generators/points/sphere.cc (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index ef9dc3ab..8dd4ea5d 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -46,7 +46,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'bottleneck', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'hera', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'clustering', ") - set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets/generators/points', ") + set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets', ") endif() if(CYTHON_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'off_reader', ") @@ -152,7 +152,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/wasserstein', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/bottleneck', ") if (NOT CGAL_VERSION VERSION_LESS 4.11.0) - set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'datasets/generators/points/sphere', ") + set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'datasets/generators/points', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'bottleneck', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'nerve_gic', ") endif () @@ -264,8 +264,7 @@ if(PYTHONINTERP_FOUND) file(COPY "gudhi/weighted_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/dtm_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/hera/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/hera") - file(COPY "gudhi/datasets/generators/points/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/datasets/generators/points") - + file(COPY "gudhi/datasets" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi" FILES_MATCHING PATTERN "*.py") # Some files for pip package file(COPY "introduction.rst" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/") diff --git a/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py b/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py index 2b023bbe..e73584d3 100644 --- a/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py +++ b/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from gudhi.datasets.generators.points import sphere +from gudhi.datasets.generators import points from gudhi import AlphaComplex @@ -22,11 +22,10 @@ print("#####################################################################") print("AlphaComplex creation from generated points on sphere") -# Generate a circle: 50 points; dim 2; radius 1 -points = sphere.generate_random_points(50, 2, 1) +gen_points = points.sphere(n_samples = 50, dim = 2, radius = 1, sample = "random") # Create an alpha complex -alpha_complex = AlphaComplex(points=points) +alpha_complex = AlphaComplex(points = gen_points) simplex_tree = alpha_complex.create_simplex_tree() result_str = 'Alpha complex is of dimension ' + repr(simplex_tree.dimension()) + ' - ' + \ diff --git a/src/python/gudhi/datasets/__init__.py b/src/python/gudhi/datasets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/gudhi/datasets/generators/__init__.py b/src/python/gudhi/datasets/generators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/gudhi/datasets/generators/points.cc b/src/python/gudhi/datasets/generators/points.cc new file mode 100644 index 00000000..f02c7d73 --- /dev/null +++ b/src/python/gudhi/datasets/generators/points.cc @@ -0,0 +1,68 @@ +/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + * Author(s): Hind Montassif + * + * Copyright (C) 2021 Inria + * + * Modification(s): + * - YYYY/MM Author: Description of the modification + */ + +#include +#include + +#include +#include + +#include + +namespace py = pybind11; + + +typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; + +py::array_t generate_points_on_sphere(size_t n_samples, int dim, double radius, std::string sample) { + + if (sample != "random") { + throw pybind11::value_error("sample type is not supported"); + } + + py::array_t points({n_samples, (size_t)dim}); + + py::buffer_info buf = points.request(); + double *ptr = static_cast(buf.ptr); + + GUDHI_CHECK(n_samples == buf.shape[0], "Py array first dimension not matching n_samples on sphere"); + GUDHI_CHECK(dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); + + + py::gil_scoped_release release; + auto points_generated = Gudhi::generate_points_on_sphere_d(n_samples, dim, radius); + + for (size_t i = 0; i < n_samples; i++) + for (int j = 0; j < dim; j++) + ptr[i*dim+j] = points_generated[i][j]; + + return points; +} + +PYBIND11_MODULE(points, m) { + m.attr("__license__") = "LGPL v3"; + m.def("sphere", &generate_points_on_sphere, + py::arg("n_samples"), py::arg("dim"), + py::arg("radius") = 1, py::arg("sample") = "random", + R"pbdoc( + Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d + + :param n_samples: The number of points to be generated. + :type n_samples: integer + :param dim: The ambient dimension d. + :type dim: integer + :param radius: The radius. + :type radius: float + :param sample: The sample type. + :type sample: string + :rtype: numpy array of float + :returns: the generated points on a sphere. + )pbdoc"); +} diff --git a/src/python/gudhi/datasets/generators/points/__init__.py b/src/python/gudhi/datasets/generators/points/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/python/gudhi/datasets/generators/points/sphere.cc b/src/python/gudhi/datasets/generators/points/sphere.cc deleted file mode 100644 index 79392ef0..00000000 --- a/src/python/gudhi/datasets/generators/points/sphere.cc +++ /dev/null @@ -1,61 +0,0 @@ -/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. - * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. - * Author(s): Hind Montassif - * - * Copyright (C) 2021 Inria - * - * Modification(s): - * - YYYY/MM Author: Description of the modification - */ - -#include -#include - -#include -#include - -#include - -namespace py = pybind11; - - -typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; - -py::array_t generate_points_on_sphere(size_t num_points, int dim, double radius) { - - py::array_t points({num_points, (size_t)dim}); - - py::buffer_info buf = points.request(); - double *ptr = static_cast(buf.ptr); - - GUDHI_CHECK(num_points == buf.shape[0], "Py array first dimension not matching num_points on sphere"); - GUDHI_CHECK(dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); - - - py::gil_scoped_release release; - auto points_generated = Gudhi::generate_points_on_sphere_d(num_points, dim, radius); - - for (size_t i = 0; i < num_points; i++) - for (int j = 0; j < dim; j++) - ptr[i*dim+j] = points_generated[i][j]; - - return points; -} - -PYBIND11_MODULE(sphere, m) { - m.attr("__license__") = "LGPL v3"; - m.def("generate_random_points", &generate_points_on_sphere, - py::arg("num_points"), py::arg("dim"), py::arg("radius") = 1, - R"pbdoc( - Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d - - :param num_points: The number of points to be generated. - :type num_points: unsigned integer - :param dim: The dimension. - :type dim: integer - :param radius: The radius. - :type radius: float - :rtype: numpy array of float - :returns: the generated points on a sphere. - )pbdoc"); -} -- cgit v1.2.3 From 5c140bbdae08561ce69f0cc05841eb1467aa8eab Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Fri, 7 May 2021 15:11:24 +0200 Subject: Use PI constant from numpy instead of math Modify grid points generation incorrect formula --- .../gudhi/datasets/generators/points/torus.py | 30 ++++++++-------------- 1 file changed, 10 insertions(+), 20 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/datasets/generators/points/torus.py b/src/python/gudhi/datasets/generators/points/torus.py index 2de696b2..5a2b9016 100644 --- a/src/python/gudhi/datasets/generators/points/torus.py +++ b/src/python/gudhi/datasets/generators/points/torus.py @@ -8,45 +8,35 @@ # - YYYY/MM Author: Description of the modification import numpy as np -import math - +import itertools def generate_random_points(num_points, dim): # Generate random angles of size num_points*dim - alpha = 2*math.pi*np.random.rand(num_points*dim) + alpha = 2*np.pi*np.random.rand(num_points*dim) - # Based on angles, construct points of size num_points*dim on a circle and reshape the result in a num_points*2*dim array - array_points = np.asarray([[np.cos(a), np.sin(a)] for a in alpha]).ravel().reshape(num_points, 2*dim) + # Based on angles, construct points of size num_points*dim on a circle and reshape the result in a num_points*2*dim array + array_points = np.column_stack([np.cos(alpha), np.sin(alpha)]).reshape(-1, 2*dim) return array_points - def generate_grid_points(num_points, dim): - num_points_grid = (int(num_points**(1./dim)))**dim + num_points_grid = int(num_points**(1./dim)) + alpha = np.linspace(0, 2*np.pi, num_points_grid, endpoint=False) - alpha = 2*math.pi*np.random.rand(num_points_grid*dim) - - array_points = np.asarray([[np.cos(a), np.sin(a)] for a in alpha]).ravel().reshape(num_points_grid, 2*dim) + array_points_inter = np.column_stack([np.cos(alpha), np.sin(alpha)]) + array_points = np.array(list(itertools.product(array_points_inter, repeat=dim))).reshape(-1, 2*dim) return array_points def generate_points(num_points, dim, sample='random'): if sample == 'random': print("Sample is random") - npoints = num_points + generate_random_points(num_points, dim) elif sample == 'grid': print("Sample is grid") - npoints = (int(num_points**(1./dim)))**dim + generate_grid_points(num_points, dim) else: print("Sample type '{}' is not supported".format(sample)) return - - # Generate random angles of size num_points*dim - alpha = 2*math.pi*np.random.rand(npoints*dim) - - # Based on angles, construct points of size num_points*dim on a circle and reshape the result in a num_points*2*dim array - array_points = np.asarray([[np.cos(a), np.sin(a)] for a in alpha]).ravel().reshape(npoints, 2*dim) - - return array_points -- cgit v1.2.3 From a1497289e6808d247f3b2be69b97dc9053e2b4d1 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Fri, 7 May 2021 15:30:08 +0200 Subject: Replace num_points with n_samples to be consistent with sphere --- .../gudhi/datasets/generators/points/_torus.cc | 18 +++++++----------- src/python/gudhi/datasets/generators/points/torus.py | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/datasets/generators/points/_torus.cc b/src/python/gudhi/datasets/generators/points/_torus.cc index 21638bb8..f4b4f14e 100644 --- a/src/python/gudhi/datasets/generators/points/_torus.cc +++ b/src/python/gudhi/datasets/generators/points/_torus.cc @@ -22,22 +22,18 @@ namespace py = pybind11; typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; -py::array_t generate_points_on_torus(size_t num_points, int dim, bool uniform) { +py::array_t generate_points_on_torus(size_t n_samples, int dim, bool uniform) { std::vector points_generated; { py::gil_scoped_release release; - points_generated = Gudhi::generate_points_on_torus_d(num_points, dim, uniform); + points_generated = Gudhi::generate_points_on_torus_d(n_samples, dim, uniform); } size_t npoints = points_generated.size(); - py::print("points generated size: "); - py::print(points_generated.size()); - py::print(points_generated[0].size()); - - GUDHI_CHECK(2*dim == points_generated[0].size(), "Py array second dimension not matching the double ambient space dimension"); + GUDHI_CHECK(2*dim == points_generated[0].size(), "Py array second dimension not matching the double torus dimension"); py::array_t points({npoints, (size_t)2*dim}); @@ -54,15 +50,15 @@ py::array_t generate_points_on_torus(size_t num_points, int dim, bool un PYBIND11_MODULE(_torus, m) { m.attr("__license__") = "LGPL v3"; m.def("generate_random_points", &generate_points_on_torus, - py::arg("num_points"), py::arg("dim"), py::arg("uniform") = false, + py::arg("n_samples"), py::arg("dim"), py::arg("uniform") = false, R"pbdoc( Generate random i.i.d. points on a d-torus in R^2d - :param num_points: The number of points to be generated. - :type num_points: unsigned integer + :param n_samples: The number of points to be generated. + :type n_samples: integer :param dim: The dimension. :type dim: integer - :param uniform: A flag to define if the points generation is uniform (generated as a grid). + :param uniform: A flag to define if the points generation is uniform (i.e generated as a grid). :type uniform: bool :rtype: numpy array of float :returns: the generated points on a torus. diff --git a/src/python/gudhi/datasets/generators/points/torus.py b/src/python/gudhi/datasets/generators/points/torus.py index 5a2b9016..1df0a930 100644 --- a/src/python/gudhi/datasets/generators/points/torus.py +++ b/src/python/gudhi/datasets/generators/points/torus.py @@ -10,33 +10,33 @@ import numpy as np import itertools -def generate_random_points(num_points, dim): +def generate_random_points(n_samples, dim): - # Generate random angles of size num_points*dim - alpha = 2*np.pi*np.random.rand(num_points*dim) + # Generate random angles of size n_samples*dim + alpha = 2*np.pi*np.random.rand(n_samples*dim) - # Based on angles, construct points of size num_points*dim on a circle and reshape the result in a num_points*2*dim array + # Based on angles, construct points of size n_samples*dim on a circle and reshape the result in a n_samples*2*dim array array_points = np.column_stack([np.cos(alpha), np.sin(alpha)]).reshape(-1, 2*dim) return array_points -def generate_grid_points(num_points, dim): +def generate_grid_points(n_samples, dim): - num_points_grid = int(num_points**(1./dim)) - alpha = np.linspace(0, 2*np.pi, num_points_grid, endpoint=False) + n_samples_grid = int(n_samples**(1./dim)) + alpha = np.linspace(0, 2*np.pi, n_samples_grid, endpoint=False) array_points_inter = np.column_stack([np.cos(alpha), np.sin(alpha)]) array_points = np.array(list(itertools.product(array_points_inter, repeat=dim))).reshape(-1, 2*dim) return array_points -def generate_points(num_points, dim, sample='random'): +def generate_points(n_samples, dim, sample='random'): if sample == 'random': print("Sample is random") - generate_random_points(num_points, dim) + generate_random_points(n_samples, dim) elif sample == 'grid': print("Sample is grid") - generate_grid_points(num_points, dim) + generate_grid_points(n_samples, dim) else: print("Sample type '{}' is not supported".format(sample)) return -- cgit v1.2.3 From 303b014508f849d8cb8a4369430068f54fa74c46 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Mon, 10 May 2021 10:27:57 +0200 Subject: Add __init__.py files at every module level Standardize functions to match the existing ones in sphere --- src/python/CMakeLists.txt | 6 +- src/python/gudhi/datasets/__init__.py | 0 src/python/gudhi/datasets/generators/__init__.py | 0 src/python/gudhi/datasets/generators/_points.cc | 66 ++++++++++++++++++++++ src/python/gudhi/datasets/generators/points.py | 42 ++++++++++++++ .../gudhi/datasets/generators/points/__init__.py | 0 .../gudhi/datasets/generators/points/_torus.cc | 66 ---------------------- .../gudhi/datasets/generators/points/torus.py | 42 -------------- 8 files changed, 111 insertions(+), 111 deletions(-) create mode 100644 src/python/gudhi/datasets/__init__.py create mode 100644 src/python/gudhi/datasets/generators/__init__.py create mode 100644 src/python/gudhi/datasets/generators/_points.cc create mode 100644 src/python/gudhi/datasets/generators/points.py delete mode 100644 src/python/gudhi/datasets/generators/points/__init__.py delete mode 100644 src/python/gudhi/datasets/generators/points/_torus.cc delete mode 100644 src/python/gudhi/datasets/generators/points/torus.py (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 1b9db2b5..e146fedc 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -46,7 +46,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'bottleneck', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'hera', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'clustering', ") - set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets/generators/points', ") + set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets', ") endif() if(CYTHON_FOUND) set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'off_reader', ") @@ -152,7 +152,7 @@ if(PYTHONINTERP_FOUND) set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/wasserstein', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'hera/bottleneck', ") if (NOT CGAL_VERSION VERSION_LESS 4.11.0) - set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'datasets/generators/points/_torus', ") + set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'datasets/generators/_points', ") set(GUDHI_PYBIND11_MODULES "${GUDHI_PYBIND11_MODULES}'bottleneck', ") set(GUDHI_CYTHON_MODULES "${GUDHI_CYTHON_MODULES}'nerve_gic', ") endif () @@ -264,7 +264,7 @@ if(PYTHONINTERP_FOUND) file(COPY "gudhi/weighted_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/dtm_rips_complex.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi") file(COPY "gudhi/hera/__init__.py" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/hera") - file(COPY "gudhi/datasets/generators/points/" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi/datasets/generators/points/" FILES_MATCHING PATTERN "*.py") + file(COPY "gudhi/datasets" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/gudhi" FILES_MATCHING PATTERN "*.py") # Some files for pip package diff --git a/src/python/gudhi/datasets/__init__.py b/src/python/gudhi/datasets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/gudhi/datasets/generators/__init__.py b/src/python/gudhi/datasets/generators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc new file mode 100644 index 00000000..561fd6d8 --- /dev/null +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -0,0 +1,66 @@ +/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + * Author(s): Hind Montassif + * + * Copyright (C) 2021 Inria + * + * Modification(s): + * - YYYY/MM Author: Description of the modification + */ + +#include +#include + +#include +#include + +#include + +namespace py = pybind11; + + +typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; + + +py::array_t generate_points_on_torus(size_t n_samples, int dim, bool uniform) { + + std::vector points_generated; + + { + py::gil_scoped_release release; + points_generated = Gudhi::generate_points_on_torus_d(n_samples, dim, uniform); + } + + size_t npoints = points_generated.size(); + + GUDHI_CHECK(2*dim == points_generated[0].size(), "Py array second dimension not matching the double torus dimension"); + + py::array_t points({npoints, (size_t)2*dim}); + + py::buffer_info buf = points.request(); + double *ptr = static_cast(buf.ptr); + + for (size_t i = 0; i < npoints; i++) + for (int j = 0; j < 2*dim; j++) + ptr[i*(2*dim)+j] = points_generated[i][j]; + + return points; +} + +PYBIND11_MODULE(_points, m) { + m.attr("__license__") = "LGPL v3"; + m.def("torus", &generate_points_on_torus, + py::arg("n_samples"), py::arg("dim"), py::arg("uniform") = false, + R"pbdoc( + Generate random i.i.d. points on a d-torus in R^2d + + :param n_samples: The number of points to be generated. + :type n_samples: integer + :param dim: The dimension. + :type dim: integer + :param uniform: A flag to define if the points generation is uniform (i.e generated as a grid). + :type uniform: bool + :rtype: numpy array of float + :returns: the generated points on a torus. + )pbdoc"); +} diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py new file mode 100644 index 00000000..d5a370ad --- /dev/null +++ b/src/python/gudhi/datasets/generators/points.py @@ -0,0 +1,42 @@ +# This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. +# See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. +# Author(s): Hind Montassif +# +# Copyright (C) 2021 Inria +# +# Modification(s): +# - YYYY/MM Author: Description of the modification + +import numpy as np +import itertools + +def _generate_random_points(n_samples, dim): + + # Generate random angles of size n_samples*dim + alpha = 2*np.pi*np.random.rand(n_samples*dim) + + # Based on angles, construct points of size n_samples*dim on a circle and reshape the result in a n_samples*2*dim array + array_points = np.column_stack([np.cos(alpha), np.sin(alpha)]).reshape(-1, 2*dim) + + return array_points + +def _generate_grid_points(n_samples, dim): + + n_samples_grid = int(n_samples**(1./dim)) + alpha = np.linspace(0, 2*np.pi, n_samples_grid, endpoint=False) + + array_points_inter = np.column_stack([np.cos(alpha), np.sin(alpha)]) + array_points = np.array(list(itertools.product(array_points_inter, repeat=dim))).reshape(-1, 2*dim) + + return array_points + +def torus(n_samples, dim, sample='random'): + if sample == 'random': + print("Sample is random") + return _generate_random_points(n_samples, dim) + elif sample == 'grid': + print("Sample is grid") + return _generate_grid_points(n_samples, dim) + else: + raise Exception("Sample type '{}' is not supported".format(sample)) + return diff --git a/src/python/gudhi/datasets/generators/points/__init__.py b/src/python/gudhi/datasets/generators/points/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/python/gudhi/datasets/generators/points/_torus.cc b/src/python/gudhi/datasets/generators/points/_torus.cc deleted file mode 100644 index f4b4f14e..00000000 --- a/src/python/gudhi/datasets/generators/points/_torus.cc +++ /dev/null @@ -1,66 +0,0 @@ -/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. - * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. - * Author(s): Hind Montassif - * - * Copyright (C) 2021 Inria - * - * Modification(s): - * - YYYY/MM Author: Description of the modification - */ - -#include -#include - -#include -#include - -#include - -namespace py = pybind11; - - -typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; - - -py::array_t generate_points_on_torus(size_t n_samples, int dim, bool uniform) { - - std::vector points_generated; - - { - py::gil_scoped_release release; - points_generated = Gudhi::generate_points_on_torus_d(n_samples, dim, uniform); - } - - size_t npoints = points_generated.size(); - - GUDHI_CHECK(2*dim == points_generated[0].size(), "Py array second dimension not matching the double torus dimension"); - - py::array_t points({npoints, (size_t)2*dim}); - - py::buffer_info buf = points.request(); - double *ptr = static_cast(buf.ptr); - - for (size_t i = 0; i < npoints; i++) - for (int j = 0; j < 2*dim; j++) - ptr[i*(2*dim)+j] = points_generated[i][j]; - - return points; -} - -PYBIND11_MODULE(_torus, m) { - m.attr("__license__") = "LGPL v3"; - m.def("generate_random_points", &generate_points_on_torus, - py::arg("n_samples"), py::arg("dim"), py::arg("uniform") = false, - R"pbdoc( - Generate random i.i.d. points on a d-torus in R^2d - - :param n_samples: The number of points to be generated. - :type n_samples: integer - :param dim: The dimension. - :type dim: integer - :param uniform: A flag to define if the points generation is uniform (i.e generated as a grid). - :type uniform: bool - :rtype: numpy array of float - :returns: the generated points on a torus. - )pbdoc"); -} diff --git a/src/python/gudhi/datasets/generators/points/torus.py b/src/python/gudhi/datasets/generators/points/torus.py deleted file mode 100644 index 1df0a930..00000000 --- a/src/python/gudhi/datasets/generators/points/torus.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. -# See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. -# Author(s): Hind Montassif -# -# Copyright (C) 2021 Inria -# -# Modification(s): -# - YYYY/MM Author: Description of the modification - -import numpy as np -import itertools - -def generate_random_points(n_samples, dim): - - # Generate random angles of size n_samples*dim - alpha = 2*np.pi*np.random.rand(n_samples*dim) - - # Based on angles, construct points of size n_samples*dim on a circle and reshape the result in a n_samples*2*dim array - array_points = np.column_stack([np.cos(alpha), np.sin(alpha)]).reshape(-1, 2*dim) - - return array_points - -def generate_grid_points(n_samples, dim): - - n_samples_grid = int(n_samples**(1./dim)) - alpha = np.linspace(0, 2*np.pi, n_samples_grid, endpoint=False) - - array_points_inter = np.column_stack([np.cos(alpha), np.sin(alpha)]) - array_points = np.array(list(itertools.product(array_points_inter, repeat=dim))).reshape(-1, 2*dim) - - return array_points - -def generate_points(n_samples, dim, sample='random'): - if sample == 'random': - print("Sample is random") - generate_random_points(n_samples, dim) - elif sample == 'grid': - print("Sample is grid") - generate_grid_points(n_samples, dim) - else: - print("Sample type '{}' is not supported".format(sample)) - return -- cgit v1.2.3 From 62510e70009ff2fc65028b88b56886fb53743e51 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Mon, 10 May 2021 10:58:36 +0200 Subject: Rename dim to ambient_dim for sphere (to be consistent with dim in torus) --- ..._complex_from_generated_points_on_sphere_example.py | 2 +- src/python/gudhi/datasets/generators/points.cc | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) (limited to 'src/python') diff --git a/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py b/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py index e73584d3..267e6436 100644 --- a/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py +++ b/src/python/example/alpha_complex_from_generated_points_on_sphere_example.py @@ -22,7 +22,7 @@ print("#####################################################################") print("AlphaComplex creation from generated points on sphere") -gen_points = points.sphere(n_samples = 50, dim = 2, radius = 1, sample = "random") +gen_points = points.sphere(n_samples = 50, ambient_dim = 2, radius = 1, sample = "random") # Create an alpha complex alpha_complex = AlphaComplex(points = gen_points) diff --git a/src/python/gudhi/datasets/generators/points.cc b/src/python/gudhi/datasets/generators/points.cc index f02c7d73..e2626b09 100644 --- a/src/python/gudhi/datasets/generators/points.cc +++ b/src/python/gudhi/datasets/generators/points.cc @@ -21,27 +21,27 @@ namespace py = pybind11; typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; -py::array_t generate_points_on_sphere(size_t n_samples, int dim, double radius, std::string sample) { +py::array_t generate_points_on_sphere(size_t n_samples, int ambient_dim, double radius, std::string sample) { if (sample != "random") { throw pybind11::value_error("sample type is not supported"); } - py::array_t points({n_samples, (size_t)dim}); + py::array_t points({n_samples, (size_t)ambient_dim}); py::buffer_info buf = points.request(); double *ptr = static_cast(buf.ptr); GUDHI_CHECK(n_samples == buf.shape[0], "Py array first dimension not matching n_samples on sphere"); - GUDHI_CHECK(dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); + GUDHI_CHECK(ambient_dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); py::gil_scoped_release release; - auto points_generated = Gudhi::generate_points_on_sphere_d(n_samples, dim, radius); + auto points_generated = Gudhi::generate_points_on_sphere_d(n_samples, ambient_dim, radius); for (size_t i = 0; i < n_samples; i++) - for (int j = 0; j < dim; j++) - ptr[i*dim+j] = points_generated[i][j]; + for (int j = 0; j < ambient_dim; j++) + ptr[i*ambient_dim+j] = points_generated[i][j]; return points; } @@ -49,15 +49,15 @@ py::array_t generate_points_on_sphere(size_t n_samples, int dim, double PYBIND11_MODULE(points, m) { m.attr("__license__") = "LGPL v3"; m.def("sphere", &generate_points_on_sphere, - py::arg("n_samples"), py::arg("dim"), + py::arg("n_samples"), py::arg("ambient_dim"), py::arg("radius") = 1, py::arg("sample") = "random", R"pbdoc( Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d :param n_samples: The number of points to be generated. :type n_samples: integer - :param dim: The ambient dimension d. - :type dim: integer + :param ambient_dim: The ambient dimension d. + :type ambient_dim: integer :param radius: The radius. :type radius: float :param sample: The sample type. -- cgit v1.2.3 From a571f198535b5ab5751eb55693371e9348aa1804 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Fri, 21 May 2021 17:32:43 +0200 Subject: Fix issue #489 RipsComplex out of bounds access --- ...ips_complex_diagram_persistence_from_distance_matrix_file_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py b/src/python/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py index 236d085d..9320d904 100755 --- a/src/python/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py +++ b/src/python/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py @@ -44,7 +44,7 @@ print("RipsComplex creation from distance matrix read in a csv file") message = "RipsComplex with max_edge_length=" + repr(args.max_edge_length) print(message) -distance_matrix = gudhi.read_lower_triangular_matrix_from_csv_file(csv_file=args.file) +distance_matrix = gudhi.read_lower_triangular_matrix_from_csv_file(csv_file=args.file, separator=',') rips_complex = gudhi.RipsComplex( distance_matrix=distance_matrix, max_edge_length=args.max_edge_length ) -- cgit v1.2.3 From 8b3c55502718e4c184d828151ee6f75fd2cfc9eb Mon Sep 17 00:00:00 2001 From: Hind-M Date: Tue, 25 May 2021 18:01:54 +0200 Subject: Add a separator argument that goes with the rips_complex_diagram_persistence_from_distance_matrix_file_example input file Specify explicitly the separator when using a specific input file --- src/common/test/test_distance_matrix_reader.cpp | 2 +- src/python/CMakeLists.txt | 2 +- ..._complex_diagram_persistence_from_distance_matrix_file_example.py | 5 +++-- src/python/test/test_reader_utils.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/common/test/test_distance_matrix_reader.cpp b/src/common/test/test_distance_matrix_reader.cpp index 73be8104..92e899b8 100644 --- a/src/common/test/test_distance_matrix_reader.cpp +++ b/src/common/test/test_distance_matrix_reader.cpp @@ -57,7 +57,7 @@ BOOST_AUTO_TEST_CASE( full_square_distance_matrix ) { Distance_matrix from_full_square; // Read full_square_distance_matrix.csv file where the separator is the default one ';' - from_full_square = Gudhi::read_lower_triangular_matrix_from_csv_file("full_square_distance_matrix.csv"); + from_full_square = Gudhi::read_lower_triangular_matrix_from_csv_file("full_square_distance_matrix.csv", ';'); for (auto& i : from_full_square) { for (auto j : i) { std::clog << j << " "; diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index a1440cbc..bc9a3b7b 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -457,7 +457,7 @@ if(PYTHONINTERP_FOUND) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py" - --no-diagram -f ${CMAKE_SOURCE_DIR}/data/distance_matrix/lower_triangular_distance_matrix.csv -e 12.0 -d 3) + --no-diagram -f ${CMAKE_SOURCE_DIR}/data/distance_matrix/lower_triangular_distance_matrix.csv -s , -e 12.0 -d 3) add_test(NAME rips_complex_diagram_persistence_from_off_file_example_py_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} diff --git a/src/python/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py b/src/python/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py index 9320d904..8a9cc857 100755 --- a/src/python/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py +++ b/src/python/example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py @@ -21,11 +21,12 @@ parser = argparse.ArgumentParser( description="RipsComplex creation from " "a distance matrix read in a csv file.", epilog="Example: " "example/rips_complex_diagram_persistence_from_distance_matrix_file_example.py " - "-f ../data/distance_matrix/lower_triangular_distance_matrix.csv -e 12.0 -d 3" + "-f ../data/distance_matrix/lower_triangular_distance_matrix.csv -s , -e 12.0 -d 3" "- Constructs a Rips complex with the " "distance matrix from the given csv file.", ) parser.add_argument("-f", "--file", type=str, required=True) +parser.add_argument("-s", "--separator", type=str, required=True) parser.add_argument("-e", "--max_edge_length", type=float, default=0.5) parser.add_argument("-d", "--max_dimension", type=int, default=1) parser.add_argument("-b", "--band", type=float, default=0.0) @@ -44,7 +45,7 @@ print("RipsComplex creation from distance matrix read in a csv file") message = "RipsComplex with max_edge_length=" + repr(args.max_edge_length) print(message) -distance_matrix = gudhi.read_lower_triangular_matrix_from_csv_file(csv_file=args.file, separator=',') +distance_matrix = gudhi.read_lower_triangular_matrix_from_csv_file(csv_file=args.file, separator=args.separator) rips_complex = gudhi.RipsComplex( distance_matrix=distance_matrix, max_edge_length=args.max_edge_length ) diff --git a/src/python/test/test_reader_utils.py b/src/python/test/test_reader_utils.py index 90da6651..e96e0569 100755 --- a/src/python/test/test_reader_utils.py +++ b/src/python/test/test_reader_utils.py @@ -30,7 +30,7 @@ def test_full_square_distance_matrix_csv_file(): test_file.write("0;1;2;3;\n1;0;4;5;\n2;4;0;6;\n3;5;6;0;") test_file.close() matrix = gudhi.read_lower_triangular_matrix_from_csv_file( - csv_file="full_square_distance_matrix.csv" + csv_file="full_square_distance_matrix.csv", separator=";" ) assert matrix == [[], [1.0], [2.0, 4.0], [3.0, 5.0, 6.0]] -- cgit v1.2.3 From ad1145bc4ac224954055f9b9ad955c2a53ceb687 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 26 May 2021 17:27:01 +0200 Subject: Change some output messages and documentation to me more explicit --- src/python/gudhi/datasets/generators/points.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/datasets/generators/points.cc b/src/python/gudhi/datasets/generators/points.cc index e2626b09..d658946b 100644 --- a/src/python/gudhi/datasets/generators/points.cc +++ b/src/python/gudhi/datasets/generators/points.cc @@ -24,7 +24,7 @@ typedef CGAL::Epick_d< CGAL::Dynamic_dimension_tag > Kern; py::array_t generate_points_on_sphere(size_t n_samples, int ambient_dim, double radius, std::string sample) { if (sample != "random") { - throw pybind11::value_error("sample type is not supported"); + throw pybind11::value_error("This sample type is not supported"); } py::array_t points({n_samples, (size_t)ambient_dim}); @@ -50,7 +50,7 @@ PYBIND11_MODULE(points, m) { m.attr("__license__") = "LGPL v3"; m.def("sphere", &generate_points_on_sphere, py::arg("n_samples"), py::arg("ambient_dim"), - py::arg("radius") = 1, py::arg("sample") = "random", + py::arg("radius") = 1., py::arg("sample") = "random", R"pbdoc( Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d @@ -58,9 +58,9 @@ PYBIND11_MODULE(points, m) { :type n_samples: integer :param ambient_dim: The ambient dimension d. :type ambient_dim: integer - :param radius: The radius. + :param radius: The radius. Default value is `1.`. :type radius: float - :param sample: The sample type. + :param sample: The sample type. Default and only available value is `"random"`. :type sample: string :rtype: numpy array of float :returns: the generated points on a sphere. -- cgit v1.2.3 From 0b238a336f15128d777252cd084ee996491e6882 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Thu, 27 May 2021 09:56:02 +0200 Subject: Add documentation to python torus fonction and apply some modifications according to PR comments --- src/python/gudhi/datasets/generators/_points.cc | 2 +- src/python/gudhi/datasets/generators/points.py | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc index 561fd6d8..003b65a3 100644 --- a/src/python/gudhi/datasets/generators/_points.cc +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -56,7 +56,7 @@ PYBIND11_MODULE(_points, m) { :param n_samples: The number of points to be generated. :type n_samples: integer - :param dim: The dimension. + :param dim: The dimension of the torus on which points would be generated in R^2*dim. :type dim: integer :param uniform: A flag to define if the points generation is uniform (i.e generated as a grid). :type uniform: bool diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py index d5a370ad..a8f5ad54 100644 --- a/src/python/gudhi/datasets/generators/points.py +++ b/src/python/gudhi/datasets/generators/points.py @@ -10,7 +10,7 @@ import numpy as np import itertools -def _generate_random_points(n_samples, dim): +def _generate_random_points_on_torus(n_samples, dim): # Generate random angles of size n_samples*dim alpha = 2*np.pi*np.random.rand(n_samples*dim) @@ -20,8 +20,9 @@ def _generate_random_points(n_samples, dim): return array_points -def _generate_grid_points(n_samples, dim): +def _generate_grid_points_on_torus(n_samples, dim): + # Generate points on a dim-torus as a grid n_samples_grid = int(n_samples**(1./dim)) alpha = np.linspace(0, 2*np.pi, n_samples_grid, endpoint=False) @@ -31,12 +32,25 @@ def _generate_grid_points(n_samples, dim): return array_points def torus(n_samples, dim, sample='random'): + ''' + Generate points on a dim-torus in R^2dim either randomly or on a grid + + :param n_samples: The number of points to be generated. + :param dim: The dimension of the torus on which points would be generated in R^2*dim. + :param sample: The sample type of the generated points. Can be 'random' or 'grid'. + :returns: numpy array containing the generated points on a torus. + The shape of returned numpy array is : + if sample is 'random' : (n_samples, 2*dim) + if sample is 'grid' : ((int(n_samples**(1./dim)))**dim, 2*dim) + ''' if sample == 'random': + # Generate points randomly print("Sample is random") - return _generate_random_points(n_samples, dim) + return _generate_random_points_on_torus(n_samples, dim) elif sample == 'grid': + # Generate points on a grid print("Sample is grid") - return _generate_grid_points(n_samples, dim) + return _generate_grid_points_on_torus(n_samples, dim) else: - raise Exception("Sample type '{}' is not supported".format(sample)) + raise ValueError("Sample type '{}' is not supported".format(sample)) return -- cgit v1.2.3 From 09214d0ad3abd0c81b3a2c8051bf8b370350d6e5 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Tue, 1 Jun 2021 11:20:26 +0200 Subject: Add datasets generators documentation --- src/python/doc/datasets_generators.inc | 14 ++++ src/python/doc/datasets_generators.rst | 97 ++++++++++++++++++++++++++ src/python/doc/examples.rst | 1 + src/python/doc/index.rst | 5 ++ src/python/gudhi/datasets/generators/points.py | 10 +-- 5 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 src/python/doc/datasets_generators.inc create mode 100644 src/python/doc/datasets_generators.rst (limited to 'src/python') diff --git a/src/python/doc/datasets_generators.inc b/src/python/doc/datasets_generators.inc new file mode 100644 index 00000000..c88115c3 --- /dev/null +++ b/src/python/doc/datasets_generators.inc @@ -0,0 +1,14 @@ +.. table:: + :widths: 30 40 30 + + +-----------------------------------+--------------------------------------------+--------------------------------------------------------------------------------------+ + | | :math:`(x_1, x_2, \ldots, x_d)` | Datasets generators (points). | :Authors: Hind Montassif | + | | | | + | | | :Since: GUDHI 3.5.0 | + | | | | + | | | :License: MIT (`LGPL v3 `_) | + | | | | + | | | :Requires: `CGAL `_ | + +-----------------------------------+--------------------------------------------+--------------------------------------------------------------------------------------+ + | * :doc:`datasets_generators` | + +-----------------------------------+-----------------------------------------------------------------------------------------------------------------------------------+ diff --git a/src/python/doc/datasets_generators.rst b/src/python/doc/datasets_generators.rst new file mode 100644 index 00000000..ef21c9d2 --- /dev/null +++ b/src/python/doc/datasets_generators.rst @@ -0,0 +1,97 @@ + +:orphan: + +.. To get rid of WARNING: document isn't included in any toctree + +=========================== +Datasets generators manual +=========================== + +We provide the generation of different customizable datasets to use as inputs for Gudhi complexes and data structures. + + +Points generators +------------------ + +Points on sphere +^^^^^^^^^^^^^^^^ + +The module **_points** enables the generation of random i.i.d. points uniformly on a (d-1)-sphere in :math:`R^d`. +The user should provide the number of points to be generated on the sphere :code:`n_samples` and the ambient dimension :code:`ambient_dim`. +The :code:`radius` of sphere is optional and is equal to **1** by default. +Only random points generation is currently available. + +The generated points are given as an array of shape :math:`(n\_samples, ambient\_dim)`. + +Example +""""""" + +.. code-block:: python + + from gudhi.datasets.generators import _points + from gudhi import AlphaComplex + + # Generate 50 points on a sphere in R^2 + gen_points = _points.sphere(n_samples = 50, ambient_dim = 2, radius = 1, sample = "random") + + # Create an alpha complex from the generated points + alpha_complex = AlphaComplex(points = gen_points) + +.. autofunction:: gudhi.datasets.generators._points.sphere + +Points on torus +^^^^^^^^^^^^^^^^ + +You can also generate points on a torus. + +Two modules are available and give the same output: the first one depends on **CGAL** and the second does not and consists of full python code. + +On another hand, two sample types are provided : you can either generate i.i.d. points on a d-torus in :math:`R^{2d}` *randomly* or on a *grid*. + +First module : **_points** +"""""""""""""""""""""""""" + +The user should provide the number of points to be generated on the torus :code:`n_samples`, and the dimension :code:`dim` of the torus on which points would be generated in :math:`R^{2dim}`. +The flag :code:`uniform` is optional and is set to **False** by default, meaning that the points will be generated randomly. +In this case, the returned generated points would be an array of shape :math:`(n\_samples, 2*dim)`. +Otherwise, if set to **True**, the points are generated as a grid and would be given as an array of shape : + +.. math:: + + ( [n\_samples^{1 \over {dim}}]^{dim}, 2*dim ) + +Example +""""""" +.. code-block:: python + + from gudhi.datasets.generators import _points + + # Generate 50 points randomly on a torus in R^6 + gen_points = _points.torus(n_samples = 50, dim = 3) + + # Generate 27 points on a torus as a grid in R^6 + gen_points = _points.torus(n_samples = 50, dim = 3, uniform = True) + +.. autofunction:: gudhi.datasets.generators._points.torus + +Second module : **points** +"""""""""""""""""""""""""" + +The user should provide the number of points to be generated on the torus :code:`n_samples` and the dimension :code:`dim` of the torus on which points would be generated in :math:`R^{2dim}`. +The :code:`sample` argument is optional and is set to **'random'** by default. +The other allowed value of sample type is **'grid'** and is equivalent to :code:`uniform = True` in the first module. + +Example +""""""" +.. code-block:: python + + from gudhi.datasets.generators import points + + # Generate 50 points randomly on a torus in R^6 + gen_points = points.torus(n_samples = 50, dim = 3) + + # Generate 27 points on a torus as a grid in R^6 + gen_points = points.torus(n_samples = 50, dim = 3, sample = 'grid') + + +.. autofunction:: gudhi.datasets.generators.points.torus diff --git a/src/python/doc/examples.rst b/src/python/doc/examples.rst index 76e5d4c7..1442f185 100644 --- a/src/python/doc/examples.rst +++ b/src/python/doc/examples.rst @@ -8,6 +8,7 @@ Examples .. only:: builder_html * :download:`alpha_complex_diagram_persistence_from_off_file_example.py <../example/alpha_complex_diagram_persistence_from_off_file_example.py>` + * :download:`alpha_complex_from_generated_points_on_sphere_example.py <../example/alpha_complex_from_generated_points_on_sphere_example.py>` * :download:`alpha_complex_from_points_example.py <../example/alpha_complex_from_points_example.py>` * :download:`alpha_rips_persistence_bottleneck_distance.py <../example/alpha_rips_persistence_bottleneck_distance.py>` * :download:`bottleneck_basic_example.py <../example/bottleneck_basic_example.py>` diff --git a/src/python/doc/index.rst b/src/python/doc/index.rst index 040e57a4..2d7921ae 100644 --- a/src/python/doc/index.rst +++ b/src/python/doc/index.rst @@ -91,3 +91,8 @@ Clustering ********** .. include:: clustering.inc + +Datasets generators +******************* + +.. include:: datasets_generators.inc diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py index a8f5ad54..3870dea6 100644 --- a/src/python/gudhi/datasets/generators/points.py +++ b/src/python/gudhi/datasets/generators/points.py @@ -32,17 +32,17 @@ def _generate_grid_points_on_torus(n_samples, dim): return array_points def torus(n_samples, dim, sample='random'): - ''' + """ Generate points on a dim-torus in R^2dim either randomly or on a grid :param n_samples: The number of points to be generated. :param dim: The dimension of the torus on which points would be generated in R^2*dim. :param sample: The sample type of the generated points. Can be 'random' or 'grid'. :returns: numpy array containing the generated points on a torus. - The shape of returned numpy array is : - if sample is 'random' : (n_samples, 2*dim) - if sample is 'grid' : ((int(n_samples**(1./dim)))**dim, 2*dim) - ''' + The shape of returned numpy array is : + if sample is 'random' : (n_samples, 2*dim). + if sample is 'grid' : ([n_samples**(1./dim)]**dim, 2*dim). + """ if sample == 'random': # Generate points randomly print("Sample is random") -- cgit v1.2.3 From b04759faf8f474ff98e9e229c41d85ff3bf009da Mon Sep 17 00:00:00 2001 From: Hind-M Date: Mon, 7 Jun 2021 11:56:48 +0200 Subject: Add a 3d sphere image to doc for datasets_generators --- src/python/doc/datasets_generators.inc | 4 ++-- src/python/doc/img/sphere_3d.png | Bin 0 -> 90550 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/python/doc/img/sphere_3d.png (limited to 'src/python') diff --git a/src/python/doc/datasets_generators.inc b/src/python/doc/datasets_generators.inc index c88115c3..8d169275 100644 --- a/src/python/doc/datasets_generators.inc +++ b/src/python/doc/datasets_generators.inc @@ -2,8 +2,8 @@ :widths: 30 40 30 +-----------------------------------+--------------------------------------------+--------------------------------------------------------------------------------------+ - | | :math:`(x_1, x_2, \ldots, x_d)` | Datasets generators (points). | :Authors: Hind Montassif | - | | | | + | .. figure:: | Datasets generators (points). | :Authors: Hind Montassif | + | img/sphere_3d.png | | | | | | :Since: GUDHI 3.5.0 | | | | | | | | :License: MIT (`LGPL v3 `_) | diff --git a/src/python/doc/img/sphere_3d.png b/src/python/doc/img/sphere_3d.png new file mode 100644 index 00000000..7e7f0ab2 Binary files /dev/null and b/src/python/doc/img/sphere_3d.png differ -- cgit v1.2.3 From b9160fb8410bbb999913b0b4e91f1aa1ff58d2cd Mon Sep 17 00:00:00 2001 From: Hind-M Date: Mon, 7 Jun 2021 17:07:55 +0200 Subject: Replace 'uniform' flag of torus generation with 'sample' taking two possible values: 'grid'(i.e uniform==True) or 'random' (i.e uniform==False) --- src/Tangential_complex/benchmark/benchmark_tc.cpp | 2 +- src/common/include/gudhi/random_point_generators.h | 10 +++++----- src/common/utilities/off_file_from_shape_generator.cpp | 2 +- src/python/doc/datasets_generators.rst | 8 ++++---- src/python/gudhi/datasets/generators/_points.cc | 10 +++++----- 5 files changed, 16 insertions(+), 16 deletions(-) (limited to 'src/python') diff --git a/src/Tangential_complex/benchmark/benchmark_tc.cpp b/src/Tangential_complex/benchmark/benchmark_tc.cpp index e3b2a04f..6da1425f 100644 --- a/src/Tangential_complex/benchmark/benchmark_tc.cpp +++ b/src/Tangential_complex/benchmark/benchmark_tc.cpp @@ -704,7 +704,7 @@ int main() { points = Gudhi::generate_points_on_torus_d( num_points, intrinsic_dim, - param1 == "Y", // uniform + (param1 == "Y") ? "grid" : "random", // grid or random sample type std::atof(param2.c_str())); // radius_noise_percentage } else if (input == "generate_klein_bottle_3D") { points = Gudhi::generate_points_on_klein_bottle_3D( diff --git a/src/common/include/gudhi/random_point_generators.h b/src/common/include/gudhi/random_point_generators.h index 33fb182d..07e4f3da 100644 --- a/src/common/include/gudhi/random_point_generators.h +++ b/src/common/include/gudhi/random_point_generators.h @@ -185,7 +185,7 @@ std::vector generate_points_on_torus_3D(std::size_t nu // "Private" function used by generate_points_on_torus_d template -static void generate_uniform_points_on_torus_d(const Kernel &k, int dim, std::size_t num_slices, +static void generate_grid_points_on_torus_d(const Kernel &k, int dim, std::size_t num_slices, OutputIterator out, double radius_noise_percentage = 0., std::vector current_point = @@ -208,14 +208,14 @@ static void generate_uniform_points_on_torus_d(const Kernel &k, int dim, std::si double alpha = two_pi * slice_idx / num_slices; cp2.push_back(radius_noise_ratio * std::cos(alpha)); cp2.push_back(radius_noise_ratio * std::sin(alpha)); - generate_uniform_points_on_torus_d( + generate_grid_points_on_torus_d( k, dim, num_slices, out, radius_noise_percentage, cp2); } } } template -std::vector generate_points_on_torus_d(std::size_t num_points, int dim, bool uniform = false, +std::vector generate_points_on_torus_d(std::size_t num_points, int dim, std::string sample = "random", double radius_noise_percentage = 0.) { using namespace boost::math::double_constants; @@ -226,9 +226,9 @@ std::vector generate_points_on_torus_d(std::size_t num std::vector points; points.reserve(num_points); - if (uniform) { + if (sample == "grid") { std::size_t num_slices = (std::size_t)std::pow(num_points, 1. / dim); - generate_uniform_points_on_torus_d( + generate_grid_points_on_torus_d( k, dim, num_slices, std::back_inserter(points), radius_noise_percentage); } else { for (std::size_t i = 0; i < num_points;) { diff --git a/src/common/utilities/off_file_from_shape_generator.cpp b/src/common/utilities/off_file_from_shape_generator.cpp index 6efef4fc..71ede434 100644 --- a/src/common/utilities/off_file_from_shape_generator.cpp +++ b/src/common/utilities/off_file_from_shape_generator.cpp @@ -135,7 +135,7 @@ int main(int argc, char **argv) { if (dimension == 3) points = Gudhi::generate_points_on_torus_3D(points_number, dimension, radius, radius/2.); else - points = Gudhi::generate_points_on_torus_d(points_number, dimension, true); + points = Gudhi::generate_points_on_torus_d(points_number, dimension, "grid"); break; case Data_shape::klein: switch (dimension) { diff --git a/src/python/doc/datasets_generators.rst b/src/python/doc/datasets_generators.rst index ef21c9d2..2802eccd 100644 --- a/src/python/doc/datasets_generators.rst +++ b/src/python/doc/datasets_generators.rst @@ -52,9 +52,9 @@ First module : **_points** """""""""""""""""""""""""" The user should provide the number of points to be generated on the torus :code:`n_samples`, and the dimension :code:`dim` of the torus on which points would be generated in :math:`R^{2dim}`. -The flag :code:`uniform` is optional and is set to **False** by default, meaning that the points will be generated randomly. +The :code:`sample` argument is optional and is set to **'random'** by default. In this case, the returned generated points would be an array of shape :math:`(n\_samples, 2*dim)`. -Otherwise, if set to **True**, the points are generated as a grid and would be given as an array of shape : +Otherwise, if set to **'grid'**, the points are generated on a grid and would be given as an array of shape : .. math:: @@ -70,7 +70,7 @@ Example gen_points = _points.torus(n_samples = 50, dim = 3) # Generate 27 points on a torus as a grid in R^6 - gen_points = _points.torus(n_samples = 50, dim = 3, uniform = True) + gen_points = _points.torus(n_samples = 50, dim = 3, sample = 'grid') .. autofunction:: gudhi.datasets.generators._points.torus @@ -79,7 +79,7 @@ Second module : **points** The user should provide the number of points to be generated on the torus :code:`n_samples` and the dimension :code:`dim` of the torus on which points would be generated in :math:`R^{2dim}`. The :code:`sample` argument is optional and is set to **'random'** by default. -The other allowed value of sample type is **'grid'** and is equivalent to :code:`uniform = True` in the first module. +The other allowed value of sample type is **'grid'**. Example """"""" diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc index 55b21b2b..6bbdf284 100644 --- a/src/python/gudhi/datasets/generators/_points.cc +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -46,13 +46,13 @@ py::array_t generate_points_on_sphere(size_t n_samples, int ambient_dim, return points; } -py::array_t generate_points_on_torus(size_t n_samples, int dim, bool uniform) { +py::array_t generate_points_on_torus(size_t n_samples, int dim, std::string sample) { std::vector points_generated; { py::gil_scoped_release release; - points_generated = Gudhi::generate_points_on_torus_d(n_samples, dim, uniform); + points_generated = Gudhi::generate_points_on_torus_d(n_samples, dim, sample); } size_t npoints = points_generated.size(); @@ -93,7 +93,7 @@ PYBIND11_MODULE(_points, m) { )pbdoc"); m.def("torus", &generate_points_on_torus, - py::arg("n_samples"), py::arg("dim"), py::arg("uniform") = false, + py::arg("n_samples"), py::arg("dim"), py::arg("sample") = "random", R"pbdoc( Generate random i.i.d. points on a d-torus in R^2d @@ -101,8 +101,8 @@ PYBIND11_MODULE(_points, m) { :type n_samples: integer :param dim: The dimension of the torus on which points would be generated in R^2*dim. :type dim: integer - :param uniform: A flag to define if the points generation is uniform (i.e generated as a grid). - :type uniform: bool + :param sample: The sample type. Available values are: `"random"` and `"grid"`. Default value is `"random"`. + :type sample: string :rtype: numpy array of float :returns: the generated points on a torus. )pbdoc"); -- cgit v1.2.3 From 01d62172c54b258a1072a42d25a39fd21a38c284 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 24 Jun 2021 16:15:02 +0200 Subject: Use python_docs_theme. Dependency added in gudhi-deploy --- src/python/doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/doc/conf.py b/src/python/doc/conf.py index b06baf9c..46debd87 100755 --- a/src/python/doc/conf.py +++ b/src/python/doc/conf.py @@ -120,7 +120,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'classic' +html_theme = 'python_docs_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the -- cgit v1.2.3 From b32f61a66d52a25be177dd96de469420e5b4a980 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 25 Jun 2021 07:07:32 +0200 Subject: add sphinx-paramlinks and python-docs-theme in installation guide --- src/python/doc/installation.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/installation.rst b/src/python/doc/installation.rst index 9c16b04e..451ae020 100644 --- a/src/python/doc/installation.rst +++ b/src/python/doc/installation.rst @@ -194,8 +194,10 @@ A complete configuration would be : Documentation ============= -To build the documentation, `sphinx-doc `_ and -`sphinxcontrib-bibtex `_ are +To build the documentation, `sphinx-doc `_, +`sphinxcontrib-bibtex `_, +`sphinxcontrib-paramlinks `_ and +`python-docs-theme `_ are required. As the documentation is auto-tested, `CGAL`_, `Eigen`_, `Matplotlib`_, `NumPy`_, `POT`_, `Scikit-learn`_ and `SciPy`_ are also mandatory to build the documentation. -- cgit v1.2.3 From c0cca8dbd60e3da5147517f6f8f37d9bdeefe511 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 25 Jun 2021 16:26:55 +0200 Subject: update headers, conf.py for the new theme and bottleneck image to be smaller --- src/Bottleneck_distance/doc/perturb_pd.png | Bin 20864 -> 15532 bytes src/common/doc/header.html | 1 + src/python/doc/_templates/layout.html | 1 + src/python/doc/conf.py | 3 --- 4 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/Bottleneck_distance/doc/perturb_pd.png b/src/Bottleneck_distance/doc/perturb_pd.png index be638de0..eabf3c8c 100644 Binary files a/src/Bottleneck_distance/doc/perturb_pd.png and b/src/Bottleneck_distance/doc/perturb_pd.png differ diff --git a/src/common/doc/header.html b/src/common/doc/header.html index 9da20bbc..7c20478b 100644 --- a/src/common/doc/header.html +++ b/src/common/doc/header.html @@ -49,6 +49,7 @@ $extrastylesheet
  • Related projects
  • They are talking about us
  • GUDHI in action
  • +
  • Etymology
  • diff --git a/src/python/doc/_templates/layout.html b/src/python/doc/_templates/layout.html index cd40a51b..e074b6c7 100644 --- a/src/python/doc/_templates/layout.html +++ b/src/python/doc/_templates/layout.html @@ -194,6 +194,7 @@
  • Related projects
  • They are talking about us
  • GUDHI in action
  • +
  • Etymology
  • diff --git a/src/python/doc/conf.py b/src/python/doc/conf.py index 46debd87..e69e2751 100755 --- a/src/python/doc/conf.py +++ b/src/python/doc/conf.py @@ -126,9 +126,6 @@ html_theme = 'python_docs_theme' # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - "sidebarbgcolor": "#A1ADCD", - "sidebartextcolor": "black", - "sidebarlinkcolor": "#334D5C", "body_max_width": "100%", } -- cgit v1.2.3 From 50d7eb5bc89900dd90f7f2d97ca8bc3c19c8a057 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Fri, 25 Jun 2021 17:02:54 +0200 Subject: Fix #486 --- .../modules/GUDHI_third_party_libraries.cmake | 2 + src/python/CMakeLists.txt | 116 ++++++++++++--------- 2 files changed, 69 insertions(+), 49 deletions(-) (limited to 'src/python') diff --git a/src/cmake/modules/GUDHI_third_party_libraries.cmake b/src/cmake/modules/GUDHI_third_party_libraries.cmake index e1566877..023061f1 100644 --- a/src/cmake/modules/GUDHI_third_party_libraries.cmake +++ b/src/cmake/modules/GUDHI_third_party_libraries.cmake @@ -156,6 +156,8 @@ if( PYTHONINTERP_FOUND ) find_python_module("eagerpy") find_python_module_no_version("hnswlib") find_python_module("tensorflow") + find_python_module("sphinx_paramlinks") + find_python_module_no_version("python_docs_theme") endif() if(NOT GUDHI_PYTHON_PATH) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 98f2b85f..f534fc2a 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -107,6 +107,16 @@ if(PYTHONINTERP_FOUND) if(TENSORFLOW_FOUND) add_gudhi_debug_info("TensorFlow version ${TENSORFLOW_VERSION}") endif() + if(SPHINX_FOUND) + add_gudhi_debug_info("Sphinx version ${SPHINX_VERSION}") + endif() + if(SPHINX_PARAMLINKS_FOUND) + add_gudhi_debug_info("Sphinx-paramlinks version ${SPHINX_PARAMLINKS_VERSION}") + endif() + if(PYTHON_DOCS_THEME_FOUND) + # Does not have a version number... + add_gudhi_debug_info("python_docs_theme found") + endif() set(GUDHI_PYTHON_EXTRA_COMPILE_ARGS "${GUDHI_PYTHON_EXTRA_COMPILE_ARGS}'-DBOOST_RESULT_OF_USE_DECLTYPE', ") set(GUDHI_PYTHON_EXTRA_COMPILE_ARGS "${GUDHI_PYTHON_EXTRA_COMPILE_ARGS}'-DBOOST_ALL_NO_LIB', ") @@ -281,62 +291,70 @@ if(PYTHONINTERP_FOUND) # Documentation generation is available through sphinx - requires all modules # Make it first as sphinx test is by far the longest test which is nice when testing in parallel if(SPHINX_PATH) - if(MATPLOTLIB_FOUND) - if(NUMPY_FOUND) - if(SCIPY_FOUND) - if(SKLEARN_FOUND) - if(OT_FOUND) - if(PYBIND11_FOUND) - if(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) - set (GUDHI_SPHINX_MESSAGE "Generating API documentation with Sphinx in ${CMAKE_CURRENT_BINARY_DIR}/sphinx/") - # User warning - Sphinx is a static pages generator, and configured to work fine with user_version - # Images and biblio warnings because not found on developper version - if (GUDHI_PYTHON_PATH STREQUAL "src/python") - set (GUDHI_SPHINX_MESSAGE "${GUDHI_SPHINX_MESSAGE} \n WARNING : Sphinx is configured for user version, you run it on developper version. Images and biblio will miss") - endif() - # sphinx target requires gudhi.so, because conf.py reads gudhi version from it - add_custom_target(sphinx - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc - COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" - ${SPHINX_PATH} -b html ${CMAKE_CURRENT_SOURCE_DIR}/doc ${CMAKE_CURRENT_BINARY_DIR}/sphinx - DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/gudhi.so" - COMMENT "${GUDHI_SPHINX_MESSAGE}" VERBATIM) - - add_test(NAME sphinx_py_test - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" - ${SPHINX_PATH} -b doctest ${CMAKE_CURRENT_SOURCE_DIR}/doc ${CMAKE_CURRENT_BINARY_DIR}/doctest) - - # Set missing or not modules - set(GUDHI_MODULES ${GUDHI_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MODULES") - else(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) - message("++ Python documentation module will not be compiled because it requires a Eigen3 and CGAL version >= 4.11.0") + if(SPHINX_PARAMLINKS_FOUND) + if(PYTHON_DOCS_THEME_FOUND) + if(MATPLOTLIB_FOUND) + if(NUMPY_FOUND) + if(SCIPY_FOUND) + if(SKLEARN_FOUND) + if(OT_FOUND) + if(PYBIND11_FOUND) + if(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) + set (GUDHI_SPHINX_MESSAGE "Generating API documentation with Sphinx in ${CMAKE_CURRENT_BINARY_DIR}/sphinx/") + # User warning - Sphinx is a static pages generator, and configured to work fine with user_version + # Images and biblio warnings because not found on developper version + if (GUDHI_PYTHON_PATH STREQUAL "src/python") + set (GUDHI_SPHINX_MESSAGE "${GUDHI_SPHINX_MESSAGE} \n WARNING : Sphinx is configured for user version, you run it on developper version. Images and biblio will miss") + endif() + # sphinx target requires gudhi.so, because conf.py reads gudhi version from it + add_custom_target(sphinx + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc + COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" + ${SPHINX_PATH} -b html ${CMAKE_CURRENT_SOURCE_DIR}/doc ${CMAKE_CURRENT_BINARY_DIR}/sphinx + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/gudhi.so" + COMMENT "${GUDHI_SPHINX_MESSAGE}" VERBATIM) + add_test(NAME sphinx_py_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" + ${SPHINX_PATH} -b doctest ${CMAKE_CURRENT_SOURCE_DIR}/doc ${CMAKE_CURRENT_BINARY_DIR}/doctest) + # Set missing or not modules + set(GUDHI_MODULES ${GUDHI_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MODULES") + else(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) + message("++ Python documentation module will not be compiled because it requires a Eigen3 and CGAL version >= 4.11.0") + set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") + endif(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) + else(PYBIND11_FOUND) + message("++ Python documentation module will not be compiled because pybind11 was not found") + set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") + endif(PYBIND11_FOUND) + else(OT_FOUND) + message("++ Python documentation module will not be compiled because POT was not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) - else(PYBIND11_FOUND) - message("++ Python documentation module will not be compiled because pybind11 was not found") + endif(OT_FOUND) + else(SKLEARN_FOUND) + message("++ Python documentation module will not be compiled because scikit-learn was not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(PYBIND11_FOUND) - else(OT_FOUND) - message("++ Python documentation module will not be compiled because POT was not found") + endif(SKLEARN_FOUND) + else(SCIPY_FOUND) + message("++ Python documentation module will not be compiled because scipy was not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(OT_FOUND) - else(SKLEARN_FOUND) - message("++ Python documentation module will not be compiled because scikit-learn was not found") + endif(SCIPY_FOUND) + else(NUMPY_FOUND) + message("++ Python documentation module will not be compiled because numpy was not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(SKLEARN_FOUND) - else(SCIPY_FOUND) - message("++ Python documentation module will not be compiled because scipy was not found") + endif(NUMPY_FOUND) + else(MATPLOTLIB_FOUND) + message("++ Python documentation module will not be compiled because matplotlib was not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(SCIPY_FOUND) - else(NUMPY_FOUND) - message("++ Python documentation module will not be compiled because numpy was not found") + endif(MATPLOTLIB_FOUND) + else(PYTHON_DOCS_THEME_FOUND) + message("++ Python documentation module will not be compiled because python-docs-theme was not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(NUMPY_FOUND) - else(MATPLOTLIB_FOUND) - message("++ Python documentation module will not be compiled because matplotlib was not found") + endif(PYTHON_DOCS_THEME_FOUND) + else(SPHINX_PARAMLINKS_FOUND) + message("++ Python documentation module will not be compiled because sphinxcontrib-paramlinks was not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(MATPLOTLIB_FOUND) + endif(SPHINX_PARAMLINKS_FOUND) else(SPHINX_PATH) message("++ Python documentation module will not be compiled because sphinx and sphinxcontrib-bibtex were not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") -- cgit v1.2.3 From 68031184fb94cf19c8b3c6f0de122db447693847 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Tue, 29 Jun 2021 11:48:28 +0200 Subject: Fix issue #502: check homology_coeff_field primality before computing persistence --- src/python/include/Persistent_cohomology_interface.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'src/python') diff --git a/src/python/include/Persistent_cohomology_interface.h b/src/python/include/Persistent_cohomology_interface.h index e5a3dfba..6877f190 100644 --- a/src/python/include/Persistent_cohomology_interface.h +++ b/src/python/include/Persistent_cohomology_interface.h @@ -43,6 +43,21 @@ persistent_cohomology::Persistent_cohomology 1; + if ((n % 2 == 0) || (n % 3 == 0)) + return false; + int i = 5; + while (i*i <= n) { + if ((n % i == 0) || (n % (i + 2) == 0)) + return false; + i += 6; + } + return true; + } + public: Persistent_cohomology_interface(FilteredComplex* stptr, bool persistence_dim_max=false) : Base(*stptr, persistence_dim_max), @@ -50,6 +65,11 @@ persistent_cohomology::Persistent_cohomology Date: Thu, 1 Jul 2021 15:56:11 +0200 Subject: Move primality test to Field_Zp::init Throw exception when not prime Add tests --- src/Persistent_cohomology/example/CMakeLists.txt | 2 +- .../include/gudhi/Persistent_cohomology/Field_Zp.h | 19 ++++++++++++++++++- .../test/persistent_cohomology_unit_test.cpp | 22 +++++++++++++++++++++- src/python/gudhi/cubical_complex.pyx | 2 +- src/python/gudhi/periodic_cubical_complex.pyx | 2 +- src/python/gudhi/simplex_tree.pxd | 2 +- .../include/Persistent_cohomology_interface.h | 20 -------------------- 7 files changed, 43 insertions(+), 26 deletions(-) (limited to 'src/python') diff --git a/src/Persistent_cohomology/example/CMakeLists.txt b/src/Persistent_cohomology/example/CMakeLists.txt index c68c6524..3e7e9369 100644 --- a/src/Persistent_cohomology/example/CMakeLists.txt +++ b/src/Persistent_cohomology/example/CMakeLists.txt @@ -11,7 +11,7 @@ if (TBB_FOUND) target_link_libraries(persistence_from_simple_simplex_tree ${TBB_LIBRARIES}) endif() add_test(NAME Persistent_cohomology_example_from_simple_simplex_tree COMMAND $ - "1" "0") + "2" "0") if(TARGET Boost::program_options) add_executable(rips_persistence_step_by_step rips_persistence_step_by_step.cpp) diff --git a/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h b/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h index 0673625c..4bfd95c0 100644 --- a/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h +++ b/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h @@ -13,6 +13,8 @@ #include #include +#include +#include namespace Gudhi { @@ -34,15 +36,30 @@ class Field_Zp { void init(int charac) { assert(charac > 0); // division by zero + non negative values + Prime = charac; + + // Check for primality + if ((Prime == 0) || (Prime == 1) || ((Prime > 3) && ((Prime % 2 == 0) || (Prime % 3 == 0)))) + throw std::invalid_argument("homology_coeff_field must be a prime number"); + inverse_.clear(); inverse_.reserve(charac); inverse_.push_back(0); for (int i = 1; i < Prime; ++i) { int inv = 1; - while (((inv * i) % Prime) != 1) + int mult = inv * i; + while ( (mult % Prime) != 1) { ++inv; + if(mult == Prime) + throw std::invalid_argument("homology_coeff_field must be a prime number"); + mult = inv * i; + } inverse_.push_back(inv); + if ( (i <= std::sqrt(Prime)) && (((i-5)%6) == 0) ) { + if ((Prime % i == 0) || (Prime % (i + 2) == 0)) + throw std::invalid_argument("homology_coeff_field must be a prime number"); + } } } diff --git a/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp b/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp index fe3f8517..9559b842 100644 --- a/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp +++ b/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp @@ -146,9 +146,14 @@ void test_rips_persistence_in_dimension(int dimension) { std::clog << "str_rips_persistence=" << str_rips_persistence << std::endl; } +BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_0 ) +{ + BOOST_CHECK_THROW(test_rips_persistence_in_dimension(0), std::invalid_argument); +} + BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_1 ) { - test_rips_persistence_in_dimension(1); + BOOST_CHECK_THROW(test_rips_persistence_in_dimension(1), std::invalid_argument); } BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_2 ) @@ -161,11 +166,26 @@ BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_3 ) test_rips_persistence_in_dimension(3); } +BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_4 ) +{ + BOOST_CHECK_THROW(test_rips_persistence_in_dimension(4), std::invalid_argument); +} + BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_5 ) { test_rips_persistence_in_dimension(5); } +BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_11 ) +{ + test_rips_persistence_in_dimension(11); +} + +BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_13 ) +{ + test_rips_persistence_in_dimension(13); +} + // TODO(VR): not working from 6 // std::string str_rips_persistence = test_rips_persistence(6, 0); // TODO(VR): division by zero diff --git a/src/python/gudhi/cubical_complex.pyx b/src/python/gudhi/cubical_complex.pyx index 28fbe3af..adc40499 100644 --- a/src/python/gudhi/cubical_complex.pyx +++ b/src/python/gudhi/cubical_complex.pyx @@ -35,7 +35,7 @@ cdef extern from "Cubical_complex_interface.h" namespace "Gudhi": cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Cubical_complex_persistence_interface "Gudhi::Persistent_cohomology_interface>": Cubical_complex_persistence_interface(Bitmap_cubical_complex_base_interface * st, bool persistence_dim_max) nogil - void compute_persistence(int homology_coeff_field, double min_persistence) nogil + void compute_persistence(int homology_coeff_field, double min_persistence) nogil except+ vector[pair[int, pair[double, double]]] get_persistence() nogil vector[vector[int]] cofaces_of_cubical_persistence_pairs() nogil vector[int] betti_numbers() nogil diff --git a/src/python/gudhi/periodic_cubical_complex.pyx b/src/python/gudhi/periodic_cubical_complex.pyx index d353d2af..0eaa5867 100644 --- a/src/python/gudhi/periodic_cubical_complex.pyx +++ b/src/python/gudhi/periodic_cubical_complex.pyx @@ -32,7 +32,7 @@ cdef extern from "Cubical_complex_interface.h" namespace "Gudhi": cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Periodic_cubical_complex_persistence_interface "Gudhi::Persistent_cohomology_interface>>": Periodic_cubical_complex_persistence_interface(Periodic_cubical_complex_base_interface * st, bool persistence_dim_max) nogil - void compute_persistence(int homology_coeff_field, double min_persistence) nogil + void compute_persistence(int homology_coeff_field, double min_persistence) nogil except + vector[pair[int, pair[double, double]]] get_persistence() nogil vector[vector[int]] cofaces_of_cubical_persistence_pairs() nogil vector[int] betti_numbers() nogil diff --git a/src/python/gudhi/simplex_tree.pxd b/src/python/gudhi/simplex_tree.pxd index 3b8ea4f9..006a24ed 100644 --- a/src/python/gudhi/simplex_tree.pxd +++ b/src/python/gudhi/simplex_tree.pxd @@ -78,7 +78,7 @@ cdef extern from "Simplex_tree_interface.h" namespace "Gudhi": cdef extern from "Persistent_cohomology_interface.h" namespace "Gudhi": cdef cppclass Simplex_tree_persistence_interface "Gudhi::Persistent_cohomology_interface>": Simplex_tree_persistence_interface(Simplex_tree_interface_full_featured * st, bool persistence_dim_max) nogil - void compute_persistence(int homology_coeff_field, double min_persistence) nogil + void compute_persistence(int homology_coeff_field, double min_persistence) nogil except + vector[pair[int, pair[double, double]]] get_persistence() nogil vector[int] betti_numbers() nogil vector[int] persistent_betti_numbers(double from_value, double to_value) nogil diff --git a/src/python/include/Persistent_cohomology_interface.h b/src/python/include/Persistent_cohomology_interface.h index 6877f190..e5a3dfba 100644 --- a/src/python/include/Persistent_cohomology_interface.h +++ b/src/python/include/Persistent_cohomology_interface.h @@ -43,21 +43,6 @@ persistent_cohomology::Persistent_cohomology 1; - if ((n % 2 == 0) || (n % 3 == 0)) - return false; - int i = 5; - while (i*i <= n) { - if ((n % i == 0) || (n % (i + 2) == 0)) - return false; - i += 6; - } - return true; - } - public: Persistent_cohomology_interface(FilteredComplex* stptr, bool persistence_dim_max=false) : Base(*stptr, persistence_dim_max), @@ -65,11 +50,6 @@ persistent_cohomology::Persistent_cohomology Date: Mon, 12 Jul 2021 11:25:57 +0200 Subject: Limit homology_coeff_field value to max allowed Add test with first prime outside the allowed range --- .../include/gudhi/Persistent_cohomology/Field_Zp.h | 7 +++++-- .../test/persistent_cohomology_unit_test.cpp | 8 ++++---- src/python/gudhi/simplex_tree.pyx | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) (limited to 'src/python') diff --git a/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h b/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h index 4bfd95c0..7ecc9a80 100644 --- a/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h +++ b/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h @@ -14,7 +14,6 @@ #include #include #include -#include namespace Gudhi { @@ -39,6 +38,10 @@ class Field_Zp { Prime = charac; + // Check that the provided prime is less than the maximum allowed as int and calculation below : 46337 ; i.e (max_prime-1)**2 <= INT_MAX + if(Prime > 46337) + throw std::invalid_argument("Maximum homology_coeff_field allowed value is 46337"); + // Check for primality if ((Prime == 0) || (Prime == 1) || ((Prime > 3) && ((Prime % 2 == 0) || (Prime % 3 == 0)))) throw std::invalid_argument("homology_coeff_field must be a prime number"); @@ -56,7 +59,7 @@ class Field_Zp { mult = inv * i; } inverse_.push_back(inv); - if ( (i <= std::sqrt(Prime)) && (((i-5)%6) == 0) ) { + if ( (i*i <= Prime) && (((i-5)%6) == 0) ) { if ((Prime % i == 0) || (Prime % (i + 2) == 0)) throw std::invalid_argument("homology_coeff_field must be a prime number"); } diff --git a/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp b/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp index 9559b842..35bb5988 100644 --- a/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp +++ b/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp @@ -186,10 +186,10 @@ BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_13 ) test_rips_persistence_in_dimension(13); } -// TODO(VR): not working from 6 -// std::string str_rips_persistence = test_rips_persistence(6, 0); -// TODO(VR): division by zero -// std::string str_rips_persistence = test_rips_persistence(0, 0); +BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_46349 ) +{ + BOOST_CHECK_THROW(test_rips_persistence_in_dimension(46349), std::invalid_argument); +} /** SimplexTree minimal options to test the limits. * diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index be08a3a1..9c51cb46 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -412,7 +412,7 @@ cdef class SimplexTree: """This function retrieves good values for extended persistence, and separate the diagrams into the Ordinary, Relative, Extended+ and Extended- subdiagrams. - :param homology_coeff_field: The homology coefficient field. Must be a prime number. Default value is 11. + :param homology_coeff_field: The homology coefficient field. Must be a prime number. Default value is 11. Max is 46337. :type homology_coeff_field: int :param min_persistence: The minimum persistence value (i.e., the absolute value of the difference between the persistence diagram point coordinates) to take into account (strictly greater than min_persistence). @@ -449,7 +449,7 @@ cdef class SimplexTree: """This function computes and returns the persistence of the simplicial complex. :param homology_coeff_field: The homology coefficient field. Must be a - prime number. Default value is 11. + prime number. Default value is 11. Max is 46337. :type homology_coeff_field: int :param min_persistence: The minimum persistence value to take into account (strictly greater than min_persistence). Default value is @@ -472,7 +472,7 @@ cdef class SimplexTree: when you do not want the list :func:`persistence` returns. :param homology_coeff_field: The homology coefficient field. Must be a - prime number. Default value is 11. + prime number. Default value is 11. Max is 46337. :type homology_coeff_field: int :param min_persistence: The minimum persistence value to take into account (strictly greater than min_persistence). Default value is -- cgit v1.2.3 From a93e26976e5898b267d8b743e080e8869ff41b4f Mon Sep 17 00:00:00 2001 From: Hind-M Date: Tue, 27 Jul 2021 11:01:31 +0200 Subject: Remove unnecessary checks for primality Document homology_coeff_field values in cubical --- .../include/gudhi/Persistent_cohomology/Field_Zp.h | 8 ++------ .../test/persistent_cohomology_unit_test.cpp | 5 +++++ src/python/gudhi/cubical_complex.pyx | 4 ++-- src/python/gudhi/periodic_cubical_complex.pyx | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) (limited to 'src/python') diff --git a/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h b/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h index 7ecc9a80..8ec89e41 100644 --- a/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h +++ b/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h @@ -38,12 +38,12 @@ class Field_Zp { Prime = charac; - // Check that the provided prime is less than the maximum allowed as int and calculation below : 46337 ; i.e (max_prime-1)**2 <= INT_MAX + // Check that the provided prime is less than the maximum allowed as int, calculation below, and 'plus_times_equal' function : 46337 ; i.e (max_prime-1)*max_prime <= INT_MAX if(Prime > 46337) throw std::invalid_argument("Maximum homology_coeff_field allowed value is 46337"); // Check for primality - if ((Prime == 0) || (Prime == 1) || ((Prime > 3) && ((Prime % 2 == 0) || (Prime % 3 == 0)))) + if (Prime <= 1) throw std::invalid_argument("homology_coeff_field must be a prime number"); inverse_.clear(); @@ -59,10 +59,6 @@ class Field_Zp { mult = inv * i; } inverse_.push_back(inv); - if ( (i*i <= Prime) && (((i-5)%6) == 0) ) { - if ((Prime % i == 0) || (Prime % (i + 2) == 0)) - throw std::invalid_argument("homology_coeff_field must be a prime number"); - } } } diff --git a/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp b/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp index 35bb5988..041cb0fd 100644 --- a/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp +++ b/src/Persistent_cohomology/test/persistent_cohomology_unit_test.cpp @@ -176,6 +176,11 @@ BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_5 ) test_rips_persistence_in_dimension(5); } +BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_6 ) +{ + BOOST_CHECK_THROW(test_rips_persistence_in_dimension(6), std::invalid_argument); +} + BOOST_AUTO_TEST_CASE( rips_persistent_cohomology_single_field_dim_11 ) { test_rips_persistence_in_dimension(11); diff --git a/src/python/gudhi/cubical_complex.pyx b/src/python/gudhi/cubical_complex.pyx index adc40499..97c69a2d 100644 --- a/src/python/gudhi/cubical_complex.pyx +++ b/src/python/gudhi/cubical_complex.pyx @@ -147,7 +147,7 @@ cdef class CubicalComplex: :func:`persistence` returns. :param homology_coeff_field: The homology coefficient field. Must be a - prime number + prime number. Default value is 11. Max is 46337. :type homology_coeff_field: int. :param min_persistence: The minimum persistence value to take into account (strictly greater than min_persistence). Default value is @@ -169,7 +169,7 @@ cdef class CubicalComplex: """This function computes and returns the persistence of the complex. :param homology_coeff_field: The homology coefficient field. Must be a - prime number + prime number. Default value is 11. Max is 46337. :type homology_coeff_field: int. :param min_persistence: The minimum persistence value to take into account (strictly greater than min_persistence). Default value is diff --git a/src/python/gudhi/periodic_cubical_complex.pyx b/src/python/gudhi/periodic_cubical_complex.pyx index 0eaa5867..ef1d3080 100644 --- a/src/python/gudhi/periodic_cubical_complex.pyx +++ b/src/python/gudhi/periodic_cubical_complex.pyx @@ -148,7 +148,7 @@ cdef class PeriodicCubicalComplex: :func:`persistence` returns. :param homology_coeff_field: The homology coefficient field. Must be a - prime number + prime number. Default value is 11. Max is 46337. :type homology_coeff_field: int. :param min_persistence: The minimum persistence value to take into account (strictly greater than min_persistence). Default value is @@ -170,7 +170,7 @@ cdef class PeriodicCubicalComplex: """This function computes and returns the persistence of the complex. :param homology_coeff_field: The homology coefficient field. Must be a - prime number + prime number. Default value is 11. Max is 46337. :type homology_coeff_field: int. :param min_persistence: The minimum persistence value to take into account (strictly greater than min_persistence). Default value is -- cgit v1.2.3 From a91e3bc16d511ef66bb296da6a990a0723100657 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 5 Aug 2021 18:00:40 +0200 Subject: mailing lists migration and rephrase contributions in installation guides --- .github/for_maintainers/new_gudhi_version_creation.md | 4 ++-- src/common/doc/installation.h | 8 +++++--- src/python/doc/installation.rst | 9 +++++---- src/python/setup.py.in | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) (limited to 'src/python') diff --git a/.github/for_maintainers/new_gudhi_version_creation.md b/.github/for_maintainers/new_gudhi_version_creation.md index d6c4cdd3..3e5295c5 100644 --- a/.github/for_maintainers/new_gudhi_version_creation.md +++ b/.github/for_maintainers/new_gudhi_version_creation.md @@ -128,5 +128,5 @@ docker image on docker hub. ## Mail sending Send version mail to the following lists : -* gudhi-devel@lists.gforge.inria.fr -* gudhi-users@lists.gforge.inria.fr (not for release candidate) +* gudhi-devel@inria.fr +* gudhi-users@inria.fr (not for release candidate) diff --git a/src/common/doc/installation.h b/src/common/doc/installation.h index 610aa17e..5d40a48e 100644 --- a/src/common/doc/installation.h +++ b/src/common/doc/installation.h @@ -243,10 +243,12 @@ make \endverbatim * Witness_complex/example_nearest_landmark_table.cpp * * \section Contributions Bug reports and contributions - * Please help us improving the quality of the GUDHI library. You may report bugs or suggestions to: - * \verbatim Contact: gudhi-users@lists.gforge.inria.fr \endverbatim + * Please help us improving the quality of the GUDHI library. + * You may report bugs or + * contact us for any suggestions. * - * GUDHI is open to external contributions. If you want to join our development team, please contact us. + * GUDHI is open to external contributions. If you want to join our development team, please take some time to read our + * contributing guide. * */ diff --git a/src/python/doc/installation.rst b/src/python/doc/installation.rst index 9c16b04e..56f27b21 100644 --- a/src/python/doc/installation.rst +++ b/src/python/doc/installation.rst @@ -396,8 +396,9 @@ TensorFlow Bug reports and contributions ***************************** -Please help us improving the quality of the GUDHI library. You may report bugs or suggestions to: +Please help us improving the quality of the GUDHI library. +You may `report bugs `_ or +`contact us `_ for any suggestions. - Contact: gudhi-users@lists.gforge.inria.fr - -GUDHI is open to external contributions. If you want to join our development team, please contact us. +GUDHI is open to external contributions. If you want to join our development team, please take some time to read our +`contributing guide `_. diff --git a/src/python/setup.py.in b/src/python/setup.py.in index 759ec8d8..c400b601 100644 --- a/src/python/setup.py.in +++ b/src/python/setup.py.in @@ -71,7 +71,7 @@ setup( name = 'gudhi', packages=find_packages(), # find_namespace_packages(include=["gudhi*"]) author='GUDHI Editorial Board', - author_email='gudhi-contact@lists.gforge.inria.fr', + author_email='gudhi-contact@inria.fr', version='@GUDHI_VERSION@', url='https://gudhi.inria.fr/', project_urls={ -- cgit v1.2.3 From 575beed582f9288d83a403f4f578731f172f7f5f Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 11 Aug 2021 14:35:25 +0200 Subject: Add test for sphere and torus Fix numerical approximations inconsistencies with dim fraction exponent when generating points as grid on torus Add notes in doc regarding the torus versions use cases --- src/common/include/gudhi/random_point_generators.h | 2 +- src/python/CMakeLists.txt | 3 ++ src/python/doc/datasets_generators.rst | 5 +++ src/python/gudhi/datasets/generators/_points.cc | 4 +++ src/python/gudhi/datasets/generators/points.py | 5 +-- src/python/test/test_datasets_generators.py | 40 ++++++++++++++++++++++ 6 files changed, 54 insertions(+), 5 deletions(-) create mode 100755 src/python/test/test_datasets_generators.py (limited to 'src/python') diff --git a/src/common/include/gudhi/random_point_generators.h b/src/common/include/gudhi/random_point_generators.h index 07e4f3da..25a7392d 100644 --- a/src/common/include/gudhi/random_point_generators.h +++ b/src/common/include/gudhi/random_point_generators.h @@ -227,7 +227,7 @@ std::vector generate_points_on_torus_d(std::size_t num std::vector points; points.reserve(num_points); if (sample == "grid") { - std::size_t num_slices = (std::size_t)std::pow(num_points, 1. / dim); + std::size_t num_slices = (std::size_t)std::pow(num_points + .5, 1. / dim); // add .5 to avoid rounding down with numerical approximations generate_grid_points_on_torus_d( k, dim, num_slices, std::back_inserter(points), radius_noise_percentage); } else { diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 8c46004a..f30dfe6d 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -443,6 +443,9 @@ if(PYTHONINTERP_FOUND) # Euclidean witness add_gudhi_py_test(test_euclidean_witness_complex) + # Datasets generators + add_gudhi_py_test(test_datasets_generators) # TODO separate full python datasets generators in another test file independant from CGAL ? + endif (NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) # Cubical diff --git a/src/python/doc/datasets_generators.rst b/src/python/doc/datasets_generators.rst index 2802eccd..e63dde82 100644 --- a/src/python/doc/datasets_generators.rst +++ b/src/python/doc/datasets_generators.rst @@ -60,6 +60,9 @@ Otherwise, if set to **'grid'**, the points are generated on a grid and would be ( [n\_samples^{1 \over {dim}}]^{dim}, 2*dim ) + +**Note:** This version is recommended when the user wishes to use **'grid'** as sample type, or **'random'** with a relatively small number of samples (~ less than 150). + Example """"""" .. code-block:: python @@ -81,6 +84,8 @@ The user should provide the number of points to be generated on the torus :code: The :code:`sample` argument is optional and is set to **'random'** by default. The other allowed value of sample type is **'grid'**. +**Note:** This version is recommended when the user wishes to use **'random'** as sample type with a great number of samples and a low dimension. + Example """"""" .. code-block:: python diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc index 6bbdf284..3d38ff90 100644 --- a/src/python/gudhi/datasets/generators/_points.cc +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -48,6 +48,10 @@ py::array_t generate_points_on_sphere(size_t n_samples, int ambient_dim, py::array_t generate_points_on_torus(size_t n_samples, int dim, std::string sample) { + if ( (sample != "random") && (sample != "grid")) { + throw pybind11::value_error("This sample type is not supported"); + } + std::vector points_generated; { diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py index 3870dea6..daada486 100644 --- a/src/python/gudhi/datasets/generators/points.py +++ b/src/python/gudhi/datasets/generators/points.py @@ -23,7 +23,7 @@ def _generate_random_points_on_torus(n_samples, dim): def _generate_grid_points_on_torus(n_samples, dim): # Generate points on a dim-torus as a grid - n_samples_grid = int(n_samples**(1./dim)) + n_samples_grid = int((n_samples+.5)**(1./dim)) # add .5 to avoid rounding down with numerical approximations alpha = np.linspace(0, 2*np.pi, n_samples_grid, endpoint=False) array_points_inter = np.column_stack([np.cos(alpha), np.sin(alpha)]) @@ -45,12 +45,9 @@ def torus(n_samples, dim, sample='random'): """ if sample == 'random': # Generate points randomly - print("Sample is random") return _generate_random_points_on_torus(n_samples, dim) elif sample == 'grid': # Generate points on a grid - print("Sample is grid") return _generate_grid_points_on_torus(n_samples, dim) else: raise ValueError("Sample type '{}' is not supported".format(sample)) - return diff --git a/src/python/test/test_datasets_generators.py b/src/python/test/test_datasets_generators.py new file mode 100755 index 00000000..656c30ee --- /dev/null +++ b/src/python/test/test_datasets_generators.py @@ -0,0 +1,40 @@ +""" This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. + See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. + Author(s): Hind Montassif + + Copyright (C) 2021 Inria + + Modification(s): + - YYYY/MM Author: Description of the modification +""" + +from gudhi.datasets.generators import points +from gudhi.datasets.generators import _points + +import pytest + +def test_sphere(): + assert _points.sphere(n_samples = 10, ambient_dim = 2, radius = 1., sample = 'random').shape == (10, 2) + + with pytest.raises(ValueError): + _points.sphere(n_samples = 10, ambient_dim = 2, radius = 1., sample = 'other') + +def test_torus(): + assert _points.torus(n_samples = 64, dim = 3, sample = 'random').shape == (64, 6) + assert _points.torus(n_samples = 64, dim = 3, sample = 'grid').shape == (64, 6) + + assert _points.torus(n_samples = 10, dim = 4, sample = 'random').shape == (10, 8) + assert _points.torus(n_samples = 10, dim = 4, sample = 'grid').shape == (1, 8) + + with pytest.raises(ValueError): + _points.torus(n_samples = 10, dim = 4, sample = 'other') + +def test_torus_full_python(): + assert points.torus(n_samples = 64, dim = 3, sample = 'random').shape == (64, 6) + assert points.torus(n_samples = 64, dim = 3, sample = 'grid').shape == (64, 6) + + assert points.torus(n_samples = 10, dim = 4, sample = 'random').shape == (10, 8) + assert points.torus(n_samples = 10, dim = 4, sample = 'grid').shape == (1, 8) + + with pytest.raises(ValueError): + points.torus(n_samples = 10, dim = 4, sample = 'other') -- cgit v1.2.3 From b786b34a30544cdfcdf04caae6db0e20e940e14d Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 26 Aug 2021 14:56:25 +0200 Subject: remove install_requires as pep 518 recommands --- src/python/setup.py.in | 1 - 1 file changed, 1 deletion(-) (limited to 'src/python') diff --git a/src/python/setup.py.in b/src/python/setup.py.in index c400b601..fd22962e 100644 --- a/src/python/setup.py.in +++ b/src/python/setup.py.in @@ -86,6 +86,5 @@ setup( long_description=long_description, ext_modules = ext_modules, install_requires = ['numpy >= 1.15.0',], - setup_requires = ['cython','numpy >= 1.15.0','pybind11',], package_data={"": ["*.dll"], }, ) -- cgit v1.2.3 From 6ffa047e76b680cc7b36b36d330d52a7419ce1ba Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 26 Aug 2021 15:00:19 +0200 Subject: Add all build dependencies (to be buildable in a virtual env - even if cmake already detects it) --- src/python/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/python/pyproject.toml (limited to 'src/python') diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml new file mode 100644 index 00000000..a9fb4985 --- /dev/null +++ b/src/python/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel", "numpy>=1.15.0", "cython", "pybind11"] +build-backend = "setuptools.build_meta" -- cgit v1.2.3 From 9c96a0711853c342fda5cf00e06d795c55783bf8 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 26 Aug 2021 15:04:48 +0200 Subject: Deploy pyproject.toml --- src/python/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index f534fc2a..927440ef 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -278,6 +278,7 @@ if(PYTHONINTERP_FOUND) # Some files for pip package file(COPY "introduction.rst" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/") + file(COPY "pyproject.toml" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/") add_custom_command( OUTPUT gudhi.so -- cgit v1.2.3 From cdd91e723aaf9e99d8161397fee683824e67cd78 Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 26 Aug 2021 15:37:32 +0200 Subject: Pybind11 is mandatory for python module (as explained in the doc and as it is imported by setup.py) --- src/python/CMakeLists.txt | 76 ++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 41 deletions(-) (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 927440ef..96107cfe 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -41,14 +41,15 @@ function( add_gudhi_debug_info DEBUG_INFO ) endfunction( add_gudhi_debug_info ) if(PYTHONINTERP_FOUND) - if(PYBIND11_FOUND) + if(PYBIND11_FOUND AND CYTHON_FOUND) add_gudhi_debug_info("Pybind11 version ${PYBIND11_VERSION}") + # PyBind11 modules set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'bottleneck', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'hera', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'clustering', ") set(GUDHI_PYTHON_MODULES_EXTRA "${GUDHI_PYTHON_MODULES_EXTRA}'datasets', ") - endif() - if(CYTHON_FOUND) + + # Cython modules set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'off_reader', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'simplex_tree', ") set(GUDHI_PYTHON_MODULES "${GUDHI_PYTHON_MODULES}'rips_complex', ") @@ -299,35 +300,30 @@ if(PYTHONINTERP_FOUND) if(SCIPY_FOUND) if(SKLEARN_FOUND) if(OT_FOUND) - if(PYBIND11_FOUND) - if(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) - set (GUDHI_SPHINX_MESSAGE "Generating API documentation with Sphinx in ${CMAKE_CURRENT_BINARY_DIR}/sphinx/") - # User warning - Sphinx is a static pages generator, and configured to work fine with user_version - # Images and biblio warnings because not found on developper version - if (GUDHI_PYTHON_PATH STREQUAL "src/python") - set (GUDHI_SPHINX_MESSAGE "${GUDHI_SPHINX_MESSAGE} \n WARNING : Sphinx is configured for user version, you run it on developper version. Images and biblio will miss") - endif() - # sphinx target requires gudhi.so, because conf.py reads gudhi version from it - add_custom_target(sphinx - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc - COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" - ${SPHINX_PATH} -b html ${CMAKE_CURRENT_SOURCE_DIR}/doc ${CMAKE_CURRENT_BINARY_DIR}/sphinx - DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/gudhi.so" - COMMENT "${GUDHI_SPHINX_MESSAGE}" VERBATIM) - add_test(NAME sphinx_py_test - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" - ${SPHINX_PATH} -b doctest ${CMAKE_CURRENT_SOURCE_DIR}/doc ${CMAKE_CURRENT_BINARY_DIR}/doctest) - # Set missing or not modules - set(GUDHI_MODULES ${GUDHI_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MODULES") - else(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) - message("++ Python documentation module will not be compiled because it requires a Eigen3 and CGAL version >= 4.11.0") - set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) - else(PYBIND11_FOUND) - message("++ Python documentation module will not be compiled because pybind11 was not found") + if(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) + set (GUDHI_SPHINX_MESSAGE "Generating API documentation with Sphinx in ${CMAKE_CURRENT_BINARY_DIR}/sphinx/") + # User warning - Sphinx is a static pages generator, and configured to work fine with user_version + # Images and biblio warnings because not found on developper version + if (GUDHI_PYTHON_PATH STREQUAL "src/python") + set (GUDHI_SPHINX_MESSAGE "${GUDHI_SPHINX_MESSAGE} \n WARNING : Sphinx is configured for user version, you run it on developper version. Images and biblio will miss") + endif() + # sphinx target requires gudhi.so, because conf.py reads gudhi version from it + add_custom_target(sphinx + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc + COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" + ${SPHINX_PATH} -b html ${CMAKE_CURRENT_SOURCE_DIR}/doc ${CMAKE_CURRENT_BINARY_DIR}/sphinx + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/gudhi.so" + COMMENT "${GUDHI_SPHINX_MESSAGE}" VERBATIM) + add_test(NAME sphinx_py_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" + ${SPHINX_PATH} -b doctest ${CMAKE_CURRENT_SOURCE_DIR}/doc ${CMAKE_CURRENT_BINARY_DIR}/doctest) + # Set missing or not modules + set(GUDHI_MODULES ${GUDHI_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MODULES") + else(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) + message("++ Python documentation module will not be compiled because it requires a Eigen3 and CGAL version >= 4.11.0") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(PYBIND11_FOUND) + endif(NOT CGAL_WITH_EIGEN3_VERSION VERSION_LESS 4.11.0) else(OT_FOUND) message("++ Python documentation module will not be compiled because POT was not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python-documentation" CACHE INTERNAL "GUDHI_MISSING_MODULES") @@ -403,9 +399,7 @@ if(PYTHONINTERP_FOUND) COMMAND ${CMAKE_COMMAND} -E env "${GUDHI_PYTHON_PATH_ENV}" ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/example/bottleneck_basic_example.py") - if (PYBIND11_FOUND) - add_gudhi_py_test(test_bottleneck_distance) - endif() + add_gudhi_py_test(test_bottleneck_distance) # Cover complex file(COPY ${CMAKE_SOURCE_DIR}/data/points/human.off DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/) @@ -519,14 +513,14 @@ if(PYTHONINTERP_FOUND) add_gudhi_py_test(test_reader_utils) # Wasserstein - if(OT_FOUND AND PYBIND11_FOUND) + if(OT_FOUND) # EagerPy dependency because of enable_autodiff=True if(EAGERPY_FOUND) add_gudhi_py_test(test_wasserstein_distance) endif() + add_gudhi_py_test(test_wasserstein_barycenter) - endif() - if(OT_FOUND) + if(TORCH_FOUND AND TENSORFLOW_FOUND AND EAGERPY_FOUND) add_gudhi_py_test(test_wasserstein_with_tensors) endif() @@ -547,7 +541,7 @@ if(PYTHONINTERP_FOUND) endif() # Tomato - if(SCIPY_FOUND AND SKLEARN_FOUND AND PYBIND11_FOUND) + if(SCIPY_FOUND AND SKLEARN_FOUND) add_gudhi_py_test(test_tomato) endif() @@ -564,10 +558,10 @@ if(PYTHONINTERP_FOUND) # Set missing or not modules set(GUDHI_MODULES ${GUDHI_MODULES} "python" CACHE INTERNAL "GUDHI_MODULES") - else(CYTHON_FOUND) - message("++ Python module will not be compiled because cython was not found") + else(PYBIND11_FOUND AND CYTHON_FOUND) + message("++ Python module will not be compiled because cython and/or pybind11 was/were not found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python" CACHE INTERNAL "GUDHI_MISSING_MODULES") - endif(CYTHON_FOUND) + endif(PYBIND11_FOUND AND CYTHON_FOUND) else(PYTHONINTERP_FOUND) message("++ Python module will not be compiled because no Python interpreter was found") set(GUDHI_MISSING_MODULES ${GUDHI_MISSING_MODULES} "python" CACHE INTERNAL "GUDHI_MISSING_MODULES") -- cgit v1.2.3 From 3954d88bf505e4ae5af0dec81b978b1cdf0f5adf Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Thu, 26 Aug 2021 15:38:41 +0200 Subject: Fix #416 --- src/python/setup.py.in | 1 + 1 file changed, 1 insertion(+) (limited to 'src/python') diff --git a/src/python/setup.py.in b/src/python/setup.py.in index fd22962e..23746998 100644 --- a/src/python/setup.py.in +++ b/src/python/setup.py.in @@ -82,6 +82,7 @@ setup( }, description='The Gudhi library is an open source library for ' \ 'Computational Topology and Topological Data Analysis (TDA).', + data_files=[('.', ['./introduction.rst'])], long_description_content_type='text/x-rst', long_description=long_description, ext_modules = ext_modules, -- cgit v1.2.3 From 2024c0af61c1b14e50eccfae9a0011cb061b16d2 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Fri, 27 Aug 2021 11:34:08 +0200 Subject: Fix issue #314 Add overflow and nan warnings in knn when using torch and hnswlib --- src/python/gudhi/point_cloud/knn.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'src/python') diff --git a/src/python/gudhi/point_cloud/knn.py b/src/python/gudhi/point_cloud/knn.py index 829bf1bf..7a5616e3 100644 --- a/src/python/gudhi/point_cloud/knn.py +++ b/src/python/gudhi/point_cloud/knn.py @@ -257,6 +257,12 @@ class KNearestNeighbors: if ef is not None: self.graph.set_ef(ef) neighbors, distances = self.graph.knn_query(X, k, num_threads=self.params["num_threads"]) + if numpy.any(numpy.isnan(distances)): + import warnings + warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) + if numpy.any(numpy.isinf(distances)): + import warnings + warnings.warn("Overflow value encountered while computing 'distances'", RuntimeWarning) # The k nearest neighbors are always sorted. I couldn't find it in the doc, but the code calls searchKnn, # which returns a priority_queue, and then fills the return array backwards with top/pop on the queue. if self.return_index: @@ -290,6 +296,12 @@ class KNearestNeighbors: if self.return_index: if self.return_distance: distances, neighbors = mat.Kmin_argKmin(k, dim=1) + if distances.isnan().any(): + import warnings + warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) + if distances.isinf().any(): + import warnings + warnings.warn("Overflow encountered while computing 'distances'", RuntimeWarning) if p != numpy.inf: distances = distances ** (1.0 / p) return neighbors, distances @@ -298,6 +310,12 @@ class KNearestNeighbors: return neighbors if self.return_distance: distances = mat.Kmin(k, dim=1) + if distances.isnan().any(): + import warnings + warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) + if distances.isinf().any(): + import warnings + warnings.warn("Overflow encountered while computing 'distances'", RuntimeWarning) if p != numpy.inf: distances = distances ** (1.0 / p) return distances -- cgit v1.2.3 From 7ea4e020af2fa8bf2fdfefe85ca24a1bcc2d08e1 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Mon, 30 Aug 2021 15:34:15 +0200 Subject: Fix dtm and knn failing tests --- src/python/gudhi/point_cloud/knn.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/point_cloud/knn.py b/src/python/gudhi/point_cloud/knn.py index 7a5616e3..dec5f88f 100644 --- a/src/python/gudhi/point_cloud/knn.py +++ b/src/python/gudhi/point_cloud/knn.py @@ -296,10 +296,10 @@ class KNearestNeighbors: if self.return_index: if self.return_distance: distances, neighbors = mat.Kmin_argKmin(k, dim=1) - if distances.isnan().any(): + if torch.isnan(distances).any(): import warnings warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) - if distances.isinf().any(): + if torch.isinf(distances).any(): import warnings warnings.warn("Overflow encountered while computing 'distances'", RuntimeWarning) if p != numpy.inf: @@ -310,10 +310,10 @@ class KNearestNeighbors: return neighbors if self.return_distance: distances = mat.Kmin(k, dim=1) - if distances.isnan().any(): + if torch.isnan(distances).any(): import warnings warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) - if distances.isinf().any(): + if torch.isinf(distances).any(): import warnings warnings.warn("Overflow encountered while computing 'distances'", RuntimeWarning) if p != numpy.inf: -- cgit v1.2.3 From 4db14bad8006638fc8249cb867a1720f581e044d Mon Sep 17 00:00:00 2001 From: ROUVREAU Vincent Date: Tue, 31 Aug 2021 18:18:51 +0200 Subject: Fix broken links. Add a tool to generate cpp example page --- biblio/bibliography.bib | 1 - scripts/cpp_examples_for_doxygen.py | 16 ++ .../utilities/bottleneckdistance.md | 4 +- src/common/doc/examples.h | 164 ++++++++++++--------- src/common/doc/installation.h | 40 ++--- src/python/doc/installation.rst | 2 +- 6 files changed, 137 insertions(+), 90 deletions(-) create mode 100644 scripts/cpp_examples_for_doxygen.py (limited to 'src/python') diff --git a/biblio/bibliography.bib b/biblio/bibliography.bib index 16fa29d0..eed871be 100644 --- a/biblio/bibliography.bib +++ b/biblio/bibliography.bib @@ -15,7 +15,6 @@ title = {{Statistical analysis and parameter selection for Mapper}}, volume = {19}, year = {2018}, url = {http://jmlr.org/papers/v19/17-291.html}, -doi = {10.5555/3291125.3291137} } @inproceedings{Dey13, diff --git a/scripts/cpp_examples_for_doxygen.py b/scripts/cpp_examples_for_doxygen.py new file mode 100644 index 00000000..5c091c4f --- /dev/null +++ b/scripts/cpp_examples_for_doxygen.py @@ -0,0 +1,16 @@ +import os +import glob + +for gd_mod in glob.glob("src/*/"): + mod_files = [] + for paths in [gd_mod + 'utilities', gd_mod + 'example']: + if os.path.isdir(paths): + for root, dirs, files in os.walk(paths): + for file in files: + if file.endswith(".cpp"): + mod_files.append(str(os.path.join(root, file)).split(paths)[1][1:]) + if len(mod_files) > 0: + mod = str(gd_mod).split('/')[1] + print(' * \section ' + mod + '_example_section ' + mod) + for file in mod_files: + print(' * @example ' + file) diff --git a/src/Bottleneck_distance/utilities/bottleneckdistance.md b/src/Bottleneck_distance/utilities/bottleneckdistance.md index a81426cf..2f5dedc9 100644 --- a/src/Bottleneck_distance/utilities/bottleneckdistance.md +++ b/src/Bottleneck_distance/utilities/bottleneckdistance.md @@ -10,14 +10,14 @@ Leave the lines above as it is required by the web site generator 'Jekyll' {:/comment} -## bottleneck_read_file_example ## +## bottleneck_distance ## This program computes the Bottleneck distance between two persistence diagram files. **Usage** ``` - bottleneck_read_file_example [] + bottleneck_distance [] ``` where diff --git a/src/common/doc/examples.h b/src/common/doc/examples.h index b557727b..a2de4335 100644 --- a/src/common/doc/examples.h +++ b/src/common/doc/examples.h @@ -1,98 +1,130 @@ -// List of GUDHI examples - Doxygen needs at least a file tag to analyse comments -// In user_version, `find . -name "*.cpp"` in example and utilities folders +// List of GUDHI examples and utils - Doxygen needs at least a file tag to analyse comments +// Generated from scripts/cpp_examples_for_doxygen.py /*! @file Examples - * \section Alpha_complex_examples Alpha complex - * @example Alpha_complex_from_off.cpp + * \section Skeleton_blocker_example_section Skeleton_blocker + * @example Skeleton_blocker_iteration.cpp + * @example Skeleton_blocker_from_simplices.cpp + * @example Skeleton_blocker_link.cpp + * \section Alpha_complex_example_section Alpha_complex + * @example alpha_complex_persistence.cpp + * @example alpha_complex_3d_persistence.cpp * @example Alpha_complex_from_points.cpp - * \section bottleneck_examples bottleneck - * @example bottleneck_basic_example.cpp - * @example alpha_rips_persistence_bottleneck_distance.cpp - * @example example_nearest_landmark_table.cpp - * @example example_witness_complex_off.cpp - * @example example_witness_complex_sphere.cpp - * @example example_strong_witness_complex_off.cpp + * @example Alpha_complex_3d_from_points.cpp + * @example Weighted_alpha_complex_3d_from_points.cpp + * @example Fast_alpha_complex_from_off.cpp + * @example Alpha_complex_from_off.cpp + * @example Weighted_alpha_complex_from_points.cpp + * \section Simplex_tree_example_section Simplex_tree + * @example cech_complex_cgal_mini_sphere_3d.cpp * @example mini_simplex_tree.cpp + * @example example_alpha_shapes_3_simplex_tree_from_off_file.cpp * @example graph_expansion_with_blocker.cpp * @example simple_simplex_tree.cpp * @example simplex_tree_from_cliques_of_graph.cpp - * @example example_alpha_shapes_3_simplex_tree_from_off_file.cpp - * @example cech_complex_cgal_mini_sphere_3d.cpp - * @example plain_homology.cpp - * @example persistence_from_file.cpp - * @example rips_persistence_step_by_step.cpp + * \section Collapse_example_section Collapse + * @example point_cloud_edge_collapse_rips_persistence.cpp + * @example distance_matrix_edge_collapse_rips_persistence.cpp + * @example edge_collapse_basic_example.cpp + * @example edge_collapse_conserve_persistence.cpp + * \section Persistent_cohomology_example_section Persistent_cohomology * @example rips_persistence_via_boundary_matrix.cpp + * @example rips_persistence_step_by_step.cpp + * @example plain_homology.cpp * @example custom_persistence_sort.cpp * @example persistence_from_simple_simplex_tree.cpp + * @example persistence_from_file.cpp * @example rips_multifield_persistence.cpp - * @example Skeleton_blocker_from_simplices.cpp - * @example Skeleton_blocker_iteration.cpp - * @example Skeleton_blocker_link.cpp - * @example Garland_heckbert.cpp - * @example Rips_contraction.cpp - * @example Random_bitmap_cubical_complex.cpp - * @example example_CGAL_3D_points_off_reader.cpp - * @example example_vector_double_points_off_reader.cpp - * @example example_CGAL_points_off_reader.cpp - * @example example_one_skeleton_rips_from_distance_matrix.cpp - * @example example_one_skeleton_rips_from_points.cpp - * @example example_rips_complex_from_csv_distance_matrix_file.cpp - * @example example_rips_complex_from_off_file.cpp - * @example persistence_intervals.cpp - * @example persistence_vectors.cpp - * @example persistence_heat_maps.cpp - * @example persistence_landscape_on_grid.cpp - * @example persistence_landscape.cpp - * @example example_basic.cpp - * @example example_with_perturb.cpp - * @example example_custom_distance.cpp - * @example example_choose_n_farthest_points.cpp - * @example example_sparsify_point_set.cpp - * @example example_pick_n_random_points.cpp - * @example CoordGIC.cpp + * \section Nerve_GIC_example_section Nerve_GIC + * @example VoronoiGIC.cpp * @example Nerve.cpp * @example FuncGIC.cpp - * @example VoronoiGIC.cpp - * @example example_spatial_searching.cpp - * @example alpha_complex_3d_persistence.cpp - * @example alpha_complex_persistence.cpp - * @example Weighted_alpha_complex_3d_from_points.cpp - * @example bottleneck_distance.cpp - * @example weak_witness_persistence.cpp - * @example strong_witness_persistence.cpp - * @example cubical_complex_persistence.cpp - * @example periodic_cubical_complex_persistence.cpp - * @example off_file_from_shape_generator.cpp - * @example rips_distance_matrix_persistence.cpp + * @example CoordGIC.cpp + * \section Rips_complex_example_section Rips_complex * @example rips_persistence.cpp + * @example sparse_rips_persistence.cpp + * @example rips_correlation_matrix_persistence.cpp + * @example rips_distance_matrix_persistence.cpp + * @example example_one_skeleton_rips_from_correlation_matrix.cpp + * @example example_rips_complex_from_csv_distance_matrix_file.cpp + * @example example_sparse_rips.cpp + * @example example_one_skeleton_rips_from_distance_matrix.cpp + * @example example_rips_complex_from_off_file.cpp + * @example example_one_skeleton_rips_from_points.cpp + * \section Persistence_representations_example_section Persistence_representations + * @example persistence_landscapes_on_grid/average_landscapes_on_grid.cpp * @example persistence_landscapes_on_grid/create_landscapes_on_grid.cpp + * @example persistence_landscapes_on_grid/compute_distance_of_landscapes_on_grid.cpp * @example persistence_landscapes_on_grid/plot_landscapes_on_grid.cpp * @example persistence_landscapes_on_grid/compute_scalar_product_of_landscapes_on_grid.cpp - * @example persistence_landscapes_on_grid/compute_distance_of_landscapes_on_grid.cpp - * @example persistence_landscapes_on_grid/average_landscapes_on_grid.cpp * @example persistence_intervals/compute_birth_death_range_in_persistence_diagram.cpp - * @example persistence_intervals/compute_number_of_dominant_intervals.cpp - * @example persistence_intervals/plot_persistence_Betti_numbers.cpp * @example persistence_intervals/plot_persistence_intervals.cpp * @example persistence_intervals/plot_histogram_of_intervals_lengths.cpp * @example persistence_intervals/compute_bottleneck_distance.cpp - * @example persistence_heat_maps/create_pssk.cpp - * @example persistence_heat_maps/create_p_h_m_weighted_by_arctan_of_their_persistence.cpp + * @example persistence_intervals/plot_persistence_Betti_numbers.cpp + * @example persistence_intervals/compute_number_of_dominant_intervals.cpp + * @example persistence_heat_maps/average_persistence_heat_maps.cpp * @example persistence_heat_maps/create_p_h_m_weighted_by_squared_diag_distance.cpp - * @example persistence_heat_maps/compute_distance_of_persistence_heat_maps.cpp - * @example persistence_heat_maps/compute_scalar_product_of_persistence_heat_maps.cpp + * @example persistence_heat_maps/create_pssk.cpp * @example persistence_heat_maps/create_p_h_m_weighted_by_distance_from_diagonal.cpp - * @example persistence_heat_maps/average_persistence_heat_maps.cpp + * @example persistence_heat_maps/compute_distance_of_persistence_heat_maps.cpp + * @example persistence_heat_maps/create_p_h_m_weighted_by_arctan_of_their_persistence.cpp * @example persistence_heat_maps/plot_persistence_heat_map.cpp + * @example persistence_heat_maps/compute_scalar_product_of_persistence_heat_maps.cpp * @example persistence_heat_maps/create_persistence_heat_maps.cpp - * @example persistence_vectors/plot_persistence_vectors.cpp - * @example persistence_vectors/compute_distance_of_persistence_vectors.cpp * @example persistence_vectors/average_persistence_vectors.cpp + * @example persistence_vectors/plot_persistence_vectors.cpp * @example persistence_vectors/create_persistence_vectors.cpp * @example persistence_vectors/compute_scalar_product_of_persistence_vectors.cpp - * @example persistence_landscapes/average_landscapes.cpp - * @example persistence_landscapes/compute_scalar_product_of_landscapes.cpp + * @example persistence_vectors/compute_distance_of_persistence_vectors.cpp * @example persistence_landscapes/create_landscapes.cpp + * @example persistence_landscapes/average_landscapes.cpp * @example persistence_landscapes/compute_distance_of_landscapes.cpp * @example persistence_landscapes/plot_landscapes.cpp + * @example persistence_landscapes/compute_scalar_product_of_landscapes.cpp + * @example persistence_heat_maps.cpp + * @example sliced_wasserstein.cpp + * @example persistence_intervals.cpp + * @example persistence_vectors.cpp + * @example persistence_landscape.cpp + * @example persistence_landscape_on_grid.cpp + * \section common_example_section common + * @example off_file_from_shape_generator.cpp + * @example example_CGAL_points_off_reader.cpp + * @example example_vector_double_points_off_reader.cpp + * @example example_CGAL_3D_points_off_reader.cpp + * \section Subsampling_example_section Subsampling + * @example example_pick_n_random_points.cpp + * @example example_choose_n_farthest_points.cpp + * @example example_sparsify_point_set.cpp + * @example example_custom_distance.cpp + * \section Contraction_example_section Contraction + * @example Rips_contraction.cpp + * @example Garland_heckbert.cpp + * \section Bottleneck_distance_example_section Bottleneck_distance + * @example bottleneck_distance.cpp + * @example bottleneck_basic_example.cpp + * @example alpha_rips_persistence_bottleneck_distance.cpp + * \section Tangential_complex_example_section Tangential_complex + * @example example_basic.cpp + * @example example_with_perturb.cpp + * \section Cech_complex_example_section Cech_complex + * @example cech_persistence.cpp + * @example cech_complex_example_from_points.cpp + * @example cech_complex_step_by_step.cpp + * \section Spatial_searching_example_section Spatial_searching + * @example example_spatial_searching.cpp + * \section Bitmap_cubical_complex_example_section Bitmap_cubical_complex + * @example periodic_cubical_complex_persistence.cpp + * @example cubical_complex_persistence.cpp + * @example Random_bitmap_cubical_complex.cpp + * \section Toplex_map_example_section Toplex_map + * @example simple_toplex_map.cpp + * \section Witness_complex_example_section Witness_complex + * @example weak_witness_persistence.cpp + * @example strong_witness_persistence.cpp + * @example example_witness_complex_sphere.cpp + * @example example_strong_witness_complex_off.cpp + * @example example_nearest_landmark_table.cpp + * @example example_witness_complex_off.cpp */ diff --git a/src/common/doc/installation.h b/src/common/doc/installation.h index 5d40a48e..609f2f2f 100644 --- a/src/common/doc/installation.h +++ b/src/common/doc/installation.h @@ -88,9 +88,9 @@ make \endverbatim * Witness_complex/example_witness_complex_off.cpp * \li * Witness_complex/example_witness_complex_sphere.cpp - * \li + * \li * Alpha_complex/Alpha_complex_from_off.cpp - * \li + * \li * Alpha_complex/Alpha_complex_from_points.cpp * \li * Alpha_complex/alpha_complex_persistence.cpp @@ -100,15 +100,15 @@ make \endverbatim * Bottleneck_distance/alpha_rips_persistence_bottleneck_distance.cpp.cpp * \li * Bottleneck_distance/bottleneck_basic_example.cpp - * \li + * \li * Bottleneck_distance/bottleneck_distance.cpp - * \li + * \li * Nerve_GIC/CoordGIC.cpp - * \li + * \li * Nerve_GIC/FuncGIC.cpp - * \li + * \li * Nerve_GIC/Nerve.cpp - * \li + * \li * Nerve_GIC/VoronoiGIC.cpp * \li * Spatial_searching/example_spatial_searching.cpp @@ -122,7 +122,7 @@ make \endverbatim * Tangential_complex/example_basic.cpp * \li * Tangential_complex/example_with_perturb.cpp - * \li + * \li * Alpha_complex/Weighted_alpha_complex_3d_from_points.cpp * \li * Alpha_complex/alpha_complex_3d_persistence.cpp @@ -134,15 +134,15 @@ make \endverbatim * * The following examples/utilities require the Eigen and will not be * built if Eigen is not installed: - * \li + * \li * Alpha_complex/Alpha_complex_from_off.cpp - * \li + * \li * Alpha_complex/Alpha_complex_from_points.cpp * \li * Alpha_complex/alpha_complex_persistence.cpp * \li * Alpha_complex/alpha_complex_3d_persistence.cpp - * \li + * \li * Alpha_complex/Weighted_alpha_complex_3d_from_points.cpp * \li * Bottleneck_distance/alpha_rips_persistence_bottleneck_distance.cpp.cpp @@ -179,27 +179,27 @@ make \endverbatim * Having Intel® TBB installed is recommended to parallelize and accelerate some GUDHI computations. * * The following examples/utilities are using Intel® TBB if installed: - * \li + * \li * Alpha_complex/Alpha_complex_from_off.cpp - * \li + * \li * Alpha_complex/Alpha_complex_from_points.cpp * \li * Alpha_complex/alpha_complex_3d_persistence.cpp * \li * Alpha_complex/alpha_complex_persistence.cpp - * \li + * \li * Bitmap_cubical_complex/cubical_complex_persistence.cpp - * \li + * \li * Bitmap_cubical_complex/periodic_cubical_complex_persistence.cpp - * \li + * \li * Bitmap_cubical_complex/Random_bitmap_cubical_complex.cpp - * \li + * \li * Nerve_GIC/CoordGIC.cpp - * \li + * \li * Nerve_GIC/FuncGIC.cpp - * \li + * \li * Nerve_GIC/Nerve.cpp - * \li + * \li * Nerve_GIC/VoronoiGIC.cpp * \li * Simplex_tree/simple_simplex_tree.cpp diff --git a/src/python/doc/installation.rst b/src/python/doc/installation.rst index cb7700ce..35c344e3 100644 --- a/src/python/doc/installation.rst +++ b/src/python/doc/installation.rst @@ -359,7 +359,7 @@ Python Optimal Transport ------------------------ The :doc:`Wasserstein distance ` -module requires `POT `_, a library that provides +module requires `POT `_, a library that provides several solvers for optimization problems related to Optimal Transport. PyTorch -- cgit v1.2.3 From 145fcba2de5f174b8fcdeab5ac1997978ffcdc0d Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 8 Sep 2021 18:01:11 +0200 Subject: Set the warning filter to "always" Add test for dtm overflow warning --- src/python/gudhi/point_cloud/knn.py | 6 ++++++ src/python/test/test_dtm.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+) (limited to 'src/python') diff --git a/src/python/gudhi/point_cloud/knn.py b/src/python/gudhi/point_cloud/knn.py index dec5f88f..0724ce94 100644 --- a/src/python/gudhi/point_cloud/knn.py +++ b/src/python/gudhi/point_cloud/knn.py @@ -259,9 +259,11 @@ class KNearestNeighbors: neighbors, distances = self.graph.knn_query(X, k, num_threads=self.params["num_threads"]) if numpy.any(numpy.isnan(distances)): import warnings + warnings.simplefilter("always") warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) if numpy.any(numpy.isinf(distances)): import warnings + warnings.simplefilter("always") warnings.warn("Overflow value encountered while computing 'distances'", RuntimeWarning) # The k nearest neighbors are always sorted. I couldn't find it in the doc, but the code calls searchKnn, # which returns a priority_queue, and then fills the return array backwards with top/pop on the queue. @@ -298,9 +300,11 @@ class KNearestNeighbors: distances, neighbors = mat.Kmin_argKmin(k, dim=1) if torch.isnan(distances).any(): import warnings + warnings.simplefilter("always") warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) if torch.isinf(distances).any(): import warnings + warnings.simplefilter("always") warnings.warn("Overflow encountered while computing 'distances'", RuntimeWarning) if p != numpy.inf: distances = distances ** (1.0 / p) @@ -312,9 +316,11 @@ class KNearestNeighbors: distances = mat.Kmin(k, dim=1) if torch.isnan(distances).any(): import warnings + warnings.simplefilter("always") warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) if torch.isinf(distances).any(): import warnings + warnings.simplefilter("always") warnings.warn("Overflow encountered while computing 'distances'", RuntimeWarning) if p != numpy.inf: distances = distances ** (1.0 / p) diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index 0a52279e..c29471cf 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -13,6 +13,7 @@ import numpy import pytest import torch import math +import warnings def test_dtm_compare_euclidean(): @@ -87,3 +88,16 @@ def test_density(): assert density == pytest.approx(expected) density = DTMDensity(weights=[0.5, 0.5], metric="neighbors", dim=1).fit_transform(distances) assert density == pytest.approx(expected) + +def test_dtm_overflow_warnings(): + pts = numpy.array([[10., 100000000000000000000000000000.], [1000., 100000000000000000000000000.]]) + impl_warn = ["keops", "hnsw"] + + with warnings.catch_warnings(record=True) as w: + for impl in impl_warn: + dtm = DistanceToMeasure(2, q=10000, implementation=impl) + r = dtm.fit_transform(pts) + assert len(w) == 2 + for i in range(len(w)): + assert issubclass(w[i].category, RuntimeWarning) + assert "Overflow" in str(w[i].message) -- cgit v1.2.3 From c5a12380e0744eaa26b59ae043dd4fa160d9f268 Mon Sep 17 00:00:00 2001 From: VincentRouvreau Date: Mon, 13 Sep 2021 16:34:23 +0200 Subject: make_filtration_non_decreasing no more called in alpha complex dD exact version and exact and safe 3d versions --- src/Alpha_complex/doc/Intro_alpha_complex.h | 2 ++ src/Alpha_complex/include/gudhi/Alpha_complex.h | 6 ++++-- src/Alpha_complex/include/gudhi/Alpha_complex_3d.h | 6 ++++-- src/python/doc/alpha_complex_user.rst | 3 +++ 4 files changed, 13 insertions(+), 4 deletions(-) (limited to 'src/python') diff --git a/src/Alpha_complex/doc/Intro_alpha_complex.h b/src/Alpha_complex/doc/Intro_alpha_complex.h index f417ebb2..5ab23720 100644 --- a/src/Alpha_complex/doc/Intro_alpha_complex.h +++ b/src/Alpha_complex/doc/Intro_alpha_complex.h @@ -152,6 +152,8 @@ Table of Contents * not quite define a proper filtration (i.e. non-decreasing with respect to inclusion). * We fix that up by calling `SimplicialComplexForAlpha::make_filtration_non_decreasing()`. * + * \note This is not the case in `exact` version, this is the reason why it is not called in this case. + * * \subsubsection pruneabove Prune above given filtration value * * The simplex tree is pruned from the given maximum \f$ \alpha^2 \f$ value (cf. diff --git a/src/Alpha_complex/include/gudhi/Alpha_complex.h b/src/Alpha_complex/include/gudhi/Alpha_complex.h index b315fa99..e03bb161 100644 --- a/src/Alpha_complex/include/gudhi/Alpha_complex.h +++ b/src/Alpha_complex/include/gudhi/Alpha_complex.h @@ -435,8 +435,10 @@ class Alpha_complex { // -------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------- - // As Alpha value is an approximation, we have to make filtration non decreasing while increasing the dimension - complex.make_filtration_non_decreasing(); + if (!exact) + // As Alpha value is an approximation, we have to make filtration non decreasing while increasing the dimension + // Only in not exact version, cf. https://github.com/GUDHI/gudhi-devel/issues/57 + complex.make_filtration_non_decreasing(); // Remove all simplices that have a filtration value greater than max_alpha_square complex.prune_above_filtration(max_alpha_square); // -------------------------------------------------------------------------------------------- diff --git a/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h b/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h index 4e5fc933..ccc3d852 100644 --- a/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h +++ b/src/Alpha_complex/include/gudhi/Alpha_complex_3d.h @@ -554,8 +554,10 @@ Weighted_alpha_complex_3d::Weighted_point_3 wp0(Weighted_alpha_complex_3d::Bare_ std::clog << "cells \t\t" << count_cells << std::endl; #endif // DEBUG_TRACES // -------------------------------------------------------------------------------------------- - // As Alpha value is an approximation, we have to make filtration non decreasing while increasing the dimension - complex.make_filtration_non_decreasing(); + if (Complexity == complexity::FAST) + // As Alpha value is an approximation, we have to make filtration non decreasing while increasing the dimension + // Only in FAST version, cf. https://github.com/GUDHI/gudhi-devel/issues/57 + complex.make_filtration_non_decreasing(); // Remove all simplices that have a filtration value greater than max_alpha_square complex.prune_above_filtration(max_alpha_square); // -------------------------------------------------------------------------------------------- diff --git a/src/python/doc/alpha_complex_user.rst b/src/python/doc/alpha_complex_user.rst index fffcb3db..ef67a457 100644 --- a/src/python/doc/alpha_complex_user.rst +++ b/src/python/doc/alpha_complex_user.rst @@ -165,6 +165,9 @@ respect to inclusion). We fix that up by calling :func:`~gudhi.SimplexTree.make_filtration_non_decreasing` (cf. `C++ version `_). +.. note:: + This is not the case in `exact` version, this is the reason why it is not called in this case. + Prune above given filtration value ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- cgit v1.2.3 From 65619d48af98680294bf41c4023e04ee94f2745d Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 22 Sep 2021 09:36:18 +0200 Subject: Replace the doc sphere image plotted with trisurf with another using scattered points --- src/python/doc/img/sphere_3d.png | Bin 90550 -> 529148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/img/sphere_3d.png b/src/python/doc/img/sphere_3d.png index 7e7f0ab2..70f3184f 100644 Binary files a/src/python/doc/img/sphere_3d.png and b/src/python/doc/img/sphere_3d.png differ -- cgit v1.2.3 From e23ca84fadcc2c65fd8cf2d141be804bf18b2fd6 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 22 Sep 2021 15:20:03 +0200 Subject: Rename function of torus cpp version and import it with sphere in points Change documentation accordingly --- src/python/doc/datasets_generators.rst | 28 +++++++++++++------------ src/python/gudhi/datasets/generators/_points.cc | 9 +++++--- src/python/gudhi/datasets/generators/points.py | 3 +++ src/python/test/test_datasets_generators.py | 15 +++++++------ 4 files changed, 31 insertions(+), 24 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/datasets_generators.rst b/src/python/doc/datasets_generators.rst index e63dde82..c0bbb973 100644 --- a/src/python/doc/datasets_generators.rst +++ b/src/python/doc/datasets_generators.rst @@ -13,10 +13,12 @@ We provide the generation of different customizable datasets to use as inputs fo Points generators ------------------ +The module **points** enables the generation of random points on a sphere, random points on a torus and as a grid. + Points on sphere ^^^^^^^^^^^^^^^^ -The module **_points** enables the generation of random i.i.d. points uniformly on a (d-1)-sphere in :math:`R^d`. +The function **sphere** enables the generation of random i.i.d. points uniformly on a (d-1)-sphere in :math:`R^d`. The user should provide the number of points to be generated on the sphere :code:`n_samples` and the ambient dimension :code:`ambient_dim`. The :code:`radius` of sphere is optional and is equal to **1** by default. Only random points generation is currently available. @@ -28,28 +30,28 @@ Example .. code-block:: python - from gudhi.datasets.generators import _points + from gudhi.datasets.generators import points from gudhi import AlphaComplex # Generate 50 points on a sphere in R^2 - gen_points = _points.sphere(n_samples = 50, ambient_dim = 2, radius = 1, sample = "random") + gen_points = points.sphere(n_samples = 50, ambient_dim = 2, radius = 1, sample = "random") # Create an alpha complex from the generated points alpha_complex = AlphaComplex(points = gen_points) -.. autofunction:: gudhi.datasets.generators._points.sphere +.. autofunction:: gudhi.datasets.generators.points.sphere Points on torus ^^^^^^^^^^^^^^^^ You can also generate points on a torus. -Two modules are available and give the same output: the first one depends on **CGAL** and the second does not and consists of full python code. +Two functions are available and give the same output: the first one depends on **CGAL** and the second does not and consists of full python code. On another hand, two sample types are provided : you can either generate i.i.d. points on a d-torus in :math:`R^{2d}` *randomly* or on a *grid*. -First module : **_points** -"""""""""""""""""""""""""" +First function : **ctorus** +""""""""""""""""""""""""""" The user should provide the number of points to be generated on the torus :code:`n_samples`, and the dimension :code:`dim` of the torus on which points would be generated in :math:`R^{2dim}`. The :code:`sample` argument is optional and is set to **'random'** by default. @@ -67,18 +69,18 @@ Example """"""" .. code-block:: python - from gudhi.datasets.generators import _points + from gudhi.datasets.generators import points # Generate 50 points randomly on a torus in R^6 - gen_points = _points.torus(n_samples = 50, dim = 3) + gen_points = points.ctorus(n_samples = 50, dim = 3) # Generate 27 points on a torus as a grid in R^6 - gen_points = _points.torus(n_samples = 50, dim = 3, sample = 'grid') + gen_points = points.ctorus(n_samples = 50, dim = 3, sample = 'grid') -.. autofunction:: gudhi.datasets.generators._points.torus +.. autofunction:: gudhi.datasets.generators.points.ctorus -Second module : **points** -"""""""""""""""""""""""""" +Second function : **torus** +""""""""""""""""""""""""""" The user should provide the number of points to be generated on the torus :code:`n_samples` and the dimension :code:`dim` of the torus on which points would be generated in :math:`R^{2dim}`. The :code:`sample` argument is optional and is set to **'random'** by default. diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc index 3d38ff90..536fa949 100644 --- a/src/python/gudhi/datasets/generators/_points.cc +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -96,10 +96,10 @@ PYBIND11_MODULE(_points, m) { :returns: the generated points on a sphere. )pbdoc"); - m.def("torus", &generate_points_on_torus, + m.def("ctorus", &generate_points_on_torus, py::arg("n_samples"), py::arg("dim"), py::arg("sample") = "random", R"pbdoc( - Generate random i.i.d. points on a d-torus in R^2d + Generate random i.i.d. points on a d-torus in R^2d or as a grid :param n_samples: The number of points to be generated. :type n_samples: integer @@ -107,7 +107,10 @@ PYBIND11_MODULE(_points, m) { :type dim: integer :param sample: The sample type. Available values are: `"random"` and `"grid"`. Default value is `"random"`. :type sample: string - :rtype: numpy array of float + :rtype: numpy array of float. + The shape of returned numpy array is : + if sample is 'random' : (n_samples, 2*dim). + if sample is 'grid' : ([n_samples**(1./dim)]**dim, 2*dim). :returns: the generated points on a torus. )pbdoc"); } diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py index daada486..1995f769 100644 --- a/src/python/gudhi/datasets/generators/points.py +++ b/src/python/gudhi/datasets/generators/points.py @@ -10,6 +10,9 @@ import numpy as np import itertools +from ._points import ctorus +from ._points import sphere + def _generate_random_points_on_torus(n_samples, dim): # Generate random angles of size n_samples*dim diff --git a/src/python/test/test_datasets_generators.py b/src/python/test/test_datasets_generators.py index 656c30ee..4c087c57 100755 --- a/src/python/test/test_datasets_generators.py +++ b/src/python/test/test_datasets_generators.py @@ -9,25 +9,24 @@ """ from gudhi.datasets.generators import points -from gudhi.datasets.generators import _points import pytest def test_sphere(): - assert _points.sphere(n_samples = 10, ambient_dim = 2, radius = 1., sample = 'random').shape == (10, 2) + assert points.sphere(n_samples = 10, ambient_dim = 2, radius = 1., sample = 'random').shape == (10, 2) with pytest.raises(ValueError): - _points.sphere(n_samples = 10, ambient_dim = 2, radius = 1., sample = 'other') + points.sphere(n_samples = 10, ambient_dim = 2, radius = 1., sample = 'other') def test_torus(): - assert _points.torus(n_samples = 64, dim = 3, sample = 'random').shape == (64, 6) - assert _points.torus(n_samples = 64, dim = 3, sample = 'grid').shape == (64, 6) + assert points.ctorus(n_samples = 64, dim = 3, sample = 'random').shape == (64, 6) + assert points.ctorus(n_samples = 64, dim = 3, sample = 'grid').shape == (64, 6) - assert _points.torus(n_samples = 10, dim = 4, sample = 'random').shape == (10, 8) - assert _points.torus(n_samples = 10, dim = 4, sample = 'grid').shape == (1, 8) + assert points.ctorus(n_samples = 10, dim = 4, sample = 'random').shape == (10, 8) + assert points.ctorus(n_samples = 10, dim = 4, sample = 'grid').shape == (1, 8) with pytest.raises(ValueError): - _points.torus(n_samples = 10, dim = 4, sample = 'other') + points.ctorus(n_samples = 10, dim = 4, sample = 'other') def test_torus_full_python(): assert points.torus(n_samples = 64, dim = 3, sample = 'random').shape == (64, 6) -- cgit v1.2.3 From dbdc62a494e54c3dd409a2e80fa169560355ce19 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Thu, 7 Oct 2021 15:25:25 +0200 Subject: Move warnings import to the beginning of knn.py file Use isfinite instead of isinf and isnan Use catch_warnings context manager instead of "always" with simplefilter --- src/python/gudhi/point_cloud/knn.py | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/point_cloud/knn.py b/src/python/gudhi/point_cloud/knn.py index 0724ce94..de5844f9 100644 --- a/src/python/gudhi/point_cloud/knn.py +++ b/src/python/gudhi/point_cloud/knn.py @@ -8,6 +8,7 @@ # - YYYY/MM Author: Description of the modification import numpy +import warnings # TODO: https://github.com/facebookresearch/faiss @@ -257,14 +258,9 @@ class KNearestNeighbors: if ef is not None: self.graph.set_ef(ef) neighbors, distances = self.graph.knn_query(X, k, num_threads=self.params["num_threads"]) - if numpy.any(numpy.isnan(distances)): - import warnings - warnings.simplefilter("always") - warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) - if numpy.any(numpy.isinf(distances)): - import warnings - warnings.simplefilter("always") - warnings.warn("Overflow value encountered while computing 'distances'", RuntimeWarning) + with warnings.catch_warnings(): + if not(numpy.all(numpy.isfinite(distances))): + warnings.warn("Overflow/infinite value encountered while computing 'distances'", RuntimeWarning) # The k nearest neighbors are always sorted. I couldn't find it in the doc, but the code calls searchKnn, # which returns a priority_queue, and then fills the return array backwards with top/pop on the queue. if self.return_index: @@ -298,14 +294,9 @@ class KNearestNeighbors: if self.return_index: if self.return_distance: distances, neighbors = mat.Kmin_argKmin(k, dim=1) - if torch.isnan(distances).any(): - import warnings - warnings.simplefilter("always") - warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) - if torch.isinf(distances).any(): - import warnings - warnings.simplefilter("always") - warnings.warn("Overflow encountered while computing 'distances'", RuntimeWarning) + with warnings.catch_warnings(): + if not(torch.isfinite(distances).all()): + warnings.warn("Overflow/infinite value encountered while computing 'distances'", RuntimeWarning) if p != numpy.inf: distances = distances ** (1.0 / p) return neighbors, distances @@ -314,14 +305,9 @@ class KNearestNeighbors: return neighbors if self.return_distance: distances = mat.Kmin(k, dim=1) - if torch.isnan(distances).any(): - import warnings - warnings.simplefilter("always") - warnings.warn("NaN value encountered while computing 'distances'", RuntimeWarning) - if torch.isinf(distances).any(): - import warnings - warnings.simplefilter("always") - warnings.warn("Overflow encountered while computing 'distances'", RuntimeWarning) + with warnings.catch_warnings(): + if not(torch.isfinite(distances).all()): + warnings.warn("Overflow/infinite value encountered while computing 'distances'", RuntimeWarning) if p != numpy.inf: distances = distances ** (1.0 / p) return distances -- cgit v1.2.3 From f461f050ee8bad509814b4851ab7ae8f43502962 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Fri, 8 Oct 2021 11:18:46 +0200 Subject: Add warnings in dtm.py for DistanceToMeasure and DTMDensity Add DTMDensity warning test --- src/python/gudhi/point_cloud/dtm.py | 11 +++++++++++ src/python/test/test_dtm.py | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index 55ac58e6..96a9e7bf 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -9,6 +9,7 @@ from .knn import KNearestNeighbors import numpy as np +import warnings __author__ = "Marc Glisse" __copyright__ = "Copyright (C) 2020 Inria" @@ -66,6 +67,11 @@ class DistanceToMeasure: distances = distances ** self.q dtm = distances.sum(-1) / self.k dtm = dtm ** (1.0 / self.q) + with warnings.catch_warnings(): + import torch + if isinstance(dtm, torch.Tensor): + if not(torch.isfinite(dtm).all()): + warnings.warn("Overflow/infinite value encountered while computing 'dtm'", RuntimeWarning) # We compute too many powers, 1/p in knn then q in dtm, 1/q in dtm then q or some log in the caller. # Add option to skip the final root? return dtm @@ -163,6 +169,11 @@ class DTMDensity: distances = self.knn.transform(X) distances = distances ** q dtm = (distances * weights).sum(-1) + with warnings.catch_warnings(): + import torch + if isinstance(dtm, torch.Tensor): + if not(torch.isfinite(dtm).all()): + warnings.warn("Overflow/infinite value encountered while computing 'dtm' for density", RuntimeWarning) if self.normalize: dtm /= (np.arange(1, k + 1) ** (q / dim) * weights).sum() density = dtm ** (-dim / q) diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index c29471cf..52468d0f 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -97,7 +97,15 @@ def test_dtm_overflow_warnings(): for impl in impl_warn: dtm = DistanceToMeasure(2, q=10000, implementation=impl) r = dtm.fit_transform(pts) - assert len(w) == 2 + assert len(w) == 3 for i in range(len(w)): assert issubclass(w[i].category, RuntimeWarning) assert "Overflow" in str(w[i].message) + +def test_density_overflow_warning(): + distances = numpy.array([[10., 100.], [10000000000000., 10.]]) + with warnings.catch_warnings(record=True) as w: + density = DTMDensity(k=2, q=100000, implementation="keops", dim=1).fit_transform(distances) + assert len(w) == 1 + assert issubclass(w[0].category, RuntimeWarning) + assert "Overflow" in str(w[0].message) -- cgit v1.2.3 From 44659b4d5c2df18745e36280317ecbc9c6a5b411 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Fri, 8 Oct 2021 16:03:54 +0200 Subject: Add torch dependency for some python tests --- src/python/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index f534fc2a..7b7aff1e 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -546,17 +546,17 @@ if(PYTHONINTERP_FOUND) endif() # Tomato - if(SCIPY_FOUND AND SKLEARN_FOUND AND PYBIND11_FOUND) + if(SCIPY_FOUND AND SKLEARN_FOUND AND PYBIND11_FOUND AND TORCH_FOUND) add_gudhi_py_test(test_tomato) endif() # Weighted Rips - if(SCIPY_FOUND) + if(SCIPY_FOUND AND TORCH_FOUND) add_gudhi_py_test(test_weighted_rips_complex) endif() # DTM Rips - if(SCIPY_FOUND) + if(SCIPY_FOUND AND TORCH_FOUND) add_gudhi_py_test(test_dtm_rips_complex) endif() -- cgit v1.2.3 From ec06a9b9ae0a9ff1897249dcbc2b497764f54aaf Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Mon, 18 Oct 2021 17:01:02 +0200 Subject: First part of the fix --- src/python/gudhi/cubical_complex.pyx | 7 ++- src/python/gudhi/periodic_cubical_complex.pyx | 7 ++- src/python/gudhi/representations/vector_methods.py | 60 ++++++++++++++-------- src/python/gudhi/simplex_tree.pyx | 26 ++++++---- src/python/test/test_cubical_complex.py | 25 +++++++++ src/python/test/test_representations.py | 37 +++++++++++++ 6 files changed, 129 insertions(+), 33 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/cubical_complex.pyx b/src/python/gudhi/cubical_complex.pyx index 97c69a2d..04569bd8 100644 --- a/src/python/gudhi/cubical_complex.pyx +++ b/src/python/gudhi/cubical_complex.pyx @@ -281,4 +281,9 @@ cdef class CubicalComplex: launched first. """ assert self.pcohptr != NULL, "compute_persistence() must be called before persistence_intervals_in_dimension()" - return np.array(self.pcohptr.intervals_in_dimension(dimension)) + piid = np.array(self.pcohptr.intervals_in_dimension(dimension)) + # Workaround https://github.com/GUDHI/gudhi-devel/issues/507 + if piid.shape[0] == 0: + return np.empty(shape = [0, 2]) + else: + return piid diff --git a/src/python/gudhi/periodic_cubical_complex.pyx b/src/python/gudhi/periodic_cubical_complex.pyx index ef1d3080..bd91ccde 100644 --- a/src/python/gudhi/periodic_cubical_complex.pyx +++ b/src/python/gudhi/periodic_cubical_complex.pyx @@ -280,4 +280,9 @@ cdef class PeriodicCubicalComplex: launched first. """ assert self.pcohptr != NULL, "compute_persistence() must be called before persistence_intervals_in_dimension()" - return np.array(self.pcohptr.intervals_in_dimension(dimension)) + piid = np.array(self.pcohptr.intervals_in_dimension(dimension)) + # Workaround https://github.com/GUDHI/gudhi-devel/issues/507 + if piid.shape[0] == 0: + return np.empty(shape = [0, 2]) + else: + return piid diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 84bc99a2..711c32a7 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -44,11 +44,15 @@ class PersistenceImage(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - if np.isnan(np.array(self.im_range)).any(): - new_X = BirthPersistenceTransform().fit_transform(X) - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(new_X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.im_range = np.where(np.isnan(np.array(self.im_range)), np.array([mx, Mx, my, My]), np.array(self.im_range)) + try: + if np.isnan(np.array(self.im_range)).any(): + new_X = BirthPersistenceTransform().fit_transform(X) + pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(new_X,y) + [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] + self.im_range = np.where(np.isnan(np.array(self.im_range)), np.array([mx, Mx, my, My]), np.array(self.im_range)) + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + pass return self def transform(self, X): @@ -120,9 +124,13 @@ class Landscape(BaseEstimator, TransformerMixin): y (n x 1 array): persistence diagram labels (unused). """ if self.nan_in_range.any(): - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.sample_range = np.where(self.nan_in_range, np.array([mx, My]), np.array(self.sample_range)) + try: + pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) + [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] + self.sample_range = np.where(self.nan_in_range, np.array([mx, My]), np.array(self.sample_range)) + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + pass return self def transform(self, X): @@ -218,10 +226,14 @@ class Silhouette(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - if np.isnan(np.array(self.sample_range)).any(): - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + try: + if np.isnan(np.array(self.sample_range)).any(): + pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) + [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] + self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + pass return self def transform(self, X): @@ -307,10 +319,14 @@ class BettiCurve(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - if np.isnan(np.array(self.sample_range)).any(): - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + try: + if np.isnan(np.array(self.sample_range)).any(): + pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) + [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] + self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + pass return self def transform(self, X): @@ -374,10 +390,14 @@ class Entropy(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - if np.isnan(np.array(self.sample_range)).any(): - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + try: + if np.isnan(np.array(self.sample_range)).any(): + pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) + [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] + self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + pass return self def transform(self, X): diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index 9c51cb46..e9bac036 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -9,8 +9,7 @@ from cython.operator import dereference, preincrement from libc.stdint cimport intptr_t -import numpy -from numpy import array as np_array +import numpy as np cimport gudhi.simplex_tree __author__ = "Vincent Rouvreau" @@ -542,7 +541,12 @@ cdef class SimplexTree: function to be launched first. """ assert self.pcohptr != NULL, "compute_persistence() must be called before persistence_intervals_in_dimension()" - return np_array(self.pcohptr.intervals_in_dimension(dimension)) + piid = np.array(self.pcohptr.intervals_in_dimension(dimension)) + # Workaround https://github.com/GUDHI/gudhi-devel/issues/507 + if piid.shape[0] == 0: + return np.empty(shape = [0, 2]) + else: + return piid def persistence_pairs(self): """This function returns a list of persistence birth and death simplices pairs. @@ -583,8 +587,8 @@ cdef class SimplexTree: """ assert self.pcohptr != NULL, "lower_star_persistence_generators() requires that persistence() be called first." gen = self.pcohptr.lower_star_generators() - normal = [np_array(d).reshape(-1,2) for d in gen.first] - infinite = [np_array(d) for d in gen.second] + normal = [np.array(d).reshape(-1,2) for d in gen.first] + infinite = [np.array(d) for d in gen.second] return (normal, infinite) def flag_persistence_generators(self): @@ -602,19 +606,19 @@ cdef class SimplexTree: assert self.pcohptr != NULL, "flag_persistence_generators() requires that persistence() be called first." gen = self.pcohptr.flag_generators() if len(gen.first) == 0: - normal0 = numpy.empty((0,3)) + normal0 = np.empty((0,3)) normals = [] else: l = iter(gen.first) - normal0 = np_array(next(l)).reshape(-1,3) - normals = [np_array(d).reshape(-1,4) for d in l] + normal0 = np.array(next(l)).reshape(-1,3) + normals = [np.array(d).reshape(-1,4) for d in l] if len(gen.second) == 0: - infinite0 = numpy.empty(0) + infinite0 = np.empty(0) infinites = [] else: l = iter(gen.second) - infinite0 = np_array(next(l)) - infinites = [np_array(d).reshape(-1,2) for d in l] + infinite0 = np.array(next(l)) + infinites = [np.array(d).reshape(-1,2) for d in l] return (normal0, normals, infinite0, infinites) def collapse_edges(self, nb_iterations = 1): diff --git a/src/python/test/test_cubical_complex.py b/src/python/test/test_cubical_complex.py index d0e4e9e8..29d559b3 100755 --- a/src/python/test/test_cubical_complex.py +++ b/src/python/test/test_cubical_complex.py @@ -174,3 +174,28 @@ def test_periodic_cofaces_of_persistence_pairs_when_pd_has_no_paired_birth_and_d assert np.array_equal(pairs[1][0], np.array([0])) assert np.array_equal(pairs[1][1], np.array([0, 1])) assert np.array_equal(pairs[1][2], np.array([1])) + +def test_cubical_persistence_intervals_in_dimension(): + cub = CubicalComplex( + dimensions=[3, 3], + top_dimensional_cells=[1, 2, 3, 4, 5, 6, 7, 8, 9], + ) + cub.compute_persistence() + H0 = cub.persistence_intervals_in_dimension(0) + assert np.array_equal(H0, np.array([[ 1., float("inf")]])) + assert cub.persistence_intervals_in_dimension(1).shape == (0, 2) + +def test_periodic_cubical_persistence_intervals_in_dimension(): + cub = PeriodicCubicalComplex( + dimensions=[3, 3], + top_dimensional_cells=[1, 2, 3, 4, 5, 6, 7, 8, 9], + periodic_dimensions = [True, True] + ) + cub.compute_persistence() + H0 = cub.persistence_intervals_in_dimension(0) + assert np.array_equal(H0, np.array([[ 1., float("inf")]])) + H1 = cub.persistence_intervals_in_dimension(1) + assert np.array_equal(H1, np.array([[ 3., float("inf")], [ 7., float("inf")]])) + H2 = cub.persistence_intervals_in_dimension(2) + assert np.array_equal(H2, np.array([[ 9., float("inf")]])) + assert cub.persistence_intervals_in_dimension(3).shape == (0, 2) diff --git a/src/python/test/test_representations.py b/src/python/test/test_representations.py index cda1a15b..c1f4df12 100755 --- a/src/python/test/test_representations.py +++ b/src/python/test/test_representations.py @@ -6,6 +6,12 @@ import pytest from sklearn.cluster import KMeans +from gudhi.representations import (DiagramSelector, Clamping, Landscape, Silhouette, BettiCurve, ComplexPolynomial,\ + TopologicalVector, DiagramScaler, BirthPersistenceTransform,\ + PersistenceImage, PersistenceWeightedGaussianKernel, Entropy, \ + PersistenceScaleSpaceKernel, SlicedWassersteinDistance,\ + SlicedWassersteinKernel, PersistenceFisherKernel, WassersteinDistance) + def test_representations_examples(): # Disable graphics for testing purposes @@ -98,3 +104,34 @@ def test_infinity(): assert c[1] == 0 assert c[7] == 3 assert c[9] == 2 + +def pow(n): + return lambda x: np.power(x[1]-x[0],n) + +def test_vectorization_empty_diagrams(): + empty_diag = np.empty(shape = [0, 2]) + Landscape(resolution=1000)(empty_diag) + Silhouette(resolution=1000, weight=pow(2))(empty_diag) + BettiCurve(resolution=1000)(empty_diag) + ComplexPolynomial(threshold=-1, polynomial_type="T")(empty_diag) + TopologicalVector(threshold=-1)(empty_diag) + PersistenceImage(bandwidth=.1, weight=lambda x: x[1], im_range=[0,1,0,1], resolution=[100,100])(empty_diag) + #Entropy(mode="scalar")(empty_diag) + #Entropy(mode="vector", normalized=False)(empty_diag) + +#def arctan(C,p): +# return lambda x: C*np.arctan(np.power(x[1], p)) +# +#def test_kernel_empty_diagrams(): +# empty_diag = np.empty(shape = [0, 2]) +# PersistenceWeightedGaussianKernel(bandwidth=1., kernel_approx=None, weight=arctan(1.,1.))(empty_diag, empty_diag) +# PersistenceWeightedGaussianKernel(kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])), weight=arctan(1.,1.))(empty_diag, empty_diag) +# PersistenceScaleSpaceKernel(bandwidth=1.)(empty_diag, empty_diag) +# PersistenceScaleSpaceKernel(kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])))(empty_diag, empty_diag) +# SlicedWassersteinDistance(num_directions=100)(empty_diag, empty_diag) +# SlicedWassersteinKernel(num_directions=100, bandwidth=1.)(empty_diag, empty_diag) +# WassersteinDistance(order=2, internal_p=2, mode="pot")(empty_diag, empty_diag) +# WassersteinDistance(order=2, internal_p=2, mode="hera", delta=0.0001)(empty_diag, empty_diag) +# BottleneckDistance(epsilon=.001)(empty_diag, empty_diag) +# PersistenceFisherKernel(bandwidth_fisher=1., bandwidth=1.)(empty_diag, empty_diag) +# PersistenceFisherKernel(bandwidth_fisher=1., bandwidth=1., kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])))(empty_diag, empty_diag) -- cgit v1.2.3 From 0c8e1e2b69c7658c153df99931e3407ec18c1332 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Tue, 19 Oct 2021 21:20:01 +0200 Subject: Add empty diags tests --- src/python/test/test_representations.py | 60 +++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 21 deletions(-) (limited to 'src/python') diff --git a/src/python/test/test_representations.py b/src/python/test/test_representations.py index c1f4df12..b888b7f1 100755 --- a/src/python/test/test_representations.py +++ b/src/python/test/test_representations.py @@ -6,9 +6,16 @@ import pytest from sklearn.cluster import KMeans -from gudhi.representations import (DiagramSelector, Clamping, Landscape, Silhouette, BettiCurve, ComplexPolynomial,\ - TopologicalVector, DiagramScaler, BirthPersistenceTransform,\ - PersistenceImage, PersistenceWeightedGaussianKernel, Entropy, \ +# Vectorization +from gudhi.representations import (Landscape, Silhouette, BettiCurve, ComplexPolynomial,\ + TopologicalVector, PersistenceImage, Entropy) + +# Preprocessing +from gudhi.representations import (BirthPersistenceTransform, Clamping, DiagramScaler, Padding, ProminentPoints, \ + DiagramSelector) + +# Kernel +from gudhi.representations import (PersistenceWeightedGaussianKernel, \ PersistenceScaleSpaceKernel, SlicedWassersteinDistance,\ SlicedWassersteinKernel, PersistenceFisherKernel, WassersteinDistance) @@ -105,33 +112,44 @@ def test_infinity(): assert c[7] == 3 assert c[9] == 2 + +def test_preprocessing_empty_diagrams(): + empty_diag = np.empty(shape = [0, 2]) + assert not np.any(BirthPersistenceTransform()(empty_diag)) + assert not np.any(Clamping().fit_transform(empty_diag)) + assert not np.any(DiagramScaler()(empty_diag)) + assert not np.any(Padding()(empty_diag)) + assert not np.any(ProminentPoints()(empty_diag)) + assert not np.any(DiagramSelector()(empty_diag)) + def pow(n): return lambda x: np.power(x[1]-x[0],n) def test_vectorization_empty_diagrams(): empty_diag = np.empty(shape = [0, 2]) - Landscape(resolution=1000)(empty_diag) - Silhouette(resolution=1000, weight=pow(2))(empty_diag) - BettiCurve(resolution=1000)(empty_diag) - ComplexPolynomial(threshold=-1, polynomial_type="T")(empty_diag) - TopologicalVector(threshold=-1)(empty_diag) - PersistenceImage(bandwidth=.1, weight=lambda x: x[1], im_range=[0,1,0,1], resolution=[100,100])(empty_diag) - #Entropy(mode="scalar")(empty_diag) - #Entropy(mode="vector", normalized=False)(empty_diag) - -#def arctan(C,p): -# return lambda x: C*np.arctan(np.power(x[1], p)) + assert not np.any(Landscape(resolution=1000)(empty_diag)) + assert not np.any(Silhouette(resolution=1000, weight=pow(2))(empty_diag)) + assert not np.any(BettiCurve(resolution=1000)(empty_diag)) + assert not np.any(ComplexPolynomial(threshold=-1, polynomial_type="T")(empty_diag)) + assert not np.any(TopologicalVector(threshold=-1)(empty_diag)) + assert not np.any(PersistenceImage(bandwidth=.1, weight=lambda x: x[1], im_range=[0,1,0,1], resolution=[100,100])(empty_diag)) + assert not np.any(Entropy(mode="scalar")(empty_diag)) + assert not np.any(Entropy(mode="vector", normalized=False)(empty_diag)) + +def arctan(C,p): + return lambda x: C*np.arctan(np.power(x[1], p)) # -#def test_kernel_empty_diagrams(): -# empty_diag = np.empty(shape = [0, 2]) +def test_kernel_empty_diagrams(): + empty_diag = np.empty(shape = [0, 2]) # PersistenceWeightedGaussianKernel(bandwidth=1., kernel_approx=None, weight=arctan(1.,1.))(empty_diag, empty_diag) # PersistenceWeightedGaussianKernel(kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])), weight=arctan(1.,1.))(empty_diag, empty_diag) # PersistenceScaleSpaceKernel(bandwidth=1.)(empty_diag, empty_diag) # PersistenceScaleSpaceKernel(kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])))(empty_diag, empty_diag) -# SlicedWassersteinDistance(num_directions=100)(empty_diag, empty_diag) -# SlicedWassersteinKernel(num_directions=100, bandwidth=1.)(empty_diag, empty_diag) -# WassersteinDistance(order=2, internal_p=2, mode="pot")(empty_diag, empty_diag) -# WassersteinDistance(order=2, internal_p=2, mode="hera", delta=0.0001)(empty_diag, empty_diag) -# BottleneckDistance(epsilon=.001)(empty_diag, empty_diag) + assert SlicedWassersteinDistance(num_directions=100)(empty_diag, empty_diag) == 0. + assert SlicedWassersteinKernel(num_directions=100, bandwidth=1.)(empty_diag, empty_diag) == 1. + assert WassersteinDistance(mode="hera", delta=0.0001)(empty_diag, empty_diag) == 0. + assert WassersteinDistance(mode="pot")(empty_diag, empty_diag) == 0. + assert BottleneckDistance(epsilon=.001)(empty_diag, empty_diag) == 0. + assert BottleneckDistance()(empty_diag, empty_diag) == 0. # PersistenceFisherKernel(bandwidth_fisher=1., bandwidth=1.)(empty_diag, empty_diag) # PersistenceFisherKernel(bandwidth_fisher=1., bandwidth=1., kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])))(empty_diag, empty_diag) -- cgit v1.2.3 From 74e6bad5e4bb848ad7d15afd5d6302ec4c698ac9 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Wed, 20 Oct 2021 09:57:43 +0200 Subject: Improve tests for empty representations --- src/python/test/test_representations.py | 48 ++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 16 deletions(-) (limited to 'src/python') diff --git a/src/python/test/test_representations.py b/src/python/test/test_representations.py index b888b7f1..93461f1e 100755 --- a/src/python/test/test_representations.py +++ b/src/python/test/test_representations.py @@ -3,6 +3,7 @@ import sys import matplotlib.pyplot as plt import numpy as np import pytest +import random from sklearn.cluster import KMeans @@ -127,29 +128,44 @@ def pow(n): def test_vectorization_empty_diagrams(): empty_diag = np.empty(shape = [0, 2]) - assert not np.any(Landscape(resolution=1000)(empty_diag)) - assert not np.any(Silhouette(resolution=1000, weight=pow(2))(empty_diag)) - assert not np.any(BettiCurve(resolution=1000)(empty_diag)) - assert not np.any(ComplexPolynomial(threshold=-1, polynomial_type="T")(empty_diag)) - assert not np.any(TopologicalVector(threshold=-1)(empty_diag)) - assert not np.any(PersistenceImage(bandwidth=.1, weight=lambda x: x[1], im_range=[0,1,0,1], resolution=[100,100])(empty_diag)) - assert not np.any(Entropy(mode="scalar")(empty_diag)) - assert not np.any(Entropy(mode="vector", normalized=False)(empty_diag)) - -def arctan(C,p): - return lambda x: C*np.arctan(np.power(x[1], p)) -# + random_resolution = random.randint(50,100)*10 # between 500 and 1000 + print("resolution = ", random_resolution) + lsc = Landscape(resolution=random_resolution)(empty_diag) + assert not np.any(lsc) + assert lsc.shape[0]%random_resolution == 0 + slt = Silhouette(resolution=random_resolution, weight=pow(2))(empty_diag) + assert not np.any(slt) + assert slt.shape[0] == random_resolution + btc = BettiCurve(resolution=random_resolution)(empty_diag) + assert not np.any(btc) + assert btc.shape[0] == random_resolution + cpp = ComplexPolynomial(threshold=random_resolution, polynomial_type="T")(empty_diag) + assert not np.any(cpp) + assert cpp.shape[0] == random_resolution + tpv = TopologicalVector(threshold=random_resolution)(empty_diag) + assert tpv.shape[0] == random_resolution + assert not np.any(tpv) + prmg = PersistenceImage(resolution=[random_resolution,random_resolution])(empty_diag) + assert not np.any(prmg) + assert prmg.shape[0] == random_resolution * random_resolution + sce = Entropy(mode="scalar", resolution=random_resolution)(empty_diag) + assert not np.any(sce) + assert sce.shape[0] == 1 + scv = Entropy(mode="vector", normalized=False, resolution=random_resolution)(empty_diag) + assert not np.any(scv) + assert scv.shape[0] == random_resolution + def test_kernel_empty_diagrams(): empty_diag = np.empty(shape = [0, 2]) -# PersistenceWeightedGaussianKernel(bandwidth=1., kernel_approx=None, weight=arctan(1.,1.))(empty_diag, empty_diag) -# PersistenceWeightedGaussianKernel(kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])), weight=arctan(1.,1.))(empty_diag, empty_diag) -# PersistenceScaleSpaceKernel(bandwidth=1.)(empty_diag, empty_diag) -# PersistenceScaleSpaceKernel(kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])))(empty_diag, empty_diag) assert SlicedWassersteinDistance(num_directions=100)(empty_diag, empty_diag) == 0. assert SlicedWassersteinKernel(num_directions=100, bandwidth=1.)(empty_diag, empty_diag) == 1. assert WassersteinDistance(mode="hera", delta=0.0001)(empty_diag, empty_diag) == 0. assert WassersteinDistance(mode="pot")(empty_diag, empty_diag) == 0. assert BottleneckDistance(epsilon=.001)(empty_diag, empty_diag) == 0. assert BottleneckDistance()(empty_diag, empty_diag) == 0. +# PersistenceWeightedGaussianKernel(bandwidth=1., kernel_approx=None, weight=arctan(1.,1.))(empty_diag, empty_diag) +# PersistenceWeightedGaussianKernel(kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])), weight=arctan(1.,1.))(empty_diag, empty_diag) +# PersistenceScaleSpaceKernel(bandwidth=1.)(empty_diag, empty_diag) +# PersistenceScaleSpaceKernel(kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])))(empty_diag, empty_diag) # PersistenceFisherKernel(bandwidth_fisher=1., bandwidth=1.)(empty_diag, empty_diag) # PersistenceFisherKernel(bandwidth_fisher=1., bandwidth=1., kernel_approx=RBFSampler(gamma=1./2, n_components=100000).fit(np.ones([1,2])))(empty_diag, empty_diag) -- cgit v1.2.3 From 44946b900ea13b2d6bb8d285c18cf0d37d515215 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Wed, 20 Oct 2021 11:30:29 +0200 Subject: Add simplex tree test for persistence_intervals_in_dimension --- src/python/test/test_simplex_tree.py | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'src/python') diff --git a/src/python/test/test_simplex_tree.py b/src/python/test/test_simplex_tree.py index a3eacaa9..31c46213 100755 --- a/src/python/test/test_simplex_tree.py +++ b/src/python/test/test_simplex_tree.py @@ -9,6 +9,7 @@ """ from gudhi import SimplexTree, __GUDHI_USE_EIGEN3 +import numpy as np import pytest __author__ = "Vincent Rouvreau" @@ -404,3 +405,46 @@ def test_boundaries_iterator(): with pytest.raises(RuntimeError): list(st.get_boundaries([6])) # (6) does not exist + +def test_persistence_intervals_in_dimension(): + # Here is our triangulation of a 2-torus - taken from https://dioscuri-tda.org/Paris_TDA_Tutorial_2021.html + # 0-----3-----4-----0 + # | \ | \ | \ | \ | + # | \ | \ | \| \ | + # 1-----8-----7-----1 + # | \ | \ | \ | \ | + # | \ | \ | \ | \ | + # 2-----5-----6-----2 + # | \ | \ | \ | \ | + # | \ | \ | \ | \ | + # 0-----3-----4-----0 + st = SimplexTree() + st.insert([0,1,8]) + st.insert([0,3,8]) + st.insert([3,7,8]) + st.insert([3,4,7]) + st.insert([1,4,7]) + st.insert([0,1,4]) + st.insert([1,2,5]) + st.insert([1,5,8]) + st.insert([5,6,8]) + st.insert([6,7,8]) + st.insert([2,6,7]) + st.insert([1,2,7]) + st.insert([0,2,3]) + st.insert([2,3,5]) + st.insert([3,4,5]) + st.insert([4,5,6]) + st.insert([0,4,6]) + st.insert([0,2,6]) + st.compute_persistence(persistence_dim_max=True) + + H0 = st.persistence_intervals_in_dimension(0) + assert np.array_equal(H0, np.array([[ 0., float("inf")]])) + H1 = st.persistence_intervals_in_dimension(1) + assert np.array_equal(H1, np.array([[ 0., float("inf")], [ 0., float("inf")]])) + H2 = st.persistence_intervals_in_dimension(2) + assert np.array_equal(H2, np.array([[ 0., float("inf")]])) + # Test empty case + assert st.persistence_intervals_in_dimension(3).shape == (0, 2) + \ No newline at end of file -- cgit v1.2.3 From e4122147ee4643dbca6c65efebf83eb2adad6aec Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Wed, 20 Oct 2021 11:31:00 +0200 Subject: Make Entropy work and also fix a bug in the loop --- src/python/gudhi/representations/vector_methods.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 711c32a7..47c5224c 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -416,9 +416,12 @@ class Entropy(BaseEstimator, TransformerMixin): new_X = BirthPersistenceTransform().fit_transform(X) for i in range(num_diag): - orig_diagram, diagram, num_pts_in_diag = X[i], new_X[i], X[i].shape[0] - new_diagram = DiagramScaler(use=True, scalers=[([1], MaxAbsScaler())]).fit_transform([diagram])[0] + try: + new_diagram = DiagramScaler(use=True, scalers=[([1], MaxAbsScaler())]).fit_transform([diagram])[0] + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + new_diagram = np.empty(shape = [0, 2]) if self.mode == "scalar": ent = - np.sum( np.multiply(new_diagram[:,1], np.log(new_diagram[:,1])) ) @@ -432,12 +435,11 @@ class Entropy(BaseEstimator, TransformerMixin): max_idx = np.clip(np.ceil((py - self.sample_range[0]) / step_x).astype(int), 0, self.resolution) for k in range(min_idx, max_idx): ent[k] += (-1) * new_diagram[j,1] * np.log(new_diagram[j,1]) - if self.normalized: - ent = ent / np.linalg.norm(ent, ord=1) - Xfit.append(np.reshape(ent,[1,-1])) - - Xfit = np.concatenate(Xfit, 0) + if self.normalized: + ent = ent / np.linalg.norm(ent, ord=1) + Xfit.append(np.reshape(ent,[1,-1])) + Xfit = np.concatenate(Xfit, axis=0) return Xfit def __call__(self, diag): -- cgit v1.2.3 From 36959807d5091b79aedabbc67c363dd761c9d5ee Mon Sep 17 00:00:00 2001 From: Hind-M Date: Thu, 21 Oct 2021 17:08:43 +0200 Subject: Factorize cpp and python torus tests implementations --- src/python/test/test_datasets_generators.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) (limited to 'src/python') diff --git a/src/python/test/test_datasets_generators.py b/src/python/test/test_datasets_generators.py index 4c087c57..e2d300e0 100755 --- a/src/python/test/test_datasets_generators.py +++ b/src/python/test/test_datasets_generators.py @@ -18,22 +18,16 @@ def test_sphere(): with pytest.raises(ValueError): points.sphere(n_samples = 10, ambient_dim = 2, radius = 1., sample = 'other') -def test_torus(): - assert points.ctorus(n_samples = 64, dim = 3, sample = 'random').shape == (64, 6) - assert points.ctorus(n_samples = 64, dim = 3, sample = 'grid').shape == (64, 6) - - assert points.ctorus(n_samples = 10, dim = 4, sample = 'random').shape == (10, 8) - assert points.ctorus(n_samples = 10, dim = 4, sample = 'grid').shape == (1, 8) - - with pytest.raises(ValueError): - points.ctorus(n_samples = 10, dim = 4, sample = 'other') +def _basic_torus(impl): + assert impl(n_samples = 64, dim = 3, sample = 'random').shape == (64, 6) + assert impl(n_samples = 64, dim = 3, sample = 'grid').shape == (64, 6) -def test_torus_full_python(): - assert points.torus(n_samples = 64, dim = 3, sample = 'random').shape == (64, 6) - assert points.torus(n_samples = 64, dim = 3, sample = 'grid').shape == (64, 6) - - assert points.torus(n_samples = 10, dim = 4, sample = 'random').shape == (10, 8) - assert points.torus(n_samples = 10, dim = 4, sample = 'grid').shape == (1, 8) + assert impl(n_samples = 10, dim = 4, sample = 'random').shape == (10, 8) + assert impl(n_samples = 10, dim = 4, sample = 'grid').shape == (1, 8) with pytest.raises(ValueError): - points.torus(n_samples = 10, dim = 4, sample = 'other') + impl(n_samples = 10, dim = 4, sample = 'other') + +def test_torus(): + for torus_impl in [points.torus, points.ctorus]: + _basic_torus(torus_impl) -- cgit v1.2.3 From 4a0bc0fe1d6424da9bf979cfc322067a62f41cc9 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Fri, 22 Oct 2021 12:44:07 +0200 Subject: Fix exception management when sklearn version < 1.0 --- src/python/gudhi/representations/vector_methods.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 47c5224c..b83c2a87 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -500,7 +500,11 @@ class TopologicalVector(BaseEstimator, TransformerMixin): diagram, num_pts_in_diag = X[i], X[i].shape[0] pers = 0.5 * (diagram[:,1]-diagram[:,0]) min_pers = np.minimum(pers,np.transpose(pers)) - distances = DistanceMetric.get_metric("chebyshev").pairwise(diagram) + # Works fine with sklearn 1.0, but an ValueError exception is thrown on past versions + try: + distances = DistanceMetric.get_metric("chebyshev").pairwise(diagram) + except ValueError: + distances = np.empty(shape = [0, 0]) vect = np.flip(np.sort(np.triu(np.minimum(distances, min_pers)), axis=None), 0) dim = min(len(vect), thresh) Xfit[i, :dim] = vect[:dim] -- cgit v1.2.3 From 00dc44281f59aa2b7bb612ec2a7f46720bb944f9 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Mon, 25 Oct 2021 10:21:59 +0200 Subject: Revert "Add torch dependency for some python tests" This reverts commit 44659b4d5c2df18745e36280317ecbc9c6a5b411. --- src/python/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 7b7aff1e..f534fc2a 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -546,17 +546,17 @@ if(PYTHONINTERP_FOUND) endif() # Tomato - if(SCIPY_FOUND AND SKLEARN_FOUND AND PYBIND11_FOUND AND TORCH_FOUND) + if(SCIPY_FOUND AND SKLEARN_FOUND AND PYBIND11_FOUND) add_gudhi_py_test(test_tomato) endif() # Weighted Rips - if(SCIPY_FOUND AND TORCH_FOUND) + if(SCIPY_FOUND) add_gudhi_py_test(test_weighted_rips_complex) endif() # DTM Rips - if(SCIPY_FOUND AND TORCH_FOUND) + if(SCIPY_FOUND) add_gudhi_py_test(test_dtm_rips_complex) endif() -- cgit v1.2.3 From 0bf357f6346fab6edf96d580a9195c2acbb79bae Mon Sep 17 00:00:00 2001 From: Hind-M Date: Mon, 25 Oct 2021 10:23:45 +0200 Subject: Revert "Add warnings in dtm.py for DistanceToMeasure and DTMDensity" This reverts commit f461f050ee8bad509814b4851ab7ae8f43502962. --- src/python/gudhi/point_cloud/dtm.py | 11 ----------- src/python/test/test_dtm.py | 10 +--------- 2 files changed, 1 insertion(+), 20 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/point_cloud/dtm.py b/src/python/gudhi/point_cloud/dtm.py index 96a9e7bf..55ac58e6 100644 --- a/src/python/gudhi/point_cloud/dtm.py +++ b/src/python/gudhi/point_cloud/dtm.py @@ -9,7 +9,6 @@ from .knn import KNearestNeighbors import numpy as np -import warnings __author__ = "Marc Glisse" __copyright__ = "Copyright (C) 2020 Inria" @@ -67,11 +66,6 @@ class DistanceToMeasure: distances = distances ** self.q dtm = distances.sum(-1) / self.k dtm = dtm ** (1.0 / self.q) - with warnings.catch_warnings(): - import torch - if isinstance(dtm, torch.Tensor): - if not(torch.isfinite(dtm).all()): - warnings.warn("Overflow/infinite value encountered while computing 'dtm'", RuntimeWarning) # We compute too many powers, 1/p in knn then q in dtm, 1/q in dtm then q or some log in the caller. # Add option to skip the final root? return dtm @@ -169,11 +163,6 @@ class DTMDensity: distances = self.knn.transform(X) distances = distances ** q dtm = (distances * weights).sum(-1) - with warnings.catch_warnings(): - import torch - if isinstance(dtm, torch.Tensor): - if not(torch.isfinite(dtm).all()): - warnings.warn("Overflow/infinite value encountered while computing 'dtm' for density", RuntimeWarning) if self.normalize: dtm /= (np.arange(1, k + 1) ** (q / dim) * weights).sum() density = dtm ** (-dim / q) diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index 52468d0f..c29471cf 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -97,15 +97,7 @@ def test_dtm_overflow_warnings(): for impl in impl_warn: dtm = DistanceToMeasure(2, q=10000, implementation=impl) r = dtm.fit_transform(pts) - assert len(w) == 3 + assert len(w) == 2 for i in range(len(w)): assert issubclass(w[i].category, RuntimeWarning) assert "Overflow" in str(w[i].message) - -def test_density_overflow_warning(): - distances = numpy.array([[10., 100.], [10000000000000., 10.]]) - with warnings.catch_warnings(record=True) as w: - density = DTMDensity(k=2, q=100000, implementation="keops", dim=1).fit_transform(distances) - assert len(w) == 1 - assert issubclass(w[0].category, RuntimeWarning) - assert "Overflow" in str(w[0].message) -- cgit v1.2.3 From bb8c4994b89fb6bfdd80b76912acadf6197f93cc Mon Sep 17 00:00:00 2001 From: Hind-M Date: Tue, 26 Oct 2021 13:59:44 +0200 Subject: Add comments and some minor changes following code review --- src/python/doc/datasets_generators.rst | 13 +++++++------ src/python/gudhi/datasets/generators/_points.cc | 2 +- src/python/gudhi/datasets/generators/points.py | 6 +++--- src/python/test/test_datasets_generators.py | 2 ++ 4 files changed, 13 insertions(+), 10 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/datasets_generators.rst b/src/python/doc/datasets_generators.rst index c0bbb973..3700b8a2 100644 --- a/src/python/doc/datasets_generators.rst +++ b/src/python/doc/datasets_generators.rst @@ -48,22 +48,23 @@ You can also generate points on a torus. Two functions are available and give the same output: the first one depends on **CGAL** and the second does not and consists of full python code. -On another hand, two sample types are provided : you can either generate i.i.d. points on a d-torus in :math:`R^{2d}` *randomly* or on a *grid*. +On another hand, two sample types are provided: you can either generate i.i.d. points on a d-torus in :math:`R^{2d}` *randomly* or on a *grid*. -First function : **ctorus** +First function: **ctorus** """"""""""""""""""""""""""" The user should provide the number of points to be generated on the torus :code:`n_samples`, and the dimension :code:`dim` of the torus on which points would be generated in :math:`R^{2dim}`. The :code:`sample` argument is optional and is set to **'random'** by default. In this case, the returned generated points would be an array of shape :math:`(n\_samples, 2*dim)`. -Otherwise, if set to **'grid'**, the points are generated on a grid and would be given as an array of shape : +Otherwise, if set to **'grid'**, the points are generated on a grid and would be given as an array of shape: .. math:: - ( [n\_samples^{1 \over {dim}}]^{dim}, 2*dim ) + ( ⌊n\_samples^{1 \over {dim}}⌋^{dim}, 2*dim ) +**Note 1:** The output array first shape is rounded down to the closest perfect :math:`dim^{th}` power. -**Note:** This version is recommended when the user wishes to use **'grid'** as sample type, or **'random'** with a relatively small number of samples (~ less than 150). +**Note 2:** This version is recommended when the user wishes to use **'grid'** as sample type, or **'random'** with a relatively small number of samples (~ less than 150). Example """"""" @@ -79,7 +80,7 @@ Example .. autofunction:: gudhi.datasets.generators.points.ctorus -Second function : **torus** +Second function: **torus** """"""""""""""""""""""""""" The user should provide the number of points to be generated on the torus :code:`n_samples` and the dimension :code:`dim` of the torus on which points would be generated in :math:`R^{2dim}`. diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc index 536fa949..5d675930 100644 --- a/src/python/gudhi/datasets/generators/_points.cc +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -110,7 +110,7 @@ PYBIND11_MODULE(_points, m) { :rtype: numpy array of float. The shape of returned numpy array is : if sample is 'random' : (n_samples, 2*dim). - if sample is 'grid' : ([n_samples**(1./dim)]**dim, 2*dim). + if sample is 'grid' : (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. :returns: the generated points on a torus. )pbdoc"); } diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py index 1995f769..7f4667af 100644 --- a/src/python/gudhi/datasets/generators/points.py +++ b/src/python/gudhi/datasets/generators/points.py @@ -36,15 +36,15 @@ def _generate_grid_points_on_torus(n_samples, dim): def torus(n_samples, dim, sample='random'): """ - Generate points on a dim-torus in R^2dim either randomly or on a grid + Generate points on a flat dim-torus in R^2dim either randomly or on a grid :param n_samples: The number of points to be generated. :param dim: The dimension of the torus on which points would be generated in R^2*dim. :param sample: The sample type of the generated points. Can be 'random' or 'grid'. :returns: numpy array containing the generated points on a torus. - The shape of returned numpy array is : + The shape of returned numpy array is: if sample is 'random' : (n_samples, 2*dim). - if sample is 'grid' : ([n_samples**(1./dim)]**dim, 2*dim). + if sample is 'grid' : (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. """ if sample == 'random': # Generate points randomly diff --git a/src/python/test/test_datasets_generators.py b/src/python/test/test_datasets_generators.py index e2d300e0..933a763e 100755 --- a/src/python/test/test_datasets_generators.py +++ b/src/python/test/test_datasets_generators.py @@ -23,6 +23,8 @@ def _basic_torus(impl): assert impl(n_samples = 64, dim = 3, sample = 'grid').shape == (64, 6) assert impl(n_samples = 10, dim = 4, sample = 'random').shape == (10, 8) + + # Here 1**dim < n_samples < 2**dim, the output shape is therefore (1, 2*dim) = (1, 8), where shape[0] is rounded down to the closest perfect 'dim'th power assert impl(n_samples = 10, dim = 4, sample = 'grid').shape == (1, 8) with pytest.raises(ValueError): -- cgit v1.2.3 From 3a29558decccafe0b07dbf07d66f1410df6c187f Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 27 Oct 2021 09:58:52 +0200 Subject: Replace itertools in grid torus generation function with something faster in most general use cases --- src/python/gudhi/datasets/generators/points.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py index 7f4667af..cf97777d 100644 --- a/src/python/gudhi/datasets/generators/points.py +++ b/src/python/gudhi/datasets/generators/points.py @@ -8,7 +8,6 @@ # - YYYY/MM Author: Description of the modification import numpy as np -import itertools from ._points import ctorus from ._points import sphere @@ -29,10 +28,11 @@ def _generate_grid_points_on_torus(n_samples, dim): n_samples_grid = int((n_samples+.5)**(1./dim)) # add .5 to avoid rounding down with numerical approximations alpha = np.linspace(0, 2*np.pi, n_samples_grid, endpoint=False) - array_points_inter = np.column_stack([np.cos(alpha), np.sin(alpha)]) - array_points = np.array(list(itertools.product(array_points_inter, repeat=dim))).reshape(-1, 2*dim) - - return array_points + array_points = np.column_stack([np.cos(alpha), np.sin(alpha)]) + array_points_idx = np.empty([n_samples_grid]*dim + [dim], dtype=int) + for i, x in enumerate(np.ix_(*([np.arange(n_samples_grid)]*dim))): + array_points_idx[...,i] = x + return array_points[array_points_idx].reshape(-1, 2*dim) def torus(n_samples, dim, sample='random'): """ -- cgit v1.2.3 From 2ffbd4d63afd59b9d1995a7755db087b31f9c998 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 27 Oct 2021 10:01:23 +0200 Subject: Add test to check outputs of the two torus implementations --- src/python/test/test_datasets_generators.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/python') diff --git a/src/python/test/test_datasets_generators.py b/src/python/test/test_datasets_generators.py index 933a763e..91ec4a65 100755 --- a/src/python/test/test_datasets_generators.py +++ b/src/python/test/test_datasets_generators.py @@ -33,3 +33,7 @@ def _basic_torus(impl): def test_torus(): for torus_impl in [points.torus, points.ctorus]: _basic_torus(torus_impl) + # Check that the two versions (torus and ctorus) generate the same output + assert points.ctorus(n_samples = 64, dim = 3, sample = 'random').all() == points.torus(n_samples = 64, dim = 3, sample = 'random').all() + assert points.ctorus(n_samples = 64, dim = 3, sample = 'grid').all() == points.torus(n_samples = 64, dim = 3, sample = 'grid').all() + assert points.ctorus(n_samples = 10, dim = 3, sample = 'grid').all() == points.torus(n_samples = 10, dim = 3, sample = 'grid').all() -- cgit v1.2.3 From 6bbd3c0c62d02ac5641ba77a33f7c5d100320ce0 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau <10407034+VincentRouvreau@users.noreply.github.com> Date: Tue, 2 Nov 2021 14:38:01 +0100 Subject: Precise the torus is flat Co-authored-by: Marc Glisse --- src/python/doc/datasets_generators.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/doc/datasets_generators.rst b/src/python/doc/datasets_generators.rst index 3700b8a2..6f36bce1 100644 --- a/src/python/doc/datasets_generators.rst +++ b/src/python/doc/datasets_generators.rst @@ -41,7 +41,7 @@ Example .. autofunction:: gudhi.datasets.generators.points.sphere -Points on torus +Points on a flat torus ^^^^^^^^^^^^^^^^ You can also generate points on a torus. -- cgit v1.2.3 From 93df8a0622836ab03ada2eac075132388708d2c4 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau <10407034+VincentRouvreau@users.noreply.github.com> Date: Tue, 2 Nov 2021 14:38:46 +0100 Subject: Apply MG's suggestion --- src/python/gudhi/datasets/generators/_points.cc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc index 5d675930..70ce4925 100644 --- a/src/python/gudhi/datasets/generators/_points.cc +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -36,8 +36,12 @@ py::array_t generate_points_on_sphere(size_t n_samples, int ambient_dim, GUDHI_CHECK(ambient_dim == buf.shape[1], "Py array second dimension not matching the ambient space dimension"); - py::gil_scoped_release release; - auto points_generated = Gudhi::generate_points_on_sphere_d(n_samples, ambient_dim, radius); + std::vector points_generated; + + { + py::gil_scoped_release release; + points_generated = Gudhi::generate_points_on_sphere_d(n_samples, ambient_dim, radius); + } for (size_t i = 0; i < n_samples; i++) for (int j = 0; j < ambient_dim; j++) -- cgit v1.2.3 From 79e6a20ce026ff4c86b1632bb3f1ed16ae5c92a1 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Tue, 2 Nov 2021 17:27:25 +0100 Subject: No more gforge references --- .../for_maintainers/new_gudhi_version_creation.md | 22 ++++---- scripts/create_gudhi_version.sh | 66 ---------------------- src/python/doc/alpha_complex_user.rst | 2 +- 3 files changed, 11 insertions(+), 79 deletions(-) delete mode 100755 scripts/create_gudhi_version.sh (limited to 'src/python') diff --git a/.github/for_maintainers/new_gudhi_version_creation.md b/.github/for_maintainers/new_gudhi_version_creation.md index 3e5295c5..812758e3 100644 --- a/.github/for_maintainers/new_gudhi_version_creation.md +++ b/.github/for_maintainers/new_gudhi_version_creation.md @@ -68,20 +68,18 @@ make -j 4 all && ctest -j 4 --output-on-failure ## Upload the documentation -Upload by ftp the content of the directory gudhi.doc.@GUDHI_VERSION@/cpp in a new directory on ForgeLogin@scm.gforge.inria.fr:/home/groups/gudhi/htdocs/doc/@GUDHI_VERSION@ +[GUDHI GitHub pages](https://gudhi.github.io/) is only used as a _"qualification"_ web hosting service. +The _"production"_ web hosting service is https://files.inria.fr (cf. [this doc](https://doc-si.inria.fr/display/SU/Espace+web) +or [this one](https://www.nextinpact.com/article/30325/109058-se-connecter-a-serveur-webdav-sous-linux-macos-ou-windows)). -Upload by ftp the content of the directory gudhi.doc.@GUDHI_VERSION@/python in a new directory on ForgeLogin@scm.gforge.inria.fr:/home/groups/gudhi/htdocs/python/@GUDHI_VERSION@ +Upload the content of the directory gudhi.doc.@GUDHI_VERSION@/cpp in a new directory on gudhi WebDAV in doc/@GUDHI_VERSION@ +Delete the directory doc/latest on gudhi WebDAV. +Copy gudhi WebDAV doc/@GUDHI_VERSION@ as doc/latest (no symbolic link with WebDAV). + +Upload the content of the directory gudhi.doc.@GUDHI_VERSION@/python in a new directory on gudhi WebDAV in python/@GUDHI_VERSION@ +Delete the directory python/latest on gudhi WebDAV. +Copy gudhi WebDAV python/@GUDHI_VERSION@ as python/latest (no symbolic link with WebDAV). -Through ssh, make the **latest** link to your new version of the documentation: -```bash -ssh ForgeLogin@scm.gforge.inria.fr -cd /home/groups/gudhi/htdocs/doc -rm latest -ln -s @GUDHI_VERSION@ latest -cd /home/groups/gudhi/htdocs/python -rm latest -ln -s @GUDHI_VERSION@ latest -``` ## Put a version label on files diff --git a/scripts/create_gudhi_version.sh b/scripts/create_gudhi_version.sh deleted file mode 100755 index f2a9233f..00000000 --- a/scripts/create_gudhi_version.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -login="vrouvrea" -version="2.3.0" -cgaldir="/home/vincent/workspace/CGAL-4.11-HO/build" -cpucount=7 - - -# We start from scripts dir in the dev branch -cd .. -RELATIVEURL=`svn info . |grep -F "Relative URL:" | awk '{print $NF}'` - -if [ "$RELATIVEURL" != "^/trunk" ] -then -echo "Script must be launched in trunk and not in $RELATIVEURL" -exit -fi - -rm -rf build; mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=Debug -DDEBUG_TRACES=ON -DCGAL_DIR=${cgaldir} -DWITH_GUDHI_EXAMPLE=ON -DWITH_GUDHI_BENCHMARK=ON -DPython_ADDITIONAL_VERSIONS=3 .. -cmake -DCMAKE_BUILD_TYPE=Debug . - -CURRENTDIRECTORY=`pwd` -export PYTHONPATH=$CURRENTDIRECTORY/src/cython:$PYTHONPATH - -make -j ${cpucount} all test - -cd .. -svn st | grep -v GUDHIVersion.cmake | grep "^\?" | awk "{print \$2}" | xargs rm -rf - -svn copy svn+ssh://${login}@scm.gforge.inria.fr/svnroot/gudhi/trunk svn+ssh://${login}@scm.gforge.inria.fr/svnroot/gudhi/tags/gudhi-release-${version} \ - -m "Creating a tag of Gudhi release version ${version}." - -cd build -make user_version - -userversiondir=`find . -type d -name "*_GUDHI_${version}" | sed 's/\.\///g'` -echo "User version directory = ${userversiondir}" - -tar -czvf ${userversiondir}.tar.gz ${userversiondir} - -userdocdir=${userversiondir/GUDHI/GUDHI_DOC} -echo "User documentation directory = ${userdocdir}" -mkdir ${userdocdir} -make doxygen - -cp -R ${userversiondir}/doc/html ${userdocdir}/cpp -cd ${userversiondir} -rm -rf build; mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./installed -DCGAL_DIR=${cgaldir} -DWITH_GUDHI_EXAMPLE=ON -DPython_ADDITIONAL_VERSIONS=3 .. - -CURRENTDIRECTORY=`pwd` -export PYTHONPATH=$CURRENTDIRECTORY/cython:$PYTHONPATH - -make sphinx - -cp -R cython/sphinx ../../${userdocdir}/python -cd ../.. -tar -czvf ${userdocdir}.tar.gz ${userdocdir} - -cd ${userversiondir}/build -make -j ${cpucount} all test install - -cd ../.. -actualdir=`pwd` -echo "Library is available at ${actualdir}/${userversiondir}.tar.gz" -sha256sum ${userversiondir}.tar.gz -echo "Documentation is available at ${actualdir}/${userdocdir}.tar.gz" diff --git a/src/python/doc/alpha_complex_user.rst b/src/python/doc/alpha_complex_user.rst index fffcb3db..3de94bb8 100644 --- a/src/python/doc/alpha_complex_user.rst +++ b/src/python/doc/alpha_complex_user.rst @@ -163,7 +163,7 @@ As the squared radii computed by CGAL are an approximation, it might happen that :math:`\alpha^2` values do not quite define a proper filtration (i.e. non-decreasing with respect to inclusion). We fix that up by calling :func:`~gudhi.SimplexTree.make_filtration_non_decreasing` (cf. -`C++ version `_). +`C++ version `_). Prune above given filtration value ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- cgit v1.2.3 From a2761c01ceb26a057b94be1d45433335704c1581 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Thu, 4 Nov 2021 17:24:15 +0100 Subject: code review: try-except inside the if --- src/python/gudhi/representations/vector_methods.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index b83c2a87..e7ee57a4 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -44,15 +44,15 @@ class PersistenceImage(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - try: - if np.isnan(np.array(self.im_range)).any(): + if np.isnan(np.array(self.im_range)).any(): + try: new_X = BirthPersistenceTransform().fit_transform(X) pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(new_X,y) [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] self.im_range = np.where(np.isnan(np.array(self.im_range)), np.array([mx, Mx, my, My]), np.array(self.im_range)) - except ValueError: - # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 - pass + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + pass return self def transform(self, X): -- cgit v1.2.3 From 7c26436a703a476d28cf568949275d26d1827c36 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Thu, 4 Nov 2021 17:26:04 +0100 Subject: code review: use len instead of .shape[0] --- src/python/gudhi/cubical_complex.pyx | 5 ++--- src/python/gudhi/periodic_cubical_complex.pyx | 5 ++--- src/python/gudhi/simplex_tree.pyx | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/cubical_complex.pyx b/src/python/gudhi/cubical_complex.pyx index 04569bd8..8e244bb8 100644 --- a/src/python/gudhi/cubical_complex.pyx +++ b/src/python/gudhi/cubical_complex.pyx @@ -283,7 +283,6 @@ cdef class CubicalComplex: assert self.pcohptr != NULL, "compute_persistence() must be called before persistence_intervals_in_dimension()" piid = np.array(self.pcohptr.intervals_in_dimension(dimension)) # Workaround https://github.com/GUDHI/gudhi-devel/issues/507 - if piid.shape[0] == 0: + if len(piid) == 0: return np.empty(shape = [0, 2]) - else: - return piid + return piid diff --git a/src/python/gudhi/periodic_cubical_complex.pyx b/src/python/gudhi/periodic_cubical_complex.pyx index bd91ccde..6c21e902 100644 --- a/src/python/gudhi/periodic_cubical_complex.pyx +++ b/src/python/gudhi/periodic_cubical_complex.pyx @@ -282,7 +282,6 @@ cdef class PeriodicCubicalComplex: assert self.pcohptr != NULL, "compute_persistence() must be called before persistence_intervals_in_dimension()" piid = np.array(self.pcohptr.intervals_in_dimension(dimension)) # Workaround https://github.com/GUDHI/gudhi-devel/issues/507 - if piid.shape[0] == 0: + if len(piid) == 0: return np.empty(shape = [0, 2]) - else: - return piid + return piid diff --git a/src/python/gudhi/simplex_tree.pyx b/src/python/gudhi/simplex_tree.pyx index e9bac036..c3720936 100644 --- a/src/python/gudhi/simplex_tree.pyx +++ b/src/python/gudhi/simplex_tree.pyx @@ -543,10 +543,9 @@ cdef class SimplexTree: assert self.pcohptr != NULL, "compute_persistence() must be called before persistence_intervals_in_dimension()" piid = np.array(self.pcohptr.intervals_in_dimension(dimension)) # Workaround https://github.com/GUDHI/gudhi-devel/issues/507 - if piid.shape[0] == 0: + if len(piid) == 0: return np.empty(shape = [0, 2]) - else: - return piid + return piid def persistence_pairs(self): """This function returns a list of persistence birth and death simplices pairs. -- cgit v1.2.3 From 3094e1fe51acc49e4ea7e4f38648bb25d96784a4 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Fri, 5 Nov 2021 10:27:46 +0100 Subject: code review: factorize sample range computation --- src/python/gudhi/representations/vector_methods.py | 46 ++++++++++++---------- 1 file changed, 26 insertions(+), 20 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index e7ee57a4..140162af 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -6,6 +6,7 @@ # # Modification(s): # - 2020/06 Martin: ATOL integration +# - 2021/11 Vincent Rouvreau: factorize _automatic_sample_range import numpy as np from sklearn.base import BaseEstimator, TransformerMixin @@ -98,6 +99,23 @@ class PersistenceImage(BaseEstimator, TransformerMixin): """ return self.fit_transform([diag])[0,:] +def _automatic_sample_range(sample_range, X, y): + """ + Compute and returns sample range from the persistence diagrams if one of the sample_range values is numpy.nan. + + Parameters: + sample_range (a numpy array of 2 float): minimum and maximum of all piecewise-linear function domains, of + the form [x_min, x_max]. + X (list of n x 2 numpy arrays): input persistence diagrams. + y (n x 1 array): persistence diagram labels (unused). + """ + nan_in_range = np.isnan(sample_range) + if nan_in_range.any(): + pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) + [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] + return np.where(nan_in_range, np.array([mx, My]), sample_range) + return sample_range + class Landscape(BaseEstimator, TransformerMixin): """ This is a class for computing persistence landscapes from a list of persistence diagrams. A persistence landscape is a collection of 1D piecewise-linear functions computed from the rank function associated to the persistence diagram. These piecewise-linear functions are then sampled evenly on a given range and the corresponding vectors of samples are concatenated and returned. See http://jmlr.org/papers/v16/bubenik15a.html for more details. @@ -123,14 +141,11 @@ class Landscape(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - if self.nan_in_range.any(): - try: - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.sample_range = np.where(self.nan_in_range, np.array([mx, My]), np.array(self.sample_range)) - except ValueError: - # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 - pass + try: + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + pass return self def transform(self, X): @@ -227,10 +242,7 @@ class Silhouette(BaseEstimator, TransformerMixin): y (n x 1 array): persistence diagram labels (unused). """ try: - if np.isnan(np.array(self.sample_range)).any(): - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) except ValueError: # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 pass @@ -320,10 +332,7 @@ class BettiCurve(BaseEstimator, TransformerMixin): y (n x 1 array): persistence diagram labels (unused). """ try: - if np.isnan(np.array(self.sample_range)).any(): - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) except ValueError: # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 pass @@ -391,10 +400,7 @@ class Entropy(BaseEstimator, TransformerMixin): y (n x 1 array): persistence diagram labels (unused). """ try: - if np.isnan(np.array(self.sample_range)).any(): - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - self.sample_range = np.where(np.isnan(np.array(self.sample_range)), np.array([mx, My]), np.array(self.sample_range)) + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) except ValueError: # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 pass -- cgit v1.2.3 From 37d7743a91f7fb970425a06798ac6cb61b0be109 Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau Date: Fri, 5 Nov 2021 12:05:45 +0100 Subject: code review: try/except in function and assert on length of diagrams for error menagement --- src/python/gudhi/representations/vector_methods.py | 38 +++++++++------------- 1 file changed, 15 insertions(+), 23 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/representations/vector_methods.py b/src/python/gudhi/representations/vector_methods.py index 140162af..e883b5dd 100644 --- a/src/python/gudhi/representations/vector_methods.py +++ b/src/python/gudhi/representations/vector_methods.py @@ -111,9 +111,14 @@ def _automatic_sample_range(sample_range, X, y): """ nan_in_range = np.isnan(sample_range) if nan_in_range.any(): - pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) - [mx,my],[Mx,My] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]], [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] - return np.where(nan_in_range, np.array([mx, My]), sample_range) + try: + pre = DiagramScaler(use=True, scalers=[([0], MinMaxScaler()), ([1], MinMaxScaler())]).fit(X,y) + [mx,my] = [pre.scalers[0][1].data_min_[0], pre.scalers[1][1].data_min_[0]] + [Mx,My] = [pre.scalers[0][1].data_max_[0], pre.scalers[1][1].data_max_[0]] + return np.where(nan_in_range, np.array([mx, My]), sample_range) + except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + pass return sample_range class Landscape(BaseEstimator, TransformerMixin): @@ -141,11 +146,7 @@ class Landscape(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - try: - self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) - except ValueError: - # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 - pass + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) return self def transform(self, X): @@ -241,11 +242,7 @@ class Silhouette(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - try: - self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) - except ValueError: - # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 - pass + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) return self def transform(self, X): @@ -331,11 +328,7 @@ class BettiCurve(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - try: - self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) - except ValueError: - # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 - pass + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) return self def transform(self, X): @@ -399,11 +392,7 @@ class Entropy(BaseEstimator, TransformerMixin): X (list of n x 2 numpy arrays): input persistence diagrams. y (n x 1 array): persistence diagram labels (unused). """ - try: - self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) - except ValueError: - # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 - pass + self.sample_range = _automatic_sample_range(np.array(self.sample_range), X, y) return self def transform(self, X): @@ -427,6 +416,7 @@ class Entropy(BaseEstimator, TransformerMixin): new_diagram = DiagramScaler(use=True, scalers=[([1], MaxAbsScaler())]).fit_transform([diagram])[0] except ValueError: # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + assert len(diagram) == 0 new_diagram = np.empty(shape = [0, 2]) if self.mode == "scalar": @@ -510,6 +500,8 @@ class TopologicalVector(BaseEstimator, TransformerMixin): try: distances = DistanceMetric.get_metric("chebyshev").pairwise(diagram) except ValueError: + # Empty persistence diagram case - https://github.com/GUDHI/gudhi-devel/issues/507 + assert len(diagram) == 0 distances = np.empty(shape = [0, 0]) vect = np.flip(np.sort(np.triu(np.minimum(distances, min_pers)), axis=None), 0) dim = min(len(vect), thresh) -- cgit v1.2.3 From fe75d33d715d038e348b7e48512b14c7488ee4f4 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Tue, 9 Nov 2021 16:08:10 +0100 Subject: Remove sphinx warnings for torus --- src/python/doc/datasets_generators.rst | 2 +- src/python/gudhi/datasets/generators/_points.cc | 13 +++---------- src/python/gudhi/datasets/generators/points.py | 5 +---- 3 files changed, 5 insertions(+), 15 deletions(-) (limited to 'src/python') diff --git a/src/python/doc/datasets_generators.rst b/src/python/doc/datasets_generators.rst index 6f36bce1..260c3882 100644 --- a/src/python/doc/datasets_generators.rst +++ b/src/python/doc/datasets_generators.rst @@ -42,7 +42,7 @@ Example .. autofunction:: gudhi.datasets.generators.points.sphere Points on a flat torus -^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^ You can also generate points on a torus. diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc index 70ce4925..6baed673 100644 --- a/src/python/gudhi/datasets/generators/_points.cc +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -85,9 +85,7 @@ PYBIND11_MODULE(_points, m) { m.def("sphere", &generate_points_on_sphere, py::arg("n_samples"), py::arg("ambient_dim"), py::arg("radius") = 1., py::arg("sample") = "random", - R"pbdoc( - Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d - + R"pbdoc( Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d :param n_samples: The number of points to be generated. :type n_samples: integer :param ambient_dim: The ambient dimension d. @@ -102,19 +100,14 @@ PYBIND11_MODULE(_points, m) { m.def("ctorus", &generate_points_on_torus, py::arg("n_samples"), py::arg("dim"), py::arg("sample") = "random", - R"pbdoc( - Generate random i.i.d. points on a d-torus in R^2d or as a grid - + R"pbdoc( Generate random i.i.d. points on a d-torus in R^2d or as a grid :param n_samples: The number of points to be generated. :type n_samples: integer :param dim: The dimension of the torus on which points would be generated in R^2*dim. :type dim: integer :param sample: The sample type. Available values are: `"random"` and `"grid"`. Default value is `"random"`. :type sample: string - :rtype: numpy array of float. - The shape of returned numpy array is : - if sample is 'random' : (n_samples, 2*dim). - if sample is 'grid' : (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. + :rtype: numpy array of float. The shape of returned numpy array is: If sample is 'random': (n_samples, 2*dim). If sample is 'grid': (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. :returns: the generated points on a torus. )pbdoc"); } diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py index cf97777d..481f3f71 100644 --- a/src/python/gudhi/datasets/generators/points.py +++ b/src/python/gudhi/datasets/generators/points.py @@ -41,10 +41,7 @@ def torus(n_samples, dim, sample='random'): :param n_samples: The number of points to be generated. :param dim: The dimension of the torus on which points would be generated in R^2*dim. :param sample: The sample type of the generated points. Can be 'random' or 'grid'. - :returns: numpy array containing the generated points on a torus. - The shape of returned numpy array is: - if sample is 'random' : (n_samples, 2*dim). - if sample is 'grid' : (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. + :returns: numpy array containing the generated points on a torus. The shape of returned numpy array is: If sample is 'random': (n_samples, 2*dim). If sample is 'grid': (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. """ if sample == 'random': # Generate points randomly -- cgit v1.2.3 From d88e125fe3b4e1dd0c95c95c5bc715b1a2f28ce6 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Mon, 15 Nov 2021 14:53:39 +0100 Subject: Disable test of dtm warnings until next version of pykeops is released (cf. issue #543) --- src/python/test/test_dtm.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) (limited to 'src/python') diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index c29471cf..bdf003a3 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -13,7 +13,7 @@ import numpy import pytest import torch import math -import warnings +#import warnings # used in test_dtm_overflow_warnings def test_dtm_compare_euclidean(): @@ -89,15 +89,16 @@ def test_density(): density = DTMDensity(weights=[0.5, 0.5], metric="neighbors", dim=1).fit_transform(distances) assert density == pytest.approx(expected) -def test_dtm_overflow_warnings(): - pts = numpy.array([[10., 100000000000000000000000000000.], [1000., 100000000000000000000000000.]]) - impl_warn = ["keops", "hnsw"] +# TODO Uncomment this test when next version of pykeops (current is 1.5) is released (should fix the problem (cf. issue #543)) +#def test_dtm_overflow_warnings(): + #pts = numpy.array([[10., 100000000000000000000000000000.], [1000., 100000000000000000000000000.]]) + #impl_warn = ["keops", "hnsw"] - with warnings.catch_warnings(record=True) as w: - for impl in impl_warn: - dtm = DistanceToMeasure(2, q=10000, implementation=impl) - r = dtm.fit_transform(pts) - assert len(w) == 2 - for i in range(len(w)): - assert issubclass(w[i].category, RuntimeWarning) - assert "Overflow" in str(w[i].message) + #with warnings.catch_warnings(record=True) as w: + #for impl in impl_warn: + #dtm = DistanceToMeasure(2, q=10000, implementation=impl) + #r = dtm.fit_transform(pts) + #assert len(w) == 2 + #for i in range(len(w)): + #assert issubclass(w[i].category, RuntimeWarning) + #assert "Overflow" in str(w[i].message) -- cgit v1.2.3 From 4e4a300ac9d914fa1350ca9b920ec2cc09bfd244 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Mon, 15 Nov 2021 15:52:01 +0100 Subject: Keep dtm warnings test for "hnsw" --- src/python/test/test_dtm.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) (limited to 'src/python') diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index bdf003a3..09876496 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -13,7 +13,7 @@ import numpy import pytest import torch import math -#import warnings # used in test_dtm_overflow_warnings +import warnings def test_dtm_compare_euclidean(): @@ -89,16 +89,13 @@ def test_density(): density = DTMDensity(weights=[0.5, 0.5], metric="neighbors", dim=1).fit_transform(distances) assert density == pytest.approx(expected) -# TODO Uncomment this test when next version of pykeops (current is 1.5) is released (should fix the problem (cf. issue #543)) -#def test_dtm_overflow_warnings(): - #pts = numpy.array([[10., 100000000000000000000000000000.], [1000., 100000000000000000000000000.]]) - #impl_warn = ["keops", "hnsw"] +def test_dtm_overflow_warnings(): + pts = numpy.array([[10., 100000000000000000000000000000.], [1000., 100000000000000000000000000.]]) - #with warnings.catch_warnings(record=True) as w: - #for impl in impl_warn: - #dtm = DistanceToMeasure(2, q=10000, implementation=impl) - #r = dtm.fit_transform(pts) - #assert len(w) == 2 - #for i in range(len(w)): - #assert issubclass(w[i].category, RuntimeWarning) - #assert "Overflow" in str(w[i].message) + with warnings.catch_warnings(record=True) as w: + # TODO Test "keops" implementation as well when next version of pykeops (current is 1.5) is released (should fix the problem (cf. issue #543)) + dtm = DistanceToMeasure(2, q=10000, implementation="hnsw") + r = dtm.fit_transform(pts) + assert len(w) == 1 + assert issubclass(w[0].category, RuntimeWarning) + assert "Overflow" in str(w[0].message) -- cgit v1.2.3 From 6c2c1f60b40e4085519de2316198c8e1e14bf49d Mon Sep 17 00:00:00 2001 From: Hind-M Date: Mon, 15 Nov 2021 16:26:35 +0100 Subject: Remove irrelevant q in test_dtm_overflow_warnings --- src/python/test/test_dtm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/python') diff --git a/src/python/test/test_dtm.py b/src/python/test/test_dtm.py index 09876496..e46d616c 100755 --- a/src/python/test/test_dtm.py +++ b/src/python/test/test_dtm.py @@ -94,7 +94,7 @@ def test_dtm_overflow_warnings(): with warnings.catch_warnings(record=True) as w: # TODO Test "keops" implementation as well when next version of pykeops (current is 1.5) is released (should fix the problem (cf. issue #543)) - dtm = DistanceToMeasure(2, q=10000, implementation="hnsw") + dtm = DistanceToMeasure(2, implementation="hnsw") r = dtm.fit_transform(pts) assert len(w) == 1 assert issubclass(w[0].category, RuntimeWarning) -- cgit v1.2.3 From b1a635c72d3e287c012212a491da07357b0c6136 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Fri, 3 Dec 2021 16:17:53 +0100 Subject: Indent properly the docstring Remove redundant return type --- src/python/gudhi/datasets/generators/_points.cc | 16 ++++++++++++---- src/python/gudhi/datasets/generators/points.py | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) (limited to 'src/python') diff --git a/src/python/gudhi/datasets/generators/_points.cc b/src/python/gudhi/datasets/generators/_points.cc index 6baed673..82fea25b 100644 --- a/src/python/gudhi/datasets/generators/_points.cc +++ b/src/python/gudhi/datasets/generators/_points.cc @@ -85,7 +85,9 @@ PYBIND11_MODULE(_points, m) { m.def("sphere", &generate_points_on_sphere, py::arg("n_samples"), py::arg("ambient_dim"), py::arg("radius") = 1., py::arg("sample") = "random", - R"pbdoc( Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d + R"pbdoc( + Generate random i.i.d. points uniformly on a (d-1)-sphere in R^d + :param n_samples: The number of points to be generated. :type n_samples: integer :param ambient_dim: The ambient dimension d. @@ -94,20 +96,26 @@ PYBIND11_MODULE(_points, m) { :type radius: float :param sample: The sample type. Default and only available value is `"random"`. :type sample: string - :rtype: numpy array of float :returns: the generated points on a sphere. )pbdoc"); m.def("ctorus", &generate_points_on_torus, py::arg("n_samples"), py::arg("dim"), py::arg("sample") = "random", - R"pbdoc( Generate random i.i.d. points on a d-torus in R^2d or as a grid + R"pbdoc( + Generate random i.i.d. points on a d-torus in R^2d or as a grid + :param n_samples: The number of points to be generated. :type n_samples: integer :param dim: The dimension of the torus on which points would be generated in R^2*dim. :type dim: integer :param sample: The sample type. Available values are: `"random"` and `"grid"`. Default value is `"random"`. :type sample: string - :rtype: numpy array of float. The shape of returned numpy array is: If sample is 'random': (n_samples, 2*dim). If sample is 'grid': (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. :returns: the generated points on a torus. + + The shape of returned numpy array is: + + If sample is 'random': (n_samples, 2*dim). + + If sample is 'grid': (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. )pbdoc"); } diff --git a/src/python/gudhi/datasets/generators/points.py b/src/python/gudhi/datasets/generators/points.py index 481f3f71..9bb2799d 100644 --- a/src/python/gudhi/datasets/generators/points.py +++ b/src/python/gudhi/datasets/generators/points.py @@ -19,15 +19,15 @@ def _generate_random_points_on_torus(n_samples, dim): # Based on angles, construct points of size n_samples*dim on a circle and reshape the result in a n_samples*2*dim array array_points = np.column_stack([np.cos(alpha), np.sin(alpha)]).reshape(-1, 2*dim) - + return array_points def _generate_grid_points_on_torus(n_samples, dim): - + # Generate points on a dim-torus as a grid n_samples_grid = int((n_samples+.5)**(1./dim)) # add .5 to avoid rounding down with numerical approximations alpha = np.linspace(0, 2*np.pi, n_samples_grid, endpoint=False) - + array_points = np.column_stack([np.cos(alpha), np.sin(alpha)]) array_points_idx = np.empty([n_samples_grid]*dim + [dim], dtype=int) for i, x in enumerate(np.ix_(*([np.arange(n_samples_grid)]*dim))): @@ -35,13 +35,19 @@ def _generate_grid_points_on_torus(n_samples, dim): return array_points[array_points_idx].reshape(-1, 2*dim) def torus(n_samples, dim, sample='random'): - """ + """ Generate points on a flat dim-torus in R^2dim either randomly or on a grid - + :param n_samples: The number of points to be generated. :param dim: The dimension of the torus on which points would be generated in R^2*dim. :param sample: The sample type of the generated points. Can be 'random' or 'grid'. - :returns: numpy array containing the generated points on a torus. The shape of returned numpy array is: If sample is 'random': (n_samples, 2*dim). If sample is 'grid': (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. + :returns: numpy array containing the generated points on a torus. + + The shape of returned numpy array is: + + If sample is 'random': (n_samples, 2*dim). + + If sample is 'grid': (⌊n_samples**(1./dim)⌋**dim, 2*dim), where shape[0] is rounded down to the closest perfect 'dim'th power. """ if sample == 'random': # Generate points randomly -- cgit v1.2.3 From a2b5e8afab53835ef442451c6763c103f02ac4f5 Mon Sep 17 00:00:00 2001 From: Hind-M Date: Wed, 15 Dec 2021 11:36:05 +0100 Subject: Fix failing tests in debug mode --- .../include/gudhi/Persistent_cohomology/Field_Zp.h | 2 -- src/python/CMakeLists.txt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'src/python') diff --git a/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h b/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h index 8ec89e41..f442b632 100644 --- a/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h +++ b/src/Persistent_cohomology/include/gudhi/Persistent_cohomology/Field_Zp.h @@ -34,8 +34,6 @@ class Field_Zp { } void init(int charac) { - assert(charac > 0); // division by zero + non negative values - Prime = charac; // Check that the provided prime is less than the maximum allowed as int, calculation below, and 'plus_times_equal' function : 46337 ; i.e (max_prime-1)*max_prime <= INT_MAX diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 4a017251..1faaf50d 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -243,7 +243,7 @@ if(PYTHONINTERP_FOUND) if (TBB_FOUND AND WITH_GUDHI_USE_TBB) add_gudhi_debug_info("TBB version ${TBB_INTERFACE_VERSION} found and used") set(GUDHI_PYTHON_EXTRA_COMPILE_ARGS "${GUDHI_PYTHON_EXTRA_COMPILE_ARGS}'-DGUDHI_USE_TBB', ") - if(CMAKE_BUILD_TYPE MATCHES Debug) + if((CMAKE_BUILD_TYPE MATCHES Debug) AND TBB_DEBUG_LIBRARY) add_GUDHI_PYTHON_lib("${TBB_DEBUG_LIBRARY}") add_GUDHI_PYTHON_lib("${TBB_MALLOC_DEBUG_LIBRARY}") else() -- cgit v1.2.3 From c6a7f0258406542b0c2b10bb6b2878f27b13394b Mon Sep 17 00:00:00 2001 From: Vincent Rouvreau <10407034+VincentRouvreau@users.noreply.github.com> Date: Fri, 17 Dec 2021 10:15:18 +0100 Subject: Use vs 2019 for appveyor as well as python 3.9 (#534) * Visual studio 2019 (instead of 2017) * Let vcpkg install cgal manage its dependencies (boost, mpfr and gmp) * vcpkg install tbb also * need to vcpkg integrate install * pip installations are no more in --user mode * Modify all / to \ for windows pths * Compile python module --inplace, and copy required dll inside gudhi directory (add dll path to windows PATH is not enough) * Cmake add_GUDHI_PYTHON_lib_dir was not multi-path proof (happens in windows with release_path;debug_path mechanism) * With CGAL >= 5.3.0, CGAL_HEADER_ONLY CMake variable is no more set as it is the default mode * GUDHI_PYTHON_PATH_ENV Cmake variable requires ; as path separator on windows, and : otherwise --- .appveyor.yml | 52 ++++++++++++++++++++++++++++------------------- src/python/CMakeLists.txt | 40 +++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 35 deletions(-) (limited to 'src/python') diff --git a/.appveyor.yml b/.appveyor.yml index 9ff8f157..d5e35780 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,5 +1,5 @@ image: - - Visual Studio 2017 + - Visual Studio 2019 build: parallel: true @@ -10,11 +10,10 @@ configuration: environment: # update the vcpkg cache even if build fails - APPVEYOR_SAVE_CACHE_ON_ERROR: true - PYTHON: "C:\\Python37-x64" - CMAKE_GMP_FLAGS: -DGMP_INCLUDE_DIR="c:/Tools/vcpkg/installed/x64-windows/include" -DGMP_LIBRARIES="c:/Tools/vcpkg/installed/x64-windows/lib/mpir.lib" - CMAKE_MPFR_FLAGS: -DMPFR_INCLUDE_DIR="c:/Tools/vcpkg/installed/x64-windows/include" -DMPFR_LIBRARIES="c:/Tools/vcpkg/installed/x64-windows/lib/mpfr.lib" - CMAKE_VCPKG_FLAGS: -DCMAKE_TOOLCHAIN_FILE=c:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake + #APPVEYOR_SAVE_CACHE_ON_ERROR: true + PYTHON: "C:\\Python39-x64" + PYTHONPATH: "C:\\Python39-x64\\lib\\site-packages" + CMAKE_VCPKG_FLAGS: -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_TOOLCHAIN_FILE=c:\Tools\vcpkg\scripts\buildsystems\vcpkg.cmake matrix: - target: Examples @@ -30,38 +29,49 @@ environment: CMAKE_FLAGS: -DWITH_GUDHI_EXAMPLE=OFF -DWITH_GUDHI_TEST=OFF -DWITH_GUDHI_UTILITIES=OFF -DWITH_GUDHI_PYTHON=ON -cache: - - c:\Tools\vcpkg\installed - - '%LOCALAPPDATA%\pip\Cache' +#cache: +# - c:\Tools\vcpkg\installed +# - '%LOCALAPPDATA%\pip\Cache' init: - echo %target% -# tbb:x64-windows install: - git submodule update --init - - vcpkg install boost-disjoint-sets:x64-windows boost-serialization:x64-windows boost-date-time:x64-windows boost-system:x64-windows boost-filesystem:x64-windows boost-units:x64-windows boost-thread:x64-windows boost-program-options:x64-windows eigen3:x64-windows mpfr:x64-windows mpir:x64-windows cgal:x64-windows - - SET PATH=c:\Tools\vcpkg\installed\x64-windows\bin;%PATH% - - SET PATH=%PYTHON%;%PYTHON%\Scripts;%PYTHON%\Library\bin;%PATH% - - SET PYTHONPATH=%PYTHON%\\Lib\\site-packages;%PYTHONPATH% - - CALL "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 + - vcpkg install boost-filesystem:x64-windows boost-test:x64-windows boost-program-options:x64-windows tbb:x64-windows eigen3:x64-windows cgal:x64-windows + - dir "C:\Tools\vcpkg\installed\x64-windows\bin\" + - vcpkg integrate install + - CALL "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 + - "set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - python --version - pip --version - - python -m pip install --user --upgrade pip - - python -m pip install --user -r ext/gudhi-deploy/build-requirements.txt + - python -m pip install --upgrade pip + - python -m pip install --upgrade setuptools + - python -m pip install -r ext\gudhi-deploy\build-requirements.txt # No PyKeOps on windows, let's workaround this one. - - for /F "tokens=*" %%A in (ext/gudhi-deploy/test-requirements.txt) do python -m pip install --user %%A + - for /F "tokens=*" %%A in (ext\gudhi-deploy\test-requirements.txt) do python -m pip install %%A + - dir "c:\python39-x64\lib\site-packages" + - dir "%LOCALAPPDATA%\pip\Cache" + - python -c "from scipy import spatial; print(spatial.cKDTree)" build_script: - mkdir build - cd build - - cmake -G "Visual Studio 15 2017 Win64" %CMAKE_FLAGS% %CMAKE_GMP_FLAGS% %CMAKE_MPFR_FLAGS% %CMAKE_VCPKG_FLAGS% .. + - cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=Release %CMAKE_FLAGS% %CMAKE_VCPKG_FLAGS% .. - if [%target%]==[Python] ( - cd src/python & + cd src\python & + dir . & type setup.py & - MSBuild Cython.sln /m /p:Configuration=Release /p:Platform=x64 & + copy "C:\Tools\vcpkg\installed\x64-windows\bin\mpfr-6.dll" ".\gudhi\" & + copy "C:\Tools\vcpkg\installed\x64-windows\bin\gmp.dll" ".\gudhi\" & + copy "C:\Tools\vcpkg\installed\x64-windows\bin\tbb.dll" ".\gudhi\" & + copy "C:\Tools\vcpkg\installed\x64-windows\bin\tbbmalloc.dll" ".\gudhi\" & + python setup.py build_ext --inplace & + SET PYTHONPATH=%CD%;%PYTHONPATH% & + echo %PYTHONPATH% & ctest -j 1 --output-on-failure -C Release ) else ( + dir . & MSBuild GUDHIdev.sln /m /p:Configuration=Release /p:Platform=x64 & ctest -j 1 --output-on-failure -C Release -E diff_files ) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 4a017251..0603ba71 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -14,13 +14,16 @@ function( add_GUDHI_PYTHON_lib THE_LIB ) endif(EXISTS ${THE_LIB}) endfunction( add_GUDHI_PYTHON_lib ) -function( add_GUDHI_PYTHON_lib_dir THE_LIB_DIR ) - # deals when it is not set - error on windows - if(EXISTS ${THE_LIB_DIR}) - set(GUDHI_PYTHON_LIBRARY_DIRS "${GUDHI_PYTHON_LIBRARY_DIRS}'${THE_LIB_DIR}', " PARENT_SCOPE) - else() - message("add_GUDHI_PYTHON_lib_dir - '${THE_LIB_DIR}' does not exist") - endif() +function( add_GUDHI_PYTHON_lib_dir) + # Argument may be a list (specifically on windows with release/debug paths) + foreach(THE_LIB_DIR IN LISTS ARGN) + # deals when it is not set - error on windows + if(EXISTS ${THE_LIB_DIR}) + set(GUDHI_PYTHON_LIBRARY_DIRS "${GUDHI_PYTHON_LIBRARY_DIRS}'${THE_LIB_DIR}', " PARENT_SCOPE) + else() + message("add_GUDHI_PYTHON_lib_dir - '${THE_LIB_DIR}' does not exist") + endif() + endforeach() endfunction( add_GUDHI_PYTHON_lib_dir ) # THE_TEST is the python test file name (without .py extension) containing tests functions @@ -176,6 +179,10 @@ if(PYTHONINTERP_FOUND) endif () if(CGAL_FOUND) + if(NOT CGAL_VERSION VERSION_LESS 5.3.0) + # CGAL_HEADER_ONLY has been dropped for CGAL >= 5.3. Only the header-only version is supported. + set(CGAL_HEADER_ONLY True) + endif(NOT CGAL_VERSION VERSION_LESS 5.3.0) # Add CGAL compilation args if(CGAL_HEADER_ONLY) add_gudhi_debug_info("CGAL header only version ${CGAL_VERSION}") @@ -183,7 +190,7 @@ if(PYTHONINTERP_FOUND) else(CGAL_HEADER_ONLY) add_gudhi_debug_info("CGAL version ${CGAL_VERSION}") add_GUDHI_PYTHON_lib("${CGAL_LIBRARY}") - add_GUDHI_PYTHON_lib_dir("${CGAL_LIBRARIES_DIR}") + add_GUDHI_PYTHON_lib_dir(${CGAL_LIBRARIES_DIR}) message("** Add CGAL ${CGAL_LIBRARIES_DIR}") # If CGAL is not header only, CGAL library may link with boost system, if(CMAKE_BUILD_TYPE MATCHES Debug) @@ -191,7 +198,7 @@ if(PYTHONINTERP_FOUND) else() add_GUDHI_PYTHON_lib("${Boost_SYSTEM_LIBRARY_RELEASE}") endif() - add_GUDHI_PYTHON_lib_dir("${Boost_LIBRARY_DIRS}") + add_GUDHI_PYTHON_lib_dir(${Boost_LIBRARY_DIRS}) message("** Add Boost ${Boost_LIBRARY_DIRS}") endif(CGAL_HEADER_ONLY) # GMP and GMPXX are not required, but if present, CGAL will link with them. @@ -203,13 +210,13 @@ if(PYTHONINTERP_FOUND) get_filename_component(GMP_LIBRARIES_DIR ${GMP_LIBRARIES} PATH) message("GMP_LIBRARIES_DIR from GMP_LIBRARIES set to ${GMP_LIBRARIES_DIR}") endif(NOT GMP_LIBRARIES_DIR) - add_GUDHI_PYTHON_lib_dir("${GMP_LIBRARIES_DIR}") + add_GUDHI_PYTHON_lib_dir(${GMP_LIBRARIES_DIR}) message("** Add gmp ${GMP_LIBRARIES_DIR}") if(GMPXX_FOUND) add_gudhi_debug_info("GMPXX_LIBRARIES = ${GMPXX_LIBRARIES}") set(GUDHI_PYTHON_EXTRA_COMPILE_ARGS "${GUDHI_PYTHON_EXTRA_COMPILE_ARGS}'-DCGAL_USE_GMPXX', ") add_GUDHI_PYTHON_lib("${GMPXX_LIBRARIES}") - add_GUDHI_PYTHON_lib_dir("${GMPXX_LIBRARIES_DIR}") + add_GUDHI_PYTHON_lib_dir(${GMPXX_LIBRARIES_DIR}) message("** Add gmpxx ${GMPXX_LIBRARIES_DIR}") endif(GMPXX_FOUND) endif(GMP_FOUND) @@ -222,7 +229,7 @@ if(PYTHONINTERP_FOUND) get_filename_component(MPFR_LIBRARIES_DIR ${MPFR_LIBRARIES} PATH) message("MPFR_LIBRARIES_DIR from MPFR_LIBRARIES set to ${MPFR_LIBRARIES_DIR}") endif(NOT MPFR_LIBRARIES_DIR) - add_GUDHI_PYTHON_lib_dir("${MPFR_LIBRARIES_DIR}") + add_GUDHI_PYTHON_lib_dir(${MPFR_LIBRARIES_DIR}) message("** Add mpfr ${MPFR_LIBRARIES_DIR}") endif(MPFR_FOUND) endif(CGAL_FOUND) @@ -250,7 +257,7 @@ if(PYTHONINTERP_FOUND) add_GUDHI_PYTHON_lib("${TBB_RELEASE_LIBRARY}") add_GUDHI_PYTHON_lib("${TBB_MALLOC_RELEASE_LIBRARY}") endif() - add_GUDHI_PYTHON_lib_dir("${TBB_LIBRARY_DIRS}") + add_GUDHI_PYTHON_lib_dir(${TBB_LIBRARY_DIRS}) message("** Add tbb ${TBB_LIBRARY_DIRS}") set(GUDHI_PYTHON_INCLUDE_DIRS "${GUDHI_PYTHON_INCLUDE_DIRS}'${TBB_INCLUDE_DIRS}', ") endif() @@ -290,7 +297,12 @@ if(PYTHONINTERP_FOUND) add_custom_target(python ALL DEPENDS gudhi.so COMMENT "Do not forget to add ${CMAKE_CURRENT_BINARY_DIR}/ to your PYTHONPATH before using examples or tests") - set(GUDHI_PYTHON_PATH_ENV "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}:$ENV{PYTHONPATH}") + # Path separator management for windows + if (WIN32) + set(GUDHI_PYTHON_PATH_ENV "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR};$ENV{PYTHONPATH}") + else(WIN32) + set(GUDHI_PYTHON_PATH_ENV "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}:$ENV{PYTHONPATH}") + endif(WIN32) # Documentation generation is available through sphinx - requires all modules # Make it first as sphinx test is by far the longest test which is nice when testing in parallel if(SPHINX_PATH) -- cgit v1.2.3